PHP 8.4 and various fixes.

This commit is contained in:
flash 2025-01-19 20:59:30 +00:00
parent ada2baee17
commit 2ef4d6d66d
38 changed files with 639 additions and 983 deletions

View file

@ -1,8 +1,8 @@
{ {
"require": { "require": {
"flashwave/index": "^0.2410", "flashwave/index": "^0.2501",
"flashii/apii": "^0.2", "flashii/apii": "^0.3",
"erusev/parsedown": "~1.6", "erusev/parsedown": "^1.7",
"sentry/sdk": "^4.0" "sentry/sdk": "^4.0"
}, },
"autoload": { "autoload": {
@ -19,6 +19,6 @@
] ]
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^1.11" "phpstan/phpstan": "^2.1"
} }
} }

131
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "f71663659023233c6bbd47cc74f1d954", "content-hash": "ac4e8872cae3ee611a65a591d65acb1f",
"packages": [ "packages": [
{ {
"name": "erusev/parsedown", "name": "erusev/parsedown",
@ -58,11 +58,11 @@
}, },
{ {
"name": "flashii/apii", "name": "flashii/apii",
"version": "v0.2.1", "version": "v0.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://patchii.net/flashii/apii-php.git", "url": "https://patchii.net/flashii/apii-php.git",
"reference": "6a93d31375dd7e75ff9264f3024f2208ce602f49" "reference": "2d6c135faddd359341762afcb9c429e279d87059"
}, },
"require": { "require": {
"php": ">=8.1" "php": ">=8.1"
@ -91,25 +91,25 @@
], ],
"description": "Client library for the Flashii.net API.", "description": "Client library for the Flashii.net API.",
"homepage": "https://api.flashii.net", "homepage": "https://api.flashii.net",
"time": "2024-11-16T16:03:42+00:00" "time": "2024-11-22T21:36:01+00:00"
}, },
{ {
"name": "flashwave/index", "name": "flashwave/index",
"version": "v0.2410.191603", "version": "v0.2501.182241",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://patchii.net/flash/index.git", "url": "https://patchii.net/flash/index.git",
"reference": "17cdb4d1c239241200d7e30968122a8cd8b26509" "reference": "cab5f480db65f8a720c1ad0852423708bc415446"
}, },
"require": { "require": {
"ext-mbstring": "*", "ext-mbstring": "*",
"php": ">=8.3", "php": ">=8.4",
"twig/html-extra": "^3.13", "twig/html-extra": "^3.18",
"twig/twig": "^3.14" "twig/twig": "^3.18"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^1.11", "phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^11.2" "phpunit/phpunit": "^11.5"
}, },
"suggest": { "suggest": {
"ext-memcache": "Support for the Index\\Cache\\Memcached namespace (only if you can't use ext-memcached for some reason).", "ext-memcache": "Support for the Index\\Cache\\Memcached namespace (only if you can't use ext-memcached for some reason).",
@ -146,7 +146,7 @@
], ],
"description": "Composer package for the common library for my projects.", "description": "Composer package for the common library for my projects.",
"homepage": "https://railgun.sh/index", "homepage": "https://railgun.sh/index",
"time": "2024-10-19T16:04:17+00:00" "time": "2025-01-18T22:41:28+00:00"
}, },
{ {
"name": "guzzlehttp/psr7", "name": "guzzlehttp/psr7",
@ -671,16 +671,16 @@
}, },
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
"version": "v3.5.0", "version": "v3.5.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git", "url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -688,12 +688,12 @@
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": { "branch-alias": {
"dev-main": "3.5-dev" "dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
} }
}, },
"autoload": { "autoload": {
@ -718,7 +718,7 @@
"description": "A generic function and convention to trigger deprecation notices", "description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
}, },
"funding": [ "funding": [
{ {
@ -734,20 +734,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-04-18T09:32:20+00:00" "time": "2024-09-25T14:20:29+00:00"
}, },
{ {
"name": "symfony/mime", "name": "symfony/mime",
"version": "v7.1.6", "version": "v7.2.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/mime.git", "url": "https://github.com/symfony/mime.git",
"reference": "caa1e521edb2650b8470918dfe51708c237f0598" "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/caa1e521edb2650b8470918dfe51708c237f0598", "url": "https://api.github.com/repos/symfony/mime/zipball/7f9617fcf15cb61be30f8b252695ed5e2bfac283",
"reference": "caa1e521edb2650b8470918dfe51708c237f0598", "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -802,7 +802,7 @@
"mime-type" "mime-type"
], ],
"support": { "support": {
"source": "https://github.com/symfony/mime/tree/v7.1.6" "source": "https://github.com/symfony/mime/tree/v7.2.1"
}, },
"funding": [ "funding": [
{ {
@ -818,20 +818,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-10-25T15:11:02+00:00" "time": "2024-12-07T08:50:44+00:00"
}, },
{ {
"name": "symfony/options-resolver", "name": "symfony/options-resolver",
"version": "v7.1.6", "version": "v7.2.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/options-resolver.git", "url": "https://github.com/symfony/options-resolver.git",
"reference": "85e95eeede2d41cd146146e98c9c81d9214cae85" "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/85e95eeede2d41cd146146e98c9c81d9214cae85", "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50",
"reference": "85e95eeede2d41cd146146e98c9c81d9214cae85", "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -869,7 +869,7 @@
"options" "options"
], ],
"support": { "support": {
"source": "https://github.com/symfony/options-resolver/tree/v7.1.6" "source": "https://github.com/symfony/options-resolver/tree/v7.2.0"
}, },
"funding": [ "funding": [
{ {
@ -885,7 +885,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-25T14:20:29+00:00" "time": "2024-11-20T11:17:29+00:00"
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
@ -913,8 +913,8 @@
"type": "library", "type": "library",
"extra": { "extra": {
"thanks": { "thanks": {
"name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "name": "symfony/polyfill"
} }
}, },
"autoload": { "autoload": {
@ -990,8 +990,8 @@
"type": "library", "type": "library",
"extra": { "extra": {
"thanks": { "thanks": {
"name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "name": "symfony/polyfill"
} }
}, },
"autoload": { "autoload": {
@ -1072,8 +1072,8 @@
"type": "library", "type": "library",
"extra": { "extra": {
"thanks": { "thanks": {
"name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "name": "symfony/polyfill"
} }
}, },
"autoload": { "autoload": {
@ -1156,8 +1156,8 @@
"type": "library", "type": "library",
"extra": { "extra": {
"thanks": { "thanks": {
"name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "name": "symfony/polyfill"
} }
}, },
"autoload": { "autoload": {
@ -1230,8 +1230,8 @@
"type": "library", "type": "library",
"extra": { "extra": {
"thanks": { "thanks": {
"name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill",
"url": "https://github.com/symfony/polyfill" "name": "symfony/polyfill"
} }
}, },
"autoload": { "autoload": {
@ -1288,16 +1288,16 @@
}, },
{ {
"name": "twig/html-extra", "name": "twig/html-extra",
"version": "v3.15.0", "version": "v3.18.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/twigphp/html-extra.git", "url": "https://github.com/twigphp/html-extra.git",
"reference": "2086023d3ffc4bae2b1115f715d17f97fd013665" "reference": "c63b28e192c1b7c15bb60f81d2e48b140846239a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/twigphp/html-extra/zipball/2086023d3ffc4bae2b1115f715d17f97fd013665", "url": "https://api.github.com/repos/twigphp/html-extra/zipball/c63b28e192c1b7c15bb60f81d2e48b140846239a",
"reference": "2086023d3ffc4bae2b1115f715d17f97fd013665", "reference": "c63b28e192c1b7c15bb60f81d2e48b140846239a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1340,7 +1340,7 @@
"twig" "twig"
], ],
"support": { "support": {
"source": "https://github.com/twigphp/html-extra/tree/v3.15.0" "source": "https://github.com/twigphp/html-extra/tree/v3.18.0"
}, },
"funding": [ "funding": [
{ {
@ -1352,20 +1352,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-30T06:41:48+00:00" "time": "2024-12-29T10:29:59+00:00"
}, },
{ {
"name": "twig/twig", "name": "twig/twig",
"version": "v3.15.0", "version": "v3.18.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/twigphp/Twig.git", "url": "https://github.com/twigphp/Twig.git",
"reference": "2d5b3964cc21d0188633d7ddce732dc8e874db02" "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/2d5b3964cc21d0188633d7ddce732dc8e874db02", "url": "https://api.github.com/repos/twigphp/Twig/zipball/acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50",
"reference": "2d5b3964cc21d0188633d7ddce732dc8e874db02", "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1376,6 +1376,7 @@
"symfony/polyfill-php81": "^1.29" "symfony/polyfill-php81": "^1.29"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^2.0",
"psr/container": "^1.0|^2.0", "psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
}, },
@ -1419,7 +1420,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/twigphp/Twig/issues", "issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.15.0" "source": "https://github.com/twigphp/Twig/tree/v3.18.0"
}, },
"funding": [ "funding": [
{ {
@ -1431,26 +1432,26 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-11-17T15:59:19+00:00" "time": "2024-12-29T10:51:50+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "1.12.11", "version": "2.1.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733" "reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7",
"reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", "reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.2|^8.0" "php": "^7.4|^8.0"
}, },
"conflict": { "conflict": {
"phpstan/phpstan-shim": "*" "phpstan/phpstan-shim": "*"
@ -1491,15 +1492,15 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-11-17T14:08:01+00:00" "time": "2025-01-05T16:43:48+00:00"
} }
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": [], "stability-flags": {},
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": [], "platform": {},
"platform-dev": [], "platform-dev": {},
"plugin-api-version": "2.6.0" "plugin-api-version": "2.6.0"
} }

View file

@ -1,6 +1,7 @@
<?php <?php
namespace Seria; namespace Seria;
use RuntimeException;
use Flashii\{FlashiiClient,FlashiiUrls}; use Flashii\{FlashiiClient,FlashiiUrls};
use Flashii\Credentials\MisuzuCredentials; use Flashii\Credentials\MisuzuCredentials;
use Seria\Users\UserInfo; use Seria\Users\UserInfo;
@ -9,19 +10,19 @@ require_once __DIR__ . '/../seria.php';
$authToken = (string)filter_input(INPUT_COOKIE, 'msz_auth'); $authToken = (string)filter_input(INPUT_COOKIE, 'msz_auth');
$flashii = new FlashiiClient('Seria', new MisuzuCredentials($authToken), new FlashiiUrls( $flashii = new FlashiiClient('Seria', new MisuzuCredentials($authToken), new FlashiiUrls(
$cfg->getString('apii:api', FlashiiUrls::PROD_API_URL), $cfg->getString('apii:api', FlashiiUrls::PROD_API_URL)
$cfg->getString('apii:id', FlashiiUrls::PROD_ID_URL)
)); ));
$authInfo = $flashii->v1()->me();
if($authInfo !== null) { try {
$users = $seria->getUsersContext()->getUsers(); $authInfo = $flashii->v1()->me();
$users->syncApiUser($authInfo); $seria->usersCtx->users->syncApiUser($authInfo);
$sUserInfo = $users->getUser($authInfo->getId(), 'id'); $sUserInfo = $seria->usersCtx->users->getUser($authInfo->getId(), 'id');
$seria->getAuthInfo()->setInfo($sUserInfo); $seria->authInfo->userInfo = $sUserInfo;
} else $sUserInfo = null; } catch(RuntimeException $ex) {
$authInfo = $sUserInfo = null;
}
$seria->startCSRFP( $seria->initCsrfToken(
$cfg->getString('csrfp:secret', 'mewow'), $cfg->getString('csrfp:secret', 'mewow'),
$authInfo === null ? (string)filter_input(INPUT_SERVER, 'REMOTE_ADDR') : $authToken $authInfo === null ? (string)filter_input(INPUT_SERVER, 'REMOTE_ADDR') : $authToken
); );

View file

@ -5,39 +5,21 @@ use Index\Colour\Colour;
use Seria\Users\UserInfo; use Seria\Users\UserInfo;
class AuthInfo { class AuthInfo {
private ?UserInfo $userInfo; public ?UserInfo $userInfo = null;
public function __construct() { public bool $loggedIn {
$this->setInfo(); get => $this->userInfo !== null;
} }
public function setInfo( public ?string $userId {
?UserInfo $userInfo = null get => $this->userInfo?->id;
): void {
$this->userInfo = $userInfo;
} }
public function removeInfo(): void { public ?string $userName {
$this->setInfo(); get => $this->userInfo?->name;
} }
public function isLoggedIn(): bool { public ?Colour $userColour {
return $this->userInfo !== null; get => $this->userInfo?->colour;
}
public function getUserId(): ?string {
return $this->userInfo?->getId();
}
public function getUserName(): ?string {
return $this->userInfo?->getName();
}
public function getUserColour(): ?Colour {
return $this->userInfo?->getColour();
}
public function getUserInfo(): ?UserInfo {
return $this->userInfo;
} }
} }

View file

@ -6,16 +6,16 @@ use Index\Colour\Colour;
use Index\Colour\ColourRgb; use Index\Colour\ColourRgb;
final class Colours { final class Colours {
public const RATIO_GOOD = 0x008000; public const int RATIO_GOOD = 0x008000;
public const RATIO_WARN = 0xFFAA00; public const int RATIO_WARN = 0xFFAA00;
public const RATIO_BAD = 0xFF0000; public const int RATIO_BAD = 0xFF0000;
public const FILE_UP_TO_1GB = 0xA0F5B8; public const int FILE_UP_TO_1GB = 0xA0F5B8;
public const FILE_UP_TO_5GB = 0xBAE9C7; public const int FILE_UP_TO_5GB = 0xBAE9C7;
public const FILE_UP_TO_10GB = 0xCCDDDD; public const int FILE_UP_TO_10GB = 0xCCDDDD;
public const FILE_UP_TO_20GB = 0xCCA1F4; public const int FILE_UP_TO_20GB = 0xCCA1F4;
public const FILE_UP_TO_50GB = 0xDB5FF1; public const int FILE_UP_TO_50GB = 0xDB5FF1;
public const FILE_OVER_50GB = 0xEC32A4; public const int FILE_OVER_50GB = 0xEC32A4;
private static array $colourCache = []; private static array $colourCache = [];

View file

@ -1,11 +1,12 @@
<?php <?php
namespace Seria; namespace Seria;
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\Http\HttpResponseBuilder;
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon};
use Index\Templating\TplEnvironment; use Index\Templating\TplEnvironment;
class HomeRoutes implements RouteHandler { class HomeRoutes implements RouteHandler {
use RouteHandlerTrait; use RouteHandlerCommon;
public function __construct( public function __construct(
private ?TplEnvironment $templating private ?TplEnvironment $templating
@ -17,7 +18,7 @@ class HomeRoutes implements RouteHandler {
} }
#[HttpGet('/index.php')] #[HttpGet('/index.php')]
public function getIndexPHP($response): void { public function getIndexPHP(HttpResponseBuilder $response): void {
$response->redirect('/', true); $response->redirect('/', true);
} }
} }

View file

@ -12,10 +12,6 @@ class RoutingContext {
$this->router->registerContentHandler(new StringableContentHandler); // this should really be in Index lol but i can't be bothered rn! $this->router->registerContentHandler(new StringableContentHandler); // this should really be in Index lol but i can't be bothered rn!
} }
public function getRouter(): Router {
return $this->router;
}
public function register(RouteHandler $handler): void { public function register(RouteHandler $handler): void {
$this->router->register($handler); $this->router->register($handler);
} }

View file

@ -11,21 +11,20 @@ class RoutingErrorHandler implements HttpErrorHandler {
public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void { public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void {
if($code === 500) { if($code === 500) {
$response->setTypeHTML(); $response->setTypeHTML();
$response->setContent(file_get_contents(SERIA_DIR_TEMPLATES . '/500.html')); $response->content = file_get_contents(SERIA_DIR_TEMPLATES . '/500.html');
return; return;
} }
$templating = $this->context->getTemplating(); if($this->context->templating === null) {
if($templating === null) {
$response->setTypePlain(); $response->setTypePlain();
$response->setContent((string)$code); $response->content = (string)$code;
return; return;
} }
$response->setTypeHTML(); $response->setTypeHTML();
$response->setContent($templating->render('http-error', [ $response->content = $this->context->templating->render('http-error', [
'http_code' => $code, 'http_code' => $code,
'http_text' => $message, 'http_text' => $message,
])); ]);
} }
} }

View file

@ -12,22 +12,19 @@ use Seria\Torrents\TorrentsContext;
use Seria\Users\UsersContext; use Seria\Users\UsersContext;
final class SeriaContext { final class SeriaContext {
private DbConnection $dbConn; public private(set) AuthInfo $authInfo;
private Config $config; public private(set) SiteInfo $siteInfo;
private ?TplEnvironment $templating = null;
private AuthInfo $authInfo; public private(set) CsrfToken $csrf;
private SiteInfo $siteInfo; public private(set) ?TplEnvironment $templating = null;
private CsrfToken $csrfp; public private(set) TorrentsContext $torrentsCtx;
public private(set) UsersContext $usersCtx;
private TorrentsContext $torrentsCtx;
private UsersContext $usersCtx;
public function __construct(DbConnection $dbConn, Config $config) {
$this->dbConn = $dbConn;
$this->config = $config;
public function __construct(
private DbConnection $dbConn,
private Config $config
) {
$this->authInfo = new AuthInfo; $this->authInfo = new AuthInfo;
$this->siteInfo = new SiteInfo($config->scopeTo('site')); $this->siteInfo = new SiteInfo($config->scopeTo('site'));
@ -35,10 +32,6 @@ final class SeriaContext {
$this->usersCtx = new UsersContext($dbConn); $this->usersCtx = new UsersContext($dbConn);
} }
public function getDbConn(): DbConnection {
return $this->dbConn;
}
public function getDbQueryCount(): int { public function getDbQueryCount(): int {
$result = $this->dbConn->query('SHOW SESSION STATUS LIKE "Questions"'); $result = $this->dbConn->query('SHOW SESSION STATUS LIKE "Questions"');
return $result->next() ? $result->getInteger(1) : 0; return $result->next() ? $result->getInteger(1) : 0;
@ -52,39 +45,15 @@ final class SeriaContext {
return new FsDbMigrationRepo(SERIA_DIR_MIGRATIONS); return new FsDbMigrationRepo(SERIA_DIR_MIGRATIONS);
} }
public function getAuthInfo(): AuthInfo { public function initCsrfToken(string $secretKey, string $identity): void {
return $this->authInfo; $this->csrf = new CsrfToken($secretKey, $identity);
}
public function getSiteInfo(): SiteInfo {
return $this->siteInfo;
}
public function getTorrentsContext(): TorrentsContext {
return $this->torrentsCtx;
}
public function getUsersContext(): UsersContext {
return $this->usersCtx;
}
public function getTemplating(): ?TplEnvironment {
return $this->templating;
}
public function getCSRFP(): CsrfToken {
return $this->csrfp;
}
public function startCSRFP(string $secretKey, string $identity): void {
$this->csrfp = new CsrfToken($secretKey, $identity);
} }
public function startTemplating(): void { public function startTemplating(): void {
$globals = [ $globals = [
'auth_info' => $this->authInfo, 'auth_info' => $this->authInfo,
'site_info' => $this->siteInfo, 'site_info' => $this->siteInfo,
'display_timings_info' => SERIA_DEBUG, // + isFlash 'display_timings_info' => SERIA_DEBUG,
]; ];
$this->templating = new TplEnvironment( $this->templating = new TplEnvironment(
@ -101,18 +70,18 @@ final class SeriaContext {
$routing->register(new HomeRoutes($this->templating)); $routing->register(new HomeRoutes($this->templating));
$routing->register(new Users\ProfileRoutes($this->authInfo, $this->torrentsCtx, $this->usersCtx, $this->templating)); $routing->register(new Users\ProfileRoutes($this->authInfo, $this->torrentsCtx, $this->usersCtx, $this->templating));
$routing->register(new Users\SettingsRoutes($this->authInfo, $this->usersCtx, $this->csrfp, $this->templating)); $routing->register(new Users\SettingsRoutes($this->authInfo, $this->usersCtx, $this->csrf, $this->templating));
$routing->register(new Torrents\AnnounceRouting($this->torrentsCtx, $this->usersCtx)); $routing->register(new Torrents\AnnounceRoutes($this->torrentsCtx, $this->usersCtx));
$routing->register(new Torrents\TorrentCreateRouting($this->dbConn, $this->authInfo, $this->torrentsCtx, $this->csrfp, $this->templating)); $routing->register(new Torrents\TorrentCreateRoutes($this->dbConn, $this->authInfo, $this->torrentsCtx, $this->csrf, $this->templating));
$routing->register(new Torrents\TorrentInfoRouting( $routing->register(new Torrents\TorrentInfoRoutes(
$this->config->scopeTo('announce'), $this->config->scopeTo('announce'),
$this->authInfo, $this->authInfo,
$this->torrentsCtx, $this->torrentsCtx,
$this->usersCtx, $this->usersCtx,
$this->csrfp, $this->csrf,
$this->templating $this->templating
)); ));
$routing->register(new Torrents\TorrentListRouting($this->authInfo, $this->torrentsCtx, $this->usersCtx, $this->templating)); $routing->register(new Torrents\TorrentListRoutes($this->authInfo, $this->torrentsCtx, $this->usersCtx, $this->templating));
return $routing; return $routing;
} }

View file

@ -7,32 +7,32 @@ use Seria\Users\UserInfo;
class SiteInfo { class SiteInfo {
public function __construct(private Config $config) {} public function __construct(private Config $config) {}
public function getName(): string { public string $name {
return $this->config->getString('name'); get => $this->config->getString('name');
} }
public function getHost(): string { public string $host {
return $this->config->getString('host'); get => $this->config->getString('host');
} }
public function getMainSiteName(): string { public string $mainSiteName {
return $this->config->getString('parent'); get => $this->config->getString('parent');
} }
public function getLoginUrl(): string { public string $loginUrl {
return $this->config->getString('login'); get => $this->config->getString('login');
} }
public function getProfileUrl(UserInfo|string $userInfo): string { public function getProfileUrl(UserInfo|string $userInfo): string {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->id;
return sprintf($this->config->getString('profile'), $userInfo); return sprintf($this->config->getString('profile'), $userInfo);
} }
public function getAvatarUrl(UserInfo|string $userInfo, int $res = 0): string { public function getAvatarUrl(UserInfo|string $userInfo, int $res = 0): string {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->id;
return sprintf($this->config->getString($res < 1 ? 'avatar' : 'avatar:res'), $userInfo, $res); return sprintf($this->config->getString($res < 1 ? 'avatar' : 'avatar:res'), $userInfo, $res);
} }

View file

@ -11,6 +11,6 @@ class StringableContentHandler implements HttpContentHandler {
/** @param Stringable $content */ /** @param Stringable $content */
public function handle(HttpResponseBuilder $response, mixed $content): void { public function handle(HttpResponseBuilder $response, mixed $content): void {
$response->setContent(new StringHttpContent((string)$content)); $response->content = new StringHttpContent((string)$content);
} }
} }

View file

@ -6,18 +6,14 @@ use Twig\TwigFunction;
use Seria\Torrents\TorrentPeers; use Seria\Torrents\TorrentPeers;
final class TemplatingExtension extends AbstractExtension { final class TemplatingExtension extends AbstractExtension {
private TorrentPeers $peers;
public function __construct( public function __construct(
private SeriaContext $ctx, private SeriaContext $ctx,
private SiteInfo $siteInfo private SiteInfo $siteInfo
) { ) {}
$this->peers = $ctx->getTorrentsContext()->getPeers();
}
public function getFunctions() { public function getFunctions() {
return [ return [
new TwigFunction('csrfp_token', $this->ctx->getCSRFP()->createToken(...)), new TwigFunction('csrfp_token', $this->ctx->csrf->createToken(...)),
new TwigFunction('git_commit_hash', GitInfo::hash(...)), new TwigFunction('git_commit_hash', GitInfo::hash(...)),
new TwigFunction('git_tag', GitInfo::tag(...)), new TwigFunction('git_tag', GitInfo::tag(...)),
new TwigFunction('git_branch', GitInfo::branch(...)), new TwigFunction('git_branch', GitInfo::branch(...)),
@ -26,16 +22,15 @@ final class TemplatingExtension extends AbstractExtension {
new TwigFunction('seria_header_menu', $this->getHeaderMenu(...)), new TwigFunction('seria_header_menu', $this->getHeaderMenu(...)),
new TwigFunction('seria_ratio_colour', Colours::forRatio(...)), new TwigFunction('seria_ratio_colour', Colours::forRatio(...)),
new TwigFunction('seria_filesize_colour', Colours::forFileSize(...)), new TwigFunction('seria_filesize_colour', Colours::forFileSize(...)),
new TwigFunction('seria_count_user_uploading', $this->peers->countUserUploading(...)), new TwigFunction('seria_count_user_uploading', $this->ctx->torrentsCtx->peers->countUserUploading(...)),
new TwigFunction('seria_count_user_downloading', $this->peers->countUserDownloading(...)), new TwigFunction('seria_count_user_downloading', $this->ctx->torrentsCtx->peers->countUserDownloading(...)),
]; ];
} }
public function getHeaderMenu(): array { public function getHeaderMenu(): array {
$menu = []; $menu = [];
$authInfo = $this->ctx->getAuthInfo();
if($authInfo->isLoggedIn()) if($this->ctx->authInfo->loggedIn)
$menu[] = [ $menu[] = [
'text' => 'Settings', 'text' => 'Settings',
'url' => '/settings', 'url' => '/settings',
@ -43,7 +38,7 @@ final class TemplatingExtension extends AbstractExtension {
else else
$menu[] = [ $menu[] = [
'text' => 'Log in', 'text' => 'Log in',
'url' => $this->siteInfo->getLoginUrl(), 'url' => $this->siteInfo->loginUrl,
]; ];
$menu[] = [ $menu[] = [
@ -51,16 +46,14 @@ final class TemplatingExtension extends AbstractExtension {
'url' => '/available', 'url' => '/available',
]; ];
if($authInfo->isLoggedIn()) { if($this->ctx->authInfo->loggedIn) {
$userInfo = $authInfo->getUserInfo(); if($this->ctx->authInfo->userInfo->canCreateTorrents)
if($userInfo->canCreateTorrents())
$menu[] = [ $menu[] = [
'text' => 'Create Torrent', 'text' => 'Create Torrent',
'url' => '/create', 'url' => '/create',
]; ];
if($userInfo->canApproveTorrents()) if($this->ctx->authInfo->userInfo->canApproveTorrents)
$menu[] = [ $menu[] = [
'text' => 'Pending Torrents', 'text' => 'Pending Torrents',
'url' => '/pending', 'url' => '/pending',

View file

@ -1,37 +1,29 @@
<?php <?php
namespace Seria\Torrents; namespace Seria\Torrents;
use Index\Bencode\{BencodeProperty,BencodeSerializable,BencodeSerializableTrait}; use Index\Bencode\{BencodeProperty,BencodeSerializable,BencodeSerializableCommon};
class AnnounceEmpty implements BencodeSerializable { class AnnounceEmpty implements BencodeSerializable {
use BencodeSerializableTrait; use BencodeSerializableCommon;
public function __construct( public function __construct(
private bool $compactPeers private bool $compactPeers
) {} ) {}
#[BencodeProperty('interval')] #[BencodeProperty]
public function getInterval(): int { public private(set) int $interval = 0;
return 0;
}
#[BencodeProperty('min interval')] #[BencodeProperty('min interval')]
public function getMinimumInterval(): int { public private(set) int $minInterval = 0;
return 0;
}
#[BencodeProperty('complete')] #[BencodeProperty]
public function getComplete(): int { public private(set) int $complete = 0;
return 0;
}
#[BencodeProperty('incomplete')] #[BencodeProperty]
public function getIncomplete(): int { public private(set) int $incomplete = 0;
return 0;
}
#[BencodeProperty('peers')] #[BencodeProperty]
public function getIncomplete(): string|array { public string|array $peers {
return $this->compactPeers ? '' : []; get => $this->compactPeers ? '' : [];
} }
} }

View file

@ -1,15 +1,13 @@
<?php <?php
namespace Seria\Torrents; namespace Seria\Torrents;
use Index\Bencode\{BencodeProperty,BencodeSerializable,BencodeSerializableTrait}; use Index\Bencode\{BencodeProperty,BencodeSerializable,BencodeSerializableCommon};
class AnnounceFailure implements BencodeSerializable { class AnnounceFailure implements BencodeSerializable {
use BencodeSerializableTrait; use BencodeSerializableCommon;
public function __construct(private string $reason) {} public function __construct(
#[BencodeProperty('failure reason')]
#[BencodeProperty('failure reason')] public private(set) string $reason
public function getReason(): string { ) {}
return $this->reason;
}
} }

View file

@ -1,78 +1,48 @@
<?php <?php
namespace Seria\Torrents; namespace Seria\Torrents;
use Index\Bencode\{BencodeProperty,BencodeSerializable,BencodeSerializableTrait}; use Index\XArray;
use Index\Bencode\{BencodeProperty,BencodeSerializable,BencodeSerializableCommon};
class AnnounceInfo implements BencodeSerializable { class AnnounceInfo implements BencodeSerializable {
use BencodeSerializableTrait; use BencodeSerializableCommon;
// todo: keep these in a field in ser_torrents so we don't need this slightly filthy hack anymore #[BencodeProperty]
// also useful for ipv6 peer support public private(set) int $complete = 0;
private int $completePeers = 0;
private int $incompletePeers = 0; #[BencodeProperty]
public private(set) int $incomplete = 0;
public function __construct( public function __construct(
private array $peers, private array $peers,
private int $interval, #[BencodeProperty]
private int $minInterval, public private(set) int $interval,
#[BencodeProperty('min interval')]
public private(set) int $minInterval,
private bool $compactPeers, private bool $compactPeers,
private bool $noPeerIds private bool $noPeerIds
) {} ) {
foreach($peers as $peerInfo)
$peerInfo->seed ? ++$this->complete : ++$this->incomplete;
}
private function createCompactPeersList(): string { private function createCompactPeersList(): string {
$peers = ''; return implode('', XArray::select($this->peers, fn($peerInfo) => ($peerInfo->addressRaw . pack('n', $peerInfo->port))));
foreach($this->peers as $peerInfo) {
$peerInfo->isSeed() ? ++$this->completePeers : ++$this->incompletePeers;
$peers .= $peerInfo->getAddressRaw() . pack('n', $peerInfo->getPort());
}
return $peers;
} }
private function createPeersListWithIds(): array { private function createPeersListWithIds(): array {
$peers = []; return XArray::select($this->peers, fn($peerInfo) => [
foreach($this->peers as $peerInfo) { 'peer id' => $peerInfo->id,
$peerInfo->isSeed() ? ++$this->completePeers : ++$this->incompletePeers; 'ip' => $peerInfo->address,
$peers[] = [ 'port' => $peerInfo->port,
'peer id' => $peerInfo->getId(), ]);
'ip' => $peerInfo->getAddress(),
'port' => $peerInfo->getPort(),
];
}
return $peers;
} }
private function createPeersListWithoutIds(): array { private function createPeersListWithoutIds(): array {
$peers = []; return XArray::select($this->peers, fn($peerInfo) => [
foreach($this->peers as $peerInfo) { 'ip' => $peerInfo->address,
$peerInfo->isSeed() ? ++$this->completePeers : ++$this->incompletePeers; 'port' => $peerInfo->port,
$peers[] = [ ]);
'ip' => $peerInfo->getAddress(),
'port' => $peerInfo->getPort(),
];
}
return $peers;
}
#[BencodeProperty('interval')]
public function getInterval(): int {
return $this->interval;
}
#[BencodeProperty('min interval')]
public function getMinimumInterval(): int {
return $this->minInterval;
}
#[BencodeProperty('complete')]
public function getComplete(): int {
return $this->completePeers;
}
#[BencodeProperty('incomplete')]
public function getIncomplete(): int {
return $this->incompletePeers;
} }
// #[BencodeProperty('downloaded')] // #[BencodeProperty('downloaded')]

View file

@ -3,11 +3,12 @@ namespace Seria\Torrents;
use RuntimeException; use RuntimeException;
use Index\Bencode\Bencode; use Index\Bencode\Bencode;
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\Http\{HttpResponseBuilder,HttpRequest};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon};
use Seria\Users\UsersContext; use Seria\Users\UsersContext;
class AnnounceRouting implements RouteHandler { class AnnounceRoutes implements RouteHandler {
use RouteHandlerTrait; use RouteHandlerCommon;
public const INTERVAL = 1800; public const INTERVAL = 1800;
public const INTERVAL_MIN = 30; public const INTERVAL_MIN = 30;
@ -21,19 +22,14 @@ class AnnounceRouting implements RouteHandler {
#[HttpGet('/announce.php')] #[HttpGet('/announce.php')]
#[HttpGet('/announce/([A-Za-z0-9]+)')] #[HttpGet('/announce/([A-Za-z0-9]+)')]
#[HttpGet('/announce.php/([A-Za-z0-9]+)')] #[HttpGet('/announce.php/([A-Za-z0-9]+)')]
public function getAnnounce($response, $request, string $key = '') { public function getAnnounce(HttpResponseBuilder $response, HttpRequest $request, string $key = '') {
$remoteAddr = $request->getRemoteAddress(); if(strlen(inet_pton($request->remoteAddress)) !== 4)
if(strlen(inet_pton($remoteAddr)) !== 4)
return new AnnounceFailure('Tracker is only supported over IPv4, please reset your DNS cache.'); return new AnnounceFailure('Tracker is only supported over IPv4, please reset your DNS cache.');
$torrents = $this->torrentsCtx->getTorrents();
$peers = $this->torrentsCtx->getPeers();
$users = $this->usersCtx->getUsers();
$userInfo = null; $userInfo = null;
if($key !== '') if($key !== '')
try { try {
$userInfo = $users->getUser($key, 'passkey'); $userInfo = $this->usersCtx->users->getUser($key, 'passkey');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
sleep(3); sleep(3);
return new AnnounceFailure('Authentication failed.'); return new AnnounceFailure('Authentication failed.');
@ -78,7 +74,7 @@ class AnnounceRouting implements RouteHandler {
$wantsPeerAmount = (int)$request->getParam('numwant', FILTER_SANITIZE_NUMBER_INT); $wantsPeerAmount = (int)$request->getParam('numwant', FILTER_SANITIZE_NUMBER_INT);
try { try {
$torrentInfo = $torrents->getTorrent($infoHash, 'hash'); $torrentInfo = $this->torrentsCtx->torrents->getTorrent($infoHash, 'hash');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
return new AnnounceFailure('Info hash not found.'); return new AnnounceFailure('Info hash not found.');
} }
@ -92,12 +88,12 @@ class AnnounceRouting implements RouteHandler {
default => $canDownload, default => $canDownload,
}); });
$peerInfo = $peers->getPeer($torrentInfo, $peerId); $peerInfo = $this->torrentsCtx->peers->getPeer($torrentInfo, $peerId);
if($peerInfo === null) { if($peerInfo === null) {
// could probably skip this is the event is 'stopped' // could probably skip this is the event is 'stopped'
$peerInfo = $peers->createPeer( $peerInfo = $this->torrentsCtx->peers->createPeer(
$torrentInfo, $userInfo, $peerId, $remoteAddr, $port, self::INTERVAL, $torrentInfo, $userInfo, $peerId, $request->remoteAddress, $port, self::INTERVAL,
$userAgent, $key, $bytesUploaded, $bytesDownloaded, $bytesRemaining $userAgent, $key, $bytesUploaded, $bytesDownloaded, $bytesRemaining
); );
} else { } else {
@ -112,26 +108,26 @@ class AnnounceRouting implements RouteHandler {
} }
if($userInfo !== null) if($userInfo !== null)
$users->incrementTransferStats( $this->usersCtx->users->incrementTransferStats(
$userInfo, $userInfo,
$bytesDownloaded - $peerInfo->getBytesDownloaded(), $bytesDownloaded - $peerInfo->bytesDownloaded,
$bytesUploaded - $peerInfo->getBytesUploaded() $bytesUploaded - $peerInfo->bytesUploaded
); );
$peers->updatePeer( $this->torrentsCtx->peers->updatePeer(
$torrentInfo, $peerInfo, $remoteAddr, $port, self::INTERVAL, $torrentInfo, $peerInfo, $request->remoteAddress, $port, self::INTERVAL,
$userAgent, $bytesUploaded, $bytesDownloaded, $bytesRemaining $userAgent, $bytesUploaded, $bytesDownloaded, $bytesRemaining
); );
} }
if($event === 'stopped') { if($event === 'stopped') {
$peers->deletePeer($torrentInfo, $peerInfo); $this->torrentsCtx->peers->deletePeer($torrentInfo, $peerInfo);
return new AnnounceEmpty($compact); return new AnnounceEmpty($compact);
} }
$peers->pruneExpiredPeers(); $this->torrentsCtx->peers->pruneExpiredPeers();
$peerInfos = $peers->getPeers($torrentInfo); $peerInfos = $this->torrentsCtx->peers->getPeers($torrentInfo);
return new AnnounceInfo( return new AnnounceInfo(
$peerInfos, $peerInfos,

View file

@ -9,60 +9,48 @@ use Index\Db\DbConnection;
use Seria\Users\UserInfo; use Seria\Users\UserInfo;
class TorrentBuilder { class TorrentBuilder {
private ?string $userId = null;
private string $name = '';
private int $created;
private int $pieceLength = 0;
private bool $isPrivate = false;
private string $comment = '';
private array $files = [];
private array $pieces = [];
public function __construct() { public function __construct() {
$this->created = time(); $this->createdTime = time();
} }
public function setUser(?UserInfo $userInfo): self { public ?string $userId = null;
return $this->setUserId($userInfo?->getId());
public string $name = '' {
get => $this->name;
set(string $name) {
if(empty($name))
throw new InvalidArgumentException('$name may not be empty.');
$this->name = $name;
}
} }
public function setUserId(?string $userId): self { public int $createdTime {
$this->userId = $userId; get => $this->createdTime;
return $this; set(int $createdTime) {
if($createdTime < 0 || $createdTime > 0x7FFFFFFF)
throw new InvalidArgumentException('$createdTime is not a valid timestamp.');
$this->createdTime = $createdTime;
}
} }
public function setName(string $name): self { public int $pieceLength = 0 {
if(empty($name)) get => $this->pieceLength;
throw new InvalidArgumentException('$name may not be empty.'); set(int $pieceLength) {
$this->name = $name; if($pieceLength < 1)
return $this; throw new InvalidArgumentException('$pieceLength is not a valid piece length.');
$this->pieceLength = $pieceLength;
}
} }
public function setCreatedTime(int $created): self { public bool $private = false;
if($created < 0 || $created > 0x7FFFFFFF) public string $comment = '';
throw new InvalidArgumentException('$created is not a valid timestamp.');
$this->created = $created;
return $this;
}
public function setPieceLength(int $pieceLength): self { private array $files = [];
if($pieceLength < 1)
throw new InvalidArgumentException('$pieceLength is not a valid piece length.');
$this->pieceLength = $pieceLength;
return $this;
}
public function setPrivate(bool $private): self { public function addFile(string|array $path, int $length): void {
$this->isPrivate = $private;
return $this;
}
public function setComment(string $comment): self {
$this->comment = $comment;
return $this;
}
public function addFile(string|array $path, int $length): self {
if(is_array($path)) if(is_array($path))
$path = implode('/', $path); $path = implode('/', $path);
@ -74,17 +62,15 @@ class TorrentBuilder {
'length' => $length, 'length' => $length,
'path' => explode('/', $path), 'path' => explode('/', $path),
]; ];
return $this;
} }
public function addPiece(string $hash): self { private array $pieces = [];
public function addPiece(string $hash): void {
if(strlen($hash) !== 20) if(strlen($hash) !== 20)
throw new InvalidArgumentException('$hash is not a valid piece hash.'); throw new InvalidArgumentException('$hash is not a valid piece hash.');
$this->pieces[] = $hash; $this->pieces[] = $hash;
return $this;
} }
public function calculateInfoHash(): string { public function calculateInfoHash(): string {
@ -95,31 +81,27 @@ class TorrentBuilder {
'pieces' => implode($this->pieces), 'pieces' => implode($this->pieces),
]; ];
if(!empty($this->isPrivate)) if(!empty($this->private))
$info['private'] = 1; $info['private'] = 1;
return hash('sha1', Bencode::encode($info), true); return hash('sha1', Bencode::encode($info), true);
} }
public function create(DbConnection $dbConn, TorrentsContext $torrentsCtx): string { public function create(DbConnection $dbConn, TorrentsContext $torrentsCtx): string {
$torrents = $torrentsCtx->getTorrents();
$pieces = $torrentsCtx->getPieces();
$files = $torrentsCtx->getFiles();
$tx = $dbConn->beginTransaction(); $tx = $dbConn->beginTransaction();
try { try {
$infoHash = $this->calculateInfoHash(); $infoHash = $this->calculateInfoHash();
$torrentId = $torrents->createTorrent( $torrentId = $torrentsCtx->torrents->createTorrent(
$this->userId, $infoHash, $this->name, $this->created, $this->userId, $infoHash, $this->name, $this->createdTime,
$this->pieceLength, $this->isPrivate, $this->comment $this->pieceLength, $this->private, $this->comment
); );
foreach($this->files as $file) foreach($this->files as $file)
$files->createFile($torrentId, $file['length'], $file['path']); $torrentsCtx->files->createFile($torrentId, $file['length'], $file['path']);
foreach($this->pieces as $piece) foreach($this->pieces as $piece)
$pieces->createPiece($torrentId, $piece); $torrentsCtx->pieces->createPiece($torrentId, $piece);
$tx->commit(); $tx->commit();
} catch(Exception $ex) { } catch(Exception $ex) {
@ -132,22 +114,22 @@ class TorrentBuilder {
public static function import( public static function import(
TorrentInfo $torrent, TorrentInfo $torrent,
array $pieces, iterable $pieces,
array $files iterable $files
): self { ): self {
$builder = new TorrentBuilder; $builder = new TorrentBuilder;
$builder->setUserId($torrent->getUserId()); $builder->userId = $torrent->userId;
$builder->setName($torrent->getName()); $builder->name = $torrent->name;
$builder->setPieceLength($torrent->getPieceLength()); $builder->pieceLength = $torrent->pieceLength;
$builder->setPrivate($torrent->isPrivate()); $builder->private = $torrent->private;
$builder->setCreatedTime($torrent->getCreatedTime()); $builder->createdTime = $torrent->createdTime;
$builder->setComment($torrent->getComment()); $builder->comment = $torrent->comment;
foreach($pieces as $piece) foreach($pieces as $piece)
$builder->addPiece($piece->getHash()); $builder->addPiece($piece->hash);
foreach($files as $file) foreach($files as $file)
$builder->addFile($file->getPath(), $file->getLength()); $builder->addFile($file->path, $file->length);
return $builder; return $builder;
} }
@ -168,16 +150,15 @@ class TorrentBuilder {
throw new InvalidArgumentException('info.piece length key missing.'); throw new InvalidArgumentException('info.piece length key missing.');
$builder = new TorrentBuilder; $builder = new TorrentBuilder;
$builder->setName($source['info']['name']); $builder->name = $source['info']['name'];
$builder->setPieceLength($source['info']['piece length']); $builder->pieceLength = $source['info']['piece length'];
$builder->setPrivate(!empty($source['info']['private'])); $builder->private = !empty($source['info']['private']);
if(isset($source['creation date']) if(isset($source['creation date']) && is_int($source['creation date']))
&& is_int($source['creation date'])) $builder->createdTime = $source['creation date'];
$builder->setCreatedTime($source['creation date']);
if(!empty($source['comment'])) if(!empty($source['comment']))
$builder->setComment($source['comment']); $builder->comment = $source['comment'];
foreach($source['info']['files'] as $file) { foreach($source['info']['files'] as $file) {
if(empty($file) if(empty($file)

View file

@ -5,13 +5,14 @@ use Exception;
use RuntimeException; use RuntimeException;
use Index\CsrfToken; use Index\CsrfToken;
use Index\Db\DbConnection; use Index\Db\DbConnection;
use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler,RouteHandlerTrait}; use Index\Http\{FormHttpContent,HttpResponseBuilder,HttpRequest};
use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler,RouteHandlerCommon};
use Index\Templating\TplEnvironment; use Index\Templating\TplEnvironment;
use Seria\Auth\AuthInfo; use Seria\Auth\AuthInfo;
use Seria\Users\UsersContext; use Seria\Users\UsersContext;
class TorrentCreateRouting implements RouteHandler { class TorrentCreateRoutes implements RouteHandler {
use RouteHandlerTrait; use RouteHandlerCommon;
public function __construct( public function __construct(
private DbConnection $dbConn, private DbConnection $dbConn,
@ -23,14 +24,14 @@ class TorrentCreateRouting implements RouteHandler {
#[HttpMiddleware('/create')] #[HttpMiddleware('/create')]
public function checkAccess() { public function checkAccess() {
if(!$this->authInfo->isLoggedIn()) if(!$this->authInfo->loggedIn)
return 403; return 403;
if(!$this->authInfo->getUserInfo()->canCreateTorrents()) if(!$this->authInfo->userInfo->canCreateTorrents)
return 403; return 403;
} }
#[HttpGet('/create')] #[HttpGet('/create')]
public function getCreate($response, $request) { public function getCreate(HttpResponseBuilder $response, HttpRequest $request) {
$template = $this->templating->load('create'); $template = $this->templating->load('create');
if($request->hasParam('error')) if($request->hasParam('error'))
@ -46,26 +47,24 @@ class TorrentCreateRouting implements RouteHandler {
} }
#[HttpPost('/create')] #[HttpPost('/create')]
public function postCreate($response, $request) { public function postCreate(HttpResponseBuilder $response, HttpRequest $request) {
if(!$request->isFormContent()) if(!($request->content instanceof FormHttpContent))
return 400; return 400;
$content = $request->getContent(); if(!$request->content->hasUploadedFile('torrent')) {
if(!$content->hasUploadedFile('torrent')) {
$response->redirect('/create?error=file'); $response->redirect('/create?error=file');
return; return;
} }
if(!$this->csrfp->verifyToken((string)$content->getParam('_csrfp'))) { if(!$this->csrfp->verifyToken((string)$request->content->getParam('_csrfp'))) {
$response->redirect('/create?error=verify'); $response->redirect('/create?error=verify');
return; return;
} }
$torrent = $content->getUploadedFile('torrent'); $torrent = $request->content->getUploadedFile('torrent');
$error = $torrent->getErrorCode(); if($torrent->errorCode !== UPLOAD_ERR_OK) {
if($error !== UPLOAD_ERR_OK) { $response->redirect('/create?error=' . match($torrent->errorCode) {
$response->redirect('/create?error=' . match($error) {
UPLOAD_ERR_NO_FILE => 'file', UPLOAD_ERR_NO_FILE => 'file',
UPLOAD_ERR_INI_SIZE => 'size', UPLOAD_ERR_INI_SIZE => 'size',
UPLOAD_ERR_FORM_SIZE => 'size', UPLOAD_ERR_FORM_SIZE => 'size',
@ -74,16 +73,15 @@ class TorrentCreateRouting implements RouteHandler {
return; return;
} }
$path = $torrent->getLocalFileName(); if($torrent->localFileName === null || !is_file($torrent->localFileName)) {
if($path === null || !is_file($path)) {
$response->redirect('/create?error=file'); $response->redirect('/create?error=file');
return; return;
} }
$file = fopen($path, 'rb'); $file = fopen($torrent->localFileName, 'rb');
try { try {
$torrentBuilder = TorrentBuilder::decode($file); $torrentBuilder = TorrentBuilder::decode($file);
$torrentBuilder->setUser($this->authInfo->getUserInfo()); $torrentBuilder->userId = $this->authInfo->userInfo?->id;
$torrentId = $torrentBuilder->create($this->dbConn, $this->torrentsCtx); $torrentId = $torrentBuilder->create($this->dbConn, $this->torrentsCtx);
} catch(Exception $ex) { } catch(Exception $ex) {
$response->redirect('/create?error=format'); $response->redirect('/create?error=format');
@ -97,7 +95,7 @@ class TorrentCreateRouting implements RouteHandler {
} }
#[HttpGet('/create.php')] #[HttpGet('/create.php')]
public function getCreatePHP($response, $request) { public function getCreatePHP(HttpResponseBuilder $response, HttpRequest $request) {
$response->redirect('/create', true); $response->redirect('/create', true);
} }
} }

View file

@ -1,43 +1,30 @@
<?php <?php
namespace Seria\Torrents; namespace Seria\Torrents;
use Index\Bencode\{BencodeProperty,BencodeSerializable,BencodeSerializableTrait}; use Index\Bencode\{BencodeProperty,BencodeSerializable,BencodeSerializableCommon};
use Index\Db\DbResult; use Index\Db\DbResult;
readonly class TorrentFileInfo implements BencodeSerializable { class TorrentFileInfo implements BencodeSerializable {
use BencodeSerializableTrait; use BencodeSerializableCommon;
private string $id; public function __construct(
private string $torrentId; public private(set) string $id,
private int $length; public private(set) string $torrentId,
private string $path; #[BencodeProperty] public private(set) int $length,
public private(set) string $path,
) {}
public function __construct(DbResult $result) { public static function fromResult(DbResult $result): TorrentFileInfo {
$this->id = $result->getString(0); return new TorrentFileInfo(
$this->torrentId = $result->getString(1); id: $result->getString(0),
$this->length = $result->getInteger(2); torrentId: $result->getString(1),
$this->path = $result->getString(3); length: $result->getInteger(2),
} path: $result->getString(3),
);
public function getId(): string {
return $this->id;
}
public function getTorrentId(): string {
return $this->torrentId;
}
#[BencodeProperty('length')]
public function getLength(): int {
return $this->length;
}
public function getPath(): string {
return $this->path;
} }
#[BencodeProperty('path')] #[BencodeProperty('path')]
public function getPathArray(): array { public array $pathArray {
return explode('/', $this->path); get => explode('/', $this->path);
} }
} }

View file

@ -12,18 +12,12 @@ class TorrentFiles {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
public function getFiles(TorrentInfo|string $torrentInfo): array { public function getFiles(TorrentInfo|string $torrentInfo): iterable {
$stmt = $this->cache->get('SELECT file_id, torrent_id, file_length, file_path FROM ser_torrents_files WHERE torrent_id = ? ORDER BY file_id ASC'); $stmt = $this->cache->get('SELECT file_id, torrent_id, file_length, file_path FROM ser_torrents_files WHERE torrent_id = ? ORDER BY file_id ASC');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); return $stmt->getResultIterator(TorrentFileInfo::fromResult(...));
$files = [];
while($result->next())
$files[] = new TorrentFileInfo($result);
return $files;
} }
public function createFile( public function createFile(
@ -32,16 +26,17 @@ class TorrentFiles {
array|string $path array|string $path
): void { ): void {
$stmt = $this->cache->get('INSERT INTO ser_torrents_files (torrent_id, file_length, file_path) VALUES (?, ?, ?)'); $stmt = $this->cache->get('INSERT INTO ser_torrents_files (torrent_id, file_length, file_path) VALUES (?, ?, ?)');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->addParameter(2, $length); $stmt->nextParameter($length);
$stmt->addParameter(3, is_array($path) ? implode('/', $path) : $path); $stmt->nextParameter(is_array($path) ? implode('/', $path) : $path);
$stmt->execute(); $stmt->execute();
} }
public function countTotalSize(TorrentInfo|string $torrentInfo): int { public function countTotalSize(TorrentInfo|string $torrentInfo): int {
$stmt = $this->cache->get('SELECT SUM(file_length) FROM ser_torrents_files WHERE torrent_id = ?'); $stmt = $this->cache->get('SELECT SUM(file_length) FROM ser_torrents_files WHERE torrent_id = ?');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); $result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0; return $result->next() ? $result->getInteger(0) : 0;
} }

View file

@ -3,80 +3,36 @@ namespace Seria\Torrents;
use Index\Db\DbResult; use Index\Db\DbResult;
readonly class TorrentInfo { class TorrentInfo {
private string $id; public function __construct(
private ?string $userId; public private(set) string $id,
private string $infoHash; public private(set) ?string $userId,
private int $active; public private(set) string $infoHash,
private string $name; public private(set) bool $active, // i don't think anything uses this field
private int $created; public private(set) string $name,
private ?int $approved; public private(set) int $createdTime,
private int $pieceLength; public private(set) ?int $approvedTime,
private int $private; public private(set) int $pieceLength,
private string $comment; public private(set) bool $private,
public private(set) string $comment,
) {}
public function __construct(DbResult $result) { public static function fromResult(DbResult $result): TorrentInfo {
$this->id = $result->getString(0); return new TorrentInfo(
$this->userId = $result->isNull(1) ? null : $result->getString(1); id: $result->getString(0),
$this->infoHash = $result->getString(2); userId: $result->getStringOrNull(1),
$this->active = $result->getInteger(3); // i don't think anything uses this field infoHash: $result->getString(2),
$this->name = $result->getString(4); active: $result->getBoolean(3),
$this->created = $result->getInteger(5); name: $result->getString(4),
$this->approved = $result->isNull(6) ? null : $result->getInteger(6); createdTime: $result->getInteger(5),
$this->pieceLength = $result->getInteger(7); approvedTime: $result->getIntegerOrNull(6),
$this->private = $result->getInteger(8); pieceLength: $result->getInteger(7),
$this->comment = $result->getString(9); private: $result->getBoolean(8),
comment: $result->getString(9),
);
} }
public function getId(): string { public bool $approved {
return $this->id; get => $this->approvedTime !== null;
}
public function hasUser(): bool {
return $this->userId !== null;
}
public function getUserId(): ?string {
return $this->userId;
}
public function getHash(): string {
return $this->infoHash;
}
public function isActive(): bool {
return $this->active !== 0;
}
public function getName(): string {
return $this->name;
}
public function getCreatedTime(): int {
return $this->created;
}
public function getApprovedTime(): ?int {
return $this->approved;
}
public function isApproved(): bool {
return $this->approved !== null;
}
public function getPieceLength(): int {
return $this->pieceLength;
}
public function isPrivate(): bool {
return $this->private !== 0;
}
public function hasComment(): bool {
return $this->comment !== '';
}
public function getComment(): string {
return $this->comment;
} }
} }

View file

@ -4,13 +4,14 @@ namespace Seria\Torrents;
use RuntimeException; use RuntimeException;
use Index\CsrfToken; use Index\CsrfToken;
use Index\Config\Config; use Index\Config\Config;
use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler,RouteHandlerTrait}; use Index\Http\{FormHttpContent,HttpResponseBuilder,HttpRequest};
use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler,RouteHandlerCommon};
use Index\Templating\TplEnvironment; use Index\Templating\TplEnvironment;
use Seria\Auth\AuthInfo; use Seria\Auth\AuthInfo;
use Seria\Users\UsersContext; use Seria\Users\UsersContext;
class TorrentInfoRouting implements RouteHandler { class TorrentInfoRoutes implements RouteHandler {
use RouteHandlerTrait; use RouteHandlerCommon;
private ?TorrentInfo $torrentInfo = null; private ?TorrentInfo $torrentInfo = null;
@ -24,17 +25,17 @@ class TorrentInfoRouting implements RouteHandler {
) {} ) {}
#[HttpGet('/download/([0-9]+)')] #[HttpGet('/download/([0-9]+)')]
public function getDownload($response, $request, string $torrentId) { public function getDownload(HttpResponseBuilder $response, HttpRequest $request, string $torrentId) {
try { try {
$torrentInfo = $this->torrentsCtx->getTorrents()->getTorrent($torrentId); $torrentInfo = $this->torrentsCtx->torrents->getTorrent($torrentId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$response->setStatusCode(404); $response->statusCode = 404;
return 'Download not found.'; return 'Download not found.';
} }
$canDownload = $this->torrentsCtx->canDownloadTorrent($torrentInfo, $this->authInfo->getUserInfo()); $canDownload = $this->torrentsCtx->canDownloadTorrent($torrentInfo, $this->authInfo->userInfo);
if($canDownload !== '') { if($canDownload !== '') {
$response->setStatusCode(403); $response->statusCode = 403;
return match($canDownload) { return match($canDownload) {
'inactive' => 'This download is inactive.', 'inactive' => 'This download is inactive.',
@ -45,33 +46,32 @@ class TorrentInfoRouting implements RouteHandler {
} }
$trackerUrl = ''; $trackerUrl = '';
if($this->authInfo->isLoggedIn()) { if($this->authInfo->loggedIn) {
$userInfo = $this->authInfo->getUserInfo(); $passKey = $this->authInfo->userInfo->passKey !== null
$passKey = $userInfo->hasPassKey() ? $this->authInfo->userInfo->passKey
? $userInfo->getPassKey() : $this->usersCtx->users->updatePassKey($this->authInfo->userInfo);
: $this->usersCtx->getUsers()->updatePassKey($userInfo);
$trackerUrl = sprintf($this->config->getString('url:user'), $passKey); $trackerUrl = sprintf($this->config->getString('url:user'), $passKey);
} else } else
$trackerUrl = $this->config->getString('url:anon'); $trackerUrl = $this->config->getString('url:anon');
$response->setContentType('application/x-bittorrent'); $response->setContentType('application/x-bittorrent; charset=us-ascii');
$response->setFileName(htmlspecialchars($torrentInfo->getName()) . '.torrent'); $response->setFileName(htmlspecialchars($torrentInfo->name) . '.torrent');
return $this->torrentsCtx->encodeTorrent($torrentInfo, $trackerUrl); return $this->torrentsCtx->encodeTorrent($torrentInfo, $trackerUrl);
} }
private function getTorrentInfo(string $torrentId): int { private function getTorrentInfo(string $torrentId): int {
if($this->torrentInfo?->getId() === $torrentId) if($this->torrentInfo?->id === $torrentId)
return 0; return 0;
try { try {
$this->torrentInfo = $this->torrentsCtx->getTorrents()->getTorrent($torrentId); $this->torrentInfo = $this->torrentsCtx->torrents->getTorrent($torrentId);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
return 404; return 404;
} }
$canDownload = $this->torrentsCtx->canDownloadTorrent($this->torrentInfo, $this->authInfo->getUserInfo()); $canDownload = $this->torrentsCtx->canDownloadTorrent($this->torrentInfo, $this->authInfo->userInfo);
if($canDownload !== '') if($canDownload !== '')
return 403; return 403;
@ -79,22 +79,20 @@ class TorrentInfoRouting implements RouteHandler {
} }
#[HttpGet('/info/([0-9]+)')] #[HttpGet('/info/([0-9]+)')]
public function getInfo($response, $request, string $torrentId) { public function getInfo(HttpResponseBuilder $response, HttpRequest $request, string $torrentId) {
$error = $this->getTorrentInfo($torrentId); $error = $this->getTorrentInfo($torrentId);
if($error > 0) return $error; if($error > 0)
return $error;
if($this->torrentInfo->hasUser()) { $userInfo = $this->torrentInfo->userId !== null
$users = $this->usersCtx->getUsers(); ? $this->usersCtx->users->getUser($this->torrentInfo->userId, 'id')
$userInfo = $users->getUser($this->torrentInfo->getUserId(), 'id'); : null;
} else $userInfo = null;
$peers = $this->torrentsCtx->getPeers(); $completePeers = $this->torrentsCtx->peers->countCompletePeers($this->torrentInfo);
$completePeers = $peers->countCompletePeers($this->torrentInfo); $incompletePeers = $this->torrentsCtx->peers->countIncompletePeers($this->torrentInfo);
$incompletePeers = $peers->countIncompletePeers($this->torrentInfo);
$files = $this->torrentsCtx->getFiles(); $totalFileSize = $this->torrentsCtx->files->countTotalSize($this->torrentInfo);
$totalFileSize = $files->countTotalSize($this->torrentInfo); $fileList = $this->torrentsCtx->files->getFiles($this->torrentInfo);
$fileList = $files->getFiles($this->torrentInfo);
return $this->templating->render('info', [ return $this->templating->render('info', [
'torrent_info' => $this->torrentInfo, 'torrent_info' => $this->torrentInfo,
@ -102,22 +100,19 @@ class TorrentInfoRouting implements RouteHandler {
'torrent_total_size' => $totalFileSize, 'torrent_total_size' => $totalFileSize,
'torrent_complete_peers' => $completePeers, 'torrent_complete_peers' => $completePeers,
'torrent_incomplete_peers' => $incompletePeers, 'torrent_incomplete_peers' => $incompletePeers,
'torrent_files' => $fileList, 'torrent_files' => iterator_to_array($fileList, false),
]); ]);
} }
#[HttpMiddleware('/info/([0-9]+)/rehash')] #[HttpMiddleware('/info/([0-9]+)/rehash')]
#[HttpMiddleware('/info/([0-9]+)/approve')] #[HttpMiddleware('/info/([0-9]+)/approve')]
#[HttpMiddleware('/info/([0-9]+)/deny')] #[HttpMiddleware('/info/([0-9]+)/deny')]
public function verifyRequest($response, $request, string $torrentId) { public function verifyRequest(HttpResponseBuilder $response, HttpRequest $request, string $torrentId) {
if(!$this->authInfo->isLoggedIn()) if(!$this->authInfo->loggedIn)
return 401; return 401;
if(!($request->content instanceof FormHttpContent))
if(!$request->isFormContent())
return 400; return 400;
if(!$this->csrfp->verifyToken((string)$request->content->getParam('_csrfp')))
$content = $request->getContent();
if(!$this->csrfp->verifyToken((string)$content->getParam('_csrfp')))
return 403; return 403;
$error = $this->getTorrentInfo($torrentId); $error = $this->getTorrentInfo($torrentId);
@ -125,20 +120,20 @@ class TorrentInfoRouting implements RouteHandler {
} }
#[HttpPost('/info/([0-9]+)/rehash')] #[HttpPost('/info/([0-9]+)/rehash')]
public function postRehash($response, $request, string $torrentId) { public function postRehash(HttpResponseBuilder $response, HttpRequest $request, string $torrentId) {
$error = $this->getTorrentInfo($torrentId); $error = $this->getTorrentInfo($torrentId);
if($error > 0) return $error; if($error > 0) return $error;
if(!$this->authInfo->getUserInfo()->canRecalculateInfoHash()) if(!$this->authInfo->userInfo->canRecalculateInfoHash)
return 403; return 403;
$builder = TorrentBuilder::import( $builder = TorrentBuilder::import(
$this->torrentInfo, $this->torrentInfo,
$this->torrentsCtx->getPieces()->getPieces($this->torrentInfo), $this->torrentsCtx->pieces->getPieces($this->torrentInfo),
$this->torrentsCtx->getFiles()->getFiles($this->torrentInfo) $this->torrentsCtx->files->getFiles($this->torrentInfo)
); );
$infoHash = $builder->calculateInfoHash(); $infoHash = $builder->calculateInfoHash();
$this->torrentsCtx->getTorrents()->updateTorrentInfoHash($this->torrentInfo, $infoHash); $this->torrentsCtx->torrents->updateTorrentInfoHash($this->torrentInfo, $infoHash);
return [ return [
'hash' => base64_encode($infoHash), 'hash' => base64_encode($infoHash),
@ -146,33 +141,34 @@ class TorrentInfoRouting implements RouteHandler {
} }
#[HttpPost('/info/([0-9]+)/approve')] #[HttpPost('/info/([0-9]+)/approve')]
public function postApprove($response, $request, string $torrentId) { public function postApprove(HttpResponseBuilder $response, HttpRequest $request, string $torrentId) {
$error = $this->getTorrentInfo($torrentId); $error = $this->getTorrentInfo($torrentId);
if($error > 0) return $error; if($error > 0)
return $error;
if(!$this->authInfo->getUserInfo()->canApproveTorrents()) if(!$this->authInfo->userInfo->canApproveTorrents)
return 403; return 403;
$this->torrentsCtx->getTorrents()->approveTorrent($this->torrentInfo); $this->torrentsCtx->torrents->approveTorrent($this->torrentInfo);
return 204; return 204;
} }
#[HttpPost('/info/([0-9]+)/deny')] #[HttpPost('/info/([0-9]+)/deny')]
public function postDeny($response, $request, string $torrentId) { public function postDeny(HttpResponseBuilder $response, HttpRequest $request, string $torrentId) {
$error = $this->getTorrentInfo($torrentId); $error = $this->getTorrentInfo($torrentId);
if($error > 0) return $error; if($error > 0) return $error;
if(!$this->authInfo->getUserInfo()->canApproveTorrents()) if(!$this->authInfo->userInfo->canApproveTorrents)
return 403; return 403;
$this->torrentsCtx->getTorrents()->deleteTorrent($this->torrentInfo); $this->torrentsCtx->torrents->deleteTorrent($this->torrentInfo);
return 204; return 204;
} }
#[HttpGet('/info.php')] #[HttpGet('/info.php')]
public function getInfoPHP($response, $request) { public function getInfoPHP(HttpResponseBuilder $response, HttpRequest $request) {
$torrentId = (int)$request->getParam('id', FILTER_SANITIZE_NUMBER_INT); $torrentId = (int)$request->getParam('id', FILTER_SANITIZE_NUMBER_INT);
if($torrentId < 1) if($torrentId < 1)
return 404; return 404;
@ -181,7 +177,7 @@ class TorrentInfoRouting implements RouteHandler {
} }
#[HttpGet('/download.php')] #[HttpGet('/download.php')]
public function getDownloadPHP($response, $request) { public function getDownloadPHP(HttpResponseBuilder $response, HttpRequest $request) {
$torrentId = (int)$request->getParam('id', FILTER_SANITIZE_NUMBER_INT); $torrentId = (int)$request->getParam('id', FILTER_SANITIZE_NUMBER_INT);
if($torrentId < 1) if($torrentId < 1)
return 404; return 404;

View file

@ -2,13 +2,15 @@
namespace Seria\Torrents; namespace Seria\Torrents;
use RuntimeException; use RuntimeException;
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\XArray;
use Index\Http\{HttpResponseBuilder,HttpRequest};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon};
use Index\Templating\TplEnvironment; use Index\Templating\TplEnvironment;
use Seria\Auth\AuthInfo; use Seria\Auth\AuthInfo;
use Seria\Users\UsersContext; use Seria\Users\UsersContext;
class TorrentListRouting implements RouteHandler { class TorrentListRoutes implements RouteHandler {
use RouteHandlerTrait; use RouteHandlerCommon;
public function __construct( public function __construct(
private AuthInfo $authInfo, private AuthInfo $authInfo,
@ -18,15 +20,12 @@ class TorrentListRouting implements RouteHandler {
) {} ) {}
#[HttpGet('/available')] #[HttpGet('/available')]
public function getAvailable($response, $request) { public function getAvailable(HttpResponseBuilder $response, HttpRequest $request) {
$users = $this->usersCtx->getUsers();
$peers = $this->torrentsCtx->getPeers();
if($request->hasParam('name')) { if($request->hasParam('name')) {
$userName = (string)$request->getParam('name'); $userName = (string)$request->getParam('name');
try { try {
$userInfo = $users->getUser($userName, 'name'); $userInfo = $this->usersCtx->users->getUser($userName, 'name');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
return 404; return 404;
} }
@ -34,7 +33,7 @@ class TorrentListRouting implements RouteHandler {
$url = '/available?'; $url = '/available?';
if($userInfo !== null) if($userInfo !== null)
$url .= 'name=' . $userInfo->getName() . '&'; $url .= 'name=' . $userInfo->name . '&';
$startAt = -1; $startAt = -1;
$take = 20; $take = 20;
@ -42,21 +41,18 @@ class TorrentListRouting implements RouteHandler {
if($request->hasParam('start')) if($request->hasParam('start'))
$startAt = (int)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT); $startAt = (int)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT);
$torrents = []; $torrents = XArray::select($this->torrentsCtx->torrents->getTorrents(
$torrentInfos = $this->torrentsCtx->getTorrents()->getTorrents( public: $this->authInfo->loggedIn ? null : true,
public: $this->authInfo->isLoggedIn() ? null : true,
approved: true, approved: true,
userInfo: $userInfo, userInfo: $userInfo,
startAt: $startAt, startAt: $startAt,
take: $take take: $take
); ), fn($torrentInfo) => [
foreach($torrentInfos as $torrentInfo) 'info' => $torrentInfo,
$torrents[] = [ 'user' => $torrentInfo->userId !== null ? $this->usersCtx->users->getUser($torrentInfo->userId, 'id') : null,
'info' => $torrentInfo, 'complete_peers' => $this->torrentsCtx->peers->countCompletePeers($torrentInfo),
'user' => $torrentInfo->hasUser() ? $users->getUser($torrentInfo->getUserId(), 'id') : null, 'incomplete_peers' => $this->torrentsCtx->peers->countIncompletePeers($torrentInfo),
'complete_peers' => $peers->countCompletePeers($torrentInfo), ]);
'incomplete_peers' => $peers->countIncompletePeers($torrentInfo),
];
return $this->templating->render('available', [ return $this->templating->render('available', [
'page_url' => $url, 'page_url' => $url,
@ -66,15 +62,12 @@ class TorrentListRouting implements RouteHandler {
} }
#[HttpGet('/pending')] #[HttpGet('/pending')]
public function getPending($response, $request) { public function getPending(HttpResponseBuilder $response, HttpRequest $request) {
if(!$this->authInfo->isLoggedIn()) if(!$this->authInfo->loggedIn)
return 403; return 403;
if(!$this->authInfo->getUserInfo()->canApproveTorrents()) if(!$this->authInfo->userInfo->canApproveTorrents)
return 403; return 403;
$users = $this->usersCtx->getUsers();
$peers = $this->torrentsCtx->getPeers();
$url = '/pending?'; $url = '/pending?';
$startAt = -1; $startAt = -1;
$take = 20; $take = 20;
@ -82,19 +75,16 @@ class TorrentListRouting implements RouteHandler {
if($request->hasParam('start')) if($request->hasParam('start'))
$startAt = (int)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT); $startAt = (int)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT);
$torrents = []; $torrents = XArray::select($this->torrentsCtx->torrents->getTorrents(
$torrentInfos = $this->torrentsCtx->getTorrents()->getTorrents(
approved: false, approved: false,
startAt: $startAt, startAt: $startAt,
take: $take take: $take
); ), fn($torrentInfo) => [
foreach($torrentInfos as $torrentInfo) 'info' => $torrentInfo,
$torrents[] = [ 'user' => $torrentInfo->userId !== null ? $this->usersCtx->users->getUser($torrentInfo->userId, 'id') : null,
'info' => $torrentInfo, 'complete_peers' => $this->torrentsCtx->peers->countCompletePeers($torrentInfo),
'user' => $torrentInfo->hasUser() ? $users->getUser($torrentInfo->getUserId(), 'id') : null, 'incomplete_peers' => $this->torrentsCtx->peers->countIncompletePeers($torrentInfo),
'complete_peers' => $peers->countCompletePeers($torrentInfo), ]);
'incomplete_peers' => $peers->countIncompletePeers($torrentInfo),
];
return $this->templating->render('pending', [ return $this->templating->render('pending', [
'page_url' => $url, 'page_url' => $url,
@ -103,7 +93,7 @@ class TorrentListRouting implements RouteHandler {
} }
#[HttpGet('/available.php')] #[HttpGet('/available.php')]
public function getAvailablePHP($response, $request): void { public function getAvailablePHP(HttpResponseBuilder $response, HttpRequest $request): void {
$query = []; $query = [];
$name = (string)$request->getParam('name'); $name = (string)$request->getParam('name');
@ -121,7 +111,7 @@ class TorrentListRouting implements RouteHandler {
} }
#[HttpGet('/pending.php')] #[HttpGet('/pending.php')]
public function getPendingPHP($response, $request): void { public function getPendingPHP(HttpResponseBuilder $response, HttpRequest $request): void {
$query = []; $query = [];
$start = (int)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT); $start = (int)$request->getParam('start', FILTER_SANITIZE_NUMBER_INT);

View file

@ -4,106 +4,58 @@ namespace Seria\Torrents;
use Index\Db\DbResult; use Index\Db\DbResult;
use Seria\Users\UserInfo; use Seria\Users\UserInfo;
readonly class TorrentPeerInfo { class TorrentPeerInfo {
private string $id; public function __construct(
private string $torrentId; public private(set) string $id,
private ?string $userId; public private(set) string $torrentId,
private string $address; public private(set) ?string $userId,
private int $port; public private(set) string $address,
private int $updated; public private(set) int $port,
private int $expires; public private(set) int $updatedTime,
private string $agent; public private(set) int $expiresTime,
private string $key; public private(set) string $agent,
private int $bytesUploaded; #[\SensitiveParameter] private string $key,
private int $bytesDownloaded; public private(set) int $bytesUploaded,
private int $bytesLeft; public private(set) int $bytesDownloaded,
public private(set) int $bytesLeft,
) {}
public function __construct(DbResult $result) { public static function fromResult(DbResult $result): TorrentPeerInfo {
$this->id = $result->getString(0); return new TorrentPeerInfo(
$this->torrentId = $result->getString(1); id: $result->getString(0),
$this->userId = $result->isNull(2) ? null : $result->getString(2); torrentId: $result->getString(1),
$this->address = $result->getString(3); userId: $result->getStringOrNull(2),
$this->port = $result->getInteger(4); address: $result->getString(3),
$this->updated = $result->getInteger(5); port: $result->getInteger(4),
$this->expires = $result->getInteger(6); updatedTime: $result->getInteger(5),
$this->agent = $result->getString(7); expiresTime: $result->getInteger(6),
$this->key = $result->getString(8); agent: $result->getString(7),
$this->bytesUploaded = $result->getInteger(9); key: $result->getString(8),
$this->bytesDownloaded = $result->getInteger(10); bytesUploaded: $result->getInteger(9),
$this->bytesLeft = $result->getInteger(11); bytesDownloaded: $result->getInteger(10),
bytesLeft: $result->getInteger(11),
);
} }
public function getId(): string { public string $addressRaw {
return $this->id; get => inet_pton($this->address);
} }
public function getTorrentId(): string { public bool $seed {
return $this->torrentId; get => $this->bytesLeft <= 0;
} }
public function getUserId(): ?string { public bool $leech {
return $this->userId; get => $this->bytesLeft > 0;
}
public function hasUserId(): bool {
return $this->userId !== null;
} }
public function verifyUser(?UserInfo $userInfo): bool { public function verifyUser(?UserInfo $userInfo): bool {
return $this->userId === null return $this->userId === null
? $userInfo === null ? $userInfo === null
: $this->userId === $userInfo->getId(); : $this->userId === $userInfo->id;
}
public function getAddress(): string {
return $this->address;
}
public function getAddressRaw(): string {
return inet_pton($this->address);
}
public function getPort(): int {
return $this->port;
}
public function getUpdatedTime(): int {
return $this->updated;
}
public function getExpiresTime(): int {
return $this->expires;
}
public function getAgent(): string {
return $this->agent;
}
public function getKey(): string {
return $this->key;
}
public function getBytesUploaded(): int {
return $this->bytesUploaded;
}
public function getBytesDownloaded(): int {
return $this->bytesDownloaded;
}
public function getBytesRemaining(): int {
return $this->bytesLeft;
}
public function isSeed(): bool {
return $this->bytesLeft <= 0;
}
public function isLeech(): bool {
return $this->bytesLeft > 0;
} }
public function verifyKey(string $key): bool { public function verifyKey(string $key): bool {
return hash_equals($this->getKey(), $key); return hash_equals($this->key, $key);
} }
} }

View file

@ -17,31 +17,25 @@ class TorrentPeers {
$this->dbConn->execute('DELETE FROM ser_torrents_peers WHERE peer_expires < NOW()'); $this->dbConn->execute('DELETE FROM ser_torrents_peers WHERE peer_expires < NOW()');
} }
public function getPeers(TorrentInfo|string $torrentInfo): array { public function getPeers(TorrentInfo|string $torrentInfo): iterable {
$stmt = $this->cache->get('SELECT peer_id, torrent_id, user_id, INET6_NTOA(peer_address), peer_port, UNIX_TIMESTAMP(peer_updated), UNIX_TIMESTAMP(peer_expires), peer_agent, peer_key, peer_uploaded, peer_downloaded, peer_left FROM ser_torrents_peers WHERE torrent_id = ?'); $stmt = $this->cache->get('SELECT peer_id, torrent_id, user_id, INET6_NTOA(peer_address), peer_port, UNIX_TIMESTAMP(peer_updated), UNIX_TIMESTAMP(peer_expires), peer_agent, peer_key, peer_uploaded, peer_downloaded, peer_left FROM ser_torrents_peers WHERE torrent_id = ?');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute(); $stmt->execute();
$peers = []; return $stmt->getResultIterator(TorrentPeerInfo::fromResult(...));
$result = $stmt->getResult();
while($result->next())
$peers[] = new TorrentPeerInfo($result);
return $peers;
} }
public function getPeer(TorrentInfo|string $torrentInfo, string $peerId): ?TorrentPeerInfo { public function getPeer(TorrentInfo|string $torrentInfo, string $peerId): ?TorrentPeerInfo {
$stmt = $this->cache->get('SELECT peer_id, torrent_id, user_id, INET6_NTOA(peer_address), peer_port, UNIX_TIMESTAMP(peer_updated), UNIX_TIMESTAMP(peer_expires), peer_agent, peer_key, peer_uploaded, peer_downloaded, peer_left FROM ser_torrents_peers WHERE torrent_id = ? AND peer_id = ?'); $stmt = $this->cache->get('SELECT peer_id, torrent_id, user_id, INET6_NTOA(peer_address), peer_port, UNIX_TIMESTAMP(peer_updated), UNIX_TIMESTAMP(peer_expires), peer_agent, peer_key, peer_uploaded, peer_downloaded, peer_left FROM ser_torrents_peers WHERE torrent_id = ? AND peer_id = ?');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->addParameter(2, $peerId); $stmt->nextParameter($peerId);
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); $result = $stmt->getResult();
if(!$result->next()) if(!$result->next())
return null; return null;
return new TorrentPeerInfo($result); return TorrentPeerInfo::fromResult($result);
} }
public function createPeer( public function createPeer(
@ -58,20 +52,24 @@ class TorrentPeers {
int $bytesRemaining int $bytesRemaining
): TorrentPeerInfo { ): TorrentPeerInfo {
$stmt = $this->cache->get('INSERT INTO ser_torrents_peers (torrent_id, user_id, peer_id, peer_address, peer_port, peer_updated, peer_expires, peer_agent, peer_key, peer_uploaded, peer_downloaded, peer_left) VALUES (?, ?, ?, INET6_ATON(?), ?, NOW(), NOW() + INTERVAL ? SECOND, ?, ?, ?, ?, ?)'); $stmt = $this->cache->get('INSERT INTO ser_torrents_peers (torrent_id, user_id, peer_id, peer_address, peer_port, peer_updated, peer_expires, peer_agent, peer_key, peer_uploaded, peer_downloaded, peer_left) VALUES (?, ?, ?, INET6_ATON(?), ?, NOW(), NOW() + INTERVAL ? SECOND, ?, ?, ?, ?, ?)');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->addParameter(2, $userInfo === null ? null : ($userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo)); $stmt->nextParameter($userInfo === null ? null : ($userInfo instanceof UserInfo ? $userInfo->id : $userInfo));
$stmt->addParameter(3, $peerId); $stmt->nextParameter($peerId);
$stmt->addParameter(4, $remoteAddr); $stmt->nextParameter($remoteAddr);
$stmt->addParameter(5, $remotePort); $stmt->nextParameter($remotePort);
$stmt->addParameter(6, $interval); $stmt->nextParameter($interval);
$stmt->addParameter(7, $peerAgent); $stmt->nextParameter($peerAgent);
$stmt->addParameter(8, $peerKey); $stmt->nextParameter($peerKey);
$stmt->addParameter(9, $bytesUploaded); $stmt->nextParameter($bytesUploaded);
$stmt->addParameter(10, $bytesDownloaded); $stmt->nextParameter($bytesDownloaded);
$stmt->addParameter(11, $bytesRemaining); $stmt->nextParameter($bytesRemaining);
$stmt->execute(); $stmt->execute();
return $this->getPeer($torrentInfo, $peerId) ?? throw new RuntimeException('Failed to record peer information.'); $peerInfo = $this->getPeer($torrentInfo, $peerId);
if($peerInfo === null)
throw new RuntimeException('Failed to record peer information.');
return $peerInfo;
} }
public function updatePeer( public function updatePeer(
@ -86,28 +84,28 @@ class TorrentPeers {
int $bytesRemaining int $bytesRemaining
): void { ): void {
$stmt = $this->cache->get('UPDATE ser_torrents_peers SET peer_address = INET6_ATON(?), peer_port = ?, peer_updated = NOW(), peer_expires = NOW() + INTERVAL ? SECOND, peer_agent = ?, peer_uploaded = ?, peer_downloaded = ?, peer_left = ? WHERE torrent_id = ? AND peer_id = ?'); $stmt = $this->cache->get('UPDATE ser_torrents_peers SET peer_address = INET6_ATON(?), peer_port = ?, peer_updated = NOW(), peer_expires = NOW() + INTERVAL ? SECOND, peer_agent = ?, peer_uploaded = ?, peer_downloaded = ?, peer_left = ? WHERE torrent_id = ? AND peer_id = ?');
$stmt->addParameter(1, $remoteAddr); $stmt->nextParameter($remoteAddr);
$stmt->addParameter(2, $remotePort); $stmt->nextParameter($remotePort);
$stmt->addParameter(3, $interval); $stmt->nextParameter($interval);
$stmt->addParameter(4, $peerAgent); $stmt->nextParameter($peerAgent);
$stmt->addParameter(5, $bytesUploaded); $stmt->nextParameter($bytesUploaded);
$stmt->addParameter(6, $bytesDownloaded); $stmt->nextParameter($bytesDownloaded);
$stmt->addParameter(7, $bytesRemaining); $stmt->nextParameter($bytesRemaining);
$stmt->addParameter(8, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->addParameter(9, $peerInfo instanceof TorrentPeerInfo ? $peerInfo->getId() : $peerInfo); $stmt->nextParameter($peerInfo instanceof TorrentPeerInfo ? $peerInfo->id : $peerInfo);
$stmt->execute(); $stmt->execute();
} }
public function deletePeer(TorrentInfo|string $torrentInfo, TorrentPeerInfo|string $peerInfo): void { public function deletePeer(TorrentInfo|string $torrentInfo, TorrentPeerInfo|string $peerInfo): void {
$stmt = $this->cache->get('DELETE FROM ser_torrents_peers WHERE torrent_id = ? AND peer_id = ?'); $stmt = $this->cache->get('DELETE FROM ser_torrents_peers WHERE torrent_id = ? AND peer_id = ?');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->addParameter(2, $peerInfo instanceof TorrentPeerInfo ? $peerInfo->getId() : $peerInfo); $stmt->nextParameter($peerInfo instanceof TorrentPeerInfo ? $peerInfo->id : $peerInfo);
$stmt->execute(); $stmt->execute();
} }
public function countIncompletePeers(TorrentInfo|string $torrentInfo): int { public function countIncompletePeers(TorrentInfo|string $torrentInfo): int {
$stmt = $this->cache->get('SELECT COUNT(*) FROM ser_torrents_peers WHERE torrent_id = ? AND peer_left > 0'); $stmt = $this->cache->get('SELECT COUNT(*) FROM ser_torrents_peers WHERE torrent_id = ? AND peer_left > 0');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); $result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0; return $result->next() ? $result->getInteger(0) : 0;
@ -115,7 +113,7 @@ class TorrentPeers {
public function countCompletePeers(TorrentInfo|string $torrentInfo): int { public function countCompletePeers(TorrentInfo|string $torrentInfo): int {
$stmt = $this->cache->get('SELECT COUNT(*) FROM ser_torrents_peers WHERE torrent_id = ? AND peer_left <= 0'); $stmt = $this->cache->get('SELECT COUNT(*) FROM ser_torrents_peers WHERE torrent_id = ? AND peer_left <= 0');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); $result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0; return $result->next() ? $result->getInteger(0) : 0;
@ -123,7 +121,7 @@ class TorrentPeers {
public function countUserDownloading(UserInfo|string $userInfo): int { public function countUserDownloading(UserInfo|string $userInfo): int {
$stmt = $this->cache->get('SELECT COUNT(*) FROM ser_torrents_peers WHERE user_id = ? AND peer_left > 0'); $stmt = $this->cache->get('SELECT COUNT(*) FROM ser_torrents_peers WHERE user_id = ? AND peer_left > 0');
$stmt->addParameter(1, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo); $stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); $result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0; return $result->next() ? $result->getInteger(0) : 0;
@ -131,7 +129,7 @@ class TorrentPeers {
public function countUserUploading(UserInfo|string $userInfo): int { public function countUserUploading(UserInfo|string $userInfo): int {
$stmt = $this->cache->get('SELECT COUNT(*) FROM ser_torrents_peers WHERE user_id = ? AND peer_left <= 0'); $stmt = $this->cache->get('SELECT COUNT(*) FROM ser_torrents_peers WHERE user_id = ? AND peer_left <= 0');
$stmt->addParameter(1, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo); $stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); $result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0; return $result->next() ? $result->getInteger(0) : 0;

View file

@ -3,26 +3,18 @@ namespace Seria\Torrents;
use Index\Db\DbResult; use Index\Db\DbResult;
readonly class TorrentPieceInfo { class TorrentPieceInfo {
private string $id; public function __construct(
private string $torrentId; public private(set) string $id,
private string $hash; public private(set) string $torrentId,
public private(set) string $hash,
) {}
public function __construct(DbResult $result) { public static function fromResult(DbResult $result): TorrentPieceInfo {
$this->id = $result->getString(0); return new TorrentPieceInfo(
$this->torrentId = $result->getString(1); id: $result->getString(0),
$this->hash = $result->getString(2); torrentId: $result->getString(1),
} hash: $result->getString(2),
);
public function getId(): string {
return $this->id;
}
public function getTorrentId(): string {
return $this->torrentId;
}
public function getHash(): string {
return $this->hash;
} }
} }

View file

@ -12,18 +12,11 @@ class TorrentPieces {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
public function getPieces(TorrentInfo|string $torrentInfo): array { public function getPieces(TorrentInfo|string $torrentInfo): iterable {
$stmt = $this->cache->get('SELECT piece_id, torrent_id, piece_hash FROM ser_torrents_pieces WHERE torrent_id = ? ORDER BY piece_id ASC'); $stmt = $this->cache->get('SELECT piece_id, torrent_id, piece_hash FROM ser_torrents_pieces WHERE torrent_id = ? ORDER BY piece_id ASC');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute(); $stmt->execute();
return $stmt->getResultIterator(TorrentPieceInfo::fromResult(...));
$result = $stmt->getResult();
$pieces = [];
while($result->next())
$pieces[] = new TorrentPieceInfo($result);
return $pieces;
} }
public function createPiece( public function createPiece(
@ -31,8 +24,8 @@ class TorrentPieces {
string $hash string $hash
): void { ): void {
$stmt = $this->cache->get('INSERT INTO ser_torrents_pieces (torrent_id, piece_hash) VALUES (?, ?)'); $stmt = $this->cache->get('INSERT INTO ser_torrents_pieces (torrent_id, piece_hash) VALUES (?, ?)');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->addParameter(2, $hash); $stmt->nextParameter($hash);
$stmt->execute(); $stmt->execute();
} }
} }

View file

@ -9,8 +9,9 @@ use Seria\Users\UserInfo;
class Torrents { class Torrents {
private DbStatementCache $cache; private DbStatementCache $cache;
public function __construct(private DbConnection $dbConn) { public function __construct(
$this->dbConn = $dbConn; private DbConnection $dbConn
) {
$this->cache = new DbStatementCache($dbConn); $this->cache = new DbStatementCache($dbConn);
} }
@ -20,7 +21,7 @@ class Torrents {
UserInfo|string|null $userInfo = null, UserInfo|string|null $userInfo = null,
int $startAt = -1, int $startAt = -1,
int $take = -1 int $take = -1
): array { ): iterable {
$hasPublic = $public !== null; $hasPublic = $public !== null;
$hasApproved = $approved !== null; $hasApproved = $approved !== null;
$hasUserInfo = $userInfo !== null; $hasUserInfo = $userInfo !== null;
@ -43,23 +44,16 @@ class Torrents {
if($hasTake) if($hasTake)
$query .= ' LIMIT ?'; $query .= ' LIMIT ?';
$args = 0;
$stmt = $this->cache->get($query); $stmt = $this->cache->get($query);
if($hasUserInfo) if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo); $stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
if($hasStartAt) if($hasStartAt)
$stmt->addParameter(++$args, $startAt); $stmt->nextParameter($startAt);
if($hasTake) if($hasTake)
$stmt->addParameter(++$args, $take); $stmt->nextParameter($take);
$stmt->execute(); $stmt->execute();
$torrents = []; return $stmt->getResultIterator(TorrentInfo::fromResult(...));
$result = $stmt->getResult();
while($result->next())
$torrents[] = new TorrentInfo($result);
return $torrents;
} }
public const GET_TORRENT_ID = 0x01; public const GET_TORRENT_ID = 0x01;
@ -101,19 +95,18 @@ class Torrents {
if($selectHash) if($selectHash)
$query .= sprintf(' %s torrent_hash = ?', ++$args > 1 ? 'OR' : 'WHERE'); $query .= sprintf(' %s torrent_hash = ?', ++$args > 1 ? 'OR' : 'WHERE');
$args = 0;
$stmt = $this->cache->get($query); $stmt = $this->cache->get($query);
if($selectId) if($selectId)
$stmt->addParameter(++$args, $value); $stmt->nextParameter($value);
if($selectHash) if($selectHash)
$stmt->addParameter(++$args, $value); $stmt->nextParameter($value);
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); $result = $stmt->getResult();
if(!$result->next()) if(!$result->next())
throw new RuntimeException('Download info not found.'); throw new RuntimeException('Download info not found.');
return new TorrentInfo($result); return TorrentInfo::fromResult($result);
} }
public function createTorrent( public function createTorrent(
@ -126,34 +119,34 @@ class Torrents {
string $comment string $comment
): string { ): string {
$stmt = $this->cache->get('INSERT INTO ser_torrents (user_id, torrent_hash, torrent_name, torrent_created, torrent_piece_length, torrent_private, torrent_comment) VALUES (?, ?, ?, FROM_UNIXTIME(?), ?, ?, ?)'); $stmt = $this->cache->get('INSERT INTO ser_torrents (user_id, torrent_hash, torrent_name, torrent_created, torrent_piece_length, torrent_private, torrent_comment) VALUES (?, ?, ?, FROM_UNIXTIME(?), ?, ?, ?)');
$stmt->addParameter(1, $userInfo === null ? null : ($userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo)); $stmt->nextParameter($userInfo === null ? null : ($userInfo instanceof UserInfo ? $userInfo->id : $userInfo));
$stmt->addParameter(2, $infoHash); $stmt->nextParameter($infoHash);
$stmt->addParameter(3, $name); $stmt->nextParameter($name);
$stmt->addParameter(4, $created); $stmt->nextParameter($created);
$stmt->addParameter(5, $pieceLength); $stmt->nextParameter($pieceLength);
$stmt->addParameter(6, $isPrivate ? 1 : 0); $stmt->nextParameter($isPrivate ? 1 : 0);
$stmt->addParameter(7, $comment); $stmt->nextParameter($comment);
$stmt->execute(); $stmt->execute();
return (string)$this->dbConn->getLastInsertId(); return (string)$this->dbConn->lastInsertId;
} }
public function updateTorrentInfoHash(TorrentInfo|string $torrentInfo, string $infoHash): void { public function updateTorrentInfoHash(TorrentInfo|string $torrentInfo, string $infoHash): void {
$stmt = $this->cache->get('UPDATE ser_torrents SET torrent_hash = ? WHERE torrent_id = ?'); $stmt = $this->cache->get('UPDATE ser_torrents SET torrent_hash = ? WHERE torrent_id = ?');
$stmt->addParameter(1, $infoHash); $stmt->nextParameter($infoHash);
$stmt->addParameter(2, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute(); $stmt->execute();
} }
public function approveTorrent(TorrentInfo|string $torrentInfo): void { public function approveTorrent(TorrentInfo|string $torrentInfo): void {
$stmt = $this->cache->get('UPDATE ser_torrents SET torrent_approved = COALESCE(torrent_approved, NOW()) WHERE torrent_id = ?'); $stmt = $this->cache->get('UPDATE ser_torrents SET torrent_approved = COALESCE(torrent_approved, NOW()) WHERE torrent_id = ?');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute(); $stmt->execute();
} }
public function deleteTorrent(TorrentInfo|string $torrentInfo): void { public function deleteTorrent(TorrentInfo|string $torrentInfo): void {
$stmt = $this->cache->get('DELETE FROM ser_torrents WHERE torrent_id = ?'); $stmt = $this->cache->get('DELETE FROM ser_torrents WHERE torrent_id = ?');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo); $stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute(); $stmt->execute();
} }
} }

View file

@ -1,16 +1,17 @@
<?php <?php
namespace Seria\Torrents; namespace Seria\Torrents;
use Index\XArray;
use Index\Bencode\Bencode; use Index\Bencode\Bencode;
use Index\Db\DbConnection; use Index\Db\DbConnection;
use Seria\GitInfo; use Seria\GitInfo;
use Seria\Users\UserInfo; use Seria\Users\UserInfo;
class TorrentsContext { class TorrentsContext {
private Torrents $torrents; public private(set) Torrents $torrents;
private TorrentFiles $files; public private(set) TorrentFiles $files;
private TorrentPeers $peers; public private(set) TorrentPeers $peers;
private TorrentPieces $pieces; public private(set) TorrentPieces $pieces;
public function __construct(DbConnection $dbConn) { public function __construct(DbConnection $dbConn) {
$this->torrents = new Torrents($dbConn); $this->torrents = new Torrents($dbConn);
@ -19,33 +20,17 @@ class TorrentsContext {
$this->pieces = new TorrentPieces($dbConn); $this->pieces = new TorrentPieces($dbConn);
} }
public function getTorrents(): Torrents {
return $this->torrents;
}
public function getFiles(): TorrentFiles {
return $this->files;
}
public function getPeers(): TorrentPeers {
return $this->peers;
}
public function getPieces(): TorrentPieces {
return $this->pieces;
}
public function canDownloadTorrent(TorrentInfo|string $torrentInfo, ?UserInfo $userInfo): string { public function canDownloadTorrent(TorrentInfo|string $torrentInfo, ?UserInfo $userInfo): string {
if(is_string($torrentInfo)) if(is_string($torrentInfo))
$torrentInfo = $this->torrents->getTorrent($torrentInfo, 'id'); $torrentInfo = $this->torrents->getTorrent($torrentInfo, 'id');
if(!$torrentInfo->isActive()) if(!$torrentInfo->active)
return 'inactive'; return 'inactive';
if($torrentInfo->isPrivate() && $userInfo === null) if($torrentInfo->private && $userInfo === null)
return 'private'; return 'private';
if(!$torrentInfo->isApproved() && ($userInfo !== null && !$userInfo->canApproveTorrents() && $torrentInfo->getUserId() !== $userInfo->getId())) if(!$torrentInfo->approved && ($userInfo !== null && !$userInfo->canApproveTorrents && $torrentInfo->userId !== $userInfo->id))
return 'pending'; return 'pending';
return ''; return '';
@ -55,32 +40,27 @@ class TorrentsContext {
if(is_string($torrentInfo)) if(is_string($torrentInfo))
$torrentInfo = $this->torrents->getTorrent($torrentInfo, 'id'); $torrentInfo = $this->torrents->getTorrent($torrentInfo, 'id');
$pieces = '';
$pieceInfos = $this->pieces->getPieces($torrentInfo);
foreach($pieceInfos as $piece)
$pieces .= $piece->getHash();
// VERY IMPORTANT DETAIL: keep ordering identical to how it is in TorrentBuilder to not fuck up info hashes // VERY IMPORTANT DETAIL: keep ordering identical to how it is in TorrentBuilder to not fuck up info hashes
// this should really be combined somehow // this should really be combined somehow
$info = [ $info = [
'files' => $this->files->getFiles($torrentInfo), 'files' => iterator_to_array($this->files->getFiles($torrentInfo), false),
'name' => $torrentInfo->getName(), 'name' => $torrentInfo->name,
'piece length' => $torrentInfo->getPieceLength(), 'piece length' => $torrentInfo->pieceLength,
'pieces' => $pieces, 'pieces' => implode('', XArray::select($this->pieces->getPieces($torrentInfo), fn($pieceInfo) => $pieceInfo->hash)),
]; ];
if($torrentInfo->isPrivate()) if($torrentInfo->private)
$info['private'] = 1; $info['private'] = 1;
$data = [ $data = [
'announce' => $announceUrl, 'announce' => $announceUrl,
'created by' => sprintf('Seria %s', GitInfo::version()), 'created by' => sprintf('Seria %s', GitInfo::version()),
'creation date' => $torrentInfo->getCreatedTime(), 'creation date' => $torrentInfo->createdTime,
'info' => $info, 'info' => $info,
]; ];
if($torrentInfo->hasComment()) if(!empty($torrentInfo->comment))
$data['comment'] = $torrentInfo->getComment(); $data['comment'] = $torrentInfo->comment;
return Bencode::encode($data); return Bencode::encode($data);
} }

View file

@ -2,13 +2,15 @@
namespace Seria\Users; namespace Seria\Users;
use RuntimeException; use RuntimeException;
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait}; use Index\XArray;
use Index\Http\{HttpResponseBuilder,HttpRequest};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon};
use Index\Templating\TplEnvironment; use Index\Templating\TplEnvironment;
use Seria\Auth\AuthInfo; use Seria\Auth\AuthInfo;
use Seria\Torrents\{TorrentsContext,TorrentInfo,TorrentPeerInfo}; use Seria\Torrents\{TorrentsContext,TorrentInfo,TorrentPeerInfo};
class ProfileRoutes implements RouteHandler { class ProfileRoutes implements RouteHandler {
use RouteHandlerTrait; use RouteHandlerCommon;
public function __construct( public function __construct(
private AuthInfo $authInfo, private AuthInfo $authInfo,
@ -18,32 +20,24 @@ class ProfileRoutes implements RouteHandler {
) {} ) {}
#[HttpGet('/profile/([a-zA-Z0-9\-_]+)')] #[HttpGet('/profile/([a-zA-Z0-9\-_]+)')]
public function getProfile($response, $request, string $name) { public function getProfile(HttpResponseBuilder $response, HttpRequest $request, string $name) {
if(!$this->authInfo->isLoggedIn()) if(!$this->authInfo->loggedIn)
return 403; return 403;
$users = $this->usersCtx->getUsers();
$torrents = $this->torrentsCtx->getTorrents();
$peers = $this->torrentsCtx->getPeers();
try { try {
$userInfo = $users->getUser($name, 'name'); $userInfo = $this->usersCtx->users->getUser($name, 'name');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
return 404; return 404;
} }
$submissions = []; $submissions = XArray::select($this->torrentsCtx->torrents->getTorrents(approved: true, userInfo: $userInfo, take: 3), fn($torrentInfo) => [
$torrentInfos = $torrents->getTorrents(approved: true, userInfo: $userInfo, take: 3); 'info' => $torrentInfo,
foreach($torrentInfos as $torrentInfo) { 'complete_peers' => $this->torrentsCtx->peers->countCompletePeers($torrentInfo),
$submissions[] = [ 'incomplete_peers' => $this->torrentsCtx->peers->countIncompletePeers($torrentInfo),
'info' => $torrentInfo, ]);
'complete_peers' => $peers->countCompletePeers($torrentInfo),
'incomplete_peers' => $peers->countIncompletePeers($torrentInfo),
];
}
$uploading = $peers->countUserUploading($userInfo); $uploading = $this->torrentsCtx->peers->countUserUploading($userInfo);
$downloading = $peers->countUserDownloading($userInfo); $downloading = $this->torrentsCtx->peers->countUserDownloading($userInfo);
return $this->templating->render('profile', [ return $this->templating->render('profile', [
'profile_user' => $userInfo, 'profile_user' => $userInfo,
@ -54,14 +48,12 @@ class ProfileRoutes implements RouteHandler {
} }
#[HttpGet('/profile/([a-zA-Z0-9\-_]+)/history')] #[HttpGet('/profile/([a-zA-Z0-9\-_]+)/history')]
public function getHistory($response, $request, string $name) { public function getHistory(HttpResponseBuilder $response, HttpRequest $request, string $name) {
if(!$this->authInfo->isLoggedIn()) if(!$this->authInfo->loggedIn)
return 403; return 403;
$users = $this->usersCtx->getUsers();
try { try {
$userInfo = $users->getUser($name, 'name'); $userInfo = $this->usersCtx->users->getUser($name, 'name');
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
return 404; return 404;
} }
@ -72,15 +64,15 @@ class ProfileRoutes implements RouteHandler {
} }
#[HttpGet('/profile.php')] #[HttpGet('/profile.php')]
public function getProfilePHP($response, $request): void { public function getProfilePHP(HttpResponseBuilder $response, HttpRequest $request): void {
$response->redirect(sprintf('/profile/%s', (string)$request->getParam('name')), true); $response->redirect(sprintf('/profile/%s', (string)$request->getParam('name')), true);
} }
#[HttpGet('/history.php')] #[HttpGet('/history.php')]
public function getHistoryPHP($response, $request) { public function getHistoryPHP(HttpResponseBuilder $response, HttpRequest $request) {
$userName = (string)$request->getParam('name'); $userName = (string)$request->getParam('name');
if($userName === '' && $this->authInfo->isLoggedIn()) if($userName === '' && $this->authInfo->loggedIn)
$userName = $this->authInfo->getUserName(); $userName = $this->authInfo->userName;
if($userName === '') if($userName === '')
return 404; return 404;

View file

@ -2,13 +2,14 @@
namespace Seria\Users; namespace Seria\Users;
use Index\CsrfToken; use Index\CsrfToken;
use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler,RouteHandlerTrait}; use Index\Http\{FormHttpContent,HttpResponseBuilder,HttpRequest};
use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler,RouteHandlerCommon};
use Index\Templating\TplEnvironment; use Index\Templating\TplEnvironment;
use Seria\Auth\AuthInfo; use Seria\Auth\AuthInfo;
use Seria\Users\UsersContext; use Seria\Users\UsersContext;
class SettingsRoutes implements RouteHandler { class SettingsRoutes implements RouteHandler {
use RouteHandlerTrait; use RouteHandlerCommon;
public function __construct( public function __construct(
private AuthInfo $authInfo, private AuthInfo $authInfo,
@ -18,33 +19,31 @@ class SettingsRoutes implements RouteHandler {
) {} ) {}
#[HttpMiddleware('/settings')] #[HttpMiddleware('/settings')]
public function checkLogin($response, $request) { public function checkLogin(HttpResponseBuilder $response, HttpRequest $request) {
if(!$this->authInfo->isLoggedIn()) if(!$this->authInfo->loggedIn)
return 403; return 403;
if($request->getMethod() === 'POST') { if($request->method === 'POST') {
if(!$request->isFormContent()) if(!($request->content instanceof FormHttpContent))
return 400; return 400;
if(!$this->csrfp->verifyToken((string)$request->content->getParam('_csrfp')))
$content = $request->getContent();
if(!$this->csrfp->verifyToken((string)$content->getParam('_csrfp')))
return 403; return 403;
} }
} }
#[HttpGet('/settings')] #[HttpGet('/settings')]
public function getIndex($response) { public function getIndex(HttpResponseBuilder $response) {
return $this->templating->render('settings'); return $this->templating->render('settings');
} }
#[HttpPost('/settings/passkey')] #[HttpPost('/settings/passkey')]
public function postPasskey($response) { public function postPasskey(HttpResponseBuilder $response) {
$this->usersCtx->getUsers()->updatePassKey($this->authInfo->getUserInfo()); $this->usersCtx->users->updatePassKey($this->authInfo->userInfo);
$response->redirect('/settings'); $response->redirect('/settings');
} }
#[HttpGet('/settings.php')] #[HttpGet('/settings.php')]
public function getSettingsPHP($response): void { public function getSettingsPHP(HttpResponseBuilder $response): void {
$response->redirect('/settings', true); $response->redirect('/settings', true);
} }
} }

View file

@ -6,94 +6,55 @@ use Index\Colour\Colour;
use Index\Db\DbResult; use Index\Db\DbResult;
use Seria\Colours; use Seria\Colours;
readonly class UserInfo { class UserInfo {
private string $id; public function __construct(
private string $name; public private(set) string $id,
private ?int $colour; public private(set) string $name,
private int $rank; public private(set) ?int $colourRaw,
private int $perms; public private(set) int $rank,
private ?string $passKey; public private(set) int $perms,
private int $bytesDownloaded; #[\SensitiveParameter] public private(set) ?string $passKey,
private int $bytesUploaded; public private(set) int $bytesDownloaded,
public private(set) int $bytesUploaded,
) {}
public function __construct(DbResult $result) { public static function fromResult(DbResult $result): UserInfo {
$this->id = $result->getString(0); $colour = $result->getIntegerOrNull(2);
$this->name = $result->getString(1);
$colour = $result->isNull(2) ? null : $result->getInteger(2); return new UserInfo(
$this->colour = $colour === null || ($colour & 0x40000000) ? null : $colour; id: $result->getString(0),
name: $result->getString(1),
$this->rank = $result->getInteger(3); colourRaw: $colour === null || $colour & 0x40000000 ? null : $colour,
$this->perms = $result->getInteger(4); rank: $result->getInteger(3),
$this->passKey = $result->isNull(5) ? null : $result->getString(5); perms: $result->getInteger(4),
$this->bytesDownloaded = $result->getInteger(6); passKey: $result->getStringOrNull(5),
$this->bytesUploaded = $result->getInteger(7); bytesDownloaded: $result->getInteger(6),
bytesUploaded: $result->getInteger(7),
);
} }
public function getId(): string { public Colour $colour {
return $this->id; get => $this->colourRaw === null ? Colour::none() : Colours::cached($this->colourRaw);
} }
public function getName(): string { // The can* things should be in UsersContext as methods instead
return $this->name; public private(set) bool $canCreateTorrents = true;
public bool $canApproveTorrents {
get => $this->id === '1';
} }
public function hasColour(): bool { public bool $canRecalculateInfoHash {
return $this->colour !== null; get => $this->id === '1';
} }
public function getColour(): Colour { public float $ratio {
return $this->colour === null ? Colour::none() : Colours::cached($this->colour); get {
} $bd = $this->bytesDownloaded;
if($bd === 0)
return 0;
public function getColourRaw(): ?int { return $this->bytesUploaded / $bd;
return $this->colour; }
}
public function getRank(): int {
return $this->rank;
}
public function getPermsRaw(): int {
return $this->perms;
}
public function hasPassKey(): bool {
return $this->passKey !== null;
}
public function getPassKey(): ?string {
return $this->passKey;
}
public function getBytesDownloaded(): int {
return $this->bytesDownloaded;
}
public function getBytesUploaded(): int {
return $this->bytesUploaded;
}
public function isFlash(): bool {
return $this->id === '1';
}
public function canCreateTorrents(): bool {
return true;
}
public function canApproveTorrents(): bool {
return $this->isFlash();
}
public function canRecalculateInfoHash(): bool {
return $this->isFlash();
}
public function calculateRatio(): float {
$bd = $this->getBytesDownloaded();
if($bd === 0)
return 0;
return $this->getBytesUploaded() / $bd;
} }
} }

View file

@ -76,31 +76,30 @@ class Users {
if($selectPassKey) if($selectPassKey)
$query .= sprintf(' %s user_pass_key = ?', ++$args > 1 ? 'OR' : 'WHERE'); $query .= sprintf(' %s user_pass_key = ?', ++$args > 1 ? 'OR' : 'WHERE');
$args = 0;
$stmt = $this->cache->get($query); $stmt = $this->cache->get($query);
if($selectId) if($selectId)
$stmt->addParameter(++$args, $value); $stmt->nextParameter($value);
if($selectName) if($selectName)
$stmt->addParameter(++$args, $value); $stmt->nextParameter($value);
if($selectPassKey) if($selectPassKey)
$stmt->addParameter(++$args, $value); $stmt->nextParameter($value);
$stmt->execute(); $stmt->execute();
$result = $stmt->getResult(); $result = $stmt->getResult();
if(!$result->next()) if(!$result->next())
throw new RuntimeException('User not found.'); throw new RuntimeException('User not found.');
return new UserInfo($result); return UserInfo::fromResult($result);
} }
public function updatePassKey(UserInfo|string $userInfo): string { public function updatePassKey(UserInfo|string $userInfo): string {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->id;
$passKey = self::generatePassKey(); $passKey = self::generatePassKey();
$stmt = $this->cache->get('UPDATE ser_users SET user_pass_key = ? WHERE user_id = ?'); $stmt = $this->cache->get('UPDATE ser_users SET user_pass_key = ? WHERE user_id = ?');
$stmt->addParameter(1, $passKey); $stmt->nextParameter($passKey);
$stmt->addParameter(2, $userInfo); $stmt->nextParameter($userInfo);
$stmt->execute(); $stmt->execute();
return $passKey; return $passKey;
@ -108,12 +107,12 @@ class Users {
public function incrementTransferStats(UserInfo|string $userInfo, int $bytesDownloaded, int $bytesUploaded): void { public function incrementTransferStats(UserInfo|string $userInfo, int $bytesDownloaded, int $bytesUploaded): void {
if($userInfo instanceof UserInfo) if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId(); $userInfo = $userInfo->id;
$stmt = $this->cache->get('UPDATE ser_users SET user_bytes_downloaded = user_bytes_downloaded + ?, user_bytes_uploaded = user_bytes_uploaded + ? WHERE user_id = ?'); $stmt = $this->cache->get('UPDATE ser_users SET user_bytes_downloaded = user_bytes_downloaded + ?, user_bytes_uploaded = user_bytes_uploaded + ? WHERE user_id = ?');
$stmt->addParameter(1, $bytesDownloaded); $stmt->nextParameter($bytesDownloaded);
$stmt->addParameter(2, $bytesUploaded); $stmt->nextParameter($bytesUploaded);
$stmt->addParameter(3, $userInfo); $stmt->nextParameter($userInfo);
$stmt->execute(); $stmt->execute();
} }
} }

View file

@ -4,13 +4,9 @@ namespace Seria\Users;
use Index\Db\DbConnection; use Index\Db\DbConnection;
class UsersContext { class UsersContext {
private Users $users; public private(set) Users $users;
public function __construct(DbConnection $dbConn) { public function __construct(DbConnection $dbConn) {
$this->users = new Users($dbConn); $this->users = new Users($dbConn);
} }
public function getUsers(): Users {
return $this->users;
}
} }

View file

@ -8,7 +8,7 @@
<h2>{{ torrent_info.name }}</h2> <h2>{{ torrent_info.name }}</h2>
<ul> <ul>
<li><a href="/download/{{ torrent_info.id }}"><img src="//static.flash.moe/images/silk/link.png" alt=""> Download</a></li> <li><a href="/download/{{ torrent_info.id }}"><img src="//static.flash.moe/images/silk/link.png" alt=""> Download</a></li>
{% if globals.auth_info.isLoggedIn and globals.auth_info.userInfo.canRecalculateInfoHash %} {% if globals.auth_info.loggedIn and globals.auth_info.userInfo.canRecalculateInfoHash %}
<li><a href="javascript:;" class="js-info-hash-recalc"><img src="//static.flash.moe/images/silk/calculator.png" alt=""> Recalculate Info Hash</a></li> <li><a href="javascript:;" class="js-info-hash-recalc"><img src="//static.flash.moe/images/silk/calculator.png" alt=""> Recalculate Info Hash</a></li>
{% endif %} {% endif %}
</ul> </ul>
@ -20,7 +20,7 @@
<div class="info-stats-item-value"><time datetime="{{ torrent_info.createdTime|date('c') }}">{{ torrent_info.createdTime|date('Y-m-d H:i:s e') }}</time></div> <div class="info-stats-item-value"><time datetime="{{ torrent_info.createdTime|date('c') }}">{{ torrent_info.createdTime|date('Y-m-d H:i:s e') }}</time></div>
</div> </div>
{% if torrent_info.isApproved and globals.auth_info.isLoggedIn and globals.auth_info.userInfo.canApproveTorrents %} {% if torrent_info.approved and globals.auth_info.loggedIn and globals.auth_info.userInfo.canApproveTorrents %}
<div class="info-stats-item info-stats-approved"> <div class="info-stats-item info-stats-approved">
<div class="info-stats-item-title">Approved on</div> <div class="info-stats-item-title">Approved on</div>
<div class="info-stats-item-value"><time datetime="{{ torrent_info.approvedTime|date('c') }}">{{ torrent_info.approvedTime|date('Y-m-d H:i:s e') }}</time></div> <div class="info-stats-item-value"><time datetime="{{ torrent_info.approvedTime|date('c') }}">{{ torrent_info.approvedTime|date('Y-m-d H:i:s e') }}</time></div>
@ -42,7 +42,7 @@
<div class="info-stats-item-value">{{ torrent_incomplete_peers|number_format }}</div> <div class="info-stats-item-value">{{ torrent_incomplete_peers|number_format }}</div>
</div> </div>
{% if torrent_info.isPrivate %} {% if torrent_info.private %}
<div class="info-stats-item info-stats-visibility"> <div class="info-stats-item info-stats-visibility">
<div class="info-stats-item-title">Visibility</div> <div class="info-stats-item-title">Visibility</div>
<div class="info-stats-item-value">Private</div> <div class="info-stats-item-value">Private</div>
@ -58,7 +58,7 @@
</div> </div>
{% endif %} {% endif %}
{% if not torrent_info.isApproved and globals.auth_info.isLoggedIn and globals.auth_info.userInfo.canApproveTorrents %} {% if not torrent_info.approved and globals.auth_info.loggedIn and globals.auth_info.userInfo.canApproveTorrents %}
<div class="info-pending"> <div class="info-pending">
<div class="info-pending-text">This torrent is pending approval.</div> <div class="info-pending-text">This torrent is pending approval.</div>
<a class="info-pending-approve js-info-approve" href="javascript:;">APPROVE</a> <a class="info-pending-approve js-info-approve" href="javascript:;">APPROVE</a>
@ -66,7 +66,7 @@
</div> </div>
{% endif %} {% endif %}
{% if torrent_info.hasComment %} {% if torrent_info.comment is not empty %}
<div class="info-comment"> <div class="info-comment">
<div class="info-comment-header">Description</div> <div class="info-comment-header">Description</div>
<div class="info-comment-content"><pre>{{ torrent_info.comment }}</pre></div> <div class="info-comment-content"><pre>{{ torrent_info.comment }}</pre></div>

View file

@ -19,9 +19,9 @@
<div class="tdl-stats-downloading" title="Downloading"><div class="arrow">&#8595;</div><div class="number">{{ torrent.incomplete_peers|number_format }}</div></div> <div class="tdl-stats-downloading" title="Downloading"><div class="arrow">&#8595;</div><div class="number">{{ torrent.incomplete_peers|number_format }}</div></div>
</div> </div>
{% if globals.auth_info.isLoggedIn %} {% if globals.auth_info.loggedIn %}
<div class="tdl-actions"> <div class="tdl-actions">
{% if not torrent_info.isApproved and globals.auth_info.userInfo.canApproveTorrents %} {% if not torrent_info.approved and globals.auth_info.userInfo.canApproveTorrents %}
<a href="javascript:;" title="Approve" class="js-listing-approve" data-dlid="{{ torrent_info.id }}"><img src="//static.flash.moe/images/silk/tick.png" alt="Approve"></a> <a href="javascript:;" title="Approve" class="js-listing-approve" data-dlid="{{ torrent_info.id }}"><img src="//static.flash.moe/images/silk/tick.png" alt="Approve"></a>
<a href="javascript:;" title="Deny" class="js-listing-deny" data-dlid="{{ torrent_info.id }}"><img src="//static.flash.moe/images/silk/cross.png" alt="Deny"></a> <a href="javascript:;" title="Deny" class="js-listing-deny" data-dlid="{{ torrent_info.id }}"><img src="//static.flash.moe/images/silk/cross.png" alt="Deny"></a>
{% endif %} {% endif %}

View file

@ -9,9 +9,9 @@
<body> <body>
<div class="wrapper"> <div class="wrapper">
<nav class="header"> <nav class="header">
{% if globals.auth_info.isLoggedIn %} {% if globals.auth_info.loggedIn %}
{% set user_info = globals.auth_info.userInfo %} {% set user_info = globals.auth_info.userInfo %}
{% set user_ratio = user_info.calculateRatio %} {% set user_ratio = user_info.ratio %}
<div class="header-stats"> <div class="header-stats">
<a href="/profile/{{ globals.auth_info.userName }}">{{ globals.auth_info.userName }}</a> <a href="/profile/{{ globals.auth_info.userName }}">{{ globals.auth_info.userName }}</a>
&nbsp;&#183;&nbsp; <a href="/profile/{{ globals.auth_info.userName }}/history">R:&nbsp; <span style="color: {{ seria_ratio_colour(user_ratio) }};">{{ user_ratio|number_format(3) }}</span></a> &nbsp;&#183;&nbsp; <a href="/profile/{{ globals.auth_info.userName }}/history">R:&nbsp; <span style="color: {{ seria_ratio_colour(user_ratio) }};">{{ user_ratio|number_format(3) }}</span></a>

View file

@ -3,7 +3,7 @@
{% set title = profile_user.name %} {% set title = profile_user.name %}
{% set profile_ratio = profile_user.calculateRatio %} {% set profile_ratio = profile_user.ratio %}
{% block content %} {% block content %}
<div class="profile" style="--user-colour: {{ profile_user.colour }}" id="p{{ profile_user.id }}"> <div class="profile" style="--user-colour: {{ profile_user.colour }}" id="p{{ profile_user.id }}">