<?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;
use seekquarry\yioop\library as L;
/**
* 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;
/* A wiki page or group may name a theme of its own, which is read
from the themes folder. Otherwise the page uses the theme the
host it is served for links to from its auxiliary.css, or the
default domain's. */
$page_theme = (!empty($page_id)) ?
($this->view->head_objects[$page_id]['page_theme'] ?? "") : "";
if (!empty($page_theme) && !in_array($page_theme,
L\ResourceFolders::NOT_THEMES) && file_exists(C\APP_DIR .
"/css/$page_theme.css")) {
if (C\REDIRECTS_ON) {
$aux_css = "{$path_url}wd/css/$page_theme.css";
} else {
$aux_css = "$path_url?c=resource&a=get&".
"f=css&n=$page_theme.css";
}
} else {
$host = strtolower(preg_replace('/:\d+$/', "",
$_SERVER['HTTP_HOST'] ?? ""));
$domain_css = L\ResourceFolders::domainStylesheetUrl($host);
if ($domain_css != "") {
$aux_css = $path_url . $domain_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 .
L\ResourceFolders::siteImageUrl('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&a=get&".
"f=css&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
}
$background_image = !empty($data['BACKGROUND_IMAGE']) ?
$data['BACKGROUND_IMAGE'] :
L\ResourceFolders::siteImageUrl('BACKGROUND_IMAGE');
if ($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\p('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, "?")) ? "&" :
"?";
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&a=get" .
"&f=scripts&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&a=get" .
"&f=scripts&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&a=get" .
"&f=locale&sf=$locale_tag/resources".
"&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();
listen(window, "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);
}
}