symphonycms/symphony-2

View on GitHub
symphony/lib/core/class.administration.php

Summary

Maintainability
D
2 days
Test Coverage
<?php

/**
 * @package core
 */

/**
 * The Administration class is an instance of Symphony that controls
 * all backend pages. These pages are HTMLPages are usually generated
 * using XMLElement before being rendered as HTML. These pages do not
 * use XSLT. The Administration is only accessible by logged in Authors
 */

class Administration extends Symphony
{
    /**
     * The path of the current page, ie. '/blueprints/sections/'
     * @var string
     */
    private $_currentPage  = null;

    /**
     * An associative array of the page's callback, including the keys
     * 'driver', which is a lowercase version of `$this->_currentPage`
     * with any slashes removed, 'classname', which is the name of the class
     * for this page, 'pageroot', which is the root page for the given page, (ie.
     * excluding /saved/, /created/ or any sub pages of the current page that are
     * handled using the _switchboard function.
     *
     * @see toolkit.AdministrationPage#__switchboard()
     * @var array|boolean
     */
    private $_callback = null;

    /**
     * The class representation of the current Symphony backend page,
     * which is a subclass of the `HTMLPage` class. Symphony uses a convention
     * of prefixing backend page classes with 'content'. ie. 'contentBlueprintsSections'
     * @var HTMLPage
     */
    public $Page;

    /**
     * Overrides the default Symphony constructor to add XSRF checking
     */
    protected function __construct()
    {
        parent::__construct();

        // Ensure the request is legitimate. RE: #1874
        if (self::isXSRFEnabled()) {
            XSRF::validateRequest();
        }
    }

    /**
     * This function returns an instance of the Administration
     * class. It is the only way to create a new Administration, as
     * it implements the Singleton interface
     *
     * @return Administration
     */
    public static function instance()
    {
        if (!(self::$_instance instanceof Administration)) {
            self::$_instance = new Administration;
        }

        return self::$_instance;
    }

    /**
     * Returns the current Page path, excluding the domain and Symphony path.
     *
     * @return string
     *  The path of the current page, ie. '/blueprints/sections/'
     */
    public function getCurrentPageURL()
    {
        return $this->_currentPage;
    }

    /**
     * Overrides the Symphony isLoggedIn function to allow Authors
     * to become logged into the backend when `$_REQUEST['auth-token']`
     * is present. This logs an Author in using the loginFromToken function.
     *
     * @uses loginFromToken()
     * @uses isLoggedIn()
     * @return boolean
     */
    public static function isLoggedIn()
    {
        if (isset($_REQUEST['auth-token']) && $_REQUEST['auth-token']) {
            return static::loginFromToken($_REQUEST['auth-token']);
        }

        return parent::isLoggedIn();
    }

    public static function login($username, $password, $isHash = false)
    {
        $loggedin = parent::login($username, $password, $isHash);
        if ($loggedin) {
            Lang::set(static::Author()->get('language'));
        }
        return $loggedin;
    }

    /**
     * Given the URL path of a Symphony backend page, this function will
     * attempt to resolve the URL to a Symphony content page in the backend
     * or a page provided by an extension. This function checks to ensure a user
     * is logged in, otherwise it will direct them to the login page
     *
     * @param string $page
     *  The URL path after the root of the Symphony installation, including a starting
     *  slash, such as '/login/'
     * @throws SymphonyException
     * @throws Exception
     * @return HTMLPage
     */
    private function __buildPage($page)
    {
        $is_logged_in = static::isLoggedIn();

        if (empty($page) || is_null($page)) {
            if (!$is_logged_in) {
                $page  = "/login";
            } else {
                // Will redirect an Author to their default area of the Backend
                // Integers are indicative of section's, text is treated as the path
                // to the page after `SYMPHONY_URL`
                $default_area = null;

                if (is_numeric(Symphony::Author()->get('default_area'))) {
                    $default_section = (new SectionManager)
                        ->select()
                        ->section(Symphony::Author()->get('default_area'))
                        ->execute()
                        ->next();

                    if ($default_section) {
                        $section_handle = $default_section->get('handle');
                    }

                    if (!$section_handle) {
                        $all_sections = (new SectionManager)->select()->execute()->rows();

                        if (!empty($all_sections)) {
                            $section_handle = $all_sections[0]->get('handle');
                        } else {
                            $section_handle = null;
                        }
                    }

                    if (!is_null($section_handle)) {
                        $default_area = "/publish/{$section_handle}/";
                    }
                } elseif (!is_null(Symphony::Author()->get('default_area'))) {
                    $default_area = preg_replace('/^' . preg_quote(SYMPHONY_URL, '/') . '/i', '', Symphony::Author()->get('default_area'));
                }

                // Fallback: No default area found
                if (is_null($default_area)) {
                    if (Symphony::Author()->isDeveloper()) {
                        // Redirect to the section index if author is a developer
                        redirect(SYMPHONY_URL . '/blueprints/sections/');
                    } else {
                        // Redirect to the author page if author is not a developer
                        redirect(SYMPHONY_URL . "/system/authors/edit/".Symphony::Author()->get('id')."/");
                    }
                } else {
                    redirect(SYMPHONY_URL . $default_area);
                }
            }
        }

        if (!$this->_callback = $this->getPageCallback($page)) {
            if ($page === '/publish/') {
                $sections = (new SectionManager)->select()->sort('sortorder')->execute()->rows();
                $section = current($sections);
                redirect(SYMPHONY_URL . '/publish/' . $section->get('handle') . '/');
            } else {
                $this->errorPageNotFound();
            }
        }

        require_once($this->_callback['driver_location']);
        $this->Page = new $this->_callback['classname'];

        if (!$is_logged_in && $this->_callback['driver'] !== 'login') {
            if (is_callable(array($this->Page, 'handleFailedAuthorisation'))) {
                $this->Page->handleFailedAuthorisation();
            } else {
                $this->Page = new contentLogin;

                // Include the query string for the login, RE: #2324
                if ($queryString = $this->Page->__buildQueryString(array('symphony-page', 'mode'), FILTER_SANITIZE_STRING)) {
                    $page .= '?' . $queryString;
                }
                $this->Page->build(array('redirect' => $page));
            }
        } else {
            if (!is_array($this->_callback['context'])) {
                $this->_callback['context'] = [];
            }

            if ($this->__canAccessAlerts()) {
                // Can the core be updated?
                $this->checkCoreForUpdates();
                // Do any extensions need updating?
                $this->checkExtensionsForUpdates();
            }

            $this->Page->build($this->_callback['context']);
        }

        return $this->Page;
    }

    /**
     * Scan the install directory to look for new migrations that can be applied
     * to update this version of Symphony. If one if found, a new Alert is added
     * to the page.
     *
     * @since Symphony 2.5.2
     * @return boolean
     *  Returns true if there is an update available, false otherwise.
     */
    public function checkCoreForUpdates()
    {
        // Is there even an install directory to check?
        if ($this->isInstallerAvailable() === false) {
            return false;
        }

        try {
            // The updater contains a version higher than the current Symphony version.
            if ($this->isUpgradeAvailable()) {
                $message = __('An update has been found in your installation to upgrade Symphony to %s.', array($this->getMigrationVersion())) . ' <a href="' . URL . '/install/">' . __('View update.') . '</a>';

                // The updater contains a version lower than the current Symphony version.
                // The updater is the same version as the current Symphony install.
            } else {
                $message = __('Your Symphony installation is up to date, but the installer was still detected. For security reasons, it should be removed.') . ' <a href="' . URL . '/install/?action=remove">' . __('Remove installer?') . '</a>';
            }

            // Can't detect update Symphony version
        } catch (Exception $e) {
            $message = __('An update script has been found in your installation.') . ' <a href="' . URL . '/install/">' . __('View update.') . '</a>';
        }

        $this->Page->pageAlert($message, Alert::NOTICE);

        return true;
    }

    /**
     * Checks all installed extensions to see any have an outstanding update. If any do
     * an Alert will be added to the current page directing the Author to the Extension page
     *
     * @since Symphony 2.5.2
     */
    public function checkExtensionsForUpdates()
    {
        $extensions = Symphony::ExtensionManager()->listInstalledHandles();

        if (is_array($extensions) && !empty($extensions)) {
            foreach ($extensions as $name) {
                $about = Symphony::ExtensionManager()->about($name);

                if (array_key_exists('status', $about) && in_array(Extension::EXTENSION_REQUIRES_UPDATE, $about['status'])) {
                    $this->Page->pageAlert(
                        __('An extension requires updating.') . ' <a href="' . SYMPHONY_URL . '/system/extensions/">' . __('View extensions') . '</a>'
                    );
                    break;
                }
            }
        }
    }

    /**
     * This function determines whether an administrative alert can be
     * displayed on the current page. It ensures that the page exists,
     * and the user is logged in and a developer
     *
     * @since Symphony 2.2
     * @return boolean
     */
    private function __canAccessAlerts()
    {
        if ($this->Page instanceof AdministrationPage && static::isLoggedIn() && static::Author()->isDeveloper()) {
            return true;
        }

        return false;
    }

    /**
     * This function resolves the string of the page to the relevant
     * backend page class. The path to the backend page is split on
     * the slashes and the resulting pieces used to determine if the page
     * is provided by an extension, is a section (index or entry creation)
     * or finally a standard Symphony content page. If no page driver can
     * be found, this function will return false.
     *
     * @uses AdminPagePostCallback
     * @param string $page
     *  The full path (including the domain) of the Symphony backend page
     * @return array|boolean
     *  If successful, this function will return an associative array that at the
     *  very least will return the page's classname, pageroot, driver, driver_location
     *  and context, otherwise this will return false.
     */
    public function getPageCallback($page = null)
    {
        if (!$page && $this->_callback) {
            return $this->_callback;
        } elseif (!$page && !$this->_callback) {
            $this->throwCustomError(__('Cannot request a page callback without first specifying the page.'));
        }

        $this->_currentPage = SYMPHONY_URL . preg_replace('/\/{2,}/', '/', $page);
        $bits = preg_split('/\//', trim($page, '/'), 3, PREG_SPLIT_NO_EMPTY);
        $callback = array(
            'driver' => null,
            'driver_location' => null,
            'context' => [],
            'classname' => null,
            'pageroot' => null
        );

        // Login page, /symphony/login/
        if ($bits[0] == 'login') {
            $callback['driver'] = 'login';
            $callback['driver_location'] = CONTENT . '/content.login.php';
            $callback['classname'] = 'contentLogin';
            $callback['pageroot'] = '/login/';

        // Extension page, /symphony/extension/{extension_name}/
        } elseif ($bits[0] == 'extension' && isset($bits[1])) {
            $extension_name = $bits[1];
            $bits = preg_split('/\//', trim($bits[2], '/'), 2, PREG_SPLIT_NO_EMPTY);

            // check if extension is enabled, if it's not, pretend the extension doesn't
            // even exist. #2367
            if (!ExtensionManager::isInstalled($extension_name)) {
                return false;
            }

            $callback['driver'] = 'index';
            $callback['classname'] = 'contentExtension' . ucfirst($extension_name) . 'Index';
            $callback['pageroot'] = '/extension/' . $extension_name. '/';
            $callback['extension'] = $extension_name;

            if (isset($bits[0])) {
                $callback['driver'] = $bits[0];
                $callback['classname'] = 'contentExtension' . ucfirst($extension_name) . ucfirst($bits[0]);
                $callback['pageroot'] .= $bits[0] . '/';
            }

            if (isset($bits[1])) {
                $callback['context'] = preg_split('/\//', $bits[1], -1, PREG_SPLIT_NO_EMPTY);
            }

            $callback['driver_location'] = EXTENSIONS . '/' . $extension_name . '/content/content.' . $callback['driver'] . '.php';
            // Extensions won't be part of the autoloader chain, so first try to require them if they are available.
            if (!is_file($callback['driver_location'])) {
                return false;
            } else {
                require_once $callback['driver_location'];
            }

        // Publish page, /symphony/publish/{section_handle}/
        } elseif ($bits[0] == 'publish') {
            if (!isset($bits[1])) {
                return false;
            }

            $callback['driver'] = 'publish';
            $callback['driver_location'] = CONTENT . '/content.publish.php';
            $callback['pageroot'] = '/' . $bits[0] . '/' . $bits[1] . '/';
            $callback['classname'] = 'contentPublish';

        // Everything else
        } else {
            $callback['driver'] = ucfirst($bits[0]);
            $callback['pageroot'] = '/' . $bits[0] . '/';

            if (isset($bits[1])) {
                $callback['driver'] = $callback['driver'] . ucfirst($bits[1]);
                $callback['pageroot'] .= $bits[1] . '/';
            }

            $callback['classname'] = 'content' . $callback['driver'];
            $callback['driver'] = strtolower($callback['driver']);
            $callback['driver_location'] = CONTENT . '/content.' . $callback['driver'] . '.php';
        }

        // Parse the context
        if (isset($callback['classname'])) {
            // Check if the class exists
            if (!class_exists($callback['classname'])) {
                $this->errorPageNotFound();
            }
            // Create the page
            $page = new $callback['classname'];

            // Named context
            if (method_exists($page, 'parseContext')) {
                $page->parseContext($callback['context'], $bits);

            // Default context
            } elseif (isset($bits[2])) {
                $callback['context'] = preg_split('/\//', $bits[2], -1, PREG_SPLIT_NO_EMPTY);
            }
        }

        /**
         * Immediately after determining which class will resolve the current page, this
         * delegate allows extension to modify the routing or provide additional information.
         *
         * @since Symphony 2.3.1
         * @delegate AdminPagePostCallback
         * @param string $context
         *  '/backend/'
         * @param string $page
         *  The current URL string, after the SYMPHONY_URL constant (which is `/symphony/`
         *  at the moment.
         * @param array $parts
         *  An array representation of `$page`
         * @param array $callback
         *  An associative array that contains `driver`, `pageroot`, `classname` and
         *  `context` keys. The `driver_location` is the path to the class to render this
         *  page, `driver` should be the view to render, the `classname` the name of the
         *  class, `pageroot` the rootpage, before any extra URL params and `context` can
         *  provide additional information about the page
         */
        Symphony::ExtensionManager()->notifyMembers('AdminPagePostCallback', '/backend/', array(
            'page' => $this->_currentPage,
            'parts' => $bits,
            'callback' => &$callback
        ));

        if (!isset($callback['driver_location']) || !is_file($callback['driver_location'])) {
            return false;
        }

        return $callback;
    }

    /**
     * Called by `symphony_launcher()`, this function is responsible for rendering the current
     * page on the Frontend. Two delegates are fired, AdminPagePreGenerate and
     * AdminPagePostGenerate. This function runs the Profiler for the page build
     * process.
     *
     * @uses AdminPagePreBuild
     * @uses AdminPagePreGenerate
     * @uses AdminPagePostGenerate
     * @see core.Symphony#__buildPage()
     * @see boot.getCurrentPage()
     * @param string $page
     *  The result of getCurrentPage, which returns the $_GET['symphony-page']
     *  variable.
     * @throws Exception
     * @throws SymphonyException
     * @return string
     *  The content of the page to echo to the client
     */
    public function display($page)
    {
        Symphony::Profiler()->sample('Page build process started');

        /**
         * Immediately before building the admin page. Provided with the page parameter
         * @delegate AdminPagePreBuild
         * @since Symphony 2.6.0
         * @param string $context
         *  '/backend/'
         * @param string $page
         *  The result of getCurrentPage, which returns the $_GET['symphony-page']
         *  variable.
         */
        Symphony::ExtensionManager()->notifyMembers('AdminPagePreBuild', '/backend/', array('page' => $page));

        $this->__buildPage($page);

        // Add XSRF token to form's in the backend
        if (self::isXSRFEnabled() && isset($this->Page->Form)) {
            $this->Page->Form->prependChild(XSRF::formToken());
        }

        /**
         * Immediately before generating the admin page. Provided with the page object
         * @delegate AdminPagePreGenerate
         * @param string $context
         *  '/backend/'
         * @param HTMLPage $oPage
         *  An instance of the current page to be rendered, this will usually be a class that
         *  extends HTMLPage. The Symphony backend uses a convention of contentPageName
         *  as the class that extends the HTMLPage
         */
        Symphony::ExtensionManager()->notifyMembers('AdminPagePreGenerate', '/backend/', array('oPage' => &$this->Page));

        $output = $this->Page->generate();

        /**
         * Immediately after generating the admin page. Provided with string containing page source
         * @delegate AdminPagePostGenerate
         * @param string $context
         *  '/backend/'
         * @param string $output
         *  The resulting backend page HTML as a string, passed by reference
         */
        Symphony::ExtensionManager()->notifyMembers('AdminPagePostGenerate', '/backend/', array('output' => &$output));

        Symphony::Profiler()->sample('Page built');

        return $output;
    }

    /**
     * If a page is not found in the Symphony backend, this function should
     * be called which will raise a customError to display the default Symphony
     * page not found template
     */
    public function errorPageNotFound()
    {
        $this->throwCustomError(
            __('The page you requested does not exist.'),
            __('Page Not Found'),
            Page::HTTP_STATUS_NOT_FOUND
        );
    }
}