<?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
 *
 * Main web interface entry point for Yioop!
 * search site. Used to both get and display
 * search results. Also used for inter-machine
 * communication during crawling
 *
 * @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;

use seekquarry\atto as A;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\mail as ML;
use seekquarry\yioop\models as M;

/**
 * Main entry point to the Yioop web app.
 *
 * Initialization is done in  a function to avoid polluting the global
 * namespace with variables.
 * @param object $web_site optional WebSite instance when running
 *      under the bundled CLI server (atto_servers/WebSite); null
 *      under a conventional PHP-FPM / Apache deployment
 * @param bool $start_new_session whether to start a session or not
 */
function bootstrap($web_site = null, $start_new_session = true)
{
    //check if mobile css and formatting should be used or not
    if (isset($_SERVER['HTTP_USER_AGENT'])) {
        $agent = $_SERVER['HTTP_USER_AGENT'];
        if (stristr($agent, "mobile") &&
            !stristr($agent, "ipad") ) {
            $_SERVER["MOBILE"] = true;
        } else {
            $_SERVER["MOBILE"] = false;
        }
    } else {
        $_SERVER["MOBILE"] = false;
    }
    /**
     * Did we come to this index.php from ../index.php? If so, rewriting
     * must be on
     */
    if (!C\nsdefined("REDIRECTS_ON")) {
        C\nsdefine("REDIRECTS_ON", false);
    }
    /**
     * Check if doing url rewriting, and if so, do initial routing
     */
    configureRewrites($web_site);
    if ((C\p('DEBUG_LEVEL') & C\ERROR_INFO) == C\ERROR_INFO) {
        set_error_handler(C\NS_CONFIGS . "yioop_error_handler");
    }
    /**
     * Load global functions related to localization
     */
    require_once __DIR__ . "/library/LocaleFunctions.php";
    ini_set("memory_limit", C\INDEX_FILE_MEMORY_LIMIT);
    if (!empty($web_site)) {
        if ((empty($_REQUEST['c']) || $_REQUEST['c'] != 'resource')) {
            $web_site->header(
                "Content-Security-Policy: frame-ancestors 'self'");
                //prevent click-jacking
        }
        $web_site->header("X-Content-Type-Options: nosniff"); /*
        Let browsers know that we should be setting the mimetype correctly --
        For non dumb browsers this should help prevent against XSS attacks
        to images containing HTML. Also, might help against PRSSI attacks.
        */
        if ($start_new_session) {
            if (checkCookieConsent()) {
                /*
                    Keep PHP from garbage-collecting session data
                    before Yioop's own AUTOLOGOUT window elapses.
                    The PHP default for session.gc_maxlifetime is
                    1440 seconds (24 minutes); when AUTOLOGOUT is
                    longer than that (it defaults to 1 hour), an
                    idle session can be GC'd server-side while
                    its cookie is still valid, kicking the user
                    back to the signin screen well before
                    AUTOLOGOUT actually expires. Setting it here
                    so the session file's GC window matches.
                 */
                $gc_window = max(intval(C\p('AUTOLOGOUT')),
                    intval(ini_get('session.gc_maxlifetime')));
                if (!headers_sent()) {
                    ini_set('session.gc_maxlifetime',
                        (string)$gc_window);
                }
                $options = ['name' => C\p('SESSION_NAME'),
                    'cookie_lifetime' => C\p('COOKIE_LIFETIME'),
                    'cookie_httponly' => true,
                    'cookie_samesite' => C\SAME_SITE_COOKIE];
                if (C\nsdefined("SECURE_COOKIE") && C\SECURE_COOKIE) {
                    $options["cookie_secure"] = true;
                }
                if (intval(C\p('COOKIE_LIFETIME')) <= 0) {
                    $options['cookie_lifetime'] = C\p('AUTOLOGOUT');
                }
                if (isset($_REQUEST['cookieconsent'])) {
                    if (empty($_REQUEST['cookieconsent']) ||
                        $_REQUEST['cookieconsent'] == "false") {
                        //remove cookie from browser if consent revoked
                        $options["cookie_lifetime"] = - C\ONE_HOUR;
                    }
                }
                $web_site->sessionStart($options);
                if (!empty($_GET['cookieconsent'])) {
                    $uri = preg_replace("/cookieconsent\=[a-zA-Z]+(\&+)?/", "",
                        $_SERVER['REQUEST_URI']);
                    if (strlen($uri) > 1 && str_ends_with($uri, "?")) {
                        $uri = substr($uri, 0, -1);
                    }
                    $web_site->header("HTTP/1.0 301 Moved Permanently");
                    $web_site->header("Location:" . $uri);
                    A\webExit();
                }
            }
        }
    }
    /**
     * Load global functions related to checking Yioop! version
     */
    require_once C\BASE_DIR . "/library/UpgradeFunctions.php";
    if (!function_exists('mb_internal_encoding')) {
        echo "PHP Zend Multibyte Support must be enabled for Yioop! to run.";
        exit();
    }
    /**
     * Make an initial setting of controllers. This can be overridden in
     * configs/LocalConfig.php
     */
    $available_controllers = ["admin", "api", "archive",  "cache",
        "classifier", "crawl", "fetch", "group", "jobs", "machine", "resource",
        "search", "static"];
    if (C\DISPLAY_TESTS) {
        $available_controllers[] = "tests";
    }
    if (function_exists(C\NS_CONFIGS . "localControllers")) {
        $available_controllers = array_merge($available_controllers,
            C\localControllers());
    }
    if (in_array(C\p('REGISTRATION_TYPE'), ['no_activation',
        'email_registration',
        'admin_activation'])) {
        $available_controllers[] = "register";
    }
    if (!C\p('WEB_ACCESS')) {
        $available_controllers = ["admin", "archive", "cache", "crawl",
            "fetch", "jobs", "machine"];
    }
    if (C\PROFILE && L\upgradeDatabaseWorkDirectoryCheck()) {
        /**
         * Load global functions needed to upgrade between versions
         * (note only do this if need to upgrade). Run before the
         * controller dispatch below, which may consult tables (such
         * as DOMAIN_ROUTE) that a pending upgrade introduces.
         */
        require_once C\BASE_DIR . "/library/VersionFunctions.php";
        L\upgradeDatabaseWorkDirectory();
    }
    //the request variable c is used to determine the controller
    if (!isset($_REQUEST['c'])) {
        $controller_name = "search";
        $route_group_id = 0;
        if (C\PROFILE && !isset($_REQUEST['q']) &&
            empty($_REQUEST['a'])) {
            /* a secure domain can be routed to serve a particular
               group's Main wiki page as its landing page (set in
               Server Settings); look the request host up before the
               site-wide landing page default is applied. Only a bare
               landing request is rewritten: requests carrying an
               action (for example /?a=signout, the sign out link)
               must keep their meaning on a routed domain
             */
            $host = requestHostNoPort();
            if ($host !== "") {
                $group_model = new M\GroupModel();
                $route_group_id =
                    $group_model->getDomainRouteGroupId($host);
            }
        }
        if ($route_group_id > 0) {
            /* a routed domain's landing page is served through the
               static controller so it renders as a forward-facing page
               like the site's own Main, rather than through the group
               controller's membership chrome. This holds for the Public
               group (world readable) and for any custom landing group an
               admin points a domain at in the Web Server field set. The
               resolved group id rides on a server variable the browser
               cannot set, since it is not an HTTP_ header, so only an
               admin-set DOMAIN_ROUTE, never a request parameter, can
               choose which group the static controller reads.
             */
            $controller_name = "static";
            $_REQUEST['c'] = "static";
            $_REQUEST['p'] = "Main";
            $_SERVER['ROUTE_GROUP_ID'] = $route_group_id;
        } else if (C\nsdefined('LANDING_PAGE') && C\p('LANDING_PAGE') &&
            !isset($_REQUEST['q'])) {
            $controller_name = "static";
            $_REQUEST['c'] = "static";
            $_REQUEST['p'] = "Main";
        }
    } else {
        $controller_name = $_REQUEST['c'];
    }
    if ($controller_name == "static" && C\PROFILE &&
        empty($_SERVER['ROUTE_GROUP_ID'])) {
        /* a routed domain serves not only its landing page but all of
           its static wiki pages through the static controller, so a link
           such as p/Bob on that domain must read the routed group's Bob
           page and not the site's Public one. Resolve the request host
           to its landing group here, covering the p/ clean urls and
           ?c=static requests the landing block above does not. The group
           comes from the admin-set DOMAIN_ROUTE table, so a request
           parameter cannot choose it.
         */
        $host = requestHostNoPort();
        if ($host !== "") {
            $group_model = $group_model ?? new M\GroupModel();
            $route_group_id = $group_model->getDomainRouteGroupId($host);
            if ($route_group_id > 0) {
                $_SERVER['ROUTE_GROUP_ID'] = $route_group_id;
            }
        }
    }
    if (!in_array($controller_name, $available_controllers))
    {
        $controller_name = "static";
        $_REQUEST['c'] = "static";
        $_REQUEST['p'] = "404";
    }
    // if no profile exists we force the page to be the configuration page
    if (!C\PROFILE || (C\nsdefined("FIX_NAME_SERVER") && C\FIX_NAME_SERVER)) {
        $controller_name = "admin";
    }
    $locale_tag = L\getLocaleTag();
    if (C\PROFILE && L\upgradeLocalesCheck($locale_tag)) {
        L\upgradeLocales();
        /* upgrade manipulations might mess with global local,
            so set it back here
         */
        L\setLocaleObject($locale_tag);
    }
    /**
     * Loads controller responsible for calculating
     * the data needed to render the scene
     *
     */
    $controller_class = C\NS_CONTROLLERS . ucfirst($controller_name) .
        "Controller";
    $controller = new $controller_class($web_site);
    /*
        Web mail pages read IMAP, signin verifies a password with a
        deliberately slow bcrypt hash, a wiki page can assemble a media
        list of a thousand or more resources, and registration and group
        actions send mail over SMTP. Managing machines fetches the status
        and logs of every configured machine, and a search query may fan
        out across index shards on other machines. Each can take long
        enough to freeze a single-process server, so run them through the
        cooperative path: the request runs inside a fiber that yields to
        the loop while the slow work happens, so one mailbox fetch, one
        login, one big page, one mail send, one machine round-up, or one
        search does not stall every other connection. deferResponse runs
        the work straight through under Apache or any other
        non-cooperative caller, so behaviour there is unchanged.
        Attachment downloads stay on the ordinary path: they stream binary
        bytes rather than render a page.
     */
    $reads_mailbox = ($_REQUEST['a'] ?? '') === 'userMail' &&
        !in_array($_REQUEST['arg'] ?? '',
            ['downloadAttachment', 'downloadAllAttachments']);
    $checks_credentials = isset($_REQUEST['u']) && isset($_REQUEST['p']);
    $renders_wiki = ($_REQUEST['a'] ?? '') === 'wiki';
    $sends_mail = ($_REQUEST['c'] ?? '') === 'register' ||
        in_array($_REQUEST['a'] ?? '', ['groupFeeds', 'manageGroups']);
    $manages_machines = ($_REQUEST['a'] ?? '') === 'manageMachines';
    $runs_search = $controller_name === 'search';
    if ($web_site !== null && ($reads_mailbox || $checks_credentials ||
        $renders_wiki || $sends_mail || $manages_machines ||
        $runs_search)) {
        $web_site->deferResponse(function () use ($controller) {
            $controller->processRequest();
        });
    } else {
        $controller->processRequest();
    }
}
/**
 * Checks if a cookie consent form was obtained. This
 * This function returns true if a session cookie
 * was received from the browser, or a form variable
 * saying cookies are okay was received, or the cookie
 * Yioop profile says the consent mechanism is disabled.
 * An Appearance domain switch counts too: it carries a
 * handoff token for an admin who is already signed in and
 * using cookies on the domain they came from, and the
 * domain they land on needs a session to sign them into.
 *
 * @return bool cookie consent (true) else false
 */
function checkCookieConsent()
{
    if (C\PROFILE && intval(C\p('COOKIE_LIFETIME')) > 0 &&
        empty($_COOKIE[C\p('SESSION_NAME')])
        && empty($_REQUEST['cookieconsent'])
        && empty($_REQUEST['domain_handoff'])) {
        $_SERVER["COOKIE_CONSENT"] = false;
        return false;
    }
    return true;
}
/**
 * Used to setup and handles url rewriting for the Yioop Web app
 *
 * Developers can add new routes by creating a Routes class in
 * the app_dir with a static method getRoutes which should return
 * an associating array of incoming_path => handler function
 * @param object $web_site used to send error pages if configuration
 *  fails
 */
function configureRewrites($web_site)
{
    /*
        In secure-server mode the plain port-80 listener exists only
        to answer the ACME HTTP-01 challenge and to send everyone
        else to HTTPS. A request that arrived on 80 has no HTTPS
        server flag (the 443 listener sets it). Redirect such
        requests to the https form of the same host and path, except
        the acme-challenge path, which the certificate authority
        must reach over plain HTTP during validation.
     */
    if (C\nsdefined("IS_SECURE_SERVER") && C\IS_SECURE_SERVER &&
        empty($_SERVER['HTTPS'])) {
        $request_uri = $_SERVER['REQUEST_URI'] ?? '/';
        $is_acme = strpos($request_uri,
            '/.well-known/acme-challenge/') !== false;
        if (!$is_acme) {
            $host = $_SERVER['HTTP_HOST'] ??
                ($_SERVER['SERVER_NAME'] ?? '');
            /*
                Drop any port from the Host header (for example a
                literal host:80) so the redirect target is the bare
                https host, not https://host:80. An IPv6 literal in
                brackets keeps its colons; only a trailing :port is
                removed.
             */
            if ($host !== '' && $host[0] !== '[') {
                $colon = strrpos($host, ':');
                if ($colon !== false) {
                    $host = substr($host, 0, $colon);
                }
            } else if ($host !== '' && $host[0] === '[') {
                $close = strpos($host, ']');
                if ($close !== false) {
                    $host = substr($host, 0, $close + 1);
                }
            }
            $web_site->header("HTTP/1.1 301 Moved Permanently");
            $web_site->header("Location: https://" . $host .
                $request_uri);
            A\webExit();
            return;
        }
    }
    $route_map = [
        'wd' => 'routeAppFile',
        'favicon.ico' => 'routeBaseFile',
        'robots.txt' => 'routeRobotsTxt',
        'humans.txt' => 'routeHumansTxt',
        '.well-known' => 'routeWellKnown',
        'yioopbar.xml' => 'routeBaseFile',
        'css' => 'routeBaseFile',
        'locale' => 'routeBaseFile',
        'resources' => 'routeBaseFile',
        'scripts' => 'routeBaseFile',
        'blog' => 'routeBlog',
        'admin' => 'routeController',
        'register' => 'routeController',
        'tests' => 'routeController',
        'advertise' => 'routeDirect',
        'bot' => 'routeDirect',
        'privacy' => 'routeDirect',
        'terms' => 'routeDirect',
        'group' => 'routeFeeds',
        'thread' => 'routeFeeds',
        'user' => 'routeFeeds',
        'user_messages' => 'routeUserMessages',
        'user_mail' => 'routeUserMail',
        's' => "routeSubsearch",
        'serp_icon' => 'routeSerpIcon',
        'suggest' => 'routeSuggest',
        'p' => 'routeWiki'
    ];
    if (class_exists(C\NS. "Routes")) {
        $route_map = array_merge($route_map, Routes::getRoutes());
    }
    /**
     * Check for paths of the form index.php/something which yioop doesn't
     * support
     */
    $s_name = $_SERVER['SCRIPT_NAME'] . "/";
    $path_name = substr($_SERVER["REQUEST_URI"], 0, strlen($s_name));
    if (strcmp($path_name, $s_name) == 0) {
        $_SERVER["PATH_TRANSLATED"] = C\BASE_DIR;
        $script_info = pathinfo($s_name);
        $_SERVER["PATH_INFO"] = ($script_info["dirname"] == "/") ? "" :
            $script_info["dirname"] ;
        $error = directUrl("error", false, true);
        $web_site->header("Location: $error");
        A\webExit();
    }
    if (!isset($_SERVER["PATH_INFO"])) {
        $_SERVER["PATH_INFO"] = ".";
    }
    if (!C\REDIRECTS_ON) {
        return;
    }
    /**
     * Now look for and handle routes
     */
    $index_php = "index.php";
    if ((php_sapi_name() == 'cli')) {
        $script_path = "/";
    } else {
        $script_path = substr($_SERVER['PHP_SELF'], 0, -strlen($index_php));
    }
    $request_script = "";
    if (empty($_SERVER['QUERY_STRING'])) {
        $request_script = rtrim(
            substr($_SERVER['REQUEST_URI'], strlen($script_path)), "?");
    } else {
        $q_pos = strpos($_SERVER['REQUEST_URI'], "?");
        if ($q_pos !== false) {
            $request_script = substr($_SERVER['REQUEST_URI'], 0,
                $q_pos);
        }
        $request_script = substr($request_script, strlen($script_path));
    }
    $request_script = ($request_script == "") ? $index_php : $request_script;
    if (in_array($request_script, ['', '/', $index_php])) {
        return;
    }
    $request_parts = explode("/", $request_script);
    $handled = false;
    if (isGitRepositoryRequest($request_parts)) {
        $handled = routeGit($request_parts);
    }
    if (!$handled && isset($route_map[$request_parts[0]])) {
        if (empty($_REQUEST['c']) || $_REQUEST['c'] == $request_parts[0]) {
            $route = C\NS . $route_map[$request_parts[0]];
            $handled = $route($request_parts, $web_site);
        } else if (!empty($_REQUEST['c'])) {
            $handled = true;
        }
    }
    if (!$handled) {
        $error = directUrl("error", false, true);
        $error_location = "Location:" . $error;
        if (!C\REDIRECTS_ON) {
            $error_location .= ".php";
        }
        if ($request_parts[0] != "error" && $request_parts[0] != "error.php") {
            $web_site->header($error_location);
            return;
        }
        $web_site->header("HTTP/1.0 404 Not Found");
        $route_args = ["404"];
        routeDirect($route_args);
    }
}
/**
 * Used to handle routes that will eventually just serve
 * files from either the APP_DIR
 * These include files like css, scripts, suggest tries, images, and videos.
 * @param array $route_args of url parts (split on slash)
 * @return bool whether was able to compute a route or not
 */
function routeAppFile($route_args)
{
    if (empty($route_args[2])) {
        return false;
    }
    $num_args = count($route_args);
    $_REQUEST['route']['c'] = true;
    $_REQUEST['c'] = "resource";
    $_REQUEST['route']['a'] = true;
    $_REQUEST['a'] = "get";
    if ($route_args[1] == 'suggest') {
        $_REQUEST['a'] = "suggest";
        $_REQUEST['route']['locale'] = true;
        $_REQUEST['locale'] = $route_args[2];
        return true;
    } else if ($route_args[1] == 'users') {
        $_REQUEST['route']['f'] = true;
        $_REQUEST['f'] = "resources";
        $_REQUEST['route']['s'] = true;
        $_REQUEST['s'] = $route_args[2];
        $_REQUEST['n'] = 'user_icon.webp';
        $_REQUEST['route']['n'] = true;
        return true;
    } else if (in_array($route_args[1], ['css', 'scripts', 'locale'])) {
        $_REQUEST['route']['f'] = true;
        $_REQUEST['f'] = $route_args[1];
        $_REQUEST['route']['n'] = true;
        $rest_args = array_slice($route_args, 2);
        if ($route_args[1] == 'css' && count($rest_args) > 1) {
            /* the themes an admin has added sit in a folder of their
               own under the app css folder, so the part of a theme's url
               before the file name is the folder to read it from */
            $_REQUEST['route']['sf'] = true;
            $_REQUEST['sf'] = array_shift($rest_args);
        }
        $_REQUEST['n'] = implode("/", $rest_args);
        return true;
    } else if (in_array($route_args[1], ["athumbs", "resources", "thumbs"])) {
        $_REQUEST['route']['f'] = true;
        $_REQUEST['f'] = "resources";
        if (in_array($route_args[1], ["thumbs", "athumbs"])) {
            if ($num_args < 6) {
                return false;
            }
            $_REQUEST['route']['t'] = true;
            $_REQUEST['t'] = rtrim($route_args[1], "s");
        }
        if ($num_args == 3) {
            $_REQUEST['route']['n'] = true;
            $_REQUEST['n'] = $route_args[2];
            return true;
        } else if ($num_args == 4 && $route_args[1] == "resources") {
            /* /wd/resources/<subfolder>/<file>: a resource in a
               named subfolder of the resources directory, used by
               per-domain appearance images which live in a
               per-domain subfolder. The thumbs/athumbs forms always
               carry the group/page token shape (num_args >= 6), so
               this shorter form is resources-only. */
            $_REQUEST['route']['sf'] = true;
            $_REQUEST['sf'] = urldecode($route_args[2]);
            $_REQUEST['route']['n'] = true;
            $_REQUEST['n'] = $route_args[3];
            return true;
        } else if ($num_args >= 6) {
            $token_parts = explode("=", $route_args[2]);
            if (count($token_parts) != 2 && $route_args[2] != "-") {
                return false;
            }
            $_REQUEST['route'][$token_parts[0]] = true;
            if (count($token_parts) == 2) {
                $_REQUEST[$token_parts[0]] = $token_parts[1];
            }
            $_REQUEST['route']['g'] = true;
            $_REQUEST['g'] = $route_args[3];
            $_REQUEST['route']['p'] = true;
            $_REQUEST['p'] = $route_args[4];
            if ($num_args == 6) {
                $_REQUEST['route']['n'] = true;
                $_REQUEST['n'] = $route_args[5];
                return true;
            }
            if ($num_args >= 7) {
                $_REQUEST['route']['n'] = true;
                $_REQUEST['n'] = array_pop($route_args);
                $path = implode("/", array_slice($route_args, 5));
                $_REQUEST['route']['sf'] = true;
                $_REQUEST['sf'] = urldecode(urldecode($path));
                return true;
            }
        }
    }
    return false;
}
/**
 * Used to handle routes that will eventually just serve
 * files from either the BASE_DIR
 * These include files like css, scripts, images, and robots.txt.
 * @param array $route_args of url parts (split on slash).
 * @return bool whether was able to compute a route or not
 */
function routeBaseFile($route_args)
{
    $_REQUEST['route']['c'] = true;
    $_REQUEST['c'] = "resource";
    $_REQUEST['route']['a'] = true;
    $_REQUEST['a'] = "get";
    $_REQUEST['route']['b'] = true;
    $_REQUEST['b'] = $route_args[0];
    if (count($route_args) == 1) {
        $_REQUEST['route']['n'] = true;
        $_REQUEST['n'] = '-';
    } else {
        $_REQUEST['route']['n'] = true;
        array_shift($route_args);
        $_REQUEST['n'] = implode("/", $route_args);
    }
    return true;
}
/**
 * Serves the MTA-STS policy file (RFC 8461) at the well-known
 * path /.well-known/mta-sts.txt. Sending mail servers fetch this
 * over HTTPS from the dedicated host mta-sts.<domain> to learn
 * whether this site asks them to require TLS when delivering to
 * <domain>. This route is only reached when clean URLs
 * (REDIRECTS_ON) are on, since the rooted path depends on
 * rewriting; that matches the Server Settings rule that the
 * secure delivery postures need clean URLs.
 *
 * The policy is emitted directly as text/plain and the script
 * exits, rather than dispatched to a controller, because the body
 * is a few generated lines with no template, session, or CSRF
 * involvement. The mode comes from the delivery-security posture
 * ('spam' publishes testing, 'require' publishes enforce,
 * 'insecure' publishes nothing); the policy domain comes from the
 * request host (mta-sts.<configured-domain>, or the first
 * configured domain for a loopback test host). When the path is
 * not mta-sts.txt, the host does not resolve to a configured
 * domain, or the posture publishes no policy, this returns false
 * so the normal not-found handling applies.
 *
 * @param array $route_args url parts split on slash; the first is
 *      '.well-known' and the second should be 'mta-sts.txt'
 * @param object $web_site the WebSite server, used for its header
 *      method so the response is buffered and flushed correctly
 * @return bool false when no policy is served (fall through to the
 *      404 path); on the served path the response is emitted and
 *      webExit (atto) ends the request
 */
function routeWellKnown($route_args, $web_site)
{
    /*
        ACME HTTP-01 challenge: the certificate authority fetches
        /.well-known/acme-challenge/<token> over port 80 during
        validation and expects the token's key authorization back as
        plain text. The token is the path segment after
        acme-challenge; AcmeChallengeStore maps it to the key
        authorization the issuing process recorded. An unknown or
        malformed token returns 404 (the store rejects any token
        that is not a base64url string, so no path traversal is
        possible).
     */
    if (!empty($route_args[1]) && $route_args[1] === 'acme-challenge') {
        $token = $route_args[2] ?? '';
        $store = new L\AcmeChallengeStore();
        $key_authorization = $store->get($token);
        if ($key_authorization === null) {
            $web_site->header("HTTP/1.1 404 Not Found");
            A\webExit();
            return true;
        }
        $web_site->header("HTTP/1.1 200 OK");
        $web_site->header("Content-Type: application/octet-stream");
        $web_site->header("Content-Length: " .
            strlen($key_authorization));
        echo $key_authorization;
        A\webExit();
        return true;
    }
    if (empty($route_args[1]) || $route_args[1] !== 'mta-sts.txt') {
        return false;
    }
    $domains = ML\MailSiteFactory::localDomains();
    $host = $_SERVER['HTTP_HOST'] ?? '';
    $domain = ML\MailSiteFactory::mtaStsDomainForHost($host,
        $domains);
    if ($domain === '') {
        return false;
    }
    $delivery_security = C\nsdefined("MAIL_DELIVERY_SECURITY") ?
        C\p('MAIL_DELIVERY_SECURITY') : 'insecure';
    $mode = ML\MailSiteFactory::mtaStsMode($delivery_security);
    $policy = ML\MailSiteFactory::mtaStsPolicy($mode, $domain);
    if ($policy === '') {
        return false;
    }
    /* Use the WebSite object's header method and webExit so the
       response goes through its output buffer; when Yioop runs as
       its own web server the headers are buffered and flushed with
       the body, so PHP's bare header()/exit would arrive after
       output had already started. */
    $web_site->header("Content-Type: text/plain; charset=utf-8");
    $web_site->header("Content-Length: " . strlen($policy));
    echo $policy;
    A\webExit();
}
/**
 * Looks up the saved robots or humans setting for the domain in the
 * current request. Reads the per-domain appearance overrides and
 * returns whatever is stored under the given key, or an empty value
 * when the domain has nothing saved.
 *
 * @param string $key either 'ROBOTS' (the saved crawl choices array)
 *      or 'HUMANS' (the humans.txt text)
 * @return mixed the saved value, or [] for ROBOTS and "" for HUMANS
 *      when the domain has no saved value
 */
function robotsHumansForHost($key)
{
    $default = ($key == 'ROBOTS') ? [] : "";
    $host = strtolower($_SERVER['HTTP_HOST'] ?? "");
    $colon = strrpos($host, ':');
    if ($colon !== false && strpos($host, ']') === false) {
        $host = substr($host, 0, $colon);
    }
    if ($host === "") {
        return $default;
    }
    $group_model = new M\GroupModel();
    $appearance = $group_model->getDomainAppearance($host);
    return $appearance[$key] ?? $default;
}
/**
 * Serves the robots.txt file for the domain in the request. The body
 * is built fresh from the crawl choices saved for that domain (a
 * domain with no saved choices gets a sensible default that keeps
 * crawlers out of search-result addresses). The text is written
 * through the WebSite object so its headers buffer with the body when
 * Yioop is running as its own web server.
 *
 * @param array $route_args path pieces of the request (not used; the
 *      domain comes from the request host)
 * @param object $web_site WebSite object used to send headers and end
 *      the response
 * @return bool always true, the request is fully handled here
 */
function routeRobotsTxt($route_args, $web_site)
{
    $config = robotsHumansForHost('ROBOTS');
    $body = L\RobotsTxtGenerator::generate($config);
    $web_site->header("Content-Type: text/plain; charset=utf-8");
    $web_site->header("Content-Length: " . strlen($body));
    echo $body;
    A\webExit();
    return true;
}
/**
 * Serves the humans.txt file for the domain in the request. The body
 * is the free-form humans.txt text saved for that domain, or empty
 * when nothing is saved. The text is written through the WebSite
 * object so its headers buffer with the body when Yioop is running as
 * its own web server.
 *
 * @param array $route_args path pieces of the request (not used; the
 *      domain comes from the request host)
 * @param object $web_site WebSite object used to send headers and end
 *      the response
 * @return bool always true, the request is fully handled here
 */
function routeHumansTxt($route_args, $web_site)
{
    $humans = robotsHumansForHost('HUMANS');
    $body = is_string($humans) ? $humans : "";
    $web_site->header("Content-Type: text/plain; charset=utf-8");
    $web_site->header("Content-Length: " . strlen($body));
    echo $body;
    A\webExit();
    return true;
}
/**
 * Used to route page requests to pages that are fixed Public Group wiki
 * that should always be present. For example, 404 page.
 *
 * @param array $route_args of url parts (split on slash).
 * @return bool whether was able to compute a route or not
 */
function routeDirect($route_args)
{
    $_REQUEST['route']['c'] = true;
    $_REQUEST['c'] = "static";
    $_REQUEST['route']['p'] = true;
    $_REQUEST['p'] = $route_args[0];
    return true;
}
/**
 * Given the name of a fixed public group static page creates the url
 * where it can be accessed in this instance of Yioop, making use of the
 * defined variable REDIRECTS_ON.
 *
 * @param string $name of static page
 * @param bool $with_delim whether it should be terminated with nothing or
 *      ? or &
 * @param bool $with_base_url whether to use SHORT_BASE_URL or BASE_URL (true).
 * @return string url for the page in question
 */
function directUrl($name, $with_delim = false, $with_base_url = false)
{
    $base_url = ($with_base_url) ? C\baseUrl() : C\SHORT_BASE_URL;
    if (C\REDIRECTS_ON) {
        $delim = ($with_delim) ? "?" : "";
        return $base_url . $name . $delim;
    } else {
        $delim = ($with_delim) ? "&" : "";
        return "$base_url$name.php$delim";
    }
}
/**
 * Used to route page requests to for the website's public blog
 *
 * @param array $route_args of url parts (split on slash).
 * @return bool whether was able to compute a route or not
 */
function routeBlog($route_args)
{
    $_REQUEST['route']['c'] = true;
    $_REQUEST['c'] = "group";
    $_REQUEST['route']['a'] = true;
    $_REQUEST['a'] = "groupFeeds";
    $_REQUEST['route']['just_group_id'] = true;
    $_REQUEST['just_group_id'] = 2;
    return true;
}
/**
 * Reports whether a request path looks like a request for a git wiki page's
 * repository. Such a path is a numeric group id then a page name ending in
 * ".git", for example 212/Main.git/info/refs, and may also carry a leading
 * "group" segment to match the wiki page's own address, as in
 * group/212/Main.git/info/refs.
 *
 * @param array $request_parts request path split on slashes
 * @return bool true if the path is for a git repository
 */
function isGitRepositoryRequest($request_parts)
{
    if (isset($request_parts[0]) && $request_parts[0] === "group" &&
        isset($request_parts[1]) && ctype_digit($request_parts[1]) &&
        isset($request_parts[2]) &&
        str_ends_with($request_parts[2], ".git")) {
        return true;
    }
    return isset($request_parts[0]) && isset($request_parts[1]) &&
        $request_parts[0] !== "" && ctype_digit($request_parts[0]) &&
        str_ends_with($request_parts[1], ".git");
}
/**
 * Turns a git repository request path into the group-controller git activity
 * request that serves it. Any leading "group" segment is dropped first, then
 * the group id, the page name with its ".git" suffix removed, and the path
 * within the repository are read out of the path and placed where the git
 * activity expects to find them.
 *
 * @param array $request_parts request path split on slashes
 * @return bool always true, since a git path is handled here
 */
function routeGit($request_parts)
{
    if (isset($request_parts[0]) && $request_parts[0] === "group") {
        array_shift($request_parts);
    }
    $_REQUEST['c'] = "group";
    $_REQUEST['route']['c'] = true;
    $_REQUEST['a'] = "gitService";
    $_REQUEST['route']['a'] = true;
    $_REQUEST['group_id'] = $request_parts[0];
    $_REQUEST['route']['group_id'] = true;
    $_REQUEST['page_name'] = substr($request_parts[1], 0, -strlen(".git"));
    $_REQUEST['route']['page_name'] = true;
    $_REQUEST['sf'] = implode("/", array_slice($request_parts, 2));
    $_REQUEST['route']['sf'] = true;
    return true;
}
/**
 * Used to route requests for a group or thread feed.
 *
 * @param array $route_args of url parts (split on slash)
 * @return bool whether was able to compute a route or not
 */
function routeFeeds($route_args)
{
    $handled = true;
    if (isset($route_args[1]) && $route_args[1] == intval($route_args[1])) {
        $_REQUEST['c'] = "group";
        if (!empty($route_args[2])) {
            $_REQUEST['a'] = 'wiki';
            if ($route_args[2] == 'pages') {
                $_REQUEST['arg'] = 'pages';
                $_REQUEST['route']['arg'] = true;
            } else {
                if (empty($_REQUEST['page_name'])) {
                    $_REQUEST['page_name'] = $route_args[2];
                    $_REQUEST['route']['page_name'] = true;
                }
                if (empty($_REQUEST['sf']) && !empty($route_args[3]) ) {
                    $rest = array_slice($route_args, 3);
                    $_REQUEST['sf'] = implode("/", $rest);
                    $_REQUEST['route']['sf'] = true;
                }
            }
        }
        $_REQUEST['a'] = (isset($_REQUEST['a']) &&
            $_REQUEST['a'] == 'wiki') ? 'wiki' : "groupFeeds";
        $_REQUEST['route']['c'] = true;
        $_REQUEST['route']['a'] = true;
        $end = ($route_args[0] == 'thread') ? "" : "_id";
        if ($_REQUEST['a'] == 'wiki') {
            $_REQUEST['group_id'] = $route_args[1];
            $_REQUEST['route']['group_id'] = true;
        } else {
            $just_id = "just_" . $route_args[0] . $end;
            $_REQUEST[$just_id] = $route_args[1];
            $_REQUEST['route'][$just_id] = true;
        }
    } else if (!isset($route_args[1])) {
        $_REQUEST['c'] = "group";
        $_REQUEST['a'] = (isset($_REQUEST['a']) &&
            $_REQUEST['a'] == 'wiki') ? $_REQUEST['a'] : "groupFeeds";
        $_REQUEST['route']['c'] = true;
        $_REQUEST['route']['a'] = true;
    } else {
        $handled = false;
    }
    return $handled;
}
/**
 * Given the type of feed, the identifier of the feed instance, and which
 * controller is being used creates the url where that feed item can be
 * accessed from the instance of Yioop. It makes use of the
 * defined variable REDIRECTS_ON.
 *
 * @param string $type of feed: group, user, user messages, thread
 * @param int $id the identifier for that feed.
 * @param bool $with_delim whether it should be terminated with nothing or
 *      ? or &
 * @param string $controller which controller is being used to access the
 *      feed: usually admin or group
 * @param bool $use_short_base_url whether to create the url as a relative
 *   url using C\SHORT_BASE_URL or as a full url using  C\baseUrl()
 *   (the latter is useful for mail notifications)
 * @return string url for the page in question
 */
function feedsUrl($type, $id, $with_delim = false, $controller = "group",
    $use_short_base_url = true)
{
    $base_url = ($use_short_base_url) ? C\SHORT_BASE_URL : C\baseUrl();
    if (C\REDIRECTS_ON && $controller == 'group') {
        $delim = ($with_delim) ? "?" : "";
        $path = ($type == "") ? "group" : "$type/$id";
        return "$base_url$path$delim";
    } else {
        $delim = ($with_delim) ? "&" : "";
        $begin = (C\REDIRECTS_ON && $controller == "admin") ?
            "admin?" : "?c=$controller&";
        $query = "{$begin}a=groupFeeds";
        $end = ($type == 'thread') ? "" : "_id";
        if ($type != "") {
            if ($begin == "admin?" && $type == "group") {
                $query = "admin/$id";
                $delim = "?";
            } else {
                $query .= "&just_{$type}$end=$id";
            }
        }
        return "$base_url$query$delim";
    }
}
/**
 *
 * @param array $route_args of url parts (split on slash).
 * @return bool whether was able to compute a route or not
 */
function routeUserMessages($route_args)
{
    $handled = true;
    $_REQUEST['c'] = "group";
    $_REQUEST['a'] = 'userMessages';
    $_REQUEST['route']['c'] = true;
    $_REQUEST['route']['a'] = true;
    if (isset($route_args[1]) && $route_args[1] == intval($route_args[1])) {
        $_REQUEST['contact_id'] = $route_args[1];
        $_REQUEST['route']['contact_id'] = true;
    }
    return $handled;
}
/**
 * Used to route static file (or short alias) requests for the user_mail
 * URL to the GroupController's userMail activity. Mirrors routeUserMessages.
 *
 * @param array $route_args of url parts (split on slash)
 * @return bool whether was able to compute a route or not
 */
function routeUserMail($route_args)
{
    $handled = true;
    $_REQUEST['c'] = "group";
    $_REQUEST['a'] = 'userMail';
    $_REQUEST['route']['c'] = true;
    $_REQUEST['route']['a'] = true;
    return $handled;
}
/**
 * Used to route page requests to end-user controllers such as
 * register, admin. urls ending with /controller_name will
 * be routed to that controller.
 *
 * @param array $route_args of url parts (split on slash).
 * @return bool whether was able to compute a route or not
 */
function routeController($route_args)
{
    $_REQUEST['c'] = $route_args[0];
    $_REQUEST['route']['c'] = true;
    if (isset($route_args[1]) && intval($route_args[1]) == $route_args[1]) {
        if (isset($_REQUEST['a']) && $_REQUEST['a'] == 'wiki') {
            $_REQUEST['group_id'] = $route_args[1];
        } else if (!empty($route_args[2])) {
            $_REQUEST['a'] = 'wiki';
            $_REQUEST['group_id'] = $route_args[1];
            if ($route_args[2] == 'pages') {
                $_REQUEST['arg'] = 'pages';
                $_REQUEST['route']['arg'] = true;
            } else {
                $_REQUEST['page_name'] = $route_args[2];
                if (empty($_REQUEST['sf']) && !empty($route_args[3]) ) {
                    $rest = array_slice($route_args, 3);
                    $_REQUEST['sf'] = implode("/", $rest);
                    $_REQUEST['route']['sf'] = true;
                }
                $_REQUEST['route']['page_name'] = true;
            }
            $_REQUEST['route']['page_name'] = true;
            $_REQUEST['route']['a'] = true;
        } else {
            $_REQUEST['a'] = 'groupFeeds';
            $_REQUEST['just_group_id'] = $route_args[1];
        }
        $_REQUEST['route']['group_id'] = true;
    }
    return true;
}
/**
 * Given the name of a controller for which an easy end-user link is useful
 * creates the url where it can be accessed on this instance of Yioop,
 * making use of the defined variable REDIRECTS_ON. Examples of end-user
 * controllers would be the admin, and register controllers.
 *
 * @param string $name of controller
 * @param bool $with_delim whether it should be terminated with nothing or
 *      ? or &
 * @return string url for the page in question
 */
function controllerUrl($name, $with_delim = false)
{
    $base_url = C\SHORT_BASE_URL;
    if (C\REDIRECTS_ON) {
        $delim = ($with_delim) ? "?" : "";
        return $base_url . $name . $delim;
    } else {
        $delim = ($with_delim) ? "&" : "";
        return $base_url . "?c=$name$delim";
    }
}
/**
 * Used to route page requests for subsearches such as news, video, and images
 * (site owner can define other). Urls of the form /s/subsearch will
 * go the page handling the subsearch.
 *
 * @param array $route_args of url parts (split on slash).
 * @return bool whether was able to compute a route or not
 */
function routeSubsearch($route_args)
{
    $handled = true;
    if (isset($route_args[1])) {
        $_REQUEST['route']['c'] = true;
        $_REQUEST['route']['s'] = true;
        $_REQUEST['c'] = "search";
        $_REQUEST['s'] = $route_args[1];
    } else {
        $handled = false;
    }
    return $handled;
}
/**
 * Given the name of a subsearch  creates the url where it can be accessed
 * on this instance of Yioop, making use of the defined variable REDIRECTS_ON.
 * Examples of subsearches include news, video, and images. A site owner
 * can add to these and delete from these.
 *
 * @param string $name of subsearch
 * @param bool $with_delim whether it should be terminated with nothing or
 *      ? or &
 * @return string url for the page in question
 */
function subsearchUrl($name, $with_delim = false)
{
    $base_url = C\SHORT_BASE_URL;
    if (C\REDIRECTS_ON) {
        $delim = ($with_delim) ? "?" : "";
        return $base_url ."s/$name$delim";
    } else {
        $delim = ($with_delim) ? "&" : "";
        return "$base_url?s=$name$delim";
    }
}
/**
 * Used to route requests for favicons for pages in search results
 *
 * @param array $route_args of url parts (split on slash).
 * @return bool whether was able to compute a route or not
 */
function routeSerpIcon($route_args)
{
    $_REQUEST['c'] = "search";
    $_REQUEST['a'] = "cache";
    $_REQUEST['field'] = "favicon";
    $_REQUEST['arg'] = L\webdecode($route_args[2] ?? "");
    $_REQUEST['its'] = $route_args[1] ?? 0;
    $_REQUEST['route']['c'] = true;
    $_REQUEST['route']['a'] = true;
    $_REQUEST['route']['arg'] = true;
    $_REQUEST['route']['field'] = true;
    $_REQUEST['route']['its'] = true;
    return true;
}
/**
 * Return the url to repquest the favicon for a page in the search resutls,
 * making use of the defined variable REDIRECTS_ON.
 *
 * @param string $url URL of the page whose favicon URL should be
 *      built (will be URL-encoded into the returned link)
 * @param int|string $crawl_time crawl timestamp the icon should be
 *      pulled out of
 * @param bool $with_delim whether it should be terminated with nothing or
 *      ? or &
 * @return string url for the page in question
 */
function serpIconUrl($url, $crawl_time, $with_delim = false)
{
    $base_url = C\SHORT_BASE_URL;
    if (C\REDIRECTS_ON) {
        $delim = ($with_delim) ? "?" : "";
        return $base_url . "serp_icon/$crawl_time/" . L\webencode($url) .
            "$delim";
    } else {
        $delim = ($with_delim) ? "&" : "";
        return "$base_url?c=search&a=cache&field=favicon&its=$crawl_time&".
            "arg=" . urlencode($url) . $delim;
    }
}
/**
 * Used to route requests for the suggest-a-url link on the tools page.
 * If redirects on, then /suggest routes to this suggest-a-url page.
 *
 * @param array $route_args of url parts (split on slash).
 * @return bool whether was able to compute a route or not
 */
function routeSuggest($route_args)
{
    $_REQUEST['c'] = "register";
    $_REQUEST['a'] = "suggestUrl";
    return true;
}
/**
 * Return the url for the suggest-a-url link on the more tools page, making use
 * of the defined variable REDIRECTS_ON.
 *
 * @param bool $with_delim whether it should be terminated with nothing or
 *      ? or &
 * @return string url for the page in question
 */
function suggestUrl($with_delim = false)
{
    $base_url = C\SHORT_BASE_URL;
    if (C\REDIRECTS_ON) {
        $delim = ($with_delim) ? "?" : "";
        return $base_url ."suggest$delim";
    } else {
        $delim = ($with_delim) ? "&" : "";
        return "$base_url?c=register&a=suggestUrl$delim";
    }
}
/**
 * Used to route page requests for pages corresponding to a wiki page of
 * group. If it is a wiki page for the public group viewed without being
 * logged in, the route might come in as yioop_instance/p/page_name if
 * redirects are on. If it is for a non-public wiki or page accessed with
 * logged in the url will look like either:
 * yioop_instance/group/group_id?a=wiki&page_name=some_name
 * or
 * yioop_instance/admin/group_id?a=wiki&page_name=some_name&csrf_token_string
 *
 * @param array $route_args of url parts (split on slash).
 * @return bool whether was able to compute a route or not
 */
function routeWiki($route_args)
{
    $handled = true;
    if (isset($route_args[1])) {
        if ($route_args[1] == 'pages') {
            $_REQUEST['c'] = "group";
            $_REQUEST['a'] = 'wiki';
            $_REQUEST['arg'] = 'pages';
            $_REQUEST['route']['c'] = true;
            $_REQUEST['route']['a'] = true;
            $_REQUEST['route']['arg'] = true;
        } else {
            $_REQUEST['c'] = "static";
            $_REQUEST['p'] = $route_args[1];
            $_REQUEST['route']['c'] = true;
            $_REQUEST['route']['p'] = true;
            if (empty($_REQUEST['sf']) && !empty($route_args[2]) ) {
                $rest = array_slice($route_args, 2);
                $_REQUEST['sf'] = implode("/", $rest);
                $_REQUEST['route']['sf'] = true;
            }
        }
    } else {
        $handled = false;
    }
    return $handled;
}
/**
 * Reads the host name of the current request with any port number
 * dropped, lower-cased, so it can be matched against the domain routing
 * table. An IPv6 host in brackets keeps its colons; only a trailing
 * ":port" on an ordinary host is removed.
 *
 * @return string the request host without a port, or the empty string
 *      when the request carries no host
 */
function requestHostNoPort()
{
    $host = strtolower($_SERVER['HTTP_HOST'] ?? "");
    $colon = strrpos($host, ':');
    if ($colon !== false && strpos($host, ']') === false) {
        $host = substr($host, 0, $colon);
    }
    return $host;
}
/**
 * Given the name of a wiki page, the group it belongs to, and which
 * controller is being used creates the url where that wiki item can be
 * accessed from the instance of Yioop. It makes use of the
 * defined variable REDIRECTS_ON.
 *
 * @param string $name of wiki page
 * @param bool $with_delim whether it should be terminated with nothing or
 *      ? or &
 * @param string $controller which controller is being used to access the
 *      feed: usually static (for the public group), admin, or group
 * @param int $id the group the wiki page belongs to
 * @return string url for the page in question
 */
function wikiUrl($name, $with_delim = false, $controller = "static", $id =
    C\PUBLIC_GROUP_ID)
{
    $q = ($with_delim) ? "?" : "";
    $a = ($with_delim) ? "&" : "";
    $is_static = ($controller == "static");
    $base_url = C\SHORT_BASE_URL;
    if (C\REDIRECTS_ON && $controller != 'api') {
        $q = ($with_delim) ? "?" : "";
        if ($is_static) {
            if ($name == "") {
                $name = "Main";
            }
            return $base_url ."p/$name$q";
        } else {
            $page = ($name== "") ? "?a=wiki$a" : "/$name$q";
            return $base_url .
                "$controller/$id$page";
        }
    } else {
        $delim = ($with_delim) ? "&" : "";
        if ($name == 'pages') {
            if ($is_static) {
                $controller = "group";
            }
            return "$base_url?c=$controller&a=wiki&arg=pages&group_id=$id$a";
        } else {
            if ($is_static) {
                if ($name == "") {
                    $name = "main";
                }
                return "$base_url?c=static&p=$name$a";
            } else {
                $page = ($name== "") ? "" : "&page_name=$name";
                return "$base_url?c=$controller&a=wiki&group_id=$id$page$a";
            }
        }
    }
}
if (php_sapi_name() != 'cli' &&
    (empty($web_site) &&
    !defined("seekquarry\\yioop\\configs\\REDIRECTS_ON") &&
    !defined("seekquarry\\yioop\\configs\\LIBRARY"))) {
    /**
     * For error function and yioop constants if we are in non-cli
     * non-redirects situation
     */
    require_once __DIR__ . "/library/Utility.php";
    $web_site =  new A\WebSite();
    bootstrap($web_site);
}
