Browse Source

Merge branch 'master' into dbtest

dbtest
J. King 2 years ago
parent
commit
a77b47cd59
  1. 3
      CHANGELOG
  2. 2
      RoboFile.php
  3. 7
      UPGRADING
  4. 2
      composer.json
  5. 240
      composer.lock
  6. 2
      lib/Arsse.php
  7. 4
      lib/Db/MySQL/ExceptionBuilder.php
  8. 27
      lib/Misc/HTTP.php
  9. 10
      lib/REST.php
  10. 17
      lib/REST/Fever/API.php
  11. 19
      lib/REST/Miniflux/ErrorResponse.php
  12. 11
      lib/REST/Miniflux/Status.php
  13. 216
      lib/REST/Miniflux/V1.php
  14. 142
      lib/REST/NextcloudNews/V1_2.php
  15. 11
      lib/REST/NextcloudNews/Versions.php
  16. 21
      lib/REST/TinyTinyRSS/API.php
  17. 14
      lib/REST/TinyTinyRSS/Icon.php
  18. 2
      locale/en.php
  19. 28
      tests/cases/Database/SeriesArticle.php
  20. 18
      tests/cases/Database/SeriesCleanup.php
  21. 18
      tests/cases/Database/SeriesFeed.php
  22. 6
      tests/cases/Database/SeriesFolder.php
  23. 8
      tests/cases/Database/SeriesIcon.php
  24. 20
      tests/cases/Database/SeriesLabel.php
  25. 2
      tests/cases/Database/SeriesMeta.php
  26. 4
      tests/cases/Database/SeriesSession.php
  27. 22
      tests/cases/Database/SeriesSubscription.php
  28. 10
      tests/cases/Database/SeriesTag.php
  29. 4
      tests/cases/Database/SeriesToken.php
  30. 4
      tests/cases/Database/SeriesUser.php
  31. 12
      tests/cases/ImportExport/TestImportExport.php
  32. 27
      tests/cases/Misc/TestHTTP.php
  33. 50
      tests/cases/REST/Fever/TestAPI.php
  34. 22
      tests/cases/REST/Miniflux/TestErrorResponse.php
  35. 17
      tests/cases/REST/Miniflux/TestStatus.php
  36. 463
      tests/cases/REST/Miniflux/TestV1.php
  37. 149
      tests/cases/REST/NextcloudNews/TestV1_2.php
  38. 11
      tests/cases/REST/NextcloudNews/TestVersions.php
  39. 69
      tests/cases/REST/TestREST.php
  40. 57
      tests/cases/REST/TinyTinyRSS/TestAPI.php
  41. 24
      tests/cases/REST/TinyTinyRSS/TestIcon.php
  42. 36
      tests/lib/AbstractTest.php
  43. 1
      tests/phpunit.dist.xml
  44. 313
      vendor-bin/csfixer/composer.lock
  45. 277
      vendor-bin/daux/composer.lock
  46. 460
      vendor-bin/phpunit/composer.lock
  47. 172
      vendor-bin/robo/composer.lock

3
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)
===========================

2
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;
}
}

7
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
=============================

2
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": {

240
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": [

2
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",

4
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

27
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);
}
}

10
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")) {

17
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);
}
}

19
lib/REST/Miniflux/ErrorResponse.php

@ -1,19 +0,0 @@
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\REST\Miniflux;
use JKingWeb\Arsse\Arsse;
class ErrorResponse extends \Laminas\Diactoros\Response\JsonResponse {
public function __construct($data, int $status = 400, array $headers = [], int $encodingOptions = self::DEFAULT_JSON_FLAGS) {
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)];
parent::__construct($data, $status, $headers, $encodingOptions);
}
}

11
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);
}
}

216
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"]);
}
}

142
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' => [

11
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"]);
}
}
}

21
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");
}

14
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);
}
}
}

2
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',

28
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'],

18
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],

18
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','<p>Article content 1</p>','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','<p>Article content 2</p>','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','<p>Article content 3</p>','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'],
],
],

6
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],

8
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','<svg xmlns="http://www.w3.org/2000/svg" width="900" height="600"><rect fill="#fff" height="600" width="900"/><circle fill="#bc002d" cx="450" cy="300" r="180"/></svg>'],
@ -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],

20
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],

2
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"],
],

4
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

22
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],

10
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],

4
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],

4
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"],

12
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],

27
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", ["<html/>"], new Response(200, ['Content-Type' => "application/xml; charset=UTF-8"], "<html/>")],
["respXml", ["<html/>", 451, ['Content-Type' => "text/plain", 'Vary' => "ETag"]], new Response(451, ['Content-Type' => "text/plain", 'Vary' => "ETag"], "<html/>")],
];
}
}

50
tests/cases/REST/Fever/TestAPI.php

@ -9,15 +9,13 @@ namespace JKingWeb\Arsse\TestCase\REST\Fever;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\User;
use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Misc\HTTP;
use JKingWeb\Arsse\Test\Result;
use JKingWeb\Arsse\Context\Context;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\Transaction;
use JKingWeb\Arsse\REST\Fever\API;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\Response\XmlResponse;
use Laminas\Diactoros\Response\EmptyResponse;
/** @covers \JKingWeb\Arsse\REST\Fever\API<extended> */
class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
@ -192,9 +190,9 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
}
public function provideTokenAuthenticationRequests(): iterable {
$success = new JsonResponse(['auth' => 1]);
$failure = new JsonResponse(['auth' => 0]);
$denied = new EmptyResponse(401);
$success = HTTP::respJson(['auth' => 1]);
$failure = HTTP::respJson(['auth' => 0]);
$denied = HTTP::respEmpty(401);
return [
[false, true, null, [], ['api' => null], $failure],
[false, false, null, [], ['api' => null], $failure],
@ -255,7 +253,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
['id' => 2, 'name' => "Interesting", 'subscription' => 1],
['id' => 2, 'name' => "Interesting", 'subscription' => 3],
]));
$exp = new JsonResponse([
$exp = HTTP::respJson([
'groups' => [
['id' => 1, 'title' => "Fascinating"],
['id' => 2, 'title' => "Interesting"],
@ -281,7 +279,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
['id' => 2, 'name' => "Interesting", 'subscription' => 1],
['id' => 2, 'name' => "Interesting", 'subscription' => 3],
]));
$exp = new JsonResponse([
$exp = HTTP::respJson([
'feeds' => [
['id' => 1, 'favicon_id' => 42, 'title' => "Ankh-Morpork News", 'url' => "http://example.com/feed", 'site_url' => "http://example.com/", 'is_spark' => 0, 'last_updated_on_time' => strtotime("2019-01-01T21:12:00Z")],
['id' => 2, 'favicon_id' => 0, 'title' => "Ook, Ook Eek Ook!", 'url' => "http://example.net/feed", 'site_url' => "http://example.net/", 'is_spark' => 0, 'last_updated_on_time' => strtotime("1988-06-24T12:21:00Z")],
@ -301,7 +299,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
$order = [$desc ? "id desc" : "id"];
$this->dbMock->articleList->returns(new Result($this->articles['db']));
$this->dbMock->articleCount->with($this->userId, (new Context)->hidden(false))->returns(1024);
$exp = new JsonResponse([
$exp = HTTP::respJson([
'items' => $this->articles['rest'],
'total_items' => 1024,
]);
@ -330,15 +328,15 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
$unread = [['id' => 4],['id' => 5],['id' => 6]];
$this->dbMock->articleList->with($this->userId, (new Context)->starred(true)->hidden(false))->returns(new Result($saved));
$this->dbMock->articleList->with($this->userId, (new Context)->unread(true)->hidden(false))->returns(new Result($unread));
$exp = new JsonResponse(['saved_item_ids' => "1,2,3"]);
$exp = HTTP::respJson(['saved_item_ids' => "1,2,3"]);
$this->assertMessage($exp, $this->req("api&saved_item_ids"));
$exp = new JsonResponse(['unread_item_ids' => "4,5,6"]);
$exp = HTTP::respJson(['unread_item_ids' => "4,5,6"]);
$this->assertMessage($exp, $this->req("api&unread_item_ids"));
}
public function testListHotLinks(): void {
// hot links are not actually implemented, so an empty array should be all we get
$exp = new JsonResponse(['links' => []]);
$exp = HTTP::respJson(['links' => []]);
$this->assertMessage($exp, $this->req("api&links"));
}
@ -350,7 +348,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->articleList->with($this->userId, (new Context)->unread(true)->hidden(false))->returns(new Result($unread));
$this->dbMock->articleMark->returns(0);
$this->dbMock->articleMark->with($this->userId, $this->anything(), (new Context)->article(2112))->throws(new \JKingWeb\Arsse\Db\ExceptionInput("subjectMissing"));
$exp = new JsonResponse($out);
$exp = HTTP::respJson($out);
$this->assertMessage($exp, $this->req("api", $post));
if ($c && $data) {
$this->dbMock->articleMark->calledWith($this->userId, $data, $this->equalTo($c));
@ -367,7 +365,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->articleList->with($this->userId, (new Context)->unread(true)->hidden(false))->returns(new Result($unread));
$this->dbMock->articleMark->returns(0);
$this->dbMock->articleMark->with($this->userId, $this->anything(), (new Context)->article(2112))->throws(new \JKingWeb\Arsse\Db\ExceptionInput("subjectMissing"));
$exp = new JsonResponse($out);
$exp = HTTP::respJson($out);
$this->assertMessage($exp, $this->req("api&$get"));
if ($c && $data) {
$this->dbMock->articleMark->calledWith($this->userId, $data, $this->equalTo($c));
@ -421,11 +419,11 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideInvalidRequests(): iterable {
return [
'Not an API request' => ["", "", "POST", null, new EmptyResponse(404)],
'Wrong method' => ["api", "", "PUT", null, new EmptyResponse(405, ['Allow' => "OPTIONS,POST"])],
'Non-standard method' => ["api", "", "GET", null, new JsonResponse([])],
'Wrong content type' => ["api", '{"api_key":"validToken"}', "POST", "application/json", new JsonResponse([])], // some clients send nonsensical content types; Fever seems to have allowed this
'Non-standard content type' => ["api", '{"api_key":"validToken"}', "POST", "multipart/form-data; boundary=33b68964f0de4c1f-5144aa6caaa6e4a8-18bfaf416a1786c8-5c5053a45f221bc1", new JsonResponse([])], // some clients send nonsensical content types; Fever seems to have allowed this
'Not an API request' => ["", "", "POST", null, HTTP::respEmpty(404)],
'Wrong method' => ["api", "", "PUT", null, HTTP::respEmpty(405, ['Allow' => "OPTIONS,POST"])],
'Non-standard method' => ["api", "", "GET", null, HTTP::respJson([])],
'Wrong content type' => ["api", '{"api_key":"validToken"}', "POST", "application/json", HTTP::respJson([])], // some clients send nonsensical content types; Fever seems to have allowed this
'Non-standard content type' => ["api", '{"api_key":"validToken"}', "POST", "multipart/form-data; boundary=33b68964f0de4c1f-5144aa6caaa6e4a8-18bfaf416a1786c8-5c5053a45f221bc1", HTTP::respJson([])], // some clients send nonsensical content types; Fever seems to have allowed this
];
}
@ -433,21 +431,21 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
$this->hMock->baseResponse->forwards();
$this->hMock->logIn->returns(true);
$this->dbMock->subscriptionRefreshed->with($this->userId)->returns(new \DateTimeImmutable("2000-01-01T00:00:00Z"));
$exp = new JsonResponse([
$exp = HTTP::respJson([
'api_version' => API::LEVEL,
'auth' => 1,
'last_refreshed_on_time' => 946684800,
]);
$this->assertMessage($exp, $this->req("api"));
$this->dbMock->subscriptionRefreshed->with($this->userId)->returns(null); // no subscriptions
$exp = new JsonResponse([
$exp = HTTP::respJson([
'api_version' => API::LEVEL,
'auth' => 1,
'last_refreshed_on_time' => null,
]);
$this->assertMessage($exp, $this->req("api"));
$this->hMock->logIn->returns(false);
$exp = new JsonResponse([
$exp = HTTP::respJson([
'api_version' => API::LEVEL,
'auth' => 0,
]);
@ -460,7 +458,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->articleList->with($this->userId, $this->equalTo((new Context)->limit(1)->hidden(false)), ["marked_date"], ["marked_date desc"])->returns(new Result([['marked_date' => "2000-01-01 00:00:00"]]));
$this->dbMock->articleList->with($this->userId, $this->equalTo((new Context)->unread(true)->hidden(false)))->returns(new Result($unread));
$this->dbMock->articleMark->returns(0);
$exp = new JsonResponse($out);
$exp = HTTP::respJson($out);
$this->assertMessage($exp, $this->req("api", ['unread_recently_read' => 1]));
$this->dbMock->articleMark->calledWith($this->userId, ['read' => false], $this->equalTo((new Context)->unread(false)->markedRange("1999-12-31T23:59:45Z", null)->hidden(false)));
$this->dbMock->articleList->with($this->userId, (new Context)->limit(1)->hidden(false), ["marked_date"], ["marked_date desc"])->returns(new Result([]));
@ -473,7 +471,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
'items' => $this->articles['rest'],
'total_items' => 1024,
]);
$exp = new XmlResponse("<response><items><item><id>101</id><feed_id>8</feed_id><title>Article title 1</title><author></author><html>&lt;p&gt;Article content 1&lt;/p&gt;</html><url>http://example.com/1</url><is_saved>0</is_saved><is_read>0</is_read><created_on_time>946684800</created_on_time></item><item><id>102</id><feed_id>8</feed_id><title>Article title 2</title><author></author><html>&lt;p&gt;Article content 2&lt;/p&gt;</html><url>http://example.com/2</url><is_saved>0</is_saved><is_read>1</is_read><created_on_time>946771200</created_on_time></item><item><id>103</id><feed_id>9</feed_id><title>Article title 3</title><author></author><html>&lt;p&gt;Article content 3&lt;/p&gt;</html><url>http://example.com/3</url><is_saved>1</is_saved><is_read>0</is_read><created_on_time>946857600</created_on_time></item><item><id>104</id><feed_id>9</feed_id><title>Article title 4</title><author></author><html>&lt;p&gt;Article content 4&lt;/p&gt;</html><url>http://example.com/4</url><is_saved>1</is_saved><is_read>1</is_read><created_on_time>946944000</created_on_time></item><item><id>105</id><feed_id>10</feed_id><title>Article title 5</title><author></author><html>&lt;p&gt;Article content 5&lt;/p&gt;</html><url>http://example.com/5</url><is_saved>0</is_saved><is_read>0</is_read><created_on_time>947030400</created_on_time></item></items><total_items>1024</total_items></response>");
$exp = HTTP::respXml("<response><items><item><id>101</id><feed_id>8</feed_id><title>Article title 1</title><author></author><html>&lt;p&gt;Article content 1&lt;/p&gt;</html><url>http://example.com/1</url><is_saved>0</is_saved><is_read>0</is_read><created_on_time>946684800</created_on_time></item><item><id>102</id><feed_id>8</feed_id><title>Article title 2</title><author></author><html>&lt;p&gt;Article content 2&lt;/p&gt;</html><url>http://example.com/2</url><is_saved>0</is_saved><is_read>1</is_read><created_on_time>946771200</created_on_time></item><item><id>103</id><feed_id>9</feed_id><title>Article title 3</title><author></author><html>&lt;p&gt;Article content 3&lt;/p&gt;</html><url>http://example.com/3</url><is_saved>1</is_saved><is_read>0</is_read><created_on_time>946857600</created_on_time></item><item><id>104</id><feed_id>9</feed_id><title>Article title 4</title><author></author><html>&lt;p&gt;Article content 4&lt;/p&gt;</html><url>http://example.com/4</url><is_saved>1</is_saved><is_read>1</is_read><created_on_time>946944000</created_on_time></item><item><id>105</id><feed_id>10</feed_id><title>Article title 5</title><author></author><html>&lt;p&gt;Article content 5&lt;/p&gt;</html><url>http://example.com/5</url><is_saved>0</is_saved><is_read>0</is_read><created_on_time>947030400</created_on_time></item></items><total_items>1024</total_items></response>");
$this->assertMessage($exp, $this->req("api=xml"));
}
@ -485,7 +483,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
['id' => 44, 'type' => null, 'data' => "IMAGE DATA"],
['id' => 47, 'type' => null, 'data' => null],
])));
$exp = new JsonResponse(['favicons' => [
$exp = HTTP::respJson(['favicons' => [
['id' => 0, 'data' => $iconType.",".$iconData],
['id' => 42, 'data' => "image/svg+xml;base64,PHN2Zy8+"],
['id' => 44, 'data' => "application/octet-stream;base64,SU1BR0UgREFUQQ=="],
@ -494,7 +492,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
}
public function testAnswerOptionsRequest(): void {
$exp = new EmptyResponse(204, [
$exp = HTTP::respEmpty(204, [
'Allow' => "POST",
'Accept' => "application/x-www-form-urlencoded, multipart/form-data",
]);

22
tests/cases/REST/Miniflux/TestErrorResponse.php

@ -1,22 +0,0 @@
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\TestCase\REST\Miniflux;
use JKingWeb\Arsse\REST\Miniflux\ErrorResponse;
/** @covers \JKingWeb\Arsse\REST\Miniflux\ErrorResponse */
class TestErrorResponse extends \JKingWeb\Arsse\Test\AbstractTest {
public function testCreateConstantResponse(): void {
$act = new ErrorResponse("401", 401);
$this->assertSame('{"error_message":"Access Unauthorized"}', (string) $act->getBody());
}
public function testCreateVariableResponse(): void {
$act = new ErrorResponse(["InvalidBodyJSON", "Doh!"], 401);
$this->assertSame('{"error_message":"Invalid JSON payload: Doh!"}', (string) $act->getBody());
}
}

17
tests/cases/REST/Miniflux/TestStatus.php

@ -6,11 +6,10 @@
declare(strict_types=1);
namespace JKingWeb\Arsse\TestCase\REST\Miniflux;
use JKingWeb\Arsse\Misc\HTTP;
use JKingWeb\Arsse\REST\Miniflux\Status;
use JKingWeb\Arsse\REST\Miniflux\V1;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\TextResponse;
/** @covers \JKingWeb\Arsse\REST\Miniflux\Status */
class TestStatus extends \JKingWeb\Arsse\Test\AbstractTest {
@ -22,13 +21,13 @@ class TestStatus extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideRequests(): iterable {
return [
["/version", "GET", new TextResponse(V1::VERSION)],
["/version", "POST", new EmptyResponse(405, ['Allow' => "HEAD, GET"])],
["/version", "OPTIONS", new EmptyResponse(204, ['Allow' => "HEAD, GET"])],
["/healthcheck", "GET", new TextResponse("OK")],
["/healthcheck", "POST", new EmptyResponse(405, ['Allow' => "HEAD, GET"])],
["/healthcheck", "OPTIONS", new EmptyResponse(204, ['Allow' => "HEAD, GET"])],
["/version/", "GET", new EmptyResponse(404)],
["/version", "GET", HTTP::respText(V1::VERSION)],
["/version", "POST", HTTP::respEmpty(405, ['Allow' => "HEAD, GET"])],
["/version", "OPTIONS", HTTP::respEmpty(204, ['Allow' => "HEAD, GET"])],
["/healthcheck", "GET", HTTP::respText("OK")],
["/healthcheck", "POST", HTTP::respEmpty(405, ['Allow' => "HEAD, GET"])],
["/healthcheck", "OPTIONS", HTTP::respEmpty(204, ['Allow' => "HEAD, GET"])],
["/version/", "GET", HTTP::respEmpty(404)],
];
}
}

463
tests/cases/REST/Miniflux/TestV1.php

@ -14,10 +14,10 @@ use JKingWeb\Arsse\Context\RootContext;
use JKingWeb\Arsse\Context\UnionContext;
use JKingWeb\Arsse\User;
use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Misc\HTTP;
use JKingWeb\Arsse\Db\Transaction;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\REST\Miniflux\V1;
use JKingWeb\Arsse\REST\Miniflux\ErrorResponse;
use JKingWeb\Arsse\Feed\Exception as FeedException;
use JKingWeb\Arsse\ImportExport\Exception as ImportException;
use JKingWeb\Arsse\ImportExport\OPML;
@ -25,9 +25,6 @@ use JKingWeb\Arsse\User\ExceptionConflict;
use JKingWeb\Arsse\User\ExceptionInput as UserExceptionInput;
use JKingWeb\Arsse\Test\Result;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\Response\JsonResponse as Response;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\TextResponse;
/** @covers \JKingWeb\Arsse\REST\Miniflux\V1<extended> */
class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
@ -100,9 +97,15 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
return $value;
}
public function testGenerateErrorResponse() {
$act = V1::respError(["DuplicateUser", 'user' => "john.doe"], 409, ['Cache-Control' => "no-store"]);
$exp = HTTP::respJson(['error_message' => 'The user name "john.doe" already exists'], 409, ['Cache-Control' => "no-store"]);
$this->assertMessage($exp, $act);
}
/** @dataProvider provideAuthResponses */
public function testAuthenticateAUser($token, bool $auth, bool $success): void {
$exp = $success ? new EmptyResponse(404) : new ErrorResponse("401", 401);
$exp = $success ? HTTP::respEmpty(404) : V1::respError("401", 401);
$user = "john.doe@example.com";
if ($token !== null) {
$headers = ['X-Auth-Token' => $token];
@ -133,7 +136,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
/** @dataProvider provideInvalidPaths */
public function testRespondToInvalidPaths($path, $method, $code, $allow = null): void {
$exp = new EmptyResponse($code, $allow ? ['Allow' => $allow] : []);
$exp = HTTP::respEmpty($code, $allow ? ['Allow' => $allow] : []);
$this->assertMessage($exp, $this->req($method, $path));
}
@ -148,7 +151,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
/** @dataProvider provideOptionsRequests */
public function testRespondToOptionsRequests(string $url, string $allow, string $accept): void {
$exp = new EmptyResponse(204, [
$exp = HTTP::respEmpty(204, [
'Allow' => $allow,
'Accept' => $accept,
]);
@ -166,7 +169,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
}
public function testRejectBadlyTypedData(): void {
$exp = new ErrorResponse(["InvalidInputType", 'field' => "url", 'expected' => "string", 'actual' => "integer"], 422);
$exp = V1::respError(["InvalidInputType", 'field' => "url", 'expected' => "string", 'actual' => "integer"], 422);
$this->assertMessage($exp, $this->req("POST", "/discover", ['url' => 2112]));
}
@ -182,12 +185,12 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
['title' => "Feed", 'type' => "rss", 'url' => "http://localhost:8000/Feed/Discovery/Missing"],
];
return [
["http://localhost:8000/Feed/Discovery/Valid", new Response($discovered)],
["http://localhost:8000/Feed/Discovery/Invalid", new Response([])],
["http://localhost:8000/Feed/Discovery/Missing", new ErrorResponse("Fetch404", 502)],
[1, new ErrorResponse(["InvalidInputType", 'field' => "url", 'expected' => "string", 'actual' => "integer"], 422)],
["Not a URL", new ErrorResponse(["InvalidInputValue", 'field' => "url"], 422)],
[null, new ErrorResponse(["MissingInputValue", 'field' => "url"], 422)],
["http://localhost:8000/Feed/Discovery/Valid", HTTP::respJson($discovered)],
["http://localhost:8000/Feed/Discovery/Invalid", HTTP::respJson([])],
["http://localhost:8000/Feed/Discovery/Missing", V1::respError("Fetch404", 502)],
[1, V1::respError(["InvalidInputType", 'field' => "url", 'expected' => "string", 'actual' => "integer"], 422)],
["Not a URL", V1::respError(["InvalidInputValue", 'field' => "url"], 422)],
[null, V1::respError(["MissingInputValue", 'field' => "url"], 422)],
];
}
@ -226,22 +229,22 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideUserQueries(): iterable {
self::clearData();
return [
[true, "/users", new Response(self::USERS)],
[true, "/me", new Response(self::USERS[0])],
[true, "/users/john.doe@example.com", new Response(self::USERS[0])],
[true, "/users/1", new Response(self::USERS[0])],
[true, "/users/jane.doe@example.com", new Response(self::USERS[1])],
[true, "/users/2", new Response(self::USERS[1])],
[true, "/users/jack.doe@example.com", new ErrorResponse("404", 404)],
[true, "/users/47", new ErrorResponse("404", 404)],
[false, "/users", new ErrorResponse("403", 403)],
[false, "/me", new Response(self::USERS[1])],
[false, "/users/john.doe@example.com", new ErrorResponse("403", 403)],
[false, "/users/1", new ErrorResponse("403", 403)],
[false, "/users/jane.doe@example.com", new ErrorResponse("403", 403)],
[false, "/users/2", new ErrorResponse("403", 403)],
[false, "/users/jack.doe@example.com", new ErrorResponse("403", 403)],
[false, "/users/47", new ErrorResponse("403", 403)],
[true, "/users", HTTP::respJson(self::USERS)],
[true, "/me", HTTP::respJson(self::USERS[0])],
[true, "/users/john.doe@example.com", HTTP::respJson(self::USERS[0])],
[true, "/users/1", HTTP::respJson(self::USERS[0])],
[true, "/users/jane.doe@example.com", HTTP::respJson(self::USERS[1])],
[true, "/users/2", HTTP::respJson(self::USERS[1])],
[true, "/users/jack.doe@example.com", V1::respError("404", 404)],
[true, "/users/47", V1::respError("404", 404)],
[false, "/users", V1::respError("403", 403)],
[false, "/me", HTTP::respJson(self::USERS[1])],
[false, "/users/john.doe@example.com", V1::respError("403", 403)],
[false, "/users/1", V1::respError("403", 403)],
[false, "/users/jane.doe@example.com", V1::respError("403", 403)],
[false, "/users/2", V1::respError("403", 403)],
[false, "/users/jack.doe@example.com", V1::respError("403", 403)],
[false, "/users/47", V1::respError("403", 403)],
];
}
@ -306,21 +309,21 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
$resp1 = array_merge(self::USERS[1], ['username' => "john.doe@example.com"]);
$resp2 = array_merge(self::USERS[1], ['id' => 1, 'is_admin' => true]);
return [
[false, "/users/1", ['is_admin' => 0], null, null, null, null, null, null, new ErrorResponse(["InvalidInputType", 'field' => "is_admin", 'expected' => "boolean", 'actual' => "integer"], 422)],
[false, "/users/1", ['entry_sorting_direction' => "bad"], null, null, null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "entry_sorting_direction"], 422)],
[false, "/users/1", ['theme' => "stark"], null, null, null, null, null, null, new ErrorResponse("403", 403)],
[false, "/users/2", ['is_admin' => true], null, null, null, null, null, null, new ErrorResponse("InvalidElevation", 403)],
[false, "/users/2", ['language' => "fr_CA"], null, null, null, null, ['lang' => "fr_CA"], $out1, new Response($resp1, 201)],
[false, "/users/2", ['entry_sorting_direction' => "asc"], null, null, null, null, ['sort_asc' => true], $out1, new Response($resp1, 201)],
[false, "/users/2", ['entry_sorting_direction' => "desc"], null, null, null, null, ['sort_asc' => false], $out1, new Response($resp1, 201)],
[false, "/users/2", ['entries_per_page' => -1], null, null, null, null, ['page_size' => -1], new UserExceptionInput("invalidNonZeroInteger"), new ErrorResponse(["InvalidInputValue", 'field' => "entries_per_page"], 422)],
[false, "/users/2", ['timezone' => "Ook"], null, null, null, null, ['tz' => "Ook"], new UserExceptionInput("invalidTimezone"), new ErrorResponse(["InvalidInputValue", 'field' => "timezone"], 422)],
[false, "/users/2", ['username' => "j:k"], "j:k", new UserExceptionInput("invalidUsername"), null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "username"], 422)],
[false, "/users/2", ['username' => "ook"], "ook", new ExceptionConflict("alreadyExists"), null, null, null, null, new ErrorResponse(["DuplicateUser", 'user' => "ook"], 409)],
[false, "/users/2", ['password' => "ook"], null, null, "ook", "ook", null, null, new Response(array_merge($resp1, ['password' => "ook"]), 201)],
[false, "/users/2", ['username' => "ook", 'password' => "ook"], "ook", true, "ook", "ook", null, null, new Response(array_merge($resp1, ['username' => "ook", 'password' => "ook"]), 201)],
[true, "/users/1", ['theme' => "stark"], null, null, null, null, ['theme' => "stark"], $out2, new Response($resp2, 201)],
[true, "/users/3", ['theme' => "stark"], null, null, null, null, null, null, new ErrorResponse("404", 404)],
[false, "/users/1", ['is_admin' => 0], null, null, null, null, null, null, V1::respError(["InvalidInputType", 'field' => "is_admin", 'expected' => "boolean", 'actual' => "integer"], 422)],
[false, "/users/1", ['entry_sorting_direction' => "bad"], null, null, null, null, null, null, V1::respError(["InvalidInputValue", 'field' => "entry_sorting_direction"], 422)],
[false, "/users/1", ['theme' => "stark"], null, null, null, null, null, null, V1::respError("403", 403)],
[false, "/users/2", ['is_admin' => true], null, null, null, null, null, null, V1::respError("InvalidElevation", 403)],
[false, "/users/2", ['language' => "fr_CA"], null, null, null, null, ['lang' => "fr_CA"], $out1, HTTP::respJson($resp1, 201)],
[false, "/users/2", ['entry_sorting_direction' => "asc"], null, null, null, null, ['sort_asc' => true], $out1, HTTP::respJson($resp1, 201)],
[false, "/users/2", ['entry_sorting_direction' => "desc"], null, null, null, null, ['sort_asc' => false], $out1, HTTP::respJson($resp1, 201)],
[false, "/users/2", ['entries_per_page' => -1], null, null, null, null, ['page_size' => -1], new UserExceptionInput("invalidNonZeroInteger"), V1::respError(["InvalidInputValue", 'field' => "entries_per_page"], 422)],
[false, "/users/2", ['timezone' => "Ook"], null, null, null, null, ['tz' => "Ook"], new UserExceptionInput("invalidTimezone"), V1::respError(["InvalidInputValue", 'field' => "timezone"], 422)],
[false, "/users/2", ['username' => "j:k"], "j:k", new UserExceptionInput("invalidUsername"), null, null, null, null, V1::respError(["InvalidInputValue", 'field' => "username"], 422)],
[false, "/users/2", ['username' => "ook"], "ook", new ExceptionConflict("alreadyExists"), null, null, null, null, V1::respError(["DuplicateUser", 'user' => "ook"], 409)],
[false, "/users/2", ['password' => "ook"], null, null, "ook", "ook", null, null, HTTP::respJson(array_merge($resp1, ['password' => "ook"]), 201)],
[false, "/users/2", ['username' => "ook", 'password' => "ook"], "ook", true, "ook", "ook", null, null, HTTP::respJson(array_merge($resp1, ['username' => "ook", 'password' => "ook"]), 201)],
[true, "/users/1", ['theme' => "stark"], null, null, null, null, ['theme' => "stark"], $out2, HTTP::respJson($resp2, 201)],
[true, "/users/3", ['theme' => "stark"], null, null, null, null, null, null, V1::respError("404", 404)],
];
}
@ -361,18 +364,18 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideUserAdditions(): iterable {
$resp1 = array_merge(self::USERS[1], ['username' => "ook", 'password' => "eek"]);
return [
[[], null, null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "username"], 422)],
[['username' => "ook"], null, null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "password"], 422)],
[['username' => "ook", 'password' => "eek"], ["ook", "eek"], new ExceptionConflict("alreadyExists"), null, null, new ErrorResponse(["DuplicateUser", 'user' => "ook"], 409)],
[['username' => "j:k", 'password' => "eek"], ["j:k", "eek"], new UserExceptionInput("invalidUsername"), null, null, new ErrorResponse(["InvalidInputValue", 'field' => "username"], 422)],
[['username' => "ook", 'password' => "eek", 'timezone' => "ook"], ["ook", "eek"], "eek", ['tz' => "ook"], new UserExceptionInput("invalidTimezone"), new ErrorResponse(["InvalidInputValue", 'field' => "timezone"], 422)],
[['username' => "ook", 'password' => "eek", 'entries_per_page' => -1], ["ook", "eek"], "eek", ['page_size' => -1], new UserExceptionInput("invalidNonZeroInteger"), new ErrorResponse(["InvalidInputValue", 'field' => "entries_per_page"], 422)],
[['username' => "ook", 'password' => "eek", 'theme' => "default"], ["ook", "eek"], "eek", ['theme' => "default"], ['theme' => "default"], new Response($resp1, 201)],
[[], null, null, null, null, V1::respError(["MissingInputValue", 'field' => "username"], 422)],
[['username' => "ook"], null, null, null, null, V1::respError(["MissingInputValue", 'field' => "password"], 422)],
[['username' => "ook", 'password' => "eek"], ["ook", "eek"], new ExceptionConflict("alreadyExists"), null, null, V1::respError(["DuplicateUser", 'user' => "ook"], 409)],
[['username' => "j:k", 'password' => "eek"], ["j:k", "eek"], new UserExceptionInput("invalidUsername"), null, null, V1::respError(["InvalidInputValue", 'field' => "username"], 422)],
[['username' => "ook", 'password' => "eek", 'timezone' => "ook"], ["ook", "eek"], "eek", ['tz' => "ook"], new UserExceptionInput("invalidTimezone"), V1::respError(["InvalidInputValue", 'field' => "timezone"], 422)],
[['username' => "ook", 'password' => "eek", 'entries_per_page' => -1], ["ook", "eek"], "eek", ['page_size' => -1], new UserExceptionInput("invalidNonZeroInteger"), V1::respError(["InvalidInputValue", 'field' => "entries_per_page"], 422)],
[['username' => "ook", 'password' => "eek", 'theme' => "default"], ["ook", "eek"], "eek", ['theme' => "default"], ['theme' => "default"], HTTP::respJson($resp1, 201)],
];
}
public function testAddAUserWithoutAuthority(): void {
$this->assertMessage(new ErrorResponse("403", 403), $this->req("POST", "/users", []));
$this->assertMessage(V1::respError("403", 403), $this->req("POST", "/users", []));
}
public function testDeleteAUser(): void {
@ -382,7 +385,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
Arsse::$user->method("remove")->willReturn(true);
Arsse::$user->expects($this->exactly(1))->method("lookup")->with(2112);
Arsse::$user->expects($this->exactly(1))->method("remove")->with("john.doe@example.com");
$this->assertMessage(new EmptyResponse(204), $this->req("DELETE", "/users/2112"));
$this->assertMessage(HTTP::respEmpty(204), $this->req("DELETE", "/users/2112"));
}
public function testDeleteAMissingUser(): void {
@ -392,13 +395,13 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
Arsse::$user->method("remove")->willReturn(true);
Arsse::$user->expects($this->exactly(1))->method("lookup")->with(2112);
Arsse::$user->expects($this->exactly(0))->method("remove");
$this->assertMessage(new ErrorResponse("404", 404), $this->req("DELETE", "/users/2112"));
$this->assertMessage(V1::respError("404", 404), $this->req("DELETE", "/users/2112"));
}
public function testDeleteAUserWithoutAuthority(): void {
Arsse::$user->expects($this->exactly(0))->method("lookup");
Arsse::$user->expects($this->exactly(0))->method("remove");
$this->assertMessage(new ErrorResponse("403", 403), $this->req("DELETE", "/users/2112"));
$this->assertMessage(V1::respError("403", 403), $this->req("DELETE", "/users/2112"));
}
public function testListCategories(): void {
@ -406,7 +409,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
['id' => 1, 'name' => "Science"],
['id' => 20, 'name' => "Technology"],
])));
$exp = new Response([
$exp = HTTP::respJson([
['id' => 1, 'title' => "All", 'user_id' => 42],
['id' => 2, 'title' => "Science", 'user_id' => 42],
['id' => 21, 'title' => "Technology", 'user_id' => 42],
@ -416,7 +419,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
// run test again with a renamed root folder
Arsse::$user = $this->createMock(User::class);
Arsse::$user->method("propertiesGet")->willReturn(['num' => 47, 'admin' => false, 'root_folder_name' => "Uncategorized"]);
$exp = new Response([
$exp = HTTP::respJson([
['id' => 1, 'title' => "Uncategorized", 'user_id' => 47],
['id' => 2, 'title' => "Science", 'user_id' => 47],
['id' => 21, 'title' => "Technology", 'user_id' => 47],
@ -440,12 +443,12 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideCategoryAdditions(): iterable {
return [
["New", new Response(['id' => 2112, 'title' => "New", 'user_id' => 42], 201)],
["Duplicate", new ErrorResponse(["DuplicateCategory", 'title' => "Duplicate"], 409)],
["", new ErrorResponse(["InvalidCategory", 'title' => ""], 422)],
[" ", new ErrorResponse(["InvalidCategory", 'title' => " "], 422)],
[null, new ErrorResponse(["MissingInputValue", 'field' => "title"], 422)],
[false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 422)],
["New", HTTP::respJson(['id' => 2112, 'title' => "New", 'user_id' => 42], 201)],
["Duplicate", V1::respError(["DuplicateCategory", 'title' => "Duplicate"], 409)],
["", V1::respError(["InvalidCategory", 'title' => ""], 422)],
[" ", V1::respError(["InvalidCategory", 'title' => " "], 422)],
[null, V1::respError(["MissingInputValue", 'field' => "title"], 422)],
[false, V1::respError(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 422)],
];
}
@ -466,27 +469,27 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideCategoryUpdates(): iterable {
return [
[3, "New", "subjectMissing", new ErrorResponse("404", 404)],
[2, "New", true, new Response(['id' => 2, 'title' => "New", 'user_id' => 42], 201)],
[2, "Duplicate", "constraintViolation", new ErrorResponse(["DuplicateCategory", 'title' => "Duplicate"], 409)],
[2, "", "missing", new ErrorResponse(["InvalidCategory", 'title' => ""], 422)],
[2, " ", "whitespace", new ErrorResponse(["InvalidCategory", 'title' => " "], 422)],
[2, null, "missing", new ErrorResponse(["MissingInputValue", 'field' => "title"], 422)],
[2, false, "subjectMissing", new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 422)],
[1, "New", true, new Response(['id' => 1, 'title' => "New", 'user_id' => 42], 201)],
[1, "Duplicate", "constraintViolation", new Response(['id' => 1, 'title' => "Duplicate", 'user_id' => 42], 201)], // This is allowed because the name of the root folder is only a duplicate in circumstances where it is used
[1, "", "missing", new ErrorResponse(["InvalidCategory", 'title' => ""], 422)],
[1, " ", "whitespace", new ErrorResponse(["InvalidCategory", 'title' => " "], 422)],
[1, null, "missing", new ErrorResponse(["MissingInputValue", 'field' => "title"], 422)],
[1, false, false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 422)],
[3, "New", "subjectMissing", V1::respError("404", 404)],
[2, "New", true, HTTP::respJson(['id' => 2, 'title' => "New", 'user_id' => 42], 201)],
[2, "Duplicate", "constraintViolation", V1::respError(["DuplicateCategory", 'title' => "Duplicate"], 409)],
[2, "", "missing", V1::respError(["InvalidCategory", 'title' => ""], 422)],
[2, " ", "whitespace", V1::respError(["InvalidCategory", 'title' => " "], 422)],
[2, null, "missing", V1::respError(["MissingInputValue", 'field' => "title"], 422)],
[2, false, "subjectMissing", V1::respError(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 422)],
[1, "New", true, HTTP::respJson(['id' => 1, 'title' => "New", 'user_id' => 42], 201)],
[1, "Duplicate", "constraintViolation", HTTP::respJson(['id' => 1, 'title' => "Duplicate", 'user_id' => 42], 201)], // This is allowed because the name of the root folder is only a duplicate in circumstances where it is used
[1, "", "missing", V1::respError(["InvalidCategory", 'title' => ""], 422)],
[1, " ", "whitespace", V1::respError(["InvalidCategory", 'title' => " "], 422)],
[1, null, "missing", V1::respError(["MissingInputValue", 'field' => "title"], 422)],
[1, false, false, V1::respError(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 422)],
];
}
public function testDeleteARealCategory(): void {
$this->dbMock->folderRemove->returns(true)->throws(new ExceptionInput("subjectMissing"));
$this->assertMessage(new EmptyResponse(204), $this->req("DELETE", "/categories/2112"));
$this->assertMessage(HTTP::respEmpty(204), $this->req("DELETE", "/categories/2112"));
$this->dbMock->folderRemove->calledWith("john.doe@example.com", 2111);
$this->assertMessage(new ErrorResponse("404", 404), $this->req("DELETE", "/categories/47"));
$this->assertMessage(V1::respError("404", 404), $this->req("DELETE", "/categories/47"));
$this->dbMock->folderRemove->calledWith("john.doe@example.com", 46);
}
@ -497,7 +500,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
['id' => 2112],
])));
$this->dbMock->subscriptionRemove->returns(true);
$this->assertMessage(new EmptyResponse(204), $this->req("DELETE", "/categories/1"));
$this->assertMessage(HTTP::respEmpty(204), $this->req("DELETE", "/categories/1"));
Phony::inOrder(
$this->dbMock->begin->calledWith(),
$this->dbMock->subscriptionList->calledWith("john.doe@example.com", null, false),
@ -510,42 +513,42 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
public function testListFeeds(): void {
$this->dbMock->subscriptionList->returns(new Result($this->v(self::FEEDS)));
$exp = new Response(self::FEEDS_OUT);
$exp = HTTP::respJson(self::FEEDS_OUT);
$this->assertMessage($exp, $this->req("GET", "/feeds"));
}
public function testListFeedsOfACategory(): void {
$this->dbMock->subscriptionList->returns(new Result($this->v(self::FEEDS)));
$exp = new Response(self::FEEDS_OUT);
$exp = HTTP::respJson(self::FEEDS_OUT);
$this->assertMessage($exp, $this->req("GET", "/categories/2112/feeds"));
$this->dbMock->subscriptionList->calledWith(Arsse::$user->id, 2111, true);
}
public function testListFeedsOfTheRootCategory(): void {
$this->dbMock->subscriptionList->returns(new Result($this->v(self::FEEDS)));
$exp = new Response(self::FEEDS_OUT);
$exp = HTTP::respJson(self::FEEDS_OUT);
$this->assertMessage($exp, $this->req("GET", "/categories/1/feeds"));
$this->dbMock->subscriptionList->calledWith(Arsse::$user->id, 0, false);
}
public function testListFeedsOfAMissingCategory(): void {
$this->dbMock->subscriptionList->throws(new ExceptionInput("idMissing"));
$exp = new ErrorResponse("404", 404);
$exp = V1::respError("404", 404);
$this->assertMessage($exp, $this->req("GET", "/categories/2112/feeds"));
$this->dbMock->subscriptionList->calledWith(Arsse::$user->id, 2111, true);
}
public function testGetAFeed(): void {
$this->dbMock->subscriptionPropertiesGet->returns($this->v(self::FEEDS[0]))->returns($this->v(self::FEEDS[1]));
$this->assertMessage(new Response(self::FEEDS_OUT[0]), $this->req("GET", "/feeds/1"));
$this->assertMessage(HTTP::respJson(self::FEEDS_OUT[0]), $this->req("GET", "/feeds/1"));
$this->dbMock->subscriptionPropertiesGet->calledWith(Arsse::$user->id, 1);
$this->assertMessage(new Response(self::FEEDS_OUT[1]), $this->req("GET", "/feeds/55"));
$this->assertMessage(HTTP::respJson(self::FEEDS_OUT[1]), $this->req("GET", "/feeds/55"));
$this->dbMock->subscriptionPropertiesGet->calledWith(Arsse::$user->id, 55);
}
public function testGetAMissingFeed(): void {
$this->dbMock->subscriptionPropertiesGet->throws(new ExceptionInput("subjectMissing"));
$this->assertMessage(new ErrorResponse("404", 404), $this->req("GET", "/feeds/1"));
$this->assertMessage(V1::respError("404", 404), $this->req("GET", "/feeds/1"));
$this->dbMock->subscriptionPropertiesGet->calledWith(Arsse::$user->id, 1);
}
@ -611,39 +614,39 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideFeedCreations(): iterable {
self::clearData();
return [
[['category_id' => 1], null, null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "feed_url"], 422)],
[['feed_url' => "http://example.com/"], null, null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "category_id"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => "1"], null, null, null, null, new ErrorResponse(["InvalidInputType", 'field' => "category_id", 'expected' => "integer", 'actual' => "string"], 422)],
[['feed_url' => "Not a URL", 'category_id' => 1], null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "feed_url"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 0], null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "["], null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "keeplist_rules"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "["], null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "blocklist_rules"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("internalError"), null, null, null, new ErrorResponse("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidCertificate"), null, null, null, new ErrorResponse("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidUrl"), null, null, null, new ErrorResponse("Fetch404", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxRedirect"), null, null, null, new ErrorResponse("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxSize"), null, null, null, new ErrorResponse("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("timeout"), null, null, null, new ErrorResponse("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("forbidden"), null, null, null, new ErrorResponse("Fetch403", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unauthorized"), null, null, null, new ErrorResponse("Fetch401", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("transmissionError"), null, null, null, new ErrorResponse("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("connectionFailed"), null, null, null, new ErrorResponse("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("malformedXml"), null, null, null, new ErrorResponse("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("xmlEntity"), null, null, null, new ErrorResponse("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("subscriptionNotFound"), null, null, null, new ErrorResponse("Fetch404", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unsupportedFeedFormat"), null, null, null, new ErrorResponse("FetchFormat", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], 2112, new ExceptionInput("constraintViolation"), null, null, new ErrorResponse("DuplicateFeed", 409)],
[['feed_url' => "http://example.com/", 'category_id' => 1], 2112, 44, new ExceptionInput("idMissing"), null, new ErrorResponse("MissingCategory", 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1], 2112, 44, true, null, new Response(['feed_id' => 44], 201)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "^A"], 2112, 44, true, true, new Response(['feed_id' => 44], 201)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "A"], 2112, 44, true, true, new Response(['feed_id' => 44], 201)],
[['category_id' => 1], null, null, null, null, V1::respError(["MissingInputValue", 'field' => "feed_url"], 422)],
[['feed_url' => "http://example.com/"], null, null, null, null, V1::respError(["MissingInputValue", 'field' => "category_id"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => "1"], null, null, null, null, V1::respError(["InvalidInputType", 'field' => "category_id", 'expected' => "integer", 'actual' => "string"], 422)],
[['feed_url' => "Not a URL", 'category_id' => 1], null, null, null, null, V1::respError(["InvalidInputValue", 'field' => "feed_url"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 0], null, null, null, null, V1::respError(["InvalidInputValue", 'field' => "category_id"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "["], null, null, null, null, V1::respError(["InvalidInputValue", 'field' => "keeplist_rules"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "["], null, null, null, null, V1::respError(["InvalidInputValue", 'field' => "blocklist_rules"], 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("internalError"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidCertificate"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidUrl"), null, null, null, V1::respError("Fetch404", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxRedirect"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxSize"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("timeout"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("forbidden"), null, null, null, V1::respError("Fetch403", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unauthorized"), null, null, null, V1::respError("Fetch401", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("transmissionError"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("connectionFailed"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("malformedXml"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("xmlEntity"), null, null, null, V1::respError("FetchOther", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("subscriptionNotFound"), null, null, null, V1::respError("Fetch404", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unsupportedFeedFormat"), null, null, null, V1::respError("FetchFormat", 502)],
[['feed_url' => "http://example.com/", 'category_id' => 1], 2112, new ExceptionInput("constraintViolation"), null, null, V1::respError("DuplicateFeed", 409)],
[['feed_url' => "http://example.com/", 'category_id' => 1], 2112, 44, new ExceptionInput("idMissing"), null, V1::respError("MissingCategory", 422)],
[['feed_url' => "http://example.com/", 'category_id' => 1], 2112, 44, true, null, HTTP::respJson(['feed_id' => 44], 201)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "^A"], 2112, 44, true, true, HTTP::respJson(['feed_id' => 44], 201)],
[['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "A"], 2112, 44, true, true, HTTP::respJson(['feed_id' => 44], 201)],
];
}
/** @dataProvider provideFeedModifications */
public function testModifyAFeed(array $in, array $data, $out, ResponseInterface $exp): void {
$this->h = $this->partialMock(V1::class);
$this->h->getFeed->returns(new Response(self::FEEDS_OUT[0]));
$this->h->getFeed->returns(HTTP::respJson(self::FEEDS_OUT[0]));
if ($out instanceof \Exception) {
$this->dbMock->subscriptionPropertiesSet->throws($out);
} else {
@ -655,14 +658,14 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideFeedModifications(): iterable {
self::clearData();
$success = new Response(self::FEEDS_OUT[0], 201);
$success = HTTP::respJson(self::FEEDS_OUT[0], 201);
return [
[[], [], true, $success],
[[], [], new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)],
[['title' => ""], ['title' => ""], new ExceptionInput("missing"), new ErrorResponse("InvalidTitle", 422)],
[['title' => " "], ['title' => " "], new ExceptionInput("whitespace"), new ErrorResponse("InvalidTitle", 422)],
[['title' => " "], ['title' => " "], new ExceptionInput("whitespace"), new ErrorResponse("InvalidTitle", 422)],
[['category_id' => 47], ['folder' => 46], new ExceptionInput("idMissing"), new ErrorResponse("MissingCategory", 422)],
[[], [], new ExceptionInput("subjectMissing"), V1::respError("404", 404)],
[['title' => ""], ['title' => ""], new ExceptionInput("missing"), V1::respError("InvalidTitle", 422)],
[['title' => " "], ['title' => " "], new ExceptionInput("whitespace"), V1::respError("InvalidTitle", 422)],
[['title' => " "], ['title' => " "], new ExceptionInput("whitespace"), V1::respError("InvalidTitle", 422)],
[['category_id' => 47], ['folder' => 46], new ExceptionInput("idMissing"), V1::respError("MissingCategory", 422)],
[['crawler' => false], ['scrape' => false], true, $success],
[['keeplist_rules' => ""], ['keep_rule' => ""], true, $success],
[['blocklist_rules' => "ook"], ['block_rule' => "ook"], true, $success],
@ -672,21 +675,21 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
public function testModifyAFeedWithNoBody(): void {
$this->h = $this->partialMock(V1::class);
$this->h->getFeed->returns(new Response(self::FEEDS_OUT[0]));
$this->h->getFeed->returns(HTTP::respJson(self::FEEDS_OUT[0]));
$this->dbMock->subscriptionPropertiesSet->returns(true);
$this->assertMessage(new Response(self::FEEDS_OUT[0], 201), $this->req("PUT", "/feeds/2112", ""));
$this->assertMessage(HTTP::respJson(self::FEEDS_OUT[0], 201), $this->req("PUT", "/feeds/2112", ""));
$this->dbMock->subscriptionPropertiesSet->calledWith(Arsse::$user->id, 2112, []);
}
public function testDeleteAFeed(): void {
$this->dbMock->subscriptionRemove->returns(true);
$this->assertMessage(new EmptyResponse(204), $this->req("DELETE", "/feeds/2112"));
$this->assertMessage(HTTP::respEmpty(204), $this->req("DELETE", "/feeds/2112"));
$this->dbMock->subscriptionRemove->calledWith(Arsse::$user->id, 2112);
}
public function testDeleteAMissingFeed(): void {
$this->dbMock->subscriptionRemove->throws(new ExceptionInput("subjectMissing"));
$this->assertMessage(new ErrorResponse("404", 404), $this->req("DELETE", "/feeds/2112"));
$this->assertMessage(V1::respError("404", 404), $this->req("DELETE", "/feeds/2112"));
$this->dbMock->subscriptionRemove->calledWith(Arsse::$user->id, 2112);
}
@ -703,12 +706,12 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideIcons(): iterable {
return [
[['id' => 44, 'type' => "image/svg+xml", 'data' => "<svg/>"], new Response(['id' => 44, 'data' => "image/svg+xml;base64,PHN2Zy8+", 'mime_type' => "image/svg+xml"])],
[['id' => 47, 'type' => "", 'data' => "<svg/>"], new ErrorResponse("404", 404)],
[['id' => 47, 'type' => null, 'data' => "<svg/>"], new ErrorResponse("404", 404)],
[['id' => 47, 'type' => null, 'data' => null], new ErrorResponse("404", 404)],
[null, new ErrorResponse("404", 404)],
[new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)],
[['id' => 44, 'type' => "image/svg+xml", 'data' => "<svg/>"], HTTP::respJson(['id' => 44, 'data' => "image/svg+xml;base64,PHN2Zy8+", 'mime_type' => "image/svg+xml"])],
[['id' => 47, 'type' => "", 'data' => "<svg/>"], V1::respError("404", 404)],
[['id' => 47, 'type' => null, 'data' => "<svg/>"], V1::respError("404", 404)],
[['id' => 47, 'type' => null, 'data' => null], V1::respError("404", 404)],
[null, V1::respError("404", 404)],
[new ExceptionInput("subjectMissing"), V1::respError("404", 404)],
];
}
@ -744,62 +747,62 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
$c = (new Context)->limit(100);
$o = ["modified_date"]; // the default sort order
return [
["/entries?after=A", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "after"], 400)],
["/entries?before=B", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "before"], 400)],
["/entries?category_id=0", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 400)],
["/entries?after_entry_id=0", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "after_entry_id"], 400)],
["/entries?before_entry_id=0", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "before_entry_id"], 400)],
["/entries?limit=-1", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "limit"], 400)],
["/entries?offset=-1", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "offset"], 400)],
["/entries?direction=sideways", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "direction"], 400)],
["/entries?order=false", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "order"], 400)],
["/entries?starred&starred", null, null, [], false, new ErrorResponse(["DuplicateInputValue", 'field' => "starred"], 400)],
["/entries?after&after=0", null, null, [], false, new ErrorResponse(["DuplicateInputValue", 'field' => "after"], 400)],
["/entries", $c, $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?category_id=47", (clone $c)->folder(46), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?category_id=1", (clone $c)->folderShallow(0), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=unread", (clone $c)->unread(true)->hidden(false), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=read", (clone $c)->unread(false)->hidden(false), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=removed", (clone $c)->hidden(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=unread&status=read", (clone $c)->hidden(false), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=unread&status=removed", new UnionContext((clone $c)->unread(true), (clone $c)->hidden(true)), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=removed&status=read", new UnionContext((clone $c)->unread(false), (clone $c)->hidden(true)), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=removed&status=read&status=removed", new UnionContext((clone $c)->unread(false), (clone $c)->hidden(true)), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=removed&status=read&status=unread", $c, $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?starred", (clone $c)->starred(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?starred=", (clone $c)->starred(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?starred=true", (clone $c)->starred(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?starred=false", (clone $c)->starred(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?after=0", (clone $c)->modifiedRange(0, null), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?before=0", $c, $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?before=1", (clone $c)->modifiedRange(null, 1), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?before=1&after=0", (clone $c)->modifiedRange(0, 1), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?after_entry_id=42", (clone $c)->articleRange(43, null), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?before_entry_id=47", (clone $c)->articleRange(null, 46), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?search=alpha%20beta", (clone $c)->searchTerms(["alpha", "beta"]), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?limit=4", (clone $c)->limit(4), $o, self::ENTRIES, true, new Response(['total' => 2112, 'entries' => self::ENTRIES_OUT])],
["/entries?offset=20", (clone $c)->offset(20), $o, [], true, new Response(['total' => 2112, 'entries' => []])],
["/entries?direction=asc", $c, $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=id", $c, ["id"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=published_at", $c, ["modified_date"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=category_id", $c, ["top_folder"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=category_title", $c, ["top_folder_name"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=status", $c, ["hidden", "unread desc"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?direction=desc", $c, ["modified_date desc"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=id&direction=desc", $c, ["id desc"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=published_at&direction=desc", $c, ["modified_date desc"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=category_id&direction=desc", $c, ["top_folder desc"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=category_title&direction=desc", $c, ["top_folder_name desc"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=status&direction=desc", $c, ["hidden desc", "unread"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?category_id=2112", (clone $c)->folder(2111), $o, new ExceptionInput("idMissing"), false, new ErrorResponse("MissingCategory")],
["/feeds/42/entries", (clone $c)->subscription(42), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/feeds/42/entries?category_id=47", (clone $c)->subscription(42)->folder(46), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/feeds/2112/entries", (clone $c)->subscription(2112), $o, new ExceptionInput("idMissing"), false, new ErrorResponse("404", 404)],
["/categories/42/entries", (clone $c)->folder(41), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/categories/42/entries?category_id=47", (clone $c)->folder(41), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/categories/42/entries?starred", (clone $c)->folder(41)->starred(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/categories/1/entries", (clone $c)->folderShallow(0), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/categories/2112/entries", (clone $c)->folder(2111), $o, new ExceptionInput("idMissing"), false, new ErrorResponse("404", 404)],
["/entries?after=A", null, null, [], false, V1::respError(["InvalidInputValue", 'field' => "after"], 400)],
["/entries?before=B", null, null, [], false, V1::respError(["InvalidInputValue", 'field' => "before"], 400)],
["/entries?category_id=0", null, null, [], false, V1::respError(["InvalidInputValue", 'field' => "category_id"], 400)],
["/entries?after_entry_id=0", null, null, [], false, V1::respError(["InvalidInputValue", 'field' => "after_entry_id"], 400)],
["/entries?before_entry_id=0", null, null, [], false, V1::respError(["InvalidInputValue", 'field' => "before_entry_id"], 400)],
["/entries?limit=-1", null, null, [], false, V1::respError(["InvalidInputValue", 'field' => "limit"], 400)],
["/entries?offset=-1", null, null, [], false, V1::respError(["InvalidInputValue", 'field' => "offset"], 400)],
["/entries?direction=sideways", null, null, [], false, V1::respError(["InvalidInputValue", 'field' => "direction"], 400)],
["/entries?order=false", null, null, [], false, V1::respError(["InvalidInputValue", 'field' => "order"], 400)],
["/entries?starred&starred", null, null, [], false, V1::respError(["DuplicateInputValue", 'field' => "starred"], 400)],
["/entries?after&after=0", null, null, [], false, V1::respError(["DuplicateInputValue", 'field' => "after"], 400)],
["/entries", $c, $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?category_id=47", (clone $c)->folder(46), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?category_id=1", (clone $c)->folderShallow(0), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=unread", (clone $c)->unread(true)->hidden(false), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=read", (clone $c)->unread(false)->hidden(false), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=removed", (clone $c)->hidden(true), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=unread&status=read", (clone $c)->hidden(false), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=unread&status=removed", new UnionContext((clone $c)->unread(true), (clone $c)->hidden(true)), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=removed&status=read", new UnionContext((clone $c)->unread(false), (clone $c)->hidden(true)), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=removed&status=read&status=removed", new UnionContext((clone $c)->unread(false), (clone $c)->hidden(true)), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?status=removed&status=read&status=unread", $c, $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?starred", (clone $c)->starred(true), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?starred=", (clone $c)->starred(true), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?starred=true", (clone $c)->starred(true), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?starred=false", (clone $c)->starred(true), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?after=0", (clone $c)->modifiedRange(0, null), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?before=0", $c, $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?before=1", (clone $c)->modifiedRange(null, 1), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?before=1&after=0", (clone $c)->modifiedRange(0, 1), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?after_entry_id=42", (clone $c)->articleRange(43, null), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?before_entry_id=47", (clone $c)->articleRange(null, 46), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?search=alpha%20beta", (clone $c)->searchTerms(["alpha", "beta"]), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?limit=4", (clone $c)->limit(4), $o, self::ENTRIES, true, HTTP::respJson(['total' => 2112, 'entries' => self::ENTRIES_OUT])],
["/entries?offset=20", (clone $c)->offset(20), $o, [], true, HTTP::respJson(['total' => 2112, 'entries' => []])],
["/entries?direction=asc", $c, $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=id", $c, ["id"], self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=published_at", $c, ["modified_date"], self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=category_id", $c, ["top_folder"], self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=category_title", $c, ["top_folder_name"], self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=status", $c, ["hidden", "unread desc"], self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?direction=desc", $c, ["modified_date desc"], self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=id&direction=desc", $c, ["id desc"], self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=published_at&direction=desc", $c, ["modified_date desc"], self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=category_id&direction=desc", $c, ["top_folder desc"], self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=category_title&direction=desc", $c, ["top_folder_name desc"], self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?order=status&direction=desc", $c, ["hidden desc", "unread"], self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/entries?category_id=2112", (clone $c)->folder(2111), $o, new ExceptionInput("idMissing"), false, V1::respError("MissingCategory")],
["/feeds/42/entries", (clone $c)->subscription(42), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/feeds/42/entries?category_id=47", (clone $c)->subscription(42)->folder(46), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/feeds/2112/entries", (clone $c)->subscription(2112), $o, new ExceptionInput("idMissing"), false, V1::respError("404", 404)],
["/categories/42/entries", (clone $c)->folder(41), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/categories/42/entries?category_id=47", (clone $c)->folder(41), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/categories/42/entries?starred", (clone $c)->folder(41)->starred(true), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/categories/1/entries", (clone $c)->folderShallow(0), $o, self::ENTRIES, false, HTTP::respJson(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])],
["/categories/2112/entries", (clone $c)->folder(2111), $o, new ExceptionInput("idMissing"), false, V1::respError("404", 404)],
];
}
@ -828,17 +831,17 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
self::clearData();
$c = new Context;
return [
["/entries/42", (clone $c)->article(42), [self::ENTRIES[1]], new Response(self::ENTRIES_OUT[1])],
["/entries/2112", (clone $c)->article(2112), new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)],
["/feeds/47/entries/42", (clone $c)->subscription(47)->article(42), [self::ENTRIES[1]], new Response(self::ENTRIES_OUT[1])],
["/feeds/47/entries/44", (clone $c)->subscription(47)->article(44), [], new ErrorResponse("404", 404)],
["/feeds/47/entries/2112", (clone $c)->subscription(47)->article(2112), new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)],
["/feeds/2112/entries/47", (clone $c)->subscription(2112)->article(47), new ExceptionInput("idMissing"), new ErrorResponse("404", 404)],
["/categories/47/entries/42", (clone $c)->folder(46)->article(42), [self::ENTRIES[1]], new Response(self::ENTRIES_OUT[1])],
["/categories/47/entries/44", (clone $c)->folder(46)->article(44), [], new ErrorResponse("404", 404)],
["/categories/47/entries/2112", (clone $c)->folder(46)->article(2112), new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)],
["/categories/2112/entries/47", (clone $c)->folder(2111)->article(47), new ExceptionInput("idMissing"), new ErrorResponse("404", 404)],
["/categories/1/entries/42", (clone $c)->folderShallow(0)->article(42), [self::ENTRIES[1]], new Response(self::ENTRIES_OUT[1])],
["/entries/42", (clone $c)->article(42), [self::ENTRIES[1]], HTTP::respJson(self::ENTRIES_OUT[1])],
["/entries/2112", (clone $c)->article(2112), new ExceptionInput("subjectMissing"), V1::respError("404", 404)],
["/feeds/47/entries/42", (clone $c)->subscription(47)->article(42), [self::ENTRIES[1]], HTTP::respJson(self::ENTRIES_OUT[1])],
["/feeds/47/entries/44", (clone $c)->subscription(47)->article(44), [], V1::respError("404", 404)],
["/feeds/47/entries/2112", (clone $c)->subscription(47)->article(2112), new ExceptionInput("subjectMissing"), V1::respError("404", 404)],
["/feeds/2112/entries/47", (clone $c)->subscription(2112)->article(47), new ExceptionInput("idMissing"), V1::respError("404", 404)],
["/categories/47/entries/42", (clone $c)->folder(46)->article(42), [self::ENTRIES[1]], HTTP::respJson(self::ENTRIES_OUT[1])],
["/categories/47/entries/44", (clone $c)->folder(46)->article(44), [], V1::respError("404", 404)],
["/categories/47/entries/2112", (clone $c)->folder(46)->article(2112), new ExceptionInput("subjectMissing"), V1::respError("404", 404)],
["/categories/2112/entries/47", (clone $c)->folder(2111)->article(47), new ExceptionInput("idMissing"), V1::respError("404", 404)],
["/categories/1/entries/42", (clone $c)->folderShallow(0)->article(42), [self::ENTRIES[1]], HTTP::respJson(self::ENTRIES_OUT[1])],
];
}
@ -856,17 +859,17 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideEntryMarkings(): iterable {
self::clearData();
return [
[['status' => "read"], null, new ErrorResponse(["MissingInputValue", 'field' => "entry_ids"], 422)],
[['entry_ids' => [1]], null, new ErrorResponse(["MissingInputValue", 'field' => "status"], 422)],
[['entry_ids' => [], 'status' => "read"], null, new ErrorResponse(["MissingInputValue", 'field' => "entry_ids"], 422)],
[['entry_ids' => 1, 'status' => "read"], null, new ErrorResponse(["InvalidInputType", 'field' => "entry_ids", 'expected' => "array", 'actual' => "integer"], 422)],
[['entry_ids' => ["1"], 'status' => "read"], null, new ErrorResponse(["InvalidInputType", 'field' => "entry_ids", 'expected' => "integer", 'actual' => "string"], 422)],
[['entry_ids' => [1], 'status' => 1], null, new ErrorResponse(["InvalidInputType", 'field' => "status", 'expected' => "string", 'actual' => "integer"], 422)],
[['entry_ids' => [0], 'status' => "read"], null, new ErrorResponse(["InvalidInputValue", 'field' => "entry_ids"], 422)],
[['entry_ids' => [1], 'status' => "reread"], null, new ErrorResponse(["InvalidInputValue", 'field' => "status"], 422)],
[['entry_ids' => [1, 2], 'status' => "read"], ['read' => true, 'hidden' => false], new EmptyResponse(204)],
[['entry_ids' => [1, 2], 'status' => "unread"], ['read' => false, 'hidden' => false], new EmptyResponse(204)],
[['entry_ids' => [1, 2], 'status' => "removed"], ['read' => true, 'hidden' => true], new EmptyResponse(204)],
[['status' => "read"], null, V1::respError(["MissingInputValue", 'field' => "entry_ids"], 422)],
[['entry_ids' => [1]], null, V1::respError(["MissingInputValue", 'field' => "status"], 422)],
[['entry_ids' => [], 'status' => "read"], null, V1::respError(["MissingInputValue", 'field' => "entry_ids"], 422)],
[['entry_ids' => 1, 'status' => "read"], null, V1::respError(["InvalidInputType", 'field' => "entry_ids", 'expected' => "array", 'actual' => "integer"], 422)],
[['entry_ids' => ["1"], 'status' => "read"], null, V1::respError(["InvalidInputType", 'field' => "entry_ids", 'expected' => "integer", 'actual' => "string"], 422)],
[['entry_ids' => [1], 'status' => 1], null, V1::respError(["InvalidInputType", 'field' => "status", 'expected' => "string", 'actual' => "integer"], 422)],
[['entry_ids' => [0], 'status' => "read"], null, V1::respError(["InvalidInputValue", 'field' => "entry_ids"], 422)],
[['entry_ids' => [1], 'status' => "reread"], null, V1::respError(["InvalidInputValue", 'field' => "status"], 422)],
[['entry_ids' => [1, 2], 'status' => "read"], ['read' => true, 'hidden' => false], HTTP::respEmpty(204)],
[['entry_ids' => [1, 2], 'status' => "unread"], ['read' => false, 'hidden' => false], HTTP::respEmpty(204)],
[['entry_ids' => [1, 2], 'status' => "removed"], ['read' => true, 'hidden' => true], HTTP::respEmpty(204)],
];
}
@ -889,13 +892,13 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
self::clearData();
$c = (new Context)->hidden(false);
return [
["/users/42/mark-all-as-read", $c, 1123, new EmptyResponse(204)],
["/users/2112/mark-all-as-read", $c, null, new ErrorResponse("403", 403)],
["/feeds/47/mark-all-as-read", (clone $c)->subscription(47), 2112, new EmptyResponse(204)],
["/feeds/2112/mark-all-as-read", (clone $c)->subscription(2112), new ExceptionInput("idMissing"), new ErrorResponse("404", 404)],
["/categories/47/mark-all-as-read", (clone $c)->folder(46), 1337, new EmptyResponse(204)],
["/categories/2112/mark-all-as-read", (clone $c)->folder(2111), new ExceptionInput("idMissing"), new ErrorResponse("404", 404)],
["/categories/1/mark-all-as-read", (clone $c)->folderShallow(0), 6666, new EmptyResponse(204)],
["/users/42/mark-all-as-read", $c, 1123, HTTP::respEmpty(204)],
["/users/2112/mark-all-as-read", $c, null, V1::respError("403", 403)],
["/feeds/47/mark-all-as-read", (clone $c)->subscription(47), 2112, HTTP::respEmpty(204)],
["/feeds/2112/mark-all-as-read", (clone $c)->subscription(2112), new ExceptionInput("idMissing"), V1::respError("404", 404)],
["/categories/47/mark-all-as-read", (clone $c)->folder(46), 1337, HTTP::respEmpty(204)],
["/categories/2112/mark-all-as-read", (clone $c)->folder(2111), new ExceptionInput("idMissing"), V1::respError("404", 404)],
["/categories/1/mark-all-as-read", (clone $c)->folderShallow(0), 6666, HTTP::respEmpty(204)],
];
}
@ -929,26 +932,26 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideBookmarkTogglings(): iterable {
self::clearData();
return [
[1, true, new EmptyResponse(204)],
[0, false, new EmptyResponse(204)],
[new ExceptionInput("subjectMissing"), null, new ErrorResponse("404", 404)],
[1, true, HTTP::respEmpty(204)],
[0, false, HTTP::respEmpty(204)],
[new ExceptionInput("subjectMissing"), null, V1::respError("404", 404)],
];
}
public function testRefreshAFeed(): void {
$this->dbMock->subscriptionPropertiesGet->returns([]);
$this->assertMessage(new EmptyResponse(204), $this->req("PUT", "/feeds/47/refresh"));
$this->assertMessage(HTTP::respEmpty(204), $this->req("PUT", "/feeds/47/refresh"));
$this->dbMock->subscriptionPropertiesGet->calledWith(Arsse::$user->id, 47);
}
public function testRefreshAMissingFeed(): void {
$this->dbMock->subscriptionPropertiesGet->throws(new ExceptionInput("subjectMissing"));
$this->assertMessage(new ErrorResponse("404", 404), $this->req("PUT", "/feeds/2112/refresh"));
$this->assertMessage(V1::respError("404", 404), $this->req("PUT", "/feeds/2112/refresh"));
$this->dbMock->subscriptionPropertiesGet->calledWith(Arsse::$user->id, 2112);
}
public function testRefreshAllFeeds(): void {
$this->assertMessage(new EmptyResponse(204), $this->req("PUT", "/feeds/refresh"));
$this->assertMessage(HTTP::respEmpty(204), $this->req("PUT", "/feeds/refresh"));
}
/** @dataProvider provideImports */
@ -964,21 +967,21 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideImports(): iterable {
self::clearData();
return [
[new ImportException("invalidSyntax"), new ErrorResponse("InvalidBodyXML", 400)],
[new ImportException("invalidSemantics"), new ErrorResponse("InvalidBodyOPML", 422)],
[new ImportException("invalidFolderName"), new ErrorResponse("InvalidImportCategory", 422)],
[new ImportException("invalidFolderCopy"), new ErrorResponse("DuplicateImportCategory", 422)],
[new ImportException("invalidTagName"), new ErrorResponse("InvalidImportLabel", 422)],
[new FeedException("invalidUrl", ['url' => "http://example.com/"]), new ErrorResponse(["FailedImportFeed", 'url' => "http://example.com/", 'code' => 10502], 502)],
[true, new Response(['message' => Arsse::$lang->msg("API.Miniflux.ImportSuccess")])],
[new ImportException("invalidSyntax"), V1::respError("InvalidBodyXML", 400)],
[new ImportException("invalidSemantics"), V1::respError("InvalidBodyOPML", 422)],
[new ImportException("invalidFolderName"), V1::respError("InvalidImportCategory", 422)],
[new ImportException("invalidFolderCopy"), V1::respError("DuplicateImportCategory", 422)],
[new ImportException("invalidTagName"), V1::respError("InvalidImportLabel", 422)],
[new FeedException("invalidUrl", ['url' => "http://example.com/"]), V1::respError(["FailedImportFeed", 'url' => "http://example.com/", 'code' => 10502], 502)],
[true, HTTP::respJson(['message' => Arsse::$lang->msg("API.Miniflux.ImportSuccess")])],
];
}
public function testExport(): void {
$opml = $this->mock(OPML::class);
$this->objMock->get->with(OPML::class)->returns($opml);
$opml->export->returns("EXPORT DATA");
$this->assertMessage(new TextResponse("EXPORT DATA", 200, ['Content-Type' => "application/xml"]), $this->req("GET", "/export"));
$opml->export->returns("<EXPORT_DATA/>");
$this->assertMessage(HTTP::respText("<EXPORT_DATA/>", 200, ['Content-Type' => "application/xml"]), $this->req("GET", "/export"));
$opml->export->calledWith(Arsse::$user->id);
}
}

149
tests/cases/REST/NextcloudNews/TestV1_2.php

@ -11,13 +11,12 @@ use JKingWeb\Arsse\User;
use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Test\Result;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\HTTP;
use JKingWeb\Arsse\Context\Context;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\Transaction;
use JKingWeb\Arsse\REST\NextcloudNews\V1_2;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\Response\JsonResponse as Response;
use Laminas\Diactoros\Response\EmptyResponse;
/** @covers \JKingWeb\Arsse\REST\NextcloudNews\V1_2<extended> */
class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
@ -336,13 +335,13 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
}
public function testSendAuthenticationChallenge(): void {
$exp = new EmptyResponse(401);
$exp = HTTP::respEmpty(401);
$this->assertMessage($exp, $this->req("GET", "/", "", [], false));
}
/** @dataProvider provideInvalidPaths */
public function testRespondToInvalidPaths($path, $method, $code, $allow = null): void {
$exp = new EmptyResponse($code, $allow ? ['Allow' => $allow] : []);
$exp = HTTP::respEmpty($code, $allow ? ['Allow' => $allow] : []);
$this->assertMessage($exp, $this->req($method, $path));
}
@ -374,16 +373,16 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
}
public function testRespondToInvalidInputTypes(): void {
$exp = new EmptyResponse(415, ['Accept' => "application/json"]);
$exp = HTTP::respEmpty(415, ['Accept' => "application/json"]);
$this->assertMessage($exp, $this->req("PUT", "/folders/1", '<data/>', ['Content-Type' => "application/xml"]));
$exp = new EmptyResponse(400);
$exp = HTTP::respEmpty(400);
$this->assertMessage($exp, $this->req("PUT", "/folders/1", '<data/>'));
$this->assertMessage($exp, $this->req("PUT", "/folders/1", '<data/>', ['Content-Type' => null]));
}
/** @dataProvider provideOptionsRequests */
public function testRespondToOptionsRequests(string $url, string $allow, string $accept): void {
$exp = new EmptyResponse(204, [
$exp = HTTP::respEmpty(204, [
'Allow' => $allow,
'Accept' => $accept,
]);
@ -408,7 +407,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
['id' => 12, 'name' => "Hardware"],
];
$this->dbMock->folderList->with($this->userId, null, false)->returns(new Result($this->v($list)));
$exp = new Response(['folders' => $out]);
$exp = HTTP::respJson(['folders' => $out]);
$this->assertMessage($exp, $this->req("GET", "/folders"));
}
@ -432,23 +431,23 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideFolderCreations(): array {
return [
[['name' => "Software"], true, 1, new Response(['folders' => [['id' => 1, 'name' => "Software"]]])],
[['name' => "Software"], false, 1, new Response(['folders' => [['id' => 1, 'name' => "Software"]]])],
[['name' => "Hardware"], true, "2", new Response(['folders' => [['id' => 2, 'name' => "Hardware"]]])],
[['name' => "Hardware"], false, "2", new Response(['folders' => [['id' => 2, 'name' => "Hardware"]]])],
[['name' => "Software"], true, new ExceptionInput("constraintViolation"), new EmptyResponse(409)],
[['name' => ""], true, new ExceptionInput("whitespace"), new EmptyResponse(422)],
[['name' => " "], true, new ExceptionInput("whitespace"), new EmptyResponse(422)],
[['name' => null], true, new ExceptionInput("missing"), new EmptyResponse(422)],
[['name' => "Software"], true, 1, HTTP::respJson(['folders' => [['id' => 1, 'name' => "Software"]]])],
[['name' => "Software"], false, 1, HTTP::respJson(['folders' => [['id' => 1, 'name' => "Software"]]])],
[['name' => "Hardware"], true, "2", HTTP::respJson(['folders' => [['id' => 2, 'name' => "Hardware"]]])],
[['name' => "Hardware"], false, "2", HTTP::respJson(['folders' => [['id' => 2, 'name' => "Hardware"]]])],
[['name' => "Software"], true, new ExceptionInput("constraintViolation"), HTTP::respEmpty(409)],
[['name' => ""], true, new ExceptionInput("whitespace"), HTTP::respEmpty(422)],
[['name' => " "], true, new ExceptionInput("whitespace"), HTTP::respEmpty(422)],
[['name' => null], true, new ExceptionInput("missing"), HTTP::respEmpty(422)],
];
}
public function testRemoveAFolder(): void {
$this->dbMock->folderRemove->with($this->userId, 1)->returns(true)->throws(new ExceptionInput("subjectMissing"));
$exp = new EmptyResponse(204);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("DELETE", "/folders/1"));
// fail on the second invocation because it no longer exists
$exp = new EmptyResponse(404);
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->req("DELETE", "/folders/1"));
$this->dbMock->folderRemove->times(2)->calledWith($this->userId, 1);
}
@ -467,17 +466,17 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideFolderRenamings(): array {
return [
[['name' => "Software"], 1, true, new EmptyResponse(204)],
[['name' => "Software"], 2, new ExceptionInput("constraintViolation"), new EmptyResponse(409)],
[['name' => "Software"], 3, new ExceptionInput("subjectMissing"), new EmptyResponse(404)],
[['name' => ""], 2, new ExceptionInput("whitespace"), new EmptyResponse(422)],
[['name' => " "], 2, new ExceptionInput("whitespace"), new EmptyResponse(422)],
[['name' => null], 2, new ExceptionInput("missing"), new EmptyResponse(422)],
[['name' => "Software"], 1, true, HTTP::respEmpty(204)],
[['name' => "Software"], 2, new ExceptionInput("constraintViolation"), HTTP::respEmpty(409)],
[['name' => "Software"], 3, new ExceptionInput("subjectMissing"), HTTP::respEmpty(404)],
[['name' => ""], 2, new ExceptionInput("whitespace"), HTTP::respEmpty(422)],
[['name' => " "], 2, new ExceptionInput("whitespace"), HTTP::respEmpty(422)],
[['name' => null], 2, new ExceptionInput("missing"), HTTP::respEmpty(422)],
];
}
public function testRetrieveServerVersion(): void {
$exp = new Response([
$exp = HTTP::respJson([
'version' => V1_2::VERSION,
'arsse_version' => Arsse::VERSION,
]);
@ -497,9 +496,9 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->subscriptionList->with($this->userId)->returns(new Result([]))->returns(new Result($this->v($this->feeds['db'])));
$this->dbMock->articleStarred->with($this->userId)->returns($this->v(['total' => 0]))->returns($this->v(['total' => 5]));
$this->dbMock->editionLatest->with($this->userId)->returns(0)->returns(4758915);
$exp = new Response($exp1);
$exp = HTTP::respJson($exp1);
$this->assertMessage($exp, $this->req("GET", "/feeds"));
$exp = new Response($exp2);
$exp = HTTP::respJson($exp2);
$this->assertMessage($exp, $this->req("GET", "/feeds"));
}
@ -538,21 +537,21 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideNewSubscriptions(): array {
$feedException = new \JKingWeb\Arsse\Feed\Exception("", [], new \PicoFeed\Reader\SubscriptionNotFoundException);
return [
[['url' => "http://example.com/news.atom", 'folderId' => 3], 2112, 0, $this->feeds['db'][0], new ExceptionInput("idMissing"), new Response(['feeds' => [$this->feeds['rest'][0]]])],
[['url' => "http://example.org/news.atom", 'folderId' => 8], 42, 4758915, $this->feeds['db'][1], true, new Response(['feeds' => [$this->feeds['rest'][1]], 'newestItemId' => 4758915])],
[['url' => "http://example.com/news.atom", 'folderId' => 3], new ExceptionInput("constraintViolation"), 0, $this->feeds['db'][0], new ExceptionInput("idMissing"), new EmptyResponse(409)],
[['url' => "http://example.org/news.atom", 'folderId' => 8], new ExceptionInput("constraintViolation"), 4758915, $this->feeds['db'][1], true, new EmptyResponse(409)],
[[], $feedException, 0, [], false, new EmptyResponse(422)],
[['url' => "http://example.net/news.atom", 'folderId' => -1], 47, 2112, $this->feeds['db'][2], new ExceptionInput("typeViolation"), new Response(['feeds' => [$this->feeds['rest'][2]], 'newestItemId' => 2112])],
[['url' => "http://example.com/news.atom", 'folderId' => 3], 2112, 0, $this->feeds['db'][0], new ExceptionInput("idMissing"), HTTP::respJson(['feeds' => [$this->feeds['rest'][0]]])],
[['url' => "http://example.org/news.atom", 'folderId' => 8], 42, 4758915, $this->feeds['db'][1], true, HTTP::respJson(['feeds' => [$this->feeds['rest'][1]], 'newestItemId' => 4758915])],
[['url' => "http://example.com/news.atom", 'folderId' => 3], new ExceptionInput("constraintViolation"), 0, $this->feeds['db'][0], new ExceptionInput("idMissing"), HTTP::respEmpty(409)],
[['url' => "http://example.org/news.atom", 'folderId' => 8], new ExceptionInput("constraintViolation"), 4758915, $this->feeds['db'][1], true, HTTP::respEmpty(409)],
[[], $feedException, 0, [], false, HTTP::respEmpty(422)],
[['url' => "http://example.net/news.atom", 'folderId' => -1], 47, 2112, $this->feeds['db'][2], new ExceptionInput("typeViolation"), HTTP::respJson(['feeds' => [$this->feeds['rest'][2]], 'newestItemId' => 2112])],
];
}
public function testRemoveASubscription(): void {
$this->dbMock->subscriptionRemove->with($this->userId, 1)->returns(true)->throws(new ExceptionInput("subjectMissing"));
$exp = new EmptyResponse(204);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("DELETE", "/feeds/1"));
// fail on the second invocation because it no longer exists
$exp = new EmptyResponse(404);
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->req("DELETE", "/feeds/1"));
$this->dbMock->subscriptionRemove->times(2)->calledWith($this->userId, 1);
}
@ -571,17 +570,17 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->subscriptionPropertiesSet->with($this->userId, 1, ['folder' => 2112])->throws(new ExceptionInput("idMissing")); // folder does not exist
$this->dbMock->subscriptionPropertiesSet->with($this->userId, 1, ['folder' => -1])->throws(new ExceptionInput("typeViolation")); // folder is invalid
$this->dbMock->subscriptionPropertiesSet->with($this->userId, 42, $this->anything())->throws(new ExceptionInput("subjectMissing")); // subscription does not exist
$exp = new EmptyResponse(204);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[0])));
$exp = new EmptyResponse(204);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[1])));
$exp = new EmptyResponse(422);
$exp = HTTP::respEmpty(422);
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[2])));
$exp = new EmptyResponse(404);
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->req("PUT", "/feeds/42/move", json_encode($in[3])));
$exp = new EmptyResponse(422);
$exp = HTTP::respEmpty(422);
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[4])));
$exp = new EmptyResponse(422);
$exp = HTTP::respEmpty(422);
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/move", json_encode($in[5])));
}
@ -601,17 +600,17 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->subscriptionPropertiesSet->with($this->userId, 1, $this->identicalTo(['title' => ""]))->throws(new ExceptionInput("missing"));
$this->dbMock->subscriptionPropertiesSet->with($this->userId, 1, $this->identicalTo(['title' => false]))->throws(new ExceptionInput("missing"));
$this->dbMock->subscriptionPropertiesSet->with($this->userId, 42, $this->anything())->throws(new ExceptionInput("subjectMissing"));
$exp = new EmptyResponse(422);
$exp = HTTP::respEmpty(422);
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[0])));
$exp = new EmptyResponse(204);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[1])));
$exp = new EmptyResponse(422);
$exp = HTTP::respEmpty(422);
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[2])));
$exp = new EmptyResponse(422);
$exp = HTTP::respEmpty(422);
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[3])));
$exp = new EmptyResponse(404);
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->req("PUT", "/feeds/42/rename", json_encode($in[4])));
$exp = new EmptyResponse(422);
$exp = HTTP::respEmpty(422);
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/rename", json_encode($in[6])));
}
@ -627,13 +626,13 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
],
];
$this->dbMock->feedListStale->returns($this->v(array_column($out, "id")));
$exp = new Response(['feeds' => $out]);
$exp = HTTP::respJson(['feeds' => $out]);
$this->assertMessage($exp, $this->req("GET", "/feeds/all"));
}
public function testListStaleFeedsWithoutAuthority(): void {
$this->userMock->propertiesGet->returns(['admin' => false]);
$exp = new EmptyResponse(403);
$exp = HTTP::respEmpty(403);
$this->assertMessage($exp, $this->req("GET", "/feeds/all"));
$this->dbMock->feedListStale->never()->called();
}
@ -649,11 +648,11 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->feedUpdate->with(42)->returns(true);
$this->dbMock->feedUpdate->with(2112)->throws(new ExceptionInput("subjectMissing"));
$this->dbMock->feedUpdate->with($this->lessThan(1))->throws(new ExceptionInput("typeViolation"));
$exp = new EmptyResponse(204);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[0])));
$exp = new EmptyResponse(404);
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[1])));
$exp = new EmptyResponse(422);
$exp = HTTP::respEmpty(422);
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[2])));
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[3])));
$this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[4])));
@ -661,7 +660,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
public function testUpdateAFeedWithoutAuthority(): void {
$this->userMock->propertiesGet->returns(['admin' => false]);
$exp = new EmptyResponse(403);
$exp = HTTP::respEmpty(403);
$this->assertMessage($exp, $this->req("GET", "/feeds/update", ['feedId' => 42]));
$this->dbMock->feedUpdate->never()->called();
}
@ -683,8 +682,8 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$c = (new Context)->hidden(false);
$t = Date::normalize(time());
$out = new Result($this->v($this->articles['db']));
$r200 = new Response(['items' => $this->articles['rest']]);
$r422 = new EmptyResponse(422);
$r200 = HTTP::respJson(['items' => $this->articles['rest']]);
$r422 = HTTP::respEmpty(422);
return [
["/items", [], clone $c, $out, $r200],
["/items", ['type' => 0, 'id' => 42], (clone $c)->subscription(42), new ExceptionInput("idMissing"), $r422],
@ -720,13 +719,13 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$in = json_encode(['newestItemId' => 2112]);
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->folder(1)->editionRange(null, 2112)->hidden(false)))->returns(42);
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->folder(42)->editionRange(null, 2112)->hidden(false)))->throws(new ExceptionInput("idMissing")); // folder doesn't exist
$exp = new EmptyResponse(204);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("PUT", "/folders/1/read", $in));
$this->assertMessage($exp, $this->req("PUT", "/folders/1/read?newestItemId=2112"));
$exp = new EmptyResponse(422);
$exp = HTTP::respEmpty(422);
$this->assertMessage($exp, $this->req("PUT", "/folders/1/read"));
$this->assertMessage($exp, $this->req("PUT", "/folders/1/read?newestItemId=ook"));
$exp = new EmptyResponse(404);
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->req("PUT", "/folders/42/read", $in));
}
@ -735,13 +734,13 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$in = json_encode(['newestItemId' => 2112]);
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->subscription(1)->editionRange(null, 2112)->hidden(false)))->returns(42);
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->subscription(42)->editionRange(null, 2112)->hidden(false)))->throws(new ExceptionInput("idMissing")); // subscription doesn't exist
$exp = new EmptyResponse(204);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/read", $in));
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/read?newestItemId=2112"));
$exp = new EmptyResponse(422);
$exp = HTTP::respEmpty(422);
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/read"));
$this->assertMessage($exp, $this->req("PUT", "/feeds/1/read?newestItemId=ook"));
$exp = new EmptyResponse(404);
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->req("PUT", "/feeds/42/read", $in));
}
@ -749,10 +748,10 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$read = ['read' => true];
$in = json_encode(['newestItemId' => 2112]);
$this->dbMock->articleMark->with($this->userId, $read, $this->equalTo((new Context)->editionRange(null, 2112)))->returns(42);
$exp = new EmptyResponse(204);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("PUT", "/items/read", $in));
$this->assertMessage($exp, $this->req("PUT", "/items/read?newestItemId=2112"));
$exp = new EmptyResponse(422);
$exp = HTTP::respEmpty(422);
$this->assertMessage($exp, $this->req("PUT", "/items/read"));
$this->assertMessage($exp, $this->req("PUT", "/items/read?newestItemId=ook"));
}
@ -770,12 +769,12 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->articleMark->with($this->userId, $star, $this->equalTo((new Context)->article(2112)))->throws(new ExceptionInput("subjectMissing")); // article doesn't exist doesn't exist
$this->dbMock->articleMark->with($this->userId, $unstar, $this->equalTo((new Context)->article(4)))->returns(42);
$this->dbMock->articleMark->with($this->userId, $unstar, $this->equalTo((new Context)->article(1337)))->throws(new ExceptionInput("subjectMissing")); // article doesn't exist doesn't exist
$exp = new EmptyResponse(204);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("PUT", "/items/1/read"));
$this->assertMessage($exp, $this->req("PUT", "/items/2/unread"));
$this->assertMessage($exp, $this->req("PUT", "/items/1/3/star"));
$this->assertMessage($exp, $this->req("PUT", "/items/4400/4/unstar"));
$exp = new EmptyResponse(404);
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->req("PUT", "/items/42/read"));
$this->assertMessage($exp, $this->req("PUT", "/items/47/unread"));
$this->assertMessage($exp, $this->req("PUT", "/items/1/2112/star"));
@ -801,7 +800,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->articleMark->with($this->userId, $this->anything(), $this->anything())->returns(42);
$this->dbMock->articleMark->with($this->userId, $this->anything(), $this->equalTo((new Context)->editions([])))->throws(new ExceptionInput("tooShort")); // data model function requires one valid integer for multiples
$this->dbMock->articleMark->with($this->userId, $this->anything(), $this->equalTo((new Context)->articles([])))->throws(new ExceptionInput("tooShort")); // data model function requires one valid integer for multiples
$exp = new EmptyResponse(204);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("PUT", "/items/read/multiple"));
$this->assertMessage($exp, $this->req("PUT", "/items/unread/multiple"));
$this->assertMessage($exp, $this->req("PUT", "/items/star/multiple"));
@ -854,44 +853,44 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
];
$arr2['warnings']['improperlyConfiguredCron'] = true;
$arr2['warnings']['incorrectDbCharset'] = true;
$exp = new Response($arr1);
$exp = HTTP::respJson($arr1);
$this->assertMessage($exp, $this->req("GET", "/status"));
}
public function testCleanUpBeforeUpdate(): void {
$this->dbMock->feedCleanup->with()->returns(true);
$exp = new EmptyResponse(204);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("GET", "/cleanup/before-update"));
$this->dbMock->feedCleanup->calledWith();
}
public function testCleanUpBeforeUpdateWithoutAuthority(): void {
$this->userMock->propertiesGet->returns(['admin' => false]);
$exp = new EmptyResponse(403);
$exp = HTTP::respEmpty(403);
$this->assertMessage($exp, $this->req("GET", "/cleanup/before-update"));
$this->dbMock->feedCleanup->never()->called();
}
public function testCleanUpAfterUpdate(): void {
$this->dbMock->articleCleanup->with()->returns(true);
$exp = new EmptyResponse(204);
$exp = HTTP::respEmpty(204);
$this->assertMessage($exp, $this->req("GET", "/cleanup/after-update"));
$this->dbMock->articleCleanup->calledWith();
}
public function testCleanUpAfterUpdateWithoutAuthority(): void {
$this->userMock->propertiesGet->returns(['admin' => false]);
$exp = new EmptyResponse(403);
$exp = HTTP::respEmpty(403);
$this->assertMessage($exp, $this->req("GET", "/cleanup/after-update"));
$this->dbMock->feedCleanup->never()->called();
}
public function testQueryTheUserStatus(): void {
$act = $this->req("GET", "/user");
$exp = new Response([
$exp = HTTP::respJson([
'userId' => $this->userId,
'displayName' => $this->userId,
'lastLoginTimestamp' => $this->approximateTime($act->getPayload()['lastLoginTimestamp'], new \DateTimeImmutable),
'lastLoginTimestamp' => $this->approximateTime(json_decode((string) $act->getBody(), true)['lastLoginTimestamp'], new \DateTimeImmutable),
'avatar' => null,
]);
$this->assertMessage($exp, $act);
@ -906,8 +905,8 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->folderAdd->with($this->anything(), $in)->returns(1);
$this->dbMock->folderPropertiesGet->with($this->userId, 1)->returns($this->v($out1));
$this->dbMock->folderPropertiesGet->with($this->userId, 2)->returns($this->v($out2));
$exp = new Response(['folders' => [$out1]]);
$this->assertMessage($exp, $this->req("POST", "/folders?name=Hardware", json_encode($in)));
$exp = HTTP::respJson(['folders' => [$out1]]);
$this->assertMessage($exp, $this->req("POST", $url, json_encode($in)));
}
public function testMeldJsonAndQueryParameters(): void {

11
tests/cases/REST/NextcloudNews/TestVersions.php

@ -6,10 +6,9 @@
declare(strict_types=1);
namespace JKingWeb\Arsse\TestCase\REST\NextcloudNews;
use JKingWeb\Arsse\Misc\HTTP;
use JKingWeb\Arsse\REST\NextcloudNews\Versions;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\Response\JsonResponse as Response;
use Laminas\Diactoros\Response\EmptyResponse;
/** @covers \JKingWeb\Arsse\REST\NextcloudNews\Versions */
class TestVersions extends \JKingWeb\Arsse\Test\AbstractTest {
@ -25,24 +24,24 @@ class TestVersions extends \JKingWeb\Arsse\Test\AbstractTest {
}
public function testFetchVersionList(): void {
$exp = new Response(['apiLevels' => ['v1-2']]);
$exp = HTTP::respJson(['apiLevels' => ['v1-2']]);
$this->assertMessage($exp, $this->req("GET", "/"));
$this->assertMessage($exp, $this->req("GET", "/"));
$this->assertMessage($exp, $this->req("GET", "/"));
}
public function testRespondToOptionsRequest(): void {
$exp = new EmptyResponse(204, ['Allow' => "HEAD,GET"]);
$exp = HTTP::respEmpty(204, ['Allow' => "HEAD,GET"]);
$this->assertMessage($exp, $this->req("OPTIONS", "/"));
}
public function testUseIncorrectMethod(): void {
$exp = new EmptyResponse(405, ['Allow' => "HEAD,GET"]);
$exp = HTTP::respEmpty(405, ['Allow' => "HEAD,GET"]);
$this->assertMessage($exp, $this->req("POST", "/"));
}
public function testUseIncorrectPath(): void {
$exp = new EmptyResponse(404);
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->req("GET", "/ook"));
$this->assertMessage($exp, $this->req("OPTIONS", "/ook"));
}

69
tests/cases/REST/TestREST.php

@ -12,13 +12,12 @@ use JKingWeb\Arsse\REST;
use JKingWeb\Arsse\REST\Exception501;
use JKingWeb\Arsse\REST\NextcloudNews\V1_2 as NCN;
use JKingWeb\Arsse\REST\TinyTinyRSS\API as TTRSS;
use JKingWeb\Arsse\Misc\HTTP;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\Request;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequest;
use Laminas\Diactoros\Response\TextResponse;
use Laminas\Diactoros\Response\EmptyResponse;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\ServerRequest;
/** @covers \JKingWeb\Arsse\REST */
class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
@ -69,7 +68,7 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
$this->userMock->auth->with("someone.else@example.com", "")->returns(true);
Arsse::$user = $this->userMock->get();
// create an input server request
$req = new ServerRequest($serverParams);
$req = new ServerRequest("GET", "/", [], null, "1.1", $serverParams);
// create the expected output
$exp = $req;
foreach ($expAttr as $key => $value) {
@ -95,7 +94,7 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
public function testSendAuthenticationChallenges(): void {
self::setConf();
$r = new REST;
$in = new EmptyResponse(401);
$in = HTTP::respEmpty(401);
$exp = $in->withHeader("WWW-Authenticate", 'Basic realm="OOK", charset="UTF-8"');
$act = $r->challenge($in, "OOK");
$this->assertMessage($exp, $act);
@ -155,7 +154,7 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
return $origin;
});
$headers = isset($origin) ? ['Origin' => $origin] : [];
$req = new Request("", "GET", "php://memory", $headers);
$req = new Request("GET", "", $headers);
$act = $rMock->get()->corsNegotiate($req, $allowed, $denied);
$this->assertSame($exp, $act);
}
@ -188,9 +187,9 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
/** @dataProvider provideCorsHeaders */
public function testAddCorsHeaders(string $reqMethod, array $reqHeaders, array $resHeaders, array $expHeaders): void {
$r = new REST;
$req = new Request("", $reqMethod, "php://memory", $reqHeaders);
$res = new EmptyResponse(204, $resHeaders);
$exp = new EmptyResponse(204, $expHeaders);
$req = new Request($reqMethod, "php://memory", $reqHeaders);
$res = HTTP::respEmpty(204, $resHeaders);
$exp = HTTP::respEmpty(204, $expHeaders);
$act = $r->corsApply($res, $req);
$this->assertMessage($exp, $act);
}
@ -242,7 +241,7 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
["OPTIONS", ['Origin' => "http://example", 'Access-Control-Request-Headers' => ["Content-Type", "If-None-Match"]], [], [
'Access-Control-Allow-Origin' => "http://example",
'Access-Control-Allow-Credentials' => "true",
'Access-Control-Allow-Headers' => "Content-Type,If-None-Match",
'Access-Control-Allow-Headers' => "Content-Type, If-None-Match",
'Access-Control-Max-Age' => (string) (60 * 60 * 24),
'Vary' => "Origin",
]],
@ -267,21 +266,21 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
$stream = fopen("php://memory", "w+b");
fwrite($stream, "ook");
return [
[new EmptyResponse(204), new EmptyResponse(204)],
[new EmptyResponse(401), new EmptyResponse(401, ['WWW-Authenticate' => "Fake Value"])],
[new EmptyResponse(204, ['Allow' => "PUT"]), new EmptyResponse(204, ['Allow' => "PUT, OPTIONS"])],
[new EmptyResponse(204, ['Allow' => "PUT, OPTIONS"]), new EmptyResponse(204, ['Allow' => "PUT, OPTIONS"])],
[new EmptyResponse(204, ['Allow' => "PUT,OPTIONS"]), new EmptyResponse(204, ['Allow' => "PUT, OPTIONS"])],
[new EmptyResponse(204, ['Allow' => ["PUT", "OPTIONS"]]), new EmptyResponse(204, ['Allow' => "PUT, OPTIONS"])],
[new EmptyResponse(204, ['Allow' => ["PUT, DELETE", "OPTIONS"]]), new EmptyResponse(204, ['Allow' => "PUT, DELETE, OPTIONS"])],
[new EmptyResponse(204, ['Allow' => "HEAD,GET"]), new EmptyResponse(204, ['Allow' => "HEAD, GET, OPTIONS"])],
[new EmptyResponse(204, ['Allow' => "GET"]), new EmptyResponse(204, ['Allow' => "GET, HEAD, OPTIONS"])],
[new TextResponse("ook", 200), new TextResponse("ook", 200, ['Content-Length' => "3"])],
[new TextResponse("", 200), new TextResponse("", 200, ['Content-Length' => "0"])],
[new TextResponse("ook", 404), new TextResponse("ook", 404, ['Content-Length' => "3"])],
[new TextResponse("", 404), new TextResponse("", 404)],
[new Response($stream, 200), new Response($stream, 200, ['Content-Length' => "3"]), new Request("", "GET")],
[new Response($stream, 200), new EmptyResponse(200, ['Content-Length' => "3"]), new Request("", "HEAD")],
[HTTP::respEmpty(204), HTTP::respEmpty(204)],
[HTTP::respEmpty(401), HTTP::respEmpty(401, ['WWW-Authenticate' => "Fake Value"])],
[HTTP::respEmpty(204, ['Allow' => "PUT"]), HTTP::respEmpty(204, ['Allow' => "PUT, OPTIONS"])],
[HTTP::respEmpty(204, ['Allow' => "PUT, OPTIONS"]), HTTP::respEmpty(204, ['Allow' => "PUT, OPTIONS"])],
[HTTP::respEmpty(204, ['Allow' => "PUT,OPTIONS"]), HTTP::respEmpty(204, ['Allow' => "PUT, OPTIONS"])],
[HTTP::respEmpty(204, ['Allow' => ["PUT", "OPTIONS"]]), HTTP::respEmpty(204, ['Allow' => "PUT, OPTIONS"])],
[HTTP::respEmpty(204, ['Allow' => ["PUT, DELETE", "OPTIONS"]]), HTTP::respEmpty(204, ['Allow' => "PUT, DELETE, OPTIONS"])],
[HTTP::respEmpty(204, ['Allow' => "HEAD,GET"]), HTTP::respEmpty(204, ['Allow' => "HEAD, GET, OPTIONS"])],
[HTTP::respEmpty(204, ['Allow' => "GET"]), HTTP::respEmpty(204, ['Allow' => "GET, HEAD, OPTIONS"])],
[HTTP::respText("ook", 200), HTTP::respText("ook", 200, ['Content-Length' => "3"])],
[HTTP::respText("", 200), HTTP::respText("", 200, ['Content-Length' => "0"])],
[HTTP::respText("ook", 404), HTTP::respText("ook", 404, ['Content-Length' => "3"])],
[HTTP::respText("", 404), HTTP::respText("", 404)],
[new Response(200, [], $stream), new Response(200, ['Content-Length' => "3"], $stream), new Request("GET", "")],
[new Response(200, [], $stream), HTTP::respEmpty(200, ['Content-Length' => "3"]), new Request("HEAD", "")],
];
}
@ -296,7 +295,7 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
});
if ($called) {
$hMock = $this->mock($class);
$hMock->dispatch->returns(new EmptyResponse(204));
$hMock->dispatch->returns(HTTP::respEmpty(204));
$this->objMock->get->with($class)->returns($hMock);
Arsse::$obj = $this->objMock->get();
}
@ -317,13 +316,13 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest {
public function provideMockRequests(): iterable {
return [
[new ServerRequest([], [], "/index.php/apps/news/api/v1-2/feeds", "GET"), "GET", true, NCN::class, "/feeds"],
[new ServerRequest([], [], "/index.php/apps/news/api/v1-2/feeds", "HEAD"), "GET", true, NCN::class, "/feeds"],
[new ServerRequest([], [], "/index.php/apps/news/api/v1-2/feeds", "get"), "GET", true, NCN::class, "/feeds"],
[new ServerRequest([], [], "/index.php/apps/news/api/v1-2/feeds", "head"), "GET", true, NCN::class, "/feeds"],
[new ServerRequest([], [], "/tt-rss/api/", "POST"), "POST", true, TTRSS::class, "/"],
[new ServerRequest([], [], "/no/such/api/", "HEAD"), "GET", false],
[new ServerRequest([], [], "/no/such/api/", "GET"), "GET", false],
[new ServerRequest("GET", "/index.php/apps/news/api/v1-2/feeds"), "GET", true, NCN::class, "/feeds"],
[new ServerRequest("GET", "/index.php/apps/news/api/v1-2/feeds"), "GET", true, NCN::class, "/feeds"],
[new ServerRequest("get", "/index.php/apps/news/api/v1-2/feeds"), "GET", true, NCN::class, "/feeds"],
[new ServerRequest("head", "/index.php/apps/news/api/v1-2/feeds"), "GET", true, NCN::class, "/feeds"],
[new ServerRequest("POST", "/tt-rss/api/"), "POST", true, TTRSS::class, "/"],
[new ServerRequest("HEAD", "/no/such/api/"), "GET", false],
[new ServerRequest("GET", "/no/such/api/"), "GET", false],
];
}
}

57
tests/cases/REST/TinyTinyRSS/TestAPI.php

@ -11,14 +11,13 @@ use JKingWeb\Arsse\User;
use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Test\Result;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\HTTP;
use JKingWeb\Arsse\Context\Context;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\Transaction;
use JKingWeb\Arsse\REST\TinyTinyRSS\API;
use JKingWeb\Arsse\Feed\Exception as FeedException;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\Response\JsonResponse as Response;
use Laminas\Diactoros\Response\EmptyResponse;
/** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\API<extended>
* @covers \JKingWeb\Arsse\REST\TinyTinyRSS\Exception */
@ -166,17 +165,17 @@ LONG_STRING;
return $this->req($data, "POST", "", null, $user);
}
protected function respGood($content = null, $seq = 0): Response {
return new Response([
protected function respGood($content = null, $seq = 0): ResponseInterface {
return HTTP::respJson([
'seq' => $seq,
'status' => 0,
'content' => $content,
]);
}
protected function respErr(string $msg, $content = [], $seq = 0): Response {
protected function respErr(string $msg, $content = [], $seq = 0): ResponseInterface {
$err = ['error' => $msg];
return new Response([
return HTTP::respJson([
'seq' => $seq,
'status' => 1,
'content' => array_merge($err, $content, $err),
@ -188,12 +187,12 @@ LONG_STRING;
$this->assertMessage($exp, $this->req(null, "POST", "", ""));
$this->assertMessage($exp, $this->req(null, "POST", "/", ""));
$this->assertMessage($exp, $this->req(null, "POST", "/index.php", ""));
$exp = new EmptyResponse(404);
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->req(null, "POST", "/bad/path", ""));
}
public function testHandleOptionsRequest(): void {
$exp = new EmptyResponse(204, [
$exp = HTTP::respEmpty(204, [
'Allow' => "POST",
'Accept' => "application/json, text/json",
]);
@ -215,7 +214,7 @@ LONG_STRING;
$this->userMock->auth->with("jane.doe@example.com", "superman")->returns(true);
$this->dbMock->sessionCreate->with("john.doe@example.com")->returns("PriestsOfSyrinx", "SolarFederation");
$this->dbMock->sessionCreate->with("jane.doe@example.com")->returns("ClockworkAngels", "SevenCitiesOfGold");
if ($sessions instanceof EmptyResponse) {
if ($sessions instanceof ResponseInterface) {
$exp1 = $sessions;
$exp2 = $sessions;
} elseif ($sessions) {
@ -260,7 +259,7 @@ LONG_STRING;
'op' => "isLoggedIn",
'sid' => $data,
];
if ($result instanceof EmptyResponse) {
if ($result instanceof ResponseInterface) {
$exp1 = $result;
$exp2 = null;
} elseif ($result) {
@ -333,7 +332,7 @@ LONG_STRING;
'userHTTPAuthRequired' => true,
'userSessionEnforced' => false,
];
$http401 = new EmptyResponse(401);
$http401 = HTTP::respEmpty(401);
if ($type === "login") {
return [
// conf, user, data, result
@ -532,7 +531,7 @@ LONG_STRING;
'user' => $this->userId,
'password' => "secret",
];
$exp = new EmptyResponse(500);
$exp = HTTP::respEmpty(500);
$this->assertMessage($exp, $this->req($data));
}
@ -1686,10 +1685,10 @@ LONG_STRING;
$this->assertMessage($this->outputHeadlines(1), $test);
// test 'show_content'
$test = $this->req($in[1]);
$this->assertArrayHasKey("content", $test->getPayload()['content'][0]);
$this->assertArrayHasKey("content", $test->getPayload()['content'][1]);
$this->assertArrayHasKey("content", $this->extractMessageJson($test)['content'][0]);
$this->assertArrayHasKey("content", $this->extractMessageJson($test)['content'][1]);
foreach ($this->generateHeadlines(1) as $key => $row) {
$this->assertSame($row['content'], $test->getPayload()['content'][$key]['content']);
$this->assertSame($row['content'], $this->extractMessageJson($test)['content'][$key]['content']);
}
// test 'include_attachments'
$test = $this->req($in[2]);
@ -1705,22 +1704,22 @@ LONG_STRING;
'post_id' => "2112",
],
];
$this->assertArrayHasKey("attachments", $test->getPayload()['content'][0]);
$this->assertArrayHasKey("attachments", $test->getPayload()['content'][1]);
$this->assertSame([], $test->getPayload()['content'][0]['attachments']);
$this->assertSame($exp, $test->getPayload()['content'][1]['attachments']);
$this->assertArrayHasKey("attachments", $this->extractMessageJson($test)['content'][0]);
$this->assertArrayHasKey("attachments", $this->extractMessageJson($test)['content'][1]);
$this->assertSame([], $this->extractMessageJson($test)['content'][0]['attachments']);
$this->assertSame($exp, $this->extractMessageJson($test)['content'][1]['attachments']);
// test 'include_header'
$test = $this->req($in[3]);
$exp = $this->respGood([
['id' => -4, 'is_cat' => false, 'first_id' => 1],
$this->outputHeadlines(1)->getPayload()['content'],
$this->extractMessageJson($this->outputHeadlines(1))['content'],
]);
$this->assertMessage($exp, $test);
// test 'include_header' with a category
$test = $this->req($in[4]);
$exp = $this->respGood([
['id' => -3, 'is_cat' => true, 'first_id' => 1],
$this->outputHeadlines(1)->getPayload()['content'],
$this->extractMessageJson($this->outputHeadlines(1))['content'],
]);
$this->assertMessage($exp, $test);
// test 'include_header' with an empty result
@ -1742,7 +1741,7 @@ LONG_STRING;
$test = $this->req($in[7]);
$exp = $this->respGood([
['id' => -4, 'is_cat' => false, 'first_id' => 0],
$this->outputHeadlines(1)->getPayload()['content'],
$this->extractMessageJson($this->outputHeadlines(1))['content'],
]);
$this->assertMessage($exp, $test);
// test 'include_header' with skip
@ -1750,24 +1749,24 @@ LONG_STRING;
$test = $this->req($in[8]);
$exp = $this->respGood([
['id' => 42, 'is_cat' => false, 'first_id' => 1867],
$this->outputHeadlines(1)->getPayload()['content'],
$this->extractMessageJson($this->outputHeadlines(1))['content'],
]);
$this->assertMessage($exp, $test);
// test 'include_header' with skip and ascending order
$test = $this->req($in[9]);
$exp = $this->respGood([
['id' => 42, 'is_cat' => false, 'first_id' => 0],
$this->outputHeadlines(1)->getPayload()['content'],
$this->extractMessageJson($this->outputHeadlines(1))['content'],
]);
$this->assertMessage($exp, $test);
// test 'show_excerpt'
$exp1 = "“This & that, you know‽”";
$exp2 = "Pour vous faire mieux connaitre d’ou\u{300} vient l’erreur de ceux qui bla\u{302}ment la volupte\u{301}, et qui louent en…";
$test = $this->req($in[10]);
$this->assertArrayHasKey("excerpt", $test->getPayload()['content'][0]);
$this->assertArrayHasKey("excerpt", $test->getPayload()['content'][1]);
$this->assertSame($exp1, $test->getPayload()['content'][0]['excerpt']);
$this->assertSame($exp2, $test->getPayload()['content'][1]['excerpt']);
$this->assertArrayHasKey("excerpt", $this->extractMessageJson($test)['content'][0]);
$this->assertArrayHasKey("excerpt", $this->extractMessageJson($test)['content'][1]);
$this->assertSame($exp1, $this->extractMessageJson($test)['content'][0]['excerpt']);
$this->assertSame($exp2, $this->extractMessageJson($test)['content'][1]['excerpt']);
}
protected function generateHeadlines(int $id): Result {
@ -1815,7 +1814,7 @@ LONG_STRING;
]));
}
protected function outputHeadlines(int $id): Response {
protected function outputHeadlines(int $id): ResponseInterface {
return $this->respGood([
[
'id' => $id,

24
tests/cases/REST/TinyTinyRSS/TestIcon.php

@ -9,10 +9,10 @@ namespace JKingWeb\Arsse\TestCase\REST\TinyTinyRSS;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\User;
use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Misc\HTTP;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\REST\TinyTinyRSS\Icon;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\Response\EmptyResponse as Response;
/** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\Icon<extended> */
class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest {
@ -51,21 +51,21 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->subscriptionIcon->with($this->anything(), 2112, false)->returns(['url' => "http://example.net/logo.png"]);
$this->dbMock->subscriptionIcon->with($this->anything(), 1337, false)->returns(['url' => "http://example.org/icon.gif\r\nLocation: http://bad.example.com/"]);
// these requests should succeed
$exp = new Response(301, ['Location' => "http://example.com/favicon.ico"]);
$exp = HTTP::respEmpty(301, ['Location' => "http://example.com/favicon.ico"]);
$this->assertMessage($exp, $this->req("42.ico"));
$exp = new Response(301, ['Location' => "http://example.net/logo.png"]);
$exp = HTTP::respEmpty(301, ['Location' => "http://example.net/logo.png"]);
$this->assertMessage($exp, $this->req("2112.ico"));
$exp = new Response(301, ['Location' => "http://example.org/icon.gif"]);
$exp = HTTP::respEmpty(301, ['Location' => "http://example.org/icon.gif"]);
$this->assertMessage($exp, $this->req("1337.ico"));
// these requests should fail
$exp = new Response(404);
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->req("ook.ico"));
$this->assertMessage($exp, $this->req("ook"));
$this->assertMessage($exp, $this->req("47.ico"));
$this->assertMessage($exp, $this->req("2112.png"));
$this->assertMessage($exp, $this->req("1123.ico"));
// only GET is allowed
$exp = new Response(405, ['Allow' => "GET"]);
$exp = HTTP::respEmpty(405, ['Allow' => "GET"]);
$this->assertMessage($exp, $this->req("2112.ico", "PUT"));
}
@ -79,32 +79,32 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest {
$this->dbMock->subscriptionIcon->with(null, 2112, false)->returns($url);
$this->dbMock->subscriptionIcon->with(null, 1337, false)->returns($url);
// these requests should succeed
$exp = new Response(301, ['Location' => "http://example.org/icon.gif"]);
$exp = HTTP::respEmpty(301, ['Location' => "http://example.org/icon.gif"]);
$this->assertMessage($exp, $this->req("42.ico"));
$this->assertMessage($exp, $this->req("2112.ico"));
$this->assertMessage($exp, $this->req("1337.ico"));
$this->assertMessage($exp, $this->reqAuth("42.ico"));
$this->assertMessage($exp, $this->reqAuth("1337.ico"));
// these requests should fail
$exp = new Response(404);
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->reqAuth("2112.ico"));
$exp = new Response(401);
$exp = HTTP::respEmpty(401);
$this->assertMessage($exp, $this->reqAuthFailed("42.ico"));
$this->assertMessage($exp, $this->reqAuthFailed("1337.ico"));
// with HTTP auth required, only authenticated requests should succeed
self::setConf(['userHTTPAuthRequired' => true]);
$exp = new Response(301, ['Location' => "http://example.org/icon.gif"]);
$exp = HTTP::respEmpty(301, ['Location' => "http://example.org/icon.gif"]);
$this->assertMessage($exp, $this->reqAuth("42.ico"));
$this->assertMessage($exp, $this->reqAuth("1337.ico"));
// anything else should fail
$exp = new Response(401);
$exp = HTTP::respEmpty(401);
$this->assertMessage($exp, $this->req("42.ico"));
$this->assertMessage($exp, $this->req("2112.ico"));
$this->assertMessage($exp, $this->req("1337.ico"));
$this->assertMessage($exp, $this->reqAuthFailed("42.ico"));
$this->assertMessage($exp, $this->reqAuthFailed("1337.ico"));
// resources for the wrtong user should still fail, too
$exp = new Response(404);
$exp = HTTP::respEmpty(404);
$this->assertMessage($exp, $this->reqAuth("2112.ico"));
}
}

36
tests/lib/AbstractTest.php

@ -17,15 +17,13 @@ use JKingWeb\Arsse\Db\Driver;
use JKingWeb\Arsse\Db\Result;
use JKingWeb\Arsse\Factory;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo;
use JKingWeb\Arsse\Misc\URL;
use JKingWeb\Arsse\Misc\HTTP;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\ServerRequest;
use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\Response\XmlResponse;
use GuzzleHttp\Psr7\ServerRequest;
/** @coversNothing */
abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
@ -258,7 +256,8 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
}
}
$server = array_merge($server, $vars);
$req = new ServerRequest($server, [], $url, $method, "php://memory", [], [], $params, $parsedBody);
$req = new ServerRequest($method, $url, $headers, $body, "1.1", $server);
$req = $req->withParsedBody($parsedBody)->withQueryParams($params);
if (isset($user)) {
if (strlen($user)) {
$req = $req->withAttribute("authenticated", true)->withAttribute("authenticatedUser", $user);
@ -337,12 +336,13 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
$this->assertSame($exp->getMethod(), $act->getMethod(), $text);
$this->assertSame($exp->getRequestTarget(), $act->getRequestTarget(), $text);
}
if ($exp instanceof JsonResponse) {
$this->assertInstanceOf(JsonResponse::class, $act, $text);
$this->assertEquals($exp->getPayload(), $act->getPayload(), $text);
$this->assertSame($exp->getPayload(), $act->getPayload(), $text);
} elseif ($exp instanceof XmlResponse) {
$this->assertInstanceOf(XmlResponse::class, $act, $text);
if ($exp instanceof ResponseInterface && HTTP::matchType($exp, "application/json", "text/json", "+json")) {
$expBody = @json_decode((string) $exp->getBody(), true);
$actBody = @json_decode((string) $act->getBody(), true);
$this->assertSame(\JSON_ERROR_NONE, json_last_error(), "Response body is not valid JSON");
$this->assertEquals($expBody, $actBody, $text);
$this->assertSame($expBody, $actBody, $text);
} elseif ($exp instanceof ResponseInterface && HTTP::matchType($exp, "application/xml", "text/xml", "+xml")) {
$this->assertXmlStringEqualsXmlString((string) $exp->getBody(), (string) $act->getBody(), $text);
} else {
$this->assertSame((string) $exp->getBody(), (string) $act->getBody(), $text);
@ -350,6 +350,16 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
$this->assertEquals($exp->getHeaders(), $act->getHeaders(), $text);
}
protected function extractMessageJson(MessageInterface $msg) {
if (HTTP::matchType($msg, "application/json", "text/json", "+json")) {
$json = @json_decode((string) $msg->getBody(), true);
if (json_last_error() === \JSON_ERROR_NONE) {
return $json;
}
}
return null;
}
public function assertTime($exp, $test, string $msg = ''): void {
$test = $this->approximateTime($exp, $test);
$exp = Date::transform($exp, "iso8601");
@ -388,7 +398,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
}
/** Inserts into the database test data in the following format:
*
*
* ```php
* $data = [
* 'some_table' => [
@ -482,7 +492,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
// now search for the actual output row in the expected output
$found = array_keys($exp, $row, true);
foreach ($found as $k) {
if(!isset($act[$k])) {
if (!isset($act[$k])) {
$act[$k] = $row;
// skip to the next row
continue 2;

1
tests/phpunit.dist.xml

@ -115,7 +115,6 @@
<file>cases/REST/TestREST.php</file>
</testsuite>
<testsuite name="Miniflux">
<file>cases/REST/Miniflux/TestErrorResponse.php</file>
<file>cases/REST/Miniflux/TestStatus.php</file>
<file>cases/REST/Miniflux/TestV1.php</file>
<file>cases/REST/Miniflux/TestToken.php</file>

313
vendor-bin/csfixer/composer.lock

@ -227,16 +227,16 @@
},
{
"name": "doctrine/annotations",
"version": "1.13.2",
"version": "1.13.3",
"source": {
"type": "git",
"url": "https://github.com/doctrine/annotations.git",
"reference": "5b668aef16090008790395c02c893b1ba13f7e08"
"reference": "648b0343343565c4a056bfc8392201385e8d89f0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/annotations/zipball/5b668aef16090008790395c02c893b1ba13f7e08",
"reference": "5b668aef16090008790395c02c893b1ba13f7e08",
"url": "https://api.github.com/repos/doctrine/annotations/zipball/648b0343343565c4a056bfc8392201385e8d89f0",
"reference": "648b0343343565c4a056bfc8392201385e8d89f0",
"shasum": ""
},
"require": {
@ -248,9 +248,10 @@
"require-dev": {
"doctrine/cache": "^1.11 || ^2.0",
"doctrine/coding-standard": "^6.0 || ^8.1",
"phpstan/phpstan": "^0.12.20",
"phpstan/phpstan": "^1.4.10 || ^1.8.0",
"phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5",
"symfony/cache": "^4.4 || ^5.2"
"symfony/cache": "^4.4 || ^5.2",
"vimeo/psalm": "^4.10"
},
"type": "library",
"autoload": {
@ -293,9 +294,9 @@
],
"support": {
"issues": "https://github.com/doctrine/annotations/issues",
"source": "https://github.com/doctrine/annotations/tree/1.13.2"
"source": "https://github.com/doctrine/annotations/tree/1.13.3"
},
"time": "2021-08-05T19:00:23+00:00"
"time": "2022-07-02T10:48:51+00:00"
},
{
"name": "doctrine/lexer",
@ -375,16 +376,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.8.0",
"version": "v3.11.0",
"source": {
"type": "git",
"url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git",
"reference": "cbad1115aac4b5c3c5540e7210d3c9fba2f81fa3"
"reference": "7dcdea3f2f5f473464e835be9be55283ff8cfdc3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/cbad1115aac4b5c3c5540e7210d3c9fba2f81fa3",
"reference": "cbad1115aac4b5c3c5540e7210d3c9fba2f81fa3",
"url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/7dcdea3f2f5f473464e835be9be55283ff8cfdc3",
"reference": "7dcdea3f2f5f473464e835be9be55283ff8cfdc3",
"shasum": ""
},
"require": {
@ -394,7 +395,7 @@
"ext-json": "*",
"ext-tokenizer": "*",
"php": "^7.4 || ^8.0",
"php-cs-fixer/diff": "^2.0",
"sebastian/diff": "^4.0",
"symfony/console": "^5.4 || ^6.0",
"symfony/event-dispatcher": "^5.4 || ^6.0",
"symfony/filesystem": "^5.4 || ^6.0",
@ -452,7 +453,7 @@
"description": "A tool to automatically fix PHP code style",
"support": {
"issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues",
"source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.8.0"
"source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.11.0"
},
"funding": [
{
@ -460,59 +461,7 @@
"type": "github"
}
],
"time": "2022-03-18T17:20:59+00:00"
},
{
"name": "php-cs-fixer/diff",
"version": "v2.0.2",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/diff.git",
"reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/29dc0d507e838c4580d018bd8b5cb412474f7ec3",
"reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0",
"symfony/process": "^3.3"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Kore Nordmann",
"email": "mail@kore-nordmann.de"
}
],
"description": "sebastian/diff v3 backport support for PHP 5.6+",
"homepage": "https://github.com/PHP-CS-Fixer",
"keywords": [
"diff"
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/diff/issues",
"source": "https://github.com/PHP-CS-Fixer/diff/tree/v2.0.2"
},
"time": "2020-10-14T08:32:19+00:00"
"time": "2022-09-01T18:24:51+00:00"
},
{
"name": "psr/cache",
@ -716,18 +665,84 @@
},
"time": "2021-07-14T16:46:02+00:00"
},
{
"name": "sebastian/diff",
"version": "4.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d",
"reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d",
"shasum": ""
},
"require": {
"php": ">=7.3"
},
"require-dev": {
"phpunit/phpunit": "^9.3",
"symfony/process": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.0-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Kore Nordmann",
"email": "mail@kore-nordmann.de"
}
],
"description": "Diff implementation",
"homepage": "https://github.com/sebastianbergmann/diff",
"keywords": [
"diff",
"udiff",
"unidiff",
"unified diff"
],
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
"source": "https://github.com/sebastianbergmann/diff/tree/4.0.4"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-10-26T13:10:38+00:00"
},
{
"name": "symfony/console",
"version": "v6.1.0",
"version": "v6.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "c9646197ef43b0e2ff44af61e7f0571526fd4170"
"reference": "7fccea8728aa2d431a6725b02b3ce759049fc84d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/c9646197ef43b0e2ff44af61e7f0571526fd4170",
"reference": "c9646197ef43b0e2ff44af61e7f0571526fd4170",
"url": "https://api.github.com/repos/symfony/console/zipball/7fccea8728aa2d431a6725b02b3ce759049fc84d",
"reference": "7fccea8728aa2d431a6725b02b3ce759049fc84d",
"shasum": ""
},
"require": {
@ -794,7 +809,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v6.1.0"
"source": "https://github.com/symfony/console/tree/v6.1.4"
},
"funding": [
{
@ -810,11 +825,11 @@
"type": "tidelift"
}
],
"time": "2022-05-27T06:34:22+00:00"
"time": "2022-08-26T10:32:31+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.1.0",
"version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
@ -861,7 +876,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.1"
},
"funding": [
{
@ -964,7 +979,7 @@
},
{
"name": "symfony/event-dispatcher-contracts",
"version": "v3.1.0",
"version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher-contracts.git",
@ -1023,7 +1038,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.1.0"
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.1.1"
},
"funding": [
{
@ -1043,16 +1058,16 @@
},
{
"name": "symfony/filesystem",
"version": "v6.1.0",
"version": "v6.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "3132d2f43ca799c2aa099f9738d98228c56baa5d"
"reference": "3f39c04d2630c34019907b02f85672dac99f8659"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/3132d2f43ca799c2aa099f9738d98228c56baa5d",
"reference": "3132d2f43ca799c2aa099f9738d98228c56baa5d",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/3f39c04d2630c34019907b02f85672dac99f8659",
"reference": "3f39c04d2630c34019907b02f85672dac99f8659",
"shasum": ""
},
"require": {
@ -1086,7 +1101,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v6.1.0"
"source": "https://github.com/symfony/filesystem/tree/v6.1.4"
},
"funding": [
{
@ -1102,20 +1117,20 @@
"type": "tidelift"
}
],
"time": "2022-05-21T13:34:40+00:00"
"time": "2022-08-02T16:17:38+00:00"
},
{
"name": "symfony/finder",
"version": "v6.1.0",
"version": "v6.1.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "45b8beb69d6eb3b05a65689ebfd4222326773f8f"
"reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/45b8beb69d6eb3b05a65689ebfd4222326773f8f",
"reference": "45b8beb69d6eb3b05a65689ebfd4222326773f8f",
"url": "https://api.github.com/repos/symfony/finder/zipball/39696bff2c2970b3779a5cac7bf9f0b88fc2b709",
"reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709",
"shasum": ""
},
"require": {
@ -1150,7 +1165,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v6.1.0"
"source": "https://github.com/symfony/finder/tree/v6.1.3"
},
"funding": [
{
@ -1166,7 +1181,7 @@
"type": "tidelift"
}
],
"time": "2022-04-15T08:08:08+00:00"
"time": "2022-07-29T07:42:06+00:00"
},
{
"name": "symfony/options-resolver",
@ -1237,16 +1252,16 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "30885182c981ab175d4d034db0f6f469898070ab"
"reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab",
"reference": "30885182c981ab175d4d034db0f6f469898070ab",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"shasum": ""
},
"require": {
@ -1261,7 +1276,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1299,7 +1314,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0"
},
"funding": [
{
@ -1315,20 +1330,20 @@
"type": "tidelift"
}
],
"time": "2021-10-20T20:35:02+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "81b86b50cf841a64252b439e738e97f4a34e2783"
"reference": "433d05519ce6990bf3530fba6957499d327395c2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783",
"reference": "81b86b50cf841a64252b439e738e97f4a34e2783",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2",
"reference": "433d05519ce6990bf3530fba6957499d327395c2",
"shasum": ""
},
"require": {
@ -1340,7 +1355,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1380,7 +1395,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0"
},
"funding": [
{
@ -1396,20 +1411,20 @@
"type": "tidelift"
}
],
"time": "2021-11-23T21:10:46+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": {
@ -1421,7 +1436,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1464,7 +1479,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": [
{
@ -1480,20 +1495,20 @@
"type": "tidelift"
}
],
"time": "2021-02-19T12:13:01+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825"
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"shasum": ""
},
"require": {
@ -1508,7 +1523,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1547,7 +1562,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
},
"funding": [
{
@ -1563,20 +1578,20 @@
"type": "tidelift"
}
],
"time": "2021-11-30T18:21:41+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c"
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c",
"reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"shasum": ""
},
"require": {
@ -1585,7 +1600,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1630,7 +1645,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
},
"funding": [
{
@ -1646,20 +1661,20 @@
"type": "tidelift"
}
],
"time": "2022-03-04T08:16:47+00:00"
"time": "2022-05-10T07:21:04+00:00"
},
{
"name": "symfony/polyfill-php81",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f"
"reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f",
"reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1",
"reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1",
"shasum": ""
},
"require": {
@ -1668,7 +1683,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1709,7 +1724,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0"
},
"funding": [
{
@ -1725,20 +1740,20 @@
"type": "tidelift"
}
],
"time": "2021-09-13T13:58:11+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/process",
"version": "v6.1.0",
"version": "v6.1.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "318718453c2be58266f1a9e74063d13cb8dd4165"
"reference": "a6506e99cfad7059b1ab5cab395854a0a0c21292"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/318718453c2be58266f1a9e74063d13cb8dd4165",
"reference": "318718453c2be58266f1a9e74063d13cb8dd4165",
"url": "https://api.github.com/repos/symfony/process/zipball/a6506e99cfad7059b1ab5cab395854a0a0c21292",
"reference": "a6506e99cfad7059b1ab5cab395854a0a0c21292",
"shasum": ""
},
"require": {
@ -1770,7 +1785,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v6.1.0"
"source": "https://github.com/symfony/process/tree/v6.1.3"
},
"funding": [
{
@ -1786,20 +1801,20 @@
"type": "tidelift"
}
],
"time": "2022-05-11T12:12:29+00:00"
"time": "2022-06-27T17:24:16+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.1.0",
"version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "d66cd8ab656780f62c4215b903a420eb86358957"
"reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/d66cd8ab656780f62c4215b903a420eb86358957",
"reference": "d66cd8ab656780f62c4215b903a420eb86358957",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239",
"reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239",
"shasum": ""
},
"require": {
@ -1855,7 +1870,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v3.1.0"
"source": "https://github.com/symfony/service-contracts/tree/v3.1.1"
},
"funding": [
{
@ -1871,7 +1886,7 @@
"type": "tidelift"
}
],
"time": "2022-05-07T08:07:09+00:00"
"time": "2022-05-30T19:18:58+00:00"
},
{
"name": "symfony/stopwatch",
@ -1937,16 +1952,16 @@
},
{
"name": "symfony/string",
"version": "v6.1.0",
"version": "v6.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "d3edc75baf9f1d4f94879764dda2e1ac33499529"
"reference": "290972cad7b364e3befaa74ba0ec729800fb161c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/d3edc75baf9f1d4f94879764dda2e1ac33499529",
"reference": "d3edc75baf9f1d4f94879764dda2e1ac33499529",
"url": "https://api.github.com/repos/symfony/string/zipball/290972cad7b364e3befaa74ba0ec729800fb161c",
"reference": "290972cad7b364e3befaa74ba0ec729800fb161c",
"shasum": ""
},
"require": {
@ -2002,7 +2017,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v6.1.0"
"source": "https://github.com/symfony/string/tree/v6.1.4"
},
"funding": [
{
@ -2018,7 +2033,7 @@
"type": "tidelift"
}
],
"time": "2022-04-22T08:18:23+00:00"
"time": "2022-08-12T18:05:43+00:00"
}
],
"aliases": [],

277
vendor-bin/daux/composer.lock

@ -83,22 +83,22 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "7.4.3",
"version": "7.5.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "74a8602c6faec9ef74b7a9391ac82c5e65b1cdab"
"reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/74a8602c6faec9ef74b7a9391ac82c5e65b1cdab",
"reference": "74a8602c6faec9ef74b7a9391ac82c5e65b1cdab",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/b50a2a1251152e43f6a37f0fa053e730a67d25ba",
"reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.5",
"guzzlehttp/psr7": "^1.8.3 || ^2.1",
"guzzlehttp/psr7": "^1.9 || ^2.4",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
@ -107,10 +107,10 @@
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"bamarni/composer-bin-plugin": "^1.8.1",
"ext-curl": "*",
"php-http/client-integration-tests": "^3.0",
"phpunit/phpunit": "^8.5.5 || ^9.3.5",
"phpunit/phpunit": "^8.5.29 || ^9.5.23",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"suggest": {
@ -120,8 +120,12 @@
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "7.4-dev"
"dev-master": "7.5-dev"
}
},
"autoload": {
@ -187,7 +191,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.4.3"
"source": "https://github.com/guzzle/guzzle/tree/7.5.0"
},
"funding": [
{
@ -203,20 +207,20 @@
"type": "tidelift"
}
],
"time": "2022-05-25T13:24:33+00:00"
"time": "2022-08-28T15:39:27+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": {
@ -271,7 +275,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": [
{
@ -287,20 +291,20 @@
"type": "tidelift"
}
],
"time": "2021-10-22T20:56:57+00:00"
"time": "2022-08-28T14:55:35+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.2.1",
"version": "2.4.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "c94a94f120803a18554c1805ef2e539f8285f9a2"
"reference": "69568e4293f4fa993f3b0e51c9723e1e17c41379"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/c94a94f120803a18554c1805ef2e539f8285f9a2",
"reference": "c94a94f120803a18554c1805ef2e539f8285f9a2",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/69568e4293f4fa993f3b0e51c9723e1e17c41379",
"reference": "69568e4293f4fa993f3b0e51c9723e1e17c41379",
"shasum": ""
},
"require": {
@ -314,17 +318,21 @@
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"bamarni/composer-bin-plugin": "^1.8.1",
"http-interop/http-factory-tests": "^0.9",
"phpunit/phpunit": "^8.5.8 || ^9.3.10"
"phpunit/phpunit": "^8.5.29 || ^9.5.23"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "2.2-dev"
"dev-master": "2.4-dev"
}
},
"autoload": {
@ -386,7 +394,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.2.1"
"source": "https://github.com/guzzle/psr7/tree/2.4.1"
},
"funding": [
{
@ -402,7 +410,7 @@
"type": "tidelift"
}
],
"time": "2022-03-20T21:55:58+00:00"
"time": "2022-08-28T14:45:39+00:00"
},
{
"name": "league/commonmark",
@ -898,16 +906,16 @@
},
{
"name": "symfony/console",
"version": "v5.4.9",
"version": "v5.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "829d5d1bf60b2efeb0887b7436873becc71a45eb"
"reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/829d5d1bf60b2efeb0887b7436873becc71a45eb",
"reference": "829d5d1bf60b2efeb0887b7436873becc71a45eb",
"url": "https://api.github.com/repos/symfony/console/zipball/c072aa8f724c3af64e2c7a96b796a4863d24dba1",
"reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1",
"shasum": ""
},
"require": {
@ -977,7 +985,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v5.4.9"
"source": "https://github.com/symfony/console/tree/v5.4.12"
},
"funding": [
{
@ -993,11 +1001,11 @@
"type": "tidelift"
}
],
"time": "2022-05-18T06:17:34+00:00"
"time": "2022-08-17T13:18:05+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.1.0",
"version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
@ -1044,7 +1052,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.1"
},
"funding": [
{
@ -1064,16 +1072,16 @@
},
{
"name": "symfony/http-foundation",
"version": "v5.4.9",
"version": "v5.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "6b0d0e4aca38d57605dcd11e2416994b38774522"
"reference": "f4bfe9611b113b15d98a43da68ec9b5a00d56791"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/6b0d0e4aca38d57605dcd11e2416994b38774522",
"reference": "6b0d0e4aca38d57605dcd11e2416994b38774522",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/f4bfe9611b113b15d98a43da68ec9b5a00d56791",
"reference": "f4bfe9611b113b15d98a43da68ec9b5a00d56791",
"shasum": ""
},
"require": {
@ -1085,8 +1093,11 @@
"require-dev": {
"predis/predis": "~1.0",
"symfony/cache": "^4.4|^5.0|^6.0",
"symfony/dependency-injection": "^5.4|^6.0",
"symfony/expression-language": "^4.4|^5.0|^6.0",
"symfony/mime": "^4.4|^5.0|^6.0"
"symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4",
"symfony/mime": "^4.4|^5.0|^6.0",
"symfony/rate-limiter": "^5.2|^6.0"
},
"suggest": {
"symfony/mime": "To use the file extension guesser"
@ -1117,7 +1128,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-foundation/tree/v5.4.9"
"source": "https://github.com/symfony/http-foundation/tree/v5.4.12"
},
"funding": [
{
@ -1133,20 +1144,20 @@
"type": "tidelift"
}
],
"time": "2022-05-17T15:07:29+00:00"
"time": "2022-08-19T07:33:17+00:00"
},
{
"name": "symfony/mime",
"version": "v5.4.9",
"version": "v5.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "2b3802a24e48d0cfccf885173d2aac91e73df92e"
"reference": "03876e9c5a36f5b45e7d9a381edda5421eff8a90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/2b3802a24e48d0cfccf885173d2aac91e73df92e",
"reference": "2b3802a24e48d0cfccf885173d2aac91e73df92e",
"url": "https://api.github.com/repos/symfony/mime/zipball/03876e9c5a36f5b45e7d9a381edda5421eff8a90",
"reference": "03876e9c5a36f5b45e7d9a381edda5421eff8a90",
"shasum": ""
},
"require": {
@ -1200,7 +1211,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v5.4.9"
"source": "https://github.com/symfony/mime/tree/v5.4.12"
},
"funding": [
{
@ -1216,20 +1227,20 @@
"type": "tidelift"
}
],
"time": "2022-05-21T10:24:18+00:00"
"time": "2022-08-19T14:24:03+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "30885182c981ab175d4d034db0f6f469898070ab"
"reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab",
"reference": "30885182c981ab175d4d034db0f6f469898070ab",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"shasum": ""
},
"require": {
@ -1244,7 +1255,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1282,7 +1293,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0"
},
"funding": [
{
@ -1298,20 +1309,20 @@
"type": "tidelift"
}
],
"time": "2021-10-20T20:35:02+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "81b86b50cf841a64252b439e738e97f4a34e2783"
"reference": "433d05519ce6990bf3530fba6957499d327395c2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783",
"reference": "81b86b50cf841a64252b439e738e97f4a34e2783",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2",
"reference": "433d05519ce6990bf3530fba6957499d327395c2",
"shasum": ""
},
"require": {
@ -1323,7 +1334,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1363,7 +1374,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0"
},
"funding": [
{
@ -1379,20 +1390,20 @@
"type": "tidelift"
}
],
"time": "2021-11-23T21:10:46+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-intl-icu",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-icu.git",
"reference": "c023a439b8551e320cc3c8433b198e408a623af1"
"reference": "e407643d610e5f2c8a4b14189150f68934bf5e48"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/c023a439b8551e320cc3c8433b198e408a623af1",
"reference": "c023a439b8551e320cc3c8433b198e408a623af1",
"url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/e407643d610e5f2c8a4b14189150f68934bf5e48",
"reference": "e407643d610e5f2c8a4b14189150f68934bf5e48",
"shasum": ""
},
"require": {
@ -1404,7 +1415,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1450,7 +1461,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.26.0"
},
"funding": [
{
@ -1466,20 +1477,20 @@
"type": "tidelift"
}
],
"time": "2021-10-26T17:16:04+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"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": {
@ -1493,7 +1504,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1537,7 +1548,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": [
{
@ -1553,20 +1564,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": {
@ -1578,7 +1589,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1621,7 +1632,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": [
{
@ -1637,20 +1648,20 @@
"type": "tidelift"
}
],
"time": "2021-02-19T12:13:01+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825"
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"shasum": ""
},
"require": {
@ -1665,7 +1676,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1704,7 +1715,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
},
"funding": [
{
@ -1720,20 +1731,20 @@
"type": "tidelift"
}
],
"time": "2021-11-30T18:21:41+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": {
@ -1742,7 +1753,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1780,7 +1791,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": [
{
@ -1796,20 +1807,20 @@
"type": "tidelift"
}
],
"time": "2021-05-27T09:17:38+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-php73",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php73.git",
"reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5"
"reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5",
"reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5",
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/e440d35fa0286f77fb45b79a03fedbeda9307e85",
"reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85",
"shasum": ""
},
"require": {
@ -1818,7 +1829,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1859,7 +1870,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php73/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-php73/tree/v1.26.0"
},
"funding": [
{
@ -1875,20 +1886,20 @@
"type": "tidelift"
}
],
"time": "2021-06-05T21:20:04+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c"
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c",
"reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"shasum": ""
},
"require": {
@ -1897,7 +1908,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1942,7 +1953,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
},
"funding": [
{
@ -1958,20 +1969,20 @@
"type": "tidelift"
}
],
"time": "2022-03-04T08:16:47+00:00"
"time": "2022-05-10T07:21:04+00:00"
},
{
"name": "symfony/process",
"version": "v5.4.8",
"version": "v5.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3"
"reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/597f3fff8e3e91836bb0bd38f5718b56ddbde2f3",
"reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3",
"url": "https://api.github.com/repos/symfony/process/zipball/6e75fe6874cbc7e4773d049616ab450eff537bf1",
"reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1",
"shasum": ""
},
"require": {
@ -2004,7 +2015,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v5.4.8"
"source": "https://github.com/symfony/process/tree/v5.4.11"
},
"funding": [
{
@ -2020,20 +2031,20 @@
"type": "tidelift"
}
],
"time": "2022-04-08T05:07:18+00:00"
"time": "2022-06-27T16:58:25+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.1.0",
"version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "d66cd8ab656780f62c4215b903a420eb86358957"
"reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/d66cd8ab656780f62c4215b903a420eb86358957",
"reference": "d66cd8ab656780f62c4215b903a420eb86358957",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239",
"reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239",
"shasum": ""
},
"require": {
@ -2089,7 +2100,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v3.1.0"
"source": "https://github.com/symfony/service-contracts/tree/v3.1.1"
},
"funding": [
{
@ -2105,20 +2116,20 @@
"type": "tidelift"
}
],
"time": "2022-05-07T08:07:09+00:00"
"time": "2022-05-30T19:18:58+00:00"
},
{
"name": "symfony/string",
"version": "v6.1.0",
"version": "v6.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "d3edc75baf9f1d4f94879764dda2e1ac33499529"
"reference": "290972cad7b364e3befaa74ba0ec729800fb161c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/d3edc75baf9f1d4f94879764dda2e1ac33499529",
"reference": "d3edc75baf9f1d4f94879764dda2e1ac33499529",
"url": "https://api.github.com/repos/symfony/string/zipball/290972cad7b364e3befaa74ba0ec729800fb161c",
"reference": "290972cad7b364e3befaa74ba0ec729800fb161c",
"shasum": ""
},
"require": {
@ -2174,7 +2185,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v6.1.0"
"source": "https://github.com/symfony/string/tree/v6.1.4"
},
"funding": [
{
@ -2190,20 +2201,20 @@
"type": "tidelift"
}
],
"time": "2022-04-22T08:18:23+00:00"
"time": "2022-08-12T18:05:43+00:00"
},
{
"name": "symfony/yaml",
"version": "v5.4.3",
"version": "v5.4.12",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "e80f87d2c9495966768310fc531b487ce64237a2"
"reference": "7a3aa21ac8ab1a96cc6de5bbcab4bc9fc943b18c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/e80f87d2c9495966768310fc531b487ce64237a2",
"reference": "e80f87d2c9495966768310fc531b487ce64237a2",
"url": "https://api.github.com/repos/symfony/yaml/zipball/7a3aa21ac8ab1a96cc6de5bbcab4bc9fc943b18c",
"reference": "7a3aa21ac8ab1a96cc6de5bbcab4bc9fc943b18c",
"shasum": ""
},
"require": {
@ -2249,7 +2260,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v5.4.3"
"source": "https://github.com/symfony/yaml/tree/v5.4.12"
},
"funding": [
{
@ -2265,7 +2276,7 @@
"type": "tidelift"
}
],
"time": "2022-01-26T16:32:32+00:00"
"time": "2022-08-02T15:52:22+00:00"
},
{
"name": "webuni/front-matter",

460
vendor-bin/phpunit/composer.lock

@ -338,16 +338,16 @@
},
{
"name": "mikey179/vfsstream",
"version": "v1.6.10",
"version": "v1.6.11",
"source": {
"type": "git",
"url": "https://github.com/bovigo/vfsStream.git",
"reference": "250c0825537d501e327df879fb3d4cd751933b85"
"reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bovigo/vfsStream/zipball/250c0825537d501e327df879fb3d4cd751933b85",
"reference": "250c0825537d501e327df879fb3d4cd751933b85",
"url": "https://api.github.com/repos/bovigo/vfsStream/zipball/17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f",
"reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f",
"shasum": ""
},
"require": {
@ -385,7 +385,7 @@
"source": "https://github.com/bovigo/vfsStream/tree/master",
"wiki": "https://github.com/bovigo/vfsStream/wiki"
},
"time": "2021-09-25T08:05:01+00:00"
"time": "2022-02-23T02:02:42+00:00"
},
{
"name": "myclabs/deep-copy",
@ -448,16 +448,16 @@
},
{
"name": "nikic/php-parser",
"version": "v4.14.0",
"version": "v4.15.1",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1"
"reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1",
"reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900",
"reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900",
"shasum": ""
},
"require": {
@ -498,9 +498,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0"
"source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1"
},
"time": "2022-05-31T20:59:12+00:00"
"time": "2022-09-04T07:30:47+00:00"
},
{
"name": "phar-io/manifest",
@ -613,252 +613,25 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-2.x": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jaap van Otterdijk",
"email": "opensource@ijaap.nl"
}
],
"description": "Common reflection classes used by phpdocumentor to reflect the code structure",
"homepage": "http://www.phpdoc.org",
"keywords": [
"FQSEN",
"phpDocumentor",
"phpdoc",
"reflection",
"static analysis"
],
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
},
"time": "2020-06-27T09:03:43+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "5.3.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
"reference": "622548b623e81ca6d78b721c5e029f4ce664f170"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170",
"reference": "622548b623e81ca6d78b721c5e029f4ce664f170",
"shasum": ""
},
"require": {
"ext-filter": "*",
"php": "^7.2 || ^8.0",
"phpdocumentor/reflection-common": "^2.2",
"phpdocumentor/type-resolver": "^1.3",
"webmozart/assert": "^1.9.1"
},
"require-dev": {
"mockery/mockery": "~1.3.2",
"psalm/phar": "^4.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
},
{
"name": "Jaap van Otterdijk",
"email": "account@ijaap.nl"
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
"source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0"
},
"time": "2021-10-19T17:43:47+00:00"
},
{
"name": "phpdocumentor/type-resolver",
"version": "1.6.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "77a32518733312af16a44300404e945338981de3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3",
"reference": "77a32518733312af16a44300404e945338981de3",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"phpdocumentor/reflection-common": "^2.0"
},
"require-dev": {
"ext-tokenizer": "*",
"psalm/phar": "^4.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
}
],
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1"
},
"time": "2022-03-15T21:29:03+00:00"
},
{
"name": "phpspec/prophecy",
"version": "v1.15.0",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
"reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.2",
"php": "^7.2 || ~8.0, <8.2",
"phpdocumentor/reflection-docblock": "^5.2",
"sebastian/comparator": "^3.0 || ^4.0",
"sebastian/recursion-context": "^3.0 || ^4.0"
},
"require-dev": {
"phpspec/phpspec": "^6.0 || ^7.0",
"phpunit/phpunit": "^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Prophecy\\": "src/Prophecy"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Konstantin Kudryashov",
"email": "ever.zet@gmail.com",
"homepage": "http://everzet.com"
},
{
"name": "Marcello Duarte",
"email": "marcello.duarte@gmail.com"
}
],
"description": "Highly opinionated mocking framework for PHP 5.3+",
"homepage": "https://github.com/phpspec/prophecy",
"keywords": [
"Double",
"Dummy",
"fake",
"mock",
"spy",
"stub"
],
"support": {
"issues": "https://github.com/phpspec/prophecy/issues",
"source": "https://github.com/phpspec/prophecy/tree/v1.15.0"
},
"time": "2021-12-08T12:19:24+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.15",
"version": "9.2.17",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f"
"reference": "aa94dc41e8661fe90c7316849907cba3007b10d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8",
"reference": "aa94dc41e8661fe90c7316849907cba3007b10d8",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^4.13.0",
"nikic/php-parser": "^4.14",
"php": ">=7.3",
"phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2",
@ -907,7 +680,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17"
},
"funding": [
{
@ -915,7 +688,7 @@
"type": "github"
}
],
"time": "2022-03-07T09:28:20+00:00"
"time": "2022-08-30T12:24:04+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -1160,16 +933,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.5.20",
"version": "9.5.24",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba"
"reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/12bc8879fb65aef2138b26fc633cb1e3620cffba",
"reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d0aa6097bef9fd42458a9b3c49da32c6ce6129c5",
"reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5",
"shasum": ""
},
"require": {
@ -1184,7 +957,6 @@
"phar-io/manifest": "^2.0.3",
"phar-io/version": "^3.0.2",
"php": ">=7.3",
"phpspec/prophecy": "^1.12.1",
"phpunit/php-code-coverage": "^9.2.13",
"phpunit/php-file-iterator": "^3.0.5",
"phpunit/php-invoker": "^3.1.1",
@ -1199,13 +971,9 @@
"sebastian/global-state": "^5.0.1",
"sebastian/object-enumerator": "^4.0.3",
"sebastian/resource-operations": "^3.0.3",
"sebastian/type": "^3.0",
"sebastian/type": "^3.1",
"sebastian/version": "^3.0.2"
},
"require-dev": {
"ext-pdo": "*",
"phpspec/prophecy-phpunit": "^2.0.1"
},
"suggest": {
"ext-soap": "*",
"ext-xdebug": "*"
@ -1247,7 +1015,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.20"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.24"
},
"funding": [
{
@ -1259,7 +1027,7 @@
"type": "github"
}
],
"time": "2022-04-01T12:37:26+00:00"
"time": "2022-08-30T07:42:16+00:00"
},
{
"name": "sebastian/cli-parser",
@ -1430,16 +1198,16 @@
},
{
"name": "sebastian/comparator",
"version": "4.0.6",
"version": "4.0.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "55f4261989e546dc112258c7a75935a81a7ce382"
"reference": "fa0f136dd2334583309d32b62544682ee972b51a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382",
"reference": "55f4261989e546dc112258c7a75935a81a7ce382",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
"reference": "fa0f136dd2334583309d32b62544682ee972b51a",
"shasum": ""
},
"require": {
@ -1492,7 +1260,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6"
"source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
},
"funding": [
{
@ -1500,7 +1268,7 @@
"type": "github"
}
],
"time": "2020-10-26T15:49:45+00:00"
"time": "2022-09-14T12:41:17+00:00"
},
{
"name": "sebastian/complexity",
@ -1690,16 +1458,16 @@
},
{
"name": "sebastian/exporter",
"version": "4.0.4",
"version": "4.0.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9"
"reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9",
"reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
"reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
"shasum": ""
},
"require": {
@ -1755,7 +1523,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4"
"source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5"
},
"funding": [
{
@ -1763,7 +1531,7 @@
"type": "github"
}
],
"time": "2021-11-11T14:18:36+00:00"
"time": "2022-09-14T06:03:37+00:00"
},
{
"name": "sebastian/global-state",
@ -2118,16 +1886,16 @@
},
{
"name": "sebastian/type",
"version": "3.0.0",
"version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
"reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad"
"reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad",
"reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e",
"reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e",
"shasum": ""
},
"require": {
@ -2139,7 +1907,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "3.2-dev"
}
},
"autoload": {
@ -2162,7 +1930,7 @@
"homepage": "https://github.com/sebastianbergmann/type",
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
"source": "https://github.com/sebastianbergmann/type/tree/3.0.0"
"source": "https://github.com/sebastianbergmann/type/tree/3.2.0"
},
"funding": [
{
@ -2170,7 +1938,7 @@
"type": "github"
}
],
"time": "2022-03-15T09:54:48+00:00"
"time": "2022-09-12T14:47:03+00:00"
},
{
"name": "sebastian/version",
@ -2225,88 +1993,6 @@
],
"time": "2020-09-28T06:39:44+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.25.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "30885182c981ab175d4d034db0f6f469898070ab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab",
"reference": "30885182c981ab175d4d034db0f6f469898070ab",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-10-20T20:35:02+00:00"
},
{
"name": "theseer/tokenizer",
"version": "1.2.1",
@ -2357,64 +2043,6 @@
],
"time": "2021-07-28T10:34:58+00:00"
},
{
"name": "webmozart/assert",
"version": "1.10.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "6964c76c7804814a842473e0c8fd15bab0f18e25"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25",
"reference": "6964c76c7804814a842473e0c8fd15bab0f18e25",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
"phpstan/phpstan": "<0.12.20",
"vimeo/psalm": "<4.6.1 || 4.6.2"
},
"require-dev": {
"phpunit/phpunit": "^8.5.13"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.10-dev"
}
},
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"assert",
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.10.0"
},
"time": "2021-03-09T10:59:23+00:00"
},
{
"name": "webmozart/glob",
"version": "4.6.0",

172
vendor-bin/robo/composer.lock

@ -90,16 +90,16 @@
},
{
"name": "consolidation/annotated-command",
"version": "4.5.5",
"version": "4.5.6",
"source": {
"type": "git",
"url": "https://github.com/consolidation/annotated-command.git",
"reference": "67cea8e8e7656b74da651ea6f49321853996c0fd"
"reference": "3968070538761628546270935f0733a0cc408e1f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/consolidation/annotated-command/zipball/67cea8e8e7656b74da651ea6f49321853996c0fd",
"reference": "67cea8e8e7656b74da651ea6f49321853996c0fd",
"url": "https://api.github.com/repos/consolidation/annotated-command/zipball/3968070538761628546270935f0733a0cc408e1f",
"reference": "3968070538761628546270935f0733a0cc408e1f",
"shasum": ""
},
"require": {
@ -140,22 +140,22 @@
"description": "Initialize Symfony Console commands from annotated command class methods.",
"support": {
"issues": "https://github.com/consolidation/annotated-command/issues",
"source": "https://github.com/consolidation/annotated-command/tree/4.5.5"
"source": "https://github.com/consolidation/annotated-command/tree/4.5.6"
},
"time": "2022-04-26T16:18:25+00:00"
"time": "2022-06-22T20:17:12+00:00"
},
{
"name": "consolidation/config",
"version": "2.1.0",
"version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/consolidation/config.git",
"reference": "0c15841b2bf60d9af1ce29884673e7d9d50c3b75"
"reference": "dae810c162f0e799ea3f35cc2f40b0797b6e4d26"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/consolidation/config/zipball/0c15841b2bf60d9af1ce29884673e7d9d50c3b75",
"reference": "0c15841b2bf60d9af1ce29884673e7d9d50c3b75",
"url": "https://api.github.com/repos/consolidation/config/zipball/dae810c162f0e799ea3f35cc2f40b0797b6e4d26",
"reference": "dae810c162f0e799ea3f35cc2f40b0797b6e4d26",
"shasum": ""
},
"require": {
@ -200,9 +200,9 @@
"description": "Provide configuration services for a commandline tool.",
"support": {
"issues": "https://github.com/consolidation/config/issues",
"source": "https://github.com/consolidation/config/tree/2.1.0"
"source": "https://github.com/consolidation/config/tree/2.1.1"
},
"time": "2022-02-24T00:32:42+00:00"
"time": "2022-06-22T19:59:34+00:00"
},
{
"name": "consolidation/log",
@ -1070,16 +1070,16 @@
},
{
"name": "symfony/console",
"version": "v6.1.0",
"version": "v6.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "c9646197ef43b0e2ff44af61e7f0571526fd4170"
"reference": "7fccea8728aa2d431a6725b02b3ce759049fc84d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/c9646197ef43b0e2ff44af61e7f0571526fd4170",
"reference": "c9646197ef43b0e2ff44af61e7f0571526fd4170",
"url": "https://api.github.com/repos/symfony/console/zipball/7fccea8728aa2d431a6725b02b3ce759049fc84d",
"reference": "7fccea8728aa2d431a6725b02b3ce759049fc84d",
"shasum": ""
},
"require": {
@ -1146,7 +1146,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v6.1.0"
"source": "https://github.com/symfony/console/tree/v6.1.4"
},
"funding": [
{
@ -1162,11 +1162,11 @@
"type": "tidelift"
}
],
"time": "2022-05-27T06:34:22+00:00"
"time": "2022-08-26T10:32:31+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.1.0",
"version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
@ -1213,7 +1213,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.1"
},
"funding": [
{
@ -1316,7 +1316,7 @@
},
{
"name": "symfony/event-dispatcher-contracts",
"version": "v3.1.0",
"version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher-contracts.git",
@ -1375,7 +1375,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.1.0"
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.1.1"
},
"funding": [
{
@ -1395,16 +1395,16 @@
},
{
"name": "symfony/filesystem",
"version": "v6.1.0",
"version": "v6.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "3132d2f43ca799c2aa099f9738d98228c56baa5d"
"reference": "3f39c04d2630c34019907b02f85672dac99f8659"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/3132d2f43ca799c2aa099f9738d98228c56baa5d",
"reference": "3132d2f43ca799c2aa099f9738d98228c56baa5d",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/3f39c04d2630c34019907b02f85672dac99f8659",
"reference": "3f39c04d2630c34019907b02f85672dac99f8659",
"shasum": ""
},
"require": {
@ -1438,7 +1438,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v6.1.0"
"source": "https://github.com/symfony/filesystem/tree/v6.1.4"
},
"funding": [
{
@ -1454,20 +1454,20 @@
"type": "tidelift"
}
],
"time": "2022-05-21T13:34:40+00:00"
"time": "2022-08-02T16:17:38+00:00"
},
{
"name": "symfony/finder",
"version": "v6.1.0",
"version": "v6.1.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "45b8beb69d6eb3b05a65689ebfd4222326773f8f"
"reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/45b8beb69d6eb3b05a65689ebfd4222326773f8f",
"reference": "45b8beb69d6eb3b05a65689ebfd4222326773f8f",
"url": "https://api.github.com/repos/symfony/finder/zipball/39696bff2c2970b3779a5cac7bf9f0b88fc2b709",
"reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709",
"shasum": ""
},
"require": {
@ -1502,7 +1502,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v6.1.0"
"source": "https://github.com/symfony/finder/tree/v6.1.3"
},
"funding": [
{
@ -1518,20 +1518,20 @@
"type": "tidelift"
}
],
"time": "2022-04-15T08:08:08+00:00"
"time": "2022-07-29T07:42:06+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "30885182c981ab175d4d034db0f6f469898070ab"
"reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab",
"reference": "30885182c981ab175d4d034db0f6f469898070ab",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"shasum": ""
},
"require": {
@ -1546,7 +1546,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1584,7 +1584,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0"
},
"funding": [
{
@ -1600,20 +1600,20 @@
"type": "tidelift"
}
],
"time": "2021-10-20T20:35:02+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "81b86b50cf841a64252b439e738e97f4a34e2783"
"reference": "433d05519ce6990bf3530fba6957499d327395c2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783",
"reference": "81b86b50cf841a64252b439e738e97f4a34e2783",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2",
"reference": "433d05519ce6990bf3530fba6957499d327395c2",
"shasum": ""
},
"require": {
@ -1625,7 +1625,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1665,7 +1665,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0"
},
"funding": [
{
@ -1681,20 +1681,20 @@
"type": "tidelift"
}
],
"time": "2021-11-23T21:10:46+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": {
@ -1706,7 +1706,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1749,7 +1749,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": [
{
@ -1765,20 +1765,20 @@
"type": "tidelift"
}
],
"time": "2021-02-19T12:13:01+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825"
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"shasum": ""
},
"require": {
@ -1793,7 +1793,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1832,7 +1832,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
},
"funding": [
{
@ -1848,20 +1848,20 @@
"type": "tidelift"
}
],
"time": "2021-11-30T18:21:41+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/process",
"version": "v6.1.0",
"version": "v6.1.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "318718453c2be58266f1a9e74063d13cb8dd4165"
"reference": "a6506e99cfad7059b1ab5cab395854a0a0c21292"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/318718453c2be58266f1a9e74063d13cb8dd4165",
"reference": "318718453c2be58266f1a9e74063d13cb8dd4165",
"url": "https://api.github.com/repos/symfony/process/zipball/a6506e99cfad7059b1ab5cab395854a0a0c21292",
"reference": "a6506e99cfad7059b1ab5cab395854a0a0c21292",
"shasum": ""
},
"require": {
@ -1893,7 +1893,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v6.1.0"
"source": "https://github.com/symfony/process/tree/v6.1.3"
},
"funding": [
{
@ -1909,20 +1909,20 @@
"type": "tidelift"
}
],
"time": "2022-05-11T12:12:29+00:00"
"time": "2022-06-27T17:24:16+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.1.0",
"version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "d66cd8ab656780f62c4215b903a420eb86358957"
"reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/d66cd8ab656780f62c4215b903a420eb86358957",
"reference": "d66cd8ab656780f62c4215b903a420eb86358957",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239",
"reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239",
"shasum": ""
},
"require": {
@ -1978,7 +1978,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v3.1.0"
"source": "https://github.com/symfony/service-contracts/tree/v3.1.1"
},
"funding": [
{
@ -1994,20 +1994,20 @@
"type": "tidelift"
}
],
"time": "2022-05-07T08:07:09+00:00"
"time": "2022-05-30T19:18:58+00:00"
},
{
"name": "symfony/string",
"version": "v6.1.0",
"version": "v6.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "d3edc75baf9f1d4f94879764dda2e1ac33499529"
"reference": "290972cad7b364e3befaa74ba0ec729800fb161c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/d3edc75baf9f1d4f94879764dda2e1ac33499529",
"reference": "d3edc75baf9f1d4f94879764dda2e1ac33499529",
"url": "https://api.github.com/repos/symfony/string/zipball/290972cad7b364e3befaa74ba0ec729800fb161c",
"reference": "290972cad7b364e3befaa74ba0ec729800fb161c",
"shasum": ""
},
"require": {
@ -2063,7 +2063,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v6.1.0"
"source": "https://github.com/symfony/string/tree/v6.1.4"
},
"funding": [
{
@ -2079,20 +2079,20 @@
"type": "tidelift"
}
],
"time": "2022-04-22T08:18:23+00:00"
"time": "2022-08-12T18:05:43+00:00"
},
{
"name": "symfony/yaml",
"version": "v6.1.0",
"version": "v6.1.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "84ce4f9d2d68f306f971a39d949d8f4b5550dba2"
"reference": "86ee4d8fa594ed45e40d86eedfda1bcb66c8d919"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/84ce4f9d2d68f306f971a39d949d8f4b5550dba2",
"reference": "84ce4f9d2d68f306f971a39d949d8f4b5550dba2",
"url": "https://api.github.com/repos/symfony/yaml/zipball/86ee4d8fa594ed45e40d86eedfda1bcb66c8d919",
"reference": "86ee4d8fa594ed45e40d86eedfda1bcb66c8d919",
"shasum": ""
},
"require": {
@ -2137,7 +2137,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v6.1.0"
"source": "https://github.com/symfony/yaml/tree/v6.1.4"
},
"funding": [
{
@ -2153,7 +2153,7 @@
"type": "tidelift"
}
],
"time": "2022-04-15T14:25:02+00:00"
"time": "2022-08-02T16:17:38+00:00"
}
],
"aliases": [],

Loading…
Cancel
Save