Kylob/BootPress

View on GitHub
src/Admin/Files.php

Summary

Maintainability
F
1 wk
Test Coverage
<?php

namespace BootPress\Admin;

use BootPress\Blog\Twig\Parser;
use BootPress\Page\Component as Page;
use BootPress\Admin\Component as Admin;
use BootPress\Asset\Component as Asset;
use BootPress\Unzip\Component as Unzip;
use BootPress\Upload\Component as Upload;
use ZipStream; // maennchen/zipstream-php
use Intervention\Image\ImageManager;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Exception\ParseException;

class Files
{
    /**
     * Iterates over the $path and returns an array of all the directories and files contained within it.
     *
     * @param string      $path      Where to begin.  If it has a trailing slash, then all dirs and files will not have a leading slash.  If it does not have a trailing slash, the all dirs and files will have a leading slash
     * @param false|mixed $recursive If anything but false then parent directories and files will also be returned
     * @param string      $types     A pipe delimited list of acceptable file extensions eg. php|txt|yml
     *
     * @return array Dirs first, and files second.
     *
     * ```php
     * $path = $blog->folder.'content/index';
     * list($dirs, $files) = Files::iterate($path);
     * foreach ($files as $file) unlink($path.$file);
     * if (empty($dirs)) rmdir($path);
     * ```
     */
    public static function iterate($path, $recursive = false, $types = null)
    {
        $dirs = $files = array();
        if (is_dir($path)) {
            $cut = strlen($path);
            $regex = ($types) ? '/^.+\.('.$types.')$/i' : false;
            $dir = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
            if ($recursive) {
                $dir = new \RecursiveIteratorIterator($dir, \RecursiveIteratorIterator::SELF_FIRST, \RecursiveIteratorIterator::CATCH_GET_CHILD);
                if (is_int($recursive)) {
                    $dir->setMaxDepth($recursive);
                }
            }
            foreach ($dir as $file) {
                $path = str_replace('\\', '/', substr($file->getRealpath(), $cut));
                if ($file->isDir()) {
                    if (iterator_count($dir->getChildren()) === 0) {
                        rmdir($file->getRealpath()); // might as well do some garbage collection while we are at it
                    } else {
                        $dirs[] = $path;
                    }
                } elseif ($types !== false) {
                    if ($regex) {
                        if (preg_match($regex, $file->getFilename())) {
                            $files[] = $path;
                        }
                    } else {
                        $files[] = $path;
                    }
                }
            }
        }

        return array($dirs, $files);
    }

    /**
     * Makes a folder's $path pretty with no screwed up characters, doubled up punctuation, or useless dots, dashes, and slashes.
     *
     * @param string      $path     The name of the file
     * @param false|mixed $slashes  Whether to allow slashes (directories) in the $file name (anything but false), or not
     * @param false|mixed $capitals Whether to allow capitals in the $file name (anything but false), or not
     *
     * @return string The filtered $file name
     */
    public static function format($path, $slashes = false, $capitals = false)
    {
        if ($capitals === false) {
            $path = strtolower($path);
        }
        $path = str_replace(array('\\', '_'), array('/', '-'), $path);
        $path = preg_replace('/[^0-9a-z.\-\/]/i', '-', $path); // alphanumeric . - /
        $path = preg_replace('/[.\-\/](?=[.\-\/])/', '', $path); // no doubled up punctuation
        $path = trim($path, '.-/'); // no trailing (or preceding) dots, dashes, and slashes
        if (is_int($slashes) && $slashes > 0) {
            $path = explode('/', $path);
            $parts = implode('/', array_slice($path, 0, $slashes));
            if (count($path) > $slashes) {
                $parts .= '-'.implode('-', array_slice($path, $slashes));
            }
            $path = $parts;
        } elseif ($slashes === false) {
            $path = str_replace('/', '-', $path);
        }

        return $path;
    }

    /**
     * Formats bytes to a human readable string.
     *
     * @param int $size
     * @param int $precision
     *
     * @return string
     */
    private static function bytes($size, $precision = 2)
    {
        $base = log($size, 1024);
        $suffixes = array('B', 'kB', 'MB', 'GB', 'TB');

        return ($size > 0) ? round(pow(1024, $base - floor($base)), $precision).' '.$suffixes[floor($base)] : '0 B';
    }

    /**
     * Creates a textarea $field for your $form that incorporates the wyciwyg to edit your $file, and will delete the file if saved as an empty string.
     *
     * @param object       $form  The form you are working with
     * @param string       $field The name of the textarea
     * @param string|array $files If this is an array, then the first one is the main file, and the others are backup plans that we glean the contents from if it exists
     *
     * @return string A wyciwyg textarea
     */
    public static function textarea($form, $field, $files)
    {
        extract(Admin::params('page'));
        foreach ((array) $files as $file) {
            if (!is_file($file)) {
                continue;
            }
            $form->values[$field] = file_get_contents($file);
            break;
        }
        $form->validator->set($field, '');
        $file = (is_array($files)) ? array_shift($files) : $files;
        self::save(array($field => $file), array($field));
        $path = pathinfo($file);

        return $form->textarea($field, array(
            'rows' => 8,
            'data-file' => $path['basename'],
            'class' => 'wyciwyg input-sm '.$path['extension'],
        ));
    }

    /**
     * Creates a directory listing of files with links to edit, and a form to add more.
     *
     * @param string      $dir        The directory whose $files we want to manage
     * @param array       $extensions An array of file types to manage in the $dir, grouped by '**files**', '**images**', and '**resources**' array keys.  You can '**exclude**' an array of files, and if you want to unzip uploaded zip files, then put '**unzip**' ``in_array()``
     * @param false|mixed $recursive  Whether you want to limit files to this folder (false) or not (anything else), or (int) how many folders above the current you want to work with
     *
     * @return string
     *
     * @todo Unzip files in dir - actual code
     */
    public static function view($dir, array $extensions = array(), $recursive = false)
    {
        extract(Admin::params(array('bp', 'blog', 'auth', 'page', 'website')));
        $dir = rtrim($dir, '/').'/';

        // Setup the form
        $form = array_merge(array(
            'exclude' => array(), // the files we don't want to include at all
            'files' => ($auth->isAdmin(1) ? 'php|' : '').'ini|yml|twig|js|css|less|scss',
            'images' => 'jpg|jpeg|gif|png|ico',
            'resources' => 'pdf|ttf|otf|svg|eot|woff|woff2|swf|tar|gz|tgz|zip|csv|xl|xls|xlsx|word|doc|docx|ppt|ogg|wav|mp3|mp4|mpe|mpeg|mpg|mov|qt|psd',
        ), $extensions);
        $extensions = array();
        foreach (array('files', 'images', 'resources') as $type) {
            if (!empty($form[$type])) {
                $extensions[] = trim($form[$type], '|');
            }
        }
        $extensions = implode('|', $extensions);
        if (empty($extensions)) {
            return;
        }

        // Get the files
        list($dirs, $files) = self::iterate($dir, $recursive, $extensions);

        // Download (backup) all of the $files if using our link.
        if ($page->get('download') == 'files') {
            $zip = new ZipStream\ZipStream(implode('_', array(
                'backup',
                $blog->url($website),
                trim(str_replace(array($page->dir(), '/'), array('', '-'), $dir), '-'),
                date('Y-m-d'),
            )).'.zip');
            foreach ($files as $file) {
                $zip->addFileFromPath($file, $dir.$file, array('time' => filemtime($dir.$file)));
            }
            $zip->finish();
        }

        // Remove any excluded files
        if (!empty($form['exclude'])) {
            $files = array_diff($files, (array) $form['exclude']);
        }

        // Return if there are more than 200 files to manage.
        if (count($files) > 200) {
            return '<h4 class="text-center">Sorry, this directory has 200+ files which is more than we can reasonably manage here.</h4>';
        }

        // Delete file if called for
        if (($delete = $page->post('delete-file')) && in_array($delete, $files)) {
            if (is_file($dir.$delete) && unlink($dir.$delete)) {
                exit('success');
            }
            exit('error');
        }

        // Display image form if using one of our edit image links
        if ($image = $page->get('image')) {
            $eject = $page->url('delete', '', 'image');
            if (!in_array($image, $files)) {
                $page->eject($eject);
            }

            return self::image($dir.$image, $eject);
        }

        // Rename a file if it doesn't already exist.
        if ($oldname = $page->post('oldname')) {
            $type = $page->post('type');
            $newname = self::format($page->post('newname'), $recursive, ($type == '.php'));
            if (!empty($newname) && is_file($dir.$oldname.$type) && in_array($oldname.$type, $files)) {
                if (is_file($dir.$newname.$type)) {
                    return $page->sendJson(array('success' => false, 'msg' => 'This file already exists.'));
                } else {
                    if ($recursive && !is_dir(dirname($dir.$newname.$type))) {
                        mkdir(dirname($dir.$newname.$type), 0755, true);
                    }
                    rename($dir.$oldname.$type, $dir.$newname.$type);
                    if (($key = array_search($oldname.$type, $files)) !== false) {
                        $files[$key] = $newname.$type;
                    }
                    $data = array('success' => true, 'newValue' => $newname);
                }
            } else {
                $data = array('success' => true, 'newValue' => $oldname);
            }
        }

        // Save a file if it is being summoned.
        $save = array_flip(preg_grep('/\.(php|ini|yml|twig|js|css|less|scss)$/', $files));
        foreach ($save as $file => $uri) {
            $save[$file] = $dir.$file;
        }
        self::save($save);

        // Group the $files for displaying in chunks, and make each an ``array($link, $edit, $file, $size, $delete)``
        $images = $links = array();
        $url = str_replace($page->dir(), $page->path('dir'), $dir);
        sort($files);
        foreach ($files as $file) {
            $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
            $view = $bp->button('xs link', 'view '.$bp->icon('new-window'), array('href' => $url.$file, 'target' => '_blank'));
            $rename = '<a class="rename-file text-nowrap" href="#" data-name=".'.$ext.'">'.substr($file, 0, -(strlen($ext) + 1)).'</a>.'.$ext;
            $size = (is_file($dir.$file)) ? str_replace('Bytes', 'bytes', self::bytes(filesize($dir.$file), 1)) : '0 bytes';
            $delete = '<a class="delete-file pull-right" href="#" data-uri="'.$file.'">'.$bp->icon('trash').'</a>';
            switch ($ext) {
                case 'php':
                case 'ini':
                case 'yml':
                case 'twig':
                    $view = '';
                    // We don't break here for the $edit button below.
                case 'css':
                case 'js':
                    $edit = $bp->button('xs warning wyciwyg '.$ext, $bp->icon('pencil').' edit', array(
                        'href' => '#',
                        'data-retrieve' => $file,
                        'data-file' => $file,
                    ));
                    $files[$ext][] = array($view, $edit, $rename, $size, $delete);
                    break;
                case 'less':
                case 'scss':
                    $edit = $bp->button('xs warning wyciwyg '.$ext, $bp->icon('pencil').' edit', array(
                        'href' => '#',
                        'data-retrieve' => $file,
                        'data-file' => $file,
                    ));
                    $files['css'][] = array('', $edit, $rename, $size, $delete);
                    break;
                case 'jpg':
                case 'jpeg':
                case 'gif':
                case 'png':
                case 'ico':
                    if ($dimensions = getimagesize($dir.$file)) {
                        $view = '<a href="'.$url.$file.'" target="_blank"><img src="'.$url.$file.'#~50x50" class="img-responsive"></a>';
                        $edit = $bp->button('xs warning', $bp->icon('pencil').' edit', array('href' => $page->url('add', '', 'image', $file)));
                        $files['images'][] = array($view, $edit, $rename.'|'.$dimensions[0].'x'.$dimensions[1], $size, $delete);
                    }
                    break;
                default:
                    $files['links'][] = array($view, '', $rename, $size, $delete);
                    break;
            }
        }

        // Create the $files manager html.
        $html = '';
        if (!empty($files)) {
            $html .= $bp->table->open('class=table responsive striped condensed');
            foreach (array('php' => 'PHP', 'ini' => 'INI', 'yml' => 'YAML', 'twig' => 'TWIG', 'css' => 'CSS', 'js' => 'JavaScript', 'images' => 'Images', 'links' => 'Links') as $type => $name) {
                if (isset($files[$type])) {
                    $html .= $bp->table->head();
                    $html .= $bp->table->cell('colspan=6|style=padding-top:10px; padding-bottom:10px;', $name);
                    foreach ($files[$type] as $values) {
                        list($link, $edit, $file, $size, $delete) = $values;
                        $html .= $bp->table->row();
                        $html .= $bp->table->cell('class=col-sm-1', $link);
                        $html .= $bp->table->cell('class=col-sm-1', $edit);
                        if ($type == 'images') {
                            list($file, $dimensions) = explode('|', $file);
                            $html .= $bp->table->cell('class=text-nowrap', $file);
                            $html .= $bp->table->cell('style=width:100px; text-align:center;', $dimensions);
                        } else {
                            $html .= $bp->table->cell('colspan=2|class=text-nowrap', $file);
                        }
                        $html .= $bp->table->cell('style=width:100px; text-align:center;|class=text-nowrap', $size);
                        $html .= $bp->table->cell('style=width:30px;', $delete);
                    }
                }
            }
            $html .= $bp->table->close();
        }

        // Update the html when renaming a file.
        if ($oldname) {
            $data['html'] = Asset::urls($html);

            return $page->sendJson($data);
        }

        // Create a form to upload and create new files
        $files = $html; // They have all been converted over to something we can work with now.
        if ($form) {
            $html = '';
            $ext = $form;
            $form = $bp->form('admin_file_upload');
            if (!empty($ext['images']) && empty($ext['files']) && empty($ext['resourcees'])) {
                $upload = array(
                    Upload::bootstrap($form)->file('upload[]', array(
                        'size' => '10M',
                        'types' => $ext['images'],
                    )),
                    'Upload additional images that you would like to include.',
                );
            } else {
                $upload = array(
                    Upload::bootstrap($form)->file('upload[]', array(
                        'size' => '10M',
                        'types' => $extensions,
                    )),
                    'Upload additional files that you would like to include.',
                );
            }
            if (!empty($ext['files'])) {
                $form->validator->set('file');
            }
            if ($recursive) {
                $form->validator->set('directory');
            }
            if ($vars = $form->validator->certified()) {
                if (!empty($ext['files']) && !empty($vars['file'])) {
                    $file = self::format($vars['file'], $recursive, (substr($vars['file'], -4) == '.php'));
                    if (preg_match('/^.+\.('.$ext['files'].')$/', $file)) {
                        if (!is_dir(dirname($dir.$file))) {
                            mkdir(dirname($dir.$file), 0755, true);
                        }
                        file_put_contents($dir.$file, '');
                    }
                }
                if (!empty($vars['upload'])) {
                    $temp = ($recursive && !empty($vars['directory'])) ? self::format($vars['directory'], true).'/' : '';
                    if (!empty($temp) && !is_dir(dirname($dir.self::format($temp.'bogus.file', $recursive)))) {
                        mkdir(dirname($dir.self::format($temp.'bogus.file')), 0755, true);
                    }
                    foreach ($vars['upload'] as $file) {
                        if (is_file(Upload::$folder.$file)) {
                            $safe = self::format($temp.substr($file, 32), $recursive, (substr($file, -4) == '.php'));
                            rename(Upload::$folder.$file, $dir.$safe);
                            if (in_array('unzip', $ext) && substr($file, -4) == '.zip') {
                                // unzip
                            }
                        }
                    }
                }
                $page->eject($form->eject);
            }
            $html .= $form->header();
            if (!empty($ext['files'])) {
                $html .= $form->field(array('File',
                    'Enter the name of the file that you would like to create.  The only file types allowed are: .'.implode(', .', explode('|', $ext['files'])),
                ), $form->text('file'));
            }
            if ($recursive) {
                $html .= $form->field(array('Directory',
                    'Enter the directory (if any) where you would like your uploaded files to go.',
                ), $form->text('directory'));
            }
            list($field, $info) = $upload;
            $html .= $form->field(array('Upload', $info), $field);
            $html .= $form->submit('Submit', $bp->button('link pull-right', $bp->icon('download').' Backup', array(
                'href' => $page->url('add', '', 'download', 'files'),
                'title' => 'Click to Download',
            )));
            $html .= $form->close();
            $form = $html;
        } else {
            $form = '';
        }
        $page->link(array(
            'https://cdn.jsdelivr.net/bootstrap.editable/1.5.1/css/bootstrap-editable.min.css',
            'https://cdn.jsdelivr.net/bootstrap.editable/1.5.1/js/bootstrap-editable.min.js',
            '<script>function xEditable () {
                $("#admin_manage_files .rename-file").editable({
                    pk: "rename",
                    type: "text",
                    title: "Rename File",
                    url: window.location.href,
                    savenochange: true,
                    ajaxOptions: {dataType:"json"},
                    validate: function(value) { if($.trim(value) == "") return "This field is required"; },
                    params: function(params) { return {oldname:$(this).text(), newname:params.value, type:params.name}; },
                    success: function(response, newValue) {
                        if(!response.success) return response.msg;
                        $("#admin_manage_files").html(response.html);
                        xEditable();
                    }
                });
            }</script>',
        ));
        $page->jquery('xEditable();
            $("#admin_manage_files").on("click", "a.delete-file", function(){
                var file = $(this).data("uri");
                var row = $(this).closest("tr");
                bootbox.confirm({
                    size: "large",
                    backdrop: false,
                    message: "Are you sure you would like to delete this file?",
                    callback: function (result) {
                        if (result) {
                            row.hide();
                            $.post(location.href, {"delete-file":file}, function(data){
                                if (data != "success") row.show();
                            }, "text");
                        }
                    }
                });
                return false;
            });
        ');

        return '<div id="admin_manage_files">'.$files.'</div><br>'.$form;
    }

    /**
     * Coordinates with the wyciwyg to retrieve and save files.
     *
     * @param array $files  An ``array('field|file'=>'path')``
     * @param array $remove The $file's keys whose path's you would like to remove if saved empty
     */
    public static function save(array $files, array $remove = array())
    {
        $page = Page::html();
        if (($retrieve = $page->post('retrieve')) && isset($files[$retrieve])) {
            exit(is_file($files[$retrieve]) ? file_get_contents($files[$retrieve]) : '');
        }
        if (($save = $page->post('field')) && isset($files[$save]) && !is_null($page->post('wyciwyg'))) {
            $file = $files[$save];
            $code = str_replace("\r\n", "\n", base64_decode(base64_encode($_POST['wyciwyg'])));
            if (empty($code) && in_array($save, $remove)) {
                if (is_file($file)) {
                    unlink($file);
                }
                exit('Saved');
            }
            if (!is_dir(dirname($file))) {
                mkdir(dirname($file), 0755, true);
            }
            if (!empty($code)) {
                extract(Admin::params('page', 'blog'));
                switch (pathinfo($file, PATHINFO_EXTENSION)) {
                    case 'php':
                        $linter = $page->file(md5($file).'.php');
                        file_put_contents($linter, $code);
                        // exec(PHP_BINARY.' -l '.escapeshellarg($linter).' 2>&1', $output);
                        exec('php -l '.escapeshellarg($linter).' 2>&1', $output);
                        unlink($linter);
                        $output = trim(implode("\n", $output));
                        if (!empty($output) && strpos($output, 'No syntax errors') === false) {
                            exit(preg_replace('#'.str_replace('/', '[\\\\//]{1}', preg_quote($linter)).'#', str_replace('.php', '', $save).'.php', $output));
                        }
                        break;
                    case 'twig':
                        $twig = $blog->theme->getTwig();
                        $twig->setParser(new Parser($twig));
                        try {
                            $twig->parse($twig->tokenize(new \Twig_Source($code, basename($file))));
                        } catch (\Twig_Error_Syntax $e) {
                            exit($e->getMessage());
                        }
                        break;
                    case 'yml':
                        try {
                            $yaml = Yaml::parse($code);
                        } catch (ParseException $e) {
                            exit($e->getMessage());
                        }
                        break;
                    case 'ini':
                        // http://stackoverflow.com/questions/1241728/can-i-try-catch-a-warning
                        set_error_handler(function ($errno, $errstr) {
                            throw new Exception($errstr);
                        });
                        $linter = $page->file(md5($file).'.ini');
                        file_put_contents($linter, $code);
                        try {
                            $output = parse_ini_file($linter);
                            unlink($linter);
                        } catch (Exception $e) {
                            exit(preg_replace('#'.str_replace('/', '[\\\\//]{1}', preg_quote($linter)).'#', str_replace('.ini', '', $save).'.ini', $e->getMessage()));
                        }
                        restore_error_handler();
                        break;
                }
            }
            if (!is_writable($file)) {
                exit('This file is not writable');
            } elseif (file_put_contents($file, $code) === false) {
                exit('There was an error');
            } else {
                exit('Saved');
            }
        }
    }

    /**
     * Creates a form for editing an image $path.
     *
     * @param string $path  The filepath to the image
     * @param string $eject Where to eject after editing
     *
     * @return string
     *
     * @used-by Files::view()
     */
    private static function image($path, $eject = '')
    {
        $html = '';
        $imagick = (extension_loaded('imagick') && class_exists('Imagick')) ? true : false;
        $types = array('jpg', 'gif', 'png');
        if ($imagick) {
            $types[] = 'ico';
        }
        if ((!$resource = self::resource($path)) || !in_array($resource['ext'], $types)) {
            return $html;
        }
        extract(Admin::params(array('bp', 'page')));
        $form = $bp->form('admin_image_resize');
        $form->menu('type', array_combine($types, $types));
        $form->values = array(
            'type' => $resource['ext'],
            'width' => $resource['width'],
            'height' => $resource['height'],
            'quality' => 90,
        );
        $form->validator->set(array(
            'type' => 'required|inList['.implode(',', $types).']',
            'width' => 'required|digits|max['.$resource['width'].']',
            'height' => 'required|digits|max['.$resource['height'].']',
            'quality' => 'required|digits|max[100]',
            'coords',
        ));
        if ($vars = $form->validator->certified()) {
            $coords = explode(',', $vars['coords']);
            if (count($coords) == 4) {
                list($x1, $y1, $x2, $y2) = $coords;
                $count = 1;
                while (is_file($resource['dir'].$resource['name'].'-'.$count.'.'.$vars['type'])) {
                    ++$count;
                }
                $name = $resource['dir'].$resource['name'].'-'.$count.'.'.$vars['type'];
                $manager = new ImageManager(array('driver' => $imagick ? 'imagick' : 'gd'));
                $image = $manager->make($resource['path']);
                $image->crop(($x2 - $x1), ($y2 - $y1), $x1, $y1);
                $image->resize($vars['width'], $vars['height']);
                $image->save($name, $vars['quality']);
            }
            $page->eject($page->url('delete', $eject, 'submitted'));
        }
        $form->validator->jquery($form->header['name']);
        $html .= $form->header();
        $div = '<div class="col-sm-4" style="padding:0px;">';
        $html .= $form->field(array('Type',
            'This will convert the image to the selected format.',
        ), $div.$form->select('type', array()).'</div>');
        $html .= $form->field(array('Width',
            'Set the new width of your image.',
        ), $div.$form->group('', 'px', $form->text('width', array('maxlength' => 4))).'</div>');
        $html .= $form->field(array('Height',
            'Set the new height of your image.',
        ), $div.$form->group('', 'px', $form->text('height', array('maxlength' => 4))).'</div>');
        $html .= $form->field(array('Quality',
            'The image quality from 0 (poor quality, small file) to 100 (best quality, big file).',
        ), $div.$form->group('', '%', $form->text('quality', array('maxlength' => 3))).'</div>');
        $form->hidden['coords'] = '';
        $html .= $form->field(false, $page->tag('img', array(
            'id' => 'crop',
            'class' => 'img-responsive',
            'src' => str_replace($page->dir(), $page->path('dir'), $resource['path']),
            'width' => $resource['width'],
            'height' => $resource['height'],
            'alt' => '',
        )));
        $page->link(array(
            'https://cdn.jsdelivr.net/imagesloaded/3.2.0/imagesloaded.pkgd.min.js', // Use imagesLoaded v3 for IE8 support.
            'https://cdn.jsdelivr.net/wordpress/3.8/js/imgareaselect/jquery.imgareaselect.min.js',
            'https://cdn.jsdelivr.net/wordpress/3.8/js/imgareaselect/imgareaselect.css',
        ));
        $page->jquery('$("#crop").imagesLoaded().done(function(){
            var originalWidth = ' .$resource['width'].';
            var originalHeight = ' .$resource['height'].';
            var ias = $("img#crop").attr("width", $("img#crop").width()).attr("height", $("img#crop").height()).imgAreaSelect({
                instance: true,
                handles: "corners",
                imageWidth: ' .$resource['width'].',
                imageHeight: ' .$resource['height'].',
                aspectRatio: false,
                onSelectEnd: function(img, selection){
                    $("input[name=coords]").val(selection.x1 + "," + selection.y1 + "," + selection.x2 + "," + selection.y2);
                }
            });

            $("input[name=width]").change(function(){
                var width = parseInt($("input[name=width]").val());
                if (isNaN(width) || width > originalWidth) width = originalWidth;
                $("input[name=width]").val(width);
                $("input[name=height]").val(parseInt(originalHeight / originalWidth * width));
                reCrop();
            });

            $("input[name=height]").change(function(){
                var height = parseInt($("input[name=height]").val());
                if (isNaN(height) || height > originalHeight) height = originalHeight;
                $("input[name=height]").val(height);
                reCrop();
            });

            function reCrop () {
                var width = $("input[name=width]").val();
                var height = $("input[name=height]").val();
                ias.setSelection(0, 0, width, height);
                ias.setOptions({
                    aspectRatio: width + ":" + height,
                    minWidth: width,
                    minHeight: height,
                    show: true
                });
                ias.update();
                $("input[name=coords]").val("0,0," + width + "," + height);
            }
        });');
        $html .= $form->submit('Resize');
        $html .= $form->close();

        return $html;
    }

    /**
     * Determines if the $path is an editable image resource.
     *
     * @param string $path
     *
     * @return false|array
     *
     * @used-by Files::image()
     */
    private static function resource($path)
    {
        if (preg_match('/\.(jpg|jpeg|gif|png|ico)$/i', $path) && is_file($path) && ($dimensions = getimagesize($path))) {
            list($width, $height, $type) = $dimensions;
            switch ($type) {
                case 1: $type = 'gif'; break;
                case 2: $type = 'jpg'; break;
                case 3: $type = 'png'; break;
                case 17: $type = 'ico'; break;
            }
            if (!is_int($type)) {
                $info = pathinfo($path);

                return array(
                    'path' => $path,
                    'dir' => $info['dirname'].'/',
                    'name' => $info['filename'],
                    'ext' => $type,
                    'width' => $width,
                    'height' => $height,
                );
            }
        }

        return false;
    }
}