SeekquarrySeekquarry - Yioop Repo - Seekquarry

/ src / library / processors / SitemapProcessor.php⤓
<?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\library\processors;

use seekquarry\yioop\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\UrlParser;

/**
 * SitemapProcessor makes the crawl summary of a sitemap or a sitemap index.
 * The summary's title is the sitemap's url, its description the words of
 * that url, and its links the urls the sitemap's loc elements list.
 * XmlProcessor hands it documents whose root element is urlset or
 * sitemapindex.
 *
 * @author Chris Pollett
 */
class SitemapProcessor extends TextProcessor
{
    /**
     * process makes the summary of the sitemap $page fetched from $url. It
     * parses the part of $page entriesPage gives for max_links_to_extract
     * entries and takes their links with links. Where some of those
     * entries give no link and more entries follow, it parses every whole
     * entry instead. A document that gives neither links nor a title is
     * summarized as text by TextProcessor. Either summary is marked
     * JUST_METAS.
     *
     * @param string $page the text of the sitemap
     * @param string $url the url the sitemap came from, which relative
     *     links are resolved against
     * @return array the summary of the sitemap
     */
    public function process($page, $url)
    {
        $summary = null;
        if (is_string($page)) {
            $max_links = self::$max_links_to_extract;
            [$entries_page, $more] = self::entriesPage($page, $max_links);
            $dom = L\getDomFromString($entries_page);
            if (!$dom) {
                self::closeDanglingTags($entries_page);
                $dom = L\getDomFromString($entries_page);
            }
            if ($dom !== false) {
                $summary[self::TITLE] = $url;
                $summary[self::DESCRIPTION] =
                    UrlParser::extractTextFromUrl($url);
                $summary[self::LANG] = "";
                $summary[self::LINKS] = self::links($dom, $url);
                if ($more && count($summary[self::LINKS]) < $max_links) {
                    /* some entries gave no link, so later entries are
                       read to make up the number */
                    [$entries_page] = self::entriesPage($page, 0);
                    $summary[self::LINKS] = self::links(
                        L\getDomFromString($entries_page), $url);
                }
                if (strlen($summary[self::DESCRIPTION] . $summary[self::TITLE])
                    == 0 && count($summary[self::LINKS]) == 0) {
                    /* maybe not a sitemap, so it is read as text, which
                       may still give urls */
                    $summary = parent::process($page, $url);
                }
                $summary[self::JUST_METAS] = true;
            } else {
                $summary = parent::process($page, $url);
                $summary[self::JUST_METAS] = true;
            }
        }
        return $summary;
    }
    /**
     * entriesPage gives the part of the sitemap $page that holds its first
     * $max_entries entries, url elements in a urlset or sitemap elements in
     * a sitemap index. The text is cut after the end tag of the last of
     * them, and the root element's end tag is put after it, so the part
     * parses as a whole sitemap. A sitemap of ten megabytes is then parsed
     * only as far as the entries that can give links. A sitemap cut off
     * where the fetcher stopped uncompressing it is cut back the same way,
     * after its last whole entry, or after the loc element of the entry the
     * cut runs through if that loc is whole, with the entry closed. So no
     * url is taken from a loc the cut runs through. A $max_entries of 0 or
     * less keeps every whole entry.
     * A page that is not such a sitemap, or one holding no more entries
     * than asked and ending in its root's end tag, is given back as it is.
     * process calls it before parsing a sitemap.
     *
     * @param string $page the text of a sitemap
     * @param int $max_entries how many entries to keep, 0 or less for all
     * @return array [the text to parse, whether $page has whole entries
     *      after it]
     */
    public static function entriesPage($page, $max_entries)
    {
        $entry_names = ["urlset" => "url", "sitemapindex" => "sitemap"];
        $root_name = XmlProcessor::rootName($page);
        $entry_name = $entry_names[$root_name] ?? "";
        if ($entry_name == "") {
            return [$page, false];
        }
        $entry_end = "</$entry_name>";
        $end = 0;
        $count = 0;
        while (($max_entries <= 0 || $count < $max_entries) &&
            ($position = strpos($page, $entry_end, $end)) !== false) {
            $end = $position + strlen($entry_end);
            $count++;
        }
        $more = $end > 0 && strpos($page, $entry_end, $end) !== false;
        if ($end == 0 ||
            (!$more && strpos($page, "</$root_name>", $end) !== false)) {
            return [$page, false];
        }
        $close_tags = "</$root_name>";
        $loc_end = ($more) ? false : strpos($page, "</loc>", $end);
        if ($loc_end !== false) {
            $end = $loc_end + strlen("</loc>");
            $close_tags = $entry_end . $close_tags;
        }
        return [substr($page, 0, $end) . $close_tags, $more];
    }
    /**
     * links gives the urls listed by the loc elements of a parsed sitemap
     * or sitemap index, each made canonical against $site. It stops after
     * max_links_to_extract loc elements have given a url, counting a url
     * each time it is listed, so a url listed twice uses two of the
     * places. Priority and change frequency entries are not read. process
     * calls it on the part of the sitemap entriesPage gives.
     *
     * @param \DOMDocument $dom the parsed sitemap
     * @param string $site the url of the sitemap
     * @return array links of the sitemap, url => "From sitemap" text
     *     naming the sitemap and the words of the url
     */
    public static function links($dom, $site)
    {
        $sites = [];
        $xpath = new \DOMXPath($dom);
        $xpath->registerNamespace('s',
            "http://www.sitemaps.org/schemas/sitemap/0.9");
        $paths = [
            "/s:urlset/s:url/s:loc",
            "/s:sitemapindex/s:sitemap/s:loc"
        ];
        $i = 0;
        foreach ($paths as $path) {
            $nodes = @$xpath->evaluate($path);
            foreach ($nodes as $node) {
                $node_content = (empty($node->textContent)) ?
                    $node->nodeValue : $node->textContent;
                $url = UrlParser::canonicalLink(
                    $node_content, $site);
                if ($url === null || $url === "" ||
                    UrlParser::checkRecursiveUrl($url) ||
                    strlen($url) >= C\MAX_URL_LEN) {
                    continue;
                }
                $sites[$url] = "From sitemap : " . $site . " .." .
                    UrlParser::extractTextFromUrl($url);
                $i++;
                if (self::$max_links_to_extract > 0 &&
                    $i >= self::$max_links_to_extract) {
                    break 2;
                }
            }
        }
        return $sites;
    }
}