plugins/ckeditor/admin_modules/yf_ck_file_browser.class.php

Summary

Maintainability
F
3 days
Test Coverage
<?php

class yf_ck_file_browser
{
    public $TOP_DIR = '/uploads/';
    public $WRITABLE_DIR = '/uploads/ck_browser/';
    public $ALLOWED_EXTS = [
        'jpg',
        'jpeg',
        'png',
        'gif',
    ];
    public $MIN_FILE_SIZE = 50;
    public $ENABLED_IMG_EDIT = true;
    public $ENABLED_IMG_DELETE = true;
    protected $base = null;


    public function _init()
    {
        $this->base = $this->_real(PROJECT_PATH . $this->TOP_DIR);
        _mkdir_m(PROJECT_PATH . $this->TOP_DIR);
        _mkdir_m(PROJECT_PATH . $this->WRITABLE_DIR);
    }

    /**
     * @param mixed $rslt
     */
    public function _ajax_out($rslt)
    {
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($rslt);
        if (is_ajax()) {
            exit();
        }
    }


    public function show()
    {
        asset('jquery-jstree');
        $slick_view = isset($_GET['CKEditorFuncNum']);
        $body = tpl()->parse(__CLASS__ . '/main', [
            'ck_funcnum' => (int) $_GET['CKEditorFuncNum'],
        ]);
        return $slick_view ? print common()->show_empty_page($body) : $body;
    }


    public function get_node()
    {
        $node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
        $rslt = $this->_lst($node, (isset($_GET['id']) && $_GET['id'] === '#'));
        return $this->_ajax_out($rslt);
    }


    public function get_content()
    {
        $node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
        $rslt = $this->_data($node);
        return $this->_ajax_out($rslt);
    }


    public function create_node()
    {
        $node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
        $rslt = $this->_create($node, isset($_GET['text']) ? $_GET['text'] : '', ( ! isset($_GET['type']) || $_GET['type'] !== 'file'));
        return $this->_ajax_out($rslt);
    }


    public function rename_node()
    {
        $node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
        $rslt = $this->_rename($node, isset($_GET['text']) ? $_GET['text'] : '');
        return $this->_ajax_out($rslt);
    }


    public function delete_node()
    {
        $node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
        $rslt = $this->_remove($node);
        return $this->_ajax_out($rslt);
    }


    public function move_node()
    {
        $node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
        $parn = isset($_GET['parent']) && $_GET['parent'] !== '#' ? $_GET['parent'] : '/';
        $rslt = $this->_move($node, $parn);
        return $this->_ajax_out($rslt);
    }


    public function copy_node()
    {
        $node = isset($_GET['id']) && $_GET['id'] !== '#' ? $_GET['id'] : '/';
        $parn = isset($_GET['parent']) && $_GET['parent'] !== '#' ? $_GET['parent'] : '/';
        $rslt = $this->_copy($node, $parn);
        return $this->_ajax_out($rslt);
    }


    public function upload_image()
    {
        return $this->upload_file();
    }


    public function upload_file()
    {
        no_graphics(true);
        if ( ! MAIN_TYPE_ADMIN) {
            return print 'access denied';
        }
        // TODO: tons of additional security checks
        $error = '';
        $add_path = $_GET['path'] ? urldecode($_GET['path']) . '/' : '';
        $upload_dir = $this->base . '/' . $add_path . $_GET['id'];
        if ( ! file_exists($upload_dir)) {
            mkdir($upload_dir, 0755, true);
        }
        $file = $_FILES['file'] ?: $_FILES['upload'] ?: current($_FILES);
        if (empty($file['tmp_name'])) {
            $error = 'File upload error';
        } else {
            $file_path = $upload_dir . '/' . $file['name'];
            if (file_exists($file_path)) {
                $error = 'File already exists';
            } else {
                $result = move_uploaded_file($file['tmp_name'], $file_path);
                if ( ! $result || ! file_exists($file_path)) {
                    $error = 'Cannot upload file to this dir';
                }
            }
        }
        if ($error) {
            echo '<script>alert("' . _prepare_html(t($error)) . '");</script>';
        } else {
            if ($_GET['CKEditorFuncNum']) {
                return $this->_ck_show_message('Uploaded OK', str_replace(PROJECT_PATH, WEB_PATH, $file_path));
            }
            echo '<script>try { parent.refreshjstree(); } catch(e) { console.log(e) }</script>';
        }
        exit;
    }

    /**
     * @param mixed $message
     * @param mixed $url
     */
    public function _ck_show_message($message, $url = '')
    {
        $funcNum = $_GET['CKEditorFuncNum'];
        echo '<script type="text/javascript">
            try {
                window.opener.CKEDITOR.tools.callFunction(' . $funcNum . ', "' . $url . '", "' . $message . '");
            } catch (e) {
                window.parent.CKEDITOR.tools.callFunction(' . $funcNum . ', "' . $url . '", "' . $message . '");
            }
        </script>';
        exit;
    }

    /**
     * Endpoint for pixlr editor for upload back edited image.
     */
    public function edit()
    {
        if ( ! $this->ENABLED_IMG_EDIT) {
            return false;
        }
        $img_url = urldecode($_REQUEST['image']);
        $title = ltrim(str_replace('|', '/', urldecode($_REQUEST['title'])), '/');
        $type = strtolower($_REQUEST['type']);
        if ( ! strlen($img_url)
            || parse_url($img_url, PHP_URL_HOST) !== 'apps.pixlr.com'
            || ! strlen($title)
            || false !== strpos($title, '../')
            || ! in_array($type, $this->ALLOWED_EXTS)
        ) {
            common()->message_error('Image upload from Pixlr error #1: wrong input');
            return common()->show_messages();
        }

        // Move image from url into temp file and analyze it
        $tmp_dir = '/tmp/pixlr_upload/';
        ! file_exists($tmp_dir) && _mkdir_m($tmp_dir);
        $tmp_path = tempnam($tmp_dir, 'pixlr_upload_');
        file_put_contents($tmp_path, file_get_contents($img_url));
        if ( ! file_exists($tmp_path) || filesize($tmp_path) <= $this->MIN_FILE_SIZE) {
            common()->message_error('Image upload from Pixlr error #2: temp file error');
            return common()->show_messages();
        }
        $target = PROJECT_PATH . $this->TOP_DIR . $title . '.' . $type;
        if ( ! file_exists($target)) {
            common()->message_error('Image upload from Pixlr error #3: target not exists');
            return common()->show_messages();
        }

        // copy old and new file as revision into separate dir
        $revs_dir = PROJECT_PATH . 'uploads/.img_revisions/';
        ! file_exists($revs_dir) && _mkdir_m($revs_dir);
        if (md5_file($target) != md5_file($tmp_path)) {
            $revid = date('YmdHis_' . str_pad(substr(microtime(true), 11, 2), 2, '0', STR_PAD_LEFT));
            $rev_path_old = $revs_dir . $revid . '__old__' . urlencode($title) . '.' . $type;
            $rev_path_new = $revs_dir . $revid . '__new__' . urlencode($title) . '.' . $type;
            file_put_contents($rev_path_old, file_get_contents($target));
            file_put_contents($rev_path_new, file_get_contents($tmp_path));
        }
        // Finally save new file
        file_put_contents($target, file_get_contents($tmp_path));
        unlink($tmp_path);
        common()->message_success('Image upload from Pixlr success!');
        $web_path = str_replace(PROJECT_PATH, MEDIA_PATH, $target);
        return common()->show_messages()
            . '<br />path: ' . _prepare_html($title) . ', size: ' . filesize($target)
            . '<br /><a href="' . $web_path . '" target="_blank"><img src="' . $web_path . '" style="max-width: 200px; max-height: 200px;"></a>'
            . '<br /><br />' . a(['href' => '/@object/show/' . urlencode($title), 'title' => 'Go Next', 'target' => '']);
    }

    /**
     * Endpoint for delete image (with backuping old).
     */
    public function delete_img()
    {
        $res = [];
        if ($this->ENABLED_IMG_DELETE && is_ajax() && is_post() && ! empty($_POST['path'])) {
            $fs_path = parse_url($_POST['path'], PHP_URL_PATH);
            if (substr($fs_path, 0, strlen($this->TOP_DIR)) === $this->TOP_DIR) {
                $fs_path = substr($fs_path, strlen($this->TOP_DIR));
            }
            $fs_path = $this->_real($this->base . '/' . trim($fs_path, '/'));
            if (is_file($fs_path)) {
                if ($this->_backup_deleted_img($fs_path) && unlink($fs_path)) {
                    $res = ['result' => 'OK'];
                } else {
                    $res = ['result' => 'Error: cannot backup or delete image, maybe file or dir permissions?'];
                }
            }
        }
        return $this->_ajax_out($res);
    }

    /**
     * copy old file as revision into separate dir.
     * @param mixed $fs_path
     */
    public function _backup_deleted_img($fs_path)
    {
        if ( ! $fs_path || ! file_exists($fs_path)) {
            return false;
        }
        $ext = pathinfo($fs_path, PATHINFO_EXTENSION);
        $path_wo_ext = substr($fs_path, strlen($this->base), -strlen('.' . $ext));
        $revs_dir = PROJECT_PATH . 'uploads/.img_revisions/';
        ! file_exists($revs_dir) && _mkdir_m($revs_dir);
        $revid = date('YmdHis_' . str_pad(substr(microtime(true), 11, 2), 2, '0', STR_PAD_LEFT));
        $rev_path = $revs_dir . $revid . '__deleted__' . urlencode($path_wo_ext) . '.' . $ext;
        return file_put_contents($rev_path, file_get_contents($fs_path));
    }

    /**
     * @param mixed $path
     */
    public function _real($path)
    {
        $temp = realpath($path);
        if ( ! $temp) {
            throw new Exception('Path does not exist: ' . $path);
        }
        if ($this->base && strlen($this->base)) {
            if (strpos($temp, $this->base) !== 0) {
                throw new Exception('Path is not inside base (' . $this->base . '): ' . $temp);
            }
        }
        return $temp;
    }

    /**
     * @param mixed $id
     */
    public function _path($id)
    {
        $id = str_replace('/', '/', $id);
        $id = trim($id, '/');
        $id = $this->_real($this->base . '/' . $id);
        return $id;
    }

    /**
     * @param mixed $path
     */
    public function _id($path)
    {
        $path = $this->_real($path);
        $path = substr($path, strlen($this->base));
        $path = str_replace('/', '/', $path);
        $path = trim($path, '/');
        return strlen($path) ? $path : '/';
    }

    /**
     * @param mixed $id
     * @param mixed $with_root
     */
    public function _lst($id, $with_root = false)
    {
        $dir = $this->_path($id);
        $res = [];
        foreach (glob(rtrim($dir, '/') . '/*') as $f) {
            if ( ! strlen($f) || ! file_exists($f)) {
                continue;
            }
            $item = basename($f);
            if (is_dir($f)) {
                $res[] = [
                    'text' => $item,
                    'children' => true,
                    'id' => $this->_id($f),
                    'icon' => 'fa fa-folder fa-lg',
                ];
            } else {
                if (filesize($f) <= $this->MIN_FILE_SIZE) {
                    continue;
                }
                $ext = strtolower(pathinfo($item, PATHINFO_EXTENSION));
                if ( ! in_array($ext, $this->ALLOWED_EXTS)) {
                    continue;
                }
                $res[] = [
                    'text' => $item,
                    'children' => false,
                    'id' => $this->_id($f),
                    'type' => 'file',
                    'icon' => 'fa fa-file-image-o fa-lg fa-file-type-' . $ext,
                ];
            }
        }
        if ($with_root && $this->_id($dir) === '/') {
            $res = [[
                'text' => basename($this->base),
                'children' => $res,
                'id' => '/',
                'icon' => 'fa fa-folder fa-lg',
                'state' => [
                    'opened' => true,
                    'disabled' => true,
                ],
            ]];
        }
        return $res;
    }

    /**
     * @param mixed $id
     */
    public function _data($id)
    {
        if (strpos($id, ':')) {
            $id = array_map([$this, 'id'], explode(':', $id));
            return [
                'type' => 'multiple',
                'content' => 'Multiple selected: ' . implode(' ', $id),
            ];
        }
        $dir = $this->_path($id);
        if (is_dir($dir)) {
            $form = form(true, [
                'action' => url('/@object/upload_file/' . urlencode($id)),
                'autocomplete' => 'off',
                'enctype' => 'multipart/form-data',
                'class' => 'form-condensed form-no-labels ck_upload_form',
                'target' => 'file_upload_process_container',
                'no_label' => 1,
            ])
            ->file('file', t('upload image'), [
                'accept' => 'image/*',
                'style' => 'width:auto; background: inherit',
                'class_add' => 'btn btn-primary',
            ])
            ->save([
                'value' => t('Upload'),
                'class' => 'btn btn-primary',
            ]);
            $images = [];
            $files = [];
            foreach (glob(rtrim($dir) . '/*') as $f) {
                if ( ! is_file($f)) {
                    continue;
                }
                $ext = strtolower(pathinfo($f, PATHINFO_EXTENSION));
                if ( ! in_array($ext, $this->ALLOWED_EXTS)) {
                    continue;
                }
                if (($fsize = filesize($f)) <= $this->MIN_FILE_SIZE) {
                    continue;
                }
                $sizes[$f] = $fsize;
                $files[$f] = filemtime($f);
            }
            // Sort files by date DESC
            arsort($files);
            foreach ((array) $files as $f => $mtime) {
                $ext = strtolower(pathinfo($f, PATHINFO_EXTENSION));
                list($w, $h) = getimagesize($f);
                $fsize = $sizes[$f];
                $fsize = round($fsize / 1024, 0, 2) . 'Kb';
                $uploads_path = str_replace('/', '|', ltrim(str_replace(PROJECT_PATH . ltrim($this->TOP_DIR, '/'), '', $f), '/'));
                $images[] = ''
                    . '<div class="ck_select_image">'
                        . '<a href="#" class="img-select" title="' . _prepare_html(basename($f)) . '">'
                            . '<img src="' . str_replace(PROJECT_PATH, MEDIA_PATH, $f) . '?m=' . (int) $mtime . '" data-uploads-path="' . _prepare_html($uploads_path) . '" />'
                        . '</a>'
                        . '<div class="img-details">' . $fsize . ' ' . $w . 'x' . $h . ' ' . strtoupper($ext) . '<br />' . date('Y-m-d H:i:s', $mtime) . '</div>'
                        . '<div class="img-actions">'
//                            . a('#', 'Choose', 'fa fa-share', '', 'btn-info')
//                            . a('#', 'View', 'fa fa-eye', '')
//                            . a('#', 'Copy', 'fa fa-copy', '')
                            . ($this->ENABLED_IMG_DELETE ? a('#', 'Delete', 'fa fa-trash', '', 'btn-danger btn-delete') : '')
                            . ($this->ENABLED_IMG_EDIT ? a('#', 'Edit', 'fa fa-edit', '', 'btn-warning btn-edit') : '')
                        . '</div>'
                    . '</div>';
            }
            return [
                'type' => 'folder',
                'content' => ''
                    . '<div>' . t('Current folder:') . ' '
                        . '<b>' . $this->TOP_DIR . $id . '</b><br />'
                        . $form . '<br />'
                        . implode(PHP_EOL, $images)
                    . '</div>',
            ];
        } elseif (is_file($dir)) {
            $ext = strtolower(pathinfo($dir, PATHINFO_EXTENSION));
            $dat = [
                'type' => $ext,
                'content' => '',
            ];
            switch ($ext) {
                case 'jpg':
                case 'jpeg':
                case 'gif':
                case 'png':
                case 'bmp':
                    $dat['content'] = MEDIA_PATH . $this->TOP_DIR . $id;
                    $dat['info'] = round(filesize(PROJECT_PATH . $this->TOP_DIR . $id) / 1024, 0, 2) . 'Kb';
                    break;
                default:
                    $dat['content'] = t('File is not an image: ' . $this->_id($dir));
                    break;
            }
            return $dat;
        }
        throw new Exception('Not a valid selection: ' . $dir);
    }

    /**
     * @param mixed $id
     * @param mixed $name
     * @param mixed $mkdir
     */
    public function _create($id, $name, $mkdir = false)
    {
        $dir = $this->_path($id);
        if (preg_match('([^ a-zа-я-_0-9.]+)ui', $name) || ! strlen($name)) {
            throw new Exception('Invalid name: ' . $name);
        }
        if ($mkdir) {
            _mkdir_m($dir . '/' . $name);
        }
        return ['id' => $this->_id($dir . '/' . $name)];
    }

    /**
     * @param mixed $id
     * @param mixed $name
     */
    public function _rename($id, $name)
    {
        $dir = $this->_path($id);
        if ($dir === $this->base) {
            throw new Exception('Cannot rename root');
        }
        if (preg_match('([^ a-zа-я-_0-9.]+)ui', $name) || ! strlen($name)) {
            throw new Exception('Invalid name: ' . $name);
        }
        $new = explode('/', $dir);
        array_pop($new);
        array_push($new, $name);
        $new = implode('/', $new);
        if ($dir !== $new) {
            if (is_file($new) || is_dir($new)) {
                throw new Exception('Path already exists: ' . $new);
            }
            rename($dir, $new);
        }
        return ['id' => $this->_id($new)];
    }

    /**
     * @param mixed $id
     */
    public function _remove($id)
    {
        $dir = $this->_path($id);
        if ($dir === $this->base) {
            throw new Exception('Cannot remove root');
        }
        if (is_dir($dir)) {
            foreach (glob(rtrim($dir, '/') . '/*') as $f) {
                $this->_remove($this->_id($dir . '/' . basename($f)));
            }
            rmdir($dir);
        }
        if (is_file($dir)) {
            unlink($dir);
        }
        return ['status' => 'OK'];
    }

    /**
     * @param mixed $id
     * @param mixed $par
     */
    public function _move($id, $par)
    {
        $dir = $this->_path($id);
        $par = $this->_path($par);
        $new = explode('/', $dir);
        $new = array_pop($new);
        $new = $par . '/' . $new;
        rename($dir, $new);
        return ['id' => $this->_id($new)];
    }

    /**
     * @param mixed $id
     * @param mixed $par
     */
    public function _copy($id, $par)
    {
        $dir = $this->_path($id);
        $par = $this->_path($par);
        $new = explode('/', $dir);
        $new = array_pop($new);
        $new = $par . '/' . $new;
        if (is_file($new) || is_dir($new)) {
            throw new Exception('Path already exists: ' . $new);
        }
        if (is_dir($dir)) {
            _mkdir_m($new);
            foreach (glob(rtrim($dir, '/') . '/*') as $f) {
                $this->_copy($this->_id($dir . '/' . basename($f)), $this->_id($new));
            }
        }
        if (is_file($dir)) {
            copy($dir, $new);
        }
        return ['id' => $this->_id($new)];
    }
}