<?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\controllers;
use seekquarry\yioop as B;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library as L;
/**
* Displays a group's wiki pages as forward-facing static pages. By
* default it serves the world-readable Public group, the way the Yioop
* site's own Main, help, and error pages are shown. A domain an admin
* routes to a landing group in the Web Server field set is served
* through this controller too, so that group's Main page renders as the
* domain's landing page rather than through the group controller.
*
* @author Chris Pollett
*/
class StaticController extends Controller
{
/**
* Says which activities (roughly methods invoke from the web)
* this controller will respond to
* @var array
*/
public $activities = ["showPage", "signout"];
/**
* Group whose wiki pages and resources this static request serves.
* A domain an admin routes to a landing group in the Web Server
* field set is served through this controller so its Main page
* renders as a forward-facing page; the router puts that group's id
* on the ROUTE_GROUP_ID server variable, which a browser cannot set
* because it is not an HTTP_ header, so only an admin-set route can
* choose the group. With no route the world-readable Public group is
* served, the historical behavior.
*
* @return int group identifier whose pages this request reads
*/
public function staticGroupId()
{
$route_group_id = $_SERVER['ROUTE_GROUP_ID'] ?? 0;
if (is_numeric($route_group_id) && $route_group_id > 0) {
return (int)$route_group_id;
}
return C\PUBLIC_GROUP_ID;
}
/**
* Rewrites a routed group's own wiki links so they point at the
* static p/ url, the way the Public group's links do. A page saved
* in a group other than Public bakes each self link as that group's
* read url on the group controller, a form ending in the token and a
* page_name query; served on a routed domain through this controller
* that url has no clean route and errors. Each such self link is
* rebuilt as the static p/ form (for example p/Bob) using the page
* name from the query, so the match does not depend on how the base
* url looked when the page was saved. Public pages already carry the
* controller-agnostic link and are returned unchanged.
*
* @param string $html a parsed wiki page whose self links may still
* be in the group-controller form
* @return string the page with any routed-group self link pointing
* at its static p/ url
*/
public function rewriteRoutedGroupLinks($html)
{
if ($this->staticGroupId() == C\PUBLIC_GROUP_ID) {
return $html;
}
$static_page_url = C\SHORT_BASE_URL . "p/";
return preg_replace(
'/href="[^"]*\[\{token\}\]&page_name=([^"]+)"/',
'href="' . $static_page_url . '$1"', $html);
}
/**
* This is the main entry point for handling people arriving to view
* a static page. It determines which page to draw and class the view
* to draw it.
*/
public function processRequest()
{
$data = [];
$view = "static";
if (isset($_SESSION['USER_ID'])) {
$user = $_SESSION['USER_ID'];
} else {
$user = L\remoteAddress();
}
if (isset($_REQUEST['a'])) {
if (in_array($_REQUEST['a'], $this->activities)) {
$activity = $_REQUEST['a'];
if ($activity == "signout") {
$data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
tl('static_controller_logout_successful')."</h1>')";
$activity = "showPage";
}
} else {
$activity = "showPage";
}
} else {
$activity = "showPage";
}
$data['VIEW'] = $view;
$data = array_merge($data, $this->call($activity));
if (isset($_SESSION['USER_ID'])) {
$user = $_SESSION['USER_ID'];
$data['ADMIN'] = 1;
} else {
$user = L\remoteAddress();
}
$data[C\p('CSRF_TOKEN')] = $this->generateCSRFToken($user);
if (!empty($_POST['CSVFORM']) && !empty($_POST[C\p('CSRF_TOKEN')])) {
$this->component("social")->processWikiFormData($data, $user,
$this->staticGroupId(), $data["SUB_PATH"] ?? "");
}
if (str_contains($data["PAGE"] ?? "", "[{proof-of-work}]")) {
$this->setupProofOfWorkViewData($data);
}
$this->initializeAdFields($data);
$this->displayView($view, $data);
}
/**
* Draws one static page: the wiki page named in the request read
* from this request's static group (the Public group, or a routed
* domain's landing group), together with its header, footer, and any
* media the page lists.
*
* @return array $data has title and page contents of the static page to
* display
*/
public function showPage()
{
$group_model = $this->model("group");
$group_id = $this->staticGroupId();
if (isset($_SESSION['USER_ID'])) {
$user = $_SESSION['USER_ID'];
} else {
$user = L\remoteAddress();
}
$data = [];
if (isset($_REQUEST['p'])) {
$page = $this->clean($_REQUEST['p'], "string");
$page = preg_replace("@(\.\.|\/)@", "", $page);
} else {
$page = "404";
}
$page_string = $this->getPage($page);
if (empty($page_string)) {
$page = "404";
$page_string = $this->getPage($page);
}
$page_parts = explode(L\WikiParser::END_HEAD_VARS, $page_string);
$data['PAGE'] = $page_parts[1] ?? $page_parts[0];
if (!isset($data["INCLUDE_SCRIPTS"])) {
$data["INCLUDE_SCRIPTS"] = [];
}
if (!isset($data["SCRIPT"])) {
$data["SCRIPT"] = "";
}
if (stripos($page_string, "chart_data") !== false) {
if (!in_array("chart", $data["INCLUDE_SCRIPTS"])) {
$data["INCLUDE_SCRIPTS"][] = "chart";
}
if (!str_contains($data["SCRIPT"], "new Chart")) {
if ($_SERVER["MOBILE"]) {
$properties = ["width" => 340, "height" => 300,
"tick_font_size" => 8];
} else {
$properties = ["width" => 700, "height" => 500];
}
$data['SCRIPT'] .= <<< 'EOD'
for (var chart_elt in chart_data) {
var chart = new Chart(
'chart_' + chart_elt,
chart_data[chart_elt],
chart_config[chart_elt]);
chart.draw();
}
EOD;
}
}
if (str_contains($page_string, "spreadsheet_data")) {
if (!in_array("spreadsheet", $data["INCLUDE_SCRIPTS"])) {
$data["INCLUDE_SCRIPTS"][] = "spreadsheet";
}
if (!str_contains($data["SCRIPT"], "new Spreadsheet")) {
$data['SCRIPT'] .= <<< 'EOD'
for (var spreadsheet_elt in spreadsheet_data) {
var spreadsheet = new Spreadsheet(
'spreadsheet_' + spreadsheet_elt,
spreadsheet_data[spreadsheet_elt],
spreadsheet_config[spreadsheet_elt]);
spreadsheet.draw();
}
EOD;
}
$data['SPREADSHEET'] = true;
}
if (str_contains($page_string, "`")){
if (!isset($data["INCLUDE_SCRIPTS"])) {
$data["INCLUDE_SCRIPTS"] = [];
}
$data["INCLUDE_SCRIPTS"][] = "math";
}
$data['page'] = $page;
$static_view = $this->view("static");
$this->parsePageHeadVarsView($static_view, $page, $page_string);
/* this is used by the captcha time out mechanism
and the suggest a site time out mechanism
see RegisterController for where $_SESSION['value'] set
*/
if (isset($_SESSION['value'])) {
$data['value'] = $this->clean($_SESSION['value'], "string");
}
$head_info = $static_view->head_objects[$data['page']];
if (isset($head_info['page_type']) &&
$head_info['page_type'] == 'page_alias' &&
$head_info['page_alias'] != '' ) {
$alias_parts = explode("@", $head_info['page_alias']);
$alias = $alias_parts[0];
$alias_group_id = $group_id;
$controller_name = "static";
if (!empty($alias_parts[1])) {
$alias = $alias_parts[1];
$alias_group_id = $group_model->getGroupId($alias_parts[0]);
$controller_name = "group";
}
return $this->redirectLocation(B\wikiUrl($alias,
true, $controller_name, $alias_group_id));
}
if ((isset($head_info['title']))) {
if ($head_info['title']) {
$data["subtitle"] = " - " . $head_info['title'];
$static_view->head_objects[$data['page']]['title'] =
tl('static_controller_complete_title',
$this->siteName(), $head_info['title']);
} else {
/* a page that gives itself no title is titled for the
site alone, rather than for the site followed by a
separator with nothing after it */
$data["subtitle"] = "";
$static_view->head_objects[$data['page']]['title'] =
$this->siteName();
}
} else {
$data["subtitle"] = "";
}
$locale_tag = L\getLocaleTag();
$data['CONTROLLER'] = "static";
$group_model = $this->model("group");
if (!empty($head_info['page_header']) &&
$head_info['page_type'] != 'presentation') {
$page_header = $group_model->getPageInfoByName($group_id,
$head_info['page_header'], $locale_tag, "read");
if (!$page_header) {
$page_header = [];
}
if (isset($page_header['PAGE'])) {
$header_parts =
explode(L\WikiParser::END_HEAD_VARS, $page_header['PAGE']);
}
$page_header['PAGE'] = $page_header['PAGE'] ?? "";
$data["PAGE_HEADER"] = $this->rewriteRoutedGroupLinks(
(isset($header_parts[1])) ?
$header_parts[1] : "" . $page_header['PAGE']);
}
if (!empty($head_info['page_footer']) &&
$head_info['page_type'] != 'presentation') {
$page_footer = $group_model->getPageInfoByName($group_id,
$head_info['page_footer'], $locale_tag, "read");
if (!$page_footer) {
$page_footer = [];
}
if (isset($page_footer['PAGE'])) {
$footer_parts =
explode(L\WikiParser::END_HEAD_VARS, $page_footer['PAGE']);
}
$page_footer['PAGE'] = $page_footer['PAGE'] ?? "";
$data['PAGE_FOOTER'] = $this->rewriteRoutedGroupLinks(
(isset($footer_parts[1])) ?
$footer_parts[1] : "" . $page_footer['PAGE']);
}
$data['PAGE_ID'] = $group_model->getPageID($group_id,
$page, $locale_tag);
if (!empty($_REQUEST['sf'])) {
$sub_path = $this->clean($_REQUEST['sf'], 'string');
$sub_path = str_replace("..", "", $sub_path);
$sub_path = str_replace("/./", "/", $sub_path);
$data['SUB_PATH'] = htmlentities($sub_path);
} else {
$sub_path = "";
}
if (!empty($_REQUEST['arg']) && $_REQUEST['arg']=='media' &&
!empty($_REQUEST['n'])) {
$data['CURRENT_LOCALE_TAG'] = $locale_tag;
$this->component("social")->mediaWiki($data, $group_id,
$data['PAGE_ID'], $sub_path);
} else if (isset($head_info['page_type'])) {
if ($head_info['page_type'] == 'media_list') {
$data['GROUP']['GROUP_ID'] = $group_id;
$data['HEAD'] = $head_info;
$data['PAGE_NAME'] = $page;
$data['CAN_EDIT'] = false;
/* A page set to be a static HTML folder answers with the
file asked for beneath it, the same as it does when
read through the group controller, so a folder of
generated documentation works whichever url form it is
read by. */
if ($this->component("social")->serveStaticFolderFile(
$data, $group_id, $data['PAGE_ID'], $sub_path, 0)) {
return;
}
$data['MODE'] = "static";
$data['RESOURCE_FILTER'] =
(isset($_REQUEST['resource_filter'])) ?
substr($this->clean($_REQUEST['resource_filter'],
'file_name'), 0, C\SHORT_TITLE_LEN) : "";
$data['page_type'] = 'media_list';
$data['RESOURCES_INFO'] =
$group_model->getGroupPageResourceUrls(
$group_id, $data['PAGE_ID'], $sub_path);
$this->component("social")->sortWikiResources($data);
} else if ($head_info['page_type'] == 'git_repository') {
/* a repository page is read through this controller the
same way it is read through the group controller, so
the public group's repositories, and a routed domain's,
are browsable at their static p/ url too */
$data['GROUP']['GROUP_ID'] = $group_id;
$data['HEAD'] = $head_info;
$data['PAGE_NAME'] = $page;
$data['CAN_EDIT'] = false;
$data['MODE'] = "static";
$data['page_type'] = 'git_repository';
$this->component("social")->initializeGitRepositoryReadMode(
$data, $group_id, $sub_path);
} else if ($head_info['page_type'] == 'presentation') {
$data['page_type'] = 'presentation';
$data['INCLUDE_SCRIPTS'][] = "frise";
$data['INCLUDE_STYLES'][] = "frise";
}
}
if ((!empty($data['PAGE']) &&
str_contains($data['PAGE'], "canvas-360")) ||
(!empty($data['page']) &&
str_contains($data['page'], "canvas-360"))) {
$data["INCLUDE_SCRIPTS"] = array_merge($data["INCLUDE_SCRIPTS"],
["wglu-program", "vr-panorama", "vr-util"]);
$data["SCRIPT"] .= ";var tl_elt = elt('tl'); tl_elt.enter_vr ='".
tl('enter_vr') . "'; tl_elt.exit_vr = '".tl('exit_vr')."';";
}
return $data;
}
/**
* Reads a wiki page from this request's static group (the Public
* group, or a routed domain's landing group) to present to visitors
* who are not logged in.
*
* @param string $page_name name of file less extension to read in
* @return string text of page
*/
public function getPage($page_name)
{
$group_model = $this->model("group");
$group_id = $this->staticGroupId();
$locale_tag = L\getLocaleTag();
$page_info = $group_model->getPageInfoByName(
$group_id, $page_name, $locale_tag, "read");
$page_string = $page_info["PAGE"] ?? "";
if (!$page_string && $locale_tag != C\p('DEFAULT_LOCALE')) {
/* fallback to default locale for translation */
$page_info = $group_model->getPageInfoByName(
$group_id, $page_name, C\p('DEFAULT_LOCALE'), "read");
$page_string = $page_info["PAGE"] ?? "";
}
if (preg_match("/\(\(resource(\-?[a-z]+)?\:(.+?)csv(.+?)\|(.+?)\)\)/ui",
$page_string)) {
$page_string = $group_model->insertResourcesParsePage(
$group_id, $page_info['ID'], $locale_tag,
$page_string, "", "admin", true);
}
return $this->rewriteRoutedGroupLinks($page_string);
}
}