<?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;
use seekquarry\yioop\library\wiki as LW;

/**
     * StaticController shows 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
{
    /**
     * askedLocaleTag gives the language a page should be read in. That
     * is the language the reader is using, unless the address names
     * another: a wiki link may say which language of a page it wants,
     * and Yioop carries that as page_locale. A page has a copy in each
     * language, so a link that names one has to reach that copy rather
     * than the reader own. What the address names is read against the
     * languages the site knows, so it cannot ask for one the site does
     * not have.
     *
     * @return string the language tag to read a page in
     */
    public function askedLocaleTag()
    {
        $locale_tag = L\getLocaleTag();
        if (empty($_REQUEST['page_locale'])) {
            return $locale_tag;
        }
        $wanted = $this->clean($_REQUEST['page_locale'], "string");
        foreach ($this->model("locale")->getLocaleList() as $known) {
            if ($known['LOCALE_TAG'] == $wanted) {
                return $wanted;
            }
        }
        return $locale_tag;
    }
    /**
     * Says which activities (roughly methods invoke from the web)
     * this controller will respond to
     * @var array
     */
    public $activities = ["showPage", "signout"];
    /**
     * staticGroupId is the 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;
    }
    /**
     * rewriteRoutedGroupLinks writes 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/";
        $html = preg_replace(
            '/href="[^"]*\[\{token\}\]&amp;page_name=([^"]+)"/',
            'href="' . $static_page_url . '$1"', $html);
        /* A static page address is a path, so a language a link names
           opens the query rather than joining one. */
        return preg_replace('/\[{locale}\]([a-zA-Z\-_]{2,10})/',
            '?page_locale=$1', $html);
    }
    /**
     * readingWikiPageData says that the page being shown is a wiki page
     * being read, and
     * which page and group it is. A page served this way is a wiki page
     * like any other, so it says so the same way one served through its
     * group does, which is what lets a domain's own stylesheet reach
     * reading alone and reach one group or one page within it.
     *
     * @param string $page the name of the page being shown
     * @param int $group_id which group it belongs to
     * @param string $group_name that group's name, empty when it has
     *      none to give
     * @return array the fields to add to what the view is drawn from
     */
    public function readingWikiPageData($page, $group_id, $group_name)
    {
        $reading = ['MODE' => "read", 'PAGE_NAME' => $page];
        if (!empty($group_name)) {
            $reading['GROUP'] = ['GROUP_ID' => $group_id,
                'GROUP_NAME' => $group_name];
        }
        return $reading;
    }
    /**
     * processRequest is the way in 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);
    }
    /**
     * showPage draws one 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");
        $wiki_model = $this->model("wiki");
        $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(LW\WikiParser::END_HEAD_VARS, $page_string);
        $data['PAGE'] = $page_parts[1] ?? $page_parts[0];
        $data = array_merge($data, $this->readingWikiPageData($page,
            $group_id, $group_model->getGroupName($group_id)));
        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']) && $head_info['title']) {
            /* A wiki page served here shows its own title exactly, the same
               as it does through the group controller, rather than the
               title wrapped with the site name. The raw title parsed above
               is already in head_objects, so it is left as it is and only
               the on-page subtitle is set. On a routed domain this is that
               domain's group's page title, since the group read from is the
               routed one. */
            $data["subtitle"] = " - " . $head_info['title'];
        } else {
            /* A page that names no title of its own falls back to the
               site's name and the page's name, read as "Site Name - Page
               Name". The site name is the routed domain's when a domain is
               served here, so frise.org's untitled pages read for frise.org
               rather than the site. */
            $data["subtitle"] = "";
            $static_view->head_objects[$data['page']]['title'] =
                $this->untitledPageTitle($this->siteName(), $data['page']);
        }
        $locale_tag = $this->askedLocaleTag();
        $data['CONTROLLER'] = "static";
        $group_model = $this->model("group");
        $wiki_model = $this->model("wiki");
        $head_info = $this->headWithGroupAppearance($head_info,
            $group_model->getGroupById($group_id, C\ROOT_ID),
            $static_view, $data['page']);
        if (!empty($head_info['page_header']) &&
                $head_info['page_type'] != 'presentation') {
            $page_header = $wiki_model->getPageInfoByName($group_id,
                $head_info['page_header'], $locale_tag, "read");
            if (!$page_header) {
                $page_header = [];
            }
            if (isset($page_header['PAGE'])) {
                $header_parts =
                    explode(
                        LW\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 = $wiki_model->getPageInfoByName($group_id,
                $head_info['page_footer'], $locale_tag, "read");
            if (!$page_footer) {
                $page_footer = [];
            }
            if (isset($page_footer['PAGE'])) {
                $footer_parts =
                    explode(
                        LW\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'] = $wiki_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'] =
                    $wiki_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;
    }
    /**
     * headWithGroupAppearance gives a page's head the look its group
     * keeps for every page in it: the theme, the header page and the
     * footer page. A group's owner sets these once for the group rather
     * than on each page, and a reader coming by a p address sees the
     * same page as one coming by a group address, so both take them the
     * same way.
     *
     * A page names one of these itself only where the group allows a
     * page to be customized; otherwise what the group says stands.
     *
     * @param array $head_info what the page itself says about how it
     *     should look
     * @param array $group the group the page belongs to, as the group
     *     model gives it
     * @param object $view the view the page is drawn with, whose own
     *     copy of the head is set as well, since the theme is read from
     *     there when the surrounding page is laid out
     * @param string $page the name of the page being drawn
     * @return array the head with the group's look filled in
     */
    public function headWithGroupAppearance($head_info, $group, $view,
        $page)
    {
        if (empty($head_info) || !is_array($head_info)) {
            $head_info = [];
        }
        $customizable = !empty($group['PAGE_CUSTOMIZE_ALLOWED']);
        $from_group = ['page_theme' => $group['GROUP_THEME'] ?? "",
            'page_header' => $group['PAGE_HEADER'] ?? "",
            'page_footer' => $group['PAGE_FOOTER'] ?? ""];
        foreach ($from_group as $key => $said) {
            if ($said === "") {
                continue;
            }
            if (!$customizable || trim($head_info[$key] ?? "") === "") {
                $head_info[$key] = $said;
                if (isset($view->head_objects[$page])) {
                    $view->head_objects[$page][$key] = $said;
                }
            }
        }
        return $head_info;
    }
    /**
     * untitledPageTitle builds the browser title for a page that names no title
     * of its own, read as "Site Name - Page Name". The page name has its
     * url encoding undone and underscores read as spaces; the site name is
     * passed in already resolved, so on a routed domain it is that domain's
     * name rather than the site's.
     * @param string $site_name the serving site's or routed domain's name
     * @param string $page the requested page name, as it appears in urls
     * @return string the browser title for the untitled page
     */
    public function untitledPageTitle($site_name, $page)
    {
        $page_name_text = str_replace("_", " ", urldecode($page));
        return tl('static_controller_complete_title', $site_name,
            $page_name_text);
    }
    /**
     * getPage reads a wiki page from this request's 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");
        $wiki_model = $this->model("wiki");
        $group_id = $this->staticGroupId();
        $locale_tag = $this->askedLocaleTag();
        $page_info = $wiki_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 = $wiki_model->getPageInfoByName(
                $group_id, $page_name, C\p('DEFAULT_LOCALE'), "read");
            $page_string = $page_info["PAGE"] ?? "";
        }
        if (!$page_string && $group_id != C\PUBLIC_GROUP_ID &&
            isset(C\ERROR_PAGE_STATUSES[$page_name])) {
            /* A routed domain says things in its own words where it has
               them, but the pages the site itself needs to be able to
               show, such as the one saying a page is not there, it may
               never have been given; rather than answer with nothing at
               all, the site's own is shown. Only these pages fall back
               this way, so a domain's ordinary pages stay its own. */
            $page_info = $wiki_model->getPageInfoByName(
                C\PUBLIC_GROUP_ID, $page_name, $locale_tag, "read");
            $page_string = $page_info["PAGE"] ?? "";
            if (!$page_string && $locale_tag != C\p('DEFAULT_LOCALE')) {
                $page_info = $wiki_model->getPageInfoByName(
                    C\PUBLIC_GROUP_ID, $page_name,
                    C\p('DEFAULT_LOCALE'), "read");
                $page_string = $page_info["PAGE"] ?? "";
            }
            if ($page_string) {
                $group_id = C\PUBLIC_GROUP_ID;
            }
        }
        if (preg_match("/\(\(resource(\-?[a-z]+)?\:(.+?)csv(.+?)\|(.+?)\)\)/ui",
                    $page_string)) {
            $page_string = $wiki_model->insertResourcesParsePage(
                 $group_id, $page_info['ID'], $locale_tag,
                $page_string, "", "admin", true);
        }
        return $this->rewriteRoutedGroupLinks($page_string);
    }
}
