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)
|
||||
- Command line functionality for clearing a password, disabling the account
|
||||
- Command line options for dealing with Fever passwords
|
||||
- Command line functionality for exporting subscriptions to OPML
|
||||
- Command line documentation of all commands and options
|
||||
|
||||
Bug fixes:
|
||||
- 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)
|
||||
==========================
|
||||
|
|
|
@ -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
|
||||
- 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"`
|
||||
- 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
|
||||
|
||||
#### Other notes
|
||||
|
|
|
@ -10,6 +10,12 @@ usually prudent:
|
|||
- If installing from source, update dependencies with:
|
||||
`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
|
||||
=============================
|
||||
|
|
|
@ -25,7 +25,7 @@ if (\PHP_SAPI === "cli") {
|
|||
$conf = file_exists(BASE."config.php") ? new Conf(BASE."config.php") : new Conf;
|
||||
Arsse::load($conf);
|
||||
// handle Web requests
|
||||
$emitter = new \Zend\Diactoros\Response\SapiEmitter();
|
||||
$emitter = new \Zend\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||
$response = (new REST)->dispatch();
|
||||
$emitter->emit($response);
|
||||
}
|
||||
|
|
|
@ -18,16 +18,17 @@
|
|||
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.0",
|
||||
"php": "7.*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-hash": "*",
|
||||
"ext-dom": "*",
|
||||
"p3k/picofeed": "0.1.*",
|
||||
"hosteurope/password-generator": "^1.0",
|
||||
"docopt/docopt": "^1.0",
|
||||
"jkingweb/druuid": "^3.0",
|
||||
"zendframework/zend-diactoros": "^1.6"
|
||||
"hosteurope/password-generator": "1.*",
|
||||
"docopt/docopt": "1.*",
|
||||
"jkingweb/druuid": "3.*",
|
||||
"zendframework/zend-diactoros": "2.*",
|
||||
"zendframework/zend-httphandlerrunner": "1.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f61a02cd168914d91847b89dcd00d464",
|
||||
"content-hash": "c2b0698669d89268ffb995a5e1d6667a",
|
||||
"packages": [
|
||||
{
|
||||
"name": "docopt/docopt",
|
||||
|
@ -190,6 +190,58 @@
|
|||
"homepage": "https://github.com/miniflux/picoFeed",
|
||||
"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",
|
||||
"version": "1.0.1",
|
||||
|
@ -241,39 +293,95 @@
|
|||
"time": "2016-08-06T14:39:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "zendframework/zend-diactoros",
|
||||
"version": "1.8.6",
|
||||
"name": "psr/http-server-handler",
|
||||
"version": "1.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zendframework/zend-diactoros.git",
|
||||
"reference": "20da13beba0dde8fb648be3cc19765732790f46e"
|
||||
"url": "https://github.com/php-fig/http-server-handler.git",
|
||||
"reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/20da13beba0dde8fb648be3cc19765732790f46e",
|
||||
"reference": "20da13beba0dde8fb648be3cc19765732790f46e",
|
||||
"url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7",
|
||||
"reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7",
|
||||
"shasum": ""
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"provide": {
|
||||
"psr/http-factory-implementation": "1.0",
|
||||
"psr/http-message-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"http-interop/http-factory-tests": "^0.5.0",
|
||||
"php-http/psr7-integration-tests": "dev-master",
|
||||
"phpunit/phpunit": "^5.7.16 || ^6.0.8 || ^7.2.7",
|
||||
"zendframework/zend-coding-standard": "~1.0"
|
||||
"phpunit/phpunit": "^7.0.2",
|
||||
"zendframework/zend-coding-standard": "~1.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.8.x-dev",
|
||||
"dev-develop": "1.9.x-dev",
|
||||
"dev-release-2.0": "2.0.x-dev"
|
||||
"dev-master": "2.1.x-dev",
|
||||
"dev-develop": "2.2.x-dev",
|
||||
"dev-release-1.8": "1.8.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -293,16 +401,70 @@
|
|||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"description": "PSR HTTP Message implementations",
|
||||
"homepage": "https://github.com/zendframework/zend-diactoros",
|
||||
"keywords": [
|
||||
"http",
|
||||
"psr",
|
||||
"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",
|
||||
|
@ -398,7 +560,7 @@
|
|||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^7.0",
|
||||
"php": "7.*",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-hash": "*",
|
||||
|
|
|
@ -9,7 +9,6 @@ namespace JKingWeb\Arsse\Context;
|
|||
class Context extends ExclusionContext {
|
||||
/** @var ExclusionContext */
|
||||
public $not;
|
||||
public $reverse = false;
|
||||
public $limit = 0;
|
||||
public $offset = 0;
|
||||
public $unread;
|
||||
|
@ -31,10 +30,6 @@ class Context extends ExclusionContext {
|
|||
unset($this->not);
|
||||
}
|
||||
|
||||
public function reverse(bool $spec = null) {
|
||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||
}
|
||||
|
||||
public function limit(int $spec = null) {
|
||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||
}
|
||||
|
|
|
@ -11,16 +11,23 @@ use JKingWeb\Arsse\Misc\Date;
|
|||
|
||||
class ExclusionContext {
|
||||
public $folder;
|
||||
public $folders;
|
||||
public $folderShallow;
|
||||
public $foldersShallow;
|
||||
public $tag;
|
||||
public $tags;
|
||||
public $tagName;
|
||||
public $tagNames;
|
||||
public $subscription;
|
||||
public $subscriptions;
|
||||
public $edition;
|
||||
public $article;
|
||||
public $editions;
|
||||
public $article;
|
||||
public $articles;
|
||||
public $label;
|
||||
public $labels;
|
||||
public $labelName;
|
||||
public $labelNames;
|
||||
public $annotationTerms;
|
||||
public $searchTerms;
|
||||
public $titleTerms;
|
||||
|
@ -42,6 +49,7 @@ class ExclusionContext {
|
|||
}
|
||||
|
||||
public function __clone() {
|
||||
// if the context was cloned because its parent was cloned, change the parent to the clone
|
||||
if ($this->parent) {
|
||||
$t = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1];
|
||||
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);
|
||||
for ($a = 0; $a < sizeof($spec); $a++) {
|
||||
if (ValueInfo::id($spec[$a])) {
|
||||
if (ValueInfo::id($spec[$a], $allowZero)) {
|
||||
$spec[$a] = (int) $spec[$a];
|
||||
} 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 {
|
||||
|
@ -99,22 +109,57 @@ class ExclusionContext {
|
|||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||
}
|
||||
|
@ -141,10 +186,24 @@ class ExclusionContext {
|
|||
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) {
|
||||
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) {
|
||||
if (isset($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);
|
||||
}
|
||||
|
||||
/** 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
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
// prepare the output column list; the column definitions are also used later
|
||||
$greatest = $this->db->sqlToken("greatest");
|
||||
$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",
|
||||
];
|
||||
$colDefs = $this->articleColumns();
|
||||
if (!$cols) {
|
||||
// if no columns are specified return a count
|
||||
$columns = "count(distinct arsse_articles.id) as count";
|
||||
// if no columns are specified return a count; don't borther with sorting
|
||||
$outColumns = "count(distinct arsse_articles.id) as count";
|
||||
} 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) {
|
||||
$col = trim(strtolower($col));
|
||||
if (!isset($colDefs[$col])) {
|
||||
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
|
||||
$q = new Query(
|
||||
"SELECT
|
||||
$columns
|
||||
$outColumns
|
||||
from arsse_articles
|
||||
join arsse_subscriptions on arsse_subscriptions.feed = arsse_articles.feed and arsse_subscriptions.owner = ?
|
||||
join arsse_feeds on arsse_subscriptions.feed = arsse_feeds.id
|
||||
|
@ -1344,7 +1357,9 @@ class Database {
|
|||
"markedSince" => ["marked_date", ">=", "datetime", "notMarkedSince"],
|
||||
"notMarkedSince" => ["marked_date", "<=", "datetime", "markedSince"],
|
||||
"folderShallow" => ["folder", "=", "int", ""],
|
||||
"foldersShallow" => ["folder", "in", "int", ""],
|
||||
"subscription" => ["subscription", "=", "int", ""],
|
||||
"subscriptions" => ["subscription", "in", "int", ""],
|
||||
"unread" => ["unread", "=", "bool", ""],
|
||||
"starred" => ["starred", "=", "bool", ""],
|
||||
];
|
||||
|
@ -1395,6 +1410,79 @@ class Database {
|
|||
$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
|
||||
if ($context->annotated()) {
|
||||
$comp = ($context->annotated) ? "<>" : "=";
|
||||
|
@ -1405,48 +1493,32 @@ class Database {
|
|||
$op = $context->labelled ? ">" : "=";
|
||||
$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()) {
|
||||
// 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
|
||||
$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()) {
|
||||
// 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
|
||||
$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
|
||||
$options = [
|
||||
"titleTerms" => ["arsse_articles.title"],
|
||||
|
@ -1454,20 +1526,20 @@ class Database {
|
|||
"authorTerms" => ["arsse_articles.author"],
|
||||
"annotationTerms" => ["arsse_marks.note"],
|
||||
];
|
||||
foreach ($options as $m => $cols) {
|
||||
foreach ($options as $m => $columns) {
|
||||
if (!$context->$m()) {
|
||||
continue;
|
||||
} elseif (!$context->$m) {
|
||||
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
|
||||
foreach ($options as $m => $cols) {
|
||||
foreach ($options as $m => $columns) {
|
||||
if (!$context->not->$m() || !$context->not->$m) {
|
||||
continue;
|
||||
}
|
||||
$q->setWhereNot(...$this->generateSearch($context->not->$m, $cols, true));
|
||||
$q->setWhereNot(...$this->generateSearch($context->not->$m, $columns, true));
|
||||
}
|
||||
// return the query
|
||||
return $q;
|
||||
|
@ -1479,16 +1551,47 @@ class Database {
|
|||
*
|
||||
* @param string $user The user whose articles are to be listed
|
||||
* @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__)) {
|
||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||
}
|
||||
// make a base query based on context and output columns
|
||||
$context = $context ?? new Context;
|
||||
$q = $this->articleQuery($user, $context, $fields);
|
||||
$q->setOrder("arsse_articles.edited".($context->reverse ? " desc" : ""));
|
||||
$q->setOrder("latest_editions.edition".($context->reverse ? " desc" : ""));
|
||||
// make an ORDER BY column list
|
||||
$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
|
||||
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\Service;
|
||||
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\AbstractException;
|
||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||
|
@ -21,26 +21,54 @@ use JKingWeb\Arsse\REST\Exception405;
|
|||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\Diactoros\Response\XmlResponse;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
|
||||
class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||
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 dispatch(ServerRequestInterface $req): ResponseInterface {
|
||||
$inR = $req->getQueryParams() ?? [];
|
||||
$inW = $req->getParsedBody() ?? [];
|
||||
if (!array_key_exists("api", $inR)) {
|
||||
$G = $this->normalizeInputGet($req->getQueryParams() ?? []);
|
||||
$P = $this->normalizeInputPost($req->getParsedBody() ?? []);
|
||||
if (!isset($G['api'])) {
|
||||
// the original would have shown the Fever UI in the absence of the "api" parameter, but we'll return 404
|
||||
return new EmptyResponse(404);
|
||||
}
|
||||
$xml = $inR['api'] === "xml";
|
||||
switch ($req->getMethod()) {
|
||||
case "OPTIONS":
|
||||
// do stuff
|
||||
break;
|
||||
return new EmptyResponse(204, [
|
||||
'Allow' => "POST",
|
||||
'Accept' => "application/x-www-form-urlencoded",
|
||||
]);
|
||||
case "POST":
|
||||
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"]);
|
||||
|
@ -58,31 +86,87 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
return new EmptyResponse(401);
|
||||
}
|
||||
// produce a full response if authenticated or a basic response otherwise
|
||||
if ($this->logIn(strtolower($inW['api_key'] ?? ""))) {
|
||||
$out = $this->processRequest($this->baseResponse(true), $inR, $inW);
|
||||
if ($this->logIn(strtolower($P['api_key'] ?? ""))) {
|
||||
$out = $this->processRequest($this->baseResponse(true), $G, $P);
|
||||
} else {
|
||||
$out = $this->baseResponse(false);
|
||||
}
|
||||
// return the result, possibly formatted as XML
|
||||
return $this->formatResponse($out, $xml);
|
||||
break;
|
||||
return $this->formatResponse($out, ($G['api'] === "xml"));
|
||||
default:
|
||||
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 {
|
||||
if (array_key_exists("feeds", $G) || array_key_exists("groups", $G)) {
|
||||
if (array_key_exists("groups", $G)) {
|
||||
$listUnread = false;
|
||||
$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();
|
||||
}
|
||||
if (array_key_exists("feeds", $G)) {
|
||||
if ($G['feeds']) {
|
||||
$out['feeds'] = $this->getFeeds();
|
||||
}
|
||||
$out['feeds_groups'] = $this->getRelationships();
|
||||
}
|
||||
if (array_key_exists("favicons", $G)) {
|
||||
# deal with favicons
|
||||
if ($G['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;
|
||||
}
|
||||
|
@ -101,12 +185,49 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
|
||||
protected function formatResponse(array $data, bool $xml): ResponseInterface {
|
||||
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 {
|
||||
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 {
|
||||
// if HTTP authentication was successful and sessions are not enforced, proceed unconditionally
|
||||
if (isset(Arsse::$user->id) && !Arsse::$conf->userSessionEnforced) {
|
||||
|
@ -123,6 +244,80 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
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() {
|
||||
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) {
|
||||
$out[] = [
|
||||
'id' => (int) $sub['id'],
|
||||
'favicon_id' => (int) ($sub['favicon'] ? $sub['feed'] : 0),
|
||||
'favicon_id' => 0, // TODO: implement favicons
|
||||
'title' => (string) $sub['title'],
|
||||
'url' => $sub['url'],
|
||||
'site_url' => $sub['source'],
|
||||
|
@ -171,4 +366,50 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
}
|
||||
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']);
|
||||
}
|
||||
// set the order of returned items
|
||||
if ($data['oldestFirst']) {
|
||||
$c->reverse(false);
|
||||
} else {
|
||||
$c->reverse(true);
|
||||
}
|
||||
$reverse = !$data['oldestFirst'];
|
||||
// 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 ($c->reverse) {
|
||||
if ($reverse) {
|
||||
$c->latestEdition($data['offset'] - 1);
|
||||
} else {
|
||||
$c->oldestEdition($data['offset'] + 1);
|
||||
|
@ -579,7 +575,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
"starred",
|
||||
"modified_date",
|
||||
"fingerprint",
|
||||
]);
|
||||
], [$reverse ? "edition desc" : "edition"]);
|
||||
} catch (ExceptionInput $e) {
|
||||
// ID of subscription or folder is not valid
|
||||
return new EmptyResponse(422);
|
||||
|
|
|
@ -8,11 +8,10 @@ namespace JKingWeb\Arsse\REST\TinyTinyRSS;
|
|||
|
||||
use JKingWeb\Arsse\Feed;
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Database;
|
||||
use JKingWeb\Arsse\User;
|
||||
use JKingWeb\Arsse\Service;
|
||||
use JKingWeb\Arsse\Misc\Date;
|
||||
use JKingWeb\Arsse\Database;
|
||||
use JKingWeb\Arsse\Context\Context;
|
||||
use JKingWeb\Arsse\Misc\Date;
|
||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
||||
use JKingWeb\Arsse\AbstractException;
|
||||
use JKingWeb\Arsse\ExceptionType;
|
||||
|
@ -1439,7 +1438,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
// no context needed here
|
||||
break;
|
||||
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;
|
||||
default:
|
||||
// any actual feed
|
||||
|
@ -1492,15 +1491,15 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
switch ($data['order_by']) {
|
||||
case "date_reverse":
|
||||
// sort oldest first
|
||||
$c->reverse(false);
|
||||
$order = ["edited_date"];
|
||||
break;
|
||||
case "feed_dates":
|
||||
// sort newest first
|
||||
$c->reverse(true);
|
||||
$order = ["edited_date desc"];
|
||||
break;
|
||||
default:
|
||||
// in TT-RSS the default sort order is unusual for some of the special feeds; we do not implement this
|
||||
$c->reverse(true);
|
||||
// sort most recently marked for special feeds, newest first otherwise
|
||||
$order = (!$cat && ($id == self::FEED_READ || $id == self::FEED_STARRED)) ? ["marked_date desc"] : ["edited_date desc"];
|
||||
break;
|
||||
}
|
||||
// set the limit and offset
|
||||
|
@ -1515,6 +1514,6 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
$c->oldestArticle($data['since_id'] + 1);
|
||||
}
|
||||
// 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
|
||||
if [ "$1" = "clean" ]; then
|
||||
"$base/vendor/bin/robo" "$roboCommand" $*
|
||||
"$base/vendor/bin/robo" "$roboCommand" "$@"
|
||||
else
|
||||
"$base/vendor/bin/robo" "$roboCommand" -- $*
|
||||
"$base/vendor/bin/robo" "$roboCommand" -- "$@"
|
||||
fi
|
||||
|
|
|
@ -10,6 +10,7 @@ use JKingWeb\Arsse\Database;
|
|||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Context\Context;
|
||||
use JKingWeb\Arsse\Misc\Date;
|
||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
||||
use Phake;
|
||||
|
||||
trait SeriesArticle {
|
||||
|
@ -424,10 +425,15 @@ trait SeriesArticle {
|
|||
return [
|
||||
'Blank context' => [new Context, [1,2,3,4,5,6,7,8,19,20]],
|
||||
'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]],
|
||||
'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]],
|
||||
'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]],
|
||||
'Multiple subscriptions' => [(new Context)->subscriptions([4,5]), [7,8,19,20]],
|
||||
'Unread' => [(new Context)->subscription(5)->unread(true), [20]],
|
||||
'Read' => [(new Context)->subscription(5)->unread(false), [19]],
|
||||
'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 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]],
|
||||
'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 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 "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]],
|
||||
'Edition ID 1001' => [(new Context)->edition(1001), [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)])), []],
|
||||
'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 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 "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 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 "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);
|
||||
}
|
||||
|
||||
/** @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() {
|
||||
Phake::when(Arsse::$user)->authorize->thenReturn(false);
|
||||
$this->assertException("notAuthorized", "User", "ExceptionAuthz");
|
||||
|
@ -783,11 +816,6 @@ trait SeriesArticle {
|
|||
$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() {
|
||||
$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);
|
||||
}
|
||||
|
||||
public function testMarkTooFewMultipleEditions() {
|
||||
$this->assertException("tooShort", "Db", "ExceptionInput");
|
||||
Arsse::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->editions([]));
|
||||
}
|
||||
|
||||
public function testMarkTooManyMultipleEditions() {
|
||||
$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);
|
||||
}
|
||||
|
||||
public function testSearchTooFewTerms() {
|
||||
/** @dataProvider provideArrayContextOptions */
|
||||
public function testUseTooFewValuesInArrayContext(string $option) {
|
||||
$this->assertException("tooShort", "Db", "ExceptionInput");
|
||||
Arsse::$db->articleList($this->user, (new Context)->searchTerms([]));
|
||||
Arsse::$db->articleList($this->user, (new Context)->$option([]));
|
||||
}
|
||||
|
||||
public function testSearchTooFewTermsInNote() {
|
||||
$this->assertException("tooShort", "Db", "ExceptionInput");
|
||||
Arsse::$db->articleList($this->user, (new Context)->annotationTerms([]));
|
||||
public function provideArrayContextOptions() {
|
||||
foreach ([
|
||||
"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,
|
||||
'offset' => 5,
|
||||
'folder' => 42,
|
||||
'folders' => [12,22],
|
||||
'folderShallow' => 42,
|
||||
'foldersShallow' => [0,1],
|
||||
'tag' => 44,
|
||||
'tags' => [44, 2112],
|
||||
'tagName' => "XLIV",
|
||||
'tagNames' => ["XLIV", "MMCXII"],
|
||||
'subscription' => 2112,
|
||||
'subscriptions' => [44, 2112],
|
||||
'article' => 255,
|
||||
'edition' => 65535,
|
||||
'latestArticle' => 47,
|
||||
|
@ -48,7 +53,9 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
'editions' => [1,2],
|
||||
'articles' => [1,2],
|
||||
'label' => 2112,
|
||||
'labels' => [2112, 1984],
|
||||
'labelName' => "Rush",
|
||||
'labelNames' => ["Rush", "Orwell"],
|
||||
'labelled' => true,
|
||||
'annotated' => true,
|
||||
'searchTerms' => ["foo", "bar"],
|
||||
|
@ -79,9 +86,19 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
}
|
||||
|
||||
public function testCleanIdArrayValues() {
|
||||
$methods = ["articles", "editions"];
|
||||
$in = [1, "2", 3.5, 3.0, "ook", 0, -20, true, false, null, new \DateTime(), -1.0];
|
||||
$out = [1,2, 3];
|
||||
$methods = ["articles", "editions", "tags", "labels", "subscriptions"];
|
||||
$in = [1, "2", 3.5, 4.0, 4, "ook", 0, -20, true, false, null, new \DateTime(), -1.0];
|
||||
$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;
|
||||
foreach ($methods as $method) {
|
||||
$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() {
|
||||
$methods = ["searchTerms", "annotationTerms", "titleTerms", "authorTerms"];
|
||||
$methods = ["searchTerms", "annotationTerms", "titleTerms", "authorTerms", "tagNames", "labelNames"];
|
||||
$now = new \DateTime;
|
||||
$in = [1, 3.0, "ook", 0, true, false, null, $now, ""];
|
||||
$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;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Conf;
|
||||
use JKingWeb\Arsse\User;
|
||||
use JKingWeb\Arsse\Database;
|
||||
use JKingWeb\Arsse\Service;
|
||||
use JKingWeb\Arsse\REST\Request;
|
||||
use JKingWeb\Arsse\Test\Result;
|
||||
use JKingWeb\Arsse\Misc\Date;
|
||||
use JKingWeb\Arsse\Context\Context;
|
||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||
use JKingWeb\Arsse\User\Exception as UserException;
|
||||
use JKingWeb\Arsse\Db\Transaction;
|
||||
use JKingWeb\Arsse\REST\Fever\API;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\ServerRequest;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\Diactoros\Response\XmlResponse;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
|
||||
/** @covers \JKingWeb\Arsse\REST\Fever\API<extended> */
|
||||
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) {
|
||||
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;
|
||||
$type = $type ?? "application/x-www-form-urlencoded";
|
||||
$server = [
|
||||
'REQUEST_METHOD' => $method,
|
||||
'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)) {
|
||||
parse_str($dataGet, $dataGet);
|
||||
}
|
||||
|
@ -45,9 +161,8 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
if (is_array($dataPost)) {
|
||||
$req = $req->withParsedBody($dataPost);
|
||||
} else {
|
||||
$body = $req->getBody();
|
||||
$body->write($dataPost);
|
||||
$req = $req->withBody($body);
|
||||
parse_str($dataPost, $arr);
|
||||
$req = $req->withParsedBody($arr);
|
||||
}
|
||||
if (isset($user)) {
|
||||
if (strlen($user)) {
|
||||
|
@ -56,7 +171,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
$req = $req->withAttribute("authenticationFailed", true);
|
||||
}
|
||||
}
|
||||
return $this->h->dispatch($req);
|
||||
return $req;
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
|
@ -95,7 +210,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
\Phake::when($this->h)->processRequest->thenReturnCallback(function($out, $G, $P) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -174,7 +289,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
['group_id' => 2, 'feed_ids' => "1,3"],
|
||||
],
|
||||
]);
|
||||
$act = $this->req("api&groups");
|
||||
$act = $this->h->dispatch($this->req("api&groups"));
|
||||
$this->assertMessage($exp, $act);
|
||||
}
|
||||
|
||||
|
@ -192,16 +307,208 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
]));
|
||||
$exp = new JsonResponse([
|
||||
'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' => 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' => [
|
||||
['group_id' => 1, 'feed_ids' => "1,2"],
|
||||
['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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -734,11 +734,11 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
['lastModified' => $t->getTimestamp()],
|
||||
['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(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)->reverse(true)->folder(2112), $this->anything())->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)->reverse(true)->folder(-1), $this->anything())->thenThrow(new ExceptionInput("typeViolation"));
|
||||
Phake::when(Arsse::$db)->articleList->thenReturn(new Result($this->v($this->articles['db'])));
|
||||
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)->folder(2112), $this->anything(), ["edition desc"])->thenThrow(new ExceptionInput("idMissing"));
|
||||
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)->folder(-1), $this->anything(), ["edition desc"])->thenThrow(new ExceptionInput("typeViolation"));
|
||||
$exp = new Response(['items' => $this->articles['rest']]);
|
||||
// check the contents of the response
|
||||
$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[11]));
|
||||
// perform method verifications
|
||||
Phake::verify(Arsse::$db, Phake::times(4))->articleList(Arsse::$user->id, (new Context)->reverse(true), $this->anything());
|
||||
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)->reverse(true)->folder(2112), $this->anything());
|
||||
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)->reverse(true)->folder(-1), $this->anything());
|
||||
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)->reverse(false)->limit(10)->oldestEdition(6), $this->anything()); // 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)->reverse(true)->unread(true), $this->anything());
|
||||
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, (new Context)->reverse(true)->limit(5), $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)->subscription(42), $this->anything(), ["edition desc"]);
|
||||
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)->subscription(-1), $this->anything(), ["edition desc"]);
|
||||
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)->starred(true), $this->anything(), ["edition desc"]);
|
||||
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)->limit(5)->latestEdition(4), $this->anything(), ["edition desc"]); // offset is one less than specified
|
||||
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)->markedSince($t), 2), $this->anything(), ["edition desc"]);
|
||||
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->limit(5), $this->anything(), ["edition desc"]);
|
||||
}
|
||||
|
||||
public function testMarkAFolderRead() {
|
||||
|
@ -958,6 +958,6 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
$url = "/items?type=2";
|
||||
Phake::when(Arsse::$db)->articleList->thenReturn(new Result([]));
|
||||
$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)->articleCount->thenReturn(0);
|
||||
Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1);
|
||||
$c = (new Context)->reverse(true);
|
||||
Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(2112), ["id"])->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(), (clone $c)->starred(true), ["id"])->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)->unread(true), ["id"])->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)->subscription(42)->starred(true), ["id"])->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)->limit(5), ["id"])->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)->limit(5)->offset(2), ["id"])->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]])));
|
||||
$c = (new Context);
|
||||
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"], ["edited_date desc"])->thenReturn(new Result($this->v($this->articles)));
|
||||
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"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 2]])));
|
||||
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"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 4]])));
|
||||
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"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 6]])));
|
||||
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"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 8]])));
|
||||
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"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 10]])));
|
||||
$out1 = [
|
||||
$this->respErr("INCORRECT_USAGE"),
|
||||
$this->respGood([]),
|
||||
|
@ -1793,9 +1793,9 @@ LONG_STRING;
|
|||
$this->assertMessage($out1[$a], $this->req($in1[$a]), "Test $a failed");
|
||||
}
|
||||
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(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"))->starred(true), 2), ["id"])->thenReturn(new Result($this->v([['id' => 1003]])));
|
||||
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"], ["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"], ["edited_date desc"])->thenReturn(new Result($this->v([['id' => 1003]])));
|
||||
$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)->articleCount->thenReturn(0);
|
||||
Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1);
|
||||
$c = (new Context)->limit(200)->reverse(true);
|
||||
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)->starred(true), $this->anything())->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)->unread(true), $this->anything())->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)->subscription(42)->starred(true), $this->anything())->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)->limit(5), $this->anything())->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)->limit(5)->offset(2), $this->anything())->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), $this->anything())->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)->folderShallow(0), $this->anything())->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)->folder(42), $this->anything())->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)->subscription(42)->searchTerms(["interesting"]), $this->anything())->thenReturn($this->generateHeadlines(17));
|
||||
$c = (new Context)->limit(200);
|
||||
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(), ["marked_date desc"])->thenReturn($this->generateHeadlines(1));
|
||||
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(), ["edited_date desc"])->thenReturn($this->generateHeadlines(3));
|
||||
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(), ["edited_date desc"])->thenReturn($this->generateHeadlines(5));
|
||||
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(), ["edited_date desc"])->thenReturn($this->generateHeadlines(7));
|
||||
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(), ["edited_date desc"])->thenReturn($this->generateHeadlines(9));
|
||||
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(), ["edited_date desc"])->thenReturn($this->generateHeadlines(11));
|
||||
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(), ["edited_date desc"])->thenReturn($this->generateHeadlines(13));
|
||||
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(), ["edited_date desc"])->thenReturn($this->generateHeadlines(15));
|
||||
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(), ["edited_date desc"])->thenReturn($this->generateHeadlines(17));
|
||||
$out2 = [
|
||||
$this->respErr("INCORRECT_USAGE"),
|
||||
$this->outputHeadlines(11),
|
||||
|
@ -1909,9 +1909,9 @@ LONG_STRING;
|
|||
$this->assertMessage($out2[$a], $this->req($in2[$a]), "Test $a failed");
|
||||
}
|
||||
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(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"))->starred(true), 2), $this->anything())->thenReturn($this->generateHeadlines(1003));
|
||||
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(), ["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(), ["edited_date desc"])->thenReturn($this->generateHeadlines(1003));
|
||||
$this->assertMessage($out3[$a], $this->req($in3[$a]), "Test $a failed");
|
||||
}
|
||||
}
|
||||
|
@ -1990,7 +1990,7 @@ LONG_STRING;
|
|||
]);
|
||||
$this->assertMessage($exp, $test);
|
||||
// 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]);
|
||||
$exp = $this->respGood([
|
||||
['id' => 2112, 'is_cat' => false, 'first_id' => 0],
|
||||
|
@ -2005,7 +2005,7 @@ LONG_STRING;
|
|||
]);
|
||||
$this->assertMessage($exp, $test);
|
||||
// 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]);
|
||||
$exp = $this->respGood([
|
||||
['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\ResponseInterface;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\Diactoros\Response\XmlResponse;
|
||||
|
||||
/** @coversNothing */
|
||||
abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
||||
|
@ -98,6 +99,8 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
|||
if ($exp instanceof JsonResponse) {
|
||||
$this->assertEquals($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 {
|
||||
$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",
|
||||
"version": "v4.3.1",
|
||||
"version": "v4.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "d50bbeeb0e17e6dd4124ea391eff235e932cbf64"
|
||||
"reference": "b592b26a24265a35172d8a2094d8b10f22b7cc39"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/d50bbeeb0e17e6dd4124ea391eff235e932cbf64",
|
||||
"reference": "d50bbeeb0e17e6dd4124ea391eff235e932cbf64",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/b592b26a24265a35172d8a2094d8b10f22b7cc39",
|
||||
"reference": "b592b26a24265a35172d8a2094d8b10f22b7cc39",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -593,20 +593,20 @@
|
|||
],
|
||||
"description": "Symfony Console Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-06-05T13:25:51+00:00"
|
||||
"time": "2019-06-13T11:03:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v4.3.1",
|
||||
"version": "v4.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
"reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f"
|
||||
"reference": "d257021c1ab28d48d24a16de79dfab445ce93398"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4e6c670af81c4fb0b6c08b035530a9915d0b691f",
|
||||
"reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d257021c1ab28d48d24a16de79dfab445ce93398",
|
||||
"reference": "d257021c1ab28d48d24a16de79dfab445ce93398",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -663,7 +663,7 @@
|
|||
],
|
||||
"description": "Symfony EventDispatcher Component",
|
||||
"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",
|
||||
|
@ -725,16 +725,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v4.3.1",
|
||||
"version": "v4.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf"
|
||||
"reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/bf2af40d738dec5e433faea7b00daa4431d0a4cf",
|
||||
"reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/b9896d034463ad6fd2bf17e2bf9418caecd6313d",
|
||||
"reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -771,20 +771,20 @@
|
|||
],
|
||||
"description": "Symfony Filesystem Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-06-03T20:27:40+00:00"
|
||||
"time": "2019-06-23T08:51:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v4.3.1",
|
||||
"version": "v4.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176"
|
||||
"reference": "33c21f7d5d3dc8a140c282854a7e13aeb5d0f91a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176",
|
||||
"reference": "b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/33c21f7d5d3dc8a140c282854a7e13aeb5d0f91a",
|
||||
"reference": "33c21f7d5d3dc8a140c282854a7e13aeb5d0f91a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -820,20 +820,20 @@
|
|||
],
|
||||
"description": "Symfony Finder Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-05-26T20:47:49+00:00"
|
||||
"time": "2019-06-13T11:03:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
"version": "v4.3.1",
|
||||
"version": "v4.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/options-resolver.git",
|
||||
"reference": "914e0edcb7cd0c9f494bc023b1d47534f4542332"
|
||||
"reference": "40762ead607c8f792ee4516881369ffa553fee6f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/914e0edcb7cd0c9f494bc023b1d47534f4542332",
|
||||
"reference": "914e0edcb7cd0c9f494bc023b1d47534f4542332",
|
||||
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/40762ead607c8f792ee4516881369ffa553fee6f",
|
||||
"reference": "40762ead607c8f792ee4516881369ffa553fee6f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -874,7 +874,7 @@
|
|||
"configuration",
|
||||
"options"
|
||||
],
|
||||
"time": "2019-05-10T05:38:46+00:00"
|
||||
"time": "2019-06-13T11:01:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
|
@ -1167,7 +1167,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v4.3.1",
|
||||
"version": "v4.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
|
@ -1274,7 +1274,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/stopwatch",
|
||||
"version": "v4.3.1",
|
||||
"version": "v4.3.2",
|
||||
"source": {
|
||||
"type": "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",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
|
||||
"reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18"
|
||||
"reference": "c4a66b97f040e3e20b3aa2a243230a1c3a9f7c8c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c99e3be9d3e85f60646f152f9002d46ed7770d18",
|
||||
"reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c4a66b97f040e3e20b3aa2a243230a1c3a9f7c8c",
|
||||
"reference": "c4a66b97f040e3e20b3aa2a243230a1c3a9f7c8c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -831,20 +831,20 @@
|
|||
"keywords": [
|
||||
"tokenizer"
|
||||
],
|
||||
"time": "2018-10-30T05:52:18+00:00"
|
||||
"time": "2019-07-08T05:24:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "7.5.13",
|
||||
"version": "7.5.14",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "b9278591caa8630127f96c63b598712b699e671c"
|
||||
"reference": "2834789aeb9ac182ad69bfdf9ae91856a59945ff"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b9278591caa8630127f96c63b598712b699e671c",
|
||||
"reference": "b9278591caa8630127f96c63b598712b699e671c",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2834789aeb9ac182ad69bfdf9ae91856a59945ff",
|
||||
"reference": "2834789aeb9ac182ad69bfdf9ae91856a59945ff",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -904,8 +904,8 @@
|
|||
"authors": [
|
||||
{
|
||||
"name": "Sebastian Bergmann",
|
||||
"email": "sebastian@phpunit.de",
|
||||
"role": "lead"
|
||||
"role": "lead",
|
||||
"email": "sebastian@phpunit.de"
|
||||
}
|
||||
],
|
||||
"description": "The PHP Unit Testing framework.",
|
||||
|
@ -915,7 +915,7 @@
|
|||
"testing",
|
||||
"xunit"
|
||||
],
|
||||
"time": "2019-06-19T12:01:51+00:00"
|
||||
"time": "2019-07-15T06:24:08+00:00"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"version": "v4.3.1",
|
||||
"version": "v4.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "d50bbeeb0e17e6dd4124ea391eff235e932cbf64"
|
||||
"reference": "b592b26a24265a35172d8a2094d8b10f22b7cc39"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/d50bbeeb0e17e6dd4124ea391eff235e932cbf64",
|
||||
"reference": "d50bbeeb0e17e6dd4124ea391eff235e932cbf64",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/b592b26a24265a35172d8a2094d8b10f22b7cc39",
|
||||
"reference": "b592b26a24265a35172d8a2094d8b10f22b7cc39",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1163,20 +1163,20 @@
|
|||
],
|
||||
"description": "Symfony Console Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-06-05T13:25:51+00:00"
|
||||
"time": "2019-06-13T11:03:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v4.3.1",
|
||||
"version": "v4.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
"reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f"
|
||||
"reference": "d257021c1ab28d48d24a16de79dfab445ce93398"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4e6c670af81c4fb0b6c08b035530a9915d0b691f",
|
||||
"reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d257021c1ab28d48d24a16de79dfab445ce93398",
|
||||
"reference": "d257021c1ab28d48d24a16de79dfab445ce93398",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1233,7 +1233,7 @@
|
|||
],
|
||||
"description": "Symfony EventDispatcher Component",
|
||||
"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",
|
||||
|
@ -1295,16 +1295,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v4.3.1",
|
||||
"version": "v4.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf"
|
||||
"reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/bf2af40d738dec5e433faea7b00daa4431d0a4cf",
|
||||
"reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/b9896d034463ad6fd2bf17e2bf9418caecd6313d",
|
||||
"reference": "b9896d034463ad6fd2bf17e2bf9418caecd6313d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1341,20 +1341,20 @@
|
|||
],
|
||||
"description": "Symfony Filesystem Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-06-03T20:27:40+00:00"
|
||||
"time": "2019-06-23T08:51:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v4.3.1",
|
||||
"version": "v4.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176"
|
||||
"reference": "33c21f7d5d3dc8a140c282854a7e13aeb5d0f91a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176",
|
||||
"reference": "b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/33c21f7d5d3dc8a140c282854a7e13aeb5d0f91a",
|
||||
"reference": "33c21f7d5d3dc8a140c282854a7e13aeb5d0f91a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1390,7 +1390,7 @@
|
|||
],
|
||||
"description": "Symfony Finder Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-05-26T20:47:49+00:00"
|
||||
"time": "2019-06-13T11:03:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
|
@ -1569,16 +1569,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v3.4.28",
|
||||
"version": "v3.4.29",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "afe411c2a6084f25cff55a01d0d4e1474c97ff13"
|
||||
"reference": "d129c017e8602507688ef2c3007951a16c1a8407"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/afe411c2a6084f25cff55a01d0d4e1474c97ff13",
|
||||
"reference": "afe411c2a6084f25cff55a01d0d4e1474c97ff13",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/d129c017e8602507688ef2c3007951a16c1a8407",
|
||||
"reference": "d129c017e8602507688ef2c3007951a16c1a8407",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1614,7 +1614,7 @@
|
|||
],
|
||||
"description": "Symfony Process Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-05-22T12:54:11+00:00"
|
||||
"time": "2019-05-30T15:47:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
|
@ -1676,7 +1676,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v4.3.1",
|
||||
"version": "v4.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/yaml.git",
|
||||
|
|
Loading…
Reference in a new issue