<?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;
/**
* Turns the handful of yes/no choices an administrator makes for one
* domain into the text of that domain's robots.txt file. The choices
* say whether to keep all crawlers out, how long crawlers should wait
* between requests, and which kinds of pages (search results, group
* feeds, wiki pages, and a wiki page's source/history, discussion, and
* page listing) crawlers are allowed to visit. This class only builds
* the text from values handed to it; it never reads the database, so a
* controller or the front controller looks up a domain's saved choices
* and passes them in.
*
* @author Chris Pollett
*/
class RobotsTxtGenerator
{
/**
* For each kind of page, the addresses that lead to it. Both the
* clean-URL path and the plain query-string form are listed for
* every kind, so a disallowed kind is kept out whether the site is
* serving clean URLs or index.php query strings.
*/
const CATEGORY_PATHS = [
'search' => ['/s', '/*?*q=', '/*?*c=search'],
'feeds' => ['/group', '/user', '/*?*c=group'],
'wiki' => ['/p', '/*?*a=wiki'],
'page_source' => ['/*?*arg=source', '/*?*arg=history'],
'page_feed' => ['/thread', '/*?*just_thread='],
'page_list' => ['/*?*arg=pages'],
];
/**
* The choices to assume for a domain that has never been
* configured, so an unconfigured domain still serves a sensible
* robots.txt. Everything is crawlable except search-result pages
* (so crawlers do not walk every possible query), matching the
* intent of the old shipped robots.txt.
*/
const DEFAULT_CONFIG = [
'disallow_all' => false,
'crawl_delay' => 0,
'search' => false,
'feeds' => true,
'wiki' => true,
'page_source' => true,
'page_feed' => true,
'page_list' => true,
'additional' => "",
];
/**
* Builds the complete robots.txt body for one domain from its saved
* choices. When the choices say to keep all crawlers out, the body
* is just a blanket disallow. Otherwise the body opens for all
* crawlers, optionally asks for a crawl delay, adds a disallow line
* for every address that leads to a kind of page the administrator
* turned off, and finally appends any extra directives the
* administrator typed in by hand.
*
* @param array $config the saved yes/no choices for the domain;
* missing choices fall back to the unconfigured defaults
* @return string the complete robots.txt body, ending in a newline
*/
public static function generate($config)
{
$config = array_merge(self::DEFAULT_CONFIG, is_array($config) ?
$config : []);
if (!empty($config['disallow_all'])) {
return "User-agent: *\nDisallow: /\n";
}
$lines = ["User-agent: *"];
$crawl_delay = intval($config['crawl_delay']);
if ($crawl_delay > 0) {
$lines[] = "Crawl-delay: " . $crawl_delay;
}
$blocked = [];
if (empty($config['search'])) {
$blocked[] = 'search';
}
if (empty($config['feeds'])) {
$blocked[] = 'feeds';
}
if (empty($config['wiki'])) {
$blocked[] = 'wiki';
} else {
if (empty($config['page_source'])) {
$blocked[] = 'page_source';
}
if (empty($config['page_feed'])) {
$blocked[] = 'page_feed';
}
if (empty($config['page_list'])) {
$blocked[] = 'page_list';
}
}
foreach ($blocked as $category) {
foreach (self::CATEGORY_PATHS[$category] as $path) {
$lines[] = "Disallow: " . $path;
}
}
$text = implode("\n", $lines) . "\n";
$additional = trim($config['additional'] ?? "");
if ($additional !== "") {
$text .= $additional . "\n";
}
return $text;
}
}