<?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\tests;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\controllers\SearchController;
use seekquarry\yioop\library\UnitTest;
/**
* Unit tests for the check that decides whether a search came from a
* person or from a program crawling the site. A search the check turns
* away is never run, so a browser wrongly caught by it cannot use the
* site at all; the user agents below are the real ones this has to get
* right.
*
* @author Chris Pollett
*/
class SearchControllerBotTest extends UnitTest
{
/**
* The controller whose check for bots is under test.
* @var SearchController
*/
public $controller;
/**
* Builds the controller without its constructor, which would want a
* web site and a database that this check has nothing to do with.
*/
public function setUp()
{
$reflection = new \ReflectionClass(SearchController::class);
$this->controller = $reflection->newInstanceWithoutConstructor();
}
/**
* Nothing to take down between test cases.
*/
public function tearDown()
{
}
/**
* The user agents real browsers send are let through. Each of these
* was sent by a browser people search with, and every one of them
* names Apple, because the engine Chrome and Edge are built on
* carries that name wherever it runs.
*/
public function browsersAreNotBotsTestCase()
{
$browsers = [
"Chrome on Windows" => "Mozilla/5.0 (Windows NT 10.0; Win64;" .
" x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126" .
".0.0.0 Safari/537.36",
"Chrome on macOS" => "Mozilla/5.0 (Macintosh; Intel Mac OS X" .
" 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome" .
"/126.0.0.0 Safari/537.36",
"Chrome on Linux" => "Mozilla/5.0 (X11; Linux x86_64) Apple" .
"WebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 " .
"Safari/537.36",
"Chrome on Android" => "Mozilla/5.0 (Linux; Android 10; K) " .
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0" .
" Mobile Safari/537.36",
"Edge on Windows" => "Mozilla/5.0 (Windows NT 10.0; Win64; " .
"x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126." .
"0.0.0 Safari/537.36 Edge/126.0.0.0",
"Safari on macOS" => "Mozilla/5.0 (Macintosh; Intel Mac OS X" .
" 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) " .
"Version/17.0 Safari/605.1.15",
"Safari on iPhone" => "Mozilla/5.0 (iPhone; CPU iPhone OS " .
"17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like " .
"Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
"Firefox on Windows" => "Mozilla/5.0 (Windows NT 10.0; Win64" .
"; x64; rv:126.0) Gecko/20100101 Firefox/126.0",
"Firefox on Linux" => "Mozilla/5.0 (X11; Linux x86_64; rv:" .
"126.0) Gecko/20100101 Firefox/126.0",
"Lynx" => "Lynx/2.9.0dev.10 libwww-FM/2.14 SSL-MM/1.4.1 " .
"OpenSSL/3.0.11"
];
foreach ($browsers as $who => $user_agent) {
$this->assertTrue(
!$this->controller->isBotUserAgent($user_agent),
"$who is a person searching, not a bot");
}
}
/**
* A program that names itself a crawler is taken at its word, wherever
* in its user agent it says so.
*/
public function namedBotsAreBotsTestCase()
{
$bots = [
"Googlebot" => "Mozilla/5.0 (compatible; Googlebot/2.1; +" .
"http://www.google.com/bot.html)",
"Bingbot" => "Mozilla/5.0 (compatible; bingbot/2.0; +http://" .
"www.bing.com/bingbot.htm)",
"Baidu" => "Mozilla/5.0 (compatible; Baiduspider/2.0; +http:" .
"//www.baidu.com/search/spider.html)",
"Yandex" => "Mozilla/5.0 (compatible; YandexBot/3.0; +http://" .
"yandex.com/bots)",
"Ahrefs" => "Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://" .
"ahrefs.com/robot/)",
"an unnamed crawler" => "Some Program (crawler)",
"an unnamed spider" => "Some Program (spider)"
];
foreach ($bots as $who => $user_agent) {
$this->assertTrue($this->controller->isBotUserAgent($user_agent),
"$who says it crawls and is treated as a bot");
}
}
/**
* A user agent claiming to be every browser at once, or to be running
* on every system at once, is not any of them.
*/
public function claimingEverythingIsABotTestCase()
{
$this->assertTrue($this->controller->isBotUserAgent(
"Mozilla/5.0 Chrome/1.0 Safari/1.0 Edge/1.0 Firefox/1.0"),
"claiming four browsers at once is a program listing them");
$this->assertTrue($this->controller->isBotUserAgent(
"Mozilla/5.0 (Windows NT 10.0; Linux x86_64; Mac OS X) " .
"AppleWebKit/537.36"),
"claiming three systems at once is not one machine");
$this->assertTrue(!$this->controller->isBotUserAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" .
" (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edge/" .
"126.0.0.0"),
"Edge claims three browsers and two systems and is a browser");
}
/**
* What a request asks for says nothing about who is asking. The site's
* own pages ask for further results in a form of their own, and
* reading that as evidence of a bot had the site turning away its own
* readers mid-page.
*/
public function whatIsAskedForIsNotWhoIsAskingTestCase()
{
$chrome = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Apple" .
"WebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/" .
"537.36";
$_REQUEST["f"] = "api";
$_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1";
$this->assertTrue(!$this->controller->isBotUserAgent($chrome),
"a browser reading further results is still a browser");
$_SERVER["SERVER_PROTOCOL"] = "HTTP/1.0";
$this->assertTrue(!$this->controller->isBotUserAgent($chrome),
"the protocol a request arrives on says nothing about it");
unset($_REQUEST["f"]);
unset($_SERVER["SERVER_PROTOCOL"]);
}
/**
* A request that says nothing about itself is let through. An empty
* user agent is rude rather than damning, and turning it away would
* catch anyone whose browser was told to send nothing.
*/
public function sayingNothingIsNotABotTestCase()
{
$this->assertTrue(!$this->controller->isBotUserAgent(""),
"a request saying nothing about itself is let through");
}
}