<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * 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 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\views\layouts;

use seekquarry\yioop\configs as C;
use seekquarry\yioop as B;

/**
 * Layout used for the seek_quarry Website
 * including pages such as search landing page
 * and settings page
 *
 * @author Chris Pollett
 */
class WebLayout extends Layout
{
    /**
     * Responsible for drawing the header of the document containing
     * Yioop! title and including basic.js. It calls the renderView method of
     * the View that lives on the layout. If the QUERY_STATISTIC config setting
     * is set, it output statistics about each query run on the database.
     * Finally, it draws the footer of the document.
     *
     * @param array $data  an array of data set up by the controller to be
     * be used in drawing the WebLayout and its View.
     */
    public function render($data)
    {
        ?><!DOCTYPE html>
        <html lang="<?= $data['LOCALE_TAG']; ?>" dir="<?=
            $data['LOCALE_DIR']?>"<?php
            $use_color_scheme = $data['USE_COLOR_SCHEME'] ??
                C\p('USE_COLOR_SCHEME');
            $color_scheme_on = !empty($use_color_scheme) &&
                $use_color_scheme !== 'false';
            if ($color_scheme_on) {
                ?> class="color-scheme-auto"<?php
            }
            ?> data-redirect="<?=
            (C\REDIRECTS_ON) ? 'true' : 'false'?>" data-base="<?=
            C\SHORT_BASE_URL; ?>" >
        <head>
        <title><?php
        if (!empty($data['page'])) {
            $page_id = $data['page'];
        } else if (!empty($data['PAGE_ID'])) {
            $page_id = $data['PAGE_ID'];
        }
        if (isset($page_id) &&
            isset($this->view->head_objects[$page_id]['title'])) {
            e($this->view->head_objects[$page_id]['title']);
        } else {
            /* a page that names no title of its own is titled by what
               the site says it is and what it is called, which on a
               routed domain are that domain's rather than the site's */
            e(tl('web_layout_title',
                $data['SITE_MOTTO'] ?? C\p('SITE_MOTTO'),
                $data['SITE_NAME'] ?? C\p('SITE_NAME')));
        } ?></title><?php
        if (isset($this->view->head_objects['robots'])) {
            ?><meta name="ROBOTS" content="<?=
            $this->view->head_objects['robots'] ?>" ><?php
        }?>
        <meta name="description" content="<?php
        if (isset($page_id) &&
            isset($this->view->head_objects[$page_id]['description'])) {
            e($this->view->head_objects[$page_id]['description']);
        } else {
            e(tl('web_layout_description'));
        } ?>" >
        <meta name="Author" content="<?=tl('web_layout_site_author') ?>" >
        <meta name="referrer" content="strict-origin-when-cross-origin" >
        <?php
        if (C\nsdefined("GENERATOR_STRING") &&
            C\GENERATOR_STRING != "") {?>
            <meta name="generator" content="<?=
            C\GENERATOR_STRING ?>" ><?php
        } ?>
        <meta charset="utf-8" >
        <meta name="viewport"
            content="width=device-width, initial-scale=1.0"><?php
        if (!empty($this->view->head_objects['additional_metas'])) {
            e($this->view->head_objects['additional_metas']);
        }
        if (!empty($page_id) &&
            !empty($this->view->head_objects[$page_id]['properties'])) {
            $property_lines = explode("\n",
                $this->view->head_objects[$page_id]['properties']);
            foreach ($property_lines as $property_pair) {
                $property_parts = explode("|", $property_pair, 2);
                if (count($property_parts) == 2) {
                    list($property, $prop_content) = $property_parts;?>
                    <meta property="<?=trim($property) ?>" content="<?=
                        trim($prop_content) ?>" >
                    <?php
                }
            }
        }
        $path_url = C\SHORT_BASE_URL;
        $aux_css = false;
        /* the theme shown is the one for the host this page is served
           from: a routed domain's theme when it has one, which the
           domain appearance overlay has already put in $data, and the
           site's theme otherwise. The themes themselves are the site's
           and all sit in one folder, so which one is worn is all that
           changes from domain to domain */
        $auxiliary_css_name = $data['PAGE_THEME_NAME'] ??
            (C\nsdefined("AUXILIARY_CSS_NAME") ?
            C\p('AUXILIARY_CSS_NAME') : "");
        if ((!empty($page_id) &&
            !empty($this->view->head_objects[$page_id]['page_theme']))) {
            $auxiliary_css_name =
                $this->view->head_objects[$page_id]['page_theme'];
        }
        $theme_scope = C\DEFAULT_THEME_FOLDER;
        if (!empty($auxiliary_css_name) &&
            file_exists(C\APP_DIR .
            "/css/$theme_scope/$auxiliary_css_name.css")) {
            if (C\REDIRECTS_ON) {
                $aux_css = "{$path_url}wd/css/$theme_scope/" .
                    "$auxiliary_css_name.css";
            } else {
                $aux_css = "$path_url?c=resource&amp;a=get&amp;".
                    "f=css&amp;sf=$theme_scope&amp;" .
                    "n=$auxiliary_css_name.css";
            }
        }
        /* A presentation is drawn as a slide deck by frise, which reads
           the x-slides and x-slide marks in the page body and takes over
           the screen. Nothing else pulls frise in, so a presentation
           showed as a plain page; it is named here rather than in the
           wiki element because the head is written before any element
           runs. */
        if (!empty($data["HEAD"]['page_type']) &&
            $data["HEAD"]['page_type'] == 'presentation' &&
            !empty($data['MODE']) && $data['MODE'] == 'read') {
            $data['INCLUDE_STYLES'] = $data['INCLUDE_STYLES'] ?? [];
            $data['INCLUDE_STYLES'][] = "frise";
            $data['INCLUDE_SCRIPTS'] = $data['INCLUDE_SCRIPTS'] ?? [];
            $data['INCLUDE_SCRIPTS'][] = "frise";
        }
        /* Remember to give complete paths to all link tag hrefs to
           avoid PRSSI attacks
           http://www.theregister.co.uk/2015/02/20/prssi_web_vuln/
         */ ?>
        <link rel="icon" href="<?= !empty($data['FAVICON']) ?
            $data['FAVICON'] : $path_url . C\p('FAVICON') ?>" ><?php
        $search_css = $path_url . 'css/search.css';
        if (file_exists(C\APP_DIR . '/css/search.css')) {
            if (C\REDIRECTS_ON) {
                $search_css = "{$path_url}wd/css/search.css";
            } else {
                $search_css = "$path_url?c=resource&amp;a=get&amp;".
                    "f=css&amp;n=search.css";
            }
        } ?>
        <link rel="stylesheet" type="text/css" href="<?=$search_css ?>" ><?php
        if ($aux_css) { ?>
            <link rel="stylesheet" type="text/css"
                href="<?=$aux_css ?>" ><?php
        }
        if (C\nsdefined("SEARCHBAR_PATH") && C\p('SEARCHBAR_PATH') != "") { ?>
            <link rel="search" type="application/opensearchdescription+xml"
                href="<?=C\p('SEARCHBAR_PATH') ?>"
                title="Content search" ><?php
        }
        if (!empty($data['RSS_FEED_URL'])) {?>
            <link rel="alternate" type="application/rss+xml"
                href="<?=$data['RSS_FEED_URL'] ?>" ><?php
        }
        if (isset($data['INCLUDE_STYLES'])) {
            foreach ($data['INCLUDE_STYLES'] as $style_name) {
                e('<link rel="stylesheet" type="text/css"
                    href="'. $path_url . 'css/'.
                    $style_name.'.css" >'."\n");
            }
        } ?>
        <style>
        <?php
        /* Only the colors a site has set for itself are sent, as new
           settings for the names search.css paints with; the rules that
           read them live there, so a page carries four values rather than
           the forty lines that use them. */
        $settable_colors = [
            'BACKGROUND_COLOR' => '--color-background',
            'FOREGROUND_COLOR' => '--color-foreground',
            'SIDEBAR_COLOR' => '--color-sidebar',
            'TOPBAR_COLOR' => '--color-topbar'
        ];
        $color_settings = "";
        /* A color a site sets is the color it wants a reader to see.
           Where the site has turned light and dark mode on, the built
           in palette in search.css gives each name a light value and a
           dark one, and sending a single value would pin the name to
           one shade and stop the mode inverting. So a site color is
           sent as the light half of such a pair, beside the dark half
           the palette already carries. */
        $dark_halves = ['--color-background' => '#1a1a1a',
            '--color-foreground' => '#1a1a1a',
            '--color-sidebar' => '#242424',
            '--color-topbar' => '#23233a'];
        foreach ($settable_colors as $setting => $property) {
            if (!C\nsdefined($setting)) {
                continue;
            }
            $color = (!empty($data[$setting])) ? $data[$setting] :
                C\p($setting);
            if (empty($color)) {
                continue;
            }
            if ($color_scheme_on && isset($dark_halves[$property])) {
                $color = "light-dark(" . $color . ", " .
                    $dark_halves[$property] . ")";
            }
            $color_settings .= "                $property: $color;\n";
        }
        if (!empty($color_settings)) { ?>
            :root
            {
<?= $color_settings ?>            }
            <?php
        }
        if (C\nsdefined('BACKGROUND_IMAGE') && C\p('BACKGROUND_IMAGE')) {
            $background_image = !empty($data['BACKGROUND_IMAGE']) ?
                $data['BACKGROUND_IMAGE'] : C\p('BACKGROUND_IMAGE'); ?>
            body
            {
                background-image: url(<?=html_entity_decode(
                    $background_image) ?>);
                background-repeat: no-repeat;
                background-size: 12in;
            }
            @media (max-width: 600px)
            {
                body
                {
                    background-size: 100%;
                }
            }
            <?php
        }
        if (!empty($data['NUM_HIGHLIGHTS'])) {
            $landing_height = ($data['NUM_HIGHLIGHTS'] >= 2) ? 0 : 2;
            $mobile_height = ($data['NUM_HIGHLIGHTS'] >= 2) ? 0 : 70;
            ?>
            .top-landing-spacer
            {
                height:<?=$landing_height; ?>in;
            }
            @media (max-width: 600px)
            {
                .top-landing-spacer
                {
                    height:<?=$mobile_height; ?>px;
                }
            }
            <?php
        } ?>
        </style><?php
        if (empty($_REQUEST['noscript'])) { ?>
            <noscript>
            <style>
            .noscript-hide
            {
                display: none;
            }
            .top-container
            {
                display: contents;
            }
            .center-container
            {
                margin-top: -65px;
            }
            .html-ltr .logo-subsearch
            {
                left: 370px;
            }
            .html-ltr .body-container
            {
                margin-left: 300px;
            }
            .html-rtl .nav-container
            {
                overflow-y:scroll;
                position:fixed;
                right: 0px;
                top: 0px;
            }
            .html-rtl .body-container
            {
                margin-right: 300px;
            }
            .html-rtl .logo-subsearch
            {
                right: 370px;
            }
            #admin-menu-options
            {
                display: block;
            }
            </style>
            </noscript><?php
        } ?>
        </head><?php
        flush();
        $is_presentation = !empty($data["HEAD"]['page_type']) &&
            $data["HEAD"]['page_type'] == 'presentation' &&
            !empty($data['MODE']) && $data['MODE'] == 'read';
        ?>
        <body class="html-<?= $data['LOCALE_DIR']
            ?> html-<?= $data['WRITING_MODE']
            . self::wikiReadingClasses($data)
            ?>" ><?php
        $view_class = substr(get_class($this->view), strlen(
            'seekquarry\\yioop\\views\\'));
        $body_view = ($view_class == 'MediadetailView') ?
                " media-detail-body " :
                (($view_class == 'SearchView') ? " search-body " : "");
        if ($is_presentation) {
            e($data['PAGE']);
            /* The deck fills the screen and carries no bar, so the way
               into editing and the way to the discussion are drawn on
               their own, held in a corner of the window. */
            $this->view->element("groupbar")->renderPresentationCorner(
                $data);
        } else {
            ?><div id="body-container" class="body-container <?=
                $body_view ?>">
            <div id="message" ></div><?php
            $this->view->renderView($data);
        }
        if (C\QUERY_STATISTICS && !$is_presentation) { ?>
            <div class="query-statistics"><?php
            e("<h1>" . tl('web_layout_query_statistics')."</h1>");
            e("<div><b>".
                $data['YIOOP_INSTANCE']
                ."</b><br><br>");
            e("<b>".tl('web_layout_total_elapsed_time',
                 $data['TOTAL_ELAPSED_TIME'])."</b></div>");
            foreach ($data['QUERY_STATISTICS'] as $query_info) {
                e("<div class='query'><div>" . $query_info['QUERY'] .
                    "</div><div><b>".
                    tl('web_layout_query_time',
                        $query_info['ELAPSED_TIME']).
                        "</b></div></div>");
            } ?>
            </div><?php
        }
        if (isset($_SERVER["COOKIE_CONSENT"]) && !$_SERVER["COOKIE_CONSENT"]
            && !(!empty($_REQUEST['c']) && in_array($_REQUEST['c'],
            ['admin', 'register']))) {
            $consent_url = htmlentities($_SERVER["REQUEST_URI"]);
            $separator = (str_contains($consent_url, "?")) ? "&amp;" :
                "?";
            if (!str_contains($consent_url, "cookieconsent=true")) {
                $consent_url .= $separator . "cookieconsent=true";
            }
            ?>
            <div class="cookie-consent">
                <?=tl('web_layout_cookie_uses') ?>
                <a href="<?=B\directUrl('privacy') ?>"><?=
                tl('web_layout_privacy_policy') ?></a>.
                <a href="<?=$consent_url; ?>" class='anchor-button-consent'><?=
                    tl('web_layout_allow_cookies') ?></a>
            </div><?php
        }
        $script_path = C\APP_DIR . "/scripts/basic.js";
        $basic_js = $path_url . "scripts/basic.js";
        if (file_exists($script_path)) {
            $basic_js = "$path_url?c=resource&amp;a=get" .
                "&amp;f=scripts&amp;n=basic.js";
        }
        ?>
        <script src="<?=$basic_js ?>" ></script><?php
        if ($this->view->helper('helpbutton')->is_help_initialized) {
            if (!isset($data["INCLUDE_SCRIPTS"])) {
                $data["INCLUDE_SCRIPTS"] = [];
            }
            $data["INCLUDE_SCRIPTS"][] = "help";
            $data["INCLUDE_SCRIPTS"][] = "wiki_parser";
        }
        if ($this->view->helper('helpbutton')->script) {
            if (!isset($data['SCRIPT'])) {
                $data['SCRIPT'] = "";
            }
            $data['SCRIPT'] = $this->view->helper('helpbutton')->script .
                   $data['SCRIPT'];
        }
        if (isset($data['INCLUDE_SCRIPTS'])) {
            /* Two parts of a screen may each ask for the same script:
               the editor's preview wants the wiki reader, and so does a
               help callout. Drawn twice, the second copy declares
               classes the first already declared, which the browser
               refuses, and every script after it on the page stops. */
            $data['INCLUDE_SCRIPTS'] = array_unique(
                $data['INCLUDE_SCRIPTS']);
            foreach ($data['INCLUDE_SCRIPTS'] as $script_name) {
                if ($script_name == "math") {
                    // don't process math if html tag has class 'none'
                    ?><script>
                    MathJax = {
                      loader: {load: ['input/asciimath', 'output/chtml',
                        'ui/menu']},
                    };
                    </script><?php
                    $math_jax_path = C\APP_DIR .
                        "/scripts/mathjax/es5/startup.js";
                    if (file_exists($math_jax_path)  && C\REDIRECTS_ON) {
                        e('<script src="' . $path_url .
                            'wd/scripts/mathjax/es5/startup.js" ></script>');
                    } else {
                        e('<script src="https://cdn.jsdelivr.net/npm/'.
                            'mathjax@3/es5/startup.js" ></script>');
                    }
                } else if ($script_name == "credit" &&
                    C\CreditConfig::isActive()) {

                    e('<script src="' . C\CreditConfig::getCreditTokenUrl() .
                        '" ></script>');
                } else {
                    $script_path = C\APP_DIR . "/scripts/" . $script_name .
                        ".js";
                    if (file_exists($script_path)) {
                        if (C\REDIRECTS_ON) {
                            $script_url = $path_url . 'wd/scripts/'.
                                $script_name.'.js';
                        } else {
                            $script_url = "$path_url?c=resource&amp;a=get" .
                                "&amp;f=scripts&amp;n=$script_name.js";
                        }
                    } else {
                        $script_url = $path_url . 'scripts/'.
                            $script_name . '.js';
                    }
                    e ("<script src='$script_url' ></script>");
                }
            }
        }
        if (isset($data['INCLUDE_LOCALE_SCRIPT'])) {
            $locale_tag = str_replace("-", "_", $data["LOCALE_TAG"]);
            $locale_path = C\APP_DIR .
                "/locale/$locale_tag/resources/locale.js";
            $default_locale_path = C\BASE_DIR .
                "/locale/$locale_tag/resources/locale.js";
            if (file_exists($locale_path)) { ?>
                <script src='<?=$path_url . "?c=resource&amp;a=get" .
                    "&amp;f=locale&amp;sf=$locale_tag/resources".
                    "&amp;n=locale.js" ?>' ></script><?php
            } else if (file_exists($default_locale_path)) { ?>
                <script src='<?=$path_url . "locale/$locale_tag" .
                "/resources/locale.js" ?>' ></script><?php
            }
        } ?>
        <div id='tl'></div>
        <script><?php
        if (isset($data['SCRIPT'])) {
            e($data['SCRIPT']);
        }
        if (isset($data['DISPLAY_MESSAGE'])) {
            /*using double quotes in case message string has single quotes*/
            e("\ndoMessage(\"<h1 class='display-message' >" .
                $data['DISPLAY_MESSAGE'] .
                "</h1>\");");
        } ?>;/*keep semi-colon just in case inserted JS didn't have */
        if (typeof yioop_post_scripts === 'object' ) {
            for (var callback_index in yioop_post_scripts) {
                yioop_post_scripts[callback_index]();
            }
        }
        <?php
        /* The panel beside the results is carried into the reading column
           when a window is too narrow to hold both, and back out when it
           widens. */
        ?>
        if (typeof placeSearchCallout === "function") {
            placeSearchCallout();
            window.addEventListener("resize", placeSearchCallout);
        }
        </script>
        </div>
        </body>
        </html><?php
    }
    /**
     * Gives the classes a wiki page being read adds to the body, so a
     * domain's own stylesheet can reach reading alone and can reach one
     * group or one page within it. There are three: read-wiki, then the
     * group's name and the page's name, each with a word in front of it
     * saying which it is. Nothing is added unless a wiki page is being
     * read. The preview beside an editor asks for these too, with the
     * mode set to read, so what a writer sees is dressed the way a
     * reader will see it.
     *
     * @param array $data the field variables the page was drawn from
     * @return string the classes to add, each with a space before it,
     *      or an empty string when this is not a wiki page being read
     */
    public static function wikiReadingClasses($data)
    {
        if (empty($data['MODE']) || $data['MODE'] != 'read' ||
            empty($data['PAGE_NAME'])) {
            return "";
        }
        $classes = " read-wiki";
        if (!empty($data['GROUP']['GROUP_NAME'])) {
            $classes .= " group-" .
                self::nameForClass($data['GROUP']['GROUP_NAME']);
        }
        $classes .= " page-" . self::nameForClass($data['PAGE_NAME']);
        return $classes;
    }
    /**
     * Turns a group's or a page's name into the part of a class name
     * that stands for it. Spaces and underscores become hyphens, since
     * neither can stand in a class name, and anything else a class name
     * cannot carry is dropped so what is left can be written in a
     * stylesheet as it stands.
     *
     * @param string $name the group's or page's name
     * @return string the name as it appears in a class
     */
    public static function nameForClass($name)
    {
        $hyphenated = preg_replace('/[\s_]+/u', "-", trim((string)$name));
        return preg_replace('/[^\p{L}\p{N}\-]/u', "", $hyphenated);
    }
}
