yf_file_manager admin module

File manager module.

Fileplugins/sys/admin_modules/yf_file_manager.class.php
Useadmin URL ?object=file_manager

Public properties

A site sets them in its config with $PROJECT_CONF['file_manager']['NAME'] = ...;

PropertyDefaultDescription
$ALLOWED_DIRS
array
[] Directories the sandbox allows; empty means [PROJECT_PATH, UPLOADS_PATH], resolved at run time
$DENIED_EXTENSIONS
array
['php', 'php3', 'php4', 'php5', 'php7', 'php8', ... (21 items)]
array (
  0 => 'php',
  1 => 'php3',
  2 => 'php4',
  3 => 'php5',
  4 => 'php7',
  5 => 'php8',
  6 => 'pht',
  7 => 'phtml',
  8 => 'phps',
  9 => 'phar',
  10 => 'shtml',
  11 => 'inc',
  12 => 'htaccess',
  13 => 'htpasswd',
  14 => 'cgi',
  15 => 'pl',
  16 => 'sh',
  17 => 'py',
  18 => 'rb',
  19 => 'ini',
  20 => 'stpl',
)
Extensions (any dotted segment of the name) the sandbox refuses to write, compared case-insensitively
$SANDBOX
bool
false When true: super-admins only, every request path must lie inside ALLOWED_DIRS, writes refuse DENIED_EXTENSIONS
$_copy_dir_deepness
int
2
$_default_chmod
int
511 @conf_skip
$_default_email
string
''
$_exclude_pattern
string
'#\\.(svn|git)#' @conf_skip Exclude files pattern
$_include_pattern
string
'' @conf_skip Include files pattern
$color_dir
string
'#0066cc' Color of the subdirs
$color_file
string
'#00aa66' Color of the files
$color_zip
string
'navy' Color of the ZIP archives

Public methods 25

chmod_item

public function chmod_item()
    public function chmod_item()
    {
        if ( ! $this->_sandbox_user_ok()) {
            return $this->_sandbox_deny();
        }
        $dir_name = urldecode($_POST['dir_name']);
        if ($this->SANDBOX && ($dir_name = $this->_confine($dir_name)) === false) {
            return $this->_sandbox_deny();
        }
        if ( ! $_POST['new_chmod']) {
            $_POST['new_chmod'] = '755';
        }
        $_old_dir_name = str_replace('\\', '/', getcwd());
        chdir($dir_name);
        foreach ((array) $_POST as $k => $v) {
            $tmp = substr($k, 0, 2);
            if ($tmp != 'd_' && $tmp != 'f_') {
                continue;
            }
            $file_name = str_replace('\\', '/', $dir_name . '/' . $v);
            if ($this->SANDBOX && $this->_sandbox_file($file_name) === '') {
                continue;
            }
            chmod($file_name, '0' . $_POST['new_chmod']);
        }
        chdir($_old_dir_name);
        return js_redirect('./?object=' . ($_GET['object'] ?? null) . '&dir_name=' . ($_POST['dir_name'] ?? null) . _add_get(['dir_name']));
    }

copy_item

public function copy_item()
    public function copy_item()
    {
        if ( ! $this->_sandbox_user_ok()) {
            return $this->_sandbox_deny();
        }
        $dir_name = urldecode($_POST['dir_name']);
        if ($this->SANDBOX && ($dir_name = $this->_confine($dir_name)) === false) {
            return $this->_sandbox_deny();
        }
        $items_to_copy = [];
        $items = null;
        foreach ((array) $_POST as $k => $v) {
            $tmp = substr($k, 0, 2);
            if ($tmp != 'd_' && $tmp != 'f_') {
                continue;
            }
            if ($tmp == 'd_') {
                $color = $this->color_dir;
                $type = strtoupper(t('directory'));
            } elseif ($tmp == 'f_') {
                $color = $this->color_file;
                $type = strtoupper(t('file'));
            }
            $file_name = str_replace('\\', '/', $dir_name . '/' . $v);
            if ($this->SANDBOX && ($file_name = $this->_confine($file_name)) === false) {
                continue;
            }
            $items_to_copy[] = $tmp . $file_name;
            $replace2 = [
                'color' => $color,
                'file_name' => $file_name,
                'type' => $type,
            ];
            $items .= tpl()->parse(($_GET['object'] ?? null) . '/copy_item', $replace2);
        }
        $replace = [
            'form_action' => './?object=' . ($_GET['object'] ?? null) . '&action=copy_item2&dir_name=' . ($_POST['dir_name'] ?? null) . _add_get(['dir_name']),
            'items' => $items,
            'items_to_copy' => urlencode($this->_pack_items($items_to_copy)),
            'dest_dirs' => $this->_get_all_dirs($this->SANDBOX && $this->_confine(dirname($dir_name)) === false ? $dir_name : dirname($dir_name)),
            'back' => back('./?object=' . $_GET['object'] . '&dir_name=' . $_POST['dir_name'] . _add_get(['dir_name'])),
        ];
        return tpl()->parse(($_GET['object'] ?? null) . '/copy', $replace);
    }

copy_item2

public function copy_item2()

Copy selected items into selected destination folders.

    public function copy_item2()
    {
        if ( ! $this->_sandbox_user_ok()) {
            return $this->_sandbox_deny();
        }
        $body = null;
        if ($_POST['items_to_copy']) {
            $items = $this->_unpack_items(urldecode($_POST['items_to_copy']));
            $items_to_copy = null;
            $dest_folders = null;
            foreach ((array) $items as $k => $v) {
                $tmp = substr($v, 0, 2);
                if ($tmp == 'd_' || $tmp == 'f_') {
                    $path = substr($v, 2);
                    if ($this->SANDBOX && ($path = $this->_confine($path)) === false) {
                        continue;
                    }
                    $items_to_copy[] = $path;
                }
            }
            foreach ((array) $_POST as $k => $v) {
                $tmp = substr($k, 0, 2);
                if ($tmp == 'd_' || $tmp == 'f_') {
                    $dest = urldecode(substr($k, 2));
                    if ($this->SANDBOX && ($dest = $this->_confine($dest)) === false) {
                        continue;
                    }
                    $dest_folders[] = $dest;
                }
            }
            if (count((array) $items_to_copy)) {
                if (count((array) $dest_folders)) {
                    foreach ((array) $items_to_copy as $k => $v) {
                        if ($this->SANDBOX && (is_link($v) || $this->_is_denied_name($v) || (is_dir($v) && $this->_dir_has_unsafe($v)))) {
                            continue;
                        }
                        if (is_dir($v)) {
                            foreach ((array) $dest_folders as $k1 => $v1) {
                                // Check if dirs are different
                                // !! Need to add checking of parentess of dirs (to avoid infinite copy recursion)
                                if ($v != $v1) {
                                    $new_name = $v1 . '/' . basename($v);
                                    if ($this->SANDBOX && is_link($new_name)) {
                                        continue;
                                    }
                                    mkdir($new_name, 0777);
                                    _class('dir')->copy_dir($v, $new_name);
                                }
                            }
                        } else {
                            foreach ((array) $dest_folders as $k1 => $v1) {
                                if ($this->SANDBOX && is_link($v1 . '/' . basename($v))) {
                                    continue;
                                }
                                copy($v, $v1 . '/' . basename($v));
                            }
                        }
                    }
                    js_redirect('./?object=' . ($_GET['object'] ?? null) . '&dir_name=' . ($_GET['dir_name'] ?? null) . _add_get(['dir_name']));
                } else {
                    $body .= t('please_select_destinatin_folders');
                }
            } else {
                $body .= t('please_select_items_to_copy');
            }
        } else {
            $body .= t('please_select_items_to_copy');
        }

        $body .= back('./?object=' . ($_GET['object'] ?? null) . '&dir_name=' . ($_GET['dir_name'] ?? null));

        return $body;
    }

delete_item

public function delete_item()
    public function delete_item()
    {
        if ( ! $this->_sandbox_user_ok()) {
            return $this->_sandbox_deny();
        }
        $dir_name = urldecode($_POST['dir_name']);
        if ($this->SANDBOX && ($dir_name = $this->_confine($dir_name)) === false) {
            return $this->_sandbox_deny();
        }
        foreach ((array) $_POST as $k => $v) {
            $file_name = str_replace('\\', '/', $dir_name . '/' . $v);

            $tmp = substr($k, 0, 2);
            if ($tmp != 'd_' && $tmp != 'f_') {
                continue;
            }
            if ( ! file_exists($file_name)) {
                continue;
            }
            if ($this->SANDBOX && (is_link($file_name) || ($file_name = $this->_confine($file_name)) === false || in_array($file_name, $this->_allowed_dirs(), true) || (is_dir($file_name) && $this->_dir_has_unsafe($file_name)))) {
                continue;
            }
            if (is_dir($file_name)) {
                _class('dir')->delete_dir($file_name);
                @closedir($file_name);
                @rmdir($file_name);
            } else {
                @unlink($file_name);
            }
        }
        return js_redirect('./?object=' . ($_GET['object'] ?? null) . '&dir_name=' . ($_POST['dir_name'] ?? null) . _add_get(['dir_name']));
    }

download_item

public function download_item()
    public function download_item()
    {
        if ( ! $this->_sandbox_user_ok()) {
            return $this->_sandbox_deny();
        }
        $dir_name = urldecode($_POST['dir_name']);
        if ($this->SANDBOX && ($dir_name = $this->_confine($dir_name)) === false) {
            return $this->_sandbox_deny();
        }

        $_old_dir_name = str_replace('\\', '/', getcwd());
        chdir($dir_name);

        foreach ((array) $_POST as $k => $v) {
            $file_name = str_replace('\\', '/', $dir_name . '/' . $v);

            $tmp = substr($k, 0, 2);
            if ($tmp != 'd_' && $tmp != 'f_') {
                continue;
            }
            $this->SANDBOX && $file_name = (string) $this->_confine($file_name);
            if (is_readable($file_name) && is_file($file_name)) {
                clearstatcache();
                header('Content-Type: application/force-download; name="' . $v . '"');
                header('Content-Type: text/plain');
                header('Content-Transfer-Encoding: binary');
                header('Content-Length: ' . filesize($file_name));
                header('Content-Disposition: attachment; filename="' . $v . '"');
                readfile($file_name);
                main()->NO_GRAPHICS = true;
                exit();
            }
            chdir($_old_dir_name);
            return js_redirect('./?object=' . ($_GET['object'] ?? null) . '&dir_name=' . ($_POST['dir_name'] ?? null) . _add_get(['dir_name']));
        }
    }

edit

public function edit()
    public function edit()
    {
        return $this->edit_item();
    }

edit_item

public function edit_item()
    public function edit_item()
    {
        if ( ! $this->_sandbox_user_ok()) {
            return $this->_sandbox_deny();
        }
        // TODO: save file revision to db on each save
        if ( ! empty($_GET['id'])) {
            $file_name = urldecode(($_GET['id'] ?? ''));
            $file_path = $file_name;
            $dir_name = dirname($file_path);
        } else {
            $name = null;
            foreach ((array) $_REQUEST as $k => $v) {
                $tmp = substr($k, 0, 2);
                if ($tmp == 'd_' || $tmp == 'f_') {
                    $name = $v;
                    break;
                }
            }
            $dir_name = urldecode(($_REQUEST['dir_name'] ?? ''));
            $file_name = str_replace('\\', '/', $dir_name . '/' . $name);
            $file_path = $file_name;
        }
        if ($this->SANDBOX) {
            if (($file_path = $this->_confine($file_path)) === false) {
                return $this->_sandbox_deny();
            }
            $file_name = $file_path;
        }
        if (main()->is_post()) {
            $file_name = urldecode($_GET['file_name']);
            if ($this->SANDBOX && ($file_name = $this->_sandbox_file($file_name)) === '') {
                return $this->_sandbox_deny();
            }
            file_put_contents($file_name, $_POST['file_text_hidden']);
            return js_redirect('/@object/show/' . urlencode($_GET['dir_name']));
        }
        $_tmp_array = [];
        $tmp_path = '/';
        foreach ((array) explode('/', dirname($file_name)) as $_folder) {
            if ($_folder) {
                $tmp_path .= $_folder . '/';
                $_tmp_array[] = a('/@object/show/' . urlencode($tmp_path), $_folder);
            }
        }
        if ($_tmp_array) {
            $file_name = '/' . implode('/', $_tmp_array) . '/' . _prepare_html(basename($file_name), 0);
        }
        $file_text = _prepare_html(file_get_contents($file_path), 0);
        $replace = [
            'form_action' => url('/@object/@action/' . urlencode($file_path)),
            'back_link' => url('/@object/show/' . urlencode(($_REQUEST['dir_name'] ?? ''))),
        ];
        $div_id = 'editor_html';
        $hidden_id = 'file_text_hidden';
        $ace_mode = common()->get_file_ext($file_path);
        $ace_mode == 'stpl' && $ace_mode = 'html';

        jquery('
			var h = $(window).height() - $(".navbar").height() - $("h4").height() * 2 - $("[type=submit]").height() * 5
			$("#' . $div_id . '").height(h);
		');
        return '<h4>Edit: ' . $file_name . '</h4>' .
            form($replace, [
                'data-onsubmit' => '$(this).find("#' . $hidden_id . '").val( $("#' . $div_id . '").data("ace_editor").session.getValue() );',
            ])
            ->container('<div id="' . $div_id . '" style="width: 100%; min-height: 500px;">' . $file_text . '</div>', '', [
                'id' => $div_id,
                'wide' => 1,
                'ace_editor' => ['mode' => $ace_mode, 'hScrollBarAlwaysVisible' => false, 'vScrollBarAlwaysVisible' => false],
            ])
            ->hidden($hidden_id)
            ->save_and_back();
    }

email_item

public function email_item()
    public function email_item()
    {
        if ( ! $this->_sandbox_user_ok()) {
            return $this->_sandbox_deny();
        }
        $dir_name = urldecode($_POST['dir_name']);
        if ($this->SANDBOX && ($dir_name = $this->_confine($dir_name)) === false) {
            return $this->_sandbox_deny();
        }

        $_old_dir_name = str_replace('\\', '/', getcwd());
        chdir($dir_name);

        $attach = [];
        foreach ((array) $_POST as $k => $v) {
            $file_name = str_replace('\\', '/', $dir_name . '/' . $v);

            $tmp = substr($k, 0, 2);
            if ($tmp != 'd_' && $tmp != 'f_') {
                continue;
            }
            $this->SANDBOX && $file_name = (string) $this->_confine($file_name);
            if (file_exists($file_name) && is_readable($file_name)) {
                $attach[] = $v;
            }
        }
        if (count((array) $attach)) {
            $email_from = 'yfix.dev auto-sender';
            if ( ! ($_POST['target_email'] ?? null)) {
                $email_to = '[email protected]';
            } else {
                $email_to = ($_POST['target_email'] ?? null);
            }
            $to_name = 'auto-sender destination';
            $subject = 'yfix.dev auto-sender generated email';
            $HTML = 'see attachment';
            $TEXT = 'see attachment';
            $result = common()->send_mail('PHP-Mailer', $email_from, $email_to, $to_name, $subject, $TEXT, $HTML, $attach);
        }
        chdir($_old_dir_name);
        return js_redirect('./?object=' . ($_GET['object'] ?? null) . '&dir_name=' . ($_POST['dir_name'] ?? null) . _add_get(['dir_name']));
    }

make_dir

public function make_dir()
    public function make_dir()
    {
        if ( ! $this->_sandbox_user_ok()) {
            return $this->_sandbox_deny();
        }
        $body = null;
        if ($_POST['verify'] && $_POST['name']) {
            $new_dir = urldecode($_POST['dir_name']) . '/' . $_POST['name'];
            $this->SANDBOX && $new_dir = $this->_sandbox_file($new_dir, false);
            if ($new_dir !== '' && ! @file_exists($new_dir)) {
                mkdir($new_dir, 0777);
            }
        }
        $body .= $this->show();
        return $body;
    }

make_zip_item

public function make_zip_item()
    public function make_zip_item()
    {
        if ( ! $this->_sandbox_user_ok()) {
            return $this->_sandbox_deny();
        }
        $dir_name = urldecode($_POST['dir_name']);
        if ($this->SANDBOX && ($dir_name = $this->_confine($dir_name)) === false) {
            return $this->_sandbox_deny();
        }
        if ( ! $_POST['new_zip_name']) {
            $_POST['new_zip_name'] = 'tmp_1234';
        }
        $new_zip_name = $dir_name . '/' . ($_POST['new_zip_name'] ?? null) . '.zip';

        $_old_dir_name = str_replace('\\', '/', getcwd());
        chdir($dir_name);

        $item_list = [];
        foreach ((array) $_POST as $k => $v) {
            $tmp = substr($k, 0, 2);
            if ($tmp != 'd_' && $tmp != 'f_') {
                continue;
            }
            $file_name = str_replace('\\', '/', $dir_name . '/' . $v);
            if ($this->SANDBOX && $this->_confine($file_name) === false) {
                continue;
            }
            if (file_exists($file_name) && is_readable($file_name)) {
                $item_list[] = $v;
            }
        }
        $_old_dir_name = str_replace('\\', '/', getcwd());
        chdir($_old_dir_name);
        // TODO: replace making ZIP with php built-in http://php.net/manual/en/zip.examples.php
        return js_redirect('./?object=' . $_GET['object'] . '&dir_name=' . $_POST['dir_name'] . _add_get(['dir_name']));
    }

show

public function show()
    public function show()
    {
        $items = $total_files_size = null;
        // TODO: show preview if file is image
        // TODO: doubleclick should edit file
        // TODO: buttons near eacj table row (edit/show revisions)
        // TODO: show revisions
        // TODO: make table condensed
        // TODO: completely avoid tpl(): speedup, code in one place
        // TODO: overall cleanup and improve UI from bootstrap
        // TODO: left area show directory tree with ajax
        // TODO: ajax file/image upload
        // TODO: wanr if we cannot edit this file _before_ edit (inside table listing)
        // TODO: rewrite JS into angularjs for UI interanction
        // TODO: copy_item rewrite to use infinite recursion
        // TODO: add merge dirs with dialog for new name
        // TODO: test on large number of files inside dir
        // TODO: add ability to allow some paths for specific admiin groups (fine grained internal permissions or ACL)
        // TODO: add ajax calculate dirs size (one-by-one to not kill server performance)
        // TODO: add filesystem stats (disks usage, mounted filesystems, iostat if possible, etc)
        if ( ! $this->_sandbox_user_ok()) {
            return $this->_sandbox_deny();
        }
        $path_info = pathinfo($_SERVER['SCRIPT_FILENAME']);
        $_old_dir_name = str_replace('\\', '/', getcwd());
        $_dir_name = ($_GET['dir_name'] ?? null) ?: ($_GET['id'] ?? null);
        if ($_dir_name) {
            $dir_name = urldecode($_dir_name);
            if ($this->SANDBOX && ($dir_name = $this->_confine($dir_name)) === false) {
                return $this->_sandbox_deny();
            }
            chdir($dir_name);
        } else {
            $dir_name = $path_info['dirname'];
            if ($this->SANDBOX) {
                if (($dir_name = $this->_confine($dir_name) ?: current($this->_allowed_dirs())) === false) {
                    return $this->_sandbox_deny();
                }
                chdir($dir_name);
            }
        }
        $path_info_up = pathinfo($dir_name);
        $cur_dir_name = str_replace('\\', '/', getcwd());
        $encoded_cur_dir = urlencode($cur_dir_name);
        $dir_contents = $this->_get_dir_contents($cur_dir_name);
        chdir($_old_dir_name);

        $items = null;
        $total_files_size = null;
        if ( ! empty($dir_contents['dirs']) || ! empty($dir_contents['files'])) {
            foreach ((array) $dir_contents['dirs'] as $k => $v) {
                $replace2 = [
                    'encoded_name' => urlencode($v),
                    'name' => $v,
                    'go_into_link' => './?object=' . ($_GET['object'] ?? null) . '&dir_name=' . urlencode($dir_name . '/' . $v) . _add_get(['dir_name']),
                    'color' => $this->color_dir,
                    'm_date' => date('Y-m-d H:i:s', filemtime($cur_dir_name . '/' . $v)),
                    'perms' => $this->_get_perms($cur_dir_name . '/' . $v),
                    'size' => 0,
                    'is_dir' => 1,
                    'is_file' => 0,
                ];
                $items .= tpl()->parse(($_GET['object'] ?? null) . '/item', $replace2);
            }
            foreach ((array) $dir_contents['files'] as $k => $v) {
                if (substr($v, -3) == 'zip') {
                    $color = $this->color_zip;
                } else {
                    $color = $this->color_file;
                }
                $file_size = filesize($cur_dir_name . '/' . $v);
                $total_files_size += $file_size;
                $replace2 = [
                    'encoded_name' => urlencode($v),
                    'name' => $v,
                    'go_into_link' => '',
                    'color' => $color,
                    'm_date' => date('Y-m-d H:i:s', filemtime($cur_dir_name . '/' . $v)),
                    'perms' => $this->_get_perms($cur_dir_name . '/' . $v),
                    'size' => $file_size,
                    'is_dir' => 0,
                    'is_file' => 1,
                ];
                $items .= tpl()->parse(($_GET['object'] ?? null) . '/item', $replace2);
            }
        }
        clearstatcache();

        $_tmp_path = '';
        $_tmp_array = [];
        foreach ((array) explode('/', $cur_dir_name) as $_folder) {
            $_tmp_path .= $_folder . '/';
            $_tmp_array[] = '<a href="./?object=' . ($_GET['object'] ?? null) . '&dir_name=' . urlencode($_tmp_path) . _add_get(['dir_name']) . '" class="btn btn-mini btn-xs">' . _prepare_html($_folder) . '</a>';
        }
        if ($_tmp_array) {
            $cur_dir_name = implode('/', $_tmp_array);
        }
        $replace = [
            'form_action' => './?object=' . ($_GET['object'] ?? null) . _add_get(['dir_name']) . '&action=',
            'upload_form_action' => './?object=' . ($_GET['object'] ?? null) . '&action=upload_file&dir_name=' . $encoded_cur_dir . _add_get(['dir_name']),
            'mkdir_form_action' => './?object=' . ($_GET['object'] ?? null) . '&action=make_dir&dir_name=' . $encoded_cur_dir . _add_get(['dir_name']),
            'cur_dir_name' => $cur_dir_name,
            'encoded_dir_name' => $encoded_cur_dir,
            'go_up_level_link' => './?object=' . ($_GET['object'] ?? null) . '&dir_name=' . urlencode($path_info_up['dirname']) . _add_get(['dir_name']),
            'go_home_link' => './?object=' . ($_GET['object'] ?? null) . _add_get(['dir_name']),
            'total_files_size' => (int) $total_files_size,
            'total_files' => (int) (count((array) $dir_contents['files'])),
            'total_dirs' => (int) (count((array) $dir_contents['dirs'])),
            'default_zip_name' => 'tmp12345',
            'default_email' => conf('webmaster_mail'),
            'default_chmod' => 755,
            'items' => $items,
        ];
        return tpl()->parse(($_GET['object'] ?? null) . '/main', $replace);
    }

unzip_item

public function unzip_item()
    public function unzip_item()
    {
        if ( ! $this->_sandbox_user_ok()) {
            return $this->_sandbox_deny();
        }
        $dir_name = urldecode($_POST['dir_name']);
        if ($this->SANDBOX && ($dir_name = $this->_confine($dir_name)) === false) {
            return $this->_sandbox_deny();
        }
        $_old_dir_name = str_replace('\\', '/', getcwd());
        foreach ((array) $_POST as $k => $name) {
            $tmp = substr($k, 0, 2);
            if ($tmp != 'd_' && $tmp != 'f_') {
                continue;
            }
            $ext = array_pop(explode('.', $name));
            if ($ext != 'zip') {
                continue;
            }
            $file_name = str_replace('\\', '/', $dir_name . '/' . $name);
            $extraction_dir = $dir_name . '/' . substr($name, 0, -4);
            if ($this->SANDBOX && ($this->_confine($file_name) === false || $this->_sandbox_file($extraction_dir, false) === '')) {
                continue;
            }
            if ( ! file_exists($extraction_dir)) {
                mkdir($extraction_dir, 0777);
            }
            chdir($extraction_dir);
            // TODO: check or replace pclzip
        }
        chdir($_old_dir_name);
        return js_redirect('./?object=' . ($_GET['object'] ?? null) . '&dir_name=' . ($_POST['dir_name'] ?? null) . _add_get(['dir_name']));
    }

upload_file

public function upload_file()
    public function upload_file()
    {
        if ( ! $this->_sandbox_user_ok()) {
            return $this->_sandbox_deny();
        }
        $body = null;
        if (($_POST['verify'] ?? null)) {
            if (($_POST['name'] ?? null)) {
                $Name = ($_POST['name'] ?? null);
            } else {
                $Name = ($_FILES['file']['name'] ?? null);
            }
            $fileName = urldecode($_POST['dir_name']) . '/' . $Name;
            $this->SANDBOX && $fileName = $this->_sandbox_file($fileName, false);
            if ($fileName === '' || ! move_uploaded_file($_FILES['file']['tmp_name'], $fileName)) {
                $body .= error_back();
            } else {
                $body .= t('save_successful') . '<br />' . PHP_EOL;
            }
        }
        $body .= $this->show();
        return $body;
    }

view

public function view()
    public function view()
    {
        return $this->view_item();
    }

view_item

public function view_item()
    public function view_item()
    {
        if ( ! $this->_sandbox_user_ok()) {
            return $this->_sandbox_deny();
        }
        if ( ! empty($_GET['id'])) {
            $file_name = urldecode(($_GET['id'] ?? ''));
            $file_path = $file_name;
            $dir_name = dirname($file_path);
        } else {
            $name = null;
            foreach ((array) $_REQUEST as $k => $v) {
                $tmp = substr($k, 0, 2);
                if ($tmp == 'd_' || $tmp == 'f_') {
                    $name = $v;
                    break;
                }
            }
            $dir_name = urldecode(($_REQUEST['dir_name'] ?? ''));
            $file_name = str_replace('\\', '/', $dir_name . '/' . $name);
            $file_path = $file_name;
        }
        if ($this->SANDBOX) {
            if (($file_path = $this->_confine($file_path)) === false) {
                return $this->_sandbox_deny();
            }
            $file_name = $file_path;
        }
        $_tmp_array = [];
        $tmp_path = '/';
        foreach ((array) explode('/', dirname($file_name)) as $_folder) {
            if ($_folder) {
                $tmp_path .= $_folder . '/';
                $_tmp_array[] = a('/@object/show/' . urlencode($tmp_path), $_folder);
            }
        }
        if ($_tmp_array) {
            $file_name = '/' . implode('/', $_tmp_array) . '/' . _prepare_html(basename($file_name), 0);
        }
        $file_text = _prepare_html(file_get_contents($file_path), 0);
        $replace = [
            'back_link' => url('/@object/show/' . urlencode(($_REQUEST['dir_name'] ?? ''))),
        ];
        $div_id = 'editor_html';
        $hidden_id = 'file_text_hidden';
        return '<h4>View: ' . $file_name . '</h4>' .
            form($replace)
            ->container('<div id="' . $div_id . '" style="width: 90%; height: 500px;">' . $file_text . '</div>', '', [
                'id' => $div_id,
                'wide' => 1,
                'ace_editor' => ['mode' => common()->get_file_ext($file_path)],
            ])
            ->hidden($hidden_id);
    }

Methods starting with _ hooks and helpers; not routable as page actions

_allowed_dirs

public function _allowed_dirs()

Returns array Real paths of the directories the sandbox allows

    public function _allowed_dirs()
    {
        $dirs = (array) $this->ALLOWED_DIRS;
        if ( ! $dirs) {
            defined('PROJECT_PATH') && $dirs[] = PROJECT_PATH;
            defined('UPLOADS_PATH') && $dirs[] = UPLOADS_PATH;
        }
        $result = [];
        foreach ($dirs as $dir) {
            $real = realpath($dir);
            $real !== false && $result[] = str_replace('\\', '/', $real);
        }
        return $result;
    }

_confine

public function _confine($path, $must_exist = true)

Same as _real() but returns false instead of throwing; a path that does not exist yet is confined by its parent dir.

    public function _confine($path, $must_exist = true)
    {
        try {
            if ($must_exist) {
                return $this->_real($path);
            }
            $path = rtrim(str_replace('\\', '/', (string) $path), '/');
            $base = basename($path);
            if ($base === '' || $base === '.' || $base === '..') {
                return false;
            }
            $new = rtrim($this->_real(dirname($path)), '/') . '/' . $base;
            return (file_exists($new) || is_link($new)) ? $this->_real($new) : $new;
        } catch (Throwable $e) {
            return false;
        }
    }
public function _dir_has_unsafe($dir)

Sandbox only: true when a directory holds a denied name or a symlink (recursive copy and delete follow links), or cannot be read.

    public function _dir_has_unsafe($dir)
    {
        try {
            $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);
            foreach ($it as $file) {
                if ($file->isLink() || $this->_is_denied_name($file->getFilename())) {
                    return true;
                }
            }
        } catch (Throwable $e) {
            return true;
        }
        return false;
    }

_get_all_dirs

public function _get_all_dirs($dir_name = '', $level = 1)

Show multi-level tree with folders inside.

    public function _get_all_dirs($dir_name = '', $level = 1)
    {
        $body = null;
        if ( ! file_exists($dir_name)) {
            return;
        }
        $_old_dir_name = str_replace('\\', '/', getcwd());
        chdir($dir_name);
        $dir_contents = $this->_get_dir_contents($dir_name);
        chdir($_old_dir_name);

        $body = null;
        foreach ((array) $dir_contents['dirs'] as $cur_name) {
            $dir_next = $dir_name . '/' . $cur_name;
            $replace = [
                'color' => $this->color_dir,
                'name' => $cur_name,
                'encoded_name' => urlencode($dir_next),
                'padding' => ($level - 1) * 30,
            ];
            $body .= tpl()->parse(($_GET['object'] ?? null) . '/copy_dir_item', $replace);
            if ($level < $this->_copy_dir_deepness) {
                $body .= $this->_get_all_dirs($dir_next, $level + 1);
            }
        }
        return $body;
    }
public function _get_dir_contents($abs_dir_name)

Return sorted directory contents.

    public function _get_dir_contents($abs_dir_name)
    {
        $contents = [
            'dirs' => [],
            'files' => [],
        ];
        $handle = opendir($abs_dir_name);
        while (false !== ($tmp_file = readdir($handle))) {
            if ($tmp_file == '.' || $tmp_file == '..') {
                continue;
            }
            if ( ! empty($this->_include_pattern)) {
                if ( ! preg_match($this->_include_pattern . 'ims', $tmp_file)) {
                    continue;
                }
            }
            if ( ! empty($this->_exclude_pattern)) {
                if (preg_match($this->_exclude_pattern . 'ims', $tmp_file)) {
                    continue;
                }
            }
            if (is_dir($tmp_file)) {
                $contents['dirs'][] = $tmp_file;
            } elseif (is_file($tmp_file)) {
                $contents['files'][] = $tmp_file;
            }
        }
        natsort($contents['dirs']);
        natsort($contents['files']);
        return $contents;
    }

_get_perms

public function _get_perms($file_name)
    public function _get_perms($file_name)
    {
        return substr(sprintf('%o', fileperms($file_name)), -3);
    }
public function _is_denied_name($name)
    public function _is_denied_name($name)
    {
        $name = rtrim(basename((string) $name), ". \t");
        if (strpos($name, "\0") !== false) {
            return true;
        }
        $parts = explode('.', strtolower($name));
        array_shift($parts);
        return (bool) array_intersect($parts, array_map('strtolower', (array) $this->DENIED_EXTENSIONS));
    }

_pack_items

public function _pack_items($items)

Items selected for copying travel through a hidden form field; base64 keeps any byte sequence in file names.

    public function _pack_items($items)
    {
        return json_encode(array_map('base64_encode', array_values((array) $items)));
    }

_real

public function _real($path)

Real path of an existing path, throws when it is missing or outside of the allowed dirs.

    public function _real($path)
    {
        if ( ! is_string($path) || ! strlen($path) || strpos($path, "\0") !== false) {
            throw new Exception('Bad path');
        }
        $temp = realpath($path);
        if ($temp === false) {
            throw new Exception('Path does not exist: ' . $path);
        }
        $temp = str_replace('\\', '/', $temp);
        foreach ($this->_allowed_dirs() as $dir) {
            if ($temp === $dir || strpos($temp, rtrim($dir, '/') . '/') === 0) {
                return $temp;
            }
        }
        throw new Exception('Path is not inside allowed dirs: ' . $temp);
    }

_unpack_items

public function _unpack_items($packed)
    public function _unpack_items($packed)
    {
        $items = json_decode((string) $packed, true);
        if ( ! is_array($items)) {
            return [];
        }
        $result = [];
        foreach ($items as $item) {
            is_string($item) && $result[] = base64_decode($item, true);
        }
        return array_filter($result, 'is_string');
    }