<?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\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\CrawlConstants;
use seekquarry\yioop\library\MediaConstants;
use seekquarry\yioop\library\UrlParser;

/**
 * Controller used to manage networked installations of Yioop
 * where there might be mulliple QueueServers and a NameServer. Command
 * sent to the nameserver web page are mapped out to queue_servers
 * using this controller. Each method of the controller essentially
 * mimics one method of CrawlModel, PhraseModel, or in general anything
 * that extends ParallelModel and is used to proxy that information
 * through a result web page back to the name_server.
 *
 * @author Chris Pollett
 */
class CrawlController extends Controller implements CrawlConstants
{
    /**
     * These are the activities supported by this controller
     * @var array
     */
    public $activities = ["clearFeedData", "clearQuerySavePoint",
        "crawlStalled", "crawlStatus", "deleteCrawl", "injectUrlsCurrentCrawl",
        "combinedCrawlInfo", "getInfoTimestamp", "getCrawlItems",
        "getCrawlList", "getCrawlSeedInfo", "sendStartCrawlMessage",
        "sendStopCrawlMessage", "setCrawlSeedInfo",
    ];
    /**
     * Checks that the request seems to be coming from a legitimate fetcher then
     * determines which activity the fetcher is requesting and calls that
     * activity for processing.
     */
    public function processRequest()
    {
        $data = [];
        /* do a quick test to see if this is a request seems like
           from a legitimate machine
         */
        if (!$this->checkRequest()) {
            return;
        }
        $activity = $_REQUEST['a'];
        if (in_array($activity, $this->activities)) {
            $this->call($activity);
        }
    }
    /**
     * Handles a request for whether or not the crawl is stalled on the
     * given local server (which means no fetcher has spoken to it in a while)
     * outputs this info back as body of the http response (url encoded,
     * serialized php data)
     */
    public function crawlStalled()
    {
        echo L\webencode(serialize($this->model("crawl")->crawlStalled()));
    }
    /**
     * Handles a request for the crawl status (memory use, recent fetchers
     * crawl rate, etc) data from a remote name server
     * and retrieves that the statistic about this that are held by the
     * local queue server
     * outputs this info back as body of the http response (url encoded,
     * serialized php data)
     */
    public function crawlStatus()
    {
        echo L\webencode(serialize($this->model("crawl")->crawlStatus()));
    }
    /**
     * Handles a request for the starting parameters of a crawl of a given
     * timestamp and retrieves that information from the bundle held by the
     * local queue server
     * outputs this info back as body of the http response (url encoded,
     * serialized php data)
     */
    public function getCrawlSeedInfo()
    {
        $timestamp = 0;
        if (isset($_REQUEST["arg"]) ) {
            $timestamp = unserialize(L\webdecode($_REQUEST["arg"]));
            $timestamp = substr($this->clean($timestamp, "int"), 0,
                C\TIMESTAMP_LEN);
        }
        echo L\webencode(serialize($this->model("crawl")->getCrawlSeedInfo(
            $timestamp)));
    }
    /**
     * Handles a request to change the parameters of a crawl of a given
     * timestamp on the local machine (does nothing if crawl doesn't exist)
     */
    public function setCrawlSeedInfo()
    {
        if (isset($_REQUEST["arg"]) ) {
            list($timestamp, $info) = unserialize(
                L\webdecode($_REQUEST["arg"]));
            $timestamp = substr($this->clean($timestamp, "int"), 0,
                C\TIMESTAMP_LEN);
            if ($timestamp && $info) {
                $this->model("crawl")->setCrawlSeedInfo($timestamp, $info);
            }
        }
    }
    /**
     * Handles a request for information about a crawl with a given timestamp
     * from a remote name server and retrieves statistics about this crawl
     * that are held by the local queue server (number of pages, name, etc)
     * outputs this info back as body of the http response (url encoded,
     * serialized php data)
     */
    public function getInfoTimestamp()
    {
        $timestamp = 0;
        if (isset($_REQUEST["arg"]) ) {
            $timestamp = unserialize(L\webdecode($_REQUEST["arg"]));
            $timestamp = substr($this->clean($timestamp, "int"), 0,
                C\TIMESTAMP_LEN);
        }
        echo L\webencode(serialize($this->model("crawl")->getInfoTimestamp(
            $timestamp)));
    }
    /**
     * Handles a request for the crawl list (what crawl are stored on the
     * machine) data from a remote name server and retrieves the
     * statistic about this that are held by the local queue server
     * outputs this info back as body of the http response (url encoded,
     * serialized php data)
     */
    public function getCrawlList()
    {
        $return_arc_bundles = false;
        $return_recrawls = false;
        if (isset($_REQUEST["arg"]) ) {
            $arg = trim(L\webdecode($_REQUEST["arg"]));
            $arg = $this->clean($arg, "int");
            if ($arg == 3 || $arg == 1) {$return_arc_bundles = true; }
            if ($arg == 3 || $arg == 2) {$return_recrawls = true; }
        }
        echo L\webencode(serialize($this->model("crawl")->getCrawlList(
            $return_arc_bundles, $return_recrawls)));
    }
    /**
     * Handles a request for the combined crawl list, stalled, and status
     * data from a remote name server and retrieves that the statistic about
     * this that are held by the local queue server
     * outputs this info back as body of the http response (url encoded,
     * serialized php data)
     */
    public function combinedCrawlInfo()
    {
        $combined =  $this->model("crawl")->combinedCrawlInfo();
        echo L\webencode(serialize($combined));
    }
    /**
     * Receives a request to delete a crawl from a remote name server
     * and then deletes crawl on the local queue server
     */
    public function deleteCrawl()
    {
        if (!isset($_REQUEST["arg"]) ) {
            return;
        }
        $timestamp = unserialize(L\webdecode($_REQUEST["arg"]));
        $timestamp = substr($this->clean($timestamp, "int"), 0,
            C\TIMESTAMP_LEN);
        $this->model("crawl")->deleteCrawl($timestamp);
    }

    /**
     * Receives a request to inject new urls into the active
     * crawl from a remote name server and then does this for
     * the local queue server
     */
    public function injectUrlsCurrentCrawl()
    {
        if (!isset($_REQUEST["arg"]) || !isset($_REQUEST["num"])
            || !isset($_REQUEST["i"])) {
            return;
        }
        $num = $this->clean($_REQUEST["num"], "int");
        $i = $this->clean($_REQUEST["i"], "int");
        list($timestamp, $inject_urls) =
            unserialize(L\webdecode($_REQUEST["arg"]));
        $timestamp = substr($this->clean($timestamp, "int"), 0,
            C\TIMESTAMP_LEN);
        $inject_urls = L\partitionByHash($inject_urls,
            null, $num, $i, C\NS_LIB . "UrlParser::getHost");
        $this->model("crawl")->injectUrlsCurrentCrawl($timestamp,
            $inject_urls, null);
    }
    /**
     * Receives a request to get crawl summary data for an array of urls
     * from a remote name server and then looks these up on the local
     * queue server
     */
     public function getCrawlItems()
     {
        $crawl_model = $this->model("crawl");
        $start_time = microtime(true);
        if (!isset($_REQUEST["arg"]) || !isset($_REQUEST["num"])
            || !isset($_REQUEST["i"])) {
            return;
        }
        $num = $this->clean($_REQUEST["num"], "int");
        $i = $this->clean($_REQUEST["i"], "int");
        $crawl_model->current_machine = $i;
        list($lookups, $exclude_fields, $format_words, $description_length) =
            unserialize(L\webdecode($_REQUEST["arg"]));
        $our_lookups = [];
        foreach ($lookups as $lookup => $lookup_info) {
            if (!is_array($lookup_info) || empty($lookup_info)) {
                $our_lookups[$lookup] = [];
                continue;
            }
            if (count($lookup_info) == 2 && !empty($lookup_info[0]) &&
                ($lookup_info[0][0] === 'h'
                || $lookup_info[0][0] === 'r'
                || $lookup_info[0][0] === 'g') ||
                (is_string($lookup_info[0]) &&
                strncmp($lookup_info[0], "cache:", 6) == 0)) {
                $our_lookups[$lookup] = $lookup_info;
            } else {
                $our_lookups[$lookup] = [];
                foreach ($lookup_info as $lookup_item) {
                    if (is_array($lookup_item) && count($lookup_item) == 2) {
                        $our_lookups[$lookup][] = $lookup_item;
                    } else {
                        list($index, , , , ) = $lookup_item;
                        if ($index == $i) {
                            $our_lookups[$lookup][] = $lookup_item;
                        }
                    }
                }
            }
        }
        $items = $crawl_model->getCrawlItems($our_lookups, null,
            $exclude_fields, $format_words, $description_length);
        $items = (empty($items)) ? [] : $items;
        $this->web_site->header("Content-Type: application/octet-stream");
        $items["ELAPSED_TIME"] = L\changeInMicrotime($start_time);
        $items = gzdeflate(serialize($items));
        $this->web_site->header("Content-Length: " . strlen($items));
        echo $items;
        flush();
     }
    /**
     * Receives a request to stop a crawl from a remote name server
     * and then stop the current crawl on the local queue server
     */
    public function sendStopCrawlMessage()
    {
        if (!isset($_REQUEST["arg"])) {
            return;
        }
        $channel = $this->clean(L\webdecode($_REQUEST["arg"]), "int");
        $this->model("crawl")->sendStopCrawlMessage($channel);
    }
    /**
     * Receives a request to start a crawl from a remote name server
     * and then starts the crawl process on the local queue server
     */
    public function sendStartCrawlMessage()
    {
        if (!isset($_REQUEST["arg"]) || !isset($_REQUEST["num"])
            || !isset($_REQUEST["i"])) {
            return;
        }
        foreach (["num", "i", "num_fetchers"] as $field) {
            //don't change !isset to empty, i can be 0
            $$field = (!isset($_REQUEST[$field])) ? -1 :
                $this->clean($_REQUEST[$field], "int");
        }
        list($crawl_params,
            $seed_info) = unserialize(L\webdecode($_REQUEST["arg"]));
        $seed_info['seed_sites']['url'] =
            L\partitionByHash(($seed_info['seed_sites']['url'] ?? []),
                null, $num, $i, C\NS_LIB . "UrlParser::getHost");
        if (isset($_REQUEST["fqs_ratio"])) {
            $fetcher_queue_server_ratio = $this->clean($_REQUEST["fqs_ratio"],
                "int");
        } else {
            $fetcher_queue_server_ratio = 1;
        }
        $this->model("crawl")->sendStartCrawlMessage($crawl_params, $seed_info,
            null, $num_fetchers, $fetcher_queue_server_ratio);
    }
    /**
     * Wrapper call to the source model method that deletes the news feed
     * and trending data stored in this Yioop instance
     * @see SourceModel::clearFeedData
     */
    public function clearFeedData()
    {
        $this->model("source")->clearFeedData();
    }
    /**
     * A save point is used to store to disk a sequence generation-doc-offset
     * pairs of a particular mix query when doing an archive crawl of a crawl
     * mix. This is used so that the mix can remember where it was the next
     * time it is invoked by the web app on the machine in question.
     * This function deletes such a save point associated with a timestamp
     */
    public function clearQuerySavePoint()
    {
        if (!isset($_REQUEST["arg"])) {
            return;
        }
        $save_timestamp = substr($this->clean($_REQUEST["arg"], "int"), 0,
            C\TIMESTAMP_LEN);
        $this->model("crawl")->clearQuerySavePoint($save_timestamp);
    }
}
