IndexController.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • sites/
  • all/
  • modules/
  • ide/
  • controllers/
  • IndexController.php
  • View
  • Commits
  • Open Download .zip Download (13 KB)
<?php
/**
 * Simple back-end for the IDE editor.
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 * @todo
 *              safe mode for broken stuff
 *              move/rename file
 *              style to better match chronicle look and feel
 *              move via drag and drop
 *              enter-key confirm dialogs
 *              enforce csrf protection
 *              only load ace js for editor requests.
 *              better error messages
 */
class Ide_IndexController extends Zend_Controller_Action
{
    public      $contexts       = array(
        'files'     => array('json'),
        'paths'     => array('json'),
        'copy'      => array('json'),
        'package'   => array('json')
    );

    protected   $_hiddenFiles   = array(
        '.DS_Store',
        '.placeholder'
    );

    /**
     * Enforce permissions.
     */
    public function init()
    {
        $this->getHelper('acl')->check('system', 'ide');
        $this->getHelper('layout')->disableLayout();
    }

    /**
     * Render the file editor (dijit).
     */
    public function indexAction()
    {
        $this->getHelper('layout')->setLayout('editor-layout');
        $this->view->headTitle()->set('IDE');

        // ace scripts are stored here instead of the .ini file so they are only loaded on the IDE page.
        $aceScripts = array(
            // main ace script
            "ace-uncompressed.js",

            // themes
            "theme-chrome.js", "theme-clouds.js", "theme-cobalt.js", "theme-crimson_editor.js",
            "theme-dawn.js", "theme-eclipse.js", "theme-idle_fingers.js", "theme-kr_theme.js",
            "theme-merbivore.js", "theme-merbivore_soft.js", "theme-mono_industrial.js",
            "theme-monokai.js", "theme-pastel_on_dark.js", "theme-solarized_dark.js",
            "theme-solarized_light.js", "theme-textmate.js", "theme-twilight.js", "theme-tomorrow.js",
            "theme-tomorrow_night.js", "theme-tomorrow_night_blue.js", "theme-tomorrow_night_bright.js",
            "theme-tomorrow_night_eighties.js", "theme-vibrant_ink.js",

            // syntax highlighting modes
            "mode-css.js", "mode-html.js", "mode-javascript.js", "mode-json.js", "mode-php.js",
            "mode-xml.js"
        );

        $module = P4Cms_Module::fetch('ide');
        foreach ($aceScripts as $script) {
            $this->view->headScript()->appendFile($module->getBaseUrl() . '/ace/' . $script);
        }
    }

    /**
     * Support reading and writing of server files.
     *  - On get, reads file and presents file contents
     *  - On post, writes file and presents bytes written
     *  - On delete, removes file and presents 'true' or 'false' for success/failure.
     */
    public function filesAction()
    {
        $view     = $this->view;
        $request  = $this->getRequest();
        $file     = $this->_resolvePath($request->getParam('file'));
        $basename = basename($request->getParam('file'));

        // if user has modified a package file, clear package cache.
        if ($request->isDelete() || $request->isPost()) {
            if ($basename === 'theme.ini') {
                P4Cms_Theme::clearCache();
            }
            if ($basename === 'module.ini') {
                P4Cms_Module::clearCache();
            }
        }

        // delete request method implies unlink.
        if ($request->isDelete()) {
            // attempt to make file writable.
            if (!is_writable($file)) {
                @chmod($file, 0755);
            }

            // present true/false.
            $view->data = @unlink($file);
            return;
        }

        // post request method implies writing.
        if ($request->isPost()) {
            // if file did not resolve, create a new file if the path exists.
            if (!$file) {
                $path = $this->_resolvePath(dirname($request->getParam('file')));
                $file = $path . "/" . $basename;
                @touch($file);
            }

            // attempt to make file writable.
            if (!is_writable($file)) {
                @chmod($file, 0755);
            }

            // present bytes written.
            // if file content was uploaded, move the temp file into place.
            // otherwise, write contents of 'data' request param to the file.
            if (isset($_FILES['data']['tmp_name'])) {
                $result     = @move_uploaded_file($_FILES['data']['tmp_name'], $file);
                $view->data = $result ? $_FILES['data']['size'] : false;
            } else {
                $view->data = @file_put_contents($file, $request->getParam('data'));
            }

            return;
        }

        $view->file = $file;
    }

    /**
     * Support listing directory contents.
     *
     * Response format is suitable for consumption by a dijit.Tree using
     * the ForestStoreModel and JsonRestStore.
     */
    public function pathsAction()
    {
        $this->contextSwitch->initContext('json');

        $view       = $this->view;
        $request    = $this->getRequest();

        // extract path parameter.
        $path       = $request->getParam('path');
        $root       = $this->_getRootPath();
        $isRoot     = $path == 'root';

        // delete request method implies recursive unlink.
        if ($request->isDelete()) {
            // if path fails to resolve, it must not exist.
            $path = $this->_resolvePath($path);
            if (!$path) {
                throw new Exception(
                    "Cannot delete '$path'. Folder doesn't exist."
                );
            }

            // present true/false.
            $view->data = P4Cms_FileUtility::deleteRecursive($path);
            return;
        }

        // if a path was posted, attempt to create it.
        if ($request->getPost('path') && !$isRoot) {
            // strip leading/trailing slashes.
            $path = trim($path, '/');

            // if path resolves, it must exist already.
            if ($this->_resolvePath($path)) {
                throw new Exception(
                    "Cannot create '$path'. Path already exists."
                );
            }

            // verify parent folder resolves.
            $parent = $this->_resolvePath(dirname($path));
            if (!$parent) {
                throw new Exception(
                    "Cannot create '$path'. Containing folder doesn't exist."
                );
            }

            // attempt to make containing folder writable.
            if (!is_writable($parent)) {
                @chmod($parent, 0755);
            }

            // try to make it.
            $view->data = @mkdir($parent . '/' . basename($path), 0755);
            return;
        }

        // normalize path parameter.
        $path       = $isRoot ? $root : $this->_resolvePath($path);

        // collect entries in given path.
        $data       = array();
        $paths      = new DirectoryIterator($path);
        foreach ($paths as $entry) {
            if ($entry->isDot() || in_array($entry->getBasename(), $this->_hiddenFiles)) {
                continue;
            }

            $basename = $entry->getBasename();
            $pathname = str_replace($root . '/', '', $entry->getPathname());

            // if entry has children, only partially load it
            // (this uses dojo's JSON references (lazy-loading)
            if ($entry->isDir()) {
                $data[$basename] = array(
                    '$ref'      => $pathname,
                    'name'      => $basename,
                    'children'  => true
                );
            } else {
                $data[$basename] = array(
                    'id'        => $pathname,
                    'name'      => $basename,
                    'type'      => P4Cms_FileUtility::getMimeType($entry->getPathname())
                );
            }
        }

        // ensure orderly results.
        uksort($data, 'strnatcasecmp');
        $data = array_values($data);

        if (!$isRoot) {
            $path = $this->getRequest()->getParam('path');
            $data = array(
                'id'        => $path,
                'name'      => basename($path),
                'children'  => $data
            );
        }

        $view->data = $data;
    }

    /**
     * Support copying a file or directory.
     */
    public function copyAction()
    {
        $this->contextSwitch->initContext('json');

        $view    = $this->view;
        $request = $this->getRequest();

        // extract/normalize source and target details.
        $source  = $this->_resolvePath($request->getParam('source'));
        $target  = trim($request->getParam('target'), '/');
        $parent  = $this->_resolvePath(dirname($target));

        // verify:
        //  - request was posted
        //  - source exists
        //  - target does not exist
        //  - target parent folder exists
        $error = "Cannot copy from $source to $target. ";
        if (!$request->isPost()) {
            throw new Exception($message . "Request method must be POST.");
        }
        if (!$source) {
            throw new Exception($message . "Source path does not exist.");
        }
        if (!$parent) {
            throw new Exception($message . "Parent of target path does not exist.");
        }
        if ($this->_resolvePath($target)) {
            throw new Exception($message . "Target path already exists.");
        }

        P4Cms_FileUtility::copyRecursive($source, $parent . '/' . basename($target));
    }

    /**
     * Create a new module or theme.
     */
    public function packageAction()
    {
        $this->contextSwitch->initContext('json');

        $view        = $this->view;
        $request     = $this->getRequest();
        $type        = $request->getParam('type');
        $label       = $request->getParam('name');
        $name        = strtolower(preg_replace('/[^a-z0-9]/i', '', $label));
        $namespace   = ucfirst($name);
        $description = $request->getParam('description');
        $tags        = $request->getParam('tags');
        $path        = $this->_getRootPath() . '/all/' . $type . 's/' . $name;

        // verify package type is valid and package does not already exist.
        $error = "Cannot create '$label' package. ";
        if ($type !== 'module' && $type !== 'theme') {
            throw new Exception($error . "Invalid package type specified.");
        }
        if (file_exists($path)) {
            throw new Exception($error . ucfirst($type) . " already exists.");
        }

        // copy the package template into place.
        P4Cms_FileUtility::copyRecursive(
            dirname(__DIR__) . '/templates/' . $type, $path
        );

        // provide 'package' macro for exclusive use by this action.
        // (it would not work as a general purpose macro).
        P4Cms_PubSub::subscribe('p4cms.macro.package',
            function($params, $body, $context) use ($label, $name, $namespace, $description, $tags)
            {
                $field = isset($params[0]) ? $params[0] : 'name';
                switch ($field) {
                    case 'label':
                        return $label;
                        break;
                    case 'name':
                        return $name;
                        break;
                    case 'namespace':
                        return $namespace;
                        break;
                    case 'description':
                        return $description;
                        break;
                    case 'tags':
                        return $tags;
                        break;
                    default:
                        return null;
                }
            }
        );

        // iterate over newly copied files and expand macros.
        $filter = new P4Cms_Filter_Macro;
        $files  = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator(
                $path,
                RecursiveDirectoryIterator::SKIP_DOTS
            ),
            RecursiveIteratorIterator::SELF_FIRST
        );
        foreach ($files as $file) {
            if (!$file->isDir()) {
                $contents = file_get_contents($file->getPathname());
                $contents = $filter->filter($contents);
                file_put_contents($file->getPathname(), $contents);
            }
        }

        // success!
        $view->data = true;
    }

    /**
     * Resolve given path to a location under the root path.
     *
     * @param   string          $path   the relative path to resolve.
     * @return  string|bool     the resolved path or false if the path cannot
     *                          be resolved to a location under the root path.
     */
    protected function _resolvePath($path)
    {
        $path = $this->_getRootPath() . '/' . trim($path, '/');
        $path = realpath($path);

        if (!$path) {
            return false;
        }

        $root = $this->_getRootPath();
        if (!$root || strpos($path, $root) !== 0) {
            return false;
        }

        return $path;
    }

    /**
     * Get the root path above which no files will be read/written.
     * Only files under the root path will be exposed via this controller.
     *
     * @return  string  the path files must be under to be exposed.
     */
    protected function _getRootPath()
    {
        return realpath(SITES_PATH);
    }
}
# Change User Description Committed
#1 16170 perforce_software Move Chronicle files to follow new path scheme for branching.
//guest/perforce_software/chronicle/sites/all/modules/ide/controllers/IndexController.php
#1 8972 Matt Attaway Initial add of the Chronicle source code