<?php
/*
 * Copyright (C) 2017 Karmabunny Pty Ltd.
 *
 * This file is a part of SproutCMS.
 *
 * SproutCMS is free software: you can redistribute it and/or modify it under the terms
 * of the GNU General Public License as published by the Free Software Foundation, either
 * version 2 of the License, or (at your option) any later version.
 *
 * For more information, visit <http://getsproutcms.com>.
 */
 
namespace Sprout\Helpers;
 
use Exception;
use InvalidArgumentException;
 
use Kohana;
 
use karmabunny\pdb\Exceptions\RowMissingException;
 
 
/**
 * Methods for working with files, including images
 */
{
    /**
     * Gets the details of a file using its id.
     *
     * Uses a prepared statement for speed when doing repeated queries.
     *
     * N.B. If the file entry doesn't exist, a reference to 'missing.png' is returned
     *
     * @param int $id The ID in the files table
     * @return array Contains keys 'filename' and 'date_file_modified'
     */
    public static function getDetails($id)
    {
        static $prepared_q = null;
 
        if (!$prepared_q) {
            $q = "SELECT filename, date_file_modified FROM ~files WHERE id = ?";
            $prepared_q = Pdb::prepare($q);
        }
 
        try {
            return Pdb::execute($prepared_q, [$id], 'row');
        } catch (RowMissingException $ex) {
            return ['filename' => 'missing.png', 'date_file_modified' => '1970-01-01 00:00:00'];
        }
    }
 
 
    /**
     * For a given file, returns the filename to use to retrieve a resized version of the file
     *
     * @param string $original Original image name, e.g. 123_example.jpg
     * @param string $size_name One of the 'file.image_transformations' config options, e.g. 'small'
     * @param string $force_ext If set, extension is set to this
     * @return string Name of resized file, e.g. 123_example.small.jpg
     */
    static function getResizeFilename($original, $size_name, $force_ext = null)
    {
        $file_noext = implode('.', $parts);  
        if ($force_ext) {
            $ext = $force_ext;
        }
 
        return "{$file_noext}.{$size_name}.{$ext}";
    }
 
 
    /**
     * Gets the (final) extension from a file name, in lowercase
     * @param string $filename Full filename, e.g. 'image.large.jpg', '/path/to/image.png'
     * @return string Extension, excluding leading dot, e.g. 'jpg', 'png'
     */
    static function getExt($filename)
    {
    }
 
 
    /**
     * Determines the file type from a file name by examining its extension
     * @param string $filename The file name
     * @return int One of the FileConstants::TYPE_* values, see {@see FileConstants}.
     *         If the type couldn't be determined, FileConstants::TYPE_OTHER is returned.
     */
    {
        $ext = self::getExt($filename);
        foreach (FileConstants::$type_exts as $type => $exts) {
            if (in_array($ext, $exts)) return $type;         }
        return FileConstants::TYPE_OTHER;
    }
 
 
    /**
    * For a given file, returns the name without an ext
    *
    * @param string Full filename
    * @return string Base part of filename
    **/
    static function getNoext($original)
    {
    }
 
 
    /**
    * Converts a file size, in bytes, into a human readable form (with trailing kb, mb, etc)
    *
    * @param int $size Size in bytes
    * @return string
    **/
    static function humanSize($size)
    {
        static 
$types = array(' bytes', ' kb', ' mb', ' gb', ' tb'); 
        $type = 0;
        while ($size > 1024) {
            $size /= 1024;
            $type++;
            if ($type > 5) break;
        }
 
        return round($size, 1) . $types[$type];     }
 
 
    /**
    * Make a filename sane - strip lots of characters which create problems
    *
    * @param string $filename Filename which may or may not already be sane
    * @return string Sane filename
    **/
    static function filenameMakeSane($filename)
    {
 
        $ext = '';
 
        $filename = trim($filename, '_');  
        if ($filename == '') $filename = time();  
 
        return $filename;
    }
 
 
    /**
    * Return the backend library to use for many file operations
    *
    * @return FilesBackend
    **/
    private static function backend()
    {
        static $backend;
 
        if ($backend === null) {
            $backend = new FilesBackendDirectory();
        }
 
        return $backend;
    }
 
 
    /**
    * Returns the public URL for a given file. Does not include domain.
    *
    * @param string $filename The name of the file in the repository
    * @return string
    **/
    public static function url($filename)
    {
        return self::backend()->absUrl($filename);
    }
 
 
    /**
    * Returns the relative public URL for a given file.
    * Doesn't contain ROOT/ or domain. Use for content areas.
    *
    * @param string $filename The name of the file in the repository
    * @return string
    **/
    public static function relUrl($filename)
    {
        return self::backend()->relUrl($filename);
    }
 
 
    /**
    * Returns the public URL for a given file, including domain.
    *
    * @param string $filename The name of the file in the repository
    * @return string
    **/
    public static function absUrl($filename)
    {
        return self::backend()->absUrl($filename);
    }
 
 
    /**
     * Returns the relative URL for a dynamically resized image.
     *
     * Size formatting is as per {@see File::parseSizeString}, e.g. c400x300
     *
     * @param int $id ID or filename from record in files table
     * @param string $size A code as per {@see File::parseSizeString}
     * @return string HTML-safe relative URL, e.g. file/resize/c400x300/123_example.jpg
     */
    public static function resizeUrl($id, $size)
    {
        return self::backend()->resizeUrl($id, $size);
    }
 
 
    /**
     * Gets the relative URL for a fixed or dynamically resized image
     *
     * @param int $id ID or filename from record in files table
     * @param string $size_name The size you want, e.g. 'small', 'banner', 'c100x100', etc.
     *        The value can either be a size name from the 'file.image_transformations' config option,
     *        or be a resize code as per {@see File::parseSizeString}
     * @param string $force_ext Force the ext to a specific value, e.g. 'jpg'
     * @param bool $create_if_missing For numeric size names (e.g. 'c100x100'), causes a resize for any missing files
     * @return string File URL, e.g. 'file/download/123/small' or 'files/123_test.c100x100.jpg'
     */
    public static function sizeUrl($id, $size_name, $force_ext = null, $create_if_missing = false)
    {
                return "file/download/{$id}/{$size_name}";
            }
            $file_details = File::getDetails($id);             $filename = $file_details['filename'];
        } else {
            $filename = $id;
            if (!self::exists($filename)) {
                try {
                    $filename = File::lookupReplacementName($filename);                 } catch (Exception $ex) {
                    return 'files/missing.png';
                }
            }
        }
 
        $url = File::getResizeFilename($filename, $size_name, $force_ext);  
        $pattern = '/^[crm][0-9]+x[0-9]+(?:-[lcr][tcb](?:~[0-9]+)?)?$/';
        if ($create_if_missing and 
preg_match($pattern, $size_name) and 
!File::exists($url)) {             File::createSize($filename, $size_name, $force_ext);         }
 
        return File::relUrl($url);     }
 
 
    /**
    * Returns TRUE if the file exists, and FALSE if it does not
    *
    * @param string $filename The name of the file in the repository. Deprecated: an ID value is also accepted in order
    *        to support older modules; such modules need to be updated to avoid an extra database lookup.
    * @return bool TRUE if the file exists, and FALSE if it does not
    **/
    public static function exists($filename)
    {
            $id = $filename;
            try {
                $filename = Pdb::q('SELECT filename FROM ~files WHERE id = ?', [$id], 'val');
            } catch (RowMissingException $ex) {
                return false;
            }
        }
        return self::backend()->exists($filename);
    }
 
 
    /**
    * Returns the size, in bytes, of the specified file
    *
    * @param string $filename The name of the file in the repository
    * @return int File size in bytes
    **/
    public static function size($filename)
    {
        if (!self::exists($filename)) {
            try {
                $filename = File::lookupReplacementName($filename);             } catch (RowMissingException $ex) {
                // No problem, return original (broken) URL
            }
        }
 
        return self::backend()->size($filename);
    }
 
 
    /**
    * Returns the modified time, in unix timestamp format, of the specified file
    *
    * @param string $filename The name of the file in the repository
    * @return int Modified time as a unix timestamp
    **/
    public static function mtime($filename)
    {
        return self::backend()->mtime($filename);
    }
 
 
    /**
    * Sets access and modification time of file
    * @return bool True if successful
    **/
    public static 
function touch($filename)     {
        return self::backend()->touch($filename);     }
 
 
    /**
    * Returns the size of an image, or false on failure.
    *
    * Output format is the same as getimagesize, but will be at a minimum:
    *   [0] => width, [1] => height, [2] => type
    *
    * @param string $filename The name of the file in the repository
    * @return array
    **/
    public static function imageSize($filename)
    {
        return self::backend()->imageSize($filename);
    }
 
 
    /**
     * Delete a file
     * If the file is an image, any resized variants (e.g. 'small', 'medium' etc.) are deleted too
     * @param string $filename The name of the file in the repository, e.g. '123_some_image.jpg'
     * @return bool True if the deletion of the main file succeeded
     */
    public static function delete($filename)
    {
        File::deleteCache($filename);         $ext = File::getExt($filename);         $base = File::getNoExt($filename);         $transforms = Kohana::config('file.image_transformations');
        foreach ($transforms as $type => $params) {
            self::backend()->delete("{$base}.{$type}.{$ext}");
        }
        return self::backend()->delete($filename);
    }
 
 
    /**
    * Delete cached versions of a file
    *
    * @param string $filename The name of the file in the repository
    **/
    public static function deleteCache($filename)
    {
 
        $files = glob(APPPATH 
. 'cache/resize-*-' . $filename);         foreach ($files as $f) {
        }
    }
 
 
    /**
    * Returns all files which match the specified mask.
    * I have a feeling this returns other sizes (e.g. .small) as well - which may not be ideal.
    *
    * @param string $mask Files to find. Supports wildcards such as * and ?
    **/
    public static 
function glob($mask)     {
        return self::backend()->glob($mask);     }
 
 
    /**
    * This is the equivalent of the php readfile function
    *
    * @param string $filename The name of the file in the repository
    **/
    public static 
function readfile($filename)     {
        return self::backend()->readfile($filename);     }
 
 
    /**
    * Returns file content as a string. Basically the same as file_get_contents
    *
    * @param string $filename The name of the file in the repository
    * @return string $content The content
    **/
    public static function getString($filename)
    {
        return self::backend()->getString($filename);
    }
 
 
    /**
    * Saves file content as a string. Basically the same as file_put_contents
    *
    * @param string $filename The name of the file in the repository
    * @param string $content The content
    * @return bool True on success, false on failure
    **/
    public static function putString($filename, $content)
    {
        return self::backend()->putString($filename, $content);
    }
 
 
    /**
    * Saves file content from a stream. Basically just fopen/stream_copy_to_stream/fclose
    *
    * @param string $filename The name of the file in the repository
    * @param resource $stream The stream to copy content from
    * @return bool True on success, false on failure
    **/
    public static function putStream($filename, $stream)
    {
        return self::backend()->putStream($filename, $stream);
    }
 
 
    /**
    * Saves file content from an existing file
    *
    * @param string $filename The name of the file in the repository
    * @param string $existing The existing file on disk
    * @return bool True on success, false on failure
    **/
    public static function putExisting($filename, $existing)
    {
        return self::backend()->putExisting($filename, $existing);
    }
 
 
    /**
    * Moves an uploaded file into the repository.
    * Returns TRUE on success, FALSE on failure.
    *
    * @param string $src Source filename
    * @param string $filename The name of the file in the repository
    * @return bool True on success, false on failure
    **/
    public static function moveUpload($src, $filename)
    {
        return self::backend()->moveUpload($src, $filename);
    }
 
 
    /**
    * Create a copy of the file in a temporary directory.
    * Don't forget to File::destroy_local_copy($temp_filename) when you're done!
    *
    * @param string $filename The file to copy into a temporary location
    * @return string Temp filename or NULL on error
    **/
    public static function createLocalCopy($filename)
    {
        return self::backend()->createLocalCopy($filename);
    }
 
 
    /**
    * Remove a local copy of a file
    * Call this once you're done with the local copy
    *
    * @param string $temp_filename The filename returned by createLocalCopy
    **/
    public static function cleanupLocalCopy($temp_filename)
    {
        return self::backend()->cleanupLocalCopy($temp_filename);
    }
 
 
    /**
    * Searches the whole database to find all records in all columns
    * which contain a given filename.
    *
    * The search looks in VARCHAR columns with more than 200 chars (exact match)
    * and in TEXT columns (contains match)
    *
    * Return value is an array of matches, in the format:
    *   [0] => table
    *   [1] => record id
    *   [2] => record name, if available
    *
    * @param string $filename The name of the file to search
    **/
    public static function findUsage($filename)
    {
        $pf = Pdb::prefix();
 
        $all_params = [
            'filename' => $filename,
            'like_filename' => Pdb::likeEscape($filename),
        ];
 
 
        $size_names = Kohana::config('file.image_transformations');
        foreach ($size_names as $size_name => $transform) {
            Pdb::validateIdentifier($size_name);
            $sizes[] = $size_name;
            $all_params["resize_{$size_name}"] = Pdb
::likeEscape(File::getResizeFilename($filename, $size_name));         }
 
        // Tables to not show results for
            $pf . 'files',
            $pf . 'history_items',
            $pf . 'cronjobs',
            $pf . 'workerjobs',
            $pf . 'pages',
            $pf . 'page_revisions',
            $pf . 'page_widgets',
            $pf . 'exception_log',
        );
 
        // Iterate the tables
        $q = "SHOW TABLE STATUS";
        $db_tables = Pdb::q($q, [], 'arr');
 
        foreach ($db_tables as $tbl) {
            if (strpos($tbl['Name'], $pf) !== 0) continue;             if (in_array($tbl['Name'], $badtables)) continue;  
            // Grab the columns
            $q = "SHOW COLUMNS FROM {$tbl['Name']}";
            $db_cols = Pdb::q($q, [], 'arr');
 
            // Build a where clause
            $cols = [];
            $where = [];
            $params = [];
            foreach ($db_cols as $col) {
                if ($col['Field'] === 'id') $cols[] = 'id';
                if ($col['Field'] === 'name') $cols[] = 'name';
 
                if (preg_match('/VARCHAR\(([0-9]+)\)/i', $col['Type'], $matches) and 
$matches[1] >= 200) {                     $where[] = "{$col['Field']} = :filename";
                    if (!isset($params['filename'])) $params['filename'] = $all_params['filename'];  
                    $where[] = "{$col['Field']} LIKE CONCAT('%', :like_filename, '%')";
                    if (!isset($params['like_filename'])) $params['like_filename'] = $all_params['like_filename'];  
                    foreach ($sizes as $size_name) {
                        $param_name = "resize_{$size_name}";
                        $where[] = "{$col['Field']} LIKE CONCAT('%', :{$param_name}, '%')";
                        if (!isset($params[$param_name])) $params[$param_name] = $all_params[$param_name];                     }
                }
            }
 
            if (count($cols) == 0 or 
count($where) == 0) continue;  
            $q = 'SELECT ' . implode(', ', $cols) . ' FROM ' . $tbl['Name'] . ' WHERE ' . implode(' OR ', $where);             $queries[$tbl['Name']] = [$q, $params];
        }
 
        // Spekky query for page revisions
        $where = [];
        $params = $all_params;
        unset($params['filename']);  
        $where[] = "widget.settings LIKE CONCAT('%', :like_filename, '%')";
        foreach ($sizes as $size_name) {
            $param_name = "resize_{$size_name}";
            $where[] = "widget.settings LIKE CONCAT('%', :{$param_name}, '%')";
        }
        $where[] = "page.banner LIKE CONCAT('%', :like_filename, '%')";
        $q = "SELECT DISTINCT page.id, page.name
            FROM ~page_revisions AS rev
            INNER JOIN ~page_widgets AS widget ON rev.id = widget.page_revision_id
                AND widget.area_id = 1 AND widget.type = 'RichText'
            INNER JOIN ~pages AS page ON rev.page_id = page.id
            WHERE (" . implode(' OR ', $where) . ')                 AND rev.status = :live';
        $params['live'] = 'live';
        $queries['sprout_pages'] = [$q, $params];
 
        // Spekky query for gallery images
        if (Sprout::moduleInstalled('galleries2')) {
            $where = [];
            $params = $all_params;
            unset($params['filename']);             $where[] = 'f.filename LIKE :like_filename';
            foreach ($sizes as $size_name) {
                $param_name = "resize_{$size_name}";
                $where[] = "f.filename LIKE :{$param_name}";
            }
 
            $q = "SELECT g.id, g.name
                FROM ~galleries AS g
                INNER JOIN ~gallery_sources AS src ON src.gallery_id = g.id AND src.type = :type_image
                INNER JOIN ~files_cat_join AS joiner ON joiner.cat_id = src.category
                INNER JOIN ~files AS f ON joiner.file_id = f.id
                WHERE (" . implode(' OR ', $where) . ')';             $params['type_image'] = GalleryConstants::SOURCE_FILES_IMAGE;
            $queries['sprout_galleries'] = [$q, $params];
        }
 
        // Run the queries
        foreach ($queries as $table => $q_and_params) {
            list($q, $params) = $q_and_params;             $res = Pdb::q($q, $params, 'pdo');
 
            // Save results
            foreach ($res as $row) {
                    $row['id'],
                    (isset($row['name']) ? 
$row['name'] : 'Record #' . $row['id']),                 );
            }
            $res->closeCursor();
        }
 
        return $output;
    }
 
 
    /**
    * Return the mimetype for a given filename.
    *
    * Only uses the extension - doesn't actually check the file
    * If you need deep checking, take a look at {@see File::checkFileContentsExtension}
    * If the extension is unrecognised, returns 'application/octet-stream'.
    *
    * @param string $filename
    * @return string E.g. 'image/png'
    **/
    public static function mimetype($filename)
    {
        $ext = File::getExt($filename);         return (isset(Constants
::$mimetypes[$ext]) ? Constants
::$mimetypes[$ext] : 'application/octet-stream');     }
 
 
    /**
     * Checks file contents match the extension
     *
     * @param $filename string The full path/filename of the file to check
     * @param $ext string The supplied file extension
     * @return boolean True if the file matches, false if it doesn't
     * @return null If there isn't a check for the supplied extension
     */
    public static function checkFileContentsExtension($filename, $ext)
    {
 
        switch ($ext) {
            case 'jpg':
            case 'jpeg':
            case 'jpe':
            case 'jif':
            case 'jfif':
            case 'jfi':
                return ($size[2] == IMAGETYPE_JPEG);
 
            case 'png':
                return ($size[2] == IMAGETYPE_PNG);
 
            case 'gif':
                return ($size[2] == IMAGETYPE_GIF);
 
            case 'pdf':
                $fp = fopen($filename, 'r');                 return ($magic == '%PDF');
        }
 
        return null;
    }
 
 
    /**
     * Get the content-type of a file using magic mime.
     *
     * This is _NOT_ limited to the whitelist of mime types described in the
     * Constants. Use this with care.
     *
     * Note mime_content_type() inspects file contents and can't always
     * determine css/js files correctly, this is a hack fix for that.
     *
     * https://stackoverflow.com/a/17736797/7694753
     *
     * @param string $path
     * @return string|null null if unknown
     */
    public static function mimetypeExtended($path)
    {
        $extension = pathinfo($path, PATHINFO_EXTENSION
);  
        switch($extension) {
            case 'css':
                return 'text/css; charset=UTF-8';
 
            case 'js':
                return 'application/javascript; charset=UTF-8';
 
            case 'svg':
                 return 'image/svg+xml; charset=UTF-8';
        }
 
        if (!$info) return null;
 
    }
 
 
    /**
    * Prompts a user to download a file, and terminates the script
    * Sets all the right headers and stuff, doesn't set caching/expires/etc headers though.
    *
    * @param string $filename The name of the file in the repository
    * @param string $download_name An optional alternate name to provide to the user
    **/
    public static function download($filename, $download_name = null)
    {
        $size = File::size($filename);         $mime = File::mimetype($filename);  
        // Set some general headers
        header('Content-type: ' . $mime);         header('Content-length: ' . $size);         header('Content-disposition: attachment; filename="' . addslashes($download_name ? 
$download_name : $filename) . '"');  
        // MSIE needs "public" when under SSL - http://support.microsoft.com/kb/316431
        header('Cache-Control: public, max-age=1');  
        Kohana::closeBuffers();
    }
 
 
    /**
    * Parse the size string used in file/resize and some helpers.
    *
    * Syntax: [crm]{number}x{number}(-[lcr][tcb])(~{number})
    *         Type  Width    Height    Crop X Y     Quality
    *
    * Returns an array.
    *   [0] type, either 'r', 'c' or 'm'
    *       r = resize, up or down, try to fill the area requested
    *       c = crop, resulting file will always be the width and height requested
    *       m = resize down only
    *   [1] width
    *   [2] height
    *   [3] x position, 'left', 'center' or 'right'
    *   [4] y position, 'top', 'center' or 'bottom'
    *   [5] jpeg quality, 0 = worst, 100 = best
    *
    * Returns an empty array on error (so you can use list() safely)
    *
    * @param $str Size string
    * @return array
    **/
    public static function parseSizeString($str)
    {
        $result = preg_match('/^([crm])([0-9]+)x([0-9]+)(?:-([lcr])([tcb]))?(?:~([0-9]+))?$/', $str, $matches);         if (! $result) return array();  
        $matches[1] = (int) $matches[1];
        $matches[2] = (int) $matches[2];
 
        if (empty($matches[3])) {             $matches[3] = 'center';
            $matches[4] = 'center';
        } else {
            if ($matches[3] == 'l') $matches[3] = 'left';
            if ($matches[3] == 'c') $matches[3] = 'center';
            if ($matches[3] == 'r') $matches[3] = 'right';
            if ($matches[4] == 't') $matches[4] = 'top';
            if ($matches[4] == 'c') $matches[4] = 'center';
            if ($matches[4] == 'b') $matches[4] = 'bottom';
        }
 
        if (empty($matches[5])) $matches[5] = null;  
        return $matches;
    }
 
 
    /**
    * Create a resized version of the specified file at a given size.
    *
    * The size is specified the same as the file/resize method (rXXXxYYY or cXXXxYYY)
    * The output filename will be basename.size.ext
    *
    * The files can be used with `size_url` or `get_resize_filename` on the front-end.
    *
    * @param string $filename The original filename
    * @param string $size The size in on-the-fly resize format
    * @param string $force_ext Force a different ext on save, such as jpg for banners
    * @return bool True on success, false on failure
    **/
    public static function createSize($filename, $size, $force_ext = null)
    {
        if (! File::exists($filename)) {             return false;
        }
 
        $temp_filename = File::createLocalCopy($filename);         if (! $temp_filename) {
            return false;
        }
 
        $img = new Image($temp_filename);
 
        // Determine the size
        list($type, $width, $height, $crop_x, $crop_y, $quality) = File::parseSizeString($size);  
        if ($type == 'm') {
            // Max size
            $file_size = File::imageSize($filename);  
            if ($width == 0) $width = PHP_INT_MAX;
            if ($height == 0) $height = PHP_INT_MAX;
 
            if ($file_size[0] > $width or $file_size[1] > $height) {
                $img->resize($width, $height);
            }
 
        } else if ($type == 'r') {
            // Resize
            $img->resize($width, $height);
 
        } else if ($type == 'c') {
            // Crop
            if ($width / $img->width > $height / $img->height) {
                $master = Image::WIDTH;
            } else {
                $master = Image::HEIGHT;
            }
 
            $img->resize($width, $height, $master);
            $img->crop($width, $height, $crop_y, $crop_x);
 
        } else {
            // What?
            File::cleanupLocalCopy($temp_filename);             throw new Exception('Incorrect resize type');
        }
 
        if ($quality) {
            $img->quality($quality);
        }
 
        $sized_filename = self::getResizeFilename($filename, $size, $force_ext);
 
        $result = $img->save();
        if (! $result) return false;
 
        $result = File::putExisting($sized_filename, $temp_filename);         if (! $result) return false;
 
        File::cleanupLocalCopy($temp_filename);  
        $result = Replication::postFileUpdate($sized_filename);
        if (! $result) return false;
 
        return true;
    }
 
 
    /**
     * Will we have enough RAM to do the resizes?
     *
     * @throws Exception If we don't
     * @param array $dimensions Dimensions of the original image; 0 = width, 1 => height
     * @return void
     */
    public static 
function calculateResizeRam
(array $dimensions)     {
        $origin_ram = $dimensions[0] * $dimensions[1] * 4;
        $memory_limit = Sprout::getMemoryLimit();
 
        $sizes = Kohana::config('file.image_transformations');
        foreach ($sizes as $size_name => $transform) {
            $size_ram = 0;
            foreach ($transform as $t) {
                $size_ram += $t->estimateRamRequirement();
            }
            $total_ram_req = $origin_ram + $size_ram;
 
            if ($total_ram_req > $memory_limit) {
                $total_ram_req = str_replace(' ', ' ', File::humanSize($total_ram_req));                 $memory_limit = str_replace(' ', ' ', File::humanSize($memory_limit));  
                throw new Exception(
                    "Unable to create size '{$size_name}'; expected RAM requirements "
                    . "of {$total_ram_req} exceeds memory limit of {$memory_limit}"
                );
            }
        }
    }
 
 
    /**
     * Create default image sizes as per the config parameter 'file.image_transformations'
     *
     * The transformed files get saved onto the server.
     * If any of the transformations in a transform-group fails,
     * the whole group will fail and the file will not be saved.
     *
     * @throw Exception
     * @param string $filename The file to create sizes for
     * @param string $specific_size Optional parameter to process only a single size
     * @return void
     */
    public static function createDefaultSizes($filename, $specific_size = null)
    {
        $file_noext = implode('.', $parts);  
        $sizes = Kohana::config('file.image_transformations');
 
        if ($specific_size) {
            if (!isset($sizes[$specific_size])) {                 throw new Exception('Invalid param $specific_size; size doesn\'t exist.');
            }
 
            $sizes = array($specific_size => $sizes[$specific_size]);         }
 
        // Look up image in DB and see if it needs author attribution
        $q = "SELECT author, embed_author
            FROM ~files
            WHERE filename = ?";
        $row = Pdb::q($q, [$filename], 'row');
        if ($row['author'] and $row['embed_author']) {
            $embed_text = $row['author'];
        } else {
            $embed_text = false;
        }
 
        foreach ($sizes as $size_name => $transform) {
            $temp_filename = File::createLocalCopy($filename);             if (!$temp_filename) {
                throw new Exception('Unable to create temporary copy for processing');
            }
 
            $img = new Image($temp_filename);
 
            $resize_filename = "{$file_noext}.{$size_name}.{$ext}";
 
            // Do the transforms
            $width = $height = 0;
            foreach ($transform as $t) {
                $res = $t->transform($img);
 
                if ($t instanceof ResizeImageTransform) {
                    $dims = $t->getDimensions();
                    $width = $dims['width'];
                    $height = $dims['height'];
                }
 
                // If an individual transform fails,
                // cancel the transforming for this group
                // The other transform groups will still be processed though
                if ($res == false) {
                    File::cleanupLocalCopy($temp_filename);                     continue 2;
                }
            }
 
            if ($embed_text) $img->addText($embed_text);
 
            $result = $img->save();
            if (! $result) {
                throw new Exception('Save of new image failed');
            }
 
            // Import temp file into media repo
            $result = File::putExisting($resize_filename, $temp_filename);             if (! $result) {
                throw new Exception('Image copy of new file into repository failed');
            }
 
            File::cleanupLocalCopy($temp_filename);         }
    }
 
 
    /**
    * Do post-processing after a file upload
    *
    * @throw Exception
    * @param string $filename The name of hte new file
    * @param int $file_id The ID of the new file
    * @param int $file_type The new file type - e.g. DOCUMENT or IMAGE; see FileConstants
    **/
    public static function postUploadProcessing($filename, $file_id, $file_type)
    {
        $file_id = (int) $file_id;
        $file_type = (int) $file_type;
 
        $update_data = ['filename' => $filename];
        Pdb::update('files', $update_data, ['id' => $file_id]);
 
        switch ($file_type) {
            case FileConstants::TYPE_DOCUMENT:
                $ext = FileIndexing::getExt($filename);
                if (FileIndexing::isExtSupported($ext)) {
                    $update_data = [];
                    $update_data['plaintext'] = FileIndexing::getPlaintext($filename, $ext);
                    Pdb::update('files', $update_data, ['id' => $file_id]);
                }
                break;
 
            case FileConstants::TYPE_IMAGE:
                File::createDefaultSizes($filename);                 break;
        }
    }
 
 
    /**
     * Generates a cropped, base-64 encoded thumbnail of an image
     * @param string $file_path Path to the original image
     * @param int $width Width to use for thumbnail
     * @param int $height Height to use for thumbnail
     * @return array Has the following keys:
     *         'encoded_thumbnail': Base-64 encoded thumbnail
     *         'original_width': width of the original image
     *         'original_height': height of the original image
     * @return false If file doesn't exist or can't be recognised as an image
     * @throws Exception if not enough RAM to generate thumbnail
     */
    public static function base64Thumb($file_path, $width, $height)
    {
        if (!$size) return false;
 
        $current_size = new ResizeImageTransform($w, $h);
        $resize = new ResizeImageTransform($width, $height);
        $resize_ram = $current_size->estimateRamRequirement();
        $resize_ram += $resize->estimateRamRequirement();
        if ($resize_ram > Sprout::getMemoryLimit()) {
            throw new Exception('Not enough RAM to generate thumbnail');
        }
        return [
            'encoded_thumbnail' => Image::base64($file_path, $resize),
            'original_width' => $w,
            'original_height' => $h,
        ];
    }
 
 
    /**
     * Checks the database for an updated URL path for a file.
     * I.e. a file which has been replaced by the admin 'replace file' tool.
     * @param string $filename Name of file, with no path, e.g. 123_image.jpg
     * @return string Updated URL (relative to root), e.g. 'files/123_new_image.png' or 'file/download/123'
     * @throws RowMissingException If no updated path was found
     * @throws InvalidArgumentException If linkspec in DB invalid
     */
    public static function lookupReplacementUrl($filename)
    {
        $q = "SELECT destination
            FROM ~redirects
            WHERE path_exact = ?";
        $dest_spec_json = Pdb::q($q, ['files/' . $filename], 'val');
        if ($dest_spec['class'] != '\\Sprout\\Helpers\\LinkSpecInternal') {
            throw new InvalidArgumentException("Link spec doesn't match expected value");
        }
        return $dest_spec['data'];
    }
 
 
    /**
     * Checks the database for an updated name for a file.
     *
     * This only works for full-sized images, e.g. 123_example.jpg, not 123_example.small.jpg
     *
     * @param string $filename Name of file, with no path, e.g. 123_image.jpg
     * @return string Updated filename, e.g. '123_new_image.png'
     * @throws RowMissingException If no updated path was found
     * @throws InvalidArgumentException If linkspec in DB invalid
     */
    public static function lookupReplacementName($filename)
    {
        $replacement = self::lookupReplacementUrl($filename);
 
        if (preg_match('#^file/download/([0-9]+)#', $replacement)) {             $file_details = self::getDetails($id);
            return $file_details['filename'];
        } else {
            throw new InvalidArgumentException("Redirect target doesn't match expected value");
        }
    }
 
 
    /**
     * Replaces a set of files to be stored in a single field; this acts as a backend for {@see Fb::chunkedUpload}
     *
     * Files are saved as '{prefix}{num}.{ext}', with 'num' starting at 1
     *
     * @param string $session_key Session key used for this file field, used for {@see FileUpload::verify}
     * @param string $field_name Name of the field used on the form, e.g. 'photos'
     * @param array $exts Allowed file extensions, e.g. ['jpg', 'jpeg', 'png', 'gif']
     * @param string $prefix The start of the new file name, e.g. 'user-gallery-123-'
     * @return array List of newly saved filenames
     */
    public static function replaceSet($session_key, $field_name, $exts, $prefix)
    {
        $files_saved = [];
        $i = 0;
        while (isset($_POST[$field_name][$i])) {             try {
                $src_file = FileUpload::verify($session_key, $field_name, $i, $exts);
                    $ext = File::getExt($_POST[$field_name][$i]);                     $dest = $prefix . ($i + 1) . '.' . $ext;
                    $res = File::moveUpload($src_file, $dest);  
                        $files_saved[] = $dest;
                    }
                }
            } catch (Exception $ex) {
            }
 
            ++$i;
        }
 
        foreach ($old_files as $file) {
            }
        }
 
        return $files_saved;
    }
 
}