<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    core_comcode_pages
 */

/**
 * Hook class.
 */
class Hook_search_comcode_pages extends FieldsSearchHook
{
    /**
     * Find details for this search hook.
     *
     * @param  boolean $check_permissions Whether to check permissions
     * @param  ?MEMBER $member_id The member ID to check with (null: current member)
     * @return ~?array Map of search hook details (null: hook is disabled) (false: access denied)
     */
    public function info(bool $check_permissions = true, ?int $member_id = null)
    {
        if ($member_id === null) {
            $member_id = get_member();
        }

        require_lang('zones');

        $info = [];
        $info['lang'] = do_lang_tempcode('PAGES');
        $info['default'] = (get_option('search_comcode_pages') == '1');
        $info['category'] = 'the_zone';
        $info['integer_category'] = false;
        $info['extra_sort_fields'] = $this->_get_extra_sort_fields('_comcode_page');

        $info['permissions'] = [];

        return $info;
    }

    /**
     * Empty the fast custom index.
     */
    public function empty_index()
    {
        $GLOBALS['SITE_DB']->query_delete('cpages_fulltext_index');
    }

    /**
     * Perform indexing using the fast custom index.
     *
     * @param  ?TIME $since Only index records newer than this (null: no limit)
     * @param  ?integer $total_singular_ngram_tokens Write into a count of singular ngrams (typically, words) in here (null: do not count)
     * @param  ?array $statistics_map Write into this map of singular ngram (typically, words) to number of occurrences (null: do not maintain a map)
     */
    public function index_for_search(?int $since = null, ?int &$total_singular_ngram_tokens = null, ?array &$statistics_map = null)
    {
        $engine = new Fast_custom_index();

        $index_table = 'cpages_fulltext_index';
        $clean_scan = ($GLOBALS['SITE_DB']->query_select_value_if_there($index_table, 'i_ngram') === null);

        $has_custom_fields = ($GLOBALS['FORUM_DB']->query_select_value_if_there('catalogue_fields', 'id', ['c_name' => '_comcode_page']) !== null);

        $fields_to_index = [
            'page_name' => APPEARANCE_CONTEXT_TITLE,
            'page_title' => APPEARANCE_CONTEXT_TITLE,
            'page_content' => APPEARANCE_CONTEXT_BODY,
            'meta_keywords' => APPEARANCE_CONTEXT_META,
            'meta_description' => APPEARANCE_CONTEXT_BODY,
        ];
        $key_transfer_map = [
            'zone_name' => 'i_zone_name',
            'page_name' => 'i_page_name',
        ];
        $filter_field_transfer_map = [
        ];

        $db = $GLOBALS['SITE_DB'];

        $zones = find_all_zones();
        $langs = find_all_langs();
        foreach ($zones as $zone) {
            foreach (array_keys($langs) as $lang) {
                $pages = find_all_pages($zone, 'comcode_custom/' . $lang, 'txt', false, $clean_scan ? null : $since, FIND_ALL_PAGES__ALL);
                foreach ($pages as $page => $page_type) {
                    if (is_integer($page)) {
                        $page = strval($page);
                    }

                    if (preg_match('#(^panel_|_)#', $page) == 0) {
                        require_code('global4');
                        if (!comcode_page_include_on_sitemap($zone, $page)) {
                            continue;
                        }

                        list($file_base, $file_path) = find_comcode_page($lang, $page, $zone);

                        list($keywords, $description) = seo_meta_get_for('comcode_page', $zone . ':' . $page);
                        $content_fields = [
                            'zone_name' => $zone,
                            'page_name' => $page,
                            'page_title' => get_comcode_page_title_from_disk($file_base . '/' . $file_path),
                            'page_content' => cms_file_get_contents_safe($file_base . '/' . $file_path),
                            'meta_keywords' => $keywords,
                            'meta_description' => $description,
                        ];

                        if ($has_custom_fields) {
                            $ce_id = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_entry_linkage', 'catalogue_entry_id', ['content_type' => 'comcode_page', 'content_id' => $zone . ':' . $page]);
                            if ($ce_id !== null) {
                                $engine->get_content_fields_from_catalogue_entry($content_fields, $fields_to_index, '_comcode_page', $ce_id, $lang);
                            }
                        }

                        $engine->index_for_search($db, $index_table, $content_fields, $fields_to_index, $key_transfer_map, $filter_field_transfer_map, $total_singular_ngram_tokens, $statistics_map, $lang, $clean_scan);
                    }
                }
            }
        }
    }

    /**
     * Get a list of entries for the content covered by this search hook. In hierarchical list selection format.
     *
     * @param  string $selected The default selected item
     * @return Tempcode Tree structure
     */
    public function get_tree(string $selected) : object
    {
        require_code('zones3');
        $tree = create_selection_list_zones($selected);
        return $tree;
    }

    /**
     * Get a list of extra fields to ask for.
     *
     * @return ?array A list of maps specifying extra fields (null: no tree)
     */
    public function get_fields() : ?array
    {
        return $this->_get_fields('_comcode_page');
    }

    /**
     * Run function for search results.
     *
     * @param  string $search_query Search query
     * @param  string $content_where WHERE clause that selects the content according to the search query; passed in addition to $search_query to avoid unnecessary reparsing.  ? refers to the yet-unknown field name (blank: full-text search)
     * @param  string $where_clause Initial WHERE clause that already takes $search_under into account (should be nothing else unless it is guaranteed hook will use the global get_search_rows function)
     * @param  string $search_under Comma-separated list of categories to search under
     * @param  boolean $only_search_meta Whether to only do a META (tags) search
     * @param  boolean $only_titles Whether only to search titles (as opposed to both titles and content)
     * @param  integer $max Start position in total results
     * @param  integer $start Maximum results to return in total
     * @param  string $sort The sort type (gets remapped to a field in this function)
     * @param  ID_TEXT $direction Order direction
     * @param  SHORT_TEXT $author Username/Author to match for
     * @param  ?MEMBER $author_id Member-ID to match for (null: unknown)
     * @param  mixed $cutoff Cutoff date (TIME or a pair representing the range or null)
     * @return array List of maps (template, orderer)
     */
    public function run(string $search_query, string $content_where, string $where_clause, string $search_under, bool $only_search_meta, bool $only_titles, int $max, int $start, string $sort, string $direction, string $author, ?int $author_id, $cutoff) : array
    {
        $remapped_orderer = '';
        switch ($sort) {
            case 'title':
                $remapped_orderer = 'the_page';
                break;

            case 'add_date':
                $remapped_orderer = 'the_zone'; // Stucked
                break;
        }

        $this->_handle_date_check($cutoff, 'p_add_date', $where_clause);

        require_lang('zones');

        // Calculate and perform query
        $db = $GLOBALS['SITE_DB'];
        $index_table = 'cpages_fulltext_index';
        $fast_custom_index = can_use_fast_custom_index('comcode_pages', $db, $index_table, $search_query, Fast_custom_index::active_search_has_special_filtering() || $cutoff !== null || $author != '' || ($search_under != '-1' && $search_under != '!'));
        if ($fast_custom_index) {
            // This search hook implements the fast custom index, which we use where possible...

            // Calculate our where clause (search)
            $where_clause = '';
            $extra_join_clause = '';
            $sq = build_search_submitter_clauses('p_submitter', $author_id, $author);
            if ($sq === null) {
                return [];
            } else {
                $where_clause .= $sq;
            }

            if (($search_under !== null) && ($search_under != '!')) {
                $extra_join_clause .= ' AND ';
                $extra_join_clause .= '(' . db_string_equal_to('ixxx.i_zone_name', $search_under) . ')';
            }

            // Category filter
            if (($search_under != '!') && ($search_under != '-1')) {
                $cats = explode(',', $search_under);
                $done_at_least_one_cat = false;
                foreach ($cats as $i => $cat) {
                    if (trim($cat) == '') {
                        continue;
                    }

                    if (!$done_at_least_one_cat) {
                        $extra_join_clause .= ' AND (';
                        $done_at_least_one_cat = true;
                    } else {
                        $extra_join_clause .= ' OR ';
                    }

                    $extra_join_clause .= db_string_equal_to('ixxx.i_zone_name', $cat);
                }
                if ($done_at_least_one_cat) {
                    $extra_join_clause .= ')';
                }
            }

            if ((!has_privilege(get_member(), 'see_not_validated')) && (addon_installed('validation'))) {
                $where_clause .= ' AND ';
                $where_clause .= 'r.p_validated=1';
            }

            $table = 'comcode_pages r';

            $g_or = get_permission_where_clause_groups(get_member(), false);
            if ($g_or !== null) {
                $where_clause .= ' AND ';
                $where_clause .= 'EXISTS(SELECT * FROM ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'group_zone_access z WHERE (z.zone_name=r.the_zone AND (' . str_replace('group_id', 'z.group_id', $g_or) . ')))';
            }

            $engine = new Fast_custom_index();

            if (Fast_custom_index::active_search_has_special_filtering()) {
                $trans_fields = [];
                $nontrans_fields = [];
                $this->_get_search_parameterisation_advanced_for_content_type('_comcode_page', $table, $where_clause, $trans_fields, $nontrans_fields, db_function('CONCAT', ['r.the_zone', 'r.the_page']));
                // ^ Nothing done with trans_fields and nontrans_fields
            }

            $key_transfer_map = ['the_zone' => 'i_zone_name', 'the_page' => 'i_page_name'];
            $rows = $engine->get_search_rows($db, $index_table, $db->get_table_prefix() . $table, $key_transfer_map, $where_clause, $extra_join_clause, $search_query, $only_search_meta, $only_titles, $max, $start, $remapped_orderer, $direction);
        } else {
            $sq = build_search_submitter_clauses('p_submitter', $author_id, $author);
            if ($sq === null) {
                return [];
            } else {
                $where_clause .= $sq;
            }

            $where_clause .= ' AND q.p_include_on_sitemap=1';
            if (($search_under !== null) && ($search_under != '!')) {
                $where_clause .= ' AND ';
                $where_clause .= '(' . db_string_equal_to('r.the_zone', $search_under) . ')';
            }

            if ((!has_privilege(get_member(), 'see_not_validated')) && (addon_installed('validation'))) {
                $where_clause .= ' AND ';
                $where_clause .= 'p_validated=1';
            }

            $table = 'cached_comcode_pages r LEFT JOIN ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'comcode_pages q ON q.the_zone=r.the_zone AND q.the_page=r.the_page';
            $trans_fields = ['r.cc_page_title' => 'SHORT_TRANS', 'r.string_index' => 'LONG_TRANS__COMCODE'];
            $nontrans_fields = [];
            $this->_get_search_parameterisation_advanced_for_content_type('_comcode_page', $table, $where_clause, $trans_fields, $nontrans_fields, db_function('CONCAT', ['r.the_zone', 'r.the_page']));

            $g_or = get_permission_where_clause_groups(get_member(), false);
            if ($g_or !== null) {
                $where_clause .= ' AND ';
                $where_clause .= 'EXISTS(SELECT * FROM ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'group_zone_access z WHERE (z.zone_name=r.the_zone AND (' . str_replace('group_id', 'z.group_id', $g_or) . ')))';
            }

            $rows = get_search_rows('comcode_page', 'the_zone:the_page', $search_query, $content_where, $where_clause, $only_search_meta, $only_titles, $max, $start, $remapped_orderer, $direction, $table, 'r.the_zone,r.the_page', $trans_fields, $nontrans_fields);
        }

        if (addon_installed('redirects_editor')) {
            $redirects = $GLOBALS['SITE_DB']->query_select('redirects', ['*']);
        } else {
            $redirects = [];
        }

        $out = [];
        $pages_found = [];
        $out_i = 0;
        foreach ($rows as $row) {
            foreach ($redirects as $redirect) {
                if (($redirect['r_from_page'] == $row['the_page']) && ($redirect['r_from_zone'] == $row['the_zone'])) {
                    continue 2;
                }
            }

            if ($row['the_zone'] == '!') {
                continue;
            }
            if (array_key_exists($row['the_zone'] . ':' . $row['the_page'], $pages_found)) {
                continue;
            }
            $pages_found[$row['the_zone'] . ':' . $row['the_page']] = 1;
            $out[$out_i]['data'] = $row + ['extra' => [$row['the_zone'], $row['the_page'], $max]];
            if (($remapped_orderer != '') && (array_key_exists($remapped_orderer, $row))) {
                $out[$out_i]['orderer'] = $row[$remapped_orderer];
            } elseif (strpos($remapped_orderer, '_rating:') !== false) {
                $out[$out_i]['orderer'] = $row[$remapped_orderer];
            }

            if (!has_page_access(get_member(), $row['the_page'], $row['the_zone'])) {
                $out[$out_i]['restricted'] = true;
            }

            $out_i++;
            $GLOBALS['TOTAL_SEARCH_RESULTS']++;
        }

        if (($author == '') && (!$fast_custom_index)) {
            // Make sure we record that for all cached Comcode pages, we know of them (only those not cached would not have been under the scope of the current search)
            $all_pages = $GLOBALS['SITE_DB']->query_select('cached_comcode_pages', ['the_zone', 'the_page', 'string_index']);
            foreach ($all_pages as $row) {
                $pages_found[$row['the_zone'] . ':' . $row['the_page']] = 1;
            }

            if (in_memory_search_match(['content' => $search_query], get_translated_text($row['string_index']))) {
                $out[$out_i]['data'] = ['the_zone' => $row['the_zone'], 'the_page' => $row['the_page']] + ['extra' => [$row['the_zone'], $row['the_page'], $max]];
                if ($remapped_orderer == 'the_page') {
                    $out[$out_i]['orderer'] = $row['the_page'];
                } elseif ($remapped_orderer == 'the_zone') {
                    $out[$out_i]['orderer'] = $row['the_zone'];
                }

                if (!has_page_access(get_member(), $row['the_page'], $row['the_zone'])) {
                    $out[$out_i]['restricted'] = true;
                }

                $out_i++;
                $GLOBALS['TOTAL_SEARCH_RESULTS']++;
            }

            // Now, look on disk for non-cached Comcode pages
            $zones = find_all_zones();
            $i = count($out);
            if (($search_under !== null) && ($search_under != '!')) {
                $zones = [$search_under];
            }
            foreach ($zones as $zone) {
                if (!has_zone_access(get_member(), $zone)) {
                    continue;
                }

                $pages = find_all_pages_wrap($zone, false, false, FIND_ALL_PAGES__PERFORMANT, 'comcode');
                foreach ($pages as $page => $dir) {
                    if (!is_string($page)) {
                        $page = strval($page);
                    }

                    if (!array_key_exists($zone . ':' . $page, $pages_found)) {
                        if (!has_page_access(get_member(), $page, $zone)) {
                            continue;
                        }

                        require_code('global4');

                        // Actually sitemap is solely for the sitemap; should use the validation addon to hide pages we do not want in search
                        /*
                            if (!comcode_page_include_on_sitemap($zone, $page)) {
                                continue;
                            }
                        */

                        foreach ($redirects as $redirect) {
                            if (($redirect['r_from_page'] == $page) && ($redirect['r_from_zone'] == $zone)) {
                                continue 2;
                            }
                        }

                        $path = zone_black_magic_filterer((($dir == 'comcode_custom') ? get_custom_file_base() : get_file_base()) . (($zone == '') ? '' : '/') . $zone . '/pages/' . $dir . '/' . $page . '.txt');
                        if (!$this->_handle_date_check_runtime($cutoff, filemtime($path))) {
                            continue;
                        }

                        $contents = cms_file_get_contents_safe($path, FILE_READ_LOCK | FILE_READ_UNIXIFIED_TEXT | FILE_READ_BOM);
                        if ($only_titles) {
                            $contents = preg_replace('#^.*\[title(="1")?\](.*)\[/title\].*$#Us', '${2}', $contents);
                        }

                        if (in_memory_search_match(['content' => $search_query], $contents)) {
                            $out[$out_i]['data'] = ['the_zone' => $zone, 'the_page' => $page] + ['extra' => [$zone, $page, $max]];
                            if ($remapped_orderer == 'the_page') {
                                $out[$out_i]['orderer'] = $page;
                            } elseif ($remapped_orderer == 'the_zone') {
                                $out[$out_i]['orderer'] = $zone;
                            }

                            if (!has_page_access(get_member(), $row['the_page'], $row['the_zone'])) {
                                $out[$out_i]['restricted'] = true;
                            }

                            $i++;
                            $GLOBALS['TOTAL_SEARCH_RESULTS']++;

                            // Let it cache for next time
                            if (get_option('is_on_comcode_page_cache') == '1') {
                                request_page($page, false, $zone, $dir, false, false);
                            }
                        }
                    }
                }
            }
        }

        return $out;
    }

    /**
     * Run function for rendering a search result.
     *
     * @param  array $row The data row stored when we retrieved the result
     * @return Tempcode The output
     */
    public function render(array $row) : object
    {
        list($zone, $page, $limit_to) = $row['extra'];
        return $this->decide_template($zone, $page, $limit_to);
    }

    /**
     * Decide how to show a Comcode page in the search results.
     *
     * @param  ID_TEXT $zone The zone for the page
     * @param  ID_TEXT $page The page name
     * @param  integer $max Maximum results to return
     * @return Tempcode The Tempcode showing the Comcode page
     */
    public function decide_template(string $zone, string $page, int $max) : object
    {
        global $SEARCH_QUERY_TERMS;

        $old_limit = cms_extend_time_limit(TIME_LIMIT_EXTEND__SLOW); // This can be slow.

        require_code('xhtml');

        $url = build_url(['page' => $page], $zone);

        $_summary = seo_meta_get_for('comcode_page', $zone . ':' . $page);
        $summary = $_summary[1];

        if ($summary == '') {
            $page_request = _request_page($page, $zone);
            if (($page_request === false) || (strpos($page_request[0], 'COMCODE') === false)) {
                cms_set_time_limit($old_limit);

                return new Tempcode();
            }
            $_zone = $page_request[count($page_request) - 1];
            $comcode_file = get_custom_file_base() . (($_zone == '') ? '' : '/') . $_zone;
            if (!is_file($comcode_file)) {
                $comcode_file = get_file_base() . (($_zone == '') ? '' : '/') . $_zone;
            }

            if (file_exists($comcode_file)) {
                push_lax_comcode(true);
                /* Tempcode compiler slowed things down so easier just to show full thing
                $temp_summary = comcode_to_tempcode(cms_file_get_contents_safe($comcode_file, FILE_READ_LOCK | FILE_READ_UNIXIFIED_TEXT | FILE_READ_BOM), null, true);
                $_temp_summary = $temp_summary->evaluate();
                if (strlen($_temp_summary) < 500) {
                    $summary = $_temp_summary;
                } else {
                    $entity = '&hellip;';
                    if (function_exists('ocp_mark_as_escaped')) {
                        ocp_mark_as_escaped($entity);
                    }
                    $pos = false;//strpos($_temp_summary, '<span class="comcode-highlight">');
                    if ($pos === false) {
                        $pos = 0;
                    }
                    $pos2 = max(0, $pos - 250);
                    $summary = (($pos2 == 0) ? '' : $entity) . xhtml_substr($_temp_summary, $pos2, 500) . $entity;
                }
                */
                $GLOBALS['OVERRIDE_SELF_ZONE'] = $zone;
                $backup_search__contents_bits = $SEARCH_QUERY_TERMS;
                $SEARCH_QUERY_TERMS = null; // We do not want highlighting, as it'll result in far too much Comcode being parsed (ok for short snippets, not many full pages!)
                $GLOBALS['TEMPCODE_SETGET']['no_comcode_page_edit_links'] = '1'; // FUDGE
                push_output_state();
                $temp_summary = request_page($page, true, $zone, strpos($comcode_file, '/comcode_custom/') ? 'comcode_custom' : 'comcode', true);
                restore_output_state();
                $SEARCH_QUERY_TERMS = $backup_search__contents_bits;
                $GLOBALS['OVERRIDE_SELF_ZONE'] = null;
                pop_lax_comcode();
                $_temp_summary = $temp_summary->evaluate();
                global $PAGES_CACHE;
                $PAGES_CACHE = []; // Decache this, or we'll eat up a tonne of RAM

                $summary = generate_text_summary($_temp_summary, ($SEARCH_QUERY_TERMS === null) ? [] : $SEARCH_QUERY_TERMS);

                $GLOBALS['TEMPCODE_SETGET']['no_comcode_page_edit_links'] = '0';
            }
        }

        $title = $page;
        global $LAST_COMCODE_PARSED_TITLE;
        if ($LAST_COMCODE_PARSED_TITLE != '') {
            $title = $LAST_COMCODE_PARSED_TITLE;
        }

        $breadcrumbs = breadcrumb_segments_to_tempcode(comcode_breadcrumbs($page, $zone));

        cms_set_time_limit($old_limit);

        return do_template('COMCODE_PAGE_BOX', [
            '_GUID' => '79cd9e7d0b63ee916c4cd74b26c2f652',
            'TITLE' => $title,
            'BREADCRUMBS' => $breadcrumbs,
            'PAGE' => $page,
            'ZONE' => $zone,
            'URL' => $url,
            'SUMMARY' => $summary,
            'GIVE_CONTEXT' => true,
        ]);
    }
}
