<?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\views\elements;

use seekquarry\yioop as B;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library\CrawlConstants;
use seekquarry\yioop\library\UrlParser;
use seekquarry\yioop\views\elements\Element;

/**
 * Element used to present search results
 * It is also contains the landing page search box for
 * people to types searches into
 *
 * @author Chris Pollett
 */
class SearchElement extends Element implements CrawlConstants
{
    /**
     * Represent extension of Git urls
     */
    const GIT_EXTENSION = ".git";
    /**
     * Number of decimals for search result scores
     */
    const SCORE_PRECISION = 4;
    /**
     * Draws the main landing pages as well as search result pages
     *
     * @param array $data  PAGES contains all the summaries of web pages
     * returned by the current query, $data also contains information
     * about how the the query took to process and the total number
     * of results, how to fetch the next results, etc.
     *
     */
    public function render($data)
    {
        if (empty($data['IS_LANDING'])) {
            $this->renderSearchResults($data);
        } else {
            $this->renderSearchLanding($data);
        }
        if (C\PROFILE && !empty($data["ADMIN"]) && empty($data['API']) ) { ?>
            <script>
            /*
                Used to warn that user is about to be logged out
             */
            function logoutWarn()
            {
                doMessage(
                    "<h2 class='red'><?php
                        e(tl('admin_view_auto_logout_one_minute'))?></h2>");
            }
            /*
                Javascript to perform autologout
             */
            function autoLogout()
            {
                document.location='<?=C\SHORT_BASE_URL ?>?a=signout';
            }
            //schedule logout warnings
            var sec = 1000;
            var minute = 60 * sec;
            var autologout = <?=C\p('AUTOLOGOUT') ?> * sec;
            setTimeout("logoutWarn()", autologout - minute);
            setTimeout("autoLogout()", autologout);
            </script><?php
        }
    }
    /**
     * Used to draw the results of a query to the Yioop Search Engine
     *
     * @param array $data an associative array containing a PAGES field needed
     *     to render search result
     */
    public function renderSearchResults($data)
    {
        $logged_in = isset($data["ADMIN"]) && $data["ADMIN"];
        $token = ($logged_in) ? $data[C\p('CSRF_TOKEN')] : "";
        $token_string = ($logged_in) ? C\p('CSRF_TOKEN') .
            "=". $data[C\p('CSRF_TOKEN')] .
            "&" : "";
        $token_string_amp = ($logged_in) ?
            C\p('CSRF_TOKEN') . "=" . $data[C\p('CSRF_TOKEN')]."&amp;" : "";
        $is_search_view = (get_class($this->view) == C\NS_VIEWS .
            "SearchView");
        $can_edit_results = !empty($data['CAN_EDIT_RESULTS']);
        if ($is_search_view) {
            if (!empty($data['PAGES']) && !$_SERVER["MOBILE"]) {
                ?><h2 class="search-stats"><?php
                if ($data['RESULTS_PER_PAGE'] != -1) {
                    $num_results = min($data['TOTAL_ROWS'],
                        $data['LIMIT'] + $data['RESULTS_PER_PAGE']);
                    $limit = min($data['LIMIT'] + 1, $num_results);
                     ?> <?= tl('search_element_calculated',
                        number_format($data['ELAPSED_TIME'], 5)) ?> <?=
                     tl('search_element_results', $limit, $num_results,
                        $data['TOTAL_ROWS']) ?><?php
                } else {
                    e(tl('search_element_num_results', $data['TOTAL_ROWS']));
                } ?></h2><?php
            } ?>
            <div id='search-body' class="search-body" >
            <?php if (C\p('WORD_SUGGEST') && empty($data['TREND_DATA']) &&
                empty($data['CHART_DATA'])) { ?>
                <div id="spell-check" class="spell"><span
                class="hidden" >&nbsp;</span></div><?php
            } ?>
            <div class="search-results"><?php
        } else { ?>
            <div class="continuous-result-separator"><?=
                $data['LIMIT'] ?></div>
            <hr id='limit-<?=$data['LIMIT'] ?>' data-total='<?=
                $data['TOTAL_ROWS'] ?>' >
            <?php
        }
        if (!empty($data['BEST_ANSWER'])) {
            ?><div id="best-answer" class="echo-link">
                 <?= tl('search_element_possible_answer') . " ".
                    $data['BEST_ANSWER']; ?>
            </div><?php
        }
        if (empty($data['PAGES']) && empty($data['TREND_DATA']) &&
            empty($data['CHART_DATA']) && $data['LIMIT'] == 0) {
            ?>
            <div class='no-search-results'>
            <?=tl('search_element_no_results') ?><?php
            if ($can_edit_results) {
                e("<br>");
                $add_result = B\controllerUrl("admin", true) .
                    $token_string . "a=resultsEditor&amp;URL=no_result".
                    "&amp;MAP_QUERY=" . $data['QUERY'] .
                    "&amp;MODE=editquerymap&amp;arg=loadquerymap"; ?>
                <a href="<?=$add_result ?>"><?=
                    tl('search_element_add_a_result_url')?></a><?php
            }?>
            </div>
            <?php
        }
        if (empty($data['PAGES'])) {
            if($is_search_view) { ?>
                </div>
                </div>
                <?php
            }
            return;
        }
        $item_cnt = $data['LIMIT'];
        foreach ($data['PAGES'] as $page) {
            $item_cnt++;
            if (isset($page[self::URL])) {
                if (str_starts_with($page[self::URL], "url|")) {
                    $url_parts = explode("|", $page[self::URL]);
                    $url = $url_parts[1];
                    $link_url = $url;
                    $title = (empty($page[self::TITLE]) ||
                        $page[self::TITLE] == $url) ?
                        UrlParser::simplifyUrl($url, 60) :
                        $page[self::TITLE];
                    $subtitle = "title='" . $page[self::URL] . "'";
                } else {
                    $url = $page[self::URL];
                    if (str_starts_with($url, "record:")) {
                        $link_url = "?" . $token_string .
                        "a=cache&q=" . urlencode($data['QUERY']).
                        "&arg=" . urlencode($url) . "&its=".
                        $page[self::CRAWL_TIME];
                    } else {
                        $link_url = $url;
                    }
                    $title = mb_convert_encoding(($page[self::TITLE] ?? ""),
                        "UTF-8", "UTF-8");
                    if (strlen(trim($title)) == 0) {
                        $title = UrlParser::simplifyUrl($url, 60);
                    }
                    $subtitle = "";
                }
            } else {
                $url = "";
                $link_url = $url;
                $title = isset($page[self::TITLE]) ? $page[self::TITLE] : "";
                $subtitle = "";
            }
            $subsearch = (isset($data["SUBSEARCH"])) ? $data["SUBSEARCH"] :
                "";
            $base_query = "?" . $token_string_amp . "c=search";
            if (isset($page['IMAGES'])) {
                $this->view->helper("images")->render($page['IMAGES'],
                    $base_query . "&amp;q=".urlencode($data['QUERY']),
                    $subsearch, $data['IMAGE_SUBSEARCH_ENABLED'],
                    $data["OPEN_IN_TABS"]);
                continue;
            } else if (isset($page['VIDEOS'])) {
                $this->view->helper("videos")->render($page['VIDEOS'],
                    $base_query . "&amp;q=".urlencode($data['QUERY']),
                    $subsearch, $data['VIDEO_SUBSEARCH_ENABLED'],
                    $data["OPEN_IN_TABS"], $data["DEFAULT_THUMB"]);
                continue;
            } else if (isset($page['FEED'])) {
                $this->view->helper("feeds")->render($page['FEED'],
                    $token, $data['QUERY'],  $subsearch,
                    $data['OPEN_IN_TABS']);
                continue;
            } else if (isset($page[self::SOURCE_NAME])) {
                $this->view->helper("feeds")->renderFeedItem($page,
                    $token, $data['QUERY'], $subsearch, $data['OPEN_IN_TABS']);
                continue;
            }
            ?><div class='result'>
            <div class="result-details"><?php
            if (C\nsdefined("SERP_FAVICONS") && C\p('SERP_FAVICONS') &&
                !empty($page[self::FAVICON_URL])) {
                ?><img class="result-fav" alt="" src="<?=B\serpIconUrl(
                    $page[self::FAVICON_URL], $page[self::CRAWL_TIME]);
                ?>" ><?php
            }?>
            <span class="echo-link" <?=$subtitle ?>><?=
                UrlParser::simplifyUrl($url, 100);
            ?></span><?php
            if (C\nsdefined('MORE_RESULT') && C\p('MORE_RESULT')) { ?>
                <span class="result-more"
                    onclick="toggleDisplay('result-extras-<?=$item_cnt ?>')"
                    tabindex="0">&bull;&bull;&bull;</span><?php
            }
            if ($can_edit_results) {
                $admin_prefix = B\controllerUrl("admin", true) .
                    $token_string . "a=resultsEditor&amp;URL=" .
                    urlencode($url) . "&amp;MAP_QUERY=" . $data['QUERY'] .
                    "&amp;";
                $edit_result_suffix = "MODE=editurl&amp;arg=loadurl";
                $edit_rank_suffix = "MODE=editquerymap&amp;arg=loadquerymap";
                $this->view->helper("options")->renderLinkDropDown(
                    "edit-result-$item_cnt",
                    [$edit_result_suffix => tl('search_element_edit_result'),
                     $edit_rank_suffix => tl('search_element_edit_rank')],
                    "", "$admin_prefix", false,
                    "edit-search-result", "⚙");
            }
            if (C\nsdefined('MORE_RESULT') && C\p('MORE_RESULT')) { ?>
                <div id="result-extras-<?=$item_cnt
                ?>" class="result-extras"><?php
                if (!$_SERVER["MOBILE"] && isset($page[self::WORD_CLOUD]) &&
                    is_array($page[self::WORD_CLOUD])) { ?>
                    <p>
                    <?php
                    if (isset($page['ANSWER'])) {
                        $answer = $page['ANSWER'];?>
                        <span class="echo-link" <?=$subtitle ?>>
                        <?php e("<span class='word-cloud-spacer'>".
                        tl('search_element_possible_answer')."</span>"); ?>
                        <?=$answer." "?>
                        </span><?php
                    }
                    $cloud = $page[self::WORD_CLOUD];
                    ?>
                    <?php
                    $i = 1;
                    if (C\nsdefined('WORD_CLOUD') && C\p('WORD_CLOUD') &&
                        !empty($cloud)) {
                        e("<span class='word-cloud-spacer'>".
                            tl('search_element_word_cloud')."</span>");
                            $len = 0;
                        foreach ($cloud as $word) {
                            $len += strlen($word);
                            if ($len > 40) {
                                break;
                            }
                            ?><span class="word-cloud">
                            <a class='word-cloud-<?= $i ?>' href="?<?=
                                $token_string_amp?>its=<?= $data['its']
                                ?>&amp;q=<?=$word ?>"><?= $word
                                ?></a></span><?php
                            $i++;
                        }
                    }
                }?>
                <p class="serp-links-score"><?php
                $aux_link_flag = false;
                if (isset($page[self::TYPE]) && $page[self::TYPE] != "link") {
                    if (C\p('CACHE_LINK') &&
                        (!isset($page[self::ROBOT_METAS]) ||
                        !(in_array("NOARCHIVE", $page[self::ROBOT_METAS]) ||
                          in_array("NONE", $page[self::ROBOT_METAS])))) {
                        $aux_link_flag = true; ?>
                        <a href="?<?=$token_string_amp ?>a=cache&amp;q=<?=
                            $data['QUERY'] ?>&amp;arg=<?=urlencode($url)
                            ?>&amp;its=<?= $page[self::CRAWL_TIME] ?>"
                            rel='nofollow'>
                            <?php
                            if ($page[self::TYPE] == "text/html" ||
                                stristr($page[self::TYPE], "image")) {
                                e(tl('search_element_cache'));
                            } else {
                                e(tl('search_element_as_text'));
                            }
                            ?></a>.<?php
                    }
                    if (C\p('SIMILAR_LINK')) {
                        $aux_link_flag = true; ?>
                        <a href="?<?=$token_string_amp
                            ?>a=related&amp;arg=<?=urlencode($url)
                            ?>&amp;its=<?= $page[self::CRAWL_TIME]
                            ?>" rel='nofollow'><?= tl('search_element_similar')
                            ?></a>.
                        <?php
                    }
                    if (C\p('IN_LINK')) {
                        $aux_link_flag = true; ?>
                        <a href="?<?= $token_string_amp ?>q=<?=
                            urlencode("link:".$url) ?>&amp;its=<?=
                            $page[self::CRAWL_TIME] ?>" rel='nofollow'><?=
                            tl('search_element_inlink') ?></a>.<?php
                    }
                    if (C\p('IP_LINK') && isset($page[self::IP_ADDRESSES])) {
                        foreach ($page[self::IP_ADDRESSES] as $address) {
                            if ($address == "0.0.0.0") {
                                continue;
                            }
                            ?>
                            <a href="?<?=$token_string_amp
                                ?>q=<?=urlencode('ip:' . $address)
                                ?>&amp;its=<?=$data['its']
                                ?>" rel='nofollow'>IP:<?=
                                $address ?></a>. <?php
                        }
                    }
                }
                if ($aux_link_flag) {e("<br class='show-narrow'>");}
                if (!C\nsdefined("RESULT_SCORE") || C\p('RESULT_SCORE')) {
                    if (!empty($page[self::SCORE])) {
                        ?><span title="<?php
                        e(tl('search_element_rank',
                            number_format($page[self::DOC_RANK], 2)) . "\n");
                        e(tl('search_element_relevancy',
                            number_format($page[self::RELEVANCE], 2) ) . "\n");
                        if (isset($page[self::USER_RANKS])) {
                            foreach ($page[self::USER_RANKS] as
                                $label => $score) {
                                e($label . ":" . number_format($score, 2) .
                                    "\n");
                            }
                        }
                        ?>" ><?=tl('search_element_score',
                            number_format($page[self::SCORE],
                            self::SCORE_PRECISION))?></span><?php
                    } else if (!empty($page[self::PINNED])) {
                        e(tl('search_element_score_pinned'));
                    }
                }
                ?>
                </p>
                </div><?php
            } ?>
            </div>
            <h2>
            <?php
            if (strpos($link_url, self::GIT_EXTENSION)) { ?>
                <a href="?<?= $token_string_amp
                    ?>a=cache&amp;q=<?= urlencode($data['QUERY'])
                    ?>&amp;arg=<?= urlencode($url) ?>&amp;its=<?=
                    $page[self::CRAWL_TIME] ?>&amp;repository=git"
                    rel='nofollow'><?php
            } else { ?>
                <a href="<?= htmlentities($link_url)
                    ?>" rel="nofollow" <?php
                    if ($data["OPEN_IN_TABS"]) { ?>
                        target="_blank" rel="noopener"<?php
                    }?> ><?php
            }
            $is_video = false;
            $is_image = false;
            if (!empty($page[self::THUMB]) && $page[self::THUMB] != 'null'
                && $page[self::THUMB] != 'NULL') {
                if (empty($page[self::IS_VIDEO])) {
                    ?><img src="<?= $page[self::THUMB] ?>"
                    loading="lazy" alt="<?=title ?>" ><?php
                    $is_video = false;
                    $is_image = true;
                } else {
                    $is_video = true;
                }
            }
            if (!$is_image) {
                e($title);
                if (isset($page[self::TYPE])) {
                    $this->view->helper("filetype")->render($page[self::TYPE]);
                }
            }
            ?></a>
            </h2>
            <?php
            if ($is_video) {
                $this->view->helper("videourl")->render($link_url,
                    $page[self::THUMB], $data["OPEN_IN_TABS"]);
            }
            if (!isset($page[self::ROBOT_METAS]) ||
                !in_array("NOSNIPPET", $page[self::ROBOT_METAS])) {
                $description = $page[self::DESCRIPTION] ?? "";
                $description = ltrim(mb_convert_encoding($description,
                    "UTF-8", "UTF-8"), C\PUNCT);
                e("<p>$description</p>");
            }
            if (!empty($page[self::TOP_LEVEL_LINKS])) {?>
                <table><tr><?php
                $row_cnt = 0;
                $row_mod = ($_SERVER["MOBILE"]) ? 3 : 5;
                $num_top_links = count($page[self::TOP_LEVEL_LINKS]);
                $abs_cnt = 0;
                foreach ($page[self::TOP_LEVEL_LINKS] as $link => $link_text) {
                    ?><td><a href="<?=$link?>"><?=$link_text ?></a></td><?php
                    if ($row_cnt == $row_mod && $abs_cnt < $num_top_links - 1) {
                        $row_cnt = 0;
                        ?></tr><tr><?php
                    }
                    $row_cnt++;
                    $abs_cnt++;
                }?>
                </tr></table><?php
            }
            ?>
        </div>
        <?php
        } //end foreach
        if($is_search_view) { ?>
            </div>
            </div>
            <?php
            if (!empty($data['RELEVANT_ADVERTISEMENT']) ||
                (C\nsdefined("SEARCH_ANALYTICS_MODE") &&
                C\p('SEARCH_ANALYTICS_MODE'))) {
                $this->renderClickRecordScript($data);
            }
        }
    }
    /**
     * Used to output the Javascript functions used to record which link
     * on a search result page was clicked.
     *
     * @param array $data an associative array containing field needed
     *     to render search result
     */
    public function renderClickRecordScript($data)
    {
        $logged_in = isset($data["ADMIN"]) && $data["ADMIN"];
        $token_string_amp = ($logged_in) ?
            C\p('CSRF_TOKEN') . "=" . $data[C\p('CSRF_TOKEN')]."&amp;" : "";
        $url = C\SHORT_BASE_URL . "?" . ($token_string_amp) .
            "c=search&a=recordClick&q={$data['QUERY']}&arg=";
        ?>
        <script>
        function recordClick(anchor, info)
        {
            if (info == null) {
                info = encodeURI(anchor.href).replace(/\//g, "%2F");
            }
            var url = '<?= $url ?>' + info;
            var flag = true;
            fetch(url)
                .then((response) => response.json())
                .then((data) => {
                    window.location = anchor.href;
                  })
                .catch((error) => {
                    console.error('Error:', error);
                    return false;
                });
            return true;
        }
        function initRecordClick()
        {
            let links = document.querySelectorAll("div.result h2 a");
            for (const link of links) {
                link.onclick = () => {
                    recordClick(link, null);
                    return false;
                };
            }
        }
        initRecordClick();
        </script>
        <?php
    }
    /**
     * Draws the landing page for this instance of Yioop when the default
     * big search bar (rather than the Main public wiki page is used)
     * @param array $data containing fields used to draw landing page
     */
    public function renderSearchLanding($data)
    {
        $logged_in = isset($data["ADMIN"]) && $data["ADMIN"];
        $query_parts = [];
        if ($logged_in) {
            $query_parts[C\p('CSRF_TOKEN')] = $data[C\p('CSRF_TOKEN')];
        }
        $logo = $this->view->helper("logo")->renderResponsive($data,
            tl('search_element_title'), "LOGO_LARGE", "LOGO_MEDIUM");
        ?>
        <div class='top-landing-spacer'></div>
        <h1 class="logo"><a href="<?= C\SHORT_BASE_URL ?><?php
        if ($logged_in) {
            e("?".http_build_query($query_parts));
        } ?>"><?php e($logo); ?></a>
        </h1><?php
        $subsearch = "";
        if (!empty($data['SUBSEARCH'])) {
            $key = array_search($data['SUBSEARCH'],
                array_column($data["SUBSEARCHES"], 'FOLDER_NAME'));
            if(!empty($key)) {
                e(" <div class='logo-subsearch'>" .
                    $data["SUBSEARCHES"][$key]['SUBSEARCH_NAME'] .
                    "</div>");
                $subsearch = "subsearch";
            }
        } ?>
        <div class="<?=$subsearch ?> search-box">
        <div class="search-box-inner" tabindex="0">
        <form id="search-form" method="get" action="<?=C\SHORT_BASE_URL ?>"
            onsubmit="processSubmit()" onreset="return resetSuggest()">
        <?php
        if (isset($data["SUBSEARCH"]) && $data["SUBSEARCH"] != "") {
            ?><input type="hidden" name="s" value="<?=
            $data['SUBSEARCH'] ?>" ><?php
        }
        if ($logged_in) { ?>
            <input id="csrf-token" type="hidden" name="<?= C\p('CSRF_TOKEN') ?>"
                value="<?= $data[C\p('CSRF_TOKEN')] ?>" ><?php
        } ?>
        <input id="its-value" type="hidden" name="its" value="<?=
            $data['its'] ?>" >
        <input type="search" autocapitalize="none" <?php
            if (C\p('WORD_SUGGEST')) { ?>
                autocomplete="off"
                onkeyup="onTypeTerm(event, this);"
            <?php } ?>
            id="query-field" name="q" value="<?php
            if (isset($data['QUERY']) && !isset($data['NO_QUERY'])) {
                e(urldecode($data['QUERY']));} ?>"
            placeholder="<?= tl('search_element_input_placeholder') ?>" >
        <?= $this->view->helper("iconlink")->renderFormButton("reset",
            "reset", false, "search-reset-button"); ?>
        <?= $this->view->helper("iconlink")->renderFormButton("submit",
            "search", false, "search-button"); ?>
        </form>
        </div>
        </div>
        <div id="suggest-dropdown">
            <ul id="suggest-results" class="suggest-list">
            </ul>
        </div>
        <?php
    }
}
