Skip to content
Snippets Groups Projects
PageLayout.php 25.8 KiB
Newer Older
<?php
# Lifter010: TODO
/**
 * PageLayout.php - configure the global page layout of Stud.IP
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * @author      Elmar Ludwig
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
 * @category    Stud.IP
 */

/**
 * The PageLayout class provides utility functions to control the
 * global page layout of Stud.IP. This includes the page title, the
 * included CSS style sheets and JavaScript files. It replaces the
 * "traditional" way of manipulating the page header via special
 * global variables (like $CURRENT_PAGE and $_include_stylesheet).
 *
 * Each Stud.IP page should at least set the page title and help
 * keyword (if a help page exists).
 */
class PageLayout
{
    /**
     * current page title (defaults to $UNI_NAME_CLEAN)
     */
    private static $title;

    /**
     * current help keyword (defaults to 'Basis.Allgemeines')
     */
    private static $help_keyword;

Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
    /**
     * current help URL (or null if unset)
     */
    private static ?string $help_url = null;

    /**
     * base item path for tab view (defaults to active item in top nav)
     */
    private static $tab_navigation_path = false;

    /**
     * base item for tab view (defaults to active item in top nav)
     */
    private static $tab_navigation = false;

    /**
     * array of HTML HEAD elements (initialized with default set)
     */
    private static $head_elements = [];

    /**
     * extra HTML text included in the BODY element (initially empty)
     */
    private static $body_elements = '';

    /**
     * id of the body tag
     */
    private static $body_element_id = false;

    /**
     * determines whether the navigation header is displayed or not
     */
    private static $display_header = true;

    /**
     * determines whether the sidebar is displayed or not
     */
    private static $display_sidebar = true;

    /**
     * determines whether the page footer is displayed or not
     */
    private static $display_footer = true;

    /*
     * Custom quicksearch on the page
     */
    private static $customQuicksearch;

    /**
     * Allow full screen mode toggle
     */
    private static $allow_full_screen = false;

    /**
     * Compatibility lookup table (old file -> new file/squeeze package)
     *
     * This is used for often used but "deprecated" assets that got
     * renamed or moved into a squeeze package.
     */
    private static $compatibility_lookup = [
        'jquery/jquery.tablesorter.js' => 'tablesorter', // @since 3.4
    ];

    /**
     * Initialize default page layout. This should only be called once
     * from phplib_local.inc.php. Don't use this otherwise.
     */
    public static function initialize()
    {
        // Get software version
        $v = StudipVersion::getStudipVersion(false);

        // set favicon
        self::addHeadElement('link', ['rel' => 'apple-touch-icon', 'sizes' => '180x180', 'href' => Assets::image_path('apple-touch-icon.png')]);
        self::addHeadElement('link', ['rel' => 'icon', 'type' => 'image/svg+xml', 'href' => Assets::image_path('favicon.svg')]);
        self::addHeadElement('link', ['rel' => 'icon', 'type' => 'image/png', 'sizes' => '64x64', 'href' => Assets::image_path('favicon-64x64.png')]);
        self::addHeadElement('link', ['rel' => 'icon', 'type' => 'image/png', 'sizes' => '32x32', 'href' => Assets::image_path('favicon-32x32.png')]);
        self::addHeadElement('link', ['rel' => 'icon', 'type' => 'image/png', 'sizes' => '16x16', 'href' => Assets::image_path('favicon-16x16.png')]);
        self::addHeadElement('link', ['rel' => 'manifest', 'href' => Assets::image_path('manifest.json')]);
        self::addHeadElement('link', ['rel' => 'mask-icon', 'href' => Assets::image_path('safari-pinned-tab.svg')]);
        self::addHeadElement('link', ['rel' => 'preload', 'href' => Assets::url('fonts/LatoLatin/LatoLatin-Regular.woff2'), 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous']);
        self::addHeadElement('link', ['rel' => 'preload', 'href' => Assets::url('fonts/LatoLatin/LatoLatin-Bold.woff2'), 'as' => 'font', 'type' => 'font/woff2', 'crossorigin' => 'anonymous']);
        self::addHeadElement('meta', ['name' => 'TileColor', 'content' => '#2b5797']);
        self::addHeadElement('meta', ['name' => 'TileImage', 'content' => Assets::image_path('mstile-144x144.png')]);
        self::addHeadElement('meta', ['name' => 'msapplication-config', 'content' => Assets::image_path('browserconfig.xml')]);
        self::addHeadElement('meta', ['name' => 'theme-color', 'content' => '#ffffff']);

        // set initial width for mobile devices
        self::addHeadElement('meta', ['name' => 'viewport', 'content' => 'width=device-width, initial-scale=1.0']);

        self::addHeadElement('link', [
            'rel'   => 'help',
            'href'  => format_help_url('Basis.VerschiedenesFormat'),
            'class' => 'text-format',
            'title' => _('Hilfe zur Textformatierung')
        ]);

        self::addStylesheet('studip-base.css?v=' . $v, ['media' => 'screen']);
        self::addScript('studip-base.js?v=' . $v);
        self::addScript('studip-wysiwyg.js?v=' . $v);

        self::addStylesheet('print.css?v=' . $v, ['media' => 'print']);

        if (Studip\Debug\DebugBar::isActivated()) {
            $old_base = URLHelper::setBaseURL($GLOBALS['ABSOLUTE_URI_STUDIP']);

            self::addHeadElement('link', [
                'href' => URLHelper::getURL('dispatch.php/debugbar/css'),
                'rel' => 'stylesheet',
                'type' => 'text/css',
            ]);
            self::addHeadElement('script', [
                'src' => URLHelper::getURL('dispatch.php/debugbar/js'),
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
            ], '');

            URLHelper::setBaseURL($old_base);
        }
    }

    /**
     * Set the page title to the given text.
     * @param string $title Page title
     */
    public static function setTitle($title)
    {
        self::$title = $title;
    }

    /**
     * Returns whether a title has been set
     * @return bool
     */
    public static function hasTitle()
    {
        return isset(self::$title);
    }

    /**
     * Get the current page title (defaults to $UNI_NAME_CLEAN).
     * @return string
     */
    public static function getTitle()
    {
        return isset(self::$title) ? self::$title :
                (isset($GLOBALS['_html_head_title']) ? $GLOBALS['_html_head_title'] :
                    (isset($GLOBALS['CURRENT_PAGE']) ? $GLOBALS['CURRENT_PAGE'] :
                        Config::get()->UNI_NAME_CLEAN));
    }

    /**
     * Set the help keyword to the given string.
     */
    public static function setHelpKeyword($help_keyword)
    {
        self::$help_keyword = $help_keyword;
    }

    /**
     * Get the current help keyword (defaults to 'Basis.Allgemeines').
     */
    public static function getHelpKeyword()
    {
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
        return self::$help_keyword ?? 'Basis.Allgemeines';
    }

    /**
     * Set the help URL in the help bar. Pass null to fall back to the help keyword.
     */
    public static function setHelpUrl(?string $url): void
    {
        self::$help_url = $url;
    }

    /**
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
     * Get the current help URL. If no URL is set explicitly, the URL for
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
     * the help keyword is used.
     */
    public static function getHelpUrl(): ?string
    {
        return self::$help_url ?? format_help_url(self::getHelpKeyword());
    }

    /**
     * Select which tabs (if any) should be displayed on the page. The
     * argument specifies a navigation item in the tree whose children
     * will form the first level of tabs. If $path is NULL, no tabs are
     * displayed. The default setting is to use the active element in
     * the top navigation.
     *
     * @param string $path       path of navigation item for tabs or NULL
     */
    public static function setTabNavigation($path)
    {
        self::$tab_navigation_path = $path;
        self::$tab_navigation      = isset($path) ? Navigation::getItem($path) : NULL;
    }

    /**
     * Returns the base navigation object (not its path) for the tabs.
     * May return NULL if tab display is disabled.
     */
    public static function getTabNavigation()
    {
        if (self::$tab_navigation === false) {
            self::$tab_navigation = Navigation::hasItem('/') ? Navigation::getItem('/')->activeSubNavigation() : null;
        }

        return self::$tab_navigation;
    }

    /**
     * Returns the base navigation path for the tabs.
     * May return NULL if tab display is disabled.
     */
    public static function getTabNavigationPath()
    {
        if (self::$tab_navigation_path === false && self::getTabNavigation()) {
            foreach (self::getTabNavigation() as $subpath => $navigation) {
                if ($navigation->isActive()) {
                    self::$tab_navigation_path = $subpath;
                }
            }
        }
        return self::$tab_navigation_path;
    }

    /**
     * Add a STYLE element to the HTML HEAD section.
     *
     * @param string $content   element contents
     * @param string $media     media types
     */
    public static function addStyle($content, $media = '')
    {
        $attr = [];
        if($media) {
            $attr = ['media' => $media];
        }
        self::addHeadElement('style', $attr, $content);
    }

    /**
     * Add a style sheet LINK element to the HTML HEAD section.
     *
     * @param string $source     style sheet URL or file in assets folder
     * @param array  $attributes additional attributes for LINK element
     */
    public static function addStylesheet($source, $attributes = [])
    {
        $attributes['rel']  = 'stylesheet';
        $attributes['href'] = Assets::stylesheet_path($source);

        self::addHeadElement('link', $attributes);
    }

    /**
     * Remove a style sheet LINK element from the HTML HEAD section.
     *
     * @param string $source     style sheet URL or file in assets folder
     * @param array  $attributes additional attributes for LINK element
     */
    public static function removeStylesheet($source, $attributes = [])
    {
        $attributes['rel']  = 'stylesheet';
        $attributes['href'] = Assets::stylesheet_path($source);

        self::removeHeadElement('link', $attributes);
    }

    /**
     * Add a JavaScript SCRIPT element to the HTML HEAD section.
     *
     * @param string $source     URL of JS file or file in assets folder
     * @param array $attributes  Additional parameters for the script tag
     */
    public static function addScript($source, $attributes = [])
    {
        // Check for compatibility lookup entry and rename file resp.
        // add according squeeze package (if lookup element does not
        // end in '.js').
        if (array_key_exists($source, self::$compatibility_lookup)) {
            $source = self::$compatibility_lookup[$source];
            if (!preg_match('/\.js$/', $source)) {
                self::addSqueezePackage($source);
                return;
            }
        }

        $attributes['src'] = Assets::javascript_path($source);

        self::addHeadElement('script', $attributes, '');
    }

    /**
     * Remove a JavaScript SCRIPT element from the HTML HEAD section.
     *
     * @param string $source     URL of JS file or file in assets folder
     * @param array $attributes  Additional parameters for the script tag
     */
    public static function removeScript($source, $attributes = [])
    {
        $attributes['src'] = Assets::javascript_path($source);

        self::removeHeadElement('script', $attributes);
    }

    /**
     * Add an extra HTML element to the HTML HEAD section. This can be
     * used to include RSS/ATOM feed links, META tags or other stuff.
     * If $content is NULL, no closing tag is generated. If the element
     * needs a closing tag (like SCRIPT) but should not have contents,
     * pass the empty string as the third parameter.
     *
     * @param string $name       element name (e.g. 'meta')
     * @param array  $attributes additional attributes for the element
     * @param string $content    element contents, if any
     */
    public static function addHeadElement($name, $attributes = [], $content = NULL)
    {
        self::$head_elements[] = compact('name', 'attributes', 'content');
    }

    /**
     * Remove HTML elements from the HTML HEAD section. This method will
     * remove all elements matching the given name and all the attributes.
     *
     * For example, to remove all META elements:
     * PageLayout::removeHeadElement('meta');
     *
     * Remove all style sheet LINK elements:
     * PageLayout::removeHeadElement('link', array('rel' => 'stylesheet'));
     *
     * Remove a particular style sheet LINK by href:
     * PageLayout::removeHeadElement('link', array('href' => '...'));
     */
    public static function removeHeadElement($name, $attributes = [])
    {
        $result = [];

        foreach (self::$head_elements as $element) {
            $remove = false;

            // match element name
            if ($name === $element['name']) {
                $remove = true;

                // match element attributes
                foreach ($attributes as $key => $value) {
                    if (!isset($element['attributes'][$key]) ||
                        $element['attributes'][$key] !== $value) {
                        $remove = false;
                        break;
                    }
                }
            }

            if (!$remove) {
                $result[] = $element;
            }
        }

        self::$head_elements = $result;
    }

    /**
     * Insert a (conditional) comment in the header. To preserve execution
     * order, this method utilizes addHeadElement() in a more or less hackish
     * way.
     *
     * @param string $content    comment content
     */
    public static function addComment($content)
    {
        self::addHeadElement(sprintf('!--%s--', $content));
    }

    /**
     * Remove a (conditional) comment from the header.
     *
     * @param string $content    comment content
     */
    public static function removeComment($content)
    {
        self::removeHeadElement(sprintf('!--%s--', $content));
    }

    /**
     * Return all HTML HEAD elements as a string.
     *
     * @return string   HTML fragment
     */
    public static function getHeadElements()
    {
        $result = '';

        $package_elements = [];

        if (isset($GLOBALS['_include_stylesheet'])) {
            self::addStylesheet($GLOBALS['_include_stylesheet'], ['media' => 'screen, print']);
        }

        $head_elements = array_merge($package_elements, self::$head_elements);

        foreach ($head_elements as $element) {
            $result .= '<' . $element['name'];

            foreach ($element['attributes'] as $key => $value) {
                $result .= sprintf(' %s="%s"', $key, htmlReady($value));
            }

            $result .= ">\n";

            if (isset($element['content'])) {
                $result .= $element['content'];
                $result .= '</' . $element['name'] . ">\n";
            }
        }

        if (isset($GLOBALS['_include_extra_stylesheet'])) {
            $result .= Assets::stylesheet($GLOBALS['_include_extra_stylesheet']);
        }

        if (isset($GLOBALS['_include_additional_header'])) {
            $result .= $GLOBALS['_include_additional_header'];
        }

        return $result;
    }

    /**
     * Add an extra HTML fragment at the start of the HTML BODY section.
     *
     * @param string $html  HTML fragment to include in BODY
     */
    public static function addBodyElements($html)
    {
        self::$body_elements .= $html;
    }

    /**
     * Return all HTML BODY fragments as a string.
     *
     * @return string   HTML fragment
     */
    public static function getBodyElements()
    {
        $result = self::$body_elements;

        if (isset($GLOBALS['_include_additional_html'])) {
            $result .= $GLOBALS['_include_additional_html'];
        }

        return $result;
    }

    /**
     * Disable output of the navigation header for this page.
     */
    public static function disableHeader()
    {
        self::$display_header = false;
    }

    /**
     * Return whether output of the navigation header is enabled.
     */
    public static function isHeaderEnabled()
    {
Moritz Strohm's avatar
Moritz Strohm committed
        return self::$display_header && empty($GLOBALS['_NOHEADER']);
    /**
     * Disable output of the sidebar for this page.
     *
     * @since Stud.IP 5.4
     */
    public static function disableSidebar(bool $state = true)
    {
        self::$display_sidebar = !$state;
    }

    /**
     * Return whether output of the sidebar is enabled.
     *
     * @since Stud.IP 5.4
     */
    public static function isSidebarEnabled(): bool
    {
        return self::$display_sidebar;
    }

    /**
     * Disable output of the page footer for this page.
     *
     * @param bool $state
     *
     * @since Stud.IP 5.4
     */
    public static function disableFooter(bool $state = true)
    {
        self::$display_footer = !$state;
    }

    /**
     * Return whether output of the page footer is enabled.
     *
     * @since Stud.IP 5.4
     */
    public static function isFooterEnabled(): bool
    {
        return self::$display_footer;
    }

    /**
     * Sets the id of the html body element.
     * The given id is stripped of all non alpha-numeric characters
     * (except for -).
     *
     * @param String $id Id of the body element
     */
    public static function setBodyElementId($id)
    {
        self::$body_element_id = preg_replace('/[^\w-]+/', '_', $id);
    }

    /**
     * Returns whether an id for the body element has been set.
     *
     * @return bool
     */
    public static function hasBodyElementId()
    {
        return self::$body_element_id !== false;
    }

    /**
     * Gets the id of the body element.
     * If non was set, it is dynamically generated base on the name of
     * the current PHP script, with the suffix removed and all
     * non-alphanumeric characters replaced with '_'.
     *
     * @return String containing the body element id
     */
    public static function getBodyElementId()
    {
        // Return specific or dynamically generated body element id
        return self::$body_element_id
            ?: preg_replace('/\W/', '_', basename($_SERVER['PHP_SELF'], '.php'));
    }

    /**
     * Registers a MessageBox object for display the next time a layout
     * is rendered. Note: This will only work for pages that use layout
     * templates.
     *
     * @param MessageBox  message object to display
     */
    public static function postMessage(LayoutMessage $message, $id = null)
    {
        if (!isset($_SESSION['messages'])) {
            $_SESSION['messages'] = [];
        }
            $_SESSION['messages'][] = $message;
            $_SESSION['messages'][$id] = $message;
        }
    }

    /**
     * Convenience method: Post a success message box.
     *
     * @param String $message       Success message to diplay
     * @param Array  $details       Additional details (optional)
     * @param bool   $close_details Show the details closed (optional,
     *                              defaults to false)
     */
    public static function postSuccess($message, $details = [], $close_details = false)
    {
        self::postMessage(MessageBox::success($message, $details, $close_details));
    }

    /**
     * Convenience method: Post an error message box.
     *
     * @param String $message       Error message to diplay
     * @param Array  $details       Additional details (optional)
     * @param bool   $close_details Show the details closed (optional,
     *                              defaults to false)
     */
    public static function postError($message, $details = [], $close_details = false)
    {
        self::postMessage(MessageBox::error($message, $details, $close_details));
    }

    /**
     * Convenience method: Post an info message box.
     *
     * @param String $message       Info message to diplay
     * @param Array  $details       Additional details (optional)
     * @param bool   $close_details Show the details closed (optional,
     *                              defaults to false)
     */
    public static function postInfo($message, $details = [], $close_details = false)
    {
        self::postMessage(MessageBox::info($message, $details, $close_details));
    }

    /**
     * Convenience method: Post a warning message box.
     *
     * @param String $message       Warning message to diplay
     * @param Array  $details       Additional details (optional)
     * @param bool   $close_details Show the details closed (optional,
     *                              defaults to false)
     */
    public static function postWarning($message, $details = [], $close_details = false)
    {
        self::postMessage(MessageBox::warning($message, $details, $close_details));
    }

    /**
     * Convenience method: Post an exception message box.
     *
     * @param String $message       Exception message to diplay
     * @param Array  $details       Additional details (optional)
     * @param bool   $close_details Show the details closed (optional,
     *                              defaults to false)
     */
    public static function postException($message, $details = [], $close_details = false)
    {
        self::postMessage(MessageBox::exception($message, $details, $close_details));
    }

    /**
     * Convenience method: Post a question to confirm an action.
     *
     * Be aware, that this will output the question as html. So you should
     * either know what you are doing, use htmlReady() or post the QuestionBox
     * for yourself using self::postMessage().
     *
     * @param String $question          Question to confirm
     * @param Array  $approve_params    Parameters to send when approving
     * @param Array  $disapprove_params Parameters to send when disapproving
     * @return QuestionBox to allow further settings like urls and such
     * @see QuestionBox
     * @since Stud.IP 4.2
     */
    public static function postQuestion($question, $accept_url = '', $decline_url = '')
    {
        $qbox = QuestionBox::createHTML($question, $accept_url, $decline_url);
        self::postMessage($qbox, 'question-box');
        return $qbox;
    }

    /**
     * Clears all messages pending for display.
     */
    public static function clearMessages()
    {
        unset($_SESSION['messages']);
    }

    /**
     * Returns the list of pending messages and clears the list.
     *
     * @return array    list of MessageBox objects
     */
    public static function getMessages(string $type = LayoutMessage::class)
        foreach ($_SESSION['messages'] ?? [] as $index => $message) {
            if (is_a($message, $type)) {
                $messages[$index] = $message;
                unset($_SESSION['messages'][$index]);
            }
        }

        return $messages;
    }

    /**
     * Return the names of the squeeze packages to use.
     *
     * Per default the squeeze package "base" is included.
     *
     * @return array  an array containing the names of the packages
     */
    public static function getSqueezePackages()
    {
        return [];
    }

    /**
     * Set the names of the squeeze packages to use
     *
     * @code
     * # use as many arguments as you want
     * PageLayout::setSqueezePackages("base");
     * PageLayout::setSqueezePackages("base", "admin");
     * PageLayout::setSqueezePackages("base", "admin", "upload");
     * # PageLayout::setSqueezePackages(...);
     * @endcode
     *
     * @param ...
     *    a variable-length argument list containing the names of the packages
     */
    public static function setSqueezePackages($package/*, ...*/)
    {
        $packages = func_get_args();
        foreach ($packages as $package) {
            self::addSqueezePackage($package);
        }
    }

    /**
     * Add a squeeze package to the list of squeeze packages to use
     *
     * @code
     * PageLayout::addSqueezePackage("admin");
     * @endcode
     *
     * @param string $package  the name of the package
     */
    public static function addSqueezePackage($package)
    {
        $oldPackages = ["admission", "base", "statusgroups", "wysiwyg"];
        $oldCssPackages = ["base", "statusgroups"];

        // tablesorter loads on demand

        // Get software version
        $v = StudipVersion::getStudipVersion(false);

        if (in_array($package, $oldPackages)) {
            self::addScript('studip-' . $package . '.js?v=' . $v);
        }
        if (in_array($package, $oldCssPackages)) {
            self::addStylesheet('studip-' . $package . '.css?v=' . $v);
        }
    }

    /**
     * Modifies the Quicksearch of Stud.IP
     *
     * @param $html HTML code for the custom quicksearch
     */
    public static function addCustomQuicksearch($html)
    {
        self::$customQuicksearch = $html;
    }

    /**
     * Check if a custom quicksearch was added to the layout
     *
     * @return bool TRUE if there is a custom quicksearch
     */
    public static function hasCustomQuicksearch()
    {
        return isset(self::$customQuicksearch);
    }

    /**
     * Retrieves the code of the custom quicksearch
     *
     * @return mixed Quicksearch code
     */
    public static function getCustomQuicksearch()
    {
        return self::$customQuicksearch;
    }

    /**
     * Defines whether full screen mode toggle is allowed or not.
     *
     * @param boolean $state
     */
    public static function allowFullscreenMode($state = true)
    {
        self::$allow_full_screen = $state;
    }

    /**
     * Returns whether full screen mode toggle is allowed.
     *
     * @return boolean
     */
    public static function isFullscreenModeAllowed()
    {
        return self::$allow_full_screen;
    }
}