diff --git a/CHANGELOG b/CHANGELOG index f6263f2e..6995ad0a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -Version 0.1?.? (2022-??-??) +Version 0.10.3 (2022-09-14) =========================== Bug fixes: @@ -6,6 +6,7 @@ Bug fixes: - Allow multiple date ranges in search strings in Tiny Tiny RSS - Honour user time zone when interpreting search strings in Tiny Tiny RSS - Perform MySQL table maintenance more reliably +- Address CVE-2022-31090, CVE-2022-31091, CVE-2022-29248, and CVE-2022-31109 Version 0.10.2 (2022-04-04) =========================== diff --git a/RoboFile.php b/RoboFile.php index 4010e875..5d7c46fe 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -164,7 +164,7 @@ class RoboFile extends \Robo\Tasks { if ( (IS_WIN && (!exec(escapeshellarg($bin)." --help $blackhole", $junk, $status) || $status)) || (!IS_WIN && (!exec("which ".escapeshellarg($bin)." $blackhole", $junk, $status) || $status)) - ) { + ) { return false; } } diff --git a/UPGRADING b/UPGRADING index f6dcfff6..0ca27113 100644 --- a/UPGRADING +++ b/UPGRADING @@ -11,6 +11,13 @@ usually prudent: `composer install -o --no-dev` +Upgrading from 0.10.2 to 0.10.3 +============================= + +- The following Composer dependencies have been removed: + - laminas/laminas-diactoros + + Upgrading from 0.8.5 to 0.9.0 ============================= diff --git a/composer.json b/composer.json index 97aedd38..38952d20 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "hosteurope/password-generator": "1.*", "docopt/docopt": "1.*", "jkingweb/druuid": "3.*", - "laminas/laminas-diactoros": "2.*", + "guzzlehttp/psr7": "1.*", "laminas/laminas-httphandlerrunner": "1.*" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 21480c42..972f8e87 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "c658930fbc56b2b2cf646e34c6a8d8d3", + "content-hash": "2671d9010a4ac73e877838baf3586df2", "packages": [ { "name": "docopt/docopt", @@ -58,24 +58,24 @@ }, { "name": "guzzlehttp/guzzle", - "version": "6.5.6", + "version": "6.5.8", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "f092dd734083473658de3ee4bef093ed77d2689c" + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f092dd734083473658de3ee4bef093ed77d2689c", - "reference": "f092dd734083473658de3ee4bef093ed77d2689c", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981", + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.6.1", + "guzzlehttp/psr7": "^1.9", "php": ">=5.5", - "symfony/polyfill-intl-idn": "^1.17.0" + "symfony/polyfill-intl-idn": "^1.17" }, "require-dev": { "ext-curl": "*", @@ -153,7 +153,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/6.5.6" + "source": "https://github.com/guzzle/guzzle/tree/6.5.8" }, "funding": [ { @@ -169,20 +169,20 @@ "type": "tidelift" } ], - "time": "2022-05-25T13:19:12+00:00" + "time": "2022-06-20T22:16:07+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.5.1", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + "reference": "b94b2807d85443f9719887892882d0329d1e2598" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", - "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598", + "reference": "b94b2807d85443f9719887892882d0329d1e2598", "shasum": "" }, "require": { @@ -237,7 +237,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.5.1" + "source": "https://github.com/guzzle/promises/tree/1.5.2" }, "funding": [ { @@ -253,20 +253,20 @@ "type": "tidelift" } ], - "time": "2021-10-22T20:56:57+00:00" + "time": "2022-08-28T14:55:35+00:00" }, { "name": "guzzlehttp/psr7", - "version": "1.8.5", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "337e3ad8e5716c15f9657bd214d16cc5e69df268" + "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/337e3ad8e5716c15f9657bd214d16cc5e69df268", - "reference": "337e3ad8e5716c15f9657bd214d16cc5e69df268", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", + "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", "shasum": "" }, "require": { @@ -287,7 +287,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -347,7 +347,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.8.5" + "source": "https://github.com/guzzle/psr7/tree/1.9.0" }, "funding": [ { @@ -363,7 +363,7 @@ "type": "tidelift" } ], - "time": "2022-03-20T21:51:18+00:00" + "time": "2022-06-20T21:43:03+00:00" }, { "name": "hosteurope/password-generator", @@ -537,105 +537,6 @@ }, "time": "2017-08-17T12:23:43+00:00" }, - { - "name": "laminas/laminas-diactoros", - "version": "2.4.1", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-diactoros.git", - "reference": "36ef09b73e884135d2059cc498c938e90821bb57" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/36ef09b73e884135d2059cc498c938e90821bb57", - "reference": "36ef09b73e884135d2059cc498c938e90821bb57", - "shasum": "" - }, - "require": { - "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^7.1", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0" - }, - "conflict": { - "phpspec/prophecy": "<1.9.0" - }, - "provide": { - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" - }, - "replace": { - "zendframework/zend-diactoros": "^2.2.1" - }, - "require-dev": { - "ext-curl": "*", - "ext-dom": "*", - "ext-gd": "*", - "ext-libxml": "*", - "http-interop/http-factory-tests": "^0.5.0", - "laminas/laminas-coding-standard": "~1.0.0", - "php-http/psr7-integration-tests": "^1.0", - "phpunit/phpunit": "^7.5.18" - }, - "type": "library", - "extra": { - "laminas": { - "config-provider": "Laminas\\Diactoros\\ConfigProvider", - "module": "Laminas\\Diactoros" - } - }, - "autoload": { - "files": [ - "src/functions/create_uploaded_file.php", - "src/functions/marshal_headers_from_sapi.php", - "src/functions/marshal_method_from_sapi.php", - "src/functions/marshal_protocol_version_from_sapi.php", - "src/functions/marshal_uri_from_sapi.php", - "src/functions/normalize_server.php", - "src/functions/normalize_uploaded_files.php", - "src/functions/parse_cookie_header.php", - "src/functions/create_uploaded_file.legacy.php", - "src/functions/marshal_headers_from_sapi.legacy.php", - "src/functions/marshal_method_from_sapi.legacy.php", - "src/functions/marshal_protocol_version_from_sapi.legacy.php", - "src/functions/marshal_uri_from_sapi.legacy.php", - "src/functions/normalize_server.legacy.php", - "src/functions/normalize_uploaded_files.legacy.php", - "src/functions/parse_cookie_header.legacy.php" - ], - "psr-4": { - "Laminas\\Diactoros\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "PSR HTTP Message implementations", - "homepage": "https://laminas.dev", - "keywords": [ - "http", - "laminas", - "psr", - "psr-17", - "psr-7" - ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-diactoros/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-diactoros/issues", - "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", - "source": "https://github.com/laminas/laminas-diactoros" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2020-09-03T14:29:41+00:00" - }, { "name": "laminas/laminas-httphandlerrunner", "version": "1.2.0", @@ -892,61 +793,6 @@ }, "time": "2020-09-15T07:28:23+00:00" }, - { - "name": "psr/http-factory", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "shasum": "" - }, - "require": { - "php": ">=7.0.0", - "psr/http-message": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" - }, - "time": "2019-04-30T12:38:16+00:00" - }, { "name": "psr/http-message", "version": "1.0.1", @@ -1153,16 +999,16 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "749045c69efb97c70d25d7463abba812e91f3a44" + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/749045c69efb97c70d25d7463abba812e91f3a44", - "reference": "749045c69efb97c70d25d7463abba812e91f3a44", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8", "shasum": "" }, "require": { @@ -1176,7 +1022,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1220,7 +1066,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.26.0" }, "funding": [ { @@ -1236,20 +1082,20 @@ "type": "tidelift" } ], - "time": "2021-09-14T14:02:44+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", "shasum": "" }, "require": { @@ -1261,7 +1107,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1304,7 +1150,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" }, "funding": [ { @@ -1320,20 +1166,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "9a142215a36a3888e30d0a9eeea9766764e96976" + "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976", - "reference": "9a142215a36a3888e30d0a9eeea9766764e96976", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/bf44a9fd41feaac72b074de600314a93e2ae78e2", + "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2", "shasum": "" }, "require": { @@ -1342,7 +1188,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1380,7 +1226,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.26.0" }, "funding": [ { @@ -1396,7 +1242,7 @@ "type": "tidelift" } ], - "time": "2021-05-27T09:17:38+00:00" + "time": "2022-05-24T11:49:31+00:00" } ], "packages-dev": [ diff --git a/lib/Arsse.php b/lib/Arsse.php index 604bccd1..9d5a57b7 100644 --- a/lib/Arsse.php +++ b/lib/Arsse.php @@ -7,7 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse; class Arsse { - public const VERSION = "0.10.2"; + public const VERSION = "0.10.3"; public const REQUIRED_EXTENSIONS = [ "intl", // as this extension is required to prepare formatted messages, its absence will throw a distinct English-only exception "dom", diff --git a/lib/Db/MySQL/ExceptionBuilder.php b/lib/Db/MySQL/ExceptionBuilder.php index 8f5be9c3..dfe01ae5 100644 --- a/lib/Db/MySQL/ExceptionBuilder.php +++ b/lib/Db/MySQL/ExceptionBuilder.php @@ -27,7 +27,7 @@ trait ExceptionBuilder { public static function buildConnectionException($code, string $msg): array { switch ($code) { case 1045: - // @codeCoverageIgnoreStart + // @codeCoverageIgnoreStart case 1043: case 1044: case 1046: @@ -48,7 +48,7 @@ trait ExceptionBuilder { case 2018: case 2026: case 2028: - // @codeCoverageIgnoreEnd + // @codeCoverageIgnoreEnd return [Exception::class, 'connectionFailure', ['engine' => "MySQL", 'message' => $msg]]; default: return [Exception::class, 'engineErrorGeneral', $msg]; // @codeCoverageIgnore diff --git a/lib/Misc/HTTP.php b/lib/Misc/HTTP.php index ac415062..b772ad2e 100644 --- a/lib/Misc/HTTP.php +++ b/lib/Misc/HTTP.php @@ -7,16 +7,41 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Misc; use Psr\Http\Message\MessageInterface; +use Psr\Http\Message\ResponseInterface; +use GuzzleHttp\Psr7\Response; class HTTP { public static function matchType(MessageInterface $msg, string ...$type): bool { $header = $msg->getHeaderLine("Content-Type") ?? ""; foreach ($type as $t) { - $pattern = "/^".preg_quote(trim($t), "/")."\s*($|;|,)/Di"; + if (($t[0] ?? "") === "+") { + $pattern = "/^[^+;,\s]*".preg_quote(trim($t), "/")."\s*($|;|,)/Di"; + } else { + $pattern = "/^".preg_quote(trim($t), "/")."\s*($|;|,)/Di"; + } if (preg_match($pattern, $header)) { return true; } } return false; } + + public static function respEmpty(int $status, ?array $headers = []): ResponseInterface { + return new Response($status, $headers ?? []); + } + + public static function respJson($body, int $status = 200, ?array $headers = []): ResponseInterface { + $headers = ($headers ?? []) + ['Content-Type' => "application/json"]; + return new Response($status, $headers, json_encode($body, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE)); + } + + public static function respText(string $body, int $status = 200, ?array $headers = []): ResponseInterface { + $headers = ($headers ?? []) + ['Content-Type' => "text/plain; charset=UTF-8"]; + return new Response($status, $headers, $body); + } + + public static function respXml(string $body, int $status = 200, ?array $headers = []): ResponseInterface { + $headers = ($headers ?? []) + ['Content-Type' => "application/xml; charset=UTF-8"]; + return new Response($status, $headers, $body); + } } diff --git a/lib/REST.php b/lib/REST.php index adc56ac5..8cae6ea2 100644 --- a/lib/REST.php +++ b/lib/REST.php @@ -7,11 +7,11 @@ declare(strict_types=1); namespace JKingWeb\Arsse; use JKingWeb\Arsse\Misc\URL; +use JKingWeb\Arsse\Misc\HTTP; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -use Laminas\Diactoros\ServerRequestFactory; -use Laminas\Diactoros\Response\EmptyResponse; +use GuzzleHttp\Psr7\ServerRequest; class REST { public const API_LIST = [ @@ -84,7 +84,7 @@ class REST { // ensure the require extensions are loaded Arsse::checkExtensions(...Arsse::REQUIRED_EXTENSIONS); // create a request object if not provided - $req = $req ?? ServerRequestFactory::fromGlobals(); + $req = $req ?? ServerRequest::fromGlobals(); // find the API to handle [, $target, $class] = $this->apiMatch($req->getRequestTarget(), $this->apis); // authenticate the request pre-emptively @@ -101,7 +101,7 @@ class REST { $res = $drv->dispatch($req); } } catch (REST\Exception501 $e) { - $res = new EmptyResponse(501); + $res = HTTP::respEmpty(501); } // modify the response so that it has all the required metadata return $this->normalizeResponse($res, $req); @@ -180,7 +180,7 @@ class REST { } // if the response is to a HEAD request, the body should be omitted if ($req && $req->getMethod() === "HEAD") { - $res = new EmptyResponse($res->getStatusCode(), $res->getHeaders()); + $res = HTTP::respEmpty($res->getStatusCode(), $res->getHeaders()); } // if an Allow header field is present, normalize it if ($res->hasHeader("Allow")) { diff --git a/lib/REST/Fever/API.php b/lib/REST/Fever/API.php index 7ad69ba5..9b243129 100644 --- a/lib/REST/Fever/API.php +++ b/lib/REST/Fever/API.php @@ -10,13 +10,10 @@ use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Context\Context; use JKingWeb\Arsse\Misc\ValueInfo as V; use JKingWeb\Arsse\Misc\Date; -use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Misc\HTTP; +use JKingWeb\Arsse\Db\ExceptionInput; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -use Laminas\Diactoros\Response\JsonResponse; -use Laminas\Diactoros\Response\XmlResponse; -use Laminas\Diactoros\Response\EmptyResponse; class API extends \JKingWeb\Arsse\REST\AbstractHandler { public const LEVEL = 3; @@ -62,11 +59,11 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { $P = $this->normalizeInputPost($req->getParsedBody() ?? []); if (!isset($G['api'])) { // the original would have shown the Fever UI in the absence of the "api" parameter, but we'll return 404 - return new EmptyResponse(404); + return HTTP::respEmpty(404); } switch ($req->getMethod()) { case "OPTIONS": - return new EmptyResponse(204, [ + return HTTP::respEmpty(204, [ 'Allow' => "POST", 'Accept' => implode(", ", self::ACCEPTED_TYPES), ]); @@ -82,7 +79,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { $out['auth'] = 1; } elseif (Arsse::$conf->userHTTPAuthRequired || Arsse::$conf->userPreAuth || $req->getAttribute("authenticationFailed", false)) { // otherwise if HTTP authentication failed or is required, deny access at the HTTP level - return new EmptyResponse(401); + return HTTP::respEmpty(401); } // produce a full response if authenticated or a basic response otherwise if ($this->logIn(strtolower($P['api_key'] ?? ""))) { @@ -93,7 +90,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // return the result, possibly formatted as XML return $this->formatResponse($out, ($G['api'] === "xml")); default: - return new EmptyResponse(405, ['Allow' => "OPTIONS,POST"]); + return HTTP::respEmpty(405, ['Allow' => "OPTIONS,POST"]); } } @@ -182,9 +179,9 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { if ($xml) { $d = new \DOMDocument("1.0", "utf-8"); $d->appendChild($this->makeXMLAssoc($data, $d->createElement("response"))); - return new XmlResponse($d->saveXML()); + return HTTP::respXml($d->saveXML()); } else { - return new JsonResponse($data, 200, [], \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE); + return HTTP::respJson($data, 200, [], \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE); } } diff --git a/lib/REST/Miniflux/ErrorResponse.php b/lib/REST/Miniflux/ErrorResponse.php deleted file mode 100644 index 1cf467ee..00000000 --- a/lib/REST/Miniflux/ErrorResponse.php +++ /dev/null @@ -1,19 +0,0 @@ - Arsse::$lang->msg("API.Miniflux.Error.".$msg, $data)]; - parent::__construct($data, $status, $headers, $encodingOptions); - } -} diff --git a/lib/REST/Miniflux/Status.php b/lib/REST/Miniflux/Status.php index 367a7a65..b84f4a13 100644 --- a/lib/REST/Miniflux/Status.php +++ b/lib/REST/Miniflux/Status.php @@ -6,10 +6,9 @@ declare(strict_types=1); namespace JKingWeb\Arsse\REST\Miniflux; +use JKingWeb\Arsse\Misc\HTTP; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -use Laminas\Diactoros\Response\EmptyResponse; -use Laminas\Diactoros\Response\TextResponse; class Status extends \JKingWeb\Arsse\REST\AbstractHandler { public function __construct() { @@ -18,13 +17,13 @@ class Status extends \JKingWeb\Arsse\REST\AbstractHandler { public function dispatch(ServerRequestInterface $req): ResponseInterface { $target = parse_url($req->getRequestTarget())['path'] ?? ""; if (!in_array($target, ["/version", "/healthcheck"])) { - return new EmptyResponse(404); + return HTTP::respEmpty(404); } $method = $req->getMethod(); if ($method === "OPTIONS") { - return new EmptyResponse(204, ['Allow' => "HEAD, GET"]); + return HTTP::respEmpty(204, ['Allow' => "HEAD, GET"]); } elseif ($method !== "GET") { - return new EmptyResponse(405, ['Allow' => "HEAD, GET"]); + return HTTP::respEmpty(405, ['Allow' => "HEAD, GET"]); } $out = ""; if ($target === "/version") { @@ -32,6 +31,6 @@ class Status extends \JKingWeb\Arsse\REST\AbstractHandler { } elseif ($target === "/healthcheck") { $out = "OK"; } - return new TextResponse($out); + return HTTP::respText($out); } } diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 6897472f..2b981c59 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -19,6 +19,7 @@ use JKingWeb\Arsse\ImportExport\OPML; use JKingWeb\Arsse\ImportExport\Exception as ImportException; use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\URL; +use JKingWeb\Arsse\Misc\HTTP; use JKingWeb\Arsse\Misc\ValueInfo as V; use JKingWeb\Arsse\REST\Exception; use JKingWeb\Arsse\Rule\Rule; @@ -26,10 +27,7 @@ use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\Exception as UserException; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -use Laminas\Diactoros\Response\EmptyResponse; -use Laminas\Diactoros\Response\JsonResponse as Response; -use Laminas\Diactoros\Response\TextResponse as GenericResponse; -use Laminas\Diactoros\Uri; +use GuzzleHttp\Psr7\Uri; class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { public const VERSION = "2.0.28"; @@ -215,6 +213,14 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { public function __construct() { } + public static function respError($data, int $status = 400, array $headers = []): ResponseInterface { + assert(isset(Arsse::$lang) && Arsse::$lang instanceof \JKingWeb\Arsse\Lang, new \Exception("Language database must be initialized before use")); + $data = (array) $data; + $msg = array_shift($data); + $data = ["error_message" => Arsse::$lang->msg("API.Miniflux.Error.".$msg, $data)]; + return HTTP::respJson($data, $status, $headers); + } + protected function authenticate(ServerRequestInterface $req): bool { // first check any tokens; this is what Miniflux does if ($req->hasHeader("X-Auth-Token")) { @@ -247,7 +253,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } // try to authenticate if (!$this->authenticate($req)) { - return new ErrorResponse("401", 401); + return self::respError("401", 401); } $func = $this->chooseCall($target, $method); if ($func instanceof ResponseInterface) { @@ -256,7 +262,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { [$func, $reqAdmin, $reqPath, $reqBody, $reqQuery, $reqFields] = $func; } if ($reqAdmin && !$this->isAdmin()) { - return new ErrorResponse("403", 403); + return self::respError("403", 403); } $args = []; if ($reqPath) { @@ -271,7 +277,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $data = @json_decode($data, true); if (json_last_error() !== \JSON_ERROR_NONE) { // if the body could not be parsed as JSON, return "400 Bad Request" - return new ErrorResponse(["InvalidBodyJSON", json_last_error_msg()], 400); + return self::respError(["InvalidBodyJSON", json_last_error_msg()], 400); } } else { $data = []; @@ -295,10 +301,10 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { // @codeCoverageIgnoreStart } catch (Exception $e) { // if there was a REST exception return 400 - return new EmptyResponse(400); + return HTTP::respEmpty(400); } catch (AbstractException $e) { // if there was any other Arsse exception return 500 - return new EmptyResponse(500); + return HTTP::respEmpty(500); } // @codeCoverageIgnoreEnd } @@ -317,11 +323,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return self::CALLS[$url][$method]; } else { // otherwise return 405 - return new EmptyResponse(405, ['Allow' => implode(", ", array_keys(self::CALLS[$url]))]); + return HTTP::respEmpty(405, ['Allow' => implode(", ", array_keys(self::CALLS[$url]))]); } } else { // if the path is not supported, return 404 - return new EmptyResponse(404); + return HTTP::respEmpty(404); } } @@ -346,20 +352,20 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if (!isset($body[$k])) { $body[$k] = null; } elseif (gettype($body[$k]) !== $t) { - return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422); + return self::respError(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422); } elseif ( (in_array($k, ["keeplist_rules", "blocklist_rules"]) && !Rule::validate($body[$k])) || (in_array($k, ["url", "feed_url"]) && !URL::absolute($body[$k])) || ($k === "category_id" && $body[$k] < 1) || ($k === "status" && !in_array($body[$k], ["read", "unread", "removed"])) ) { - return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422); + return self::respError(["InvalidInputValue", 'field' => $k], 422); } elseif ($k === "entry_ids") { foreach ($body[$k] as $v) { if (gettype($v) !== "integer") { - return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => "integer", 'actual' => gettype($v)], 422); + return self::respError(["InvalidInputType", 'field' => $k, 'expected' => "integer", 'actual' => gettype($v)], 422); } elseif ($v < 1) { - return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422); + return self::respError(["InvalidInputValue", 'field' => $k], 422); } } } @@ -371,16 +377,16 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $body[$k] = null; } elseif ($k === "entry_sorting_direction") { if (!in_array($body[$k], ["asc", "desc"])) { - return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422); + return self::respError(["InvalidInputValue", 'field' => $k], 422); } } elseif (gettype($body[$k]) !== $t) { - return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422); + return self::respError(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422); } } // check for any missing required values foreach ($req as $k) { if (!isset($body[$k]) || (is_array($body[$k]) && !$body[$k])) { - return new ErrorResponse(["MissingInputValue", 'field' => $k], 422); + return self::respError(["MissingInputValue", 'field' => $k], 422); } } return $body; @@ -409,7 +415,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if ($seen[$k] && !$a) { // if the key has already been seen and it's not an array field, bail // NOTE: Miniflux itself simply ignores duplicates entirely - return new ErrorResponse(["DuplicateInputValue", 'field' => $k], 400); + return self::respError(["DuplicateInputValue", 'field' => $k], 400); } $seen[$k] = true; if ($k === "starred") { @@ -425,7 +431,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $out[$k] = V::normalize($v, $t + V::M_STRICT, "unix"); } } catch (ExceptionType $e) { - return new ErrorResponse(["InvalidInputValue", 'field' => $k], 400); + return self::respError(["InvalidInputValue", 'field' => $k], 400); } // perform additional validation if ( @@ -435,7 +441,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { || ($k === "order" && !in_array($v, ["id", "status", "published_at", "category_title", "category_id"])) || ($k === "status" && !in_array($v, ["read", "unread", "removed"])) ) { - return new ErrorResponse(["InvalidInputValue", 'field' => $k], 400); + return self::respError(["InvalidInputValue", 'field' => $k], 400); } } return $out; @@ -451,13 +457,13 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if (in_array("GET", $allowed)) { array_unshift($allowed, "HEAD"); } - return new EmptyResponse(204, [ + return HTTP::respEmpty(204, [ 'Allow' => implode(", ", $allowed), 'Accept' => implode(", ", $url === "/import" ? self::ACCEPTED_TYPES_OPML : self::ACCEPTED_TYPES_JSON), ]); } else { // if the path is not supported, return 404 - return new EmptyResponse(404); + return HTTP::respEmpty(404); } } @@ -527,40 +533,40 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 10507 => "Fetch401", 10521 => "Fetch404", ][$e->getCode()] ?? "FetchOther"; - return new ErrorResponse($msg, 502); + return self::respError($msg, 502); } $out = []; foreach ($list as $url) { // TODO: This needs to be refined once PicoFeed is replaced $out[] = ['title' => "Feed", 'type' => "rss", 'url' => $url]; } - return new Response($out); + return HTTP::respJson($out); } protected function getUsers(): ResponseInterface { $tr = Arsse::$user->begin(); - return new Response($this->listUsers(Arsse::$user->list(), false)); + return HTTP::respJson($this->listUsers(Arsse::$user->list(), false)); } protected function getUserById(array $path): ResponseInterface { try { - return new Response($this->listUsers([$path[1]], true)[0] ?? new \stdClass); + return HTTP::respJson($this->listUsers([$path[1]], true)[0] ?? new \stdClass); } catch (UserException $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } } protected function getUserByNum(array $path): ResponseInterface { try { $user = Arsse::$user->lookup((int) $path[1]); - return new Response($this->listUsers([$user], true)[0] ?? new \stdClass); + return HTTP::respJson($this->listUsers([$user], true)[0] ?? new \stdClass); } catch (UserException $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } } protected function getCurrentUser(): ResponseInterface { - return new Response($this->listUsers([Arsse::$user->id], false)[0] ?? new \stdClass); + return HTTP::respJson($this->listUsers([Arsse::$user->id], false)[0] ?? new \stdClass); } protected function createUser(array $data): ResponseInterface { @@ -572,17 +578,17 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } catch (UserException $e) { switch ($e->getCode()) { case 10403: - return new ErrorResponse(["DuplicateUser", 'user' => $data['username']], 409); + return self::respError(["DuplicateUser", 'user' => $data['username']], 409); case 10441: - return new ErrorResponse(["InvalidInputValue", 'field' => "timezone"], 422); + return self::respError(["InvalidInputValue", 'field' => "timezone"], 422); case 10443: - return new ErrorResponse(["InvalidInputValue", 'field' => "entries_per_page"], 422); + return self::respError(["InvalidInputValue", 'field' => "entries_per_page"], 422); case 10444: - return new ErrorResponse(["InvalidInputValue", 'field' => "username"], 422); + return self::respError(["InvalidInputValue", 'field' => "username"], 422); } throw $e; // @codeCoverageIgnore } - return new Response($out, 201); + return HTTP::respJson($out, 201); } protected function updateUserByNum(array $path, array $data): ResponseInterface { @@ -591,16 +597,16 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if (((int) $path[1]) === $user['num']) { if ($data['is_admin'] && !$user['admin']) { // non-admins should not be able to set themselves as admin - return new ErrorResponse("InvalidElevation", 403); + return self::respError("InvalidElevation", 403); } $user = Arsse::$user->id; } elseif (!$user['admin']) { - return new ErrorResponse("403", 403); + return self::respError("403", 403); } else { try { $user = Arsse::$user->lookup((int) $path[1]); } catch (ExceptionConflict $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } } // make any requested changes @@ -618,26 +624,26 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } catch (UserException $e) { switch ($e->getCode()) { case 10403: - return new ErrorResponse(["DuplicateUser", 'user' => $data['username']], 409); + return self::respError(["DuplicateUser", 'user' => $data['username']], 409); case 10441: - return new ErrorResponse(["InvalidInputValue", 'field' => "timezone"], 422); + return self::respError(["InvalidInputValue", 'field' => "timezone"], 422); case 10443: - return new ErrorResponse(["InvalidInputValue", 'field' => "entries_per_page"], 422); + return self::respError(["InvalidInputValue", 'field' => "entries_per_page"], 422); case 10444: - return new ErrorResponse(["InvalidInputValue", 'field' => "username"], 422); + return self::respError(["InvalidInputValue", 'field' => "username"], 422); } throw $e; // @codeCoverageIgnore } - return new Response($out, 201); + return HTTP::respJson($out, 201); } protected function deleteUserByNum(array $path): ResponseInterface { try { Arsse::$user->remove(Arsse::$user->lookup((int) $path[1])); } catch (ExceptionConflict $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } /** Returns a useful subset of user metadata @@ -667,7 +673,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { // always add 1 to the ID since the root folder will always be 1 instead of 0. $out[] = ['id' => $f['id'] + 1, 'title' => $f['name'], 'user_id' => $meta['num']]; } - return new Response($out); + return HTTP::respJson($out); } protected function createCategory(array $data): ResponseInterface { @@ -675,13 +681,13 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $id = Arsse::$db->folderAdd(Arsse::$user->id, ['name' => (string) $data['title']]); } catch (ExceptionInput $e) { if ($e->getCode() === 10236) { - return new ErrorResponse(["DuplicateCategory", 'title' => $data['title']], 409); + return self::respError(["DuplicateCategory", 'title' => $data['title']], 409); } else { - return new ErrorResponse(["InvalidCategory", 'title' => $data['title']], 422); + return self::respError(["InvalidCategory", 'title' => $data['title']], 422); } } $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); - return new Response(['id' => $id + 1, 'title' => $data['title'], 'user_id' => $meta['num']], 201); + return HTTP::respJson(['id' => $id + 1, 'title' => $data['title'], 'user_id' => $meta['num']], 201); } protected function updateCategory(array $path, array $data): ResponseInterface { @@ -700,15 +706,15 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } catch (ExceptionInput $e) { if ($e->getCode() === 10236) { - return new ErrorResponse(["DuplicateCategory", 'title' => $title], 409); + return self::respError(["DuplicateCategory", 'title' => $title], 409); } elseif (in_array($e->getCode(), [10237, 10239])) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } else { - return new ErrorResponse(["InvalidCategory", 'title' => $title], 422); + return self::respError(["InvalidCategory", 'title' => $title], 422); } } $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); - return new Response(['id' => (int) $path[1], 'title' => $title, 'user_id' => $meta['num']], 201); + return HTTP::respJson(['id' => (int) $path[1], 'title' => $title, 'user_id' => $meta['num']], 201); } protected function deleteCategory(array $path): ResponseInterface { @@ -726,9 +732,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $tr->commit(); } } catch (ExceptionInput $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } protected function transformFeed(array $sub, int $uid, string $rootName, \DateTimeZone $tz): array { @@ -772,7 +778,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $r) { $out[] = $this->transformFeed($r, $meta['num'], $meta['root'], $meta['tz']); } - return new Response($out); + return HTTP::respJson($out); } protected function getCategoryFeeds(array $path): ResponseInterface { @@ -790,9 +796,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } catch (ExceptionInput $e) { // the folder does not exist - return new ErrorResponse("404", 404); + return self::respError("404", 404); } - return new Response($out); + return HTTP::respJson($out); } protected function getFeed(array $path): ResponseInterface { @@ -800,9 +806,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $meta = $this->userMeta(Arsse::$user->id); try { $sub = Arsse::$db->subscriptionPropertiesGet(Arsse::$user->id, (int) $path[1]); - return new Response($this->transformFeed($sub, $meta['num'], $meta['root'], $meta['tz'])); + return HTTP::respJson($this->transformFeed($sub, $meta['num'], $meta['root'], $meta['tz'])); } catch (ExceptionInput $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } } @@ -825,16 +831,16 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 10521 => "Fetch404", 10522 => "FetchFormat", ][$e->getCode()] ?? "FetchOther"; - return new ErrorResponse($msg, 502); + return self::respError($msg, 502); } catch (ExceptionInput $e) { switch ($e->getCode()) { case 10235: - return new ErrorResponse("MissingCategory", 422); + return self::respError("MissingCategory", 422); case 10236: - return new ErrorResponse("DuplicateFeed", 409); + return self::respError("DuplicateFeed", 409); } } - return new Response(['feed_id' => $id], 201); + return HTTP::respJson(['feed_id' => $id], 201); } protected function updateFeed(array $path, array $data): ResponseInterface { @@ -853,11 +859,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { switch ($e->getCode()) { case 10231: case 10232: - return new ErrorResponse("InvalidTitle", 422); + return self::respError("InvalidTitle", 422); case 10235: - return new ErrorResponse("MissingCategory", 422); + return self::respError("MissingCategory", 422); case 10239: - return new ErrorResponse("404", 404); + return self::respError("404", 404); } } return $this->getFeed($path)->withStatus(201); @@ -866,9 +872,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function deleteFeed(array $path): ResponseInterface { try { Arsse::$db->subscriptionRemove(Arsse::$user->id, (int) $path[1]); - return new EmptyResponse(204); + return HTTP::respEmpty(204); } catch (ExceptionInput $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } } @@ -876,12 +882,12 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { try { $icon = Arsse::$db->subscriptionIcon(Arsse::$user->id, (int) $path[1]); } catch (ExceptionInput $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } if (!$icon || !$icon['type'] || !$icon['data']) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } - return new Response([ + return HTTP::respJson([ 'id' => (int) $icon['id'], 'data' => $icon['type'].";base64,".base64_encode($icon['data']), 'mime_type' => $icon['type'], @@ -1038,45 +1044,45 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function getEntries(array $query): ResponseInterface { try { - return new Response($this->listEntries($query, new Context)); + return HTTP::respJson($this->listEntries($query, new Context)); } catch (ExceptionInput $e) { - return new ErrorResponse("MissingCategory", 400); + return self::respError("MissingCategory", 400); } } protected function getFeedEntries(array $path, array $query): ResponseInterface { $c = (new Context)->subscription((int) $path[1]); try { - return new Response($this->listEntries($query, $c)); + return HTTP::respJson($this->listEntries($query, $c)); } catch (ExceptionInput $e) { // FIXME: this should differentiate between a missing feed and a missing category, but doesn't - return new ErrorResponse("404", 404); + return self::respError("404", 404); } } protected function getCategoryEntries(array $path, array $query): ResponseInterface { $query['category_id'] = (int) $path[1]; try { - return new Response($this->listEntries($query, new Context)); + return HTTP::respJson($this->listEntries($query, new Context)); } catch (ExceptionInput $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } } protected function getEntry(array $path): ResponseInterface { try { - return new Response($this->findEntry((int) $path[1])); + return HTTP::respJson($this->findEntry((int) $path[1])); } catch (ExceptionInput $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } } protected function getFeedEntry(array $path): ResponseInterface { $c = (new Context)->subscription((int) $path[1]); try { - return new Response($this->findEntry((int) $path[3], $c)); + return HTTP::respJson($this->findEntry((int) $path[3], $c)); } catch (ExceptionInput $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } } @@ -1088,9 +1094,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $c->folder((int) $path[1] - 1); } try { - return new Response($this->findEntry((int) $path[3], $c)); + return HTTP::respJson($this->findEntry((int) $path[3], $c)); } catch (ExceptionInput $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } } @@ -1104,7 +1110,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } assert(isset($in), new \Exception("Unknown status specified")); Arsse::$db->articleMark(Arsse::$user->id, $in, (new Context)->articles($data['entry_ids'])); - return new EmptyResponse(204); + return HTTP::respEmpty(204); } protected function massRead(Context $c): void { @@ -1115,19 +1121,19 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { // this function is restricted to the logged-in user $user = Arsse::$user->propertiesGet(Arsse::$user->id, false); if (((int) $path[1]) !== $user['num']) { - return new ErrorResponse("403", 403); + return self::respError("403", 403); } $this->massRead(new Context); - return new EmptyResponse(204); + return HTTP::respEmpty(204); } protected function markFeed(array $path): ResponseInterface { try { $this->massRead((new Context)->subscription((int) $path[1])); } catch (ExceptionInput $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } protected function markCategory(array $path): ResponseInterface { @@ -1142,9 +1148,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { try { $this->massRead($c); } catch (ExceptionInput $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } protected function toggleEntryBookmark(array $path): ResponseInterface { @@ -1160,9 +1166,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } $tr->commit(); } catch (ExceptionInput $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } protected function refreshFeed(array $path): ResponseInterface { @@ -1170,15 +1176,15 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { try { Arsse::$db->subscriptionPropertiesGet(Arsse::$user->id, (int) $path[1]); } catch (ExceptionInput $e) { - return new ErrorResponse("404", 404); + return self::respError("404", 404); } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } protected function refreshAllFeeds(): ResponseInterface { // NOTE: This is a no-op // It could be implemented, but the need is considered low since we use a dynamic schedule always - return new EmptyResponse(204); + return HTTP::respEmpty(204); } protected function opmlImport(string $data): ResponseInterface { @@ -1187,23 +1193,23 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } catch (ImportException $e) { switch ($e->getCode()) { case 10611: - return new ErrorResponse("InvalidBodyXML", 400); + return self::respError("InvalidBodyXML", 400); case 10612: - return new ErrorResponse("InvalidBodyOPML", 422); + return self::respError("InvalidBodyOPML", 422); case 10613: - return new ErrorResponse("InvalidImportCategory", 422); + return self::respError("InvalidImportCategory", 422); case 10614: - return new ErrorResponse("DuplicateImportCategory", 422); + return self::respError("DuplicateImportCategory", 422); case 10615: - return new ErrorResponse("InvalidImportLabel", 422); + return self::respError("InvalidImportLabel", 422); } } catch (FeedException $e) { - return new ErrorResponse(["FailedImportFeed", 'url' => $e->getParams()['url'], 'code' => $e->getCode()], 502); + return self::respError(["FailedImportFeed", 'url' => $e->getParams()['url'], 'code' => $e->getCode()], 502); } - return new Response(['message' => Arsse::$lang->msg("API.Miniflux.ImportSuccess")]); + return HTTP::respJson(['message' => Arsse::$lang->msg("API.Miniflux.ImportSuccess")]); } protected function opmlExport(): ResponseInterface { - return new GenericResponse(Arsse::$obj->get(OPML::class)->export(Arsse::$user->id), 200, ['Content-Type' => "application/xml"]); + return HTTP::respText(Arsse::$obj->get(OPML::class)->export(Arsse::$user->id), 200, ['Content-Type' => "application/xml"]); } } diff --git a/lib/REST/NextcloudNews/V1_2.php b/lib/REST/NextcloudNews/V1_2.php index 7ec195cc..bfcf80f8 100644 --- a/lib/REST/NextcloudNews/V1_2.php +++ b/lib/REST/NextcloudNews/V1_2.php @@ -17,8 +17,6 @@ use JKingWeb\Arsse\Misc\HTTP; use JKingWeb\Arsse\REST\Exception; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -use Laminas\Diactoros\Response\JsonResponse as Response; -use Laminas\Diactoros\Response\EmptyResponse; class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { public const VERSION = "11.0.5"; @@ -86,19 +84,19 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { if ($req->getAttribute("authenticated", false)) { Arsse::$user->id = $req->getAttribute("authenticatedUser"); } else { - return new EmptyResponse(401); + return HTTP::respEmpty(401); } // normalize the input $data = (string) $req->getBody(); if ($data) { // if the entity body is not JSON according to content type, return "415 Unsupported Media Type" if (!HTTP::matchType($req, "", self::ACCEPTED_TYPE)) { - return new EmptyResponse(415, ['Accept' => self::ACCEPTED_TYPE]); + return HTTP::respEmpty(415, ['Accept' => self::ACCEPTED_TYPE]); } $data = @json_decode($data, true); if (json_last_error() !== \JSON_ERROR_NONE) { // if the body could not be parsed as JSON, return "400 Bad Request" - return new EmptyResponse(400); + return HTTP::respEmpty(400); } } else { $data = []; @@ -117,10 +115,10 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // @codeCoverageIgnoreStart } catch (Exception $e) { // if there was a REST exception return 400 - return new EmptyResponse(400); + return HTTP::respEmpty(400); } catch (AbstractException $e) { // if there was any other Arsse exception return 500 - return new EmptyResponse(500); + return HTTP::respEmpty(500); } // @codeCoverageIgnoreEnd } @@ -162,11 +160,11 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return $this->paths[$url][$method]; } else { // otherwise return 405 - return new EmptyResponse(405, ['Allow' => implode(", ", array_keys($this->paths[$url]))]); + return HTTP::respEmpty(405, ['Allow' => implode(", ", array_keys($this->paths[$url]))]); } } else { // if the path is not supported, return 404 - return new EmptyResponse(404); + return HTTP::respEmpty(404); } } @@ -268,13 +266,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { if (in_array("GET", $allowed)) { array_unshift($allowed, "HEAD"); } - return new EmptyResponse(204, [ + return HTTP::respEmpty(204, [ 'Allow' => implode(",", $allowed), 'Accept' => self::ACCEPTED_TYPE, ]); } else { // if the path is not supported, return 404 - return new EmptyResponse(404); + return HTTP::respEmpty(404); } } @@ -284,7 +282,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { foreach (Arsse::$db->folderList(Arsse::$user->id, null, false) as $folder) { $folders[] = $this->folderTranslate($folder); } - return new Response(['folders' => $folders]); + return HTTP::respJson(['folders' => $folders]); } // create a folder @@ -294,16 +292,16 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { } catch (ExceptionInput $e) { switch ($e->getCode()) { // folder already exists - case 10236: return new EmptyResponse(409); - // folder name not acceptable + case 10236: return HTTP::respEmpty(409); + // folder name not acceptable case 10231: - case 10232: return new EmptyResponse(422); + case 10232: return HTTP::respEmpty(422); // other errors related to input - default: return new EmptyResponse(400); // @codeCoverageIgnore + default: return HTTP::respEmpty(400); // @codeCoverageIgnore } } $folder = $this->folderTranslate(Arsse::$db->folderPropertiesGet(Arsse::$user->id, $folder)); - return new Response(['folders' => [$folder]]); + return HTTP::respJson(['folders' => [$folder]]); } // delete a folder @@ -313,9 +311,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { Arsse::$db->folderRemove(Arsse::$user->id, (int) $url[1]); } catch (ExceptionInput $e) { // folder does not exist - return new EmptyResponse(404); + return HTTP::respEmpty(404); } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } // rename a folder (also supports moving nesting folders, but this is not a feature of the API) @@ -325,24 +323,24 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { } catch (ExceptionInput $e) { switch ($e->getCode()) { // folder does not exist - case 10239: return new EmptyResponse(404); - // folder already exists - case 10236: return new EmptyResponse(409); - // folder name not acceptable + case 10239: return HTTP::respEmpty(404); + // folder already exists + case 10236: return HTTP::respEmpty(409); + // folder name not acceptable case 10231: - case 10232: return new EmptyResponse(422); + case 10232: return HTTP::respEmpty(422); // other errors related to input - default: return new EmptyResponse(400); // @codeCoverageIgnore + default: return HTTP::respEmpty(400); // @codeCoverageIgnore } } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } // mark all articles associated with a folder as read protected function folderMarkRead(array $url, array $data): ResponseInterface { if (!ValueInfo::id($data['newestItemId'])) { // if the item ID is invalid (i.e. not a positive integer), this is an error - return new EmptyResponse(422); + return HTTP::respEmpty(422); } // build the context $c = (new Context)->hidden(false); @@ -353,15 +351,15 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c); } catch (ExceptionInput $e) { // folder does not exist - return new EmptyResponse(404); + return HTTP::respEmpty(404); } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } // return list of feeds which should be refreshed protected function feedListStale(array $url, array $data): ResponseInterface { if (!$this->isAdmin()) { - return new EmptyResponse(403); + return HTTP::respEmpty(403); } // list stale feeds which should be checked for updates $feeds = Arsse::$db->feedListStale(); @@ -370,27 +368,27 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // since in our implementation feeds don't belong the users, the 'userId' field will always be an empty string $out[] = ['id' => (int) $feed, 'userId' => ""]; } - return new Response(['feeds' => $out]); + return HTTP::respJson(['feeds' => $out]); } // refresh a feed protected function feedUpdate(array $url, array $data): ResponseInterface { if (!$this->isAdmin()) { - return new EmptyResponse(403); + return HTTP::respEmpty(403); } try { Arsse::$db->feedUpdate($data['feedId']); } catch (ExceptionInput $e) { switch ($e->getCode()) { case 10239: // feed does not exist - return new EmptyResponse(404); + return HTTP::respEmpty(404); case 10237: // feed ID invalid - return new EmptyResponse(422); + return HTTP::respEmpty(422); default: // other errors related to input - return new EmptyResponse(400); // @codeCoverageIgnore + return HTTP::respEmpty(400); // @codeCoverageIgnore } } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } // add a new feed @@ -401,10 +399,10 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { $id = Arsse::$db->subscriptionAdd(Arsse::$user->id, (string) $data['url']); } catch (ExceptionInput $e) { // feed already exists - return new EmptyResponse(409); + return HTTP::respEmpty(409); } catch (FeedException $e) { // feed could not be retrieved - return new EmptyResponse(422); + return HTTP::respEmpty(422); } // if a folder was specified, move the feed to the correct folder; silently ignore errors if ($data['folderId']) { @@ -422,7 +420,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { if ($newest) { $out['newestItemId'] = $newest; } - return new Response($out); + return HTTP::respJson($out); } // return list of feeds for the logged-in user @@ -438,7 +436,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { if ($newest) { $out['newestItemId'] = $newest; } - return new Response($out); + return HTTP::respJson($out); } // delete a feed @@ -447,9 +445,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { Arsse::$db->subscriptionRemove(Arsse::$user->id, (int) $url[1]); } catch (ExceptionInput $e) { // feed does not exist - return new EmptyResponse(404); + return HTTP::respEmpty(404); } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } // rename a feed @@ -459,22 +457,22 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { } catch (ExceptionInput $e) { switch ($e->getCode()) { // subscription does not exist - case 10239: return new EmptyResponse(404); - // name is invalid + case 10239: return HTTP::respEmpty(404); + // name is invalid case 10231: - case 10232: return new EmptyResponse(422); + case 10232: return HTTP::respEmpty(422); // other errors related to input - default: return new EmptyResponse(400); // @codeCoverageIgnore + default: return HTTP::respEmpty(400); // @codeCoverageIgnore } } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } // move a feed to a folder protected function subscriptionMove(array $url, array $data): ResponseInterface { // if no folder is specified this is an error if (!isset($data['folderId'])) { - return new EmptyResponse(422); + return HTTP::respEmpty(422); } // perform the move try { @@ -482,22 +480,22 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { } catch (ExceptionInput $e) { switch ($e->getCode()) { case 10239: // subscription does not exist - return new EmptyResponse(404); + return HTTP::respEmpty(404); case 10235: // folder does not exist case 10237: // folder ID is invalid - return new EmptyResponse(422); + return HTTP::respEmpty(422); default: // other errors related to input - return new EmptyResponse(400); // @codeCoverageIgnore + return HTTP::respEmpty(400); // @codeCoverageIgnore } } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } // mark all articles associated with a subscription as read protected function subscriptionMarkRead(array $url, array $data): ResponseInterface { if (!ValueInfo::id($data['newestItemId'])) { // if the item ID is invalid (i.e. not a positive integer), this is an error - return new EmptyResponse(422); + return HTTP::respEmpty(422); } // build the context $c = (new Context)->hidden(false); @@ -508,9 +506,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c); } catch (ExceptionInput $e) { // subscription does not exist - return new EmptyResponse(404); + return HTTP::respEmpty(404); } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } // list articles and their properties @@ -579,28 +577,28 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { ], [$reverse ? "edition desc" : "edition"]); } catch (ExceptionInput $e) { // ID of subscription or folder is not valid - return new EmptyResponse(422); + return HTTP::respEmpty(422); } $out = []; foreach ($items as $item) { $out[] = $this->articleTranslate($item); } $out = ['items' => $out]; - return new Response($out); + return HTTP::respJson($out); } // mark all articles as read protected function articleMarkReadAll(array $url, array $data): ResponseInterface { if (!ValueInfo::id($data['newestItemId'])) { // if the item ID is invalid (i.e. not a positive integer), this is an error - return new EmptyResponse(422); + return HTTP::respEmpty(422); } // build the context $c = (new Context)->hidden(false); $c->editionRange(null, (int) $data['newestItemId']); // perform the operation Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c); - return new EmptyResponse(204); + return HTTP::respEmpty(204); } // mark a single article as read @@ -614,9 +612,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { Arsse::$db->articleMark(Arsse::$user->id, ['read' => $set], $c); } catch (ExceptionInput $e) { // ID is not valid - return new EmptyResponse(404); + return HTTP::respEmpty(404); } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } // mark a single article as read @@ -630,9 +628,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { Arsse::$db->articleMark(Arsse::$user->id, ['starred' => $set], $c); } catch (ExceptionInput $e) { // ID is not valid - return new EmptyResponse(404); + return HTTP::respEmpty(404); } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } // mark an array of articles as read @@ -646,7 +644,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { Arsse::$db->articleMark(Arsse::$user->id, ['read' => $set], $c); } catch (ExceptionInput $e) { } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } // mark an array of articles as starred @@ -660,11 +658,11 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { Arsse::$db->articleMark(Arsse::$user->id, ['starred' => $set], $c); } catch (ExceptionInput $e) { } - return new EmptyResponse(204); + return HTTP::respEmpty(204); } protected function userStatus(array $url, array $data): ResponseInterface { - return new Response([ + return HTTP::respJson([ 'userId' => (string) Arsse::$user->id, 'displayName' => (string) Arsse::$user->id, 'lastLoginTimestamp' => time(), @@ -674,30 +672,30 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function cleanupBefore(array $url, array $data): ResponseInterface { if (!$this->isAdmin()) { - return new EmptyResponse(403); + return HTTP::respEmpty(403); } Service::cleanupPre(); - return new EmptyResponse(204); + return HTTP::respEmpty(204); } protected function cleanupAfter(array $url, array $data): ResponseInterface { if (!$this->isAdmin()) { - return new EmptyResponse(403); + return HTTP::respEmpty(403); } Service::cleanupPost(); - return new EmptyResponse(204); + return HTTP::respEmpty(204); } // return the server version protected function serverVersion(array $url, array $data): ResponseInterface { - return new Response([ + return HTTP::respJson([ 'version' => self::VERSION, 'arsse_version' => Arsse::VERSION, ]); } protected function serverStatus(array $url, array $data): ResponseInterface { - return new Response([ + return HTTP::respJson([ 'version' => self::VERSION, 'arsse_version' => Arsse::VERSION, 'warnings' => [ diff --git a/lib/REST/NextcloudNews/Versions.php b/lib/REST/NextcloudNews/Versions.php index 95ee0bf3..0a3a6f6f 100644 --- a/lib/REST/NextcloudNews/Versions.php +++ b/lib/REST/NextcloudNews/Versions.php @@ -6,10 +6,9 @@ declare(strict_types=1); namespace JKingWeb\Arsse\REST\NextcloudNews; +use JKingWeb\Arsse\Misc\HTTP; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -use Laminas\Diactoros\Response\JsonResponse as Response; -use Laminas\Diactoros\Response\EmptyResponse; class Versions implements \JKingWeb\Arsse\REST\Handler { public function __construct() { @@ -18,12 +17,12 @@ class Versions implements \JKingWeb\Arsse\REST\Handler { public function dispatch(ServerRequestInterface $req): ResponseInterface { if (!preg_match("<^/?$>D", $req->getRequestTarget())) { // if the request path is more than an empty string or a slash, the client is probably trying a version we don't support - return new EmptyResponse(404); + return HTTP::respEmpty(404); } switch ($req->getMethod()) { case "OPTIONS": // if the request method is OPTIONS, respond accordingly - return new EmptyResponse(204, ['Allow' => "HEAD,GET"]); + return HTTP::respEmpty(204, ['Allow' => "HEAD,GET"]); case "GET": // otherwise return the supported versions $out = [ @@ -31,10 +30,10 @@ class Versions implements \JKingWeb\Arsse\REST\Handler { 'v1-2', ], ]; - return new Response($out); + return HTTP::respJson($out); default: // if any other method was used, this is an error - return new EmptyResponse(405, ['Allow' => "HEAD,GET"]); + return HTTP::respEmpty(405, ['Allow' => "HEAD,GET"]); } } } diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index e167fa4b..71261000 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -12,6 +12,7 @@ use JKingWeb\Arsse\Service; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Context\Context; use JKingWeb\Arsse\Misc\Date; +use JKingWeb\Arsse\Misc\HTTP; use JKingWeb\Arsse\Misc\ValueInfo as V; use JKingWeb\Arsse\AbstractException; use JKingWeb\Arsse\ExceptionType; @@ -20,8 +21,6 @@ use JKingWeb\Arsse\Db\ResultEmpty; use JKingWeb\Arsse\Feed\Exception as FeedException; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -use Laminas\Diactoros\Response\JsonResponse as Response; -use Laminas\Diactoros\Response\EmptyResponse; class API extends \JKingWeb\Arsse\REST\AbstractHandler { public const LEVEL = 15; // emulated API level @@ -96,11 +95,11 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { public function dispatch(ServerRequestInterface $req): ResponseInterface { if (!preg_match("<^(?:/(?:index\.php)?)?$>D", $req->getRequestTarget())) { // reject paths other than the index - return new EmptyResponse(404); + return HTTP::respEmpty(404); } if ($req->getMethod() === "OPTIONS") { // respond to OPTIONS rquests; the response is a fib, as we technically accept any type or method - return new EmptyResponse(204, [ + return HTTP::respEmpty(204, [ 'Allow' => "POST", 'Accept' => implode(", ", self::ACCEPTED_TYPES), ]); @@ -110,7 +109,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // only JSON entities are allowed, but Content-Type is ignored, as is request method $data = @json_decode($data, true); if (json_last_error() !== \JSON_ERROR_NONE || !is_array($data)) { - return new Response(self::FATAL_ERR); + return HTTP::respJson(self::FATAL_ERR); } try { // normalize input @@ -125,7 +124,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { Arsse::$user->id = $req->getAttribute("authenticatedUser"); } elseif (Arsse::$conf->userHTTPAuthRequired || Arsse::$conf->userPreAuth || $req->getAttribute("authenticationFailed", false)) { // otherwise if HTTP authentication failed or is required, deny access at the HTTP level - return new EmptyResponse(401); + return HTTP::respEmpty(401); } if (strtolower((string) $data['op']) !== "login") { // unless logging in, a session identifier is required @@ -136,23 +135,23 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // TT-RSS operations are case-insensitive by dint of PHP method names being case-insensitive; this will only trigger if the method really doesn't exist throw new Exception("UNKNOWN_METHOD", ['method' => $data['op']]); } - return new Response([ + return HTTP::respJson([ 'seq' => $data['seq'], 'status' => 0, 'content' => $this->$method($data), ]); } catch (Exception $e) { - return new Response([ + return HTTP::respJson([ 'seq' => $data['seq'], 'status' => 1, 'content' => $e->getData(), ]); } catch (AbstractException $e) { - return new EmptyResponse(500); + return HTTP::respEmpty(500); } } else { // absence of a request body indicates an error - return new Response(self::FATAL_ERR); + return HTTP::respJson(self::FATAL_ERR); } } @@ -1000,7 +999,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { switch ($e->getCode()) { case 10236: // label already exists // retrieve the ID of the existing label; duplicating a label silently returns the existing one - return $this->labelOut(Arsse::$db->labelPropertiesGet(Arsse::$user->id, $in['name'], true)['id']); + return $this->labelOut(Arsse::$db->labelPropertiesGet(Arsse::$user->id, $in['name'], true)['id']); default: // other errors related to input throw new Exception("INCORRECT_USAGE"); } diff --git a/lib/REST/TinyTinyRSS/Icon.php b/lib/REST/TinyTinyRSS/Icon.php index dd718bcb..37f2ce89 100644 --- a/lib/REST/TinyTinyRSS/Icon.php +++ b/lib/REST/TinyTinyRSS/Icon.php @@ -7,10 +7,10 @@ declare(strict_types=1); namespace JKingWeb\Arsse\REST\TinyTinyRSS; use JKingWeb\Arsse\Arsse; +use JKingWeb\Arsse\Misc\HTTP; use JKingWeb\Arsse\Db\ExceptionInput; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -use Laminas\Diactoros\Response\EmptyResponse as Response; class Icon extends \JKingWeb\Arsse\REST\AbstractHandler { public function __construct() { @@ -22,25 +22,25 @@ class Icon extends \JKingWeb\Arsse\REST\AbstractHandler { Arsse::$user->id = $req->getAttribute("authenticatedUser"); } elseif ($req->getAttribute("authenticationFailed", false) || Arsse::$conf->userHTTPAuthRequired) { // otherwise if HTTP authentication failed or did not occur when it is required, deny access at the HTTP level - return new Response(401); + return HTTP::respEmpty(401); } if ($req->getMethod() !== "GET") { // only GET requests are allowed - return new Response(405, ['Allow' => "GET"]); + return HTTP::respEmpty(405, ['Allow' => "GET"]); } elseif (!preg_match("<^(\d+)\.ico$>D", $req->getRequestTarget(), $match) || !((int) $match[1])) { - return new Response(404); + return HTTP::respEmpty(404); } try { $url = Arsse::$db->subscriptionIcon(Arsse::$user->id ?? null, (int) $match[1], false)['url'] ?? null; if (!$url) { - return new Response(404); + return HTTP::respEmpty(404); } if (($pos = strpos($url, "\r")) !== false || ($pos = strpos($url, "\n")) !== false) { $url = substr($url, 0, $pos); } - return new Response(301, ['Location' => $url]); + return HTTP::respEmpty(301, ['Location' => $url]); } catch (ExceptionInput $e) { - return new Response(404); + return HTTP::respEmpty(404); } } } diff --git a/locale/en.php b/locale/en.php index a439a45b..c9546472 100644 --- a/locale/en.php +++ b/locale/en.php @@ -34,7 +34,7 @@ return [ 'API.Miniflux.Error.InvalidTitle' => 'Invalid feed title', 'API.Miniflux.Error.InvalidImportCategory' => 'Payload contains an invalid category name', 'API.Miniflux.Error.DuplicateImportCategory' => 'Payload contains the same category name twice', - 'API.Miniflux.Error.FailedImportFeed' => 'Unable to import feed at URL "{url}" (code {code}', + 'API.Miniflux.Error.FailedImportFeed' => 'Unable to import feed at URL "{url}" (code {code})', 'API.Miniflux.Error.InvalidImportLabel' => 'Payload contains an invalid label name', 'API.TTRSS.Category.Uncategorized' => 'Uncategorized', diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 4dea4baf..15f851ee 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -19,7 +19,7 @@ trait SeriesArticle { $this->data = [ 'arsse_users' => [ 'columns' => ["id", "password", "num"], - 'rows' => [ + 'rows' => [ ["jane.doe@example.com", "", 1], ["john.doe@example.com", "", 2], ["john.doe@example.org", "", 3], @@ -29,7 +29,7 @@ trait SeriesArticle { ], 'arsse_feeds' => [ 'columns' => ["id", "url", "title"], - 'rows' => [ + 'rows' => [ [1,"http://example.com/1", "Feed 1"], [2,"http://example.com/2", "Feed 2"], [3,"http://example.com/3", "Feed 3"], @@ -47,7 +47,7 @@ trait SeriesArticle { ], 'arsse_folders' => [ 'columns' => ["id", "owner", "parent", "name"], - 'rows' => [ + 'rows' => [ [1, "john.doe@example.com", null, "Technology"], [2, "john.doe@example.com", 1, "Software"], [3, "john.doe@example.com", 1, "Rocketry"], @@ -61,7 +61,7 @@ trait SeriesArticle { ], 'arsse_tags' => [ 'columns' => ["id", "owner", "name"], - 'rows' => [ + 'rows' => [ [1, "john.doe@example.com", "Technology"], [2, "john.doe@example.com", "Software"], [3, "john.doe@example.com", "Rocketry"], @@ -74,7 +74,7 @@ trait SeriesArticle { ], 'arsse_subscriptions' => [ 'columns' => ["id", "owner", "feed", "folder", "title", "scrape"], - 'rows' => [ + 'rows' => [ [1, "john.doe@example.com",1, null,"Subscription 1", 0], [2, "john.doe@example.com",2, null,null, 0], [3, "john.doe@example.com",3, 1,"Subscription 3", 0], @@ -94,7 +94,7 @@ trait SeriesArticle { ], 'arsse_tag_members' => [ 'columns' => ["tag", "subscription", "assigned"], - 'rows' => [ + 'rows' => [ [1,3,1], [1,4,1], [2,4,1], @@ -109,8 +109,8 @@ trait SeriesArticle { ], 'arsse_articles' => [ 'columns' => [ - "id", "feed", "url", "title", "author", "published", "edited", "content", "guid", - "url_title_hash", "url_content_hash", "title_content_hash", "modified", "content_scraped" + "id", "feed", "url", "title", "author", "published", "edited", "content", "guid", + "url_title_hash", "url_content_hash", "title_content_hash", "modified", "content_scraped", ], 'rows' => [ [1,1,null,"Title one", null,null,null,"First article", null,"","","","2000-01-01T00:00:00Z",null], @@ -142,7 +142,7 @@ trait SeriesArticle { ], 'arsse_enclosures' => [ 'columns' => ["article", "url", "type"], - 'rows' => [ + 'rows' => [ [102,"http://example.com/text","text/plain"], [103,"http://example.com/video","video/webm"], [104,"http://example.com/image","image/svg+xml"], @@ -152,7 +152,7 @@ trait SeriesArticle { ], 'arsse_editions' => [ 'columns' => ["id", "article"], - 'rows' => [ + 'rows' => [ [1,1], [2,2], [3,3], @@ -188,7 +188,7 @@ trait SeriesArticle { ], 'arsse_marks' => [ 'columns' => ["subscription", "article", "read", "starred", "modified", "note", "hidden"], - 'rows' => [ + 'rows' => [ [1, 1,1,1,'2000-01-01 00:00:00','',0], [5, 19,1,0,'2016-01-01 00:00:00','',0], [5, 20,0,1,'2005-01-01 00:00:00','',0], @@ -209,7 +209,7 @@ trait SeriesArticle { ], 'arsse_categories' => [ // author-supplied categories 'columns' => ["article", "name"], - 'rows' => [ + 'rows' => [ [19,"Fascinating"], [19,"Logical"], [20,"Interesting"], @@ -218,7 +218,7 @@ trait SeriesArticle { ], 'arsse_labels' => [ // labels applied to articles 'columns' => ["id", "owner", "name"], - 'rows' => [ + 'rows' => [ [1,"john.doe@example.com","Interesting"], [2,"john.doe@example.com","Fascinating"], [3,"jane.doe@example.com","Boring"], @@ -227,7 +227,7 @@ trait SeriesArticle { ], 'arsse_label_members' => [ 'columns' => ["label", "article", "subscription", "assigned", "modified"], - 'rows' => [ + 'rows' => [ [1, 1,1,1,'2000-01-01 00:00:00'], [2, 1,1,1,'2000-01-01 00:00:00'], [1,19,5,1,'2000-01-01 00:00:00'], diff --git a/tests/cases/Database/SeriesCleanup.php b/tests/cases/Database/SeriesCleanup.php index 8850ecd2..483edfd1 100644 --- a/tests/cases/Database/SeriesCleanup.php +++ b/tests/cases/Database/SeriesCleanup.php @@ -28,14 +28,14 @@ trait SeriesCleanup { $this->data = [ 'arsse_users' => [ 'columns' => ["id", "password", "num"], - 'rows' => [ + 'rows' => [ ["jane.doe@example.com", "",1], ["john.doe@example.com", "",2], ], ], 'arsse_sessions' => [ 'columns' => ["id", "created", "expires", "user"], - 'rows' => [ + 'rows' => [ ["a", $nowish, $faroff, "jane.doe@example.com"], // not expired and recently created, thus kept ["b", $nowish, $soon, "jane.doe@example.com"], // not expired and recently created, thus kept ["c", $daysago, $soon, "jane.doe@example.com"], // created more than a day ago, thus deleted @@ -45,7 +45,7 @@ trait SeriesCleanup { ], 'arsse_tokens' => [ 'columns' => ["id", "class", "user", "expires"], - 'rows' => [ + 'rows' => [ ["80fa94c1a11f11e78667001e673b2560", "fever.login", "jane.doe@example.com", $faroff], ["27c6de8da13311e78667001e673b2560", "fever.login", "jane.doe@example.com", $weeksago], // expired ["ab3b3eb8a13311e78667001e673b2560", "class.class", "jane.doe@example.com", null], @@ -54,7 +54,7 @@ trait SeriesCleanup { ], 'arsse_icons' => [ 'columns' => ["id", "url", "orphaned"], - 'rows' => [ + 'rows' => [ [1,'http://localhost:8000/Icon/PNG',$daybefore], [2,'http://localhost:8000/Icon/GIF',$daybefore], [3,'http://localhost:8000/Icon/SVG1',null], @@ -62,7 +62,7 @@ trait SeriesCleanup { ], 'arsse_feeds' => [ 'columns' => ["id", "url", "title", "orphaned", "size", "icon"], - 'rows' => [ + 'rows' => [ [1,"http://example.com/1","",$daybefore,2,null], //latest two articles should be kept [2,"http://example.com/2","",$yesterday,0,2], [3,"http://example.com/3","",null,0,1], @@ -71,7 +71,7 @@ trait SeriesCleanup { ], 'arsse_subscriptions' => [ 'columns' => ["id", "owner", "feed"], - 'rows' => [ + 'rows' => [ // one feed previously marked for deletion has a subscription again, and so should not be deleted [1,'jane.doe@example.com',1], // other subscriptions exist for article cleanup tests @@ -80,7 +80,7 @@ trait SeriesCleanup { ], 'arsse_articles' => [ 'columns' => ["id", "feed", "url_title_hash", "url_content_hash", "title_content_hash", "modified"], - 'rows' => [ + 'rows' => [ [1,1,"","","",$weeksago], // is the latest article, thus is kept [2,1,"","","",$weeksago], // is the second latest article, thus is kept [3,1,"","","",$weeksago], // is starred by one user, thus is kept @@ -94,7 +94,7 @@ trait SeriesCleanup { ], 'arsse_editions' => [ 'columns' => ["id", "article"], - 'rows' => [ + 'rows' => [ [1,1], [2,2], [3,3], @@ -105,7 +105,7 @@ trait SeriesCleanup { ], 'arsse_marks' => [ 'columns' => ["article", "subscription", "read", "starred", "hidden", "modified"], - 'rows' => [ + 'rows' => [ [3,1,0,1,0,$weeksago], [4,1,1,0,0,$daysago], [6,1,1,0,0,$nowish], diff --git a/tests/cases/Database/SeriesFeed.php b/tests/cases/Database/SeriesFeed.php index 67fb77a6..2a428cf3 100644 --- a/tests/cases/Database/SeriesFeed.php +++ b/tests/cases/Database/SeriesFeed.php @@ -18,14 +18,14 @@ trait SeriesFeed { $this->data = [ 'arsse_users' => [ 'columns' => ["id", "password", "num"], - 'rows' => [ + 'rows' => [ ["jane.doe@example.com", "",1], ["john.doe@example.com", "",2], ], ], 'arsse_icons' => [ 'columns' => ["id", "url", "type", "data"], - 'rows' => [ + 'rows' => [ [1,'http://localhost:8000/Icon/PNG','image/png',base64_decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAADUlEQVQYV2NgYGBgAAAABQABijPjAAAAAABJRU5ErkJggg==")], [2,'http://localhost:8000/Icon/GIF','image/gif',base64_decode("R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==")], // this actually contains the data of SVG2, which will lead to a row update when retieved @@ -34,7 +34,7 @@ trait SeriesFeed { ], 'arsse_feeds' => [ 'columns' => ["id", "url", "title", "err_count", "err_msg", "modified", "next_fetch", "size", "icon"], - 'rows' => [ + 'rows' => [ [1,"http://localhost:8000/Feed/Matching/3","Ook",0,"",$past,$past,0,null], [2,"http://localhost:8000/Feed/Matching/1","Eek",5,"There was an error last time",$past,$future,0,null], [3,"http://localhost:8000/Feed/Fetching/Error?code=404","Ack",0,"",$past,$now,0,null], @@ -49,7 +49,7 @@ trait SeriesFeed { ], 'arsse_subscriptions' => [ 'columns' => ["id", "owner", "feed", "keep_rule", "block_rule"], - 'rows' => [ + 'rows' => [ [1,'john.doe@example.com',1,null,'^Sport$'], [2,'john.doe@example.com',2,"",null], [3,'john.doe@example.com',3,'\w+',null], @@ -60,7 +60,7 @@ trait SeriesFeed { ], 'arsse_articles' => [ 'columns' => ["id", "feed", "url", "title", "author", "published", "edited", "content", "guid", "url_title_hash", "url_content_hash", "title_content_hash", "modified"], - 'rows' => [ + 'rows' => [ [1,1,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:00','
Article content 1
','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207',$past], [2,1,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:00','Article content 2
','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e',$past], [3,1,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:00','Article content 3
','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b',$past], @@ -72,7 +72,7 @@ trait SeriesFeed { ], 'arsse_editions' => [ 'columns' => ["id", "article", "modified"], - 'rows' => [ + 'rows' => [ [1,1,$past], [2,2,$past], [3,3,$past], @@ -82,7 +82,7 @@ trait SeriesFeed { ], 'arsse_marks' => [ 'columns' => ["article", "subscription", "read", "starred", "hidden", "modified"], - 'rows' => [ + 'rows' => [ // Jane's marks [1,6,1,0,0,$past], [2,6,1,0,0,$past], @@ -97,13 +97,13 @@ trait SeriesFeed { ], 'arsse_enclosures' => [ 'columns' => ["article", "url", "type"], - 'rows' => [ + 'rows' => [ [7,'http://example.com/png','image/png'], ], ], 'arsse_categories' => [ 'columns' => ["article", "name"], - 'rows' => [ + 'rows' => [ [7,'Syrinx'], ], ], diff --git a/tests/cases/Database/SeriesFolder.php b/tests/cases/Database/SeriesFolder.php index 4e0eec4b..9c3147f5 100644 --- a/tests/cases/Database/SeriesFolder.php +++ b/tests/cases/Database/SeriesFolder.php @@ -13,7 +13,7 @@ trait SeriesFolder { $this->data = [ 'arsse_users' => [ 'columns' => ["id", "password", "num"], - 'rows' => [ + 'rows' => [ ["jane.doe@example.com", "",1], ["john.doe@example.com", "",2], ], @@ -41,7 +41,7 @@ trait SeriesFolder { ], 'arsse_feeds' => [ 'columns' => ["id", "url", "title"], - 'rows' => [ + 'rows' => [ [1,"http://example.com/1", "Feed 1"], [2,"http://example.com/2", "Feed 2"], [3,"http://example.com/3", "Feed 3"], @@ -59,7 +59,7 @@ trait SeriesFolder { ], 'arsse_subscriptions' => [ 'columns' => ["id", "owner", "feed", "folder"], - 'rows' => [ + 'rows' => [ [1, "john.doe@example.com",1, null], [2, "john.doe@example.com",2, null], [3, "john.doe@example.com",3, 1], diff --git a/tests/cases/Database/SeriesIcon.php b/tests/cases/Database/SeriesIcon.php index 73b6cf4e..58d43780 100644 --- a/tests/cases/Database/SeriesIcon.php +++ b/tests/cases/Database/SeriesIcon.php @@ -17,14 +17,14 @@ trait SeriesIcon { $this->data = [ 'arsse_users' => [ 'columns' => ["id", "password", "num"], - 'rows' => [ + 'rows' => [ ["jane.doe@example.com", "",1], ["john.doe@example.com", "",2], ], ], 'arsse_icons' => [ 'columns' => ["id", "url", "type", "data"], - 'rows' => [ + 'rows' => [ [1,'http://localhost:8000/Icon/PNG','image/png',base64_decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAADUlEQVQYV2NgYGBgAAAABQABijPjAAAAAABJRU5ErkJggg==")], [2,'http://localhost:8000/Icon/GIF','image/gif',base64_decode("R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==")], [3,'http://localhost:8000/Icon/SVG1','image/svg+xml',''], @@ -33,7 +33,7 @@ trait SeriesIcon { ], 'arsse_feeds' => [ 'columns' => ["id", "url", "title", "err_count", "err_msg", "modified", "next_fetch", "size", "icon"], - 'rows' => [ + 'rows' => [ [1,"http://localhost:8000/Feed/Matching/3","Ook",0,"",$past,$past,0,1], [2,"http://localhost:8000/Feed/Matching/1","Eek",5,"There was an error last time",$past,$future,0,2], [3,"http://localhost:8000/Feed/Fetching/Error?code=404","Ack",0,"",$past,$now,0,3], @@ -43,7 +43,7 @@ trait SeriesIcon { ], 'arsse_subscriptions' => [ 'columns' => ["id", "owner", "feed"], - 'rows' => [ + 'rows' => [ [1,'john.doe@example.com',1], [2,'john.doe@example.com',2], [3,'john.doe@example.com',3], diff --git a/tests/cases/Database/SeriesLabel.php b/tests/cases/Database/SeriesLabel.php index d4ffa721..72d3281b 100644 --- a/tests/cases/Database/SeriesLabel.php +++ b/tests/cases/Database/SeriesLabel.php @@ -15,7 +15,7 @@ trait SeriesLabel { $this->data = [ 'arsse_users' => [ 'columns' => ["id", "password", "num"], - 'rows' => [ + 'rows' => [ ["jane.doe@example.com", "",1], ["john.doe@example.com", "",2], ["john.doe@example.org", "",3], @@ -24,7 +24,7 @@ trait SeriesLabel { ], 'arsse_folders' => [ 'columns' => ["id", "owner", "parent", "name"], - 'rows' => [ + 'rows' => [ [1, "john.doe@example.com", null, "Technology"], [2, "john.doe@example.com", 1, "Software"], [3, "john.doe@example.com", 1, "Rocketry"], @@ -38,7 +38,7 @@ trait SeriesLabel { ], 'arsse_feeds' => [ 'columns' => ["id", "url"], - 'rows' => [ + 'rows' => [ [1,"http://example.com/1"], [2,"http://example.com/2"], [3,"http://example.com/3"], @@ -56,7 +56,7 @@ trait SeriesLabel { ], 'arsse_subscriptions' => [ 'columns' => ["id", "owner", "feed", "folder"], - 'rows' => [ + 'rows' => [ [1,"john.doe@example.com",1,null], [2,"john.doe@example.com",2,null], [3,"john.doe@example.com",3,1], @@ -75,7 +75,7 @@ trait SeriesLabel { ], 'arsse_articles' => [ 'columns' => ["id", "feed", "url", "title", "author", "published", "edited", "content", "guid", "url_title_hash", "url_content_hash", "title_content_hash", "modified"], - 'rows' => [ + 'rows' => [ [1,1,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], [2,1,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], [3,2,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], @@ -105,7 +105,7 @@ trait SeriesLabel { ], 'arsse_enclosures' => [ 'columns' => ["article", "url", "type"], - 'rows' => [ + 'rows' => [ [102,"http://example.com/text","text/plain"], [103,"http://example.com/video","video/webm"], [104,"http://example.com/image","image/svg+xml"], @@ -115,7 +115,7 @@ trait SeriesLabel { ], 'arsse_editions' => [ 'columns' => ["id", "article"], - 'rows' => [ + 'rows' => [ [1,1], [2,2], [3,3], @@ -151,7 +151,7 @@ trait SeriesLabel { ], 'arsse_marks' => [ 'columns' => ["subscription", "article", "read", "starred", "modified", "hidden"], - 'rows' => [ + 'rows' => [ [1, 1,1,1,'2000-01-01 00:00:00',0], [5, 19,1,0,'2000-01-01 00:00:00',0], [5, 20,0,1,'2010-01-01 00:00:00',0], @@ -169,7 +169,7 @@ trait SeriesLabel { ], 'arsse_labels' => [ 'columns' => ["id", "owner", "name"], - 'rows' => [ + 'rows' => [ [1,"john.doe@example.com","Interesting"], [2,"john.doe@example.com","Fascinating"], [3,"jane.doe@example.com","Boring"], @@ -178,7 +178,7 @@ trait SeriesLabel { ], 'arsse_label_members' => [ 'columns' => ["label", "article", "subscription", "assigned"], - 'rows' => [ + 'rows' => [ [1, 1,1,1], [2, 1,1,1], [1,19,5,1], diff --git a/tests/cases/Database/SeriesMeta.php b/tests/cases/Database/SeriesMeta.php index aeac6b79..9f25f698 100644 --- a/tests/cases/Database/SeriesMeta.php +++ b/tests/cases/Database/SeriesMeta.php @@ -14,7 +14,7 @@ trait SeriesMeta { $dataBare = [ 'arsse_meta' => [ 'columns' => ["key", "value"], - 'rows' => [ + 'rows' => [ //['schema_version', "".\JKingWeb\Arsse\Database::SCHEMA_VERSION], ['album',"A Farewell to Kings"], ], diff --git a/tests/cases/Database/SeriesSession.php b/tests/cases/Database/SeriesSession.php index ffbba56e..b025b922 100644 --- a/tests/cases/Database/SeriesSession.php +++ b/tests/cases/Database/SeriesSession.php @@ -24,14 +24,14 @@ trait SeriesSession { $this->data = [ 'arsse_users' => [ 'columns' => ["id", "password", "num"], - 'rows' => [ + 'rows' => [ ["jane.doe@example.com", "",1], ["john.doe@example.com", "",2], ], ], 'arsse_sessions' => [ 'columns' => ["id", "user", "created", "expires"], - 'rows' => [ + 'rows' => [ ["80fa94c1a11f11e78667001e673b2560", "jane.doe@example.com", $past, $faroff], ["27c6de8da13311e78667001e673b2560", "jane.doe@example.com", $past, $past], // expired ["ab3b3eb8a13311e78667001e673b2560", "jane.doe@example.com", $old, $future], // too old diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index 0b279702..4b3143b9 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -16,7 +16,7 @@ trait SeriesSubscription { $this->data = [ 'arsse_users' => [ 'columns' => ["id", "password", "num"], - 'rows' => [ + 'rows' => [ ["jane.doe@example.com", "", 1], ["john.doe@example.com", "", 2], ["jill.doe@example.com", "", 3], @@ -25,7 +25,7 @@ trait SeriesSubscription { ], 'arsse_folders' => [ 'columns' => ["id", "owner", "parent", "name"], - 'rows' => [ + 'rows' => [ [1, "john.doe@example.com", null, "Technology"], [2, "john.doe@example.com", 1, "Software"], [3, "john.doe@example.com", 1, "Rocketry"], @@ -36,14 +36,14 @@ trait SeriesSubscription { ], 'arsse_icons' => [ 'columns' => ["id", "url", "data"], - 'rows' => [ + 'rows' => [ [1,"http://example.com/favicon.ico", "ICON DATA"], [2,"http://example.net/favicon.ico", null], ], ], 'arsse_feeds' => [ 'columns' => ["id", "url", "title", "username", "password", "updated", "next_fetch", "icon"], - 'rows' => [ + 'rows' => [ [1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),strtotime("now"),null], [2,"http://example.com/feed2", "eek", "", "",strtotime("now - 1 hour"),strtotime("now - 1 hour"),1], [3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),strtotime("now + 1 hour"),2], @@ -52,7 +52,7 @@ trait SeriesSubscription { ], 'arsse_subscriptions' => [ 'columns' => ["id", "owner", "feed", "title", "folder", "pinned", "order_type", "keep_rule", "block_rule", "scrape"], - 'rows' => [ + 'rows' => [ [1,"john.doe@example.com",2,null,null,1,2,null,null,0], [2,"jane.doe@example.com",2,null,null,0,0,null,null,0], [3,"john.doe@example.com",3,"Ook",2,0,1,null,null,0], @@ -63,7 +63,7 @@ trait SeriesSubscription { ], 'arsse_tags' => [ 'columns' => ["id", "owner", "name"], - 'rows' => [ + 'rows' => [ [1,"john.doe@example.com","Interesting"], [2,"john.doe@example.com","Fascinating"], [3,"jane.doe@example.com","Boring"], @@ -72,7 +72,7 @@ trait SeriesSubscription { ], 'arsse_tag_members' => [ 'columns' => ["tag", "subscription", "assigned"], - 'rows' => [ + 'rows' => [ [1,1,1], [1,3,0], [2,1,1], @@ -82,7 +82,7 @@ trait SeriesSubscription { ], 'arsse_articles' => [ 'columns' => ["id", "feed", "url_title_hash", "url_content_hash", "title_content_hash", "title"], - 'rows' => [ + 'rows' => [ [1,2,"","","","Title 1"], [2,2,"","","","Title 2"], [3,2,"","","","Title 3"], @@ -95,7 +95,7 @@ trait SeriesSubscription { ], 'arsse_editions' => [ 'columns' => ["id", "article"], - 'rows' => [ + 'rows' => [ [1,1], [2,2], [3,3], @@ -108,7 +108,7 @@ trait SeriesSubscription { ], 'arsse_categories' => [ 'columns' => ["article", "name"], - 'rows' => [ + 'rows' => [ [1,"A"], [2,"B"], [4,"D"], @@ -120,7 +120,7 @@ trait SeriesSubscription { ], 'arsse_marks' => [ 'columns' => ["article", "subscription", "read", "starred", "hidden"], - 'rows' => [ + 'rows' => [ [1,2,1,0,0], [2,2,1,0,0], [3,2,1,0,0], diff --git a/tests/cases/Database/SeriesTag.php b/tests/cases/Database/SeriesTag.php index 47c9fa7c..ef695706 100644 --- a/tests/cases/Database/SeriesTag.php +++ b/tests/cases/Database/SeriesTag.php @@ -14,7 +14,7 @@ trait SeriesTag { $this->data = [ 'arsse_users' => [ 'columns' => ["id", "password", "num"], - 'rows' => [ + 'rows' => [ ["jane.doe@example.com", "",1], ["john.doe@example.com", "",2], ["john.doe@example.org", "",3], @@ -23,7 +23,7 @@ trait SeriesTag { ], 'arsse_feeds' => [ 'columns' => ["id", "url", "title"], - 'rows' => [ + 'rows' => [ [1,"http://example.com/1",""], [2,"http://example.com/2",""], [3,"http://example.com/3","Feed Title"], @@ -41,7 +41,7 @@ trait SeriesTag { ], 'arsse_subscriptions' => [ 'columns' => ["id", "owner", "feed", "title"], - 'rows' => [ + 'rows' => [ [1, "john.doe@example.com", 1,"Lord of Carrots"], [2, "john.doe@example.com", 2,null], [3, "john.doe@example.com", 3,"Subscription Title"], @@ -60,7 +60,7 @@ trait SeriesTag { ], 'arsse_tags' => [ 'columns' => ["id", "owner", "name"], - 'rows' => [ + 'rows' => [ [1,"john.doe@example.com","Interesting"], [2,"john.doe@example.com","Fascinating"], [3,"jane.doe@example.com","Boring"], @@ -69,7 +69,7 @@ trait SeriesTag { ], 'arsse_tag_members' => [ 'columns' => ["tag", "subscription", "assigned"], - 'rows' => [ + 'rows' => [ [1,1,1], [1,3,0], [1,5,1], diff --git a/tests/cases/Database/SeriesToken.php b/tests/cases/Database/SeriesToken.php index ab90244e..6d2fb6e8 100644 --- a/tests/cases/Database/SeriesToken.php +++ b/tests/cases/Database/SeriesToken.php @@ -18,14 +18,14 @@ trait SeriesToken { $this->data = [ 'arsse_users' => [ 'columns' => ["id", "password", "num"], - 'rows' => [ + 'rows' => [ ["jane.doe@example.com", "",1], ["john.doe@example.com", "",2], ], ], 'arsse_tokens' => [ 'columns' => ["id", "class", "user", "expires", "data"], - 'rows' => [ + 'rows' => [ ["80fa94c1a11f11e78667001e673b2560", "fever.login", "jane.doe@example.com", $faroff, null], ["27c6de8da13311e78667001e673b2560", "fever.login", "jane.doe@example.com", $past, null], // expired ["ab3b3eb8a13311e78667001e673b2560", "class.class", "jane.doe@example.com", null, null], diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index 2caba860..0053d7a1 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -13,7 +13,7 @@ trait SeriesUser { $this->data = [ 'arsse_users' => [ 'columns' => ["id", "password", "num", "admin"], - 'rows' => [ + 'rows' => [ ["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW', 1, 1], // password is hash of "secret" ["jane.doe@example.com", "", 2, 0], ["john.doe@example.com", "", 3, 0], @@ -21,7 +21,7 @@ trait SeriesUser { ], 'arsse_user_meta' => [ 'columns' => ["owner", "key", "value"], - 'rows' => [ + 'rows' => [ ["admin@example.net", "lang", "en"], ["admin@example.net", "tz", "America/Toronto"], ["admin@example.net", "sort_asc", "0"], diff --git a/tests/cases/ImportExport/TestImportExport.php b/tests/cases/ImportExport/TestImportExport.php index a6d6194b..e6eb36ee 100644 --- a/tests/cases/ImportExport/TestImportExport.php +++ b/tests/cases/ImportExport/TestImportExport.php @@ -42,14 +42,14 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest { $this->data = [ 'arsse_users' => [ 'columns' => ["id", "password", "num"], - 'rows' => [ + 'rows' => [ ["john.doe@example.com", "", 1], ["jane.doe@example.com", "", 2], ], ], 'arsse_folders' => [ 'columns' => ["id", "owner", "parent", "name"], - 'rows' => [ + 'rows' => [ [1, "john.doe@example.com", null, "Science"], [2, "john.doe@example.com", 1, "Rocketry"], [3, "john.doe@example.com", null, "Politics"], @@ -60,7 +60,7 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest { ], 'arsse_feeds' => [ 'columns' => ["id", "url", "title"], - 'rows' => [ + 'rows' => [ [1, "http://localhost:8000/Import/nasa-jpl", "NASA JPL"], [2, "http://localhost:8000/Import/torstar", "Toronto Star"], [3, "http://localhost:8000/Import/ars", "Ars Technica"], @@ -71,7 +71,7 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest { ], 'arsse_subscriptions' => [ 'columns' => ["id", "owner", "folder", "feed", "title"], - 'rows' => [ + 'rows' => [ [1, "john.doe@example.com", 2, 1, "NASA JPL"], [2, "john.doe@example.com", 5, 2, "Toronto Star"], [3, "john.doe@example.com", 1, 3, "Ars Technica"], @@ -82,7 +82,7 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest { ], 'arsse_tags' => [ 'columns' => ["id", "owner", "name"], - 'rows' => [ + 'rows' => [ [1, "john.doe@example.com", "canada"], [2, "john.doe@example.com", "frequent"], [3, "john.doe@example.com", "gaming"], @@ -93,7 +93,7 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest { ], 'arsse_tag_members' => [ 'columns' => ["tag", "subscription", "assigned"], - 'rows' => [ + 'rows' => [ [1, 2, 1], [1, 4, 1], [1, 5, 1], diff --git a/tests/cases/Misc/TestHTTP.php b/tests/cases/Misc/TestHTTP.php index 9bffe073..fee9a67a 100644 --- a/tests/cases/Misc/TestHTTP.php +++ b/tests/cases/Misc/TestHTTP.php @@ -7,14 +7,17 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Misc; use JKingWeb\Arsse\Misc\HTTP; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Response; +use Psr\Http\Message\ResponseInterface; /** @covers \JKingWeb\Arsse\Misc\HTTP */ class TestHTTP extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideMediaTypes */ public function testMatchMediaType(string $header, array $types, bool $exp): void { - $msg = (new \Laminas\Diactoros\Request)->withHeader("Content-Type", $header); + $msg = (new Request("POST", "/"))->withHeader("Content-Type", $header); $this->assertSame($exp, HTTP::matchType($msg, ...$types)); - $msg = (new \Laminas\Diactoros\Response)->withHeader("Content-Type", $header); + $msg = (new Response)->withHeader("Content-Type", $header); $this->assertSame($exp, HTTP::matchType($msg, ...$types)); } @@ -27,6 +30,26 @@ class TestHTTP extends \JKingWeb\Arsse\Test\AbstractTest { ["", ["application/json"], false], ["", ["application/json", ""], true], ["application/json ;", ["application/json"], true], + ["application/feed+json", ["application/json", "+json"], true], + ["application/xhtml+xml", ["application/json", "+json"], false], + ]; + } + + /** @dataProvider provideTypedMessages */ + public function testCreateResponses(string $type, array $params, ResponseInterface $exp): void { + $act = call_user_func(["JKingWeb\\Arsse\\Misc\\HTTP", $type], ...$params); + $this->assertMessage($exp, $act); + } + + public function provideTypedMessages(): iterable { + return [ + ["respEmpty", [422, ['Content-Length' => "0"]], new Response(422, ['Content-Length' => "0"])], + ["respText", ["OOK"], new Response(200, ['Content-Type' => "text/plain; charset=UTF-8"], "OOK")], + ["respText", ["OOK", 201, ['Content-Type' => "application/octet-stream"]], new Response(201, ['Content-Type' => "application/octet-stream"], "OOK")], + ["respJson", [['ook' => "eek"]], new Response(200, ['Content-Type' => "application/json"], '{"ook":"eek"}')], + ["respJson", [['ook' => "eek"], 400, ['Content-Type' => "application/feed+json"]], new Response(400, ['Content-Type' => "application/feed+json"], '{"ook":"eek"}')], + ["respXml", ["