mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
Merge branch 'fever' into opml
This commit is contained in:
commit
2aa16f3405
22 changed files with 1221 additions and 301 deletions
|
@ -5,10 +5,12 @@ New features:
|
||||||
- Support for the Fever protocol (see README.md for details)
|
- Support for the Fever protocol (see README.md for details)
|
||||||
- Command line functionality for clearing a password, disabling the account
|
- Command line functionality for clearing a password, disabling the account
|
||||||
- Command line options for dealing with Fever passwords
|
- Command line options for dealing with Fever passwords
|
||||||
|
- Command line functionality for exporting subscriptions to OPML
|
||||||
- Command line documentation of all commands and options
|
- Command line documentation of all commands and options
|
||||||
|
|
||||||
Bug fixes:
|
Bug fixes:
|
||||||
- Treat command line option -h the same as --help
|
- Treat command line option -h the same as --help
|
||||||
|
- Sort Tiny Tiny RSS special feeds according to special ordering
|
||||||
|
|
||||||
Version 0.7.1 (2019-03-25)
|
Version 0.7.1 (2019-03-25)
|
||||||
==========================
|
==========================
|
||||||
|
|
|
@ -148,7 +148,6 @@ We are not aware of any other extensions to the TTRSS protocol. If you know of a
|
||||||
- Full-text search is not yet employed with any database, including PostgreSQL
|
- Full-text search is not yet employed with any database, including PostgreSQL
|
||||||
- Article hashes are normally SHA1; The Arsse uses SHA256 hashes
|
- Article hashes are normally SHA1; The Arsse uses SHA256 hashes
|
||||||
- Article attachments normally have unique IDs; The Arsse always gives attachments an ID of `"0"`
|
- Article attachments normally have unique IDs; The Arsse always gives attachments an ID of `"0"`
|
||||||
- The default sort order of the `getHeadlines` operation normally uses custom sorting for "special" feeds; The Arsse's default sort order is equivalent to `feed_dates` for all feeds
|
|
||||||
- The `getCounters` operation normally omits members with zero unread; The Arsse includes everything to appease some clients
|
- The `getCounters` operation normally omits members with zero unread; The Arsse includes everything to appease some clients
|
||||||
|
|
||||||
#### Other notes
|
#### Other notes
|
||||||
|
|
|
@ -10,6 +10,12 @@ usually prudent:
|
||||||
- If installing from source, update dependencies with:
|
- If installing from source, update dependencies with:
|
||||||
`composer install -o --no-dev`
|
`composer install -o --no-dev`
|
||||||
|
|
||||||
|
Upgrading from 0.7.1 to 0.8.0
|
||||||
|
=============================
|
||||||
|
|
||||||
|
- The database schema has changed from rev4 to rev5; if upgrading the database
|
||||||
|
manually, apply the 4.sql file
|
||||||
|
|
||||||
|
|
||||||
Upgrading from 0.5.1 to 0.6.0
|
Upgrading from 0.5.1 to 0.6.0
|
||||||
=============================
|
=============================
|
||||||
|
|
|
@ -25,7 +25,7 @@ if (\PHP_SAPI === "cli") {
|
||||||
$conf = file_exists(BASE."config.php") ? new Conf(BASE."config.php") : new Conf;
|
$conf = file_exists(BASE."config.php") ? new Conf(BASE."config.php") : new Conf;
|
||||||
Arsse::load($conf);
|
Arsse::load($conf);
|
||||||
// handle Web requests
|
// handle Web requests
|
||||||
$emitter = new \Zend\Diactoros\Response\SapiEmitter();
|
$emitter = new \Zend\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||||
$response = (new REST)->dispatch();
|
$response = (new REST)->dispatch();
|
||||||
$emitter->emit($response);
|
$emitter->emit($response);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,16 +18,17 @@
|
||||||
|
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.0",
|
"php": "7.*",
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-hash": "*",
|
"ext-hash": "*",
|
||||||
"ext-dom": "*",
|
"ext-dom": "*",
|
||||||
"p3k/picofeed": "0.1.*",
|
"p3k/picofeed": "0.1.*",
|
||||||
"hosteurope/password-generator": "^1.0",
|
"hosteurope/password-generator": "1.*",
|
||||||
"docopt/docopt": "^1.0",
|
"docopt/docopt": "1.*",
|
||||||
"jkingweb/druuid": "^3.0",
|
"jkingweb/druuid": "3.*",
|
||||||
"zendframework/zend-diactoros": "^1.6"
|
"zendframework/zend-diactoros": "2.*",
|
||||||
|
"zendframework/zend-httphandlerrunner": "1.*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"bamarni/composer-bin-plugin": "*"
|
"bamarni/composer-bin-plugin": "*"
|
||||||
|
|
196
composer.lock
generated
196
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "f61a02cd168914d91847b89dcd00d464",
|
"content-hash": "c2b0698669d89268ffb995a5e1d6667a",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "docopt/docopt",
|
"name": "docopt/docopt",
|
||||||
|
@ -190,6 +190,58 @@
|
||||||
"homepage": "https://github.com/miniflux/picoFeed",
|
"homepage": "https://github.com/miniflux/picoFeed",
|
||||||
"time": "2017-11-30T00:16:58+00:00"
|
"time": "2017-11-30T00:16:58+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"
|
||||||
|
],
|
||||||
|
"time": "2019-04-30T12:38:16+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/http-message",
|
"name": "psr/http-message",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
@ -241,39 +293,95 @@
|
||||||
"time": "2016-08-06T14:39:51+00:00"
|
"time": "2016-08-06T14:39:51+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "zendframework/zend-diactoros",
|
"name": "psr/http-server-handler",
|
||||||
"version": "1.8.6",
|
"version": "1.0.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/zendframework/zend-diactoros.git",
|
"url": "https://github.com/php-fig/http-server-handler.git",
|
||||||
"reference": "20da13beba0dde8fb648be3cc19765732790f46e"
|
"reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/20da13beba0dde8fb648be3cc19765732790f46e",
|
"url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7",
|
||||||
"reference": "20da13beba0dde8fb648be3cc19765732790f46e",
|
"reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^5.6 || ^7.0",
|
"php": ">=7.0",
|
||||||
|
"psr/http-message": "^1.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Http\\Server\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "http://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for HTTP server-side request handler",
|
||||||
|
"keywords": [
|
||||||
|
"handler",
|
||||||
|
"http",
|
||||||
|
"http-interop",
|
||||||
|
"psr",
|
||||||
|
"psr-15",
|
||||||
|
"psr-7",
|
||||||
|
"request",
|
||||||
|
"response",
|
||||||
|
"server"
|
||||||
|
],
|
||||||
|
"time": "2018-10-30T16:46:14+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zendframework/zend-diactoros",
|
||||||
|
"version": "2.1.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/zendframework/zend-diactoros.git",
|
||||||
|
"reference": "279723778c40164bcf984a2df12ff2c6ec5e61c1"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/279723778c40164bcf984a2df12ff2c6ec5e61c1",
|
||||||
|
"reference": "279723778c40164bcf984a2df12ff2c6ec5e61c1",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1",
|
||||||
|
"psr/http-factory": "^1.0",
|
||||||
"psr/http-message": "^1.0"
|
"psr/http-message": "^1.0"
|
||||||
},
|
},
|
||||||
"provide": {
|
"provide": {
|
||||||
|
"psr/http-factory-implementation": "1.0",
|
||||||
"psr/http-message-implementation": "1.0"
|
"psr/http-message-implementation": "1.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"ext-dom": "*",
|
"ext-dom": "*",
|
||||||
"ext-libxml": "*",
|
"ext-libxml": "*",
|
||||||
|
"http-interop/http-factory-tests": "^0.5.0",
|
||||||
"php-http/psr7-integration-tests": "dev-master",
|
"php-http/psr7-integration-tests": "dev-master",
|
||||||
"phpunit/phpunit": "^5.7.16 || ^6.0.8 || ^7.2.7",
|
"phpunit/phpunit": "^7.0.2",
|
||||||
"zendframework/zend-coding-standard": "~1.0"
|
"zendframework/zend-coding-standard": "~1.0.0"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "1.8.x-dev",
|
"dev-master": "2.1.x-dev",
|
||||||
"dev-develop": "1.9.x-dev",
|
"dev-develop": "2.2.x-dev",
|
||||||
"dev-release-2.0": "2.0.x-dev"
|
"dev-release-1.8": "1.8.x-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
@ -293,16 +401,70 @@
|
||||||
},
|
},
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
"license": [
|
"license": [
|
||||||
"BSD-2-Clause"
|
"BSD-3-Clause"
|
||||||
],
|
],
|
||||||
"description": "PSR HTTP Message implementations",
|
"description": "PSR HTTP Message implementations",
|
||||||
"homepage": "https://github.com/zendframework/zend-diactoros",
|
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"http",
|
"http",
|
||||||
"psr",
|
"psr",
|
||||||
"psr-7"
|
"psr-7"
|
||||||
],
|
],
|
||||||
"time": "2018-09-05T19:29:37+00:00"
|
"time": "2019-07-10T16:13:25+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zendframework/zend-httphandlerrunner",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/zendframework/zend-httphandlerrunner.git",
|
||||||
|
"reference": "75fb12751fe9d6e392cce1ee0d687dacae2db787"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/zendframework/zend-httphandlerrunner/zipball/75fb12751fe9d6e392cce1ee0d687dacae2db787",
|
||||||
|
"reference": "75fb12751fe9d6e392cce1ee0d687dacae2db787",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1",
|
||||||
|
"psr/http-message": "^1.0",
|
||||||
|
"psr/http-message-implementation": "^1.0",
|
||||||
|
"psr/http-server-handler": "^1.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^7.0.2",
|
||||||
|
"zendframework/zend-coding-standard": "~1.0.0",
|
||||||
|
"zendframework/zend-diactoros": "^1.7 || ^2.1.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.1.x-dev",
|
||||||
|
"dev-develop": "1.2.x-dev"
|
||||||
|
},
|
||||||
|
"zf": {
|
||||||
|
"config-provider": "Zend\\HttpHandlerRunner\\ConfigProvider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Zend\\HttpHandlerRunner\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"description": "Execute PSR-15 RequestHandlerInterface instances and emit responses they generate.",
|
||||||
|
"keywords": [
|
||||||
|
"ZendFramework",
|
||||||
|
"components",
|
||||||
|
"expressive",
|
||||||
|
"psr-15",
|
||||||
|
"psr-7",
|
||||||
|
"zf"
|
||||||
|
],
|
||||||
|
"time": "2019-02-19T18:20:34+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "zendframework/zendxml",
|
"name": "zendframework/zendxml",
|
||||||
|
@ -398,7 +560,7 @@
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "^7.0",
|
"php": "7.*",
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-hash": "*",
|
"ext-hash": "*",
|
||||||
|
|
|
@ -9,7 +9,6 @@ namespace JKingWeb\Arsse\Context;
|
||||||
class Context extends ExclusionContext {
|
class Context extends ExclusionContext {
|
||||||
/** @var ExclusionContext */
|
/** @var ExclusionContext */
|
||||||
public $not;
|
public $not;
|
||||||
public $reverse = false;
|
|
||||||
public $limit = 0;
|
public $limit = 0;
|
||||||
public $offset = 0;
|
public $offset = 0;
|
||||||
public $unread;
|
public $unread;
|
||||||
|
@ -31,10 +30,6 @@ class Context extends ExclusionContext {
|
||||||
unset($this->not);
|
unset($this->not);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reverse(bool $spec = null) {
|
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function limit(int $spec = null) {
|
public function limit(int $spec = null) {
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,16 +11,23 @@ use JKingWeb\Arsse\Misc\Date;
|
||||||
|
|
||||||
class ExclusionContext {
|
class ExclusionContext {
|
||||||
public $folder;
|
public $folder;
|
||||||
|
public $folders;
|
||||||
public $folderShallow;
|
public $folderShallow;
|
||||||
|
public $foldersShallow;
|
||||||
public $tag;
|
public $tag;
|
||||||
|
public $tags;
|
||||||
public $tagName;
|
public $tagName;
|
||||||
|
public $tagNames;
|
||||||
public $subscription;
|
public $subscription;
|
||||||
|
public $subscriptions;
|
||||||
public $edition;
|
public $edition;
|
||||||
public $article;
|
|
||||||
public $editions;
|
public $editions;
|
||||||
|
public $article;
|
||||||
public $articles;
|
public $articles;
|
||||||
public $label;
|
public $label;
|
||||||
|
public $labels;
|
||||||
public $labelName;
|
public $labelName;
|
||||||
|
public $labelNames;
|
||||||
public $annotationTerms;
|
public $annotationTerms;
|
||||||
public $searchTerms;
|
public $searchTerms;
|
||||||
public $titleTerms;
|
public $titleTerms;
|
||||||
|
@ -42,6 +49,7 @@ class ExclusionContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __clone() {
|
public function __clone() {
|
||||||
|
// if the context was cloned because its parent was cloned, change the parent to the clone
|
||||||
if ($this->parent) {
|
if ($this->parent) {
|
||||||
$t = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1];
|
$t = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1];
|
||||||
if (($t['object'] ?? null) instanceof self && $t['function'] === "__clone") {
|
if (($t['object'] ?? null) instanceof self && $t['function'] === "__clone") {
|
||||||
|
@ -70,16 +78,18 @@ class ExclusionContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function cleanIdArray(array $spec): array {
|
protected function cleanIdArray(array $spec, bool $allowZero = false): array {
|
||||||
$spec = array_values($spec);
|
$spec = array_values($spec);
|
||||||
for ($a = 0; $a < sizeof($spec); $a++) {
|
for ($a = 0; $a < sizeof($spec); $a++) {
|
||||||
if (ValueInfo::id($spec[$a])) {
|
if (ValueInfo::id($spec[$a], $allowZero)) {
|
||||||
$spec[$a] = (int) $spec[$a];
|
$spec[$a] = (int) $spec[$a];
|
||||||
} else {
|
} else {
|
||||||
$spec[$a] = 0;
|
$spec[$a] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return array_values(array_unique(array_filter($spec)));
|
return array_values(array_unique(array_filter($spec, function($v) {
|
||||||
|
return !is_null($v);
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function cleanStringArray(array $spec): array {
|
protected function cleanStringArray(array $spec): array {
|
||||||
|
@ -99,22 +109,57 @@ class ExclusionContext {
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function folders(array $spec = null) {
|
||||||
|
if (isset($spec)) {
|
||||||
|
$spec = $this->cleanIdArray($spec, true);
|
||||||
|
}
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
public function folderShallow(int $spec = null) {
|
public function folderShallow(int $spec = null) {
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function foldersShallow(array $spec = null) {
|
||||||
|
if (isset($spec)) {
|
||||||
|
$spec = $this->cleanIdArray($spec, true);
|
||||||
|
}
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
public function tag(int $spec = null) {
|
public function tag(int $spec = null) {
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tags(array $spec = null) {
|
||||||
|
if (isset($spec)) {
|
||||||
|
$spec = $this->cleanIdArray($spec);
|
||||||
|
}
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
public function tagName(string $spec = null) {
|
public function tagName(string $spec = null) {
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tagNames(array $spec = null) {
|
||||||
|
if (isset($spec)) {
|
||||||
|
$spec = $this->cleanStringArray($spec);
|
||||||
|
}
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
public function subscription(int $spec = null) {
|
public function subscription(int $spec = null) {
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function subscriptions(array $spec = null) {
|
||||||
|
if (isset($spec)) {
|
||||||
|
$spec = $this->cleanIdArray($spec);
|
||||||
|
}
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
public function edition(int $spec = null) {
|
public function edition(int $spec = null) {
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
@ -141,10 +186,24 @@ class ExclusionContext {
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function labels(array $spec = null) {
|
||||||
|
if (isset($spec)) {
|
||||||
|
$spec = $this->cleanIdArray($spec);
|
||||||
|
}
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
public function labelName(string $spec = null) {
|
public function labelName(string $spec = null) {
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function labelNames(array $spec = null) {
|
||||||
|
if (isset($spec)) {
|
||||||
|
$spec = $this->cleanStringArray($spec);
|
||||||
|
}
|
||||||
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
|
}
|
||||||
|
|
||||||
public function annotationTerms(array $spec = null) {
|
public function annotationTerms(array $spec = null) {
|
||||||
if (isset($spec)) {
|
if (isset($spec)) {
|
||||||
$spec = $this->cleanStringArray($spec);
|
$spec = $this->cleanStringArray($spec);
|
||||||
|
|
245
lib/Database.php
245
lib/Database.php
|
@ -1239,6 +1239,37 @@ class Database {
|
||||||
)->run($feedID, $vId, $vHashUT, $vHashUC, $vHashTC);
|
)->run($feedID, $vId, $vHashUT, $vHashUC, $vHashTC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns an associative array of result column names and their SQL computations for article queries
|
||||||
|
*
|
||||||
|
* This is used for whitelisting and defining both output column and order-by columns, as well as for resolution of some context options
|
||||||
|
*/
|
||||||
|
protected function articleColumns(): array {
|
||||||
|
$greatest = $this->db->sqlToken("greatest");
|
||||||
|
return [
|
||||||
|
'id' => "arsse_articles.id",
|
||||||
|
'edition' => "latest_editions.edition",
|
||||||
|
'url' => "arsse_articles.url",
|
||||||
|
'title' => "arsse_articles.title",
|
||||||
|
'author' => "arsse_articles.author",
|
||||||
|
'content' => "arsse_articles.content",
|
||||||
|
'guid' => "arsse_articles.guid",
|
||||||
|
'fingerprint' => "arsse_articles.url_title_hash || ':' || arsse_articles.url_content_hash || ':' || arsse_articles.title_content_hash",
|
||||||
|
'folder' => "coalesce(arsse_subscriptions.folder,0)",
|
||||||
|
'subscription' => "arsse_subscriptions.id",
|
||||||
|
'feed' => "arsse_subscriptions.feed",
|
||||||
|
'starred' => "coalesce(arsse_marks.starred,0)",
|
||||||
|
'unread' => "abs(coalesce(arsse_marks.read,0) - 1)",
|
||||||
|
'note' => "coalesce(arsse_marks.note,'')",
|
||||||
|
'published_date' => "arsse_articles.published",
|
||||||
|
'edited_date' => "arsse_articles.edited",
|
||||||
|
'modified_date' => "arsse_articles.modified",
|
||||||
|
'marked_date' => "$greatest(arsse_articles.modified, coalesce(arsse_marks.modified, '0001-01-01 00:00:00'), coalesce(label_stats.modified, '0001-01-01 00:00:00'))",
|
||||||
|
'subscription_title' => "coalesce(arsse_subscriptions.title, arsse_feeds.title)",
|
||||||
|
'media_url' => "arsse_enclosures.url",
|
||||||
|
'media_type' => "arsse_enclosures.type",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/** Computes an SQL query to find and retrieve data about articles in the database
|
/** Computes an SQL query to find and retrieve data about articles in the database
|
||||||
*
|
*
|
||||||
* If an empty column list is supplied, a count of articles matching the context is queried instead
|
* If an empty column list is supplied, a count of articles matching the context is queried instead
|
||||||
|
@ -1271,48 +1302,30 @@ class Database {
|
||||||
$this->labelValidateId($user, $context->labelName, true);
|
$this->labelValidateId($user, $context->labelName, true);
|
||||||
}
|
}
|
||||||
// prepare the output column list; the column definitions are also used later
|
// prepare the output column list; the column definitions are also used later
|
||||||
$greatest = $this->db->sqlToken("greatest");
|
$colDefs = $this->articleColumns();
|
||||||
$colDefs = [
|
|
||||||
'id' => "arsse_articles.id",
|
|
||||||
'edition' => "latest_editions.edition",
|
|
||||||
'url' => "arsse_articles.url",
|
|
||||||
'title' => "arsse_articles.title",
|
|
||||||
'author' => "arsse_articles.author",
|
|
||||||
'content' => "arsse_articles.content",
|
|
||||||
'guid' => "arsse_articles.guid",
|
|
||||||
'fingerprint' => "arsse_articles.url_title_hash || ':' || arsse_articles.url_content_hash || ':' || arsse_articles.title_content_hash",
|
|
||||||
'folder' => "coalesce(arsse_subscriptions.folder,0)",
|
|
||||||
'subscription' => "arsse_subscriptions.id",
|
|
||||||
'feed' => "arsse_subscriptions.feed",
|
|
||||||
'starred' => "coalesce(arsse_marks.starred,0)",
|
|
||||||
'unread' => "abs(coalesce(arsse_marks.read,0) - 1)",
|
|
||||||
'note' => "coalesce(arsse_marks.note,'')",
|
|
||||||
'published_date' => "arsse_articles.published",
|
|
||||||
'edited_date' => "arsse_articles.edited",
|
|
||||||
'modified_date' => "arsse_articles.modified",
|
|
||||||
'marked_date' => "$greatest(arsse_articles.modified, coalesce(arsse_marks.modified, '0001-01-01 00:00:00'), coalesce(label_stats.modified, '0001-01-01 00:00:00'))",
|
|
||||||
'subscription_title' => "coalesce(arsse_subscriptions.title, arsse_feeds.title)",
|
|
||||||
'media_url' => "arsse_enclosures.url",
|
|
||||||
'media_type' => "arsse_enclosures.type",
|
|
||||||
];
|
|
||||||
if (!$cols) {
|
if (!$cols) {
|
||||||
// if no columns are specified return a count
|
// if no columns are specified return a count; don't borther with sorting
|
||||||
$columns = "count(distinct arsse_articles.id) as count";
|
$outColumns = "count(distinct arsse_articles.id) as count";
|
||||||
} else {
|
} else {
|
||||||
$columns = [];
|
// normalize requested output and sorting columns
|
||||||
|
$norm = function($v) {
|
||||||
|
return trim(strtolower(ValueInfo::normalize($v, ValueInfo::T_STRING)));
|
||||||
|
};
|
||||||
|
$cols = array_map($norm, $cols);
|
||||||
|
// make an output column list
|
||||||
|
$outColumns = [];
|
||||||
foreach ($cols as $col) {
|
foreach ($cols as $col) {
|
||||||
$col = trim(strtolower($col));
|
|
||||||
if (!isset($colDefs[$col])) {
|
if (!isset($colDefs[$col])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$columns[] = $colDefs[$col]." as ".$col;
|
$outColumns[] = $colDefs[$col]." as ".$col;
|
||||||
}
|
}
|
||||||
$columns = implode(",", $columns);
|
$outColumns = implode(",", $outColumns);
|
||||||
}
|
}
|
||||||
// define the basic query, to which we add lots of stuff where necessary
|
// define the basic query, to which we add lots of stuff where necessary
|
||||||
$q = new Query(
|
$q = new Query(
|
||||||
"SELECT
|
"SELECT
|
||||||
$columns
|
$outColumns
|
||||||
from arsse_articles
|
from arsse_articles
|
||||||
join arsse_subscriptions on arsse_subscriptions.feed = arsse_articles.feed and arsse_subscriptions.owner = ?
|
join arsse_subscriptions on arsse_subscriptions.feed = arsse_articles.feed and arsse_subscriptions.owner = ?
|
||||||
join arsse_feeds on arsse_subscriptions.feed = arsse_feeds.id
|
join arsse_feeds on arsse_subscriptions.feed = arsse_feeds.id
|
||||||
|
@ -1344,7 +1357,9 @@ class Database {
|
||||||
"markedSince" => ["marked_date", ">=", "datetime", "notMarkedSince"],
|
"markedSince" => ["marked_date", ">=", "datetime", "notMarkedSince"],
|
||||||
"notMarkedSince" => ["marked_date", "<=", "datetime", "markedSince"],
|
"notMarkedSince" => ["marked_date", "<=", "datetime", "markedSince"],
|
||||||
"folderShallow" => ["folder", "=", "int", ""],
|
"folderShallow" => ["folder", "=", "int", ""],
|
||||||
|
"foldersShallow" => ["folder", "in", "int", ""],
|
||||||
"subscription" => ["subscription", "=", "int", ""],
|
"subscription" => ["subscription", "=", "int", ""],
|
||||||
|
"subscriptions" => ["subscription", "in", "int", ""],
|
||||||
"unread" => ["unread", "=", "bool", ""],
|
"unread" => ["unread", "=", "bool", ""],
|
||||||
"starred" => ["starred", "=", "bool", ""],
|
"starred" => ["starred", "=", "bool", ""],
|
||||||
];
|
];
|
||||||
|
@ -1395,6 +1410,79 @@ class Database {
|
||||||
$q->setWhereNot("{$colDefs[$col]} $op ?", $type, $context->not->$m);
|
$q->setWhereNot("{$colDefs[$col]} $op ?", $type, $context->not->$m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// handle labels and tags
|
||||||
|
$options = [
|
||||||
|
'label' => [
|
||||||
|
'match_col' => "arsse_articles.id",
|
||||||
|
'cte_name' => "labelled",
|
||||||
|
'cte_cols' => ["article", "label_id", "label_name"],
|
||||||
|
'cte_body' => "SELECT m.article, l.id, l.name from arsse_label_members as m join arsse_labels as l on l.id = m.label where l.owner = ? and m.assigned = 1",
|
||||||
|
'cte_types' => ["str"],
|
||||||
|
'cte_values' => [$user],
|
||||||
|
'options' => [
|
||||||
|
'label' => ['use_name' => false, 'multi' => false],
|
||||||
|
'labels' => ['use_name' => false, 'multi' => true],
|
||||||
|
'labelName' => ['use_name' => true, 'multi' => false],
|
||||||
|
'labelNames' => ['use_name' => true, 'multi' => true],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'tag' => [
|
||||||
|
'match_col' => "arsse_subscriptions.id",
|
||||||
|
'cte_name' => "tagged",
|
||||||
|
'cte_cols' => ["subscription", "tag_id", "tag_name"],
|
||||||
|
'cte_body' => "SELECT m.subscription, t.id, t.name from arsse_tag_members as m join arsse_tags as t on t.id = m.tag where t.owner = ? and m.assigned = 1",
|
||||||
|
'cte_types' => ["str"],
|
||||||
|
'cte_values' => [$user],
|
||||||
|
'options' => [
|
||||||
|
'tag' => ['use_name' => false, 'multi' => false],
|
||||||
|
'tags' => ['use_name' => false, 'multi' => true],
|
||||||
|
'tagName' => ['use_name' => true, 'multi' => false],
|
||||||
|
'tagNames' => ['use_name' => true, 'multi' => true],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
foreach ($options as $opt) {
|
||||||
|
$seen = false;
|
||||||
|
$match = $opt['match_col'];
|
||||||
|
$table = $opt['cte_name'];
|
||||||
|
foreach ($opt['options'] as $m => $props) {
|
||||||
|
$named = $props['use_name'];
|
||||||
|
$multi = $props['multi'];
|
||||||
|
$selection = $opt['cte_cols'][0];
|
||||||
|
$col = $opt['cte_cols'][$named ? 2 : 1];
|
||||||
|
if ($context->$m()) {
|
||||||
|
$seen = true;
|
||||||
|
if (!$context->$m) {
|
||||||
|
throw new Db\ExceptionInput("tooShort", ['field' => $m, 'action' => $this->caller(), 'min' => 1]); // must have at least one array element
|
||||||
|
}
|
||||||
|
if ($multi) {
|
||||||
|
list($test, $types, $values) = $this->generateIn($context->$m, $named ? "str" : "int");
|
||||||
|
$test = "in ($test)";
|
||||||
|
} else {
|
||||||
|
$test = "= ?";
|
||||||
|
$types = $named ? "str" : "int";
|
||||||
|
$values = $context->$m;
|
||||||
|
}
|
||||||
|
$q->setWhere("$match in (select $selection from $table where $col $test)", $types, $values);
|
||||||
|
}
|
||||||
|
if ($context->not->$m()) {
|
||||||
|
$seen = true;
|
||||||
|
if ($multi) {
|
||||||
|
list($test, $types, $values) = $this->generateIn($context->not->$m, $named ? "str" : "int");
|
||||||
|
$test = "in ($test)";
|
||||||
|
} else {
|
||||||
|
$test = "= ?";
|
||||||
|
$types = $named ? "str" : "int";
|
||||||
|
$values = $context->not->$m;
|
||||||
|
}
|
||||||
|
$q->setWhereNot("$match in (select $selection from $table where $col $test)", $types, $values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($seen) {
|
||||||
|
$spec = $opt['cte_name']."(".implode(",", $opt['cte_cols']).")";
|
||||||
|
$q->setCTE($spec, $opt['cte_body'], $opt['cte_types'], $opt['cte_values']);
|
||||||
|
}
|
||||||
|
}
|
||||||
// handle complex context options
|
// handle complex context options
|
||||||
if ($context->annotated()) {
|
if ($context->annotated()) {
|
||||||
$comp = ($context->annotated) ? "<>" : "=";
|
$comp = ($context->annotated) ? "<>" : "=";
|
||||||
|
@ -1405,48 +1493,32 @@ class Database {
|
||||||
$op = $context->labelled ? ">" : "=";
|
$op = $context->labelled ? ">" : "=";
|
||||||
$q->setWhere("coalesce(label_stats.assigned,0) $op 0");
|
$q->setWhere("coalesce(label_stats.assigned,0) $op 0");
|
||||||
}
|
}
|
||||||
if ($context->label() || $context->not->label() || $context->labelName() || $context->not->labelName()) {
|
|
||||||
$q->setCTE("labelled(article,label_id,label_name)", "SELECT m.article, l.id, l.name from arsse_label_members as m join arsse_labels as l on l.id = m.label where l.owner = ? and m.assigned = 1", "str", $user);
|
|
||||||
if ($context->label()) {
|
|
||||||
$q->setWhere("arsse_articles.id in (select article from labelled where label_id = ?)", "int", $context->label);
|
|
||||||
}
|
|
||||||
if ($context->not->label()) {
|
|
||||||
$q->setWhereNot("arsse_articles.id in (select article from labelled where label_id = ?)", "int", $context->not->label);
|
|
||||||
}
|
|
||||||
if ($context->labelName()) {
|
|
||||||
$q->setWhere("arsse_articles.id in (select article from labelled where label_name = ?)", "str", $context->labelName);
|
|
||||||
}
|
|
||||||
if ($context->not->labelName()) {
|
|
||||||
$q->setWhereNot("arsse_articles.id in (select article from labelled where label_name = ?)", "str", $context->not->labelName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($context->tag() || $context->not->tag() || $context->tagName() || $context->not->tagName()) {
|
|
||||||
$q->setCTE("tagged(id,name,subscription)", "SELECT arsse_tags.id, arsse_tags.name, arsse_tag_members.subscription FROM arsse_tag_members join arsse_tags on arsse_tags.id = arsse_tag_members.tag WHERE arsse_tags.owner = ? and assigned = 1", "str", $user);
|
|
||||||
if ($context->tag()) {
|
|
||||||
$q->setWhere("arsse_subscriptions.id in (select subscription from tagged where id = ?)", "int", $context->tag);
|
|
||||||
}
|
|
||||||
if ($context->not->tag()) {
|
|
||||||
$q->setWhereNot("arsse_subscriptions.id in (select subscription from tagged where id = ?)", "int", $context->not->tag);
|
|
||||||
}
|
|
||||||
if ($context->tagName()) {
|
|
||||||
$q->setWhere("arsse_subscriptions.id in (select subscription from tagged where name = ?)", "str", $context->tagName);
|
|
||||||
}
|
|
||||||
if ($context->not->tagName()) {
|
|
||||||
$q->setWhereNot("arsse_subscriptions.id in (select subscription from tagged where name = ?)", "str", $context->not->tagName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($context->folder()) {
|
if ($context->folder()) {
|
||||||
// add a common table expression to list the folder and its children so that we select from the entire subtree
|
// add a common table expression to list the folder and its children so that we select from the entire subtree
|
||||||
$q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent = folder", "int", $context->folder);
|
$q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on coalesce(parent,0) = folder", "int", $context->folder);
|
||||||
// limit subscriptions to the listed folders
|
// limit subscriptions to the listed folders
|
||||||
$q->setWhere("coalesce(arsse_subscriptions.folder,0) in (select folder from folders)");
|
$q->setWhere("coalesce(arsse_subscriptions.folder,0) in (select folder from folders)");
|
||||||
}
|
}
|
||||||
|
if ($context->folders()) {
|
||||||
|
list($inClause, $inTypes, $inValues) = $this->generateIn($context->folders, "int");
|
||||||
|
// add a common table expression to list the folders and their children so that we select from the entire subtree
|
||||||
|
$q->setCTE("folders_multi(folder)", "SELECT id as folder from (select id from (select 0 as id union select id from arsse_folders where owner = ?) as f where id in ($inClause)) as folders_multi union select id from arsse_folders join folders_multi on coalesce(parent,0) = folder", ["str", $inTypes], [$user, $inValues]);
|
||||||
|
// limit subscriptions to the listed folders
|
||||||
|
$q->setWhere("coalesce(arsse_subscriptions.folder,0) in (select folder from folders_multi)");
|
||||||
|
}
|
||||||
if ($context->not->folder()) {
|
if ($context->not->folder()) {
|
||||||
// add a common table expression to list the folder and its children so that we exclude from the entire subtree
|
// add a common table expression to list the folder and its children so that we exclude from the entire subtree
|
||||||
$q->setCTE("folders_excluded(folder)", "SELECT ? union select id from arsse_folders join folders_excluded on parent = folder", "int", $context->not->folder);
|
$q->setCTE("folders_excluded(folder)", "SELECT ? union select id from arsse_folders join folders_excluded on coalesce(parent,0) = folder", "int", $context->not->folder);
|
||||||
// excluded any subscriptions in the listed folders
|
// excluded any subscriptions in the listed folders
|
||||||
$q->setWhereNot("coalesce(arsse_subscriptions.folder,0) in (select folder from folders_excluded)");
|
$q->setWhereNot("coalesce(arsse_subscriptions.folder,0) in (select folder from folders_excluded)");
|
||||||
}
|
}
|
||||||
|
if ($context->not->folders()) {
|
||||||
|
list($inClause, $inTypes, $inValues) = $this->generateIn($context->not->folders, "int");
|
||||||
|
// add a common table expression to list the folders and their children so that we select from the entire subtree
|
||||||
|
$q->setCTE("folders_multi_excluded(folder)", "SELECT id as folder from (select id from (select 0 as id union select id from arsse_folders where owner = ?) as f where id in ($inClause)) as folders_multi_excluded union select id from arsse_folders join folders_multi_excluded on coalesce(parent,0) = folder", ["str", $inTypes], [$user, $inValues]);
|
||||||
|
// limit subscriptions to the listed folders
|
||||||
|
$q->setWhereNot("coalesce(arsse_subscriptions.folder,0) in (select folder from folders_multi_excluded)");
|
||||||
|
}
|
||||||
// handle text-matching context options
|
// handle text-matching context options
|
||||||
$options = [
|
$options = [
|
||||||
"titleTerms" => ["arsse_articles.title"],
|
"titleTerms" => ["arsse_articles.title"],
|
||||||
|
@ -1454,20 +1526,20 @@ class Database {
|
||||||
"authorTerms" => ["arsse_articles.author"],
|
"authorTerms" => ["arsse_articles.author"],
|
||||||
"annotationTerms" => ["arsse_marks.note"],
|
"annotationTerms" => ["arsse_marks.note"],
|
||||||
];
|
];
|
||||||
foreach ($options as $m => $cols) {
|
foreach ($options as $m => $columns) {
|
||||||
if (!$context->$m()) {
|
if (!$context->$m()) {
|
||||||
continue;
|
continue;
|
||||||
} elseif (!$context->$m) {
|
} elseif (!$context->$m) {
|
||||||
throw new Db\ExceptionInput("tooShort", ['field' => $m, 'action' => $this->caller(), 'min' => 1]); // must have at least one array element
|
throw new Db\ExceptionInput("tooShort", ['field' => $m, 'action' => $this->caller(), 'min' => 1]); // must have at least one array element
|
||||||
}
|
}
|
||||||
$q->setWhere(...$this->generateSearch($context->$m, $cols));
|
$q->setWhere(...$this->generateSearch($context->$m, $columns));
|
||||||
}
|
}
|
||||||
// further handle exclusionary text-matching context options
|
// further handle exclusionary text-matching context options
|
||||||
foreach ($options as $m => $cols) {
|
foreach ($options as $m => $columns) {
|
||||||
if (!$context->not->$m() || !$context->not->$m) {
|
if (!$context->not->$m() || !$context->not->$m) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$q->setWhereNot(...$this->generateSearch($context->not->$m, $cols, true));
|
$q->setWhereNot(...$this->generateSearch($context->not->$m, $columns, true));
|
||||||
}
|
}
|
||||||
// return the query
|
// return the query
|
||||||
return $q;
|
return $q;
|
||||||
|
@ -1479,16 +1551,47 @@ class Database {
|
||||||
*
|
*
|
||||||
* @param string $user The user whose articles are to be listed
|
* @param string $user The user whose articles are to be listed
|
||||||
* @param Context $context The search context
|
* @param Context $context The search context
|
||||||
* @param array $cols The columns to return in the result set, any of: id, edition, url, title, author, content, guid, fingerprint, folder, subscription, feed, starred, unread, note, published_date, edited_date, modified_date, marked_date, subscription_title, media_url, media_type
|
* @param array $fieldss The columns to return in the result set, any of: id, edition, url, title, author, content, guid, fingerprint, folder, subscription, feed, starred, unread, note, published_date, edited_date, modified_date, marked_date, subscription_title, media_url, media_type
|
||||||
|
* @param array $sort The columns to sort the result by eg. "edition desc" in decreasing order of importance
|
||||||
*/
|
*/
|
||||||
public function articleList(string $user, Context $context = null, array $fields = ["id"]): Db\Result {
|
public function articleList(string $user, Context $context = null, array $fields = ["id"], array $sort = []): Db\Result {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
|
// make a base query based on context and output columns
|
||||||
$context = $context ?? new Context;
|
$context = $context ?? new Context;
|
||||||
$q = $this->articleQuery($user, $context, $fields);
|
$q = $this->articleQuery($user, $context, $fields);
|
||||||
$q->setOrder("arsse_articles.edited".($context->reverse ? " desc" : ""));
|
// make an ORDER BY column list
|
||||||
$q->setOrder("latest_editions.edition".($context->reverse ? " desc" : ""));
|
$colDefs = $this->articleColumns();
|
||||||
|
// normalize requested output and sorting columns
|
||||||
|
$norm = function($v) {
|
||||||
|
return trim(strtolower((string) $v));
|
||||||
|
};
|
||||||
|
$fields = array_map($norm, $fields);
|
||||||
|
$sort = array_map($norm, $sort);
|
||||||
|
foreach ($sort as $spec) {
|
||||||
|
$col = explode(" ", $spec, 2);
|
||||||
|
$order = $col[1] ?? "";
|
||||||
|
$col = $col[0];
|
||||||
|
if ($order === "desc") {
|
||||||
|
$order = " desc";
|
||||||
|
} elseif ($order === "asc" || $order === "") {
|
||||||
|
$order = "";
|
||||||
|
} else {
|
||||||
|
// column direction spec is bogus
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isset($colDefs[$col])) {
|
||||||
|
// column name spec is bogus
|
||||||
|
continue;
|
||||||
|
} elseif (in_array($col, $fields)) {
|
||||||
|
// if the sort column is also an output column, use it as-is
|
||||||
|
$q->setOrder($col.$order);
|
||||||
|
} else {
|
||||||
|
// otherwise if the column name is valid, use its expression
|
||||||
|
$q->setOrder($colDefs[$col].$order);
|
||||||
|
}
|
||||||
|
}
|
||||||
// perform the query and return results
|
// perform the query and return results
|
||||||
return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());
|
return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ use JKingWeb\Arsse\Database;
|
||||||
use JKingWeb\Arsse\User;
|
use JKingWeb\Arsse\User;
|
||||||
use JKingWeb\Arsse\Service;
|
use JKingWeb\Arsse\Service;
|
||||||
use JKingWeb\Arsse\Context\Context;
|
use JKingWeb\Arsse\Context\Context;
|
||||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
use JKingWeb\Arsse\Misc\ValueInfo as V;
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
use JKingWeb\Arsse\AbstractException;
|
use JKingWeb\Arsse\AbstractException;
|
||||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||||
|
@ -21,26 +21,54 @@ use JKingWeb\Arsse\REST\Exception405;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Zend\Diactoros\Response\JsonResponse;
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
|
use Zend\Diactoros\Response\XmlResponse;
|
||||||
use Zend\Diactoros\Response\EmptyResponse;
|
use Zend\Diactoros\Response\EmptyResponse;
|
||||||
|
|
||||||
class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
const LEVEL = 3;
|
const LEVEL = 3;
|
||||||
|
const GENERIC_ICON_TYPE = "image/png;base64";
|
||||||
|
const GENERIC_ICON_DATA = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAADUlEQVQYV2NgYGBgAAAABQABijPjAAAAAABJRU5ErkJggg==";
|
||||||
|
|
||||||
|
// GET parameters for which we only check presence: these will be converted to booleans
|
||||||
|
const PARAM_BOOL = ["groups", "feeds", "items", "favicons", "links", "unread_item_ids", "saved_item_ids"];
|
||||||
|
// GET parameters which contain meaningful values
|
||||||
|
const PARAM_GET = [
|
||||||
|
'api' => V::T_STRING, // this parameter requires special handling
|
||||||
|
'page' => V::T_INT, // parameter for hot links
|
||||||
|
'range' => V::T_INT, // parameter for hot links
|
||||||
|
'offset' => V::T_INT, // parameter for hot links
|
||||||
|
'since_id' => V::T_INT,
|
||||||
|
'max_id' => V::T_INT,
|
||||||
|
'with_ids' => V::T_STRING,
|
||||||
|
'group_ids' => V::T_STRING, // undocumented parameter for 'items' lookup
|
||||||
|
'feed_ids' => V::T_STRING, // undocumented parameter for 'items' lookup
|
||||||
|
];
|
||||||
|
// POST parameters, all of which contain meaningful values
|
||||||
|
const PARAM_POST = [
|
||||||
|
'api_key' => V::T_STRING,
|
||||||
|
'mark' => V::T_STRING,
|
||||||
|
'as' => V::T_STRING,
|
||||||
|
'id' => V::T_INT,
|
||||||
|
'before' => V::T_DATE,
|
||||||
|
'unread_recently_read' => V::T_BOOL,
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function dispatch(ServerRequestInterface $req): ResponseInterface {
|
public function dispatch(ServerRequestInterface $req): ResponseInterface {
|
||||||
$inR = $req->getQueryParams() ?? [];
|
$G = $this->normalizeInputGet($req->getQueryParams() ?? []);
|
||||||
$inW = $req->getParsedBody() ?? [];
|
$P = $this->normalizeInputPost($req->getParsedBody() ?? []);
|
||||||
if (!array_key_exists("api", $inR)) {
|
if (!isset($G['api'])) {
|
||||||
// the original would have shown the Fever UI in the absence of the "api" parameter, but we'll return 404
|
// 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 new EmptyResponse(404);
|
||||||
}
|
}
|
||||||
$xml = $inR['api'] === "xml";
|
|
||||||
switch ($req->getMethod()) {
|
switch ($req->getMethod()) {
|
||||||
case "OPTIONS":
|
case "OPTIONS":
|
||||||
// do stuff
|
return new EmptyResponse(204, [
|
||||||
break;
|
'Allow' => "POST",
|
||||||
|
'Accept' => "application/x-www-form-urlencoded",
|
||||||
|
]);
|
||||||
case "POST":
|
case "POST":
|
||||||
if (strlen($req->getHeaderLine("Content-Type")) && $req->getHeaderLine("Content-Type") !== "application/x-www-form-urlencoded") {
|
if (strlen($req->getHeaderLine("Content-Type")) && $req->getHeaderLine("Content-Type") !== "application/x-www-form-urlencoded") {
|
||||||
return new EmptyResponse(415, ['Accept' => "application/x-www-form-urlencoded"]);
|
return new EmptyResponse(415, ['Accept' => "application/x-www-form-urlencoded"]);
|
||||||
|
@ -58,31 +86,87 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
return new EmptyResponse(401);
|
return new EmptyResponse(401);
|
||||||
}
|
}
|
||||||
// produce a full response if authenticated or a basic response otherwise
|
// produce a full response if authenticated or a basic response otherwise
|
||||||
if ($this->logIn(strtolower($inW['api_key'] ?? ""))) {
|
if ($this->logIn(strtolower($P['api_key'] ?? ""))) {
|
||||||
$out = $this->processRequest($this->baseResponse(true), $inR, $inW);
|
$out = $this->processRequest($this->baseResponse(true), $G, $P);
|
||||||
} else {
|
} else {
|
||||||
$out = $this->baseResponse(false);
|
$out = $this->baseResponse(false);
|
||||||
}
|
}
|
||||||
// return the result, possibly formatted as XML
|
// return the result, possibly formatted as XML
|
||||||
return $this->formatResponse($out, $xml);
|
return $this->formatResponse($out, ($G['api'] === "xml"));
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
return new EmptyResponse(405, ['Allow' => "OPTIONS,POST"]);
|
return new EmptyResponse(405, ['Allow' => "OPTIONS,POST"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function normalizeInputGet(array $data): array {
|
||||||
|
$out = [];
|
||||||
|
if (array_key_exists("api", $data)) {
|
||||||
|
// the "api" parameter must be handled specially as it a string, but null has special meaning
|
||||||
|
$data['api'] = $data['api'] ?? "json";
|
||||||
|
}
|
||||||
|
foreach (self::PARAM_BOOL as $p) {
|
||||||
|
// first handle all the boolean parameters
|
||||||
|
$out[$p] = array_key_exists($p, $data);
|
||||||
|
}
|
||||||
|
foreach (self::PARAM_GET as $p => $t) {
|
||||||
|
$out[$p] = V::normalize($data[$p] ?? null, $t | V::M_DROP, "unix");
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizeInputPost(array $data): array {
|
||||||
|
$out = [];
|
||||||
|
foreach (self::PARAM_POST as $p => $t) {
|
||||||
|
$out[$p] = V::normalize($data[$p] ?? null, $t | V::M_DROP, "unix");
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
protected function processRequest(array $out, array $G, array $P): array {
|
protected function processRequest(array $out, array $G, array $P): array {
|
||||||
if (array_key_exists("feeds", $G) || array_key_exists("groups", $G)) {
|
$listUnread = false;
|
||||||
if (array_key_exists("groups", $G)) {
|
$listSaved = false;
|
||||||
|
if ($P['unread_recently_read']) {
|
||||||
|
$this->setUnread();
|
||||||
|
$listUnread = true;
|
||||||
|
}
|
||||||
|
if ($P['mark'] && $P['as'] && is_int($P['id'])) {
|
||||||
|
// depending on which mark are being made,
|
||||||
|
// either an 'unread_item_ids' or a
|
||||||
|
// 'saved_item_ids' entry will be added later
|
||||||
|
$listSaved = $this->setMarks($P, $listUnread);
|
||||||
|
}
|
||||||
|
if ($G['feeds'] || $G['groups']) {
|
||||||
|
if ($G['groups']) {
|
||||||
$out['groups'] = $this->getGroups();
|
$out['groups'] = $this->getGroups();
|
||||||
}
|
}
|
||||||
if (array_key_exists("feeds", $G)) {
|
if ($G['feeds']) {
|
||||||
$out['feeds'] = $this->getFeeds();
|
$out['feeds'] = $this->getFeeds();
|
||||||
}
|
}
|
||||||
$out['feeds_groups'] = $this->getRelationships();
|
$out['feeds_groups'] = $this->getRelationships();
|
||||||
}
|
}
|
||||||
if (array_key_exists("favicons", $G)) {
|
if ($G['favicons']) {
|
||||||
# deal with favicons
|
// TODO: implement favicons properly
|
||||||
|
// we provide a single blank favicon for now
|
||||||
|
$out['favicons'] = [
|
||||||
|
[
|
||||||
|
'id' => 0,
|
||||||
|
'data' => self::GENERIC_ICON_TYPE.",".self::GENERIC_ICON_DATA,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($G['items']) {
|
||||||
|
$out['items'] = $this->getItems($G);
|
||||||
|
$out['total_items'] = Arsse::$db->articleCount(Arsse::$user->id);
|
||||||
|
}
|
||||||
|
if ($G['links']) {
|
||||||
|
// TODO: implement hot links
|
||||||
|
$out['links'] = [];
|
||||||
|
}
|
||||||
|
if ($G['unread_item_ids'] || $listUnread) {
|
||||||
|
$out['unread_item_ids'] = $this->getItemIds((new Context)->unread(true));
|
||||||
|
}
|
||||||
|
if ($G['saved_item_ids'] || $listSaved) {
|
||||||
|
$out['saved_item_ids'] = $this->getItemIds((new Context)->starred(true));
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
@ -101,12 +185,49 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
|
|
||||||
protected function formatResponse(array $data, bool $xml): ResponseInterface {
|
protected function formatResponse(array $data, bool $xml): ResponseInterface {
|
||||||
if ($xml) {
|
if ($xml) {
|
||||||
throw \Exception("Not implemented yet");
|
$d = new \DOMDocument("1.0", "utf-8");
|
||||||
|
$d->appendChild($this->makeXMLAssoc($data, $d->createElement("response")));
|
||||||
|
return new XmlResponse($d->saveXML());
|
||||||
} else {
|
} else {
|
||||||
return new JsonResponse($data, 200, [], \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
|
return new JsonResponse($data, 200, [], \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function makeXMLAssoc(array $data, \DOMElement $p): \DOMElement {
|
||||||
|
$d = $p->ownerDocument;
|
||||||
|
foreach ($data as $k => $v) {
|
||||||
|
if (!is_array($v)) {
|
||||||
|
$p->appendChild($d->createElement($k, (string) $v));
|
||||||
|
} elseif (isset($v[0])) {
|
||||||
|
// this is a very simplistic check for an indexed array
|
||||||
|
// it would not pass muster in the face of generic data,
|
||||||
|
// but we'll assume our code produces only well-ordered
|
||||||
|
// indexed arrays
|
||||||
|
$p->appendChild($this->makeXMLIndexed($v, $d->createElement($k), substr($k, 0, strlen($k) - 1)));
|
||||||
|
} else {
|
||||||
|
$p->appendChild($this->makeXMLAssoc($v, $d->createElement($k)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $p;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function makeXMLIndexed(array $data, \DOMElement $p, string $k): \DOMElement {
|
||||||
|
$d = $p->ownerDocument;
|
||||||
|
foreach ($data as $v) {
|
||||||
|
if (!is_array($v)) {
|
||||||
|
// this case is never encountered with Fever's output
|
||||||
|
$p->appendChild($d->createElement($k, (string) $v)); // @codeCoverageIgnore
|
||||||
|
} elseif (isset($v[0])) {
|
||||||
|
// this case is never encountered with Fever's output
|
||||||
|
$p->appendChild($this->makeXMLIndexed($v, $d->createElement($k), substr($k, 0, strlen($k) - 1))); // @codeCoverageIgnore
|
||||||
|
} else {
|
||||||
|
$p->appendChild($this->makeXMLAssoc($v, $d->createElement($k)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $p;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
protected function logIn(string $hash): bool {
|
protected function logIn(string $hash): bool {
|
||||||
// if HTTP authentication was successful and sessions are not enforced, proceed unconditionally
|
// if HTTP authentication was successful and sessions are not enforced, proceed unconditionally
|
||||||
if (isset(Arsse::$user->id) && !Arsse::$conf->userSessionEnforced) {
|
if (isset(Arsse::$user->id) && !Arsse::$conf->userSessionEnforced) {
|
||||||
|
@ -123,6 +244,80 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function setMarks(array $P, &$listUnread): bool {
|
||||||
|
$listSaved = false;
|
||||||
|
$c = new Context;
|
||||||
|
$id = $P['id'];
|
||||||
|
if ($P['before']) {
|
||||||
|
$c->notMarkedSince($P['before']);
|
||||||
|
}
|
||||||
|
switch ($P['mark']) {
|
||||||
|
case "item":
|
||||||
|
$c->article($id);
|
||||||
|
break;
|
||||||
|
case "group":
|
||||||
|
if ($id > 0) {
|
||||||
|
// concrete groups
|
||||||
|
$c->tag($id);
|
||||||
|
} elseif ($id < 0) {
|
||||||
|
// group negative-one is the "Sparks" supergroup i.e. no feeds
|
||||||
|
$c->not->folder(0);
|
||||||
|
} else {
|
||||||
|
// group zero is the "Kindling" supergroup i.e. all feeds
|
||||||
|
// nothing need to be done for this
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "feed":
|
||||||
|
$c->subscription($id);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return $listSaved;
|
||||||
|
}
|
||||||
|
switch ($P['as']) {
|
||||||
|
case "read":
|
||||||
|
$data = ['read' => true];
|
||||||
|
$listUnread = true;
|
||||||
|
break;
|
||||||
|
case "unread":
|
||||||
|
// this option is undocumented, but valid
|
||||||
|
$data = ['read' => false];
|
||||||
|
$listUnread = true;
|
||||||
|
break;
|
||||||
|
case "saved":
|
||||||
|
$data = ['starred' => true];
|
||||||
|
$listSaved = true;
|
||||||
|
break;
|
||||||
|
case "unsaved":
|
||||||
|
$data = ['starred' => false];
|
||||||
|
$listSaved = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return $listSaved;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Arsse::$db->articleMark(Arsse::$user->id, $data, $c);
|
||||||
|
} catch (ExceptionInput $e) {
|
||||||
|
// ignore any errors
|
||||||
|
}
|
||||||
|
return $listSaved;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setUnread() {
|
||||||
|
$lastUnread = Arsse::$db->articleList(Arsse::$user->id, (new Context)->limit(1), ["marked_date"], ["marked_date desc"])->getValue();
|
||||||
|
if (!$lastUnread) {
|
||||||
|
// there are no articles
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fever takes the date of the last read article less fifteen seconds as a cut-off.
|
||||||
|
// We take the date of last mark (whether it be read, unread, saved, unsaved), which
|
||||||
|
// may not actually signify a mark, but we'll otherwise also count back fifteen seconds
|
||||||
|
$c = new Context;
|
||||||
|
$lastUnread = Date::normalize($lastUnread, "sql");
|
||||||
|
$since = Date::sub("PT15S", $lastUnread);
|
||||||
|
$c->unread(false)->markedSince($since);
|
||||||
|
Arsse::$db->articleMark(Arsse::$user->id, ['read' => false], $c);
|
||||||
|
}
|
||||||
|
|
||||||
protected function getRefreshTime() {
|
protected function getRefreshTime() {
|
||||||
return Date::transform(Arsse::$db->subscriptionRefreshed(Arsse::$user->id), "unix");
|
return Date::transform(Arsse::$db->subscriptionRefreshed(Arsse::$user->id), "unix");
|
||||||
}
|
}
|
||||||
|
@ -132,7 +327,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
foreach (arsse::$db->subscriptionList(Arsse::$user->id) as $sub) {
|
foreach (arsse::$db->subscriptionList(Arsse::$user->id) as $sub) {
|
||||||
$out[] = [
|
$out[] = [
|
||||||
'id' => (int) $sub['id'],
|
'id' => (int) $sub['id'],
|
||||||
'favicon_id' => (int) ($sub['favicon'] ? $sub['feed'] : 0),
|
'favicon_id' => 0, // TODO: implement favicons
|
||||||
'title' => (string) $sub['title'],
|
'title' => (string) $sub['title'],
|
||||||
'url' => $sub['url'],
|
'url' => $sub['url'],
|
||||||
'site_url' => $sub['source'],
|
'site_url' => $sub['source'],
|
||||||
|
@ -171,4 +366,50 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getItems(array $G): array {
|
||||||
|
$c = (new Context)->limit(50);
|
||||||
|
$reverse = false;
|
||||||
|
// handle the standard options
|
||||||
|
if ($G['with_ids']) {
|
||||||
|
$c->articles(explode(",", $G['with_ids']));
|
||||||
|
} elseif ($G['max_id']) {
|
||||||
|
$c->latestArticle($G['max_id'] - 1);
|
||||||
|
$reverse = true;
|
||||||
|
} elseif ($G['since_id']) {
|
||||||
|
$c->oldestArticle($G['since_id'] + 1);
|
||||||
|
}
|
||||||
|
// handle the undocumented options
|
||||||
|
if ($G['group_ids']) {
|
||||||
|
$c->tags(explode(",", $G['group_ids']));
|
||||||
|
}
|
||||||
|
if ($G['feed_ids']) {
|
||||||
|
$c->subscriptions(explode(",", $G['feed_ids']));
|
||||||
|
}
|
||||||
|
// get results
|
||||||
|
$out = [];
|
||||||
|
$order = $reverse ? "id desc" : "id";
|
||||||
|
foreach (Arsse::$db->articleList(Arsse::$user->id, $c, ["id", "subscription", "title", "author", "content", "url", "starred", "unread", "published_date"], [$order]) as $r) {
|
||||||
|
$out[] = [
|
||||||
|
'id' => (int) $r['id'],
|
||||||
|
'feed_id' => (int) $r['subscription'],
|
||||||
|
'title' => (string) $r['title'],
|
||||||
|
'author' => (string) $r['author'],
|
||||||
|
'html' => (string) $r['content'],
|
||||||
|
'url' => (string) $r['url'],
|
||||||
|
'is_saved' => (int) $r['starred'],
|
||||||
|
'is_read' => (int) !$r['unread'],
|
||||||
|
'created_on_time' => Date::transform($r['published_date'], "unix", "sql"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getItemIds(Context $c = null): string {
|
||||||
|
$out = [];
|
||||||
|
foreach (Arsse::$db->articleList(Arsse::$user->id, $c) as $r) {
|
||||||
|
$out[] = (int) $r['id'];
|
||||||
|
}
|
||||||
|
return implode(",", $out);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -521,14 +521,10 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
$c->limit($data['batchSize']);
|
$c->limit($data['batchSize']);
|
||||||
}
|
}
|
||||||
// set the order of returned items
|
// set the order of returned items
|
||||||
if ($data['oldestFirst']) {
|
$reverse = !$data['oldestFirst'];
|
||||||
$c->reverse(false);
|
|
||||||
} else {
|
|
||||||
$c->reverse(true);
|
|
||||||
}
|
|
||||||
// set the edition mark-off; the database uses an or-equal comparison for internal consistency, but the protocol does not, so we must adjust by one
|
// set the edition mark-off; the database uses an or-equal comparison for internal consistency, but the protocol does not, so we must adjust by one
|
||||||
if ($data['offset'] > 0) {
|
if ($data['offset'] > 0) {
|
||||||
if ($c->reverse) {
|
if ($reverse) {
|
||||||
$c->latestEdition($data['offset'] - 1);
|
$c->latestEdition($data['offset'] - 1);
|
||||||
} else {
|
} else {
|
||||||
$c->oldestEdition($data['offset'] + 1);
|
$c->oldestEdition($data['offset'] + 1);
|
||||||
|
@ -579,7 +575,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
"starred",
|
"starred",
|
||||||
"modified_date",
|
"modified_date",
|
||||||
"fingerprint",
|
"fingerprint",
|
||||||
]);
|
], [$reverse ? "edition desc" : "edition"]);
|
||||||
} catch (ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
// ID of subscription or folder is not valid
|
// ID of subscription or folder is not valid
|
||||||
return new EmptyResponse(422);
|
return new EmptyResponse(422);
|
||||||
|
|
|
@ -8,11 +8,10 @@ namespace JKingWeb\Arsse\REST\TinyTinyRSS;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Feed;
|
use JKingWeb\Arsse\Feed;
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
use JKingWeb\Arsse\Database;
|
|
||||||
use JKingWeb\Arsse\User;
|
|
||||||
use JKingWeb\Arsse\Service;
|
use JKingWeb\Arsse\Service;
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
use JKingWeb\Arsse\Database;
|
||||||
use JKingWeb\Arsse\Context\Context;
|
use JKingWeb\Arsse\Context\Context;
|
||||||
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
use JKingWeb\Arsse\Misc\ValueInfo;
|
||||||
use JKingWeb\Arsse\AbstractException;
|
use JKingWeb\Arsse\AbstractException;
|
||||||
use JKingWeb\Arsse\ExceptionType;
|
use JKingWeb\Arsse\ExceptionType;
|
||||||
|
@ -1439,7 +1438,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// no context needed here
|
// no context needed here
|
||||||
break;
|
break;
|
||||||
case self::FEED_READ:
|
case self::FEED_READ:
|
||||||
$c->markedSince(Date::sub("PT24H"))->unread(false); // FIXME: this selects any recently touched article which is read, not necessarily a recently read one
|
$c->markedSince(Date::sub("PT24H"))->unread(false); // FIXME: this selects any recently touched (read, starred, annotated) article which is read, not necessarily a recently read one
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// any actual feed
|
// any actual feed
|
||||||
|
@ -1492,15 +1491,15 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
switch ($data['order_by']) {
|
switch ($data['order_by']) {
|
||||||
case "date_reverse":
|
case "date_reverse":
|
||||||
// sort oldest first
|
// sort oldest first
|
||||||
$c->reverse(false);
|
$order = ["edited_date"];
|
||||||
break;
|
break;
|
||||||
case "feed_dates":
|
case "feed_dates":
|
||||||
// sort newest first
|
// sort newest first
|
||||||
$c->reverse(true);
|
$order = ["edited_date desc"];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// in TT-RSS the default sort order is unusual for some of the special feeds; we do not implement this
|
// sort most recently marked for special feeds, newest first otherwise
|
||||||
$c->reverse(true);
|
$order = (!$cat && ($id == self::FEED_READ || $id == self::FEED_STARRED)) ? ["marked_date desc"] : ["edited_date desc"];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// set the limit and offset
|
// set the limit and offset
|
||||||
|
@ -1515,6 +1514,6 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
$c->oldestArticle($data['since_id'] + 1);
|
$c->oldestArticle($data['since_id'] + 1);
|
||||||
}
|
}
|
||||||
// return results
|
// return results
|
||||||
return Arsse::$db->articleList(Arsse::$user->id, $c, $fields);
|
return Arsse::$db->articleList(Arsse::$user->id, $c, $fields, $order);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
robo
4
robo
|
@ -5,7 +5,7 @@ shift
|
||||||
|
|
||||||
ulimit -n 2048
|
ulimit -n 2048
|
||||||
if [ "$1" = "clean" ]; then
|
if [ "$1" = "clean" ]; then
|
||||||
"$base/vendor/bin/robo" "$roboCommand" $*
|
"$base/vendor/bin/robo" "$roboCommand" "$@"
|
||||||
else
|
else
|
||||||
"$base/vendor/bin/robo" "$roboCommand" -- $*
|
"$base/vendor/bin/robo" "$roboCommand" -- "$@"
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -10,6 +10,7 @@ use JKingWeb\Arsse\Database;
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
use JKingWeb\Arsse\Context\Context;
|
use JKingWeb\Arsse\Context\Context;
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
|
use JKingWeb\Arsse\Misc\ValueInfo;
|
||||||
use Phake;
|
use Phake;
|
||||||
|
|
||||||
trait SeriesArticle {
|
trait SeriesArticle {
|
||||||
|
@ -424,10 +425,15 @@ trait SeriesArticle {
|
||||||
return [
|
return [
|
||||||
'Blank context' => [new Context, [1,2,3,4,5,6,7,8,19,20]],
|
'Blank context' => [new Context, [1,2,3,4,5,6,7,8,19,20]],
|
||||||
'Folder tree' => [(new Context)->folder(1), [5,6,7,8]],
|
'Folder tree' => [(new Context)->folder(1), [5,6,7,8]],
|
||||||
|
'Entire folder tree' => [(new Context)->folder(0), [1,2,3,4,5,6,7,8,19,20]],
|
||||||
'Leaf folder' => [(new Context)->folder(6), [7,8]],
|
'Leaf folder' => [(new Context)->folder(6), [7,8]],
|
||||||
'Root folder only' => [(new Context)->folderShallow(0), [1,2,3,4]],
|
'Multiple folder trees' => [(new Context)->folders([1,5]), [5,6,7,8,19,20]],
|
||||||
|
'Multiple folder trees including root' => [(new Context)->folders([0,1,5]), [1,2,3,4,5,6,7,8,19,20]],
|
||||||
'Shallow folder' => [(new Context)->folderShallow(1), [5,6]],
|
'Shallow folder' => [(new Context)->folderShallow(1), [5,6]],
|
||||||
|
'Root folder only' => [(new Context)->folderShallow(0), [1,2,3,4]],
|
||||||
|
'Multiple shallow folders' => [(new Context)->foldersShallow([1,6]), [5,6,7,8]],
|
||||||
'Subscription' => [(new Context)->subscription(5), [19,20]],
|
'Subscription' => [(new Context)->subscription(5), [19,20]],
|
||||||
|
'Multiple subscriptions' => [(new Context)->subscriptions([4,5]), [7,8,19,20]],
|
||||||
'Unread' => [(new Context)->subscription(5)->unread(true), [20]],
|
'Unread' => [(new Context)->subscription(5)->unread(true), [20]],
|
||||||
'Read' => [(new Context)->subscription(5)->unread(false), [19]],
|
'Read' => [(new Context)->subscription(5)->unread(false), [19]],
|
||||||
'Starred' => [(new Context)->starred(true), [1,20]],
|
'Starred' => [(new Context)->starred(true), [1,20]],
|
||||||
|
@ -455,11 +461,12 @@ trait SeriesArticle {
|
||||||
'Marked or labelled between 2000 and 2015' => [(new Context)->markedSince("2000-01-01T00:00:00Z")->notMarkedSince("2015-12-31T23:59:59Z"), [1,2,3,4,5,6,7,8,20]],
|
'Marked or labelled between 2000 and 2015' => [(new Context)->markedSince("2000-01-01T00:00:00Z")->notMarkedSince("2015-12-31T23:59:59Z"), [1,2,3,4,5,6,7,8,20]],
|
||||||
'Marked or labelled in 2010' => [(new Context)->markedSince("2010-01-01T00:00:00Z")->notMarkedSince("2010-12-31T23:59:59Z"), [2,4,6,20]],
|
'Marked or labelled in 2010' => [(new Context)->markedSince("2010-01-01T00:00:00Z")->notMarkedSince("2010-12-31T23:59:59Z"), [2,4,6,20]],
|
||||||
'Paged results' => [(new Context)->limit(2)->oldestEdition(4), [4,5]],
|
'Paged results' => [(new Context)->limit(2)->oldestEdition(4), [4,5]],
|
||||||
'Reversed paged results' => [(new Context)->limit(2)->latestEdition(7)->reverse(true), [7,6]],
|
|
||||||
'With label ID 1' => [(new Context)->label(1), [1,19]],
|
'With label ID 1' => [(new Context)->label(1), [1,19]],
|
||||||
'With label ID 2' => [(new Context)->label(2), [1,5,20]],
|
'With label ID 2' => [(new Context)->label(2), [1,5,20]],
|
||||||
|
'With label ID 1 or 2' => [(new Context)->labels([1,2]), [1,5,19,20]],
|
||||||
'With label "Interesting"' => [(new Context)->labelName("Interesting"), [1,19]],
|
'With label "Interesting"' => [(new Context)->labelName("Interesting"), [1,19]],
|
||||||
'With label "Fascinating"' => [(new Context)->labelName("Fascinating"), [1,5,20]],
|
'With label "Fascinating"' => [(new Context)->labelName("Fascinating"), [1,5,20]],
|
||||||
|
'With label "Interesting" or "Fascinating"' => [(new Context)->labelNames(["Interesting","Fascinating"]), [1,5,19,20]],
|
||||||
'Article ID 20' => [(new Context)->article(20), [20]],
|
'Article ID 20' => [(new Context)->article(20), [20]],
|
||||||
'Edition ID 1001' => [(new Context)->edition(1001), [20]],
|
'Edition ID 1001' => [(new Context)->edition(1001), [20]],
|
||||||
'Multiple articles' => [(new Context)->articles([1,20,50]), [1,20]],
|
'Multiple articles' => [(new Context)->articles([1,20,50]), [1,20]],
|
||||||
|
@ -494,12 +501,19 @@ trait SeriesArticle {
|
||||||
'Search 501 terms' => [(new Context)->searchTerms(array_merge(range(1, 500), [str_repeat("a", 1000)])), []],
|
'Search 501 terms' => [(new Context)->searchTerms(array_merge(range(1, 500), [str_repeat("a", 1000)])), []],
|
||||||
'With tag ID 1' => [(new Context)->tag(1), [5,6,7,8]],
|
'With tag ID 1' => [(new Context)->tag(1), [5,6,7,8]],
|
||||||
'With tag ID 5' => [(new Context)->tag(5), [7,8,19,20]],
|
'With tag ID 5' => [(new Context)->tag(5), [7,8,19,20]],
|
||||||
|
'With tag ID 1 or 5' => [(new Context)->tags([1,5]), [5,6,7,8,19,20]],
|
||||||
'With tag "Technology"' => [(new Context)->tagName("Technology"), [5,6,7,8]],
|
'With tag "Technology"' => [(new Context)->tagName("Technology"), [5,6,7,8]],
|
||||||
'With tag "Politics"' => [(new Context)->tagName("Politics"), [7,8,19,20]],
|
'With tag "Politics"' => [(new Context)->tagName("Politics"), [7,8,19,20]],
|
||||||
|
'With tag "Technology" or "Politics"' => [(new Context)->tagNames(["Technology","Politics"]), [5,6,7,8,19,20]],
|
||||||
'Excluding tag ID 1' => [(new Context)->not->tag(1), [1,2,3,4,19,20]],
|
'Excluding tag ID 1' => [(new Context)->not->tag(1), [1,2,3,4,19,20]],
|
||||||
'Excluding tag ID 5' => [(new Context)->not->tag(5), [1,2,3,4,5,6]],
|
'Excluding tag ID 5' => [(new Context)->not->tag(5), [1,2,3,4,5,6]],
|
||||||
'Excluding tag "Technology"' => [(new Context)->not->tagName("Technology"), [1,2,3,4,19,20]],
|
'Excluding tag "Technology"' => [(new Context)->not->tagName("Technology"), [1,2,3,4,19,20]],
|
||||||
'Excluding tag "Politics"' => [(new Context)->not->tagName("Politics"), [1,2,3,4,5,6]],
|
'Excluding tag "Politics"' => [(new Context)->not->tagName("Politics"), [1,2,3,4,5,6]],
|
||||||
|
'Excluding tags ID 1 and 5' => [(new Context)->not->tags([1,5]), [1,2,3,4]],
|
||||||
|
'Excluding tags "Technology" and "Politics"' => [(new Context)->not->tagNames(["Technology","Politics"]), [1,2,3,4]],
|
||||||
|
'Excluding entire folder tree' => [(new Context)->not->folder(0), []],
|
||||||
|
'Excluding multiple folder trees' => [(new Context)->not->folders([1,5]), [1,2,3,4]],
|
||||||
|
'Excluding multiple folder trees including root' => [(new Context)->not->folders([0,1,5]), []],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -563,6 +577,25 @@ trait SeriesArticle {
|
||||||
$this->assertEquals($this->fields, $test);
|
$this->assertEquals($this->fields, $test);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @dataProvider provideOrderedLists */
|
||||||
|
public function testListArticlesCheckingOrder(array $sortCols, array $exp) {
|
||||||
|
$act = ValueInfo::normalize(array_column(iterator_to_array(Arsse::$db->articleList("john.doe@example.com", null, ["id"], $sortCols)), "id"), ValueInfo::T_INT | ValueInfo::M_ARRAY);
|
||||||
|
$this->assertSame($exp, $act);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideOrderedLists() {
|
||||||
|
return [
|
||||||
|
[["id"], [1,2,3,4,5,6,7,8,19,20]],
|
||||||
|
[["id asc"], [1,2,3,4,5,6,7,8,19,20]],
|
||||||
|
[["id desc"], [20,19,8,7,6,5,4,3,2,1]],
|
||||||
|
[["edition"], [1,2,3,4,5,6,7,8,19,20]],
|
||||||
|
[["edition asc"], [1,2,3,4,5,6,7,8,19,20]],
|
||||||
|
[["edition desc"], [20,19,8,7,6,5,4,3,2,1]],
|
||||||
|
[["id", "edition desk"], [1,2,3,4,5,6,7,8,19,20]],
|
||||||
|
[["id", "editio"], [1,2,3,4,5,6,7,8,19,20]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function testListArticlesWithoutAuthority() {
|
public function testListArticlesWithoutAuthority() {
|
||||||
Phake::when(Arsse::$user)->authorize->thenReturn(false);
|
Phake::when(Arsse::$user)->authorize->thenReturn(false);
|
||||||
$this->assertException("notAuthorized", "User", "ExceptionAuthz");
|
$this->assertException("notAuthorized", "User", "ExceptionAuthz");
|
||||||
|
@ -783,11 +816,6 @@ trait SeriesArticle {
|
||||||
$this->compareExpectations(static::$drv, $state);
|
$this->compareExpectations(static::$drv, $state);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMarkTooFewMultipleArticles() {
|
|
||||||
$this->assertException("tooShort", "Db", "ExceptionInput");
|
|
||||||
Arsse::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->articles([]));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testMarkTooManyMultipleArticles() {
|
public function testMarkTooManyMultipleArticles() {
|
||||||
$this->assertSame(7, Arsse::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->articles(range(1, Database::LIMIT_SET_SIZE * 3))));
|
$this->assertSame(7, Arsse::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->articles(range(1, Database::LIMIT_SET_SIZE * 3))));
|
||||||
}
|
}
|
||||||
|
@ -854,11 +882,6 @@ trait SeriesArticle {
|
||||||
$this->compareExpectations(static::$drv, $state);
|
$this->compareExpectations(static::$drv, $state);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMarkTooFewMultipleEditions() {
|
|
||||||
$this->assertException("tooShort", "Db", "ExceptionInput");
|
|
||||||
Arsse::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->editions([]));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testMarkTooManyMultipleEditions() {
|
public function testMarkTooManyMultipleEditions() {
|
||||||
$this->assertSame(7, Arsse::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->editions(range(1, 51))));
|
$this->assertSame(7, Arsse::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->editions(range(1, 51))));
|
||||||
}
|
}
|
||||||
|
@ -1030,13 +1053,20 @@ trait SeriesArticle {
|
||||||
Arsse::$db->articleCategoriesGet($this->user, 19);
|
Arsse::$db->articleCategoriesGet($this->user, 19);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSearchTooFewTerms() {
|
/** @dataProvider provideArrayContextOptions */
|
||||||
|
public function testUseTooFewValuesInArrayContext(string $option) {
|
||||||
$this->assertException("tooShort", "Db", "ExceptionInput");
|
$this->assertException("tooShort", "Db", "ExceptionInput");
|
||||||
Arsse::$db->articleList($this->user, (new Context)->searchTerms([]));
|
Arsse::$db->articleList($this->user, (new Context)->$option([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSearchTooFewTermsInNote() {
|
public function provideArrayContextOptions() {
|
||||||
$this->assertException("tooShort", "Db", "ExceptionInput");
|
foreach ([
|
||||||
Arsse::$db->articleList($this->user, (new Context)->annotationTerms([]));
|
"articles", "editions",
|
||||||
|
"subscriptions", "foldersShallow", //"folders",
|
||||||
|
"tags", "tagNames", "labels", "labelNames",
|
||||||
|
"searchTerms", "authorTerms", "annotationTerms",
|
||||||
|
] as $method) {
|
||||||
|
yield [$method];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,10 +29,15 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
'limit' => 10,
|
'limit' => 10,
|
||||||
'offset' => 5,
|
'offset' => 5,
|
||||||
'folder' => 42,
|
'folder' => 42,
|
||||||
|
'folders' => [12,22],
|
||||||
'folderShallow' => 42,
|
'folderShallow' => 42,
|
||||||
|
'foldersShallow' => [0,1],
|
||||||
'tag' => 44,
|
'tag' => 44,
|
||||||
|
'tags' => [44, 2112],
|
||||||
'tagName' => "XLIV",
|
'tagName' => "XLIV",
|
||||||
|
'tagNames' => ["XLIV", "MMCXII"],
|
||||||
'subscription' => 2112,
|
'subscription' => 2112,
|
||||||
|
'subscriptions' => [44, 2112],
|
||||||
'article' => 255,
|
'article' => 255,
|
||||||
'edition' => 65535,
|
'edition' => 65535,
|
||||||
'latestArticle' => 47,
|
'latestArticle' => 47,
|
||||||
|
@ -48,7 +53,9 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
'editions' => [1,2],
|
'editions' => [1,2],
|
||||||
'articles' => [1,2],
|
'articles' => [1,2],
|
||||||
'label' => 2112,
|
'label' => 2112,
|
||||||
|
'labels' => [2112, 1984],
|
||||||
'labelName' => "Rush",
|
'labelName' => "Rush",
|
||||||
|
'labelNames' => ["Rush", "Orwell"],
|
||||||
'labelled' => true,
|
'labelled' => true,
|
||||||
'annotated' => true,
|
'annotated' => true,
|
||||||
'searchTerms' => ["foo", "bar"],
|
'searchTerms' => ["foo", "bar"],
|
||||||
|
@ -79,9 +86,19 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCleanIdArrayValues() {
|
public function testCleanIdArrayValues() {
|
||||||
$methods = ["articles", "editions"];
|
$methods = ["articles", "editions", "tags", "labels", "subscriptions"];
|
||||||
$in = [1, "2", 3.5, 3.0, "ook", 0, -20, true, false, null, new \DateTime(), -1.0];
|
$in = [1, "2", 3.5, 4.0, 4, "ook", 0, -20, true, false, null, new \DateTime(), -1.0];
|
||||||
$out = [1,2, 3];
|
$out = [1, 2, 4];
|
||||||
|
$c = new Context;
|
||||||
|
foreach ($methods as $method) {
|
||||||
|
$this->assertSame($out, $c->$method($in)->$method, "Context method $method did not return the expected results");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCleanFolderIdArrayValues() {
|
||||||
|
$methods = ["folders", "foldersShallow"];
|
||||||
|
$in = [1, "2", 3.5, 4.0, 4, "ook", 0, -20, true, false, null, new \DateTime(), -1.0];
|
||||||
|
$out = [1, 2, 4, 0];
|
||||||
$c = new Context;
|
$c = new Context;
|
||||||
foreach ($methods as $method) {
|
foreach ($methods as $method) {
|
||||||
$this->assertSame($out, $c->$method($in)->$method, "Context method $method did not return the expected results");
|
$this->assertSame($out, $c->$method($in)->$method, "Context method $method did not return the expected results");
|
||||||
|
@ -89,7 +106,7 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCleanStringArrayValues() {
|
public function testCleanStringArrayValues() {
|
||||||
$methods = ["searchTerms", "annotationTerms", "titleTerms", "authorTerms"];
|
$methods = ["searchTerms", "annotationTerms", "titleTerms", "authorTerms", "tagNames", "labelNames"];
|
||||||
$now = new \DateTime;
|
$now = new \DateTime;
|
||||||
$in = [1, 3.0, "ook", 0, true, false, null, $now, ""];
|
$in = [1, 3.0, "ook", 0, true, false, null, $now, ""];
|
||||||
$out = ["1", "3", "ook", "0", valueInfo::normalize($now, ValueInfo::T_STRING)];
|
$out = ["1", "3", "ook", "0", valueInfo::normalize($now, ValueInfo::T_STRING)];
|
||||||
|
|
|
@ -7,37 +7,153 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\TestCase\REST\Fever;
|
namespace JKingWeb\Arsse\TestCase\REST\Fever;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
use JKingWeb\Arsse\Conf;
|
|
||||||
use JKingWeb\Arsse\User;
|
use JKingWeb\Arsse\User;
|
||||||
use JKingWeb\Arsse\Database;
|
use JKingWeb\Arsse\Database;
|
||||||
use JKingWeb\Arsse\Service;
|
|
||||||
use JKingWeb\Arsse\REST\Request;
|
|
||||||
use JKingWeb\Arsse\Test\Result;
|
use JKingWeb\Arsse\Test\Result;
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
|
||||||
use JKingWeb\Arsse\Context\Context;
|
use JKingWeb\Arsse\Context\Context;
|
||||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||||
use JKingWeb\Arsse\User\Exception as UserException;
|
|
||||||
use JKingWeb\Arsse\Db\Transaction;
|
use JKingWeb\Arsse\Db\Transaction;
|
||||||
use JKingWeb\Arsse\REST\Fever\API;
|
use JKingWeb\Arsse\REST\Fever\API;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Zend\Diactoros\ServerRequest;
|
use Zend\Diactoros\ServerRequest;
|
||||||
use Zend\Diactoros\Response\JsonResponse;
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
|
use Zend\Diactoros\Response\XmlResponse;
|
||||||
use Zend\Diactoros\Response\EmptyResponse;
|
use Zend\Diactoros\Response\EmptyResponse;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\REST\Fever\API<extended> */
|
/** @covers \JKingWeb\Arsse\REST\Fever\API<extended> */
|
||||||
class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
|
/** @var \JKingWeb\Arsse\REST\Fever\API */
|
||||||
|
protected $h;
|
||||||
|
|
||||||
|
protected $articles = [
|
||||||
|
'db' => [
|
||||||
|
[
|
||||||
|
'id' => 101,
|
||||||
|
'url' => 'http://example.com/1',
|
||||||
|
'title' => 'Article title 1',
|
||||||
|
'author' => '',
|
||||||
|
'content' => '<p>Article content 1</p>',
|
||||||
|
'published_date' => '2000-01-01 00:00:00',
|
||||||
|
'unread' => 1,
|
||||||
|
'starred' => 0,
|
||||||
|
'subscription' => 8,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 102,
|
||||||
|
'url' => 'http://example.com/2',
|
||||||
|
'title' => 'Article title 2',
|
||||||
|
'author' => '',
|
||||||
|
'content' => '<p>Article content 2</p>',
|
||||||
|
'published_date' => '2000-01-02 00:00:00',
|
||||||
|
'unread' => 0,
|
||||||
|
'starred' => 0,
|
||||||
|
'subscription' => 8,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 103,
|
||||||
|
'url' => 'http://example.com/3',
|
||||||
|
'title' => 'Article title 3',
|
||||||
|
'author' => '',
|
||||||
|
'content' => '<p>Article content 3</p>',
|
||||||
|
'published_date' => '2000-01-03 00:00:00',
|
||||||
|
'unread' => 1,
|
||||||
|
'starred' => 1,
|
||||||
|
'subscription' => 9,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 104,
|
||||||
|
'url' => 'http://example.com/4',
|
||||||
|
'title' => 'Article title 4',
|
||||||
|
'author' => '',
|
||||||
|
'content' => '<p>Article content 4</p>',
|
||||||
|
'published_date' => '2000-01-04 00:00:00',
|
||||||
|
'unread' => 0,
|
||||||
|
'starred' => 1,
|
||||||
|
'subscription' => 9,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 105,
|
||||||
|
'url' => 'http://example.com/5',
|
||||||
|
'title' => 'Article title 5',
|
||||||
|
'author' => '',
|
||||||
|
'content' => '<p>Article content 5</p>',
|
||||||
|
'published_date' => '2000-01-05 00:00:00',
|
||||||
|
'unread' => 1,
|
||||||
|
'starred' => 0,
|
||||||
|
'subscription' => 10,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'rest' => [
|
||||||
|
[
|
||||||
|
'id' => 101,
|
||||||
|
'feed_id' => 8,
|
||||||
|
'title' => 'Article title 1',
|
||||||
|
'author' => '',
|
||||||
|
'html' => '<p>Article content 1</p>',
|
||||||
|
'url' => 'http://example.com/1',
|
||||||
|
'is_saved' => 0,
|
||||||
|
'is_read' => 0,
|
||||||
|
'created_on_time' => 946684800,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 102,
|
||||||
|
'feed_id' => 8,
|
||||||
|
'title' => 'Article title 2',
|
||||||
|
'author' => '',
|
||||||
|
'html' => '<p>Article content 2</p>',
|
||||||
|
'url' => 'http://example.com/2',
|
||||||
|
'is_saved' => 0,
|
||||||
|
'is_read' => 1,
|
||||||
|
'created_on_time' => 946771200,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 103,
|
||||||
|
'feed_id' => 9,
|
||||||
|
'title' => 'Article title 3',
|
||||||
|
'author' => '',
|
||||||
|
'html' => '<p>Article content 3</p>',
|
||||||
|
'url' => 'http://example.com/3',
|
||||||
|
'is_saved' => 1,
|
||||||
|
'is_read' => 0,
|
||||||
|
'created_on_time' => 946857600,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 104,
|
||||||
|
'feed_id' => 9,
|
||||||
|
'title' => 'Article title 4',
|
||||||
|
'author' => '',
|
||||||
|
'html' => '<p>Article content 4</p>',
|
||||||
|
'url' => 'http://example.com/4',
|
||||||
|
'is_saved' => 1,
|
||||||
|
'is_read' => 1,
|
||||||
|
'created_on_time' => 946944000,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 105,
|
||||||
|
'feed_id' => 10,
|
||||||
|
'title' => 'Article title 5',
|
||||||
|
'author' => '',
|
||||||
|
'html' => '<p>Article content 5</p>',
|
||||||
|
'url' => 'http://example.com/5',
|
||||||
|
'is_saved' => 0,
|
||||||
|
'is_read' => 0,
|
||||||
|
'created_on_time' => 947030400,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
protected function v($value) {
|
protected function v($value) {
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function req($dataGet, $dataPost = "", string $method = "POST", string $type = null, string $url = "", string $user = null): ResponseInterface {
|
protected function req($dataGet, $dataPost = "", string $method = "POST", string $type = null, string $url = "", string $user = null): ServerRequest {
|
||||||
$url = "/fever/".$url;
|
$url = "/fever/".$url;
|
||||||
|
$type = $type ?? "application/x-www-form-urlencoded";
|
||||||
$server = [
|
$server = [
|
||||||
'REQUEST_METHOD' => $method,
|
'REQUEST_METHOD' => $method,
|
||||||
'REQUEST_URI' => $url,
|
'REQUEST_URI' => $url,
|
||||||
'HTTP_CONTENT_TYPE' => $type ?? "application/x-www-form-urlencoded",
|
'HTTP_CONTENT_TYPE' => $type,
|
||||||
];
|
];
|
||||||
$req = new ServerRequest($server, [], $url, $method, "php://memory");
|
$req = new ServerRequest($server, [], $url, $method, "php://memory", ['Content-Type' => $type]);
|
||||||
if (!is_array($dataGet)) {
|
if (!is_array($dataGet)) {
|
||||||
parse_str($dataGet, $dataGet);
|
parse_str($dataGet, $dataGet);
|
||||||
}
|
}
|
||||||
|
@ -45,9 +161,8 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
if (is_array($dataPost)) {
|
if (is_array($dataPost)) {
|
||||||
$req = $req->withParsedBody($dataPost);
|
$req = $req->withParsedBody($dataPost);
|
||||||
} else {
|
} else {
|
||||||
$body = $req->getBody();
|
parse_str($dataPost, $arr);
|
||||||
$body->write($dataPost);
|
$req = $req->withParsedBody($arr);
|
||||||
$req = $req->withBody($body);
|
|
||||||
}
|
}
|
||||||
if (isset($user)) {
|
if (isset($user)) {
|
||||||
if (strlen($user)) {
|
if (strlen($user)) {
|
||||||
|
@ -56,7 +171,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
$req = $req->withAttribute("authenticationFailed", true);
|
$req = $req->withAttribute("authenticationFailed", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $this->h->dispatch($req);
|
return $req;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setUp() {
|
public function setUp() {
|
||||||
|
@ -95,7 +210,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
\Phake::when($this->h)->processRequest->thenReturnCallback(function($out, $G, $P) {
|
\Phake::when($this->h)->processRequest->thenReturnCallback(function($out, $G, $P) {
|
||||||
return $out;
|
return $out;
|
||||||
});
|
});
|
||||||
$act = $this->req($dataGet, $dataPost, "POST", null, "", $httpUser);
|
$act = $this->h->dispatch($this->req($dataGet, $dataPost, "POST", null, "", $httpUser));
|
||||||
$this->assertMessage($exp, $act);
|
$this->assertMessage($exp, $act);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +289,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
['group_id' => 2, 'feed_ids' => "1,3"],
|
['group_id' => 2, 'feed_ids' => "1,3"],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
$act = $this->req("api&groups");
|
$act = $this->h->dispatch($this->req("api&groups"));
|
||||||
$this->assertMessage($exp, $act);
|
$this->assertMessage($exp, $act);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,16 +307,208 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
]));
|
]));
|
||||||
$exp = new JsonResponse([
|
$exp = new JsonResponse([
|
||||||
'feeds' => [
|
'feeds' => [
|
||||||
['id' => 1, 'favicon_id' => 5, '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' => 1, 'favicon_id' => 0, '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")],
|
['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")],
|
||||||
['id' => 3, 'favicon_id' => 1, 'title' => "The Last Soul", 'url' => "http://example.org/feed", 'site_url' => "http://example.org/", 'is_spark' => 0, 'last_updated_on_time' => strtotime("1991-08-12T03:22:00Z")],
|
['id' => 3, 'favicon_id' => 0, 'title' => "The Last Soul", 'url' => "http://example.org/feed", 'site_url' => "http://example.org/", 'is_spark' => 0, 'last_updated_on_time' => strtotime("1991-08-12T03:22:00Z")],
|
||||||
],
|
],
|
||||||
'feeds_groups' => [
|
'feeds_groups' => [
|
||||||
['group_id' => 1, 'feed_ids' => "1,2"],
|
['group_id' => 1, 'feed_ids' => "1,2"],
|
||||||
['group_id' => 2, 'feed_ids' => "1,3"],
|
['group_id' => 2, 'feed_ids' => "1,3"],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
$act = $this->req("api&feeds");
|
$act = $this->h->dispatch($this->req("api&feeds"));
|
||||||
|
$this->assertMessage($exp, $act);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dataProvider provideItemListContexts */
|
||||||
|
public function testListItems(string $url, Context $c, bool $desc) {
|
||||||
|
$fields = ["id", "subscription", "title", "author", "content", "url", "starred", "unread", "published_date"];
|
||||||
|
$order = [$desc ? "id desc" : "id"];
|
||||||
|
\Phake::when(Arsse::$db)->articleList->thenReturn(new Result($this->articles['db']));
|
||||||
|
\Phake::when(Arsse::$db)->articleCount(Arsse::$user->id)->thenReturn(1024);
|
||||||
|
$exp = new JsonResponse([
|
||||||
|
'items' => $this->articles['rest'],
|
||||||
|
'total_items' => 1024,
|
||||||
|
]);
|
||||||
|
$act = $this->h->dispatch($this->req("api&$url"));
|
||||||
|
$this->assertMessage($exp, $act);
|
||||||
|
\Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, $c, $fields, $order);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideItemListContexts() {
|
||||||
|
$c = (new Context)->limit(50);
|
||||||
|
return [
|
||||||
|
["items", (clone $c), false],
|
||||||
|
["items&group_ids=1,2,3,4", (clone $c)->tags([1,2,3,4]), false],
|
||||||
|
["items&feed_ids=1,2,3,4", (clone $c)->subscriptions([1,2,3,4]), false],
|
||||||
|
["items&with_ids=1,2,3,4", (clone $c)->articles([1,2,3,4]), false],
|
||||||
|
["items&since_id=1", (clone $c)->oldestArticle(2), false],
|
||||||
|
["items&max_id=2", (clone $c)->latestArticle(1), true],
|
||||||
|
["items&with_ids=1,2,3,4&max_id=6", (clone $c)->articles([1,2,3,4]), false],
|
||||||
|
["items&with_ids=1,2,3,4&since_id=6", (clone $c)->articles([1,2,3,4]), false],
|
||||||
|
["items&max_id=3&since_id=6", (clone $c)->latestArticle(2), true],
|
||||||
|
["items&feed_ids=1,2,3,4&since_id=6", (clone $c)->subscriptions([1,2,3,4])->oldestArticle(7), false],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListItemIds() {
|
||||||
|
$saved = [['id' => 1],['id' => 2],['id' => 3]];
|
||||||
|
$unread = [['id' => 4],['id' => 5],['id' => 6]];
|
||||||
|
\Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->starred(true))->thenReturn(new Result($saved));
|
||||||
|
\Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->unread(true))->thenReturn(new Result($unread));
|
||||||
|
$exp = new JsonResponse([
|
||||||
|
'saved_item_ids' => "1,2,3"
|
||||||
|
]);
|
||||||
|
$this->assertMessage($exp, $this->h->dispatch($this->req("api&saved_item_ids")));
|
||||||
|
$exp = new JsonResponse([
|
||||||
|
'unread_item_ids' => "4,5,6"
|
||||||
|
]);
|
||||||
|
$this->assertMessage($exp, $this->h->dispatch($this->req("api&unread_item_ids")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListHotLinks() {
|
||||||
|
// hot links are not actually implemented, so an empty array should be all we get
|
||||||
|
$exp = new JsonResponse([
|
||||||
|
'links' => []
|
||||||
|
]);
|
||||||
|
$this->assertMessage($exp, $this->h->dispatch($this->req("api&links")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dataProvider provideMarkingContexts */
|
||||||
|
public function testSetMarks(string $post, Context $c, array $data, array $out) {
|
||||||
|
$saved = [['id' => 1],['id' => 2],['id' => 3]];
|
||||||
|
$unread = [['id' => 4],['id' => 5],['id' => 6]];
|
||||||
|
\Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->starred(true))->thenReturn(new Result($saved));
|
||||||
|
\Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->unread(true))->thenReturn(new Result($unread));
|
||||||
|
\Phake::when(Arsse::$db)->articleMark->thenReturn(0);
|
||||||
|
\Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $this->anything(), (new Context)->article(2112))->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("subjectMissing"));
|
||||||
|
$exp = new JsonResponse($out);
|
||||||
|
$act = $this->h->dispatch($this->req("api", $post));
|
||||||
|
$this->assertMessage($exp, $act);
|
||||||
|
if ($c && $data) {
|
||||||
|
\Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, $data, $c);
|
||||||
|
} else {
|
||||||
|
\Phake::verify(Arsse::$db, \Phake::times(0))->articleMark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideMarkingContexts() {
|
||||||
|
$markRead = ['read' => true];
|
||||||
|
$markUnread = ['read' => false];
|
||||||
|
$markSaved = ['starred' => true];
|
||||||
|
$markUnsaved = ['starred' => false];
|
||||||
|
$listSaved = ['saved_item_ids' => "1,2,3"];
|
||||||
|
$listUnread = ['unread_item_ids' => "4,5,6"];
|
||||||
|
return [
|
||||||
|
["mark=item&as=read&id=5", (new Context)->article(5), $markRead, $listUnread],
|
||||||
|
["mark=item&as=unread&id=42", (new Context)->article(42), $markUnread, $listUnread],
|
||||||
|
["mark=item&as=read&id=2112", (new Context)->article(2112), $markRead, $listUnread], // article doesn't exist
|
||||||
|
["mark=item&as=saved&id=5", (new Context)->article(5), $markSaved, $listSaved],
|
||||||
|
["mark=item&as=unsaved&id=42", (new Context)->article(42), $markUnsaved, $listSaved],
|
||||||
|
["mark=feed&as=read&id=5", (new Context)->subscription(5), $markRead, $listUnread],
|
||||||
|
["mark=feed&as=unread&id=42", (new Context)->subscription(42), $markUnread, $listUnread],
|
||||||
|
["mark=feed&as=saved&id=5", (new Context)->subscription(5), $markSaved, $listSaved],
|
||||||
|
["mark=feed&as=unsaved&id=42", (new Context)->subscription(42), $markUnsaved, $listSaved],
|
||||||
|
["mark=group&as=read&id=5", (new Context)->tag(5), $markRead, $listUnread],
|
||||||
|
["mark=group&as=unread&id=42", (new Context)->tag(42), $markUnread, $listUnread],
|
||||||
|
["mark=group&as=saved&id=5", (new Context)->tag(5), $markSaved, $listSaved],
|
||||||
|
["mark=group&as=unsaved&id=42", (new Context)->tag(42), $markUnsaved, $listSaved],
|
||||||
|
["mark=item&as=invalid&id=42", new Context, [], []],
|
||||||
|
["mark=invalid&as=unread&id=42", new Context, [], []],
|
||||||
|
["mark=group&as=read&id=0", (new Context), $markRead, $listUnread],
|
||||||
|
["mark=group&as=unread&id=0", (new Context), $markUnread, $listUnread],
|
||||||
|
["mark=group&as=saved&id=0", (new Context), $markSaved, $listSaved],
|
||||||
|
["mark=group&as=unsaved&id=0", (new Context), $markUnsaved, $listSaved],
|
||||||
|
["mark=group&as=read&id=-1", (new Context)->not->folder(0), $markRead, $listUnread],
|
||||||
|
["mark=group&as=unread&id=-1", (new Context)->not->folder(0), $markUnread, $listUnread],
|
||||||
|
["mark=group&as=saved&id=-1", (new Context)->not->folder(0), $markSaved, $listSaved],
|
||||||
|
["mark=group&as=unsaved&id=-1", (new Context)->not->folder(0), $markUnsaved, $listSaved],
|
||||||
|
["mark=group&as=read&id=-1&before=946684800", (new Context)->not->folder(0)->notMarkedSince("2000-01-01T00:00:00Z"), $markRead, $listUnread],
|
||||||
|
["mark=item&as=unread", new Context, [], []],
|
||||||
|
["mark=item&id=6", new Context, [], []],
|
||||||
|
["as=unread&id=6", new Context, [], []],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @dataProvider provideInvalidRequests */
|
||||||
|
public function testSendInvalidRequests(ServerRequest $req, ResponseInterface $exp) {
|
||||||
|
$this->assertMessage($exp, $this->h->dispatch($req));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideInvalidRequests() {
|
||||||
|
return [
|
||||||
|
'Not an API request' => [$this->req(""), new EmptyResponse(404)],
|
||||||
|
'Wrong method' => [$this->req("api", "", "GET"), new EmptyResponse(405, ['Allow' => "OPTIONS,POST"])],
|
||||||
|
'Wrong content type' => [$this->req("api", "", "POST", "application/json"), new EmptyResponse(415, ['Accept' => "application/x-www-form-urlencoded"])],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMakeABaseQuery() {
|
||||||
|
$this->h = \Phake::partialMock(API::class);
|
||||||
|
\Phake::when($this->h)->logIn->thenReturn(true);
|
||||||
|
\Phake::when(Arsse::$db)->subscriptionRefreshed(Arsse::$user->id)->thenReturn(new \DateTimeImmutable("2000-01-01T00:00:00Z"));
|
||||||
|
$exp = new JsonResponse([
|
||||||
|
'api_version' => API::LEVEL,
|
||||||
|
'auth' => 1,
|
||||||
|
'last_refreshed_on_time' => 946684800,
|
||||||
|
]);
|
||||||
|
$act = $this->h->dispatch($this->req("api"));
|
||||||
|
$this->assertMessage($exp, $act);
|
||||||
|
\Phake::when(Arsse::$db)->subscriptionRefreshed(Arsse::$user->id)->thenReturn(null); // no subscriptions
|
||||||
|
$exp = new JsonResponse([
|
||||||
|
'api_version' => API::LEVEL,
|
||||||
|
'auth' => 1,
|
||||||
|
'last_refreshed_on_time' => null,
|
||||||
|
]);
|
||||||
|
$act = $this->h->dispatch($this->req("api"));
|
||||||
|
$this->assertMessage($exp, $act);
|
||||||
|
\Phake::when($this->h)->logIn->thenReturn(false);
|
||||||
|
$exp = new JsonResponse([
|
||||||
|
'api_version' => API::LEVEL,
|
||||||
|
'auth' => 0,
|
||||||
|
]);
|
||||||
|
$act = $this->h->dispatch($this->req("api"));
|
||||||
|
$this->assertMessage($exp, $act);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUndoReadMarks() {
|
||||||
|
$unread = [['id' => 4],['id' => 5],['id' => 6]];
|
||||||
|
$out = ['unread_item_ids' => "4,5,6"];
|
||||||
|
\Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->limit(1), ["marked_date"], ["marked_date desc"])->thenReturn(new Result([['marked_date' => "2000-01-01 00:00:00"]]));
|
||||||
|
\Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->unread(true))->thenReturn(new Result($unread));
|
||||||
|
\Phake::when(Arsse::$db)->articleMark->thenReturn(0);
|
||||||
|
$exp = new JsonResponse($out);
|
||||||
|
$act = $this->h->dispatch($this->req("api", ['unread_recently_read' => 1]));
|
||||||
|
$this->assertMessage($exp, $act);
|
||||||
|
\Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, ['read' => false], (new Context)->unread(false)->markedSince("1999-12-31T23:59:45Z"));
|
||||||
|
\Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->limit(1), ["marked_date"], ["marked_date desc"])->thenReturn(new Result([]));
|
||||||
|
$act = $this->h->dispatch($this->req("api", ['unread_recently_read' => 1]));
|
||||||
|
$this->assertMessage($exp, $act);
|
||||||
|
\Phake::verify(Arsse::$db)->articleMark; // only called one time, above
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOutputToXml() {
|
||||||
|
\Phake::when($this->h)->processRequest->thenReturn([
|
||||||
|
'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><p>Article content 1</p></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><p>Article content 2</p></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><p>Article content 3</p></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><p>Article content 4</p></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><p>Article content 5</p></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>");
|
||||||
|
$act = $this->h->dispatch($this->req("api=xml"));
|
||||||
|
$this->assertMessage($exp, $act);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListFeedIcons() {
|
||||||
|
$act = $this->h->dispatch($this->req("api&favicons"));
|
||||||
|
$exp = new JsonResponse(['favicons' => [['id' => 0, 'data' => API::GENERIC_ICON_TYPE.",".API::GENERIC_ICON_DATA]]]);
|
||||||
|
$this->assertMessage($exp, $act);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnswerOptionsRequest() {
|
||||||
|
$act = $this->h->dispatch($this->req("api", "", "OPTIONS"));
|
||||||
|
$exp = new EmptyResponse(204, [
|
||||||
|
'Allow' => "POST",
|
||||||
|
'Accept' => "application/x-www-form-urlencoded",
|
||||||
|
]);
|
||||||
$this->assertMessage($exp, $act);
|
$this->assertMessage($exp, $act);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -734,11 +734,11 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
['lastModified' => $t->getTimestamp()],
|
['lastModified' => $t->getTimestamp()],
|
||||||
['oldestFirst' => false, 'batchSize' => 5, 'offset' => 0], // offset=0 should not set the latestEdition context
|
['oldestFirst' => false, 'batchSize' => 5, 'offset' => 0], // offset=0 should not set the latestEdition context
|
||||||
];
|
];
|
||||||
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, $this->anything(), $this->anything())->thenReturn(new Result($this->v($this->articles['db'])));
|
Phake::when(Arsse::$db)->articleList->thenReturn(new Result($this->v($this->articles['db'])));
|
||||||
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(42), $this->anything())->thenThrow(new ExceptionInput("idMissing"));
|
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->subscription(42), $this->anything(), ["edition desc"])->thenThrow(new ExceptionInput("idMissing"));
|
||||||
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(2112), $this->anything())->thenThrow(new ExceptionInput("idMissing"));
|
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->folder(2112), $this->anything(), ["edition desc"])->thenThrow(new ExceptionInput("idMissing"));
|
||||||
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(-1), $this->anything())->thenThrow(new ExceptionInput("typeViolation"));
|
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->subscription(-1), $this->anything(), ["edition desc"])->thenThrow(new ExceptionInput("typeViolation"));
|
||||||
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(-1), $this->anything())->thenThrow(new ExceptionInput("typeViolation"));
|
Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->folder(-1), $this->anything(), ["edition desc"])->thenThrow(new ExceptionInput("typeViolation"));
|
||||||
$exp = new Response(['items' => $this->articles['rest']]);
|
$exp = new Response(['items' => $this->articles['rest']]);
|
||||||
// check the contents of the response
|
// check the contents of the response
|
||||||
$this->assertMessage($exp, $this->req("GET", "/items")); // first instance of base context
|
$this->assertMessage($exp, $this->req("GET", "/items")); // first instance of base context
|
||||||
|
@ -759,17 +759,17 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
$this->req("GET", "/items", json_encode($in[10]));
|
$this->req("GET", "/items", json_encode($in[10]));
|
||||||
$this->req("GET", "/items", json_encode($in[11]));
|
$this->req("GET", "/items", json_encode($in[11]));
|
||||||
// perform method verifications
|
// perform method verifications
|
||||||
Phake::verify(Arsse::$db, Phake::times(4))->articleList(Arsse::$user->id, (new Context)->reverse(true), $this->anything());
|
Phake::verify(Arsse::$db, Phake::times(4))->articleList(Arsse::$user->id, new Context, $this->anything(), ["edition desc"]);
|
||||||
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(42), $this->anything());
|
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->subscription(42), $this->anything(), ["edition desc"]);
|
||||||
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(2112), $this->anything());
|
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->folder(2112), $this->anything(), ["edition desc"]);
|
||||||
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(-1), $this->anything());
|
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->subscription(-1), $this->anything(), ["edition desc"]);
|
||||||
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(-1), $this->anything());
|
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->folder(-1), $this->anything(), ["edition desc"]);
|
||||||
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->starred(true), $this->anything());
|
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->starred(true), $this->anything(), ["edition desc"]);
|
||||||
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(false)->limit(10)->oldestEdition(6), $this->anything()); // offset is one more than specified
|
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->limit(10)->oldestEdition(6), $this->anything(), ["edition"]); // offset is one more than specified
|
||||||
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5)->latestEdition(4), $this->anything()); // offset is one less than specified
|
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->limit(5)->latestEdition(4), $this->anything(), ["edition desc"]); // offset is one less than specified
|
||||||
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->unread(true), $this->anything());
|
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->unread(true), $this->anything(), ["edition desc"]);
|
||||||
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, $this->equalTo((new Context)->reverse(true)->markedSince($t), 2), $this->anything());
|
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, $this->equalTo((new Context)->markedSince($t), 2), $this->anything(), ["edition desc"]);
|
||||||
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5), $this->anything());
|
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->limit(5), $this->anything(), ["edition desc"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMarkAFolderRead() {
|
public function testMarkAFolderRead() {
|
||||||
|
@ -958,6 +958,6 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||||
$url = "/items?type=2";
|
$url = "/items?type=2";
|
||||||
Phake::when(Arsse::$db)->articleList->thenReturn(new Result([]));
|
Phake::when(Arsse::$db)->articleList->thenReturn(new Result([]));
|
||||||
$this->req("GET", $url, json_encode($in));
|
$this->req("GET", $url, json_encode($in));
|
||||||
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(false)->starred(true), $this->anything());
|
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->starred(true), $this->anything(), ["edition"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1749,19 +1749,19 @@ LONG_STRING;
|
||||||
Phake::when(Arsse::$db)->articleList->thenReturn(new Result($this->v([['id' => 0]])));
|
Phake::when(Arsse::$db)->articleList->thenReturn(new Result($this->v([['id' => 0]])));
|
||||||
Phake::when(Arsse::$db)->articleCount->thenReturn(0);
|
Phake::when(Arsse::$db)->articleCount->thenReturn(0);
|
||||||
Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1);
|
Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1);
|
||||||
$c = (new Context)->reverse(true);
|
$c = (new Context);
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(2112), ["id"])->thenThrow(new ExceptionInput("subjectMissing"));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(2112), ["id"], ["edited_date desc"])->thenThrow(new ExceptionInput("subjectMissing"));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), $c, ["id"])->thenReturn(new Result($this->v($this->articles)));
|
Phake::when(Arsse::$db)->articleList($this->anything(), $c, ["id"], ["edited_date desc"])->thenReturn(new Result($this->v($this->articles)));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->starred(true), ["id"])->thenReturn(new Result($this->v([['id' => 1]])));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->starred(true), ["id"], ["marked_date desc"])->thenReturn(new Result($this->v([['id' => 1]])));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088), ["id"])->thenReturn(new Result($this->v([['id' => 2]])));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088), ["id"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 2]])));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true), ["id"])->thenReturn(new Result($this->v([['id' => 3]])));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true), ["id"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 3]])));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088)->unread(true), ["id"])->thenReturn(new Result($this->v([['id' => 4]])));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088)->unread(true), ["id"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 4]])));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->starred(true), ["id"])->thenReturn(new Result($this->v([['id' => 5]])));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->starred(true), ["id"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 5]])));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->annotated(true), ["id"])->thenReturn(new Result($this->v([['id' => 6]])));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->annotated(true), ["id"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 6]])));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5), ["id"])->thenReturn(new Result($this->v([['id' => 7]])));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5), ["id"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 7]])));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->offset(2), ["id"])->thenReturn(new Result($this->v([['id' => 8]])));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->offset(2), ["id"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 8]])));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5)->offset(2), ["id"])->thenReturn(new Result($this->v([['id' => 9]])));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5)->offset(2), ["id"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 9]])));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->oldestArticle(48), ["id"])->thenReturn(new Result($this->v([['id' => 10]])));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->oldestArticle(48), ["id"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 10]])));
|
||||||
$out1 = [
|
$out1 = [
|
||||||
$this->respErr("INCORRECT_USAGE"),
|
$this->respErr("INCORRECT_USAGE"),
|
||||||
$this->respGood([]),
|
$this->respGood([]),
|
||||||
|
@ -1793,9 +1793,9 @@ LONG_STRING;
|
||||||
$this->assertMessage($out1[$a], $this->req($in1[$a]), "Test $a failed");
|
$this->assertMessage($out1[$a], $this->req($in1[$a]), "Test $a failed");
|
||||||
}
|
}
|
||||||
for ($a = 0; $a < sizeof($in2); $a++) {
|
for ($a = 0; $a < sizeof($in2); $a++) {
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(false)->markedSince(Date::sub("PT24H")), 2), ["id"])->thenReturn(new Result($this->v([['id' => 1001]])));
|
Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(false)->markedSince(Date::sub("PT24H")), 2), ["id"], ["marked_date desc"])->thenReturn(new Result($this->v([['id' => 1001]])));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), 2), ["id"])->thenReturn(new Result($this->v([['id' => 1002]])));
|
Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), 2), ["id"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 1002]])));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), 2), ["id"])->thenReturn(new Result($this->v([['id' => 1003]])));
|
Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), 2), ["id"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 1003]])));
|
||||||
$this->assertMessage($out2[$a], $this->req($in2[$a]), "Test $a failed");
|
$this->assertMessage($out2[$a], $this->req($in2[$a]), "Test $a failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1853,25 +1853,25 @@ LONG_STRING;
|
||||||
Phake::when(Arsse::$db)->articleList->thenReturn($this->generateHeadlines(0));
|
Phake::when(Arsse::$db)->articleList->thenReturn($this->generateHeadlines(0));
|
||||||
Phake::when(Arsse::$db)->articleCount->thenReturn(0);
|
Phake::when(Arsse::$db)->articleCount->thenReturn(0);
|
||||||
Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1);
|
Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1);
|
||||||
$c = (new Context)->limit(200)->reverse(true);
|
$c = (new Context)->limit(200);
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(2112), $this->anything())->thenThrow(new ExceptionInput("subjectMissing"));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(2112), $this->anything(), ["edited_date desc"])->thenThrow(new ExceptionInput("subjectMissing"));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->starred(true), $this->anything())->thenReturn($this->generateHeadlines(1));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->starred(true), $this->anything(), ["marked_date desc"])->thenReturn($this->generateHeadlines(1));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088), $this->anything())->thenReturn($this->generateHeadlines(2));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(2));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true), $this->anything())->thenReturn($this->generateHeadlines(3));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(3));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088)->unread(true), $this->anything())->thenReturn($this->generateHeadlines(4));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088)->unread(true), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(4));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->starred(true), $this->anything())->thenReturn($this->generateHeadlines(5));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->starred(true), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(5));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->annotated(true), $this->anything())->thenReturn($this->generateHeadlines(6));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->annotated(true), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(6));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5), $this->anything())->thenReturn($this->generateHeadlines(7));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(7));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->offset(2), $this->anything())->thenReturn($this->generateHeadlines(8));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->offset(2), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(8));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5)->offset(2), $this->anything())->thenReturn($this->generateHeadlines(9));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5)->offset(2), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(9));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->oldestArticle(48), $this->anything())->thenReturn($this->generateHeadlines(10));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->oldestArticle(48), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(10));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c), $this->anything())->thenReturn($this->generateHeadlines(11));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(11));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->labelled(true), $this->anything())->thenReturn($this->generateHeadlines(12));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->labelled(true), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(12));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folderShallow(0), $this->anything())->thenReturn($this->generateHeadlines(13));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folderShallow(0), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(13));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folderShallow(42), $this->anything())->thenReturn($this->generateHeadlines(14));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folderShallow(42), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(14));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folder(42), $this->anything())->thenReturn($this->generateHeadlines(15));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folder(42), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(15));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->reverse(false), $this->anything())->thenReturn($this->generateHeadlines(16));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c), $this->anything(), ["edited_date"])->thenReturn($this->generateHeadlines(16));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->searchTerms(["interesting"]), $this->anything())->thenReturn($this->generateHeadlines(17));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->searchTerms(["interesting"]), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(17));
|
||||||
$out2 = [
|
$out2 = [
|
||||||
$this->respErr("INCORRECT_USAGE"),
|
$this->respErr("INCORRECT_USAGE"),
|
||||||
$this->outputHeadlines(11),
|
$this->outputHeadlines(11),
|
||||||
|
@ -1909,9 +1909,9 @@ LONG_STRING;
|
||||||
$this->assertMessage($out2[$a], $this->req($in2[$a]), "Test $a failed");
|
$this->assertMessage($out2[$a], $this->req($in2[$a]), "Test $a failed");
|
||||||
}
|
}
|
||||||
for ($a = 0; $a < sizeof($in3); $a++) {
|
for ($a = 0; $a < sizeof($in3); $a++) {
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(false)->markedSince(Date::sub("PT24H")), 2), $this->anything())->thenReturn($this->generateHeadlines(1001));
|
Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(false)->markedSince(Date::sub("PT24H")), 2), $this->anything(), ["marked_date desc"])->thenReturn($this->generateHeadlines(1001));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), 2), $this->anything())->thenReturn($this->generateHeadlines(1002));
|
Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), 2), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(1002));
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), 2), $this->anything())->thenReturn($this->generateHeadlines(1003));
|
Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), 2), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(1003));
|
||||||
$this->assertMessage($out3[$a], $this->req($in3[$a]), "Test $a failed");
|
$this->assertMessage($out3[$a], $this->req($in3[$a]), "Test $a failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1990,7 +1990,7 @@ LONG_STRING;
|
||||||
]);
|
]);
|
||||||
$this->assertMessage($exp, $test);
|
$this->assertMessage($exp, $test);
|
||||||
// test 'include_header' with an erroneous result
|
// test 'include_header' with an erroneous result
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->limit(200)->reverse(true)->subscription(2112), $this->anything())->thenThrow(new ExceptionInput("subjectMissing"));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->limit(200)->subscription(2112), $this->anything(), ["edited_date desc"])->thenThrow(new ExceptionInput("subjectMissing"));
|
||||||
$test = $this->req($in[6]);
|
$test = $this->req($in[6]);
|
||||||
$exp = $this->respGood([
|
$exp = $this->respGood([
|
||||||
['id' => 2112, 'is_cat' => false, 'first_id' => 0],
|
['id' => 2112, 'is_cat' => false, 'first_id' => 0],
|
||||||
|
@ -2005,7 +2005,7 @@ LONG_STRING;
|
||||||
]);
|
]);
|
||||||
$this->assertMessage($exp, $test);
|
$this->assertMessage($exp, $test);
|
||||||
// test 'include_header' with skip
|
// test 'include_header' with skip
|
||||||
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->reverse(true)->limit(1)->subscription(42), $this->anything())->thenReturn($this->generateHeadlines(1867));
|
Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->limit(1)->subscription(42), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(1867));
|
||||||
$test = $this->req($in[8]);
|
$test = $this->req($in[8]);
|
||||||
$exp = $this->respGood([
|
$exp = $this->respGood([
|
||||||
['id' => 42, 'is_cat' => false, 'first_id' => 1867],
|
['id' => 42, 'is_cat' => false, 'first_id' => 1867],
|
||||||
|
|
|
@ -18,6 +18,7 @@ use Psr\Http\Message\RequestInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Zend\Diactoros\Response\JsonResponse;
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
|
use Zend\Diactoros\Response\XmlResponse;
|
||||||
|
|
||||||
/** @coversNothing */
|
/** @coversNothing */
|
||||||
abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
||||||
|
@ -98,6 +99,8 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
||||||
if ($exp instanceof JsonResponse) {
|
if ($exp instanceof JsonResponse) {
|
||||||
$this->assertEquals($exp->getPayload(), $act->getPayload(), $text);
|
$this->assertEquals($exp->getPayload(), $act->getPayload(), $text);
|
||||||
$this->assertSame($exp->getPayload(), $act->getPayload(), $text);
|
$this->assertSame($exp->getPayload(), $act->getPayload(), $text);
|
||||||
|
} elseif ($exp instanceof XmlResponse) {
|
||||||
|
$this->assertXmlStringEqualsXmlString((string) $exp->getBody(), (string) $act->getBody(), $text);
|
||||||
} else {
|
} else {
|
||||||
$this->assertEquals((string) $exp->getBody(), (string) $act->getBody(), $text);
|
$this->assertEquals((string) $exp->getBody(), (string) $act->getBody(), $text);
|
||||||
}
|
}
|
||||||
|
|
54
vendor-bin/csfixer/composer.lock
generated
54
vendor-bin/csfixer/composer.lock
generated
|
@ -522,16 +522,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/console",
|
"name": "symfony/console",
|
||||||
"version": "v4.3.1",
|
"version": "v4.3.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/console.git",
|
"url": "https://github.com/symfony/console.git",
|
||||||
"reference": "d50bbeeb0e17e6dd4124ea391eff235e932cbf64"
|
"reference": "b592b26a24265a35172d8a2094d8b10f22b7cc39"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/console/zipball/d50bbeeb0e17e6dd4124ea391eff235e932cbf64",
|
"url": "https://api.github.com/repos/symfony/console/zipball/b592b26a24265a35172d8a2094d8b10f22b7cc39",
|
||||||
"reference": "d50bbeeb0e17e6dd4124ea391eff235e932cbf64",
|
"reference": "b592b26a24265a35172d8a2094d8b10f22b7cc39",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -593,20 +593,20 @@
|
||||||
],
|
],
|
||||||
"description": "Symfony Console Component",
|
"description": "Symfony Console Component",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"time": "2019-06-05T13:25:51+00:00"
|
"time": "2019-06-13T11:03:18+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/event-dispatcher",
|
"name": "symfony/event-dispatcher",
|
||||||
"version": "v4.3.1",
|
"version": "v4.3.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||||
"reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f"
|
"reference": "d257021c1ab28d48d24a16de79dfab445ce93398"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4e6c670af81c4fb0b6c08b035530a9915d0b691f",
|
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d257021c1ab28d48d24a16de79dfab445ce93398",
|
||||||
"reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f",
|
"reference": "d257021c1ab28d48d24a16de79dfab445ce93398",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -663,7 +663,7 @@
|
||||||
],
|
],
|
||||||
"description": "Symfony EventDispatcher Component",
|
"description": "Symfony EventDispatcher Component",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"time": "2019-05-30T16:10:05+00:00"
|
"time": "2019-06-13T11:03:18+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/event-dispatcher-contracts",
|
"name": "symfony/event-dispatcher-contracts",
|
||||||
|
@ -725,16 +725,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/filesystem",
|
"name": "symfony/filesystem",
|
||||||
"version": "v4.3.1",
|
"version": "v4.3.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/filesystem.git",
|
"url": "https://github.com/symfony/filesystem.git",
|
||||||
"reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf"
|
"reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/bf2af40d738dec5e433faea7b00daa4431d0a4cf",
|
"url": "https://api.github.com/repos/symfony/filesystem/zipball/b9896d034463ad6fd2bf17e2bf9418caecd6313d",
|
||||||
"reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf",
|
"reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -771,20 +771,20 @@
|
||||||
],
|
],
|
||||||
"description": "Symfony Filesystem Component",
|
"description": "Symfony Filesystem Component",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"time": "2019-06-03T20:27:40+00:00"
|
"time": "2019-06-23T08:51:25+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/finder",
|
"name": "symfony/finder",
|
||||||
"version": "v4.3.1",
|
"version": "v4.3.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/finder.git",
|
"url": "https://github.com/symfony/finder.git",
|
||||||
"reference": "b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176"
|
"reference": "33c21f7d5d3dc8a140c282854a7e13aeb5d0f91a"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/finder/zipball/b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176",
|
"url": "https://api.github.com/repos/symfony/finder/zipball/33c21f7d5d3dc8a140c282854a7e13aeb5d0f91a",
|
||||||
"reference": "b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176",
|
"reference": "33c21f7d5d3dc8a140c282854a7e13aeb5d0f91a",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -820,20 +820,20 @@
|
||||||
],
|
],
|
||||||
"description": "Symfony Finder Component",
|
"description": "Symfony Finder Component",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"time": "2019-05-26T20:47:49+00:00"
|
"time": "2019-06-13T11:03:18+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/options-resolver",
|
"name": "symfony/options-resolver",
|
||||||
"version": "v4.3.1",
|
"version": "v4.3.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/options-resolver.git",
|
"url": "https://github.com/symfony/options-resolver.git",
|
||||||
"reference": "914e0edcb7cd0c9f494bc023b1d47534f4542332"
|
"reference": "40762ead607c8f792ee4516881369ffa553fee6f"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/914e0edcb7cd0c9f494bc023b1d47534f4542332",
|
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/40762ead607c8f792ee4516881369ffa553fee6f",
|
||||||
"reference": "914e0edcb7cd0c9f494bc023b1d47534f4542332",
|
"reference": "40762ead607c8f792ee4516881369ffa553fee6f",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -874,7 +874,7 @@
|
||||||
"configuration",
|
"configuration",
|
||||||
"options"
|
"options"
|
||||||
],
|
],
|
||||||
"time": "2019-05-10T05:38:46+00:00"
|
"time": "2019-06-13T11:01:17+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-ctype",
|
"name": "symfony/polyfill-ctype",
|
||||||
|
@ -1167,7 +1167,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/process",
|
"name": "symfony/process",
|
||||||
"version": "v4.3.1",
|
"version": "v4.3.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/process.git",
|
"url": "https://github.com/symfony/process.git",
|
||||||
|
@ -1274,7 +1274,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/stopwatch",
|
"name": "symfony/stopwatch",
|
||||||
"version": "v4.3.1",
|
"version": "v4.3.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/stopwatch.git",
|
"url": "https://github.com/symfony/stopwatch.git",
|
||||||
|
|
24
vendor-bin/phpunit/composer.lock
generated
24
vendor-bin/phpunit/composer.lock
generated
|
@ -786,16 +786,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/php-token-stream",
|
"name": "phpunit/php-token-stream",
|
||||||
"version": "3.0.1",
|
"version": "3.0.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
|
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
|
||||||
"reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18"
|
"reference": "c4a66b97f040e3e20b3aa2a243230a1c3a9f7c8c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c99e3be9d3e85f60646f152f9002d46ed7770d18",
|
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c4a66b97f040e3e20b3aa2a243230a1c3a9f7c8c",
|
||||||
"reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18",
|
"reference": "c4a66b97f040e3e20b3aa2a243230a1c3a9f7c8c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -831,20 +831,20 @@
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tokenizer"
|
"tokenizer"
|
||||||
],
|
],
|
||||||
"time": "2018-10-30T05:52:18+00:00"
|
"time": "2019-07-08T05:24:54+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/phpunit",
|
"name": "phpunit/phpunit",
|
||||||
"version": "7.5.13",
|
"version": "7.5.14",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||||
"reference": "b9278591caa8630127f96c63b598712b699e671c"
|
"reference": "2834789aeb9ac182ad69bfdf9ae91856a59945ff"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b9278591caa8630127f96c63b598712b699e671c",
|
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2834789aeb9ac182ad69bfdf9ae91856a59945ff",
|
||||||
"reference": "b9278591caa8630127f96c63b598712b699e671c",
|
"reference": "2834789aeb9ac182ad69bfdf9ae91856a59945ff",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -904,8 +904,8 @@
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Sebastian Bergmann",
|
"name": "Sebastian Bergmann",
|
||||||
"email": "sebastian@phpunit.de",
|
"role": "lead",
|
||||||
"role": "lead"
|
"email": "sebastian@phpunit.de"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "The PHP Unit Testing framework.",
|
"description": "The PHP Unit Testing framework.",
|
||||||
|
@ -915,7 +915,7 @@
|
||||||
"testing",
|
"testing",
|
||||||
"xunit"
|
"xunit"
|
||||||
],
|
],
|
||||||
"time": "2019-06-19T12:01:51+00:00"
|
"time": "2019-07-15T06:24:08+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/code-unit-reverse-lookup",
|
"name": "sebastian/code-unit-reverse-lookup",
|
||||||
|
|
52
vendor-bin/robo/composer.lock
generated
52
vendor-bin/robo/composer.lock
generated
|
@ -1092,16 +1092,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/console",
|
"name": "symfony/console",
|
||||||
"version": "v4.3.1",
|
"version": "v4.3.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/console.git",
|
"url": "https://github.com/symfony/console.git",
|
||||||
"reference": "d50bbeeb0e17e6dd4124ea391eff235e932cbf64"
|
"reference": "b592b26a24265a35172d8a2094d8b10f22b7cc39"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/console/zipball/d50bbeeb0e17e6dd4124ea391eff235e932cbf64",
|
"url": "https://api.github.com/repos/symfony/console/zipball/b592b26a24265a35172d8a2094d8b10f22b7cc39",
|
||||||
"reference": "d50bbeeb0e17e6dd4124ea391eff235e932cbf64",
|
"reference": "b592b26a24265a35172d8a2094d8b10f22b7cc39",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -1163,20 +1163,20 @@
|
||||||
],
|
],
|
||||||
"description": "Symfony Console Component",
|
"description": "Symfony Console Component",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"time": "2019-06-05T13:25:51+00:00"
|
"time": "2019-06-13T11:03:18+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/event-dispatcher",
|
"name": "symfony/event-dispatcher",
|
||||||
"version": "v4.3.1",
|
"version": "v4.3.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||||
"reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f"
|
"reference": "d257021c1ab28d48d24a16de79dfab445ce93398"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4e6c670af81c4fb0b6c08b035530a9915d0b691f",
|
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d257021c1ab28d48d24a16de79dfab445ce93398",
|
||||||
"reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f",
|
"reference": "d257021c1ab28d48d24a16de79dfab445ce93398",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -1233,7 +1233,7 @@
|
||||||
],
|
],
|
||||||
"description": "Symfony EventDispatcher Component",
|
"description": "Symfony EventDispatcher Component",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"time": "2019-05-30T16:10:05+00:00"
|
"time": "2019-06-13T11:03:18+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/event-dispatcher-contracts",
|
"name": "symfony/event-dispatcher-contracts",
|
||||||
|
@ -1295,16 +1295,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/filesystem",
|
"name": "symfony/filesystem",
|
||||||
"version": "v4.3.1",
|
"version": "v4.3.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/filesystem.git",
|
"url": "https://github.com/symfony/filesystem.git",
|
||||||
"reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf"
|
"reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/bf2af40d738dec5e433faea7b00daa4431d0a4cf",
|
"url": "https://api.github.com/repos/symfony/filesystem/zipball/b9896d034463ad6fd2bf17e2bf9418caecd6313d",
|
||||||
"reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf",
|
"reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -1341,20 +1341,20 @@
|
||||||
],
|
],
|
||||||
"description": "Symfony Filesystem Component",
|
"description": "Symfony Filesystem Component",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"time": "2019-06-03T20:27:40+00:00"
|
"time": "2019-06-23T08:51:25+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/finder",
|
"name": "symfony/finder",
|
||||||
"version": "v4.3.1",
|
"version": "v4.3.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/finder.git",
|
"url": "https://github.com/symfony/finder.git",
|
||||||
"reference": "b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176"
|
"reference": "33c21f7d5d3dc8a140c282854a7e13aeb5d0f91a"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/finder/zipball/b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176",
|
"url": "https://api.github.com/repos/symfony/finder/zipball/33c21f7d5d3dc8a140c282854a7e13aeb5d0f91a",
|
||||||
"reference": "b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176",
|
"reference": "33c21f7d5d3dc8a140c282854a7e13aeb5d0f91a",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -1390,7 +1390,7 @@
|
||||||
],
|
],
|
||||||
"description": "Symfony Finder Component",
|
"description": "Symfony Finder Component",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"time": "2019-05-26T20:47:49+00:00"
|
"time": "2019-06-13T11:03:18+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-ctype",
|
"name": "symfony/polyfill-ctype",
|
||||||
|
@ -1569,16 +1569,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/process",
|
"name": "symfony/process",
|
||||||
"version": "v3.4.28",
|
"version": "v3.4.29",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/process.git",
|
"url": "https://github.com/symfony/process.git",
|
||||||
"reference": "afe411c2a6084f25cff55a01d0d4e1474c97ff13"
|
"reference": "d129c017e8602507688ef2c3007951a16c1a8407"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/process/zipball/afe411c2a6084f25cff55a01d0d4e1474c97ff13",
|
"url": "https://api.github.com/repos/symfony/process/zipball/d129c017e8602507688ef2c3007951a16c1a8407",
|
||||||
"reference": "afe411c2a6084f25cff55a01d0d4e1474c97ff13",
|
"reference": "d129c017e8602507688ef2c3007951a16c1a8407",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -1614,7 +1614,7 @@
|
||||||
],
|
],
|
||||||
"description": "Symfony Process Component",
|
"description": "Symfony Process Component",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"time": "2019-05-22T12:54:11+00:00"
|
"time": "2019-05-30T15:47:52+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/service-contracts",
|
"name": "symfony/service-contracts",
|
||||||
|
@ -1676,7 +1676,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/yaml",
|
"name": "symfony/yaml",
|
||||||
"version": "v4.3.1",
|
"version": "v4.3.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/yaml.git",
|
"url": "https://github.com/symfony/yaml.git",
|
||||||
|
|
Loading…
Reference in a new issue