store the request context in the Base class

This commit is contained in:
Spine
2024-06-18 07:04:14 +00:00
parent 576f5b8b22
commit d04e7cd7d6
20 changed files with 339 additions and 145 deletions

View File

@@ -5,16 +5,25 @@ namespace Gazelle;
abstract class Base {
protected static DB\Mysql $db;
protected static Cache $cache;
protected static BaseRequestContext $requestContext;
protected static \Twig\Environment $twig;
public static function initialize(Cache $cache, DB\Mysql $db, \Twig\Environment $twig): void {
self::$db = $db;
self::$cache = $cache;
self::$twig = $twig;
static::$db = $db;
static::$cache = $cache;
static::$twig = $twig;
}
public static function setRequestContext(BaseRequestContext $c): void {
static::$requestContext = $c;
}
public function requestContext(): BaseRequestContext {
return static::$requestContext;
}
public function enumList(string $table, string $column): array {
$columnType = (string)self::$db->scalar("
$columnType = (string)static::$db->scalar("
SELECT column_type
FROM information_schema.columns
WHERE table_schema = ?
@@ -29,7 +38,7 @@ abstract class Base {
}
public function enumDefault(string $table, string $column): ?string {
$default = self::$db->scalar("
$default = static::$db->scalar("
SELECT column_default
FROM information_schema.columns
WHERE table_schema = ?

View File

@@ -0,0 +1,80 @@
<?php
namespace Gazelle;
class BaseRequestContext {
protected string $module; // previously known as global $Document
protected bool $isValid;
protected array $ua;
public function __construct(
protected readonly string $scriptName,
protected string $remoteAddr,
string $useragent,
) {
$info = pathinfo($scriptName);
if (!array_key_exists('dirname', $info)) {
$this->module = '';
$this->isValid = false;
} else {
$this->module = $info['filename'];
$this->isValid = $info['dirname'] === '/';
}
$this->ua = \parse_user_agent($useragent);
}
public function ua(): array {
return $this->ua;
}
public function browser(): ?string {
return $this->ua()['Browser'];
}
public function browserVersion(): ?string {
return $this->ua()['BrowserVersion'];
}
public function isValid(): bool {
return $this->isValid;
}
public function module(): string {
return $this->module;
}
public function os(): ?string {
return $this->ua()['OperatingSystem'];
}
public function osVersion(): ?string {
return $this->ua()['OperatingSystemVersion'];
}
public function remoteAddr(): string {
return $this->remoteAddr;
}
/**
* Because we <3 our staff
*/
public function anonymize(): static {
$this->ua = [
'Browser' => 'staff-browser',
'BrowserVersion' => null,
'OperatingSystem' => null,
'OperatingSystemVersion' => null,
];
$this->remoteAddr = '127.0.0.1';
return $this;
}
/**
* Early in the startup phase, it may be desirable to
* redirect processing to another section.
*/
public function setModule(string $module): static {
$this->module = $module;
return $this;
}
}

View File

@@ -263,17 +263,28 @@ class Mysql {
if ($this->LinkID === false) {
return false;
}
// In the event of a MySQL deadlock, we sleep allowing MySQL time to unlock, then attempt again for a maximum of 5 tries
// In the event of a MySQL deadlock, we sleep allowing MySQL time to unlock
// then attempt again for a maximum of 5 attempts
$sleep = 0.5;
for ($i = 1; $i < 6; $i++) {
$this->QueryID = $Closure();
if (!in_array(mysqli_errno($this->LinkID), [1213, 1205])) {
break;
}
global $Debug;
$Debug->analysis('Non-Fatal Deadlock:', $Query, 3600 * 24);
// if we have a viewer, we have a request context, otherwise, it must be script
global $Debug, $Viewer;
$Debug->analysis(
is_null($Viewer) && isset($_SERVER['argv'])
? ($_SERVER['argv'][0] ?? 'cli')
: $Viewer->requestContext()->module(),
'Non-Fatal Deadlock:',
$Query,
86_400,
);
trigger_error("Database deadlock, attempt $i");
sleep($i * random_int(2, 5)); // Wait longer as attempts increase
usleep((int)($sleep * 1e6));
$sleep *= 1.75;
}
$QueryEndTime = microtime(true);
// Kills admin pages, and prevents Debug->analysis when the whole set exceeds 1 MB

View File

@@ -73,7 +73,10 @@ class Debug {
}
if (isset($Reason[0])) {
$this->analysis(implode(', ', $Reason));
$this->analysis(
$user->requestContext()->module(),
implode(', ', $Reason)
);
return true;
}
@@ -135,23 +138,22 @@ class Debug {
return $id;
}
public function analysis($Message, $Report = ''): void {
$RequestURI = empty($_SERVER['REQUEST_URI']) ? '' : substr($_SERVER['REQUEST_URI'], 1);
public function analysis(string $module, string $message, string $report = ''): void {
$uri = empty($_SERVER['REQUEST_URI']) ? '' : substr($_SERVER['REQUEST_URI'], 1);
if (
PHP_SAPI === 'cli'
|| in_array($RequestURI, ['tools.php?action=db_sandbox'])
|| in_array($uri, ['tools.php?action=db_sandbox'])
) {
// Don't spam IRC from Boris or these pages
return;
}
if (empty($Report)) {
$Report = $Message;
if (empty($report)) {
$report = $message;
}
$case = $this->saveCase($Report);
global $Document;
Irc::sendMessage(IRC_CHAN_STATUS, "{$Message} $Document "
$case = $this->saveCase($report);
Irc::sendMessage(IRC_CHAN_STATUS, "{$message} $module "
. SITE_URL . "/tools.php?action=analysis&case=$case "
. SITE_URL . '/' . $RequestURI
. SITE_URL . "/{$uri}"
);
}

View File

@@ -192,7 +192,12 @@ class Torrent {
) {
$ErrMsg = "Search\Torrent constructor arguments:\n" . print_r(func_get_args(), true);
global $Debug;
$Debug->analysis('Bad arguments in Search\Torrent constructor', $ErrMsg, 3600 * 24);
$Debug->analysis(
$tgMan->requestContext()->module(),
'Bad arguments in Search\Torrent constructor',
$ErrMsg,
86_400,
);
error('-1');
}
$this->Page = $searchMany ? $Page : min($Page, SPHINX_MAX_MATCHES / $PageSize);

View File

@@ -2,4 +2,8 @@
<?php
require_once(__DIR__ . '/../lib/bootstrap.php');
Gazelle\Base::setRequestContext(
new Gazelle\BaseRequestContext('cli', '127.0.0.1', 'scheduler')
);
(new Gazelle\TaskScheduler)->run();

View File

@@ -91,19 +91,23 @@ class Sphinxql extends mysqli {
/**
* Print a message to privileged users and optionally halt page processing
*
* @param string $Msg message to display
* @param bool $Halt halt page processing. Default is to continue processing the page
*/
public function error($Msg, $Halt = false) {
public function error(string $message, bool $halt = false) {
global $Debug, $Viewer;
$ErrorMsg = 'SphinxQL (' . $this->Ident . '): ' . strval($Msg);
$Debug->analysis('SphinxQL Error', $ErrorMsg, 3600 * 24);
if ($Halt === true && (DEBUG_MODE || $Viewer->permitted('site_debug'))) {
echo '<pre>' . display_str($ErrorMsg) . '</pre>';
die();
} elseif ($Halt === true) {
error('-1');
$error = "SphinxQL ({$this->Ident}): $message";
$Debug->analysis(
$Viewer->requestContext()->module(),
'SphinxQL Error',
$error,
86_400,
);
if ($halt === true) {
if (DEBUG_MODE || $Viewer->permitted('site_debug')) {
echo '<pre>' . display_str($error) . '</pre>';
die();
} else {
error('-1');
}
}
}

View File

@@ -337,8 +337,14 @@ class Text {
}
parse_str($info['query'] ?? '', $args);
if (isset($args['postid']) && isset($info['path']) && in_array($info['path'],
['/artist.php', '/collages.php', '/requests.php', '/torrents.php'])) {
if (
isset($args['postid'])
&& isset($info['path'])
&& in_array(
$info['path'],
['/artist.php', '/collages.php', '/requests.php', '/torrents.php']
)
) {
return self::bbcodeCommentUrl((int)$args['postid']);
}
@@ -1207,8 +1213,8 @@ class Text {
* that html_escape does.
*/
public static function parse_html(string $Html): string {
$Document = new DOMDocument();
$Document->loadHTML(stripslashes($Html));
$dom = new DOMDocument();
$dom->loadHTML(stripslashes($Html));
// For any manipulation that we do on the DOM tree, always go in reverse order or
// else you end up with broken array pointers and missed elements
@@ -1222,42 +1228,42 @@ class Text {
}
};
$Elements = $Document->getElementsByTagName('div');
$Elements = $dom->getElementsByTagName('div');
for ($i = $Elements->length - 1; $i >= 0; $i--) {
/** @var \DOMElement $Element */
$Element = $Elements->item($i);
if (str_contains($Element->getAttribute('style'), 'text-align')) {
$NewElement = $Document->createElement('align');
$NewElement = $dom->createElement('align');
$CopyNode($Element, $NewElement);
$NewElement->setAttribute('align', str_replace('text-align: ', '', $Element->getAttribute('style')));
$Element->parentNode->replaceChild($NewElement, $Element);
}
}
$Elements = $Document->getElementsByTagName('span');
$Elements = $dom->getElementsByTagName('span');
for ($i = $Elements->length - 1; $i >= 0; $i--) {
/** @var \DOMElement $Element */
$Element = $Elements->item($i);
if (str_contains($Element->getAttribute('class'), 'size')) {
$NewElement = $Document->createElement('size');
$NewElement = $dom->createElement('size');
$CopyNode($Element, $NewElement);
$NewElement->setAttribute('size', str_replace('size', '', $Element->getAttribute('class')));
$Element->parentNode->replaceChild($NewElement, $Element);
} elseif (str_contains($Element->getAttribute('style'), 'font-style: italic')) {
$NewElement = $Document->createElement('italic');
$NewElement = $dom->createElement('italic');
$CopyNode($Element, $NewElement);
$Element->parentNode->replaceChild($NewElement, $Element);
} elseif (str_contains($Element->getAttribute('style'), 'text-decoration: underline')) {
$NewElement = $Document->createElement('underline');
$NewElement = $dom->createElement('underline');
$CopyNode($Element, $NewElement);
$Element->parentNode->replaceChild($NewElement, $Element);
} elseif (str_contains($Element->getAttribute('style'), 'color: ')) {
$NewElement = $Document->createElement('color');
$NewElement = $dom->createElement('color');
$CopyNode($Element, $NewElement);
$NewElement->setAttribute('color', str_replace(['color: ', ';'], '', $Element->getAttribute('style')));
$Element->parentNode->replaceChild($NewElement, $Element);
} elseif (preg_match("/display:[ ]*inline\-block;[ ]*padding:/", $Element->getAttribute('style')) !== false) {
$NewElement = $Document->createElement('pad');
$NewElement = $dom->createElement('pad');
$CopyNode($Element, $NewElement);
$Padding = explode(' ', trim(explode(':', (explode(';', $Element->getAttribute('style'))[1]))[1]));
$NewElement->setAttribute('pad', implode('|', array_map(fn($x) => rtrim($x, 'px'), $Padding)));
@@ -1265,44 +1271,44 @@ class Text {
}
}
$Elements = $Document->getElementsByTagName('ul');
$Elements = $dom->getElementsByTagName('ul');
for ($i = 0; $i < $Elements->length; $i++) {
/** @var \DOMElement $Element */
$Element = $Elements->item($i);
$InnerElements = $Element->getElementsByTagName('li');
for ($j = $InnerElements->length - 1; $j >= 0; $j--) {
$Element = $InnerElements->item($j);
$NewElement = $Document->createElement('bullet');
$NewElement = $dom->createElement('bullet');
$CopyNode($Element, $NewElement);
$Element->parentNode->replaceChild($NewElement, $Element);
}
}
$Elements = $Document->getElementsByTagName('ol');
$Elements = $dom->getElementsByTagName('ol');
for ($i = 0; $i < $Elements->length; $i++) {
/** @var \DOMElement $Element */
$Element = $Elements->item($i);
$InnerElements = $Element->getElementsByTagName('li');
for ($j = $InnerElements->length - 1; $j >= 0; $j--) {
$Element = $InnerElements->item($j);
$NewElement = $Document->createElement('number');
$NewElement = $dom->createElement('number');
$CopyNode($Element, $NewElement);
$Element->parentNode->replaceChild($NewElement, $Element);
}
}
$Elements = $Document->getElementsByTagName('strong');
$Elements = $dom->getElementsByTagName('strong');
for ($i = $Elements->length - 1; $i >= 0; $i--) {
/** @var \DOMElement $Element */
$Element = $Elements->item($i);
if (in_array('important_text', explode(' ', $Element->getAttribute('class')))) {
$NewElement = $Document->createElement('important');
$NewElement = $dom->createElement('important');
$CopyNode($Element, $NewElement);
$Element->parentNode->replaceChild($NewElement, $Element);
}
}
$Elements = $Document->getElementsByTagName('a');
$Elements = $dom->getElementsByTagName('a');
for ($i = $Elements->length - 1; $i >= 0; $i--) {
/** @var \DOMElement $Element */
$Element = $Elements->item($i);
@@ -1311,14 +1317,16 @@ class Text {
$Element->removeAttribute('target');
if ($Element->getAttribute('href') === $Element->nodeValue) {
$Element->removeAttribute('href');
} elseif ($Element->getAttribute('href') === 'javascript:void(0);'
&& $Element->getAttribute('onclick') === 'BBCode.spoiler(this);') {
$Spoilers = $Document->getElementsByTagName('blockquote');
} elseif (
$Element->getAttribute('href') === 'javascript:void(0);'
&& $Element->getAttribute('onclick') === 'BBCode.spoiler(this);'
) {
$Spoilers = $dom->getElementsByTagName('blockquote');
for ($j = $Spoilers->length - 1; $j >= 0; $j--) {
/** @var \DOMElement $Spoiler */
$Spoiler = $Spoilers->item($j);
if ($Spoiler->hasAttribute('class') && $Spoiler->getAttribute('class') === 'hidden spoiler') {
$NewElement = $Document->createElement('spoiler');
$NewElement = $dom->createElement('spoiler');
$CopyNode($Spoiler, $NewElement);
$Element->parentNode->replaceChild($NewElement, $Element);
$Spoiler->parentNode->removeChild($Spoiler);
@@ -1326,18 +1334,18 @@ class Text {
}
}
} elseif (str_starts_with($Element->getAttribute('href'), 'artist.php?artistname=')) {
$NewElement = $Document->createElement('artist');
$NewElement = $dom->createElement('artist');
$CopyNode($Element, $NewElement);
$Element->parentNode->replaceChild($NewElement, $Element);
} elseif (str_starts_with($Element->getAttribute('href'), 'user.php?action=search&search=')) {
$NewElement = $Document->createElement('user');
$NewElement = $dom->createElement('user');
$CopyNode($Element, $NewElement);
$Element->parentNode->replaceChild($NewElement, $Element);
}
}
}
$Str = (string)$Document->saveHTML($Document->getElementsByTagName('body')->item(0));
$Str = (string)$dom->saveHTML($dom->getElementsByTagName('body')->item(0));
$Str = str_replace(["<body>\n", "\n</body>", "<body>", "</body>"], "", $Str);
$Str = str_replace(["\r\n", "\n"], "", $Str);
$Str = preg_replace("/\<strong\>([a-zA-Z0-9 ]+)\<\/strong\>\: \<spoiler\>/", "[spoiler=\\1]", $Str);

View File

@@ -14,11 +14,14 @@ class View {
* @param array<string> $option
*/
public static function header(string $pageTitle, array $option = []): string {
global $Document, $Twig, $Viewer;
if ($pageTitle != '') {
$pageTitle .= ' :: ';
}
$pageTitle .= SITE_NAME;
global $Viewer;
$module = is_null($Viewer)
? 'index'
: $Viewer->requestContext()->module();
$js = [
'jquery',
@@ -34,6 +37,7 @@ class View {
array_push($js, ...explode(',', $option['js']));
}
global $Twig;
if (!isset($Viewer) || $pageTitle == 'Recover Password :: ' . SITE_NAME) {
$js[] = 'storage.class';
echo $Twig->render('index/public-header.twig', [
@@ -63,7 +67,7 @@ class View {
->setStaffPM(new Gazelle\Manager\StaffPM());
$notifier = new Gazelle\User\Notification($Viewer);
$alertList = $notifier->setDocument($Document, $_REQUEST['action'] ?? '')->alertList();
$alertList = $notifier->setDocument($module, $_REQUEST['action'] ?? '')->alertList();
foreach ($alertList as $alert) {
if (in_array($alert->display(), [Gazelle\User\Notification::DISPLAY_TRADITIONAL, Gazelle\User\Notification::DISPLAY_TRADITIONAL_PUSH])) {
$activity->setAlert(sprintf('<a href="%s">%s</a>', $alert->notificationUrl(), $alert->title()));
@@ -92,7 +96,7 @@ class View {
}
}
$PageID = [$Document, $_REQUEST['action'] ?? false, $_REQUEST['type'] ?? false];
$PageID = [$module, $_REQUEST['action'] ?? false, $_REQUEST['type'] ?? false];
$navLinks = [];
foreach ((new Gazelle\Manager\UserNavigation())->userControlList($Viewer) as $n) {
[$ID, $Key, $Title, $Target, $Tests, $TestUser, $Mandatory] = array_values($n);
@@ -149,7 +153,7 @@ class View {
'action_list' => $activity->actionList(),
'alert_list' => $activity->alertList(),
'bonus' => new Gazelle\User\Bonus($Viewer),
'document' => $Document,
'document' => $module,
'dono_target' => $payMan->monthlyPercent(new Gazelle\Manager\Donation()),
'nav_links' => $navLinks,
'user' => $Viewer,
@@ -201,8 +205,12 @@ class View {
$launch = SITE_LAUNCH_YEAR . "-$launch";
}
global $Document;
$alertList = (new Gazelle\User\Notification($Viewer))->setDocument($Document, $_REQUEST['action'] ?? '')->alertList();
$alertList = (new Gazelle\User\Notification($Viewer))
->setDocument(
$Viewer->requestContext()->module(),
$_REQUEST['action'] ?? ''
)
->alertList();
$notification = [];
foreach ($alertList as $alert) {
if (in_array($alert->display(), [Gazelle\User\Notification::DISPLAY_POPUP, Gazelle\User\Notification::DISPLAY_POPUP_PUSH])) {

View File

@@ -5,27 +5,6 @@ use Gazelle\Util\Time;
// 1. Basic sanity checks and initialization
if (PHP_VERSION_ID < 80201) {
die("Gazelle (Orpheus fork) requires at least PHP version 8.2.1");
}
foreach (['memcached', 'mysqli'] as $e) {
if (!extension_loaded($e)) {
die("$e extension not loaded");
}
}
date_default_timezone_set('UTC');
$PathInfo = pathinfo($_SERVER['SCRIPT_NAME']);
$Document = $PathInfo['filename'];
if ($PathInfo['dirname'] !== '/') { /** @phpstan-ignore-line */
exit;
} elseif (in_array($Document, ['announce', 'scrape']) || (isset($_REQUEST['info_hash']) && isset($_REQUEST['peer_id']))) {
die("d14:failure reason40:Invalid .torrent, try downloading again.e");
}
// 2. Start the engine
require_once(__DIR__ . '/lib/bootstrap.php');
global $Cache, $Debug, $Twig;
@@ -37,9 +16,27 @@ if (
) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
$_SERVER['HTTP_USER_AGENT'] = '[no-useragent]';
$context = new Gazelle\BaseRequestContext(
$_SERVER['SCRIPT_NAME'],
$_SERVER['REMOTE_ADDR'],
$_SERVER['HTTP_USER_AGENT'] ?? '[no-useragent]',
);
if (!$context->isValid()) {
exit;
}
$module = $context->module();
if (
in_array($module, ['announce', 'scrape'])
|| (
isset($_REQUEST['info_hash'])
&& isset($_REQUEST['peer_id'])
)
) {
die("d14:failure reason40:Invalid .torrent, try downloading again.e");
}
// 2. Start the engine
// 3. Do we have a viewer?
@@ -50,12 +47,12 @@ $userMan = new Gazelle\Manager\User();
Gazelle\Util\Twig::setUserMan($userMan);
// Authorization header only makes sense for the ajax endpoint
if (!empty($_SERVER['HTTP_AUTHORIZATION']) && $Document === 'ajax') {
if ($ipv4Man->isBanned($_SERVER['REMOTE_ADDR'])) {
if (!empty($_SERVER['HTTP_AUTHORIZATION']) && $module === 'ajax') {
if ($ipv4Man->isBanned($context->remoteAddr())) {
header('Content-type: application/json');
json_die('failure', 'your ip address has been banned');
}
[$success, $result] = $userMan->findByAuthorization($ipv4Man, $_SERVER['HTTP_AUTHORIZATION'], $_SERVER['REMOTE_ADDR']);
[$success, $result] = $userMan->findByAuthorization($ipv4Man, $_SERVER['HTTP_AUTHORIZATION'], $context->remoteAddr());
if ($success) {
$Viewer = $result;
define('AUTHED_BY_TOKEN', true);
@@ -66,7 +63,7 @@ if (!empty($_SERVER['HTTP_AUTHORIZATION']) && $Document === 'ajax') {
} elseif (isset($_COOKIE['session'])) {
$forceLogout = function (): never {
setcookie('session', '', [
'expires' => time() - 60 * 60 * 24 * 90,
'expires' => time() - 86_400 * 90,
'path' => '/',
'secure' => !DEBUG_MODE,
'httponly' => true,
@@ -84,7 +81,7 @@ if (!empty($_SERVER['HTTP_AUTHORIZATION']) && $Document === 'ajax') {
if (is_null($Viewer)) {
$forceLogout();
}
if ($Viewer->isDisabled() && !in_array($Document, ['index', 'login'])) {
if ($Viewer->isDisabled() && !in_array($module, ['index', 'login'])) {
$Viewer->logoutEverywhere();
$forceLogout();
}
@@ -93,26 +90,22 @@ if (!empty($_SERVER['HTTP_AUTHORIZATION']) && $Document === 'ajax') {
$Viewer->logout($SessionID);
$forceLogout();
}
$browser = parse_user_agent($_SERVER['HTTP_USER_AGENT']);
if ($Viewer->permitted('site_disable_ip_history')) {
$ipaddr = '127.0.0.1';
$browser['BrowserVersion'] = null;
$browser['OperatingSystemVersion'] = null;
} else {
$ipaddr = $_SERVER['REMOTE_ADDR'];
$context->anonymize();
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
}
$session->refresh($SessionID, $ipaddr, $browser);
unset($ipaddr, $browser, $session, $userId, $cookieData, $forceLogout);
} elseif ($Document === 'torrents' && ($_REQUEST['action'] ?? '') == 'download' && isset($_REQUEST['torrent_pass'])) {
$session->refresh($SessionID, $context->remoteAddr(), $context->ua());
unset($browser, $session, $userId, $cookieData, $forceLogout);
} elseif ($module === 'torrents' && ($_REQUEST['action'] ?? '') == 'download' && isset($_REQUEST['torrent_pass'])) {
$Viewer = $userMan->findByAnnounceKey($_REQUEST['torrent_pass']);
if (is_null($Viewer) || $Viewer->isDisabled() || $Viewer->isLocked()) {
header('HTTP/1.1 403 Forbidden');
exit;
}
} elseif (!in_array($Document, ['enable', 'index', 'login', 'recovery', 'register'])) {
} elseif (!in_array($module, ['enable', 'index', 'login', 'recovery', 'register'])) {
if (
// Ocelot is allowed
!($Document === 'tools' && ($_GET['action'] ?? '') === 'ocelot' && ($_GET['key'] ?? '') === TRACKER_SECRET)
!($module === 'tools' && ($_GET['action'] ?? '') === 'ocelot' && ($_GET['key'] ?? '') === TRACKER_SECRET)
) {
// but for everything else, we need a $Viewer
header('Location: login.php');
@@ -126,22 +119,18 @@ if ($Viewer) {
if ($Viewer->hasAttr('admin-error-reporting')) {
error_reporting(E_ALL);
}
// Because we <3 our staff
if ($Viewer->permitted('site_disable_ip_history')) {
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1';
$_SERVER['HTTP_X_REAL_IP'] = '127.0.0.1';
$_SERVER['HTTP_USER_AGENT'] = 'staff-browser';
$context->anonymize();
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
}
if ($Viewer->ipaddr() != $_SERVER['REMOTE_ADDR'] && !$Viewer->permitted('site_disable_ip_history')) {
if ($ipv4Man->isBanned($_SERVER['REMOTE_ADDR'])) {
if ($Viewer->ipaddr() != $context->remoteAddr() && !$Viewer->permitted('site_disable_ip_history')) {
if ($ipv4Man->isBanned($context->remoteAddr())) {
error('Your IP address has been banned.');
}
$ipv4Man->register($Viewer, $_SERVER['REMOTE_ADDR']);
$ipv4Man->register($Viewer, $context->remoteAddr());
}
if ($Viewer->isLocked() && !in_array($Document, ['staffpm', 'ajax', 'locked', 'logout', 'login'])) {
$Document = 'locked';
if ($Viewer->isLocked() && !in_array($module, ['staffpm', 'ajax', 'locked', 'logout', 'login'])) {
$context->setModule('locked');
}
// To proxify images (or not), or e.g. not render the name of a thread
@@ -153,11 +142,12 @@ $Debug->set_flag('load page');
if (DEBUG_MODE || ($Viewer && $Viewer->permitted('site_debug'))) {
$Twig->addExtension(new Twig\Extension\DebugExtension());
}
Gazelle\Base::setRequestContext($context);
// for sections/tools/development/process_info.php
$Cache->cache_value('php_' . getmypid(), [
'start' => Time::sqlTime(),
'document' => $Document,
'document' => $module,
'query' => $_SERVER['QUERY_STRING'],
'get' => $_GET,
'post' => array_diff_key(
@@ -184,8 +174,8 @@ register_shutdown_function(
header('Cache-Control: no-cache, must-revalidate, post-check=0, pre-check=0');
header('Pragma: no-cache');
$file = realpath(__DIR__ . "/sections/{$Document}/index.php");
if (!$file || !preg_match('/^[a-z][a-z0-9_]+$/', $Document)) {
$file = realpath(__DIR__ . "/sections/{$module}/index.php");
if (!$file || !preg_match('/^[a-z][a-z0-9_]+$/', $module)) {
error($Viewer ? 403 : 404);
}
@@ -210,5 +200,5 @@ try {
$Debug->set_flag('and send to user');
if (!is_null($Viewer)) {
$Debug->profile($Viewer, $Document);
$Debug->profile($Viewer, $module);
}

View File

@@ -2,6 +2,16 @@
/* require this file to have a fully-initialized Gazelle runtime */
if (PHP_VERSION_ID < 80201) {
die("Gazelle (Orpheus fork) requires at least PHP version 8.2.1");
}
foreach (['memcached', 'mysqli'] as $e) {
if (!extension_loaded($e)) {
die("$e extension not loaded");
}
}
date_default_timezone_set('UTC');
$now = microtime(true); // To track how long a page takes to create
if (!defined('SITE_NAME')) {

View File

@@ -355,10 +355,10 @@ function parse_user_agent(string $useragent): array {
* $Log If true, the user is given a link to search $Log in the site log.
*/
function error(int|string $Error, bool $NoHTML = false, bool $Log = false): never {
global $Debug, $Document, $Viewer, $Twig;
global $Debug, $Viewer, $Twig;
require_once(__DIR__ . '/../sections/error/index.php');
if (isset($Viewer)) {
$Debug->profile($Viewer, $Document);
$Debug->profile($Viewer, $Viewer->requestContext()->module());
}
exit;
}

View File

@@ -39,23 +39,21 @@ if (!empty($_POST['username']) && !empty($_POST['password'])) {
echo $Twig->render('login/weak-password.twig');
exit;
}
$browser = parse_user_agent($_SERVER['HTTP_USER_AGENT']);
$useragent = $_SERVER['HTTP_USER_AGENT'] ?? '[no-useragent]';
$context = new Gazelle\BaseRequestContext(
$_SERVER['SCRIPT_NAME'],
$_SERVER['REMOTE_ADDR'],
$useragent,
);
if ($user->permitted('site_disable_ip_history')) {
$ipaddr = '127.0.0.1';
$browser['BrowserVersion'] = null;
$browser['OperatingSystemVersion'] = null;
$full_ua = 'staff-browser';
} else {
$ipaddr = $_SERVER['REMOTE_ADDR'];
$full_ua = $_SERVER['HTTP_USER_AGENT'];
$context->anonymize();
}
$session = new Gazelle\User\Session($user);
$current = $session->create([
'keep-logged' => $login->persistent() ? '1' : '0',
'browser' => $browser,
'ipaddr' => $ipaddr,
'useragent' => $full_ua,
'browser' => $context->ua(),
'ipaddr' => $context->remoteAddr(),
'useragent' => $useragent,
]);
setcookie('session', $session->cookie($current['SessionID']), [
'expires' => (int)$login->persistent() * (time() + 60 * 60 * 24 * 90),

View File

@@ -0,0 +1,68 @@
<?php
use PHPUnit\Framework\TestCase;
require_once(__DIR__ . '/../../lib/bootstrap.php');
require_once(__DIR__ . '/../helper.php');
class BaseRequestContext extends TestCase {
protected \Gazelle\User $user;
public function tearDown(): void {
if (isset($this->user)) {
$this->user->remove();
}
}
public function testBaseRequestContext(): void {
$context = new Gazelle\BaseRequestContext(
'/phpunit.php',
'224.0.0.1',
'Lidarr/3.5.8 (windows 98)',
);
$this->assertTrue($context->isValid(), 'context-is-valid');
$this->assertEquals('phpunit', $context->module(), 'context-module');
$this->assertEquals('224.0.0.1', $context->remoteAddr(), 'context-remote-addr');
$this->assertEquals('Lidarr', $context->browser(), 'context-browser');
$this->assertEquals('3.5', $context->browserVersion(), 'context-version-browser');
$this->assertEquals('98', $context->osVersion(), 'context-version-os');
$this->assertEquals('Windows', $context->os(), 'context-os');
$module = randomString(4);
$context->setModule($module);
$this->assertEquals($module, $context->module(), 'context-override-module');
$context->anonymize();
$this->assertEquals('127.0.0.1', $context->remoteAddr(), 'context-override-remoteaddr');
$this->assertEquals('staff-browser', $context->browser(), 'context-override-browser');
$this->assertNull($context->os(), 'context-override-os');
}
public function testBadRequest(): void {
$context = new Gazelle\BaseRequestContext('', '', '');
$this->assertFalse($context->isValid(), 'context-not-valid');
$this->assertNull($context->browser(), 'context-invalid-browser');
}
// Any object that derives from Base has access to the request context
public function testObject(): void {
Gazelle\Base::setRequestContext(
new Gazelle\BaseRequestContext(
'/phpunit.php',
'225.0.0.1',
'Lidarr/5.8.13 (windows 98)',
)
);
$this->assertEquals(
'225.0.0.1',
(new Gazelle\Manager\TGroup())->requestContext()->remoteAddr(),
'context-manager-ip',
);
$this->user = Helper::makeUser('base.' . randomString(6), 'base object');
$this->assertEquals(
'Lidarr',
$this->user->requestContext()->browser(),
'context-user-browser',
);
}
}

View File

@@ -441,8 +441,7 @@ class DonorTest extends TestCase {
]);
global $SessionID;
$SessionID = $current['SessionID']; // more sadness
global $Document;
$Document = 'index'; // utter misery
Gazelle\Base::setRequestContext(new Gazelle\BaseRequestContext('/index.php', '127.0.0.1', ''));
$paginator = (new Gazelle\Util\Paginator(USERS_PER_PAGE, 1))->setTotal($manager->rewardTotal());
$render = (Gazelle\Util\Twig::factory())->render('donation/reward-list.twig', [

View File

@@ -546,10 +546,11 @@ class ForumTest extends TestCase {
description: 'This is where it renders',
);
$paginator = (new Gazelle\Util\Paginator(TOPICS_PER_PAGE, 1))->setTotal(1);
global $Document, $SessionID, $Viewer; // to render header()
$Document = 'forum';
Gazelle\Base::setRequestContext(new Gazelle\BaseRequestContext('/forum.php', '127.0.0.1', ''));
global $SessionID; // to render header()
$SessionID = 'phpunit';
$Viewer = $admin;
global $Viewer;
$Viewer = $admin;
$this->assertStringContainsString(
"<a href=\"forums.php#$name\">$name</a>",
(Gazelle\Util\Twig::factory())->render('forum/forum.twig', [

View File

@@ -60,8 +60,8 @@ class FriendTest extends TestCase {
'ipaddr' => '127.0.0.1',
'useragent' => 'phpunit-browser',
]);
global $Document, $SessionID, $Viewer;
$Document = 'friends';
Gazelle\Base::setRequestContext(new Gazelle\BaseRequestContext('/friends.php', '127.0.0.1', ''));
global $SessionID, $Viewer;
$SessionID = $current['SessionID'];
$Viewer = $this->friend[0]->user();

View File

@@ -131,10 +131,9 @@ class LogTest extends TestCase {
// FIXME: $Viewer should not be necessary
$this->user = Helper::makeUser('sitelog.' . randomString(6), 'sitelog');
Gazelle\Base::setRequestContext(new Gazelle\BaseRequestContext('/index.php', '127.0.0.1', ''));
global $Viewer;
$Viewer = $this->user;
global $Document;
$Document = '';
global $SessionID;
$SessionID = 'phpunit';
$html = (Gazelle\Util\Twig::factory())->render('sitelog.twig', [

View File

@@ -21,8 +21,7 @@ class UploadTest extends TestCase {
$this->assertStringContainsString($this->user->auth(), $upload->head(0), 'upload-head');
global $Document;
$Document = '';
Gazelle\Base::setRequestContext(new Gazelle\BaseRequestContext('/upload.php', '127.0.0.1', ''));
global $SessionID;
$SessionID = '';
global $Viewer;

View File

@@ -210,10 +210,9 @@ END;
}
public function testFunction(): void {
global $Document;
$Document = 'index';
Gazelle\Base::setRequestContext(new Gazelle\BaseRequestContext('/index.php', '127.0.0.1', ''));
global $Viewer;
$Viewer = $this->user;
$Viewer = $this->user;
$this->assertStringStartsWith('<!DOCTYPE html>', self::twig('{{ header("page") }}')->render(), 'twig-function-header');
$current = (new Gazelle\User\Session($Viewer))->create([