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

View file

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

View file

@ -5,39 +5,21 @@ use Index\Colour\Colour;
use Seria\Users\UserInfo;
class AuthInfo {
private ?UserInfo $userInfo;
public ?UserInfo $userInfo = null;
public function __construct() {
$this->setInfo();
public bool $loggedIn {
get => $this->userInfo !== null;
}
public function setInfo(
?UserInfo $userInfo = null
): void {
$this->userInfo = $userInfo;
public ?string $userId {
get => $this->userInfo?->id;
}
public function removeInfo(): void {
$this->setInfo();
public ?string $userName {
get => $this->userInfo?->name;
}
public function isLoggedIn(): bool {
return $this->userInfo !== null;
}
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;
public ?Colour $userColour {
get => $this->userInfo?->colour;
}
}

View file

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

View file

@ -1,11 +1,12 @@
<?php
namespace Seria;
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
use Index\Http\HttpResponseBuilder;
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon};
use Index\Templating\TplEnvironment;
class HomeRoutes implements RouteHandler {
use RouteHandlerTrait;
use RouteHandlerCommon;
public function __construct(
private ?TplEnvironment $templating
@ -17,7 +18,7 @@ class HomeRoutes implements RouteHandler {
}
#[HttpGet('/index.php')]
public function getIndexPHP($response): void {
public function getIndexPHP(HttpResponseBuilder $response): void {
$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!
}
public function getRouter(): Router {
return $this->router;
}
public function register(RouteHandler $handler): void {
$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 {
if($code === 500) {
$response->setTypeHTML();
$response->setContent(file_get_contents(SERIA_DIR_TEMPLATES . '/500.html'));
$response->content = file_get_contents(SERIA_DIR_TEMPLATES . '/500.html');
return;
}
$templating = $this->context->getTemplating();
if($templating === null) {
if($this->context->templating === null) {
$response->setTypePlain();
$response->setContent((string)$code);
$response->content = (string)$code;
return;
}
$response->setTypeHTML();
$response->setContent($templating->render('http-error', [
$response->content = $this->context->templating->render('http-error', [
'http_code' => $code,
'http_text' => $message,
]));
]);
}
}

View file

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

View file

@ -7,32 +7,32 @@ use Seria\Users\UserInfo;
class SiteInfo {
public function __construct(private Config $config) {}
public function getName(): string {
return $this->config->getString('name');
public string $name {
get => $this->config->getString('name');
}
public function getHost(): string {
return $this->config->getString('host');
public string $host {
get => $this->config->getString('host');
}
public function getMainSiteName(): string {
return $this->config->getString('parent');
public string $mainSiteName {
get => $this->config->getString('parent');
}
public function getLoginUrl(): string {
return $this->config->getString('login');
public string $loginUrl {
get => $this->config->getString('login');
}
public function getProfileUrl(UserInfo|string $userInfo): string {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
$userInfo = $userInfo->id;
return sprintf($this->config->getString('profile'), $userInfo);
}
public function getAvatarUrl(UserInfo|string $userInfo, int $res = 0): string {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
$userInfo = $userInfo->id;
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 */
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;
final class TemplatingExtension extends AbstractExtension {
private TorrentPeers $peers;
public function __construct(
private SeriaContext $ctx,
private SiteInfo $siteInfo
) {
$this->peers = $ctx->getTorrentsContext()->getPeers();
}
) {}
public function getFunctions() {
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_tag', GitInfo::tag(...)),
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_ratio_colour', Colours::forRatio(...)),
new TwigFunction('seria_filesize_colour', Colours::forFileSize(...)),
new TwigFunction('seria_count_user_uploading', $this->peers->countUserUploading(...)),
new TwigFunction('seria_count_user_downloading', $this->peers->countUserDownloading(...)),
new TwigFunction('seria_count_user_uploading', $this->ctx->torrentsCtx->peers->countUserUploading(...)),
new TwigFunction('seria_count_user_downloading', $this->ctx->torrentsCtx->peers->countUserDownloading(...)),
];
}
public function getHeaderMenu(): array {
$menu = [];
$authInfo = $this->ctx->getAuthInfo();
if($authInfo->isLoggedIn())
if($this->ctx->authInfo->loggedIn)
$menu[] = [
'text' => 'Settings',
'url' => '/settings',
@ -43,7 +38,7 @@ final class TemplatingExtension extends AbstractExtension {
else
$menu[] = [
'text' => 'Log in',
'url' => $this->siteInfo->getLoginUrl(),
'url' => $this->siteInfo->loginUrl,
];
$menu[] = [
@ -51,16 +46,14 @@ final class TemplatingExtension extends AbstractExtension {
'url' => '/available',
];
if($authInfo->isLoggedIn()) {
$userInfo = $authInfo->getUserInfo();
if($userInfo->canCreateTorrents())
if($this->ctx->authInfo->loggedIn) {
if($this->ctx->authInfo->userInfo->canCreateTorrents)
$menu[] = [
'text' => 'Create Torrent',
'url' => '/create',
];
if($userInfo->canApproveTorrents())
if($this->ctx->authInfo->userInfo->canApproveTorrents)
$menu[] = [
'text' => 'Pending Torrents',
'url' => '/pending',

View file

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

View file

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

View file

@ -1,78 +1,48 @@
<?php
namespace Seria\Torrents;
use Index\Bencode\{BencodeProperty,BencodeSerializable,BencodeSerializableTrait};
use Index\XArray;
use Index\Bencode\{BencodeProperty,BencodeSerializable,BencodeSerializableCommon};
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
// also useful for ipv6 peer support
private int $completePeers = 0;
private int $incompletePeers = 0;
#[BencodeProperty]
public private(set) int $complete = 0;
#[BencodeProperty]
public private(set) int $incomplete = 0;
public function __construct(
private array $peers,
private int $interval,
private int $minInterval,
#[BencodeProperty]
public private(set) int $interval,
#[BencodeProperty('min interval')]
public private(set) int $minInterval,
private bool $compactPeers,
private bool $noPeerIds
) {}
) {
foreach($peers as $peerInfo)
$peerInfo->seed ? ++$this->complete : ++$this->incomplete;
}
private function createCompactPeersList(): string {
$peers = '';
foreach($this->peers as $peerInfo) {
$peerInfo->isSeed() ? ++$this->completePeers : ++$this->incompletePeers;
$peers .= $peerInfo->getAddressRaw() . pack('n', $peerInfo->getPort());
}
return $peers;
return implode('', XArray::select($this->peers, fn($peerInfo) => ($peerInfo->addressRaw . pack('n', $peerInfo->port))));
}
private function createPeersListWithIds(): array {
$peers = [];
foreach($this->peers as $peerInfo) {
$peerInfo->isSeed() ? ++$this->completePeers : ++$this->incompletePeers;
$peers[] = [
'peer id' => $peerInfo->getId(),
'ip' => $peerInfo->getAddress(),
'port' => $peerInfo->getPort(),
];
}
return $peers;
return XArray::select($this->peers, fn($peerInfo) => [
'peer id' => $peerInfo->id,
'ip' => $peerInfo->address,
'port' => $peerInfo->port,
]);
}
private function createPeersListWithoutIds(): array {
$peers = [];
foreach($this->peers as $peerInfo) {
$peerInfo->isSeed() ? ++$this->completePeers : ++$this->incompletePeers;
$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;
return XArray::select($this->peers, fn($peerInfo) => [
'ip' => $peerInfo->address,
'port' => $peerInfo->port,
]);
}
// #[BencodeProperty('downloaded')]

View file

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

View file

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

View file

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

View file

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

View file

@ -12,18 +12,12 @@ class TorrentFiles {
$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->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo);
$stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute();
$result = $stmt->getResult();
$files = [];
while($result->next())
$files[] = new TorrentFileInfo($result);
return $files;
return $stmt->getResultIterator(TorrentFileInfo::fromResult(...));
}
public function createFile(
@ -32,16 +26,17 @@ class TorrentFiles {
array|string $path
): void {
$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->addParameter(2, $length);
$stmt->addParameter(3, is_array($path) ? implode('/', $path) : $path);
$stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->nextParameter($length);
$stmt->nextParameter(is_array($path) ? implode('/', $path) : $path);
$stmt->execute();
}
public function countTotalSize(TorrentInfo|string $torrentInfo): int {
$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();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
}

View file

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

View file

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

View file

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

View file

@ -4,106 +4,58 @@ namespace Seria\Torrents;
use Index\Db\DbResult;
use Seria\Users\UserInfo;
readonly class TorrentPeerInfo {
private string $id;
private string $torrentId;
private ?string $userId;
private string $address;
private int $port;
private int $updated;
private int $expires;
private string $agent;
private string $key;
private int $bytesUploaded;
private int $bytesDownloaded;
private int $bytesLeft;
class TorrentPeerInfo {
public function __construct(
public private(set) string $id,
public private(set) string $torrentId,
public private(set) ?string $userId,
public private(set) string $address,
public private(set) int $port,
public private(set) int $updatedTime,
public private(set) int $expiresTime,
public private(set) string $agent,
#[\SensitiveParameter] private string $key,
public private(set) int $bytesUploaded,
public private(set) int $bytesDownloaded,
public private(set) int $bytesLeft,
) {}
public function __construct(DbResult $result) {
$this->id = $result->getString(0);
$this->torrentId = $result->getString(1);
$this->userId = $result->isNull(2) ? null : $result->getString(2);
$this->address = $result->getString(3);
$this->port = $result->getInteger(4);
$this->updated = $result->getInteger(5);
$this->expires = $result->getInteger(6);
$this->agent = $result->getString(7);
$this->key = $result->getString(8);
$this->bytesUploaded = $result->getInteger(9);
$this->bytesDownloaded = $result->getInteger(10);
$this->bytesLeft = $result->getInteger(11);
public static function fromResult(DbResult $result): TorrentPeerInfo {
return new TorrentPeerInfo(
id: $result->getString(0),
torrentId: $result->getString(1),
userId: $result->getStringOrNull(2),
address: $result->getString(3),
port: $result->getInteger(4),
updatedTime: $result->getInteger(5),
expiresTime: $result->getInteger(6),
agent: $result->getString(7),
key: $result->getString(8),
bytesUploaded: $result->getInteger(9),
bytesDownloaded: $result->getInteger(10),
bytesLeft: $result->getInteger(11),
);
}
public function getId(): string {
return $this->id;
public string $addressRaw {
get => inet_pton($this->address);
}
public function getTorrentId(): string {
return $this->torrentId;
public bool $seed {
get => $this->bytesLeft <= 0;
}
public function getUserId(): ?string {
return $this->userId;
}
public function hasUserId(): bool {
return $this->userId !== null;
public bool $leech {
get => $this->bytesLeft > 0;
}
public function verifyUser(?UserInfo $userInfo): bool {
return $this->userId === null
? $userInfo === null
: $this->userId === $userInfo->getId();
}
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;
: $this->userId === $userInfo->id;
}
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()');
}
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->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo);
$stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute();
$peers = [];
$result = $stmt->getResult();
while($result->next())
$peers[] = new TorrentPeerInfo($result);
return $peers;
return $stmt->getResultIterator(TorrentPeerInfo::fromResult(...));
}
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->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo);
$stmt->addParameter(2, $peerId);
$stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->nextParameter($peerId);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
return null;
return new TorrentPeerInfo($result);
return TorrentPeerInfo::fromResult($result);
}
public function createPeer(
@ -58,20 +52,24 @@ class TorrentPeers {
int $bytesRemaining
): 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->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo);
$stmt->addParameter(2, $userInfo === null ? null : ($userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo));
$stmt->addParameter(3, $peerId);
$stmt->addParameter(4, $remoteAddr);
$stmt->addParameter(5, $remotePort);
$stmt->addParameter(6, $interval);
$stmt->addParameter(7, $peerAgent);
$stmt->addParameter(8, $peerKey);
$stmt->addParameter(9, $bytesUploaded);
$stmt->addParameter(10, $bytesDownloaded);
$stmt->addParameter(11, $bytesRemaining);
$stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->nextParameter($userInfo === null ? null : ($userInfo instanceof UserInfo ? $userInfo->id : $userInfo));
$stmt->nextParameter($peerId);
$stmt->nextParameter($remoteAddr);
$stmt->nextParameter($remotePort);
$stmt->nextParameter($interval);
$stmt->nextParameter($peerAgent);
$stmt->nextParameter($peerKey);
$stmt->nextParameter($bytesUploaded);
$stmt->nextParameter($bytesDownloaded);
$stmt->nextParameter($bytesRemaining);
$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(
@ -86,28 +84,28 @@ class TorrentPeers {
int $bytesRemaining
): 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->addParameter(1, $remoteAddr);
$stmt->addParameter(2, $remotePort);
$stmt->addParameter(3, $interval);
$stmt->addParameter(4, $peerAgent);
$stmt->addParameter(5, $bytesUploaded);
$stmt->addParameter(6, $bytesDownloaded);
$stmt->addParameter(7, $bytesRemaining);
$stmt->addParameter(8, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo);
$stmt->addParameter(9, $peerInfo instanceof TorrentPeerInfo ? $peerInfo->getId() : $peerInfo);
$stmt->nextParameter($remoteAddr);
$stmt->nextParameter($remotePort);
$stmt->nextParameter($interval);
$stmt->nextParameter($peerAgent);
$stmt->nextParameter($bytesUploaded);
$stmt->nextParameter($bytesDownloaded);
$stmt->nextParameter($bytesRemaining);
$stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->nextParameter($peerInfo instanceof TorrentPeerInfo ? $peerInfo->id : $peerInfo);
$stmt->execute();
}
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->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo);
$stmt->addParameter(2, $peerInfo instanceof TorrentPeerInfo ? $peerInfo->getId() : $peerInfo);
$stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->nextParameter($peerInfo instanceof TorrentPeerInfo ? $peerInfo->id : $peerInfo);
$stmt->execute();
}
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->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo);
$stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
@ -115,7 +113,7 @@ class TorrentPeers {
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->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo);
$stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
@ -123,7 +121,7 @@ class TorrentPeers {
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->addParameter(1, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
@ -131,7 +129,7 @@ class TorrentPeers {
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->addParameter(1, $userInfo instanceof UserInfo ? $userInfo->getId() : $userInfo);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;

View file

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

View file

@ -12,18 +12,11 @@ class TorrentPieces {
$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->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo);
$stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->execute();
$result = $stmt->getResult();
$pieces = [];
while($result->next())
$pieces[] = new TorrentPieceInfo($result);
return $pieces;
return $stmt->getResultIterator(TorrentPieceInfo::fromResult(...));
}
public function createPiece(
@ -31,8 +24,8 @@ class TorrentPieces {
string $hash
): void {
$stmt = $this->cache->get('INSERT INTO ser_torrents_pieces (torrent_id, piece_hash) VALUES (?, ?)');
$stmt->addParameter(1, $torrentInfo instanceof TorrentInfo ? $torrentInfo->getId() : $torrentInfo);
$stmt->addParameter(2, $hash);
$stmt->nextParameter($torrentInfo instanceof TorrentInfo ? $torrentInfo->id : $torrentInfo);
$stmt->nextParameter($hash);
$stmt->execute();
}
}

View file

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

View file

@ -1,16 +1,17 @@
<?php
namespace Seria\Torrents;
use Index\XArray;
use Index\Bencode\Bencode;
use Index\Db\DbConnection;
use Seria\GitInfo;
use Seria\Users\UserInfo;
class TorrentsContext {
private Torrents $torrents;
private TorrentFiles $files;
private TorrentPeers $peers;
private TorrentPieces $pieces;
public private(set) Torrents $torrents;
public private(set) TorrentFiles $files;
public private(set) TorrentPeers $peers;
public private(set) TorrentPieces $pieces;
public function __construct(DbConnection $dbConn) {
$this->torrents = new Torrents($dbConn);
@ -19,33 +20,17 @@ class TorrentsContext {
$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 {
if(is_string($torrentInfo))
$torrentInfo = $this->torrents->getTorrent($torrentInfo, 'id');
if(!$torrentInfo->isActive())
if(!$torrentInfo->active)
return 'inactive';
if($torrentInfo->isPrivate() && $userInfo === null)
if($torrentInfo->private && $userInfo === null)
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 '';
@ -55,32 +40,27 @@ class TorrentsContext {
if(is_string($torrentInfo))
$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
// this should really be combined somehow
$info = [
'files' => $this->files->getFiles($torrentInfo),
'name' => $torrentInfo->getName(),
'piece length' => $torrentInfo->getPieceLength(),
'pieces' => $pieces,
'files' => iterator_to_array($this->files->getFiles($torrentInfo), false),
'name' => $torrentInfo->name,
'piece length' => $torrentInfo->pieceLength,
'pieces' => implode('', XArray::select($this->pieces->getPieces($torrentInfo), fn($pieceInfo) => $pieceInfo->hash)),
];
if($torrentInfo->isPrivate())
if($torrentInfo->private)
$info['private'] = 1;
$data = [
'announce' => $announceUrl,
'created by' => sprintf('Seria %s', GitInfo::version()),
'creation date' => $torrentInfo->getCreatedTime(),
'creation date' => $torrentInfo->createdTime,
'info' => $info,
];
if($torrentInfo->hasComment())
$data['comment'] = $torrentInfo->getComment();
if(!empty($torrentInfo->comment))
$data['comment'] = $torrentInfo->comment;
return Bencode::encode($data);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@
<h2>{{ torrent_info.name }}</h2>
<ul>
<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>
{% endif %}
</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>
{% 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-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>
@ -42,7 +42,7 @@
<div class="info-stats-item-value">{{ torrent_incomplete_peers|number_format }}</div>
</div>
{% if torrent_info.isPrivate %}
{% if torrent_info.private %}
<div class="info-stats-item info-stats-visibility">
<div class="info-stats-item-title">Visibility</div>
<div class="info-stats-item-value">Private</div>
@ -58,7 +58,7 @@
</div>
{% 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-text">This torrent is pending approval.</div>
<a class="info-pending-approve js-info-approve" href="javascript:;">APPROVE</a>
@ -66,7 +66,7 @@
</div>
{% endif %}
{% if torrent_info.hasComment %}
{% if torrent_info.comment is not empty %}
<div class="info-comment">
<div class="info-comment-header">Description</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>
{% if globals.auth_info.isLoggedIn %}
{% if globals.auth_info.loggedIn %}
<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="Deny" class="js-listing-deny" data-dlid="{{ torrent_info.id }}"><img src="//static.flash.moe/images/silk/cross.png" alt="Deny"></a>
{% endif %}

View file

@ -9,9 +9,9 @@
<body>
<div class="wrapper">
<nav class="header">
{% if globals.auth_info.isLoggedIn %}
{% if globals.auth_info.loggedIn %}
{% set user_info = globals.auth_info.userInfo %}
{% set user_ratio = user_info.calculateRatio %}
{% set user_ratio = user_info.ratio %}
<div class="header-stats">
<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>

View file

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