diff --git a/composer.json b/composer.json index 94ce78c..9f7aad0 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "require": { - "flashwave/index": "^0.2410", + "flashwave/index": "^0.2503", "sentry/sdk": "^4.0" }, "autoload": { diff --git a/composer.lock b/composer.lock index 66a1580..99c7ba9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,25 +4,27 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "023574a93f076ef7a987151cdbfdb33e", + "content-hash": "ca6ae2c3d6229791acf93914e055bf41", "packages": [ { "name": "flashwave/index", - "version": "v0.2410.830205", + "version": "v0.2503.201929", "source": { "type": "git", "url": "https://patchii.net/flash/index.git", - "reference": "416c716b2efab9619d14be02a20cd6540e18a83f" + "reference": "1ba9e8fa34fbd30c84c23b2a9b6beb2152ca54f0" }, "require": { "ext-mbstring": "*", - "php": ">=8.3", - "twig/html-extra": "^3.13", - "twig/twig": "^3.14" + "php": ">=8.4", + "psr/http-message": "^2.0", + "psr/http-server-handler": "^1.0", + "twig/html-extra": "^3.20", + "twig/twig": "^3.20" }, "require-dev": { - "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^11.4" + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-memcache": "Support for the Index\\Cache\\Memcached namespace (only if you can't use ext-memcached for some reason).", @@ -59,7 +61,7 @@ ], "description": "Composer package for the common library for my projects.", "homepage": "https://railgun.sh/index", - "time": "2024-12-22T02:05:46+00:00" + "time": "2025-03-20T19:29:51+00:00" }, { "name": "guzzlehttp/psr7", @@ -179,16 +181,16 @@ }, { "name": "jean85/pretty-package-versions", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10" + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", - "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", "shasum": "" }, "require": { @@ -198,8 +200,9 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", "vimeo/psalm": "^4.3 || ^5.0" }, "type": "library", @@ -232,9 +235,9 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.0" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" }, - "time": "2024-11-18T16:19:46+00:00" + "time": "2025-03-19T14:43:43+00:00" }, { "name": "psr/http-factory", @@ -344,6 +347,62 @@ }, "time": "2023-04-04T09:54:51+00:00" }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, { "name": "psr/log", "version": "3.0.2", @@ -651,16 +710,16 @@ }, { "name": "symfony/mime", - "version": "v7.2.1", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283" + "reference": "87ca22046b78c3feaff04b337f33b38510fd686b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/7f9617fcf15cb61be30f8b252695ed5e2bfac283", - "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283", + "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b", + "reference": "87ca22046b78c3feaff04b337f33b38510fd686b", "shasum": "" }, "require": { @@ -715,7 +774,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.1" + "source": "https://github.com/symfony/mime/tree/v7.2.4" }, "funding": [ { @@ -731,7 +790,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T08:50:44+00:00" + "time": "2025-02-19T08:51:20+00:00" }, { "name": "symfony/options-resolver", @@ -1123,98 +1182,22 @@ ], "time": "2024-09-09T11:45:10+00:00" }, - { - "name": "symfony/polyfill-php81", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, { "name": "twig/html-extra", - "version": "v3.18.0", + "version": "v3.20.0", "source": { "type": "git", "url": "https://github.com/twigphp/html-extra.git", - "reference": "c63b28e192c1b7c15bb60f81d2e48b140846239a" + "reference": "f7d54d4de1b64182af745cfb66777f699b599734" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/html-extra/zipball/c63b28e192c1b7c15bb60f81d2e48b140846239a", - "reference": "c63b28e192c1b7c15bb60f81d2e48b140846239a", + "url": "https://api.github.com/repos/twigphp/html-extra/zipball/f7d54d4de1b64182af745cfb66777f699b599734", + "reference": "f7d54d4de1b64182af745cfb66777f699b599734", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/mime": "^5.4|^6.4|^7.0", "twig/twig": "^3.13|^4.0" @@ -1253,7 +1236,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/html-extra/tree/v3.18.0" + "source": "https://github.com/twigphp/html-extra/tree/v3.20.0" }, "funding": [ { @@ -1265,28 +1248,27 @@ "type": "tidelift" } ], - "time": "2024-12-29T10:29:59+00:00" + "time": "2025-01-31T20:45:36+00:00" }, { "name": "twig/twig", - "version": "v3.18.0", + "version": "v3.20.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50" + "reference": "3468920399451a384bef53cf7996965f7cd40183" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50", - "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3468920399451a384bef53cf7996965f7cd40183", + "reference": "3468920399451a384bef53cf7996965f7cd40183", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php81": "^1.29" + "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { "phpstan/phpstan": "^2.0", @@ -1333,7 +1315,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.18.0" + "source": "https://github.com/twigphp/Twig/tree/v3.20.0" }, "funding": [ { @@ -1345,7 +1327,7 @@ "type": "tidelift" } ], - "time": "2024-12-29T10:51:50+00:00" + "time": "2025-02-13T08:34:43+00:00" } ], "packages-dev": [], diff --git a/src/Apis/v1_0.php b/src/Apis/v1_0.php index bf75193..6e0ab52 100644 --- a/src/Apis/v1_0.php +++ b/src/Apis/v1_0.php @@ -17,11 +17,15 @@ use Uiharu\Lookup\NicoNicoLookupResult; use Index\MediaType; use Index\Cache\CacheProvider; use Index\Colour\{Colour,ColourRgb}; -use Index\Http\Routing\{HttpGet,HttpPost,RouteHandlerTrait}; +use Index\Http\{HttpRequest,HttpResponseBuilder}; +use Index\Http\Routing\RouteHandlerCommon; +use Index\Http\Routing\AccessControl\AccessControl; +use Index\Http\Routing\Routes\ExactRoute; +use Index\Http\Streams\Stream; use Index\Performance\Stopwatch; final class v1_0 implements \Uiharu\IApi { - use RouteHandlerTrait; + use RouteHandlerCommon; private UihContext $ctx; private CacheProvider $cache; @@ -35,8 +39,9 @@ final class v1_0 implements \Uiharu\IApi { return !str_starts_with($url, '/v'); } - #[HttpGet('/metadata/thumb/audio')] - public function getThumbAudio($response, $request) { + #[AccessControl] + #[ExactRoute('GET', '/metadata/thumb/audio')] + public function getThumbAudio(HttpResponseBuilder $response, HttpRequest $request) { $targetUrl = (string)$request->getParam('url'); if(empty($targetUrl)) @@ -53,11 +58,12 @@ final class v1_0 implements \Uiharu\IApi { $response->setContentType('image/png'); $response->setCacheControl('public', 'max-age=31536000', 'immutable'); - $response->setContent(FFMPEG::grabFirstAudioCover($parsedUrl)); + $response->body = Stream::createStream(FFMPEG::grabFirstAudioCover($parsedUrl)); } - #[HttpGet('/metadata/thumb/video')] - public function getThumbVideo($response, $request) { + #[AccessControl] + #[ExactRoute('GET', '/metadata/thumb/video')] + public function getThumbVideo(HttpResponseBuilder $response, HttpRequest $request) { $targetUrl = (string)$request->getParam('url'); if(empty($targetUrl)) @@ -74,55 +80,37 @@ final class v1_0 implements \Uiharu\IApi { $response->setContentType('image/png'); $response->setCacheControl('public', 'max-age=31536000', 'immutable'); - $response->setContent(FFMPEG::grabFirstVideoFrame($parsedUrl)); + $response->body = Stream::createStream(FFMPEG::grabFirstVideoFrame($parsedUrl)); } - #[HttpGet('/metadata')] - public function getMetadata($response, $request) { - if($request->getMethod() === 'HEAD') { - $response->setTypeJson(); - return; - } - + #[AccessControl] + #[ExactRoute('GET', '/metadata')] + public function getMetadata(HttpResponseBuilder $response, HttpRequest $request) { return $this->handleMetadata( $response, $request, (string)$request->getParam('url') ); } - #[HttpPost('/metadata')] - public function postMetadata($response, $request) { - if(!$request->isStringContent()) - return 400; - + #[AccessControl] + #[ExactRoute('POST', '/metadata')] + public function postMetadata(HttpResponseBuilder $response, HttpRequest $request) { return $this->handleMetadata( $response, $request, - (string)$request->getContent() + (string)$request->getBody() ); } - #[HttpGet('/metadata/batch')] - public function getMetadataBatch($response, $request) { - if($request->getMethod() === 'HEAD') { - $response->setTypeJson(); - return; - } - - return $this->handleMetadataBatch( - $response, $request, - $request->getParam('url', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY) - ); + #[AccessControl] + #[ExactRoute('GET', '/metadata/batch')] + public function getMetadataBatch(): int { + return 410; } - #[HttpPost('/metadata/batch')] - public function postMetadataBatch($response, $request) { - if(!$request->isFormContent()) - return 400; - - return $this->handleMetadataBatch( - $response, $request, - $request->getContent()->getParam('url', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY) - ); + #[AccessControl] + #[ExactRoute('POST', '/metadata/batch')] + public function postMetadataBatch(): int { + return 410; } private function metadataLookup(string $targetUrl) { @@ -172,11 +160,11 @@ final class v1_0 implements \Uiharu\IApi { $resp->content_type = MediaTypeExts::toV1($result->getMediaType()); if($result->hasColour()) { $colour = $result->getColour(); - if($colour->getAlpha() < 1.0) + if($colour->alpha < 1.0) $colour = new ColourRgb( - $colour->getRed(), - $colour->getGreen(), - $colour->getBlue() + $colour->red, + $colour->green, + $colour->blue, ); $resp->color = (string)$colour; @@ -278,7 +266,7 @@ final class v1_0 implements \Uiharu\IApi { } $sw->stop(); - $resp->took = $sw->getElapsedTime() / 1000; + $resp->took = $sw->elapsedTime / 1000; $respJson = json_encode($resp); $this->cache->set($cacheKey, $respJson, 10 * 60); @@ -290,7 +278,7 @@ final class v1_0 implements \Uiharu\IApi { return $resp; } - private function handleMetadata($response, $request, string $targetUrl) { + private function handleMetadata(HttpResponseBuilder $response, HttpRequest $request, string $targetUrl) { $result = $this->metadataLookup($targetUrl); if(is_int($result)) return $result; @@ -305,7 +293,7 @@ final class v1_0 implements \Uiharu\IApi { return $result; } - private function handleMetadataBatch($response, $request, array $urls) { + private function handleMetadataBatch(HttpResponseBuilder $response, HttpRequest $request, array $urls) { $sw = Stopwatch::startNew(); if(count($urls) > 20) diff --git a/src/MediaTypeExts.php b/src/MediaTypeExts.php index 8322bb8..a132e71 100644 --- a/src/MediaTypeExts.php +++ b/src/MediaTypeExts.php @@ -7,14 +7,14 @@ final class MediaTypeExts { public static function toV1(MediaType $mediaType): array { $parts = [ 'string' => (string)$mediaType, - 'type' => $mediaType->getCategory(), - 'subtype' => $mediaType->getKind(), + 'type' => $mediaType->category, + 'subtype' => $mediaType->kind, ]; - if(!empty($suffix = $mediaType->getSuffix())) + if(!empty($suffix = $mediaType->suffix)) $parts['suffix'] = $suffix; - if(!empty($params = $mediaType->getParams())) + if(!empty($params = $mediaType->params)) $parts['params'] = $params; return $parts; diff --git a/src/UihContext.php b/src/UihContext.php index 1d50af5..c1e108a 100644 --- a/src/UihContext.php +++ b/src/UihContext.php @@ -3,12 +3,15 @@ namespace Uiharu; use Index\Cache\CacheProvider; use Index\Config\Config; -use Index\Http\Routing\HttpRouter; +use Index\Http\HttpResponseBuilder; +use Index\Http\Routing\{Router,RouteHandler,RouteHandlerCommon}; +use Index\Http\Routing\Filters\PrefixFilter; +use Index\Http\Routing\Routes\ExactRoute; final class UihContext { private CacheProvider $cache; private Config $config; - private HttpRouter $router; + private Router $router; private array $apis = []; private array $lookups = []; @@ -21,52 +24,28 @@ final class UihContext { return $this->cache; } - public function getRouter(): HttpRouter { + public function getRouter(): Router { return $this->router; } - public function isOriginAllowed(string $origin): bool { - $origin = mb_strtolower(parse_url($origin, PHP_URL_HOST)); - - if($origin === $_SERVER['HTTP_HOST']) - return true; - - $allowed = $this->config->getArray('cors:origins'); - if(empty($allowed)) - return true; - - return in_array($origin, $allowed); - } - public function setupHttp(): void { - $this->router = new HttpRouter; - $this->router->use('/', function($response) { - $response->setPoweredBy('Uiharu'); - }); + $this->router = new Router( + accessControlHandler: new UiharuAccessControlHandler($this->config->getArray('cors:origins')), + ); + $this->router->register(new class implements RouteHandler { + use RouteHandlerCommon; - $this->router->use('/', function($response, $request) { - $origin = $request->getHeaderLine('Origin'); - - if(!empty($origin)) { - if(!$this->isOriginAllowed($origin)) - return 403; - - $response->setHeader('Access-Control-Allow-Origin', $origin); - $response->setHeader('Vary', 'Origin'); + #[PrefixFilter('/')] + public function filterPoweredBy(HttpResponseBuilder $response): void { + $response->setPoweredBy('Uiharu'); } - }); - $this->router->use('/', function($response, $request) { - if($request->getMethod() === 'OPTIONS') { - $response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST'); - return 204; + #[ExactRoute('GET', '/')] + public function getIndex(HttpResponseBuilder $response): void { + $response->accelRedirect('/index.html'); + $response->setContentType('text/html; charset=utf-8'); } }); - - $this->router->get('/', function($response) { - $response->accelRedirect('/index.html'); - $response->setContentType('text/html; charset=utf-8'); - }); } public function dispatchHttp(): void { diff --git a/src/UiharuAccessControlHandler.php b/src/UiharuAccessControlHandler.php new file mode 100644 index 0000000..ceda55f --- /dev/null +++ b/src/UiharuAccessControlHandler.php @@ -0,0 +1,26 @@ +<?php +namespace Uiharu; + +use Index\Http\HttpUri; +use Index\Http\Routing\HandlerContext; +use Index\Http\Routing\Routes\RouteInfo; +use Index\Http\Routing\AccessControl\{AccessControl,SimpleAccessControlHandler}; + +class UiharuAccessControlHandler extends SimpleAccessControlHandler { + public function __construct( + private array $origins, + ) {} + + #[\Override] + public function checkAccess( + HandlerContext $context, + AccessControl $accessControl, + HttpUri $origin, + ?RouteInfo $routeInfo = null, + ): string|bool { + if(!in_array($origin->host, $this->origins)) + return false; + + return $accessControl->credentials ? (string)$origin : true; + } +}