/ src / controllers / StaticController.php
<?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);
    }
}
X