diff --git a/app/Base.php b/app/Base.php
index d5a85bb2e..c6483566c 100644
--- a/app/Base.php
+++ b/app/Base.php
@@ -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 = ?
diff --git a/app/BaseRequestContext.php b/app/BaseRequestContext.php
new file mode 100644
index 000000000..77984ec01
--- /dev/null
+++ b/app/BaseRequestContext.php
@@ -0,0 +1,80 @@
+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;
+ }
+}
diff --git a/app/DB/Mysql.php b/app/DB/Mysql.php
index 40ad383af..228365e8f 100644
--- a/app/DB/Mysql.php
+++ b/app/DB/Mysql.php
@@ -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
diff --git a/app/Debug.php b/app/Debug.php
index ba1d23f57..bf744dde4 100644
--- a/app/Debug.php
+++ b/app/Debug.php
@@ -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}"
);
}
diff --git a/app/Search/Torrent.php b/app/Search/Torrent.php
index f063e85fb..662f636ae 100644
--- a/app/Search/Torrent.php
+++ b/app/Search/Torrent.php
@@ -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);
diff --git a/bin/scheduler b/bin/scheduler
index 654334479..1debc2f06 100755
--- a/bin/scheduler
+++ b/bin/scheduler
@@ -2,4 +2,8 @@
run();
diff --git a/classes/sphinxql.class.php b/classes/sphinxql.class.php
index e0a98df2d..8067b4d16 100644
--- a/classes/sphinxql.class.php
+++ b/classes/sphinxql.class.php
@@ -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 '
' . display_str($ErrorMsg) . '
';
- 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 '' . display_str($error) . '
';
+ die();
+ } else {
+ error('-1');
+ }
}
}
diff --git a/classes/text.class.php b/classes/text.class.php
index 287081bc7..3cc10c6ac 100644
--- a/classes/text.class.php
+++ b/classes/text.class.php
@@ -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(["\n", "\n", "", ""], "", $Str);
$Str = str_replace(["\r\n", "\n"], "", $Str);
$Str = preg_replace("/\([a-zA-Z0-9 ]+)\<\/strong\>\: \/", "[spoiler=\\1]", $Str);
diff --git a/classes/view.class.php b/classes/view.class.php
index 38ac6a10e..df2ec6c10 100644
--- a/classes/view.class.php
+++ b/classes/view.class.php
@@ -14,11 +14,14 @@ class View {
* @param array $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('%s', $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])) {
diff --git a/gazelle.php b/gazelle.php
index 76772009e..c45b9447b 100644
--- a/gazelle.php
+++ b/gazelle.php
@@ -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);
}
diff --git a/lib/bootstrap.php b/lib/bootstrap.php
index 18ea87239..4092122f7 100644
--- a/lib/bootstrap.php
+++ b/lib/bootstrap.php
@@ -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')) {
diff --git a/lib/util.php b/lib/util.php
index 9a315d4a3..37916fd99 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -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;
}
diff --git a/sections/login/login.php b/sections/login/login.php
index 53885c1fb..6a06c3385 100644
--- a/sections/login/login.php
+++ b/sections/login/login.php
@@ -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),
diff --git a/tests/phpunit/BaseRequestContext.php b/tests/phpunit/BaseRequestContext.php
new file mode 100644
index 000000000..54e6e28b3
--- /dev/null
+++ b/tests/phpunit/BaseRequestContext.php
@@ -0,0 +1,68 @@
+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',
+ );
+ }
+}
diff --git a/tests/phpunit/DonorTest.php b/tests/phpunit/DonorTest.php
index 60ed98e3f..630e3cae2 100644
--- a/tests/phpunit/DonorTest.php
+++ b/tests/phpunit/DonorTest.php
@@ -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', [
diff --git a/tests/phpunit/ForumTest.php b/tests/phpunit/ForumTest.php
index ab8ceb81e..ebff0545b 100644
--- a/tests/phpunit/ForumTest.php
+++ b/tests/phpunit/ForumTest.php
@@ -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(
"$name",
(Gazelle\Util\Twig::factory())->render('forum/forum.twig', [
diff --git a/tests/phpunit/FriendTest.php b/tests/phpunit/FriendTest.php
index 0f50b097c..4c80d0e72 100644
--- a/tests/phpunit/FriendTest.php
+++ b/tests/phpunit/FriendTest.php
@@ -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();
diff --git a/tests/phpunit/LogTest.php b/tests/phpunit/LogTest.php
index 12fd9d879..2f0208b8e 100644
--- a/tests/phpunit/LogTest.php
+++ b/tests/phpunit/LogTest.php
@@ -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', [
diff --git a/tests/phpunit/UploadTest.php b/tests/phpunit/UploadTest.php
index 94e96f3e2..89a4936b2 100644
--- a/tests/phpunit/UploadTest.php
+++ b/tests/phpunit/UploadTest.php
@@ -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;
diff --git a/tests/phpunit/Util/TwigTest.php b/tests/phpunit/Util/TwigTest.php
index 646005850..a3a05b278 100644
--- a/tests/phpunit/Util/TwigTest.php
+++ b/tests/phpunit/Util/TwigTest.php
@@ -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('', self::twig('{{ header("page") }}')->render(), 'twig-function-header');
$current = (new Gazelle\User\Session($Viewer))->create([