From b58a32646122450360ec3943b0e6e7e55983973d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 29 Oct 2020 11:58:45 -0400 Subject: [PATCH 001/366] Prepare for schema changes --- lib/Database.php | 2 +- sql/MySQL/6.sql | 6 ++++++ sql/PostgreSQL/6.sql | 7 +++++++ sql/SQLite3/6.sql | 8 ++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 sql/MySQL/6.sql create mode 100644 sql/PostgreSQL/6.sql create mode 100644 sql/SQLite3/6.sql diff --git a/lib/Database.php b/lib/Database.php index 6efda1b4..52d315c7 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -39,7 +39,7 @@ use JKingWeb\Arsse\Misc\URL; */ class Database { /** The version number of the latest schema the interface is aware of */ - public const SCHEMA_VERSION = 6; + public const SCHEMA_VERSION = 7; /** Makes tag/label association change operations remove members */ public const ASSOC_REMOVE = 0; /** Makes tag/label association change operations add members */ diff --git a/sql/MySQL/6.sql b/sql/MySQL/6.sql new file mode 100644 index 00000000..fff57671 --- /dev/null +++ b/sql/MySQL/6.sql @@ -0,0 +1,6 @@ +-- SPDX-License-Identifier: MIT +-- Copyright 2017 J. King, Dustin Wilson et al. +-- See LICENSE and AUTHORS files for details + + +update arsse_meta set value = '7' where "key" = 'schema_version'; diff --git a/sql/PostgreSQL/6.sql b/sql/PostgreSQL/6.sql new file mode 100644 index 00000000..4d86e98c --- /dev/null +++ b/sql/PostgreSQL/6.sql @@ -0,0 +1,7 @@ +-- SPDX-License-Identifier: MIT +-- Copyright 2017 J. King, Dustin Wilson et al. +-- See LICENSE and AUTHORS files for details + + + +update arsse_meta set value = '7' where "key" = 'schema_version'; diff --git a/sql/SQLite3/6.sql b/sql/SQLite3/6.sql new file mode 100644 index 00000000..7f74ee29 --- /dev/null +++ b/sql/SQLite3/6.sql @@ -0,0 +1,8 @@ +-- SPDX-License-Identifier: MIT +-- Copyright 2017 J. King, Dustin Wilson et al. +-- See LICENSE and AUTHORS files for details + + +-- set version marker +pragma user_version = 7; +update arsse_meta set value = '7' where "key" = 'schema_version'; From 3ac010d5b691e217750d35d9b73db1a3508b443e Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 30 Oct 2020 12:16:03 -0400 Subject: [PATCH 002/366] Fix tests in absence of database extensions --- tests/cases/Db/SQLite3/TestDriver.php | 6 ++++-- tests/cases/Db/SQLite3/TestResult.php | 6 ++++-- tests/cases/Db/SQLite3/TestStatement.php | 6 ++++-- tests/cases/Db/SQLite3/TestUpdate.php | 6 ++++-- tests/lib/DatabaseDrivers/MySQL.php | 5 ++++- tests/lib/DatabaseDrivers/PostgreSQL.php | 2 +- 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/cases/Db/SQLite3/TestDriver.php b/tests/cases/Db/SQLite3/TestDriver.php index 4c80cbad..b3eb3593 100644 --- a/tests/cases/Db/SQLite3/TestDriver.php +++ b/tests/cases/Db/SQLite3/TestDriver.php @@ -26,8 +26,10 @@ class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { } public static function tearDownAfterClass(): void { - static::$interface->close(); - static::$interface = null; + if (static::$interface) { + static::$interface->close(); + static::$interface = null; + } parent::tearDownAfterClass(); @unlink(static::$file); static::$file = null; diff --git a/tests/cases/Db/SQLite3/TestResult.php b/tests/cases/Db/SQLite3/TestResult.php index d7f8c091..5a8d0cd6 100644 --- a/tests/cases/Db/SQLite3/TestResult.php +++ b/tests/cases/Db/SQLite3/TestResult.php @@ -16,8 +16,10 @@ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { protected static $createTest = "CREATE TABLE arsse_test(id integer primary key)"; public static function tearDownAfterClass(): void { - static::$interface->close(); - static::$interface = null; + if (static::$interface) { + static::$interface->close(); + static::$interface = null; + } parent::tearDownAfterClass(); } diff --git a/tests/cases/Db/SQLite3/TestStatement.php b/tests/cases/Db/SQLite3/TestStatement.php index 1af5be4c..f7b970f2 100644 --- a/tests/cases/Db/SQLite3/TestStatement.php +++ b/tests/cases/Db/SQLite3/TestStatement.php @@ -13,8 +13,10 @@ class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { use \JKingWeb\Arsse\Test\DatabaseDrivers\SQLite3; public static function tearDownAfterClass(): void { - static::$interface->close(); - static::$interface = null; + if (static::$interface) { + static::$interface->close(); + static::$interface = null; + } parent::tearDownAfterClass(); } diff --git a/tests/cases/Db/SQLite3/TestUpdate.php b/tests/cases/Db/SQLite3/TestUpdate.php index 94842e25..409f1091 100644 --- a/tests/cases/Db/SQLite3/TestUpdate.php +++ b/tests/cases/Db/SQLite3/TestUpdate.php @@ -16,8 +16,10 @@ class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate { protected static $minimal2 = "pragma user_version=2"; public static function tearDownAfterClass(): void { - static::$interface->close(); - static::$interface = null; + if (static::$interface) { + static::$interface->close(); + static::$interface = null; + } parent::tearDownAfterClass(); } } diff --git a/tests/lib/DatabaseDrivers/MySQL.php b/tests/lib/DatabaseDrivers/MySQL.php index f1571a2b..01501f15 100644 --- a/tests/lib/DatabaseDrivers/MySQL.php +++ b/tests/lib/DatabaseDrivers/MySQL.php @@ -18,9 +18,12 @@ trait MySQL { protected static $stringOutput = true; public static function dbInterface() { + if (!class_exists("mysqli")) { + return null; + } $d = @new \mysqli(Arsse::$conf->dbMySQLHost, Arsse::$conf->dbMySQLUser, Arsse::$conf->dbMySQLPass, Arsse::$conf->dbMySQLDb, Arsse::$conf->dbMySQLPort); if ($d->connect_errno) { - return; + return null; } $d->set_charset("utf8mb4"); foreach (\JKingWeb\Arsse\Db\MySQL\PDODriver::makeSetupQueries() as $q) { diff --git a/tests/lib/DatabaseDrivers/PostgreSQL.php b/tests/lib/DatabaseDrivers/PostgreSQL.php index fb0038cb..edc75493 100644 --- a/tests/lib/DatabaseDrivers/PostgreSQL.php +++ b/tests/lib/DatabaseDrivers/PostgreSQL.php @@ -19,7 +19,7 @@ trait PostgreSQL { public static function dbInterface() { $connString = \JKingWeb\Arsse\Db\PostgreSQL\Driver::makeConnectionString(false, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, Arsse::$conf->dbPostgreSQLDb, Arsse::$conf->dbPostgreSQLHost, Arsse::$conf->dbPostgreSQLPort, ""); - if ($d = @pg_connect($connString, \PGSQL_CONNECT_FORCE_NEW)) { + if (function_exists("pg_connect") && $d = @pg_connect($connString, \PGSQL_CONNECT_FORCE_NEW)) { foreach (\JKingWeb\Arsse\Db\PostgreSQL\Driver::makeSetupQueries(Arsse::$conf->dbPostgreSQLSchema) as $q) { pg_query($d, $q); } From 4db1b95cf43abaeb8c80643cf08bd99c9a81ab73 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 30 Oct 2020 15:25:22 -0400 Subject: [PATCH 003/366] Add numeric IDs and other Miniflux data to SQLite schema --- lib/Database.php | 4 ++-- sql/SQLite3/6.sql | 24 +++++++++++++++++++ tests/cases/Database/SeriesArticle.php | 9 +++---- tests/cases/Database/SeriesCleanup.php | 5 ++-- tests/cases/Database/SeriesFeed.php | 5 ++-- tests/cases/Database/SeriesFolder.php | 5 ++-- tests/cases/Database/SeriesLabel.php | 9 +++---- tests/cases/Database/SeriesSession.php | 5 ++-- tests/cases/Database/SeriesSubscription.php | 5 ++-- tests/cases/Database/SeriesTag.php | 9 +++---- tests/cases/Database/SeriesToken.php | 5 ++-- tests/cases/Database/SeriesUser.php | 7 +++--- tests/cases/Db/BaseUpdate.php | 18 ++++++++++++++ tests/cases/ImportExport/TestImportExport.php | 5 ++-- 14 files changed, 84 insertions(+), 31 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 52d315c7..6c038ab5 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -35,7 +35,7 @@ use JKingWeb\Arsse\Misc\URL; * deletes a user from the database, and labelArticlesSet() changes a label's * associations with articles. There has been an effort to keep public method * names consistent throughout, but protected methods, having different - * concerns, will typicsally follow different conventions. + * concerns, will typically follow different conventions. */ class Database { /** The version number of the latest schema the interface is aware of */ @@ -256,7 +256,7 @@ class Database { throw new User\Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]); } $hash = (strlen($password) > 0) ? password_hash($password, \PASSWORD_DEFAULT) : ""; - $this->db->prepare("INSERT INTO arsse_users(id,password) values(?,?)", "str", "str")->runArray([$user,$hash]); + $this->db->prepare("INSERT INTO arsse_users(id,password,num) values(?, ?, coalesce((select max(num) from arsse_users), 0) + 1)", "str", "str")->runArray([$user,$hash]); return true; } diff --git a/sql/SQLite3/6.sql b/sql/SQLite3/6.sql index 7f74ee29..142fada0 100644 --- a/sql/SQLite3/6.sql +++ b/sql/SQLite3/6.sql @@ -2,6 +2,30 @@ -- Copyright 2017 J. King, Dustin Wilson et al. -- See LICENSE and AUTHORS files for details +-- Add multiple columns to the users table +-- In particular this adds a numeric identifier for each user, which Miniflux requires +create table arsse_users_new( +-- users + id text primary key not null collate nocase, -- user id + password text, -- password, salted and hashed; if using external authentication this would be blank + num integer unique not null, -- numeric identfier used by Miniflux + admin boolean not null default 0, -- Whether the user is an administrator + lang text, -- The user's chosen language code e.g. 'en', 'fr-ca'; null uses the system default + tz text not null default 'Etc/UTC', -- The user's chosen time zone, in zoneinfo format + sort_asc boolean not null default 0 -- Whether the user prefers to sort articles in ascending order +) without rowid; +create temp table arsse_users_existing( + id text not null, + num integer primary key +); +insert into arsse_users_existing(id) select id from arsse_users; +insert into arsse_users_new(id, password, num) + select id, password, num + from arsse_users + join arsse_users_existing using(id); +drop table arsse_users; +drop table arsse_users_existing; +alter table arsse_users_new rename to arsse_users; -- set version marker pragma user_version = 7; diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 2f78e9c1..a9354c71 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -19,12 +19,13 @@ trait SeriesArticle { 'columns' => [ 'id' => 'str', 'password' => 'str', + 'num' => 'int', ], 'rows' => [ - ["jane.doe@example.com", ""], - ["john.doe@example.com", ""], - ["john.doe@example.org", ""], - ["john.doe@example.net", ""], + ["jane.doe@example.com", "",1], + ["john.doe@example.com", "",2], + ["john.doe@example.org", "",3], + ["john.doe@example.net", "",4], ], ], 'arsse_feeds' => [ diff --git a/tests/cases/Database/SeriesCleanup.php b/tests/cases/Database/SeriesCleanup.php index ad40dcb3..b31f87c7 100644 --- a/tests/cases/Database/SeriesCleanup.php +++ b/tests/cases/Database/SeriesCleanup.php @@ -30,10 +30,11 @@ trait SeriesCleanup { 'columns' => [ 'id' => 'str', 'password' => 'str', + 'num' => 'int', ], 'rows' => [ - ["jane.doe@example.com", ""], - ["john.doe@example.com", ""], + ["jane.doe@example.com", "",1], + ["john.doe@example.com", "",2], ], ], 'arsse_sessions' => [ diff --git a/tests/cases/Database/SeriesFeed.php b/tests/cases/Database/SeriesFeed.php index d4a75213..1eb23bb0 100644 --- a/tests/cases/Database/SeriesFeed.php +++ b/tests/cases/Database/SeriesFeed.php @@ -19,10 +19,11 @@ trait SeriesFeed { 'columns' => [ 'id' => 'str', 'password' => 'str', + 'num' => 'int', ], 'rows' => [ - ["jane.doe@example.com", ""], - ["john.doe@example.com", ""], + ["jane.doe@example.com", "",1], + ["john.doe@example.com", "",2], ], ], 'arsse_feeds' => [ diff --git a/tests/cases/Database/SeriesFolder.php b/tests/cases/Database/SeriesFolder.php index 6d69f64a..98d12d7b 100644 --- a/tests/cases/Database/SeriesFolder.php +++ b/tests/cases/Database/SeriesFolder.php @@ -15,10 +15,11 @@ trait SeriesFolder { 'columns' => [ 'id' => 'str', 'password' => 'str', + 'num' => 'int', ], 'rows' => [ - ["jane.doe@example.com", ""], - ["john.doe@example.com", ""], + ["jane.doe@example.com", "",1], + ["john.doe@example.com", "",2], ], ], 'arsse_folders' => [ diff --git a/tests/cases/Database/SeriesLabel.php b/tests/cases/Database/SeriesLabel.php index db9c4989..d66dcdb0 100644 --- a/tests/cases/Database/SeriesLabel.php +++ b/tests/cases/Database/SeriesLabel.php @@ -17,12 +17,13 @@ trait SeriesLabel { 'columns' => [ 'id' => 'str', 'password' => 'str', + 'num' => 'int', ], 'rows' => [ - ["jane.doe@example.com", ""], - ["john.doe@example.com", ""], - ["john.doe@example.org", ""], - ["john.doe@example.net", ""], + ["jane.doe@example.com", "",1], + ["john.doe@example.com", "",2], + ["john.doe@example.org", "",3], + ["john.doe@example.net", "",4], ], ], 'arsse_folders' => [ diff --git a/tests/cases/Database/SeriesSession.php b/tests/cases/Database/SeriesSession.php index 9a354f66..163d8bf8 100644 --- a/tests/cases/Database/SeriesSession.php +++ b/tests/cases/Database/SeriesSession.php @@ -26,10 +26,11 @@ trait SeriesSession { 'columns' => [ 'id' => 'str', 'password' => 'str', + 'num' => 'int', ], 'rows' => [ - ["jane.doe@example.com", ""], - ["john.doe@example.com", ""], + ["jane.doe@example.com", "",1], + ["john.doe@example.com", "",2], ], ], 'arsse_sessions' => [ diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index d8614e24..c0a88f4a 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -18,10 +18,11 @@ trait SeriesSubscription { 'columns' => [ 'id' => 'str', 'password' => 'str', + 'num' => 'int', ], 'rows' => [ - ["jane.doe@example.com", ""], - ["john.doe@example.com", ""], + ["jane.doe@example.com", "",1], + ["john.doe@example.com", "",2], ], ], 'arsse_folders' => [ diff --git a/tests/cases/Database/SeriesTag.php b/tests/cases/Database/SeriesTag.php index f6a3f4ea..3c4b4ac8 100644 --- a/tests/cases/Database/SeriesTag.php +++ b/tests/cases/Database/SeriesTag.php @@ -16,12 +16,13 @@ trait SeriesTag { 'columns' => [ 'id' => 'str', 'password' => 'str', + 'num' => 'int', ], 'rows' => [ - ["jane.doe@example.com", ""], - ["john.doe@example.com", ""], - ["john.doe@example.org", ""], - ["john.doe@example.net", ""], + ["jane.doe@example.com", "",1], + ["john.doe@example.com", "",2], + ["john.doe@example.org", "",3], + ["john.doe@example.net", "",4], ], ], 'arsse_feeds' => [ diff --git a/tests/cases/Database/SeriesToken.php b/tests/cases/Database/SeriesToken.php index aad4a875..267be38e 100644 --- a/tests/cases/Database/SeriesToken.php +++ b/tests/cases/Database/SeriesToken.php @@ -20,10 +20,11 @@ trait SeriesToken { 'columns' => [ 'id' => 'str', 'password' => 'str', + 'num' => 'int', ], 'rows' => [ - ["jane.doe@example.com", ""], - ["john.doe@example.com", ""], + ["jane.doe@example.com", "",1], + ["john.doe@example.com", "",2], ], ], 'arsse_tokens' => [ diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index 54376600..9b97fd80 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -15,11 +15,12 @@ trait SeriesUser { 'columns' => [ 'id' => 'str', 'password' => 'str', + 'num' => 'int', ], 'rows' => [ - ["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW'], // password is hash of "secret" - ["jane.doe@example.com", ""], - ["john.doe@example.com", ""], + ["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW',1], // password is hash of "secret" + ["jane.doe@example.com", "",2], + ["john.doe@example.com", "",3], ], ], ]; diff --git a/tests/cases/Db/BaseUpdate.php b/tests/cases/Db/BaseUpdate.php index d5415134..b25e4723 100644 --- a/tests/cases/Db/BaseUpdate.php +++ b/tests/cases/Db/BaseUpdate.php @@ -134,4 +134,22 @@ class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest { $this->drv->schemaUpdate(Database::SCHEMA_VERSION); $this->assertTrue($this->drv->maintenance()); } + + public function testUpdateTo7(): void { + $this->drv->schemaUpdate(6); + $this->drv->exec(<<drv->schemaUpdate(7); + $exp = [ + ['id' => "a", 'password' => "xyz", 'num' => 1], + ['id' => "b", 'password' => "abc", 'num' => 2], + ]; + $this->assertEquals($exp, $this->drv->query("SELECT id, password, num from arsse_users")->getAll()); + $this->assertSame(2, (int) $this->drv->query("SELECT count(*) from arsse_folders")->getValue()); + } } diff --git a/tests/cases/ImportExport/TestImportExport.php b/tests/cases/ImportExport/TestImportExport.php index 4d3fef30..af0b0fe0 100644 --- a/tests/cases/ImportExport/TestImportExport.php +++ b/tests/cases/ImportExport/TestImportExport.php @@ -46,10 +46,11 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest { 'columns' => [ 'id' => 'str', 'password' => 'str', + 'num' => 'int', ], 'rows' => [ - ["john.doe@example.com", ""], - ["jane.doe@example.com", ""], + ["john.doe@example.com", "", 1], + ["jane.doe@example.com", "", 2], ], ], 'arsse_folders' => [ From 16d2e016687f1d882630048f9e699d5717cf464c Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 30 Oct 2020 19:00:11 -0400 Subject: [PATCH 004/366] New schema for PostgreSQL and MySQL --- lib/Database.php | 3 ++- sql/MySQL/6.sql | 15 +++++++++++++++ sql/PostgreSQL/6.sql | 17 ++++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 6c038ab5..b7b44881 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -256,7 +256,8 @@ class Database { throw new User\Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]); } $hash = (strlen($password) > 0) ? password_hash($password, \PASSWORD_DEFAULT) : ""; - $this->db->prepare("INSERT INTO arsse_users(id,password,num) values(?, ?, coalesce((select max(num) from arsse_users), 0) + 1)", "str", "str")->runArray([$user,$hash]); + // NOTE: This roundabout construction (with 'select' rather than 'values') is required by MySQL, because MySQL is riddled with pitfalls and exceptions + $this->db->prepare("INSERT INTO arsse_users(id,password,num) select ?, ?, ((select max(num) from arsse_users) + 1)", "str", "str")->runArray([$user,$hash]); return true; } diff --git a/sql/MySQL/6.sql b/sql/MySQL/6.sql index fff57671..e16375cc 100644 --- a/sql/MySQL/6.sql +++ b/sql/MySQL/6.sql @@ -2,5 +2,20 @@ -- Copyright 2017 J. King, Dustin Wilson et al. -- See LICENSE and AUTHORS files for details +alter table arsse_users add column num bigint unsigned unique; +alter table arsse_users add column admin boolean not null default 0; +alter table arsse_users add column lang longtext; +alter table arsse_users add column tz varchar(44) not null default 'Etc/UTC'; +alter table arsse_users add column soort_asc boolean not null default 0; +create temporary table arsse_users_existing( + id text not null, + num serial primary key +) character set utf8mb4 collate utf8mb4_unicode_ci; +insert into arsse_users_existing(id) select id from arsse_users; +update arsse_users as u, arsse_users_existing as n + set u.num = n.num +where u.id = n.id; +drop table arsse_users_existing; +alter table arsse_users modify num bigint unsigned not null; update arsse_meta set value = '7' where "key" = 'schema_version'; diff --git a/sql/PostgreSQL/6.sql b/sql/PostgreSQL/6.sql index 4d86e98c..6099e8d5 100644 --- a/sql/PostgreSQL/6.sql +++ b/sql/PostgreSQL/6.sql @@ -2,6 +2,21 @@ -- Copyright 2017 J. King, Dustin Wilson et al. -- See LICENSE and AUTHORS files for details - +alter table arsse_users add column num bigint unique; +alter table arsse_users add column admin smallint not null default 0; +alter table arsse_users add column lang text; +alter table arsse_users add column tz text not null default 'Etc/UTC'; +alter table arsse_users add column soort_asc smallint not null default 0; +create temp table arsse_users_existing( + id text not null, + num bigserial +); +insert into arsse_users_existing(id) select id from arsse_users; +update arsse_users as u + set num = e.num +from arsse_users_existing as e +where u.id = e.id; +drop table arsse_users_existing; +alter table arsse_users alter column num set not null; update arsse_meta set value = '7' where "key" = 'schema_version'; From 8ad7fc81a89fc343344a3ee3f9bc69d27f2e005c Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 31 Oct 2020 21:26:11 -0400 Subject: [PATCH 005/366] Initially mapping out of Miniflux API --- lib/REST.php | 6 +- lib/REST/Miniflux/V1.php | 120 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 lib/REST/Miniflux/V1.php diff --git a/lib/REST.php b/lib/REST.php index 41fdaa1f..0d04be93 100644 --- a/lib/REST.php +++ b/lib/REST.php @@ -40,6 +40,11 @@ class REST { 'strip' => '/fever/', 'class' => REST\Fever\API::class, ], + 'miniflux' => [ // Miniflux https://miniflux.app/docs/api.html + 'match' => '/v1/', + 'strip' => '/v1', + 'class' => REST\Miniflux\API::class, + ], // Other candidates: // Microsub https://indieweb.org/Microsub // Google Reader http://feedhq.readthedocs.io/en/latest/api/index.html @@ -48,7 +53,6 @@ class REST { // Selfoss https://github.com/SSilence/selfoss/wiki/Restful-API-for-Apps-or-any-other-external-access // NewsBlur http://www.newsblur.com/api // Unclear if clients exist: - // Miniflux https://docs.miniflux.app/en/latest/api.html#api-reference // Nextcloud News v2 https://github.com/nextcloud/news/blob/master/docs/externalapi/External-Api.md // BirdReader https://github.com/glynnbird/birdreader/blob/master/API.md // Feedbin v1 https://github.com/feedbin/feedbin-api/commit/86da10aac5f1a57531a6e17b08744e5f9e7db8a9 diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php new file mode 100644 index 00000000..8fd1dc47 --- /dev/null +++ b/lib/REST/Miniflux/V1.php @@ -0,0 +1,120 @@ + ['GET' => "getCategories", 'POST' => "createCategory"], + '/categories/1' => ['PUT' => "updateCategory", 'DELETE' => "deleteCategory"], + '/discover' => ['POST' => "discoverSubscriptions"], + '/entries' => ['GET' => "getEntries", 'PUT' => "updateEntries"], + '/entries/1' => ['GET' => "getEntry"], + '/entries/1/bookmark' => ['PUT' => "toggleEntryBookmark"], + '/export' => ['GET' => "opmlExport"], + '/feeds' => ['GET' => "getFeeds", 'POST' => "createFeed"], + '/feeds/1' => ['GET' => "getFeed", 'PUT' => "updateFeed", 'DELETE' => "removeFeed"], + '/feeds/1/entries/1' => ['GET' => "getFeedEntry"], + '/feeds/1/entries' => ['GET' => "getFeedEntries"], + '/feeds/1/icon' => ['GET' => "getFeedIcon"], + '/feeds/1/refresh' => ['PUT' => "refreshFeed"], + '/feeds/refresh' => ['PUT' => "refreshAllFeeds"], + '/healthcheck' => ['GET' => "healthCheck"], + '/import' => ['POST' => "opmlImport"], + '/me' => ['GET' => "getCurrentUser"], + '/users' => ['GET' => "getUsers", 'POST' => "createUser"], + '/users/1' => ['GET' => "getUser", 'PUT' => "updateUser", 'DELETE' => "deleteUser"], + '/users/*' => ['GET' => "getUser"], + '/version' => ['GET' => "getVersion"], + ]; + + public function __construct() { + } + + public function dispatch(ServerRequestInterface $req): ResponseInterface { + // try to authenticate + if ($req->getAttribute("authenticated", false)) { + Arsse::$user->id = $req->getAttribute("authenticatedUser"); + } else { + return new EmptyResponse(401); + } + // get the request path only; this is assumed to already be normalized + $target = parse_url($req->getRequestTarget())['path'] ?? ""; + // handle HTTP OPTIONS requests + if ($req->getMethod() === "OPTIONS") { + return $this->handleHTTPOptions($target); + } + } + + protected function normalizePathIds(string $url): string { + $path = explode("/", $url); + // any path components which are database IDs (integers greater than zero) should be replaced with "1", for easier comparison (we don't care about the specific ID) + for ($a = 0; $a < sizeof($path); $a++) { + if (ValueInfo::id($path[$a])) { + $path[$a] = "1"; + } + } + return implode("/", $path); + } + + protected function handleHTTPOptions(string $url): ResponseInterface { + // normalize the URL path: change any IDs to 1 for easier comparison + $url = $this->normalizePathIDs($url); + if (isset($this->paths[$url])) { + // if the path is supported, respond with the allowed methods and other metadata + $allowed = array_keys($this->paths[$url]); + // if GET is allowed, so is HEAD + if (in_array("GET", $allowed)) { + array_unshift($allowed, "HEAD"); + } + return new EmptyResponse(204, [ + 'Allow' => implode(",", $allowed), + 'Accept' => self::ACCEPTED_TYPE, + ]); + } else { + // if the path is not supported, return 404 + return new EmptyResponse(404); + } + } + + protected function chooseCall(string $url, string $method): string { + // // normalize the URL path: change any IDs to 1 for easier comparison + $url = $this->normalizePathIds($url); + // normalize the HTTP method to uppercase + $method = strtoupper($method); + // we now evaluate the supplied URL against every supported path for the selected scope + // the URL is evaluated as an array so as to avoid decoded escapes turning invalid URLs into valid ones + if (isset($this->paths[$url])) { + // if the path is supported, make sure the method is allowed + if (isset($this->paths[$url][$method])) { + // if it is allowed, return the object method to run + return $this->paths[$url][$method]; + } else { + // otherwise return 405 + throw new Exception405(implode(", ", array_keys($this->paths[$url]))); + } + } else { + // if the path is not supported, return 404 + throw new Exception404(); + } + } +} From 905f8938e221d2cdd8af6135a2247c77770b51b2 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 1 Nov 2020 09:37:59 -0500 Subject: [PATCH 006/366] Typo --- sql/MySQL/6.sql | 2 +- sql/PostgreSQL/6.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sql/MySQL/6.sql b/sql/MySQL/6.sql index e16375cc..248a0141 100644 --- a/sql/MySQL/6.sql +++ b/sql/MySQL/6.sql @@ -6,7 +6,7 @@ alter table arsse_users add column num bigint unsigned unique; alter table arsse_users add column admin boolean not null default 0; alter table arsse_users add column lang longtext; alter table arsse_users add column tz varchar(44) not null default 'Etc/UTC'; -alter table arsse_users add column soort_asc boolean not null default 0; +alter table arsse_users add column sort_asc boolean not null default 0; create temporary table arsse_users_existing( id text not null, num serial primary key diff --git a/sql/PostgreSQL/6.sql b/sql/PostgreSQL/6.sql index 6099e8d5..bc365704 100644 --- a/sql/PostgreSQL/6.sql +++ b/sql/PostgreSQL/6.sql @@ -6,7 +6,7 @@ alter table arsse_users add column num bigint unique; alter table arsse_users add column admin smallint not null default 0; alter table arsse_users add column lang text; alter table arsse_users add column tz text not null default 'Etc/UTC'; -alter table arsse_users add column soort_asc smallint not null default 0; +alter table arsse_users add column sort_asc smallint not null default 0; create temp table arsse_users_existing( id text not null, num bigserial From c92bb12a116ccf65b85a420bdd26640b7aa9fc68 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 1 Nov 2020 19:09:17 -0500 Subject: [PATCH 007/366] Prototype Miniflux dispatcher --- lib/REST.php | 2 +- lib/REST/Miniflux/V1.php | 50 ++++++++++++++++++++++++++++++--- lib/REST/NextcloudNews/V1_2.php | 20 ++++++------- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/lib/REST.php b/lib/REST.php index 0d04be93..011d27df 100644 --- a/lib/REST.php +++ b/lib/REST.php @@ -43,7 +43,7 @@ class REST { 'miniflux' => [ // Miniflux https://miniflux.app/docs/api.html 'match' => '/v1/', 'strip' => '/v1', - 'class' => REST\Miniflux\API::class, + 'class' => REST\Miniflux\V1::class, ], // Other candidates: // Microsub https://indieweb.org/Microsub diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 8fd1dc47..9edff158 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -23,6 +23,8 @@ use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\EmptyResponse; class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { + protected const ACCEPTED_TYPES_OPML = ["text/xml", "application/xml", "text/x-opml"]; + protected const ACCEPTED_TYPES_JSON = ["application/json", "text/json"]; protected $paths = [ '/categories' => ['GET' => "getCategories", 'POST' => "createCategory"], '/categories/1' => ['PUT' => "updateCategory", 'DELETE' => "deleteCategory"], @@ -55,14 +57,46 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if ($req->getAttribute("authenticated", false)) { Arsse::$user->id = $req->getAttribute("authenticatedUser"); } else { + // TODO: Handle X-Auth-Token authentication return new EmptyResponse(401); } // get the request path only; this is assumed to already be normalized $target = parse_url($req->getRequestTarget())['path'] ?? ""; + $method = $req->getMethod(); // handle HTTP OPTIONS requests - if ($req->getMethod() === "OPTIONS") { + if ($method === "OPTIONS") { return $this->handleHTTPOptions($target); } + $func = $this->chooseCall($target, $method); + if ($func === "opmlImport") { + if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) { + return new EmptyResponse(415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]); + } + $data = (string) $req->getBody(); + } elseif ($method === "POST" || $method === "PUT") { + if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_JSON])) { + return new EmptyResponse(415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_JSON)]); + } + $data = @json_decode($data, true); + if (json_last_error() !== \JSON_ERROR_NONE) { + // if the body could not be parsed as JSON, return "400 Bad Request" + return new EmptyResponse(400); + } + } else { + $data = null; + } + try { + $path = explode("/", ltrim($target, "/")); + return $this->$func($path, $req->getQueryParams(), $data); + // @codeCoverageIgnoreStart + } catch (Exception $e) { + // if there was a REST exception return 400 + return new EmptyResponse(400); + } catch (AbstractException $e) { + // if there was any other Arsse exception return 500 + return new EmptyResponse(500); + } + // @codeCoverageIgnoreEnd } protected function normalizePathIds(string $url): string { @@ -73,6 +107,10 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $path[$a] = "1"; } } + // handle special case "Get User By User Name", which can have any non-numeric string, non-empty as the last component + if (sizeof($path) === 3 && $path[0] === "" && $path[1] === "users" && !preg_match("/^(?:\d+)?$/", $path[2])) { + $path[2] = "*"; + } return implode("/", $path); } @@ -88,7 +126,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } return new EmptyResponse(204, [ 'Allow' => implode(",", $allowed), - 'Accept' => self::ACCEPTED_TYPE, + 'Accept' => implode(", ", $url === "/import" ? self::ACCEPTED_TYPES_OPML : self::ACCEPTED_TYPES_JSON), ]); } else { // if the path is not supported, return 404 @@ -106,8 +144,12 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if (isset($this->paths[$url])) { // if the path is supported, make sure the method is allowed if (isset($this->paths[$url][$method])) { - // if it is allowed, return the object method to run - return $this->paths[$url][$method]; + // if it is allowed, return the object method to run, assuming the method exists + if (method_exists($this, $this->paths[$url][$method])) { + return $this->paths[$url][$method]; + } else { + throw new Exception501(); // @codeCoverageIgnore + } } else { // otherwise return 405 throw new Exception405(implode(", ", array_keys($this->paths[$url]))); diff --git a/lib/REST/NextcloudNews/V1_2.php b/lib/REST/NextcloudNews/V1_2.php index c7389df8..4741e839 100644 --- a/lib/REST/NextcloudNews/V1_2.php +++ b/lib/REST/NextcloudNews/V1_2.php @@ -17,6 +17,7 @@ use JKingWeb\Arsse\Misc\HTTP; use JKingWeb\Arsse\REST\Exception; use JKingWeb\Arsse\REST\Exception404; use JKingWeb\Arsse\REST\Exception405; +use JKingWeb\Arsse\REST\Exception501; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\JsonResponse as Response; @@ -109,20 +110,15 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // merge GET and POST data, and normalize it. POST parameters are preferred over GET parameters $data = $this->normalizeInput(array_merge($req->getQueryParams(), $data), $this->validInput, "unix"); // check to make sure the requested function is implemented + // dispatch try { $func = $this->chooseCall($target, $req->getMethod()); + $path = explode("/", ltrim($target, "/")); + return $this->$func($path, $data); } catch (Exception404 $e) { return new EmptyResponse(404); } catch (Exception405 $e) { return new EmptyResponse(405, ['Allow' => $e->getMessage()]); - } - if (!method_exists($this, $func)) { - return new EmptyResponse(501); // @codeCoverageIgnore - } - // dispatch - try { - $path = explode("/", ltrim($target, "/")); - return $this->$func($path, $data); // @codeCoverageIgnoreStart } catch (Exception $e) { // if there was a REST exception return 400 @@ -155,8 +151,12 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { if (isset($this->paths[$url])) { // if the path is supported, make sure the method is allowed if (isset($this->paths[$url][$method])) { - // if it is allowed, return the object method to run - return $this->paths[$url][$method]; + // if it is allowed, return the object method to run, assuming the method exists + if (method_exists($this, $this->paths[$url][$method])) { + return $this->paths[$url][$method]; + } else { + throw new Exception501(); // @codeCoverageIgnore + } } else { // otherwise return 405 throw new Exception405(implode(", ", array_keys($this->paths[$url]))); From c21ae3eca990269d41f1d5e6117e7a32dcc32cf7 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 2 Nov 2020 15:21:04 -0500 Subject: [PATCH 008/366] Correctly send binary data to PostgreSQL This finally brings PostgreSQL to parity with SQLite and MySQL. Two tests casting binary data to text were removed since behaviour here should in fact be undefined Accountinf for any encoding when retrieving data will be addressed by a later commit --- lib/Db/PostgreSQL/Statement.php | 3 +++ tests/cases/Db/BaseStatement.php | 7 ------- tests/cases/Db/PostgreSQL/TestStatement.php | 5 +++++ tests/cases/Db/PostgreSQLPDO/TestStatement.php | 5 +++++ 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/Db/PostgreSQL/Statement.php b/lib/Db/PostgreSQL/Statement.php index 8c89053d..4472e8e5 100644 --- a/lib/Db/PostgreSQL/Statement.php +++ b/lib/Db/PostgreSQL/Statement.php @@ -44,6 +44,9 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement { } protected function bindValue($value, int $type, int $position): bool { + if ($value !== null && ($this->types[$position - 1] % self::T_NOT_NULL) === self::T_BINARY) { + $value = "\\x".bin2hex($value); + } $this->in[] = $value; return true; } diff --git a/tests/cases/Db/BaseStatement.php b/tests/cases/Db/BaseStatement.php index 206aed79..ba862693 100644 --- a/tests/cases/Db/BaseStatement.php +++ b/tests/cases/Db/BaseStatement.php @@ -57,7 +57,6 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { } else { $query = "SELECT ($exp = ?) as pass"; } - $typeStr = "'".str_replace("'", "''", $type)."'"; $s = new $this->statementClass(...$this->makeStatement($query)); $s->retype(...[$type]); $act = $s->run(...[$value])->getValue(); @@ -66,15 +65,11 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideBinaryBindings */ public function testHandleBinaryData($value, string $type, string $exp): void { - if (in_array(static::$implementation, ["PostgreSQL", "PDO PostgreSQL"])) { - $this->markTestIncomplete("Correct handling of binary data with PostgreSQL is not currently implemented"); - } if ($exp === "null") { $query = "SELECT (? is null) as pass"; } else { $query = "SELECT ($exp = ?) as pass"; } - $typeStr = "'".str_replace("'", "''", $type)."'"; $s = new $this->statementClass(...$this->makeStatement($query)); $s->retype(...[$type]); $act = $s->run(...[$value])->getValue(); @@ -297,13 +292,11 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { 'UTF-8 string as strict binary' => ["\u{e9}", "strict binary", "x'c3a9'"], 'Binary string as integer' => [chr(233).chr(233), "integer", "0"], 'Binary string as float' => [chr(233).chr(233), "float", "0.0"], - 'Binary string as string' => [chr(233).chr(233), "string", "'".chr(233).chr(233)."'"], 'Binary string as binary' => [chr(233).chr(233), "binary", "x'e9e9'"], 'Binary string as datetime' => [chr(233).chr(233), "datetime", "null"], 'Binary string as boolean' => [chr(233).chr(233), "boolean", "1"], 'Binary string as strict integer' => [chr(233).chr(233), "strict integer", "0"], 'Binary string as strict float' => [chr(233).chr(233), "strict float", "0.0"], - 'Binary string as strict string' => [chr(233).chr(233), "strict string", "'".chr(233).chr(233)."'"], 'Binary string as strict binary' => [chr(233).chr(233), "strict binary", "x'e9e9'"], 'Binary string as strict datetime' => [chr(233).chr(233), "strict datetime", "'0001-01-01 00:00:00'"], 'Binary string as strict boolean' => [chr(233).chr(233), "strict boolean", "1"], diff --git a/tests/cases/Db/PostgreSQL/TestStatement.php b/tests/cases/Db/PostgreSQL/TestStatement.php index 7b44ec1c..a7f776af 100644 --- a/tests/cases/Db/PostgreSQL/TestStatement.php +++ b/tests/cases/Db/PostgreSQL/TestStatement.php @@ -27,6 +27,11 @@ class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { return "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'"; } return $value; + case "binary": + if ($value[0] === "x") { + return "'\\x".substr($value, 2)."::bytea"; + } + // no break; default: return $value; } diff --git a/tests/cases/Db/PostgreSQLPDO/TestStatement.php b/tests/cases/Db/PostgreSQLPDO/TestStatement.php index 926df768..8878d421 100644 --- a/tests/cases/Db/PostgreSQLPDO/TestStatement.php +++ b/tests/cases/Db/PostgreSQLPDO/TestStatement.php @@ -27,6 +27,11 @@ class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { return "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'"; } return $value; + case "binary": + if ($value[0] === "x") { + return "'\\x".substr($value, 2)."::bytea"; + } + // no break; default: return $value; } From 41bcffd6fb530c1f104658be12d0cdd6603f2e8f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 3 Nov 2020 17:52:20 -0500 Subject: [PATCH 009/366] Correctly query PostgreSQL byte arrays This required different workarouynd for the native and PDO interfaces --- lib/Db/PostgreSQL/PDOResult.php | 26 +++++++++++++++++++++ lib/Db/PostgreSQL/PDOStatement.php | 14 +++++++++++ lib/Db/PostgreSQL/Result.php | 14 ++++++++++- tests/cases/Db/BaseResult.php | 13 +++++++++++ tests/cases/Db/PostgreSQL/TestResult.php | 13 +++++++++++ tests/cases/Db/PostgreSQLPDO/TestResult.php | 15 +++++++++++- tests/lib/DatabaseDrivers/PostgreSQLPDO.php | 2 +- 7 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 lib/Db/PostgreSQL/PDOResult.php diff --git a/lib/Db/PostgreSQL/PDOResult.php b/lib/Db/PostgreSQL/PDOResult.php new file mode 100644 index 00000000..91fe4c09 --- /dev/null +++ b/lib/Db/PostgreSQL/PDOResult.php @@ -0,0 +1,26 @@ +cur = $this->set->fetch(\PDO::FETCH_ASSOC); + if ($this->cur !== false) { + foreach($this->cur as $k => $v) { + if (is_resource($v)) { + $this->cur[$k] = stream_get_contents($v); + fclose($v); + } + } + return true; + } + return false; + } +} diff --git a/lib/Db/PostgreSQL/PDOStatement.php b/lib/Db/PostgreSQL/PDOStatement.php index c9b7b826..9929579a 100644 --- a/lib/Db/PostgreSQL/PDOStatement.php +++ b/lib/Db/PostgreSQL/PDOStatement.php @@ -6,6 +6,8 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Db\PostgreSQL; +use JKingWeb\Arsse\Db\Result; + class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement { public static function mungeQuery(string $query, array $types, ...$extraData): string { return Statement::mungeQuery($query, $types, false); @@ -16,4 +18,16 @@ class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement { // PostgreSQL uses SQLSTATE exclusively, so this is not used return []; } + + public function runArray(array $values = []): Result { + $this->st->closeCursor(); + $this->bindValues($values); + try { + $this->st->execute(); + } catch (\PDOException $e) { + [$excClass, $excMsg, $excData] = $this->buildPDOException(true); + throw new $excClass($excMsg, $excData); + } + return new PDOResult($this->db, $this->st); + } } diff --git a/lib/Db/PostgreSQL/Result.php b/lib/Db/PostgreSQL/Result.php index 03dba17f..67a7352d 100644 --- a/lib/Db/PostgreSQL/Result.php +++ b/lib/Db/PostgreSQL/Result.php @@ -10,6 +10,7 @@ class Result extends \JKingWeb\Arsse\Db\AbstractResult { protected $db; protected $r; protected $cur; + protected $blobs = []; // actual public methods @@ -30,6 +31,11 @@ class Result extends \JKingWeb\Arsse\Db\AbstractResult { public function __construct($db, $result) { $this->db = $db; $this->r = $result; + for ($a = 0, $stop = pg_num_fields($result); $a < $stop; $a++) { + if (pg_field_type($result, $a) === "bytea") { + $this->blobs[$a] = pg_field_name($result, $a); + } + } } public function __destruct() { @@ -41,6 +47,12 @@ class Result extends \JKingWeb\Arsse\Db\AbstractResult { public function valid() { $this->cur = pg_fetch_row($this->r, null, \PGSQL_ASSOC); - return $this->cur !== false; + if ($this->cur !== false) { + foreach($this->blobs as $f) { + $this->cur[$f] = hex2bin(substr($this->cur[$f], 2)); + } + return true; + } + return false; } } diff --git a/tests/cases/Db/BaseResult.php b/tests/cases/Db/BaseResult.php index 4d3d2c49..7d63af2e 100644 --- a/tests/cases/Db/BaseResult.php +++ b/tests/cases/Db/BaseResult.php @@ -10,6 +10,7 @@ use JKingWeb\Arsse\Db\Result; abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { protected static $insertDefault = "INSERT INTO arsse_test default values"; + protected static $selectBlob = "SELECT x'DEADBEEF' as \"blob\""; protected static $interface; protected $resultClass; @@ -129,4 +130,16 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { $test = new $this->resultClass(...$this->makeResult("SELECT '2112' as album, '2112' as track union select 'Clockwork Angels' as album, 'The Wreckers' as track")); $this->assertEquals($exp, $test->getAll()); } + + public function testGetBlobRow(): void { + $exp = ['blob' => hex2bin("DEADBEEF")]; + $test = new $this->resultClass(...$this->makeResult(self::$selectBlob)); + $this->assertEquals($exp, $test->getRow()); + } + + public function testGetBlobValue(): void { + $exp = hex2bin("DEADBEEF"); + $test = new $this->resultClass(...$this->makeResult(self::$selectBlob)); + $this->assertEquals($exp, $test->getValue()); + } } diff --git a/tests/cases/Db/PostgreSQL/TestResult.php b/tests/cases/Db/PostgreSQL/TestResult.php index 0992962b..73dd8fb6 100644 --- a/tests/cases/Db/PostgreSQL/TestResult.php +++ b/tests/cases/Db/PostgreSQL/TestResult.php @@ -15,6 +15,7 @@ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { protected static $createMeta = "CREATE TABLE arsse_meta(key text primary key not null, value text)"; protected static $createTest = "CREATE TABLE arsse_test(id bigserial primary key)"; + protected static $selectBlob = "SELECT '\\xDEADBEEF'::bytea as blob"; protected function makeResult(string $q): array { $set = pg_query(static::$interface, $q); @@ -29,4 +30,16 @@ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { } parent::tearDownAfterClass(); } + + public function testGetBlobRow(): void { + $exp = ['blob' => hex2bin("DEADBEEF")]; + $test = new $this->resultClass(...$this->makeResult(self::$selectBlob)); + $this->assertEquals($exp, $test->getRow()); + } + + public function testGetBlobValue(): void { + $exp = hex2bin("DEADBEEF"); + $test = new $this->resultClass(...$this->makeResult(self::$selectBlob)); + $this->assertEquals($exp, $test->getValue()); + } } diff --git a/tests/cases/Db/PostgreSQLPDO/TestResult.php b/tests/cases/Db/PostgreSQLPDO/TestResult.php index aaf6bca2..c810b715 100644 --- a/tests/cases/Db/PostgreSQLPDO/TestResult.php +++ b/tests/cases/Db/PostgreSQLPDO/TestResult.php @@ -8,16 +8,29 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO; /** * @group slow - * @covers \JKingWeb\Arsse\Db\PDOResult + * @covers \JKingWeb\Arsse\Db\PostgreSQL\PDOResult */ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { use \JKingWeb\Arsse\Test\DatabaseDrivers\PostgreSQLPDO; protected static $createMeta = "CREATE TABLE arsse_meta(key text primary key not null, value text)"; protected static $createTest = "CREATE TABLE arsse_test(id bigserial primary key)"; + protected static $selectBlob = "SELECT '\\xDEADBEEF'::bytea as blob"; protected function makeResult(string $q): array { $set = static::$interface->query($q); return [static::$interface, $set]; } + + public function testGetBlobRow(): void { + $exp = ['blob' => hex2bin("DEADBEEF")]; + $test = new $this->resultClass(...$this->makeResult(self::$selectBlob)); + $this->assertEquals($exp, $test->getRow()); + } + + public function testGetBlobValue(): void { + $exp = hex2bin("DEADBEEF"); + $test = new $this->resultClass(...$this->makeResult(self::$selectBlob)); + $this->assertSame($exp, $test->getValue()); + } } diff --git a/tests/lib/DatabaseDrivers/PostgreSQLPDO.php b/tests/lib/DatabaseDrivers/PostgreSQLPDO.php index 58001b6f..116c3b23 100644 --- a/tests/lib/DatabaseDrivers/PostgreSQLPDO.php +++ b/tests/lib/DatabaseDrivers/PostgreSQLPDO.php @@ -11,7 +11,7 @@ use JKingWeb\Arsse\Arsse; trait PostgreSQLPDO { protected static $implementation = "PDO PostgreSQL"; protected static $backend = "PostgreSQL"; - protected static $dbResultClass = \JKingWeb\Arsse\Db\PDOResult::class; + protected static $dbResultClass = \JKingWeb\Arsse\Db\PostgreSQL\PDOResult::class; protected static $dbStatementClass = \JKingWeb\Arsse\Db\PostgreSQL\PDOStatement::class; protected static $dbDriverClass = \JKingWeb\Arsse\Db\PostgreSQL\PDODriver::class; protected static $stringOutput = false; From b5f959aabfc426cfbadf40d8fafedccc42ae9ef7 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 3 Nov 2020 18:57:26 -0500 Subject: [PATCH 010/366] Fix blob tests --- tests/cases/Db/BaseResult.php | 4 ++-- tests/cases/Db/PostgreSQL/TestResult.php | 12 ------------ tests/cases/Db/PostgreSQLPDO/TestResult.php | 12 ------------ 3 files changed, 2 insertions(+), 26 deletions(-) diff --git a/tests/cases/Db/BaseResult.php b/tests/cases/Db/BaseResult.php index 7d63af2e..a43956da 100644 --- a/tests/cases/Db/BaseResult.php +++ b/tests/cases/Db/BaseResult.php @@ -133,13 +133,13 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { public function testGetBlobRow(): void { $exp = ['blob' => hex2bin("DEADBEEF")]; - $test = new $this->resultClass(...$this->makeResult(self::$selectBlob)); + $test = new $this->resultClass(...$this->makeResult(static::$selectBlob)); $this->assertEquals($exp, $test->getRow()); } public function testGetBlobValue(): void { $exp = hex2bin("DEADBEEF"); - $test = new $this->resultClass(...$this->makeResult(self::$selectBlob)); + $test = new $this->resultClass(...$this->makeResult(static::$selectBlob)); $this->assertEquals($exp, $test->getValue()); } } diff --git a/tests/cases/Db/PostgreSQL/TestResult.php b/tests/cases/Db/PostgreSQL/TestResult.php index 73dd8fb6..658228e0 100644 --- a/tests/cases/Db/PostgreSQL/TestResult.php +++ b/tests/cases/Db/PostgreSQL/TestResult.php @@ -30,16 +30,4 @@ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { } parent::tearDownAfterClass(); } - - public function testGetBlobRow(): void { - $exp = ['blob' => hex2bin("DEADBEEF")]; - $test = new $this->resultClass(...$this->makeResult(self::$selectBlob)); - $this->assertEquals($exp, $test->getRow()); - } - - public function testGetBlobValue(): void { - $exp = hex2bin("DEADBEEF"); - $test = new $this->resultClass(...$this->makeResult(self::$selectBlob)); - $this->assertEquals($exp, $test->getValue()); - } } diff --git a/tests/cases/Db/PostgreSQLPDO/TestResult.php b/tests/cases/Db/PostgreSQLPDO/TestResult.php index c810b715..caddba71 100644 --- a/tests/cases/Db/PostgreSQLPDO/TestResult.php +++ b/tests/cases/Db/PostgreSQLPDO/TestResult.php @@ -21,16 +21,4 @@ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { $set = static::$interface->query($q); return [static::$interface, $set]; } - - public function testGetBlobRow(): void { - $exp = ['blob' => hex2bin("DEADBEEF")]; - $test = new $this->resultClass(...$this->makeResult(self::$selectBlob)); - $this->assertEquals($exp, $test->getRow()); - } - - public function testGetBlobValue(): void { - $exp = hex2bin("DEADBEEF"); - $test = new $this->resultClass(...$this->makeResult(self::$selectBlob)); - $this->assertSame($exp, $test->getValue()); - } } From 2438f35f3dc34608e09ebedd2ffe31e72972f0b7 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 4 Nov 2020 18:34:22 -0500 Subject: [PATCH 011/366] Add icon cache to database Feed updating has not yet been adapted to store icon data (nor their URLs anymore) --- lib/Database.php | 44 ++++++++-------- sql/MySQL/6.sql | 18 +++++++ sql/PostgreSQL/6.sql | 17 ++++++ sql/SQLite3/6.sql | 58 +++++++++++++++++++++ tests/cases/Database/SeriesSubscription.php | 17 ++++-- tests/cases/Db/BaseUpdate.php | 26 +++++++-- 6 files changed, 151 insertions(+), 29 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index b7b44881..0df46df7 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -731,30 +731,32 @@ class Database { // create a complex query $q = new Query( "SELECT - arsse_subscriptions.id as id, - arsse_subscriptions.feed as feed, - url,favicon,source,folder,pinned,err_count,err_msg,order_type,added, - arsse_feeds.updated as updated, - arsse_feeds.modified as edited, - arsse_subscriptions.modified as modified, - topmost.top as top_folder, - coalesce(arsse_subscriptions.title, arsse_feeds.title) as title, + s.id as id, + s.feed as feed, + f.url,source,folder,pinned,err_count,err_msg,order_type,added, + f.updated as updated, + f.modified as edited, + s.modified as modified, + i.url as favicon, + t.top as top_folder, + coalesce(s.title, f.title) as title, (articles - marked) as unread - FROM arsse_subscriptions - left join topmost on topmost.f_id = arsse_subscriptions.folder - join arsse_feeds on arsse_feeds.id = arsse_subscriptions.feed - left join (select feed, count(*) as articles from arsse_articles group by feed) as article_stats on article_stats.feed = arsse_subscriptions.feed - left join (select subscription, sum(\"read\") as marked from arsse_marks group by subscription) as mark_stats on mark_stats.subscription = arsse_subscriptions.id" + FROM arsse_subscriptions as s + left join topmost as t on t.f_id = s.folder + join arsse_feeds as f on f.id = s.feed + left join arsse_icons as i on i.id = f.icon + left join (select feed, count(*) as articles from arsse_articles group by feed) as article_stats on article_stats.feed = s.feed + left join (select subscription, sum(\"read\") as marked from arsse_marks group by subscription) as mark_stats on mark_stats.subscription = s.id" ); - $q->setWhere("arsse_subscriptions.owner = ?", ["str"], [$user]); + $q->setWhere("s.owner = ?", ["str"], [$user]); $nocase = $this->db->sqlToken("nocase"); - $q->setOrder("pinned desc, coalesce(arsse_subscriptions.title, arsse_feeds.title) collate $nocase"); + $q->setOrder("pinned desc, coalesce(s.title, f.title) collate $nocase"); // topmost folders belonging to the user $q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders where owner = ? and parent is null union all select id,top from arsse_folders join topmost on parent=f_id", ["str"], [$user]); if ($id) { // this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder // if an ID is specified, add a suitable WHERE condition and bindings - $q->setWhere("arsse_subscriptions.id = ?", "int", $id); + $q->setWhere("s.id = ?", "int", $id); } elseif ($folder && $recursive) { // if a folder is specified and we're listing recursively, add a common table expression to list it and its children so that we select from the entire subtree $q->setCTE("folders(folder)", "SELECT ? union all select id from arsse_folders join folders on parent = folder", "int", $folder); @@ -921,13 +923,13 @@ class Database { * @param string|null $user The user who owns the subscription being queried */ public function subscriptionFavicon(int $id, string $user = null): string { - $q = new Query("SELECT favicon from arsse_feeds join arsse_subscriptions on feed = arsse_feeds.id"); - $q->setWhere("arsse_subscriptions.id = ?", "int", $id); + $q = new Query("SELECT i.url as favicon from arsse_feeds as f left join arsse_icons as i on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id"); + $q->setWhere("s.id = ?", "int", $id); if (isset($user)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) { throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); } - $q->setWhere("arsse_subscriptions.owner = ?", "str", $user); + $q->setWhere("s.owner = ?", "str", $user); } return (string) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); } @@ -1140,8 +1142,7 @@ class Database { } // lastly update the feed database itself with updated information. $this->db->prepare( - "UPDATE arsse_feeds SET title = ?, favicon = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ? WHERE id = ?", - 'str', + "UPDATE arsse_feeds SET title = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ? WHERE id = ?", 'str', 'str', 'datetime', @@ -1151,7 +1152,6 @@ class Database { 'int' )->run( $feed->data->title, - $feed->favicon, $feed->data->siteUrl, $feed->lastModified, $feed->resource->getEtag(), diff --git a/sql/MySQL/6.sql b/sql/MySQL/6.sql index 248a0141..281467ec 100644 --- a/sql/MySQL/6.sql +++ b/sql/MySQL/6.sql @@ -2,6 +2,8 @@ -- Copyright 2017 J. King, Dustin Wilson et al. -- See LICENSE and AUTHORS files for details +-- Please consult the SQLite 3 schemata for commented version + alter table arsse_users add column num bigint unsigned unique; alter table arsse_users add column admin boolean not null default 0; alter table arsse_users add column lang longtext; @@ -18,4 +20,20 @@ where u.id = n.id; drop table arsse_users_existing; alter table arsse_users modify num bigint unsigned not null; +create table arsse_icons( + id serial primary key, + url varchar(767) unique not null, + modified datetime(0), + etag varchar(255) not null default '', + next_fetch datetime(0), + orphaned datetime(0), + type text, + data longblob +) character set utf8mb4 collate utf8mb4_unicode_ci; +insert into arsse_icons(url) select distinct favicon from arsse_feeds where favicon is not null; +alter table arsse_feeds add column icon bigint unsigned; +alter table arsse_feeds add constraint foreign key (icon) references arsse_icons(id) on delete set null; +update arsse_feeds as f, arsse_icons as i set f.icon = i.id where f.favicon = i.url; +alter table arsse_feeds drop column favicon; + update arsse_meta set value = '7' where "key" = 'schema_version'; diff --git a/sql/PostgreSQL/6.sql b/sql/PostgreSQL/6.sql index bc365704..6c128a03 100644 --- a/sql/PostgreSQL/6.sql +++ b/sql/PostgreSQL/6.sql @@ -2,6 +2,8 @@ -- Copyright 2017 J. King, Dustin Wilson et al. -- See LICENSE and AUTHORS files for details +-- Please consult the SQLite 3 schemata for commented version + alter table arsse_users add column num bigint unique; alter table arsse_users add column admin smallint not null default 0; alter table arsse_users add column lang text; @@ -19,4 +21,19 @@ where u.id = e.id; drop table arsse_users_existing; alter table arsse_users alter column num set not null; +create table arsse_icons( + id bigserial primary key, + url text unique not null, + modified timestamp(0) without time zone, + etag text not null default '', + next_fetch timestamp(0) without time zone, + orphaned timestamp(0) without time zone, + type text, + data bytea +); +insert into arsse_icons(url) select distinct favicon from arsse_feeds where favicon is not null; +alter table arsse_feeds add column icon bigint references arsse_icons(id) on delete set null; +update arsse_feeds as f set icon = i.id from arsse_icons as i where f.favicon = i.url; +alter table arsse_feeds drop column favicon; + update arsse_meta set value = '7' where "key" = 'schema_version'; diff --git a/sql/SQLite3/6.sql b/sql/SQLite3/6.sql index 142fada0..ab82afcf 100644 --- a/sql/SQLite3/6.sql +++ b/sql/SQLite3/6.sql @@ -27,6 +27,64 @@ drop table arsse_users; drop table arsse_users_existing; alter table arsse_users_new rename to arsse_users; +-- Add a separate table for feed icons and replace their URLs in the feeds table with their IDs +create table arsse_icons( + -- Icons associated with feeds + -- At a minimum the URL of the icon must be known, but its content may be missing + id integer primary key, -- the identifier for the icon + url text unique not null, -- the URL of the icon + modified text, -- Last-Modified date, for caching + etag text not null default '', -- ETag, for caching + next_fetch text, -- The date at which cached data should be considered stale + orphaned text, -- time at which the icon last had no feeds associated with it + type text, -- the Content-Type of the icon, if known + data blob -- the binary data of the icon itself +); +insert into arsse_icons(url) select distinct favicon from arsse_feeds where favicon is not null; +create table arsse_feeds_new( +-- newsfeeds, deduplicated +-- users have subscriptions to these feeds in another table + id integer primary key, -- sequence number + url text not null, -- URL of feed + title text collate nocase, -- default title of feed (users can set the title of their subscription to the feed) + source text, -- URL of site to which the feed belongs + updated text, -- time at which the feed was last fetched + modified text, -- time at which the feed last actually changed + next_fetch text, -- time at which the feed should next be fetched + orphaned text, -- time at which the feed last had no subscriptions + etag text not null default '', -- HTTP ETag hash used for cache validation, changes each time the content changes + err_count integer not null default 0, -- count of successive times update resulted in error since last successful update + err_msg text, -- last error message + username text not null default '', -- HTTP authentication username + password text not null default '', -- HTTP authentication password (this is stored in plain text) + size integer not null default 0, -- number of articles in the feed at last fetch + scrape boolean not null default 0, -- whether to use picoFeed's content scraper with this feed + icon integer references arsse_icons(id) on delete set null, -- numeric identifier of any associated icon + unique(url,username,password) -- a URL with particular credentials should only appear once +); +insert into arsse_feeds_new + select f.id, f.url, title, source, updated, f.modified, f.next_fetch, f.orphaned, f.etag, err_count, err_msg, username, password, size, scrape, i.id + from arsse_feeds as f left join arsse_icons as i on f.favicon = i.url; +drop table arsse_feeds; +alter table arsse_feeds_new rename to arsse_feeds; + + + + + + + + + + + + + + + + + + -- set version marker pragma user_version = 7; update arsse_meta set value = '7' where "key" = 'schema_version'; diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index c0a88f4a..427a9843 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -41,6 +41,15 @@ trait SeriesSubscription { [6, "john.doe@example.com", 2, "Politics"], ], ], + 'arsse_icons' => [ + 'columns' => [ + 'id' => "int", + 'url' => "str", + ], + 'rows' => [ + [1,"http://example.com/favicon.ico"], + ], + ], 'arsse_feeds' => [ 'columns' => [ 'id' => "int", @@ -50,7 +59,7 @@ trait SeriesSubscription { 'password' => "str", 'updated' => "datetime", 'next_fetch' => "datetime", - 'favicon' => "str", + 'icon' => "int", ], 'rows' => [], // filled in the series setup ], @@ -136,9 +145,9 @@ trait SeriesSubscription { ], ]; $this->data['arsse_feeds']['rows'] = [ - [1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),strtotime("now"),''], - [2,"http://example.com/feed2", "eek", "", "",strtotime("now - 1 hour"),strtotime("now - 1 hour"),'http://example.com/favicon.ico'], - [3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),strtotime("now + 1 hour"),''], + [1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),strtotime("now"),null], + [2,"http://example.com/feed2", "eek", "", "",strtotime("now - 1 hour"),strtotime("now - 1 hour"),1], + [3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),strtotime("now + 1 hour"),null], ]; // initialize a partial mock of the Database object to later manipulate the feedUpdate method Arsse::$db = \Phake::partialMock(Database::class, static::$drv); diff --git a/tests/cases/Db/BaseUpdate.php b/tests/cases/Db/BaseUpdate.php index b25e4723..ba93687e 100644 --- a/tests/cases/Db/BaseUpdate.php +++ b/tests/cases/Db/BaseUpdate.php @@ -142,14 +142,34 @@ class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest { INSERT INTO arsse_users values('b', 'abc'); INSERT INTO arsse_folders(owner,name) values('a', '1'); INSERT INTO arsse_folders(owner,name) values('b', '2'); + INSERT INTO arsse_feeds(url,favicon) values('http://example.com/', 'http://example.com/icon'); + INSERT INTO arsse_feeds(url,favicon) values('http://example.org/', 'http://example.org/icon'); + INSERT INTO arsse_feeds(url,favicon) values('https://example.com/', 'http://example.com/icon'); + INSERT INTO arsse_feeds(url,favicon) values('http://example.net/', null); QUERY_TEXT ); $this->drv->schemaUpdate(7); - $exp = [ + $users = [ ['id' => "a", 'password' => "xyz", 'num' => 1], ['id' => "b", 'password' => "abc", 'num' => 2], ]; - $this->assertEquals($exp, $this->drv->query("SELECT id, password, num from arsse_users")->getAll()); - $this->assertSame(2, (int) $this->drv->query("SELECT count(*) from arsse_folders")->getValue()); + $folders = [ + ['owner' => "a", 'name' => "1"], + ['owner' => "b", 'name' => "2"], + ]; + $icons = [ + ['id' => 1, 'url' => "http://example.com/icon"], + ['id' => 2, 'url' => "http://example.org/icon"], + ]; + $feeds = [ + ['url' => 'http://example.com/', 'icon' => 1], + ['url' => 'http://example.org/', 'icon' => 2], + ['url' => 'https://example.com/', 'icon' => 1], + ['url' => 'http://example.net/', 'icon' => null], + ]; + $this->assertEquals($users, $this->drv->query("SELECT id, password, num from arsse_users order by id")->getAll()); + $this->assertEquals($folders, $this->drv->query("SELECT owner, name from arsse_folders order by owner")->getAll()); + $this->assertEquals($icons, $this->drv->query("SELECT id, url from arsse_icons order by id")->getAll()); + $this->assertEquals($feeds, $this->drv->query("SELECT url, icon from arsse_feeds order by id")->getAll()); } } From af675479b854c9416376bbcc94341a5700b03bdd Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 4 Nov 2020 18:35:36 -0500 Subject: [PATCH 012/366] Remove excess whitespace --- sql/SQLite3/6.sql | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/sql/SQLite3/6.sql b/sql/SQLite3/6.sql index ab82afcf..513422d8 100644 --- a/sql/SQLite3/6.sql +++ b/sql/SQLite3/6.sql @@ -68,23 +68,6 @@ insert into arsse_feeds_new drop table arsse_feeds; alter table arsse_feeds_new rename to arsse_feeds; - - - - - - - - - - - - - - - - - -- set version marker pragma user_version = 7; update arsse_meta set value = '7' where "key" = 'schema_version'; From c25782f98c2cf3cf370872a38dc74b12371b20d7 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 4 Nov 2020 20:00:00 -0500 Subject: [PATCH 013/366] Partial icon handling skeleton --- lib/Database.php | 18 +++++++++++++++++- lib/Feed.php | 20 +++++++++++++++----- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 0df46df7..734fbd4a 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1226,7 +1226,7 @@ class Database { [$cHashUC, $tHashUC, $vHashUC] = $this->generateIn($hashesUC, "str"); [$cHashTC, $tHashTC, $vHashTC] = $this->generateIn($hashesTC, "str"); // perform the query - return $articles = $this->db->prepare( + return $this->db->prepare( "SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed = ? and (guid in($cId) or url_title_hash in($cHashUT) or url_content_hash in($cHashUC) or title_content_hash in($cHashTC))", 'int', $tId, @@ -1236,6 +1236,22 @@ class Database { )->run($feedID, $vId, $vHashUT, $vHashUC, $vHashTC); } + protected function iconList(string $user, bool $withData = true): Db\Result { + $data = $withData ? "data" : "null as data"; + $out = $this->db->prepare("SELECT id, url, type, $data, next_fetch from arsse_icons")->run()->getRow(); + if (!$out) {} + return $out; + } + + protected function iconGet($id, bool $withData = true, bool $byUrl = false): array { + $field = $byUrl ? "url" : "id"; + $type = $byUrl ? "str" : "int"; + $data = $withData ? "data" : "null as data"; + $out = $this->db->prepare("SELECT id, url, type, $data, next_fetch from arsse_icons where $field = ?", $type)->run($id)->getRow(); + if (!$out) {} + return $out; + } + /** 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 diff --git a/lib/Feed.php b/lib/Feed.php index e6a8ebcf..2dad3269 100644 --- a/lib/Feed.php +++ b/lib/Feed.php @@ -16,7 +16,9 @@ use PicoFeed\Scraper\Scraper; class Feed { public $data = null; - public $favicon; + public $iconUrl; + public $iconType; + public $iconData; public $resource; public $modified = false; public $lastModified; @@ -113,16 +115,24 @@ class Feed { $this->resource->getContent(), $this->resource->getEncoding() )->execute(); - // Grab the favicon for the feed; returns an empty string if it cannot find one. - // Some feeds might use a different domain (eg: feedburner), so the site url is - // used instead of the feed's url. - $this->favicon = (new Favicon)->find($feed->siteUrl); } catch (PicoFeedException $e) { throw new Feed\Exception($this->resource->getUrl(), $e); } catch (\GuzzleHttp\Exception\GuzzleException $e) { // @codeCoverageIgnore throw new Feed\Exception($this->resource->getUrl(), $e); // @codeCoverageIgnore } + // Grab the favicon for the feed, or null if no valid icon is found + // Some feeds might use a different domain (eg: feedburner), so the site url is + // used instead of the feed's url. + $icon = new Favicon; + $this->iconUrl = $icon->find($feed->siteUrl); + $this->iconData = $icon->getContent(); + if (strlen($this->iconData)) { + $this->iconType = $icon->getType(); + } else { + $this->iconUrl = $this->iconData = null; + } + // PicoFeed does not provide valid ids when there is no id element. Its solution // of hashing the url, title, and content together for the id if there is no id // element is stupid. Many feeds are frankenstein mixtures of Atom and RSS, but From 7c40c81fb3d657508394104e79b97d95b0dd149c Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 5 Nov 2020 08:13:15 -0500 Subject: [PATCH 014/366] Add icons to the database upon feed update --- lib/Database.php | 74 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 734fbd4a..ca1246df 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1091,8 +1091,23 @@ class Database { 'int' ); } - // actually perform updates + // determine if the feed icon needs to be updated, and update it if appropriate $tr = $this->db->begin(); + $icon = null; + if ($feed->iconUrl) { + $icon = $this->iconGetByUrl($feed->iconUrl); + if ($icon) { + // update the existing icon if necessary + if ($feed->iconType !== $icon['type'] || $feed->iconData !== $icon['data']) { + $this->db->prepare("UPDATE arsse_icons set type = ?, data = ? where id = ?", "str", "blob", "int")->run($feed->iconType, $feed->iconData, $icon['id']); + } + $icon = $icon['id']; + } else { + // add the new icon to the cache + $icon = $this->db->prepare("INSERT INTO arsee_icons(url, type, data) values(?, ?, ?", "str", "str", "blob")->run($feed->iconUrl, $feed->iconType, $feed->iconData)->lastId(); + } + } + // actually perform updates foreach ($feed->newItems as $article) { $articleID = $qInsertArticle->run( $article->url, @@ -1142,13 +1157,14 @@ class Database { } // lastly update the feed database itself with updated information. $this->db->prepare( - "UPDATE arsse_feeds SET title = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ? WHERE id = ?", + "UPDATE arsse_feeds SET title = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ?, icon = ? WHERE id = ?", 'str', 'str', 'datetime', 'strict str', 'datetime', 'int', + 'int', 'int' )->run( $feed->data->title, @@ -1157,6 +1173,7 @@ class Database { $feed->resource->getEtag(), $feed->nextFetch, sizeof($feed->data->items), + $icon, $feedID ); $tr->commit(); @@ -1236,20 +1253,53 @@ class Database { )->run($feedID, $vId, $vHashUT, $vHashUC, $vHashTC); } - protected function iconList(string $user, bool $withData = true): Db\Result { + /** Retrieve a feed icon by URL, for use during feed refreshing + * + * @param string $url The URL of the icon to Retrieve + * @param bool $withData Whether to return the icon content along with the metadata + */ + protected function iconGetByUrl(string $url, bool $withData = true): array { $data = $withData ? "data" : "null as data"; - $out = $this->db->prepare("SELECT id, url, type, $data, next_fetch from arsse_icons")->run()->getRow(); - if (!$out) {} + return $this->db->prepare("SELECT id, url, type, $data, next_fetch from arsse_icons where url = ?", "str")->run($id)->getRow(); + } + + + /** Returns information about an icon for a feed to which a user is subscribed, with or without the binary content of the icon itself + * + * The returned information is: + * + * - "id": The umeric identifier of the icon (not the subscription) + * - "url": The URL of the icon + * - "type": The Content-Type of the icon e.g. "image/png" + * - "data": The icon itself, as a binary sring; if $withData is false this will be null + * + * @param string $user The user whose subscription icon is to be retrieved + * @param int $subscription The numeric identifier of the subscription with which the icon is associated + * @param bool $withData Whether to retrireve the icon content in addition to its metadata + */ + public function iconGet(string $user, int $subscrption, bool $withData = true): array { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } + $data = $withData ? "data" : "null as data"; + $out = $this->db->prepare("SELECT i.id, i.url, i.type, $data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id where s.owner = ? and s.id = ?", "str", "int")->run($user, $subscription)->getRow(); + if (!$out) { + throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "subscription", 'id' => $subscription]); + } return $out; } - protected function iconGet($id, bool $withData = true, bool $byUrl = false): array { - $field = $byUrl ? "url" : "id"; - $type = $byUrl ? "str" : "int"; - $data = $withData ? "data" : "null as data"; - $out = $this->db->prepare("SELECT id, url, type, $data, next_fetch from arsse_icons where $field = ?", $type)->run($id)->getRow(); - if (!$out) {} - return $out; + /** Lists icons for feeds to which a user is subscribed, with or without the binary content of the icon itself + * + * @param string $user The user whose subscription icons are to be retrieved + * @param bool $withData Whether to retrireve the icon content in addition to its metadata + */ + public function iconList(string $user, bool $withData = true): Db\Result { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } + $data = $withData ? "i.data" : "null as data"; + return $this->db->prepare("SELECT i.id, i.url, i.type, $data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id where s.owner = ?", "str")->run($user); } /** Returns an associative array of result column names and their SQL computations for article queries From 50fd127ac4cac17dcc1a0daa01c0439404799ff9 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 5 Nov 2020 10:14:42 -0500 Subject: [PATCH 015/366] Test for icon fetching --- lib/Feed.php | 2 +- tests/cases/Feed/TestFeed.php | 8 ++++++++ tests/docroot/Feed/Parsing/WithIcon.php | 8 ++++++++ tests/docroot/Icon.php | 4 ++++ 4 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 tests/docroot/Feed/Parsing/WithIcon.php create mode 100644 tests/docroot/Icon.php diff --git a/lib/Feed.php b/lib/Feed.php index 2dad3269..dffbccb9 100644 --- a/lib/Feed.php +++ b/lib/Feed.php @@ -125,7 +125,7 @@ class Feed { // Some feeds might use a different domain (eg: feedburner), so the site url is // used instead of the feed's url. $icon = new Favicon; - $this->iconUrl = $icon->find($feed->siteUrl); + $this->iconUrl = $icon->find($feed->siteUrl, $feed->getIcon()); $this->iconData = $icon->getContent(); if (strlen($this->iconData)) { $this->iconType = $icon->getType(); diff --git a/tests/cases/Feed/TestFeed.php b/tests/cases/Feed/TestFeed.php index 01f4f5f1..562d9063 100644 --- a/tests/cases/Feed/TestFeed.php +++ b/tests/cases/Feed/TestFeed.php @@ -347,4 +347,12 @@ class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { $exp = "

Partial content, followed by more content

"; $this->assertSame($exp, $f->newItems[0]->content); } + + public function testFetchWithIcon(): void { + $d = base64_decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAADUlEQVQYV2NgYGBgAAAABQABijPjAAAAAABJRU5ErkJggg=="); + $f = new Feed(null, $this->base."Parsing/WithIcon"); + $this->assertSame(self::$host."Icon", $f->iconUrl); + $this->assertSame("image/png", $f->iconType); + $this->assertSame($d, $f->iconData); + } } diff --git a/tests/docroot/Feed/Parsing/WithIcon.php b/tests/docroot/Feed/Parsing/WithIcon.php new file mode 100644 index 00000000..4f1f2776 --- /dev/null +++ b/tests/docroot/Feed/Parsing/WithIcon.php @@ -0,0 +1,8 @@ + "application/atom+xml", + 'content' => << + /Icon + +MESSAGE_BODY +]; diff --git a/tests/docroot/Icon.php b/tests/docroot/Icon.php new file mode 100644 index 00000000..f5c54d63 --- /dev/null +++ b/tests/docroot/Icon.php @@ -0,0 +1,4 @@ + "image/png", + 'content' => base64_decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAADUlEQVQYV2NgYGBgAAAABQABijPjAAAAAABJRU5ErkJggg=="), +]; From bd650765e1fcf2eb8f26a5644ffcc62837d9f0d8 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 5 Nov 2020 12:12:01 -0500 Subject: [PATCH 016/366] Generalize icon fetching tests --- .gitignore | 1 - tests/cases/Feed/TestFeed.php | 8 ++++---- .../Feed/{Parsing/WithIcon.php => WithIcon/GIF.php} | 2 +- tests/docroot/Feed/WithIcon/PNG.php | 8 ++++++++ tests/docroot/Feed/WithIcon/SVG1.php | 8 ++++++++ tests/docroot/Feed/WithIcon/SVG2.php | 8 ++++++++ tests/docroot/Icon/GIF.php | 4 ++++ tests/docroot/{Icon.php => Icon/PNG.php} | 0 tests/docroot/Icon/SVG1.php | 4 ++++ tests/docroot/Icon/SVG2.php | 4 ++++ 10 files changed, 41 insertions(+), 6 deletions(-) rename tests/docroot/Feed/{Parsing/WithIcon.php => WithIcon/GIF.php} (85%) create mode 100644 tests/docroot/Feed/WithIcon/PNG.php create mode 100644 tests/docroot/Feed/WithIcon/SVG1.php create mode 100644 tests/docroot/Feed/WithIcon/SVG2.php create mode 100644 tests/docroot/Icon/GIF.php rename tests/docroot/{Icon.php => Icon/PNG.php} (100%) create mode 100644 tests/docroot/Icon/SVG1.php create mode 100644 tests/docroot/Icon/SVG2.php diff --git a/.gitignore b/.gitignore index d90e245d..16e9c935 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,6 @@ $RECYCLE.BIN/ .DS_Store .AppleDouble .LSOverride -Icon ._* .Spotlight-V100 .Trashes diff --git a/tests/cases/Feed/TestFeed.php b/tests/cases/Feed/TestFeed.php index 562d9063..a5036d28 100644 --- a/tests/cases/Feed/TestFeed.php +++ b/tests/cases/Feed/TestFeed.php @@ -349,10 +349,10 @@ class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { } public function testFetchWithIcon(): void { - $d = base64_decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAADUlEQVQYV2NgYGBgAAAABQABijPjAAAAAABJRU5ErkJggg=="); - $f = new Feed(null, $this->base."Parsing/WithIcon"); - $this->assertSame(self::$host."Icon", $f->iconUrl); - $this->assertSame("image/png", $f->iconType); + $d = base64_decode("R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="); + $f = new Feed(null, $this->base."WithIcon/GIF"); + $this->assertSame(self::$host."Icon/GIF", $f->iconUrl); + $this->assertSame("image/gif", $f->iconType); $this->assertSame($d, $f->iconData); } } diff --git a/tests/docroot/Feed/Parsing/WithIcon.php b/tests/docroot/Feed/WithIcon/GIF.php similarity index 85% rename from tests/docroot/Feed/Parsing/WithIcon.php rename to tests/docroot/Feed/WithIcon/GIF.php index 4f1f2776..8b81b58c 100644 --- a/tests/docroot/Feed/Parsing/WithIcon.php +++ b/tests/docroot/Feed/WithIcon/GIF.php @@ -2,7 +2,7 @@ 'mime' => "application/atom+xml", 'content' => << - /Icon + /Icon/GIF MESSAGE_BODY ]; diff --git a/tests/docroot/Feed/WithIcon/PNG.php b/tests/docroot/Feed/WithIcon/PNG.php new file mode 100644 index 00000000..3bca1c9a --- /dev/null +++ b/tests/docroot/Feed/WithIcon/PNG.php @@ -0,0 +1,8 @@ + "application/atom+xml", + 'content' => << + /Icon/PNG + +MESSAGE_BODY +]; diff --git a/tests/docroot/Feed/WithIcon/SVG1.php b/tests/docroot/Feed/WithIcon/SVG1.php new file mode 100644 index 00000000..5f5acc87 --- /dev/null +++ b/tests/docroot/Feed/WithIcon/SVG1.php @@ -0,0 +1,8 @@ + "application/atom+xml", + 'content' => << + /Icon/SVG1 + +MESSAGE_BODY +]; diff --git a/tests/docroot/Feed/WithIcon/SVG2.php b/tests/docroot/Feed/WithIcon/SVG2.php new file mode 100644 index 00000000..aca3c79c --- /dev/null +++ b/tests/docroot/Feed/WithIcon/SVG2.php @@ -0,0 +1,8 @@ + "application/atom+xml", + 'content' => << + /Icon/SVG2 + +MESSAGE_BODY +]; diff --git a/tests/docroot/Icon/GIF.php b/tests/docroot/Icon/GIF.php new file mode 100644 index 00000000..50f0b78c --- /dev/null +++ b/tests/docroot/Icon/GIF.php @@ -0,0 +1,4 @@ + "image/gif", + 'content' => base64_decode("R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="), +]; diff --git a/tests/docroot/Icon.php b/tests/docroot/Icon/PNG.php similarity index 100% rename from tests/docroot/Icon.php rename to tests/docroot/Icon/PNG.php diff --git a/tests/docroot/Icon/SVG1.php b/tests/docroot/Icon/SVG1.php new file mode 100644 index 00000000..0543d91a --- /dev/null +++ b/tests/docroot/Icon/SVG1.php @@ -0,0 +1,4 @@ + "image/svg+xml", + 'content' => '' +]; diff --git a/tests/docroot/Icon/SVG2.php b/tests/docroot/Icon/SVG2.php new file mode 100644 index 00000000..4ade7ce4 --- /dev/null +++ b/tests/docroot/Icon/SVG2.php @@ -0,0 +1,4 @@ + "image/svg+xml", + 'content' => '' +]; From c3a57ca68b1b614b42778220521e7d4ae7478c0a Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 5 Nov 2020 14:19:17 -0500 Subject: [PATCH 017/366] Tests for icon cache population --- lib/Database.php | 6 +-- tests/cases/Database/SeriesFeed.php | 70 ++++++++++++++++++++++++++-- tests/docroot/Feed/WithIcon/GIF.php | 3 ++ tests/docroot/Feed/WithIcon/PNG.php | 3 ++ tests/docroot/Feed/WithIcon/SVG1.php | 3 ++ tests/docroot/Feed/WithIcon/SVG2.php | 3 ++ 6 files changed, 80 insertions(+), 8 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index ca1246df..2bed918e 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1104,7 +1104,7 @@ class Database { $icon = $icon['id']; } else { // add the new icon to the cache - $icon = $this->db->prepare("INSERT INTO arsee_icons(url, type, data) values(?, ?, ?", "str", "str", "blob")->run($feed->iconUrl, $feed->iconType, $feed->iconData)->lastId(); + $icon = $this->db->prepare("INSERT INTO arsse_icons(url, type, data) values(?, ?, ?)", "str", "str", "blob")->run($feed->iconUrl, $feed->iconType, $feed->iconData)->lastId(); } } // actually perform updates @@ -1258,9 +1258,9 @@ class Database { * @param string $url The URL of the icon to Retrieve * @param bool $withData Whether to return the icon content along with the metadata */ - protected function iconGetByUrl(string $url, bool $withData = true): array { + protected function iconGetByUrl(string $url, bool $withData = true): ?array { $data = $withData ? "data" : "null as data"; - return $this->db->prepare("SELECT id, url, type, $data, next_fetch from arsse_icons where url = ?", "str")->run($id)->getRow(); + return $this->db->prepare("SELECT id, url, type, $data, next_fetch from arsse_icons where url = ?", "str")->run($url)->getRow(); } diff --git a/tests/cases/Database/SeriesFeed.php b/tests/cases/Database/SeriesFeed.php index 1eb23bb0..f79c8cc9 100644 --- a/tests/cases/Database/SeriesFeed.php +++ b/tests/cases/Database/SeriesFeed.php @@ -26,6 +26,20 @@ trait SeriesFeed { ["john.doe@example.com", "",2], ], ], + 'arsse_icons' => [ + 'columns' => [ + 'id' => "int", + 'url' => "str", + 'type' => "str", + 'data' => "blob", + ], + 'rows' => [ + [1,'http://localhost:8000/Icon/PNG','image/png',base64_decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAADUlEQVQYV2NgYGBgAAAABQABijPjAAAAAABJRU5ErkJggg==")], + [2,'http://localhost:8000/Icon/GIF','image/gif',base64_decode("R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==")], + // this actually contains the data of SVG2, which will lead to a row update when retieved + [3,'http://localhost:8000/Icon/SVG1','image/svg+xml',''], + ], + ], 'arsse_feeds' => [ 'columns' => [ 'id' => "int", @@ -36,13 +50,19 @@ trait SeriesFeed { 'modified' => "datetime", 'next_fetch' => "datetime", 'size' => "int", + 'icon' => "int", ], 'rows' => [ - [1,"http://localhost:8000/Feed/Matching/3","Ook",0,"",$past,$past,0], - [2,"http://localhost:8000/Feed/Matching/1","Eek",5,"There was an error last time",$past,$future,0], - [3,"http://localhost:8000/Feed/Fetching/Error?code=404","Ack",0,"",$past,$now,0], - [4,"http://localhost:8000/Feed/NextFetch/NotModified?t=".time(),"Ooook",0,"",$past,$past,0], - [5,"http://localhost:8000/Feed/Parsing/Valid","Ooook",0,"",$past,$future,0], + [1,"http://localhost:8000/Feed/Matching/3","Ook",0,"",$past,$past,0,null], + [2,"http://localhost:8000/Feed/Matching/1","Eek",5,"There was an error last time",$past,$future,0,null], + [3,"http://localhost:8000/Feed/Fetching/Error?code=404","Ack",0,"",$past,$now,0,null], + [4,"http://localhost:8000/Feed/NextFetch/NotModified?t=".time(),"Ooook",0,"",$past,$past,0,null], + [5,"http://localhost:8000/Feed/Parsing/Valid","Ooook",0,"",$past,$future,0,null], + // these feeds all test icon caching + [6,"http://localhost:8000/Feed/WithIcon/PNG",null,0,"",$past,$future,0,1], // no change when updated + [7,"http://localhost:8000/Feed/WithIcon/GIF",null,0,"",$past,$future,0,1], // icon ID 2 will be assigned to feed when updated + [8,"http://localhost:8000/Feed/WithIcon/SVG1",null,0,"",$past,$future,0,3], // icon ID 3 will be modified when updated + [9,"http://localhost:8000/Feed/WithIcon/SVG2",null,0,"",$past,$future,0,null], // icon ID 4 will be created and assigned to feed when updated ], ], 'arsse_subscriptions' => [ @@ -261,4 +281,44 @@ trait SeriesFeed { Arsse::$db->feedUpdate(4); $this->assertEquals([1], Arsse::$db->feedListStale()); } + + public function testCheckIconDuringFeedUpdate(): void { + Arsse::$db->feedUpdate(6); + $state = $this->primeExpectations($this->data, [ + 'arsse_icons' => ["id","url","type","data"], + 'arsse_feeds' => ["id", "icon"], + ]); + $this->compareExpectations(static::$drv, $state); + } + + public function testAssignIconDuringFeedUpdate(): void { + Arsse::$db->feedUpdate(7); + $state = $this->primeExpectations($this->data, [ + 'arsse_icons' => ["id","url","type","data"], + 'arsse_feeds' => ["id", "icon"], + ]); + $state['arsse_feeds']['rows'][6][1] = 2; + $this->compareExpectations(static::$drv, $state); + } + + public function testChangeIconDuringFeedUpdate(): void { + Arsse::$db->feedUpdate(8); + $state = $this->primeExpectations($this->data, [ + 'arsse_icons' => ["id","url","type","data"], + 'arsse_feeds' => ["id", "icon"], + ]); + $state['arsse_icons']['rows'][2][3] = ''; + $this->compareExpectations(static::$drv, $state); + } + + public function testAddIconDuringFeedUpdate(): void { + Arsse::$db->feedUpdate(9); + $state = $this->primeExpectations($this->data, [ + 'arsse_icons' => ["id","url","type","data"], + 'arsse_feeds' => ["id", "icon"], + ]); + $state['arsse_feeds']['rows'][8][1] = 4; + $state['arsse_icons']['rows'][] = [4,'http://localhost:8000/Icon/SVG2','image/svg+xml','']; + $this->compareExpectations(static::$drv, $state); + } } diff --git a/tests/docroot/Feed/WithIcon/GIF.php b/tests/docroot/Feed/WithIcon/GIF.php index 8b81b58c..ae3ce225 100644 --- a/tests/docroot/Feed/WithIcon/GIF.php +++ b/tests/docroot/Feed/WithIcon/GIF.php @@ -3,6 +3,9 @@ 'content' => << /Icon/GIF + + Example title + MESSAGE_BODY ]; diff --git a/tests/docroot/Feed/WithIcon/PNG.php b/tests/docroot/Feed/WithIcon/PNG.php index 3bca1c9a..1e946d82 100644 --- a/tests/docroot/Feed/WithIcon/PNG.php +++ b/tests/docroot/Feed/WithIcon/PNG.php @@ -3,6 +3,9 @@ 'content' => << /Icon/PNG + + Example title + MESSAGE_BODY ]; diff --git a/tests/docroot/Feed/WithIcon/SVG1.php b/tests/docroot/Feed/WithIcon/SVG1.php index 5f5acc87..8bbabdeb 100644 --- a/tests/docroot/Feed/WithIcon/SVG1.php +++ b/tests/docroot/Feed/WithIcon/SVG1.php @@ -3,6 +3,9 @@ 'content' => << /Icon/SVG1 + + Example title + MESSAGE_BODY ]; diff --git a/tests/docroot/Feed/WithIcon/SVG2.php b/tests/docroot/Feed/WithIcon/SVG2.php index aca3c79c..ce36bb76 100644 --- a/tests/docroot/Feed/WithIcon/SVG2.php +++ b/tests/docroot/Feed/WithIcon/SVG2.php @@ -3,6 +3,9 @@ 'content' => << /Icon/SVG2 + + Example title + MESSAGE_BODY ]; From 4fc208d940dd6bda4092788c70aa49aea8418db1 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 5 Nov 2020 16:51:46 -0500 Subject: [PATCH 018/366] More consistent icon API --- lib/Database.php | 75 ++++++++++++++++------ tests/cases/Database/AbstractTest.php | 1 + tests/cases/Database/SeriesIcon.php | 89 +++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 19 deletions(-) create mode 100644 tests/cases/Database/SeriesIcon.php diff --git a/lib/Database.php b/lib/Database.php index 2bed918e..0262b78a 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -23,6 +23,7 @@ use JKingWeb\Arsse\Misc\URL; * - Folders, which belong to users and contain subscriptions * - Tags, which belong to users and can be assigned to multiple subscriptions * - Feeds to which users are subscribed + * - Icons, which are associated with feeds * - Articles, which belong to feeds and for which users can only affect metadata * - Editions, identifying authorial modifications to articles * - Labels, which belong to users and can be assigned to multiple articles @@ -933,6 +934,29 @@ class Database { } return (string) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); } + + /** Retrieves detailed information about the icon for a subscription. + * + * The returned information is: + * + * - "id": The umeric identifier of the icon (not the subscription) + * - "url": The URL of the icon + * - "type": The Content-Type of the icon e.g. "image/png" + * - "data": The icon itself, as a binary sring; if $withData is false this will be null + * + * @param string $user The user whose subscription icon is to be retrieved + * @param int $subscription The numeric identifier of the subscription + */ + public function subscriptionIcon(string $user, int $subscription): array { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } + $out = $this->db->prepare("SELECT i.id, i.url, i.type, i.data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id where s.owner = ? and s.id = ?", "str", "int")->run($user, $subscription)->getRow(); + if (!$out) { + throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "subscription", 'id' => $subscription]); + } + return $out; + } /** Returns the time at which any of a user's subscriptions (or a specific subscription) was last refreshed, as a DateTimeImmutable object */ public function subscriptionRefreshed(string $user, int $id = null): ?\DateTimeImmutable { @@ -1255,16 +1279,13 @@ class Database { /** Retrieve a feed icon by URL, for use during feed refreshing * - * @param string $url The URL of the icon to Retrieve - * @param bool $withData Whether to return the icon content along with the metadata + * @param string $url The URL of the icon to retrieve */ - protected function iconGetByUrl(string $url, bool $withData = true): ?array { - $data = $withData ? "data" : "null as data"; - return $this->db->prepare("SELECT id, url, type, $data, next_fetch from arsse_icons where url = ?", "str")->run($url)->getRow(); + protected function iconGetByUrl(string $url): ?array { + return $this->db->prepare("SELECT id, url, type, data from arsse_icons where url = ?", "str")->run($url)->getRow(); } - - - /** Returns information about an icon for a feed to which a user is subscribed, with or without the binary content of the icon itself + + /** Retrieves information about an icon * * The returned information is: * @@ -1273,18 +1294,16 @@ class Database { * - "type": The Content-Type of the icon e.g. "image/png" * - "data": The icon itself, as a binary sring; if $withData is false this will be null * - * @param string $user The user whose subscription icon is to be retrieved - * @param int $subscription The numeric identifier of the subscription with which the icon is associated - * @param bool $withData Whether to retrireve the icon content in addition to its metadata + * @param string $user The user whose icon is to be retrieved + * @param int $subscription The numeric identifier of the icon */ - public function iconGet(string $user, int $subscrption, bool $withData = true): array { + public function iconGet(string $user, int $id): array { if (!Arsse::$user->authorize($user, __FUNCTION__)) { throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); } - $data = $withData ? "data" : "null as data"; - $out = $this->db->prepare("SELECT i.id, i.url, i.type, $data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id where s.owner = ? and s.id = ?", "str", "int")->run($user, $subscription)->getRow(); + $out = $this->db->prepare("SELECT i.id, i.url, i.type, i.data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id where s.owner = ? and i.id = ?", "str", "int")->run($user, $id)->getRow(); if (!$out) { - throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "subscription", 'id' => $subscription]); + throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "subscription", 'id' => $id]); } return $out; } @@ -1292,14 +1311,32 @@ class Database { /** Lists icons for feeds to which a user is subscribed, with or without the binary content of the icon itself * * @param string $user The user whose subscription icons are to be retrieved - * @param bool $withData Whether to retrireve the icon content in addition to its metadata */ - public function iconList(string $user, bool $withData = true): Db\Result { + public function iconList(string $user): Db\Result { if (!Arsse::$user->authorize($user, __FUNCTION__)) { throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); } - $data = $withData ? "i.data" : "null as data"; - return $this->db->prepare("SELECT i.id, i.url, i.type, $data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id where s.owner = ?", "str")->run($user); + return $this->db->prepare("SELECT i.id, i.url, i.type, i.data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id where s.owner = ?", "str")->run($user); + } + + /** Deletes orphaned icons from the database + * + * Icons are orphaned if no subscribed newsfeed uses them. + */ + public function iconCleanup(): int { + $tr = $this->begin(); + // first unmark any icons which are no longer orphaned; an icon is considered orphaned if it is not used or only used by feeds which are themselves orphaned + $this->db->query("UPDATE arsse_icons set orphaned = null where id in (select distinct icon from arsse_feeds where icon is not null and orphaned is null)"); + // next mark any newly orphaned icons with the current date and time + $this->db->query("UPDATE arsse_icons set orphaned = CURRENT_TIMESTAMP where id not in (select distinct icon from arsse_feeds where icon is not null and orphaned is null)"); + // finally delete icons that have been orphaned longer than the feed retention period, if a a purge threshold has been specified + $out = 0; + if (Arsse::$conf->purgeFeeds) { + $limit = Date::sub(Arsse::$conf->purgeFeeds); + $out += $this->db->prepare("DELETE from arsse_icons where orphaned <= ?", "datetime")->run($limit)->changes(); + } + $tr->commit(); + return $out; } /** Returns an associative array of result column names and their SQL computations for article queries diff --git a/tests/cases/Database/AbstractTest.php b/tests/cases/Database/AbstractTest.php index ff5acdd5..6e0e2ec9 100644 --- a/tests/cases/Database/AbstractTest.php +++ b/tests/cases/Database/AbstractTest.php @@ -18,6 +18,7 @@ abstract class AbstractTest extends \JKingWeb\Arsse\Test\AbstractTest { use SeriesToken; use SeriesFolder; use SeriesFeed; + use SeriesIcon; use SeriesSubscription; use SeriesLabel; use SeriesTag; diff --git a/tests/cases/Database/SeriesIcon.php b/tests/cases/Database/SeriesIcon.php new file mode 100644 index 00000000..7a7e348c --- /dev/null +++ b/tests/cases/Database/SeriesIcon.php @@ -0,0 +1,89 @@ +data = [ + 'arsse_users' => [ + 'columns' => [ + 'id' => 'str', + 'password' => 'str', + 'num' => 'int', + ], + 'rows' => [ + ["jane.doe@example.com", "",1], + ["john.doe@example.com", "",2], + ], + ], + 'arsse_icons' => [ + 'columns' => [ + 'id' => "int", + 'url' => "str", + 'type' => "str", + 'data' => "blob", + ], + 'rows' => [ + [1,'http://localhost:8000/Icon/PNG','image/png',base64_decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAADUlEQVQYV2NgYGBgAAAABQABijPjAAAAAABJRU5ErkJggg==")], + [2,'http://localhost:8000/Icon/GIF','image/gif',base64_decode("R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==")], + [3,'http://localhost:8000/Icon/SVG1','image/svg+xml',''], + [4,'http://localhost:8000/Icon/SVG2','image/svg+xml',''], + ], + ], + 'arsse_feeds' => [ + 'columns' => [ + 'id' => "int", + 'url' => "str", + 'title' => "str", + 'err_count' => "int", + 'err_msg' => "str", + 'modified' => "datetime", + 'next_fetch' => "datetime", + 'size' => "int", + 'icon' => "int", + ], + 'rows' => [ + [1,"http://localhost:8000/Feed/Matching/3","Ook",0,"",$past,$past,0,null], + [2,"http://localhost:8000/Feed/Matching/1","Eek",5,"There was an error last time",$past,$future,0,null], + [3,"http://localhost:8000/Feed/Fetching/Error?code=404","Ack",0,"",$past,$now,0,null], + [4,"http://localhost:8000/Feed/NextFetch/NotModified?t=".time(),"Ooook",0,"",$past,$past,0,null], + [5,"http://localhost:8000/Feed/Parsing/Valid","Ooook",0,"",$past,$future,0,null], + // these feeds all test icon caching + [6,"http://localhost:8000/Feed/WithIcon/PNG",null,0,"",$past,$future,0,1], // no change when updated + [7,"http://localhost:8000/Feed/WithIcon/GIF",null,0,"",$past,$future,0,1], // icon ID 2 will be assigned to feed when updated + [8,"http://localhost:8000/Feed/WithIcon/SVG1",null,0,"",$past,$future,0,3], // icon ID 3 will be modified when updated + [9,"http://localhost:8000/Feed/WithIcon/SVG2",null,0,"",$past,$future,0,null], // icon ID 4 will be created and assigned to feed when updated + ], + ], + 'arsse_subscriptions' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'feed' => "int", + ], + 'rows' => [ + [1,'john.doe@example.com',1], + [2,'john.doe@example.com',2], + [3,'john.doe@example.com',3], + [4,'john.doe@example.com',4], + [5,'john.doe@example.com',5], + [6,'jane.doe@example.com',1], + ], + ], + ]; + } + + protected function tearDownSeriesIcon(): void { + unset($this->data); + } +} From dd1a80f279ae7e14f4809bb0d0c892f96bd006b9 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 5 Nov 2020 18:32:11 -0500 Subject: [PATCH 019/366] Consolidate subscription icon querying Users and tests still need adjusting --- lib/Database.php | 43 +++++++++++++------------------------------ 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 0262b78a..db4d125e 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -910,30 +910,6 @@ class Database { $out = $this->db->prepare("SELECT $field from arsse_tags where id in (select tag from arsse_tag_members where subscription = ? and assigned = 1) order by $field", "int")->run($id)->getAll(); return $out ? array_column($out, $field) : []; } - - /** Retrieves the URL of the icon for a subscription. - * - * Note that while the $user parameter is optional, it - * is NOT recommended to omit it, as this can lead to - * leaks of private information. The parameter is only - * optional because this is required for Tiny Tiny RSS, - * the original implementation of which leaks private - * information due to a design flaw. - * - * @param integer $id The numeric identifier of the subscription - * @param string|null $user The user who owns the subscription being queried - */ - public function subscriptionFavicon(int $id, string $user = null): string { - $q = new Query("SELECT i.url as favicon from arsse_feeds as f left join arsse_icons as i on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id"); - $q->setWhere("s.id = ?", "int", $id); - if (isset($user)) { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } - $q->setWhere("s.owner = ?", "str", $user); - } - return (string) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); - } /** Retrieves detailed information about the icon for a subscription. * @@ -944,16 +920,23 @@ class Database { * - "type": The Content-Type of the icon e.g. "image/png" * - "data": The icon itself, as a binary sring; if $withData is false this will be null * - * @param string $user The user whose subscription icon is to be retrieved + * @param string|null $user The user who owns the subscription being queried; using null here is supported for TT-RSS and SHOULD NOT be used elsewhere as it leaks information * @param int $subscription The numeric identifier of the subscription + * @param bool $includeData Whether to include the binary data of the icon itself in the result */ - public function subscriptionIcon(string $user, int $subscription): array { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + public function subscriptionIcon(?string $user, int $id, bool $includeData = true): array { + $data = $includeData ? "i.data" : "null as data"; + $q = new Query("SELECT i.id, i.url, i.type, $data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id"); + $q->setWhere("s.id = ?", "int", $id); + if (isset($user)) { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } + $q->setWhere("s.owner = ?", "str", $user); } - $out = $this->db->prepare("SELECT i.id, i.url, i.type, i.data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id where s.owner = ? and s.id = ?", "str", "int")->run($user, $subscription)->getRow(); + $out = $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getRow(); if (!$out) { - throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "subscription", 'id' => $subscription]); + throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "subscription", 'id' => $id]); } return $out; } From 424b14d2b44029b8cc2565058c461611ddd0f14e Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 6 Nov 2020 10:27:30 -0500 Subject: [PATCH 020/366] Clean up use of subscriptionFavicon --- lib/Database.php | 4 +- lib/REST/TinyTinyRSS/Icon.php | 11 ++++-- tests/cases/Database/SeriesSubscription.php | 41 +++++++++++---------- tests/cases/REST/TinyTinyRSS/TestIcon.php | 24 ++++++------ 4 files changed, 43 insertions(+), 37 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index db4d125e..b852ecc5 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -926,7 +926,7 @@ class Database { */ public function subscriptionIcon(?string $user, int $id, bool $includeData = true): array { $data = $includeData ? "i.data" : "null as data"; - $q = new Query("SELECT i.id, i.url, i.type, $data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id"); + $q = new Query("SELECT i.id, i.url, i.type, $data from arsse_subscriptions as s join arsse_feeds as f on s.feed = f.id left join arsse_icons as i on f.icon = i.id"); $q->setWhere("s.id = ?", "int", $id); if (isset($user)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) { @@ -936,7 +936,7 @@ class Database { } $out = $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getRow(); if (!$out) { - throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "subscription", 'id' => $id]); + throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "subscription", 'id' => $id]); } return $out; } diff --git a/lib/REST/TinyTinyRSS/Icon.php b/lib/REST/TinyTinyRSS/Icon.php index c5c9030e..b49ae4e4 100644 --- a/lib/REST/TinyTinyRSS/Icon.php +++ b/lib/REST/TinyTinyRSS/Icon.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\REST\TinyTinyRSS; use JKingWeb\Arsse\Arsse; +use JKingWeb\Arsse\Db\ExceptionInput; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\EmptyResponse as Response; @@ -29,14 +30,16 @@ class Icon extends \JKingWeb\Arsse\REST\AbstractHandler { } elseif (!preg_match("<^(\d+)\.ico$>", $req->getRequestTarget(), $match) || !((int) $match[1])) { return new Response(404); } - $url = Arsse::$db->subscriptionFavicon((int) $match[1], Arsse::$user->id ?? null); - if ($url) { - // strip out anything after literal line-end characters; this is to mitigate a potential header (e.g. cookie) injection from the URL + try { + $url = Arsse::$db->subscriptionIcon(Arsse::$user->id ?? null, (int) $match[1], false)['url']; + if (!$url) { + return new Response(404); + } if (($pos = strpos($url, "\r")) !== false || ($pos = strpos($url, "\n")) !== false) { $url = substr($url, 0, $pos); } return new Response(301, ['Location' => $url]); - } else { + } catch (ExceptionInput $e) { return new Response(404); } } diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index 427a9843..0075b992 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -462,32 +462,35 @@ trait SeriesSubscription { public function testRetrieveTheFaviconOfASubscription(): void { $exp = "http://example.com/favicon.ico"; - $this->assertSame($exp, Arsse::$db->subscriptionFavicon(1)); - $this->assertSame($exp, Arsse::$db->subscriptionFavicon(2)); - $this->assertSame('', Arsse::$db->subscriptionFavicon(3)); - $this->assertSame('', Arsse::$db->subscriptionFavicon(4)); + $this->assertSame($exp, Arsse::$db->subscriptionIcon(null, 1)['url']); + $this->assertSame($exp, Arsse::$db->subscriptionIcon(null, 2)['url']); + $this->assertSame(null, Arsse::$db->subscriptionIcon(null, 3)['url']); // authorization shouldn't have any bearing on this function \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertSame($exp, Arsse::$db->subscriptionFavicon(1)); - $this->assertSame($exp, Arsse::$db->subscriptionFavicon(2)); - $this->assertSame('', Arsse::$db->subscriptionFavicon(3)); - $this->assertSame('', Arsse::$db->subscriptionFavicon(4)); - // invalid IDs should simply return an empty string - $this->assertSame('', Arsse::$db->subscriptionFavicon(-2112)); + $this->assertSame($exp, Arsse::$db->subscriptionIcon(null, 1)['url']); + $this->assertSame($exp, Arsse::$db->subscriptionIcon(null, 2)['url']); + $this->assertSame(null, Arsse::$db->subscriptionIcon(null, 3)['url']); + } + + public function testRetrieveTheFaviconOfAMissingSubscription(): void { + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + Arsse::$db->subscriptionIcon(null, -2112); } public function testRetrieveTheFaviconOfASubscriptionWithUser(): void { $exp = "http://example.com/favicon.ico"; $user = "john.doe@example.com"; - $this->assertSame($exp, Arsse::$db->subscriptionFavicon(1, $user)); - $this->assertSame('', Arsse::$db->subscriptionFavicon(2, $user)); - $this->assertSame('', Arsse::$db->subscriptionFavicon(3, $user)); - $this->assertSame('', Arsse::$db->subscriptionFavicon(4, $user)); + $this->assertSame($exp, Arsse::$db->subscriptionIcon($user, 1)['url']); + $this->assertSame(null, Arsse::$db->subscriptionIcon($user, 3)['url']); $user = "jane.doe@example.com"; - $this->assertSame('', Arsse::$db->subscriptionFavicon(1, $user)); - $this->assertSame($exp, Arsse::$db->subscriptionFavicon(2, $user)); - $this->assertSame('', Arsse::$db->subscriptionFavicon(3, $user)); - $this->assertSame('', Arsse::$db->subscriptionFavicon(4, $user)); + $this->assertSame($exp, Arsse::$db->subscriptionIcon($user, 2)['url']); + } + + public function testRetrieveTheFaviconOfASubscriptionOfTheWrongUser(): void { + $exp = "http://example.com/favicon.ico"; + $user = "john.doe@example.com"; + $this->assertException("subjectMissing", "Db", "ExceptionInput"); + $this->assertSame(null, Arsse::$db->subscriptionIcon($user, 2)['url']); } public function testRetrieveTheFaviconOfASubscriptionWithUserWithoutAuthority(): void { @@ -495,7 +498,7 @@ trait SeriesSubscription { $user = "john.doe@example.com"; \Phake::when(Arsse::$user)->authorize->thenReturn(false); $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->subscriptionFavicon(-2112, $user); + Arsse::$db->subscriptionIcon($user, -2112); } public function testListTheTagsOfASubscription(): void { diff --git a/tests/cases/REST/TinyTinyRSS/TestIcon.php b/tests/cases/REST/TinyTinyRSS/TestIcon.php index 38dbd8fe..5341238f 100644 --- a/tests/cases/REST/TinyTinyRSS/TestIcon.php +++ b/tests/cases/REST/TinyTinyRSS/TestIcon.php @@ -48,10 +48,10 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest { } public function testRetrieveFavion(): void { - \Phake::when(Arsse::$db)->subscriptionFavicon->thenReturn(""); - \Phake::when(Arsse::$db)->subscriptionFavicon(42, $this->anything())->thenReturn("http://example.com/favicon.ico"); - \Phake::when(Arsse::$db)->subscriptionFavicon(2112, $this->anything())->thenReturn("http://example.net/logo.png"); - \Phake::when(Arsse::$db)->subscriptionFavicon(1337, $this->anything())->thenReturn("http://example.org/icon.gif\r\nLocation: http://bad.example.com/"); + \Phake::when(Arsse::$db)->subscriptionIcon->thenReturn(['url' => null]); + \Phake::when(Arsse::$db)->subscriptionIcon($this->anything(), 42, false)->thenReturn(['url' => "http://example.com/favicon.ico"]); + \Phake::when(Arsse::$db)->subscriptionIcon($this->anything(), 2112, false)->thenReturn(['url' => "http://example.net/logo.png"]); + \Phake::when(Arsse::$db)->subscriptionIcon($this->anything(), 1337, false)->thenReturn(['url' => "http://example.org/icon.gif\r\nLocation: http://bad.example.com/"]); // these requests should succeed $exp = new Response(301, ['Location' => "http://example.com/favicon.ico"]); $this->assertMessage($exp, $this->req("42.ico")); @@ -71,14 +71,14 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest { } public function testRetrieveFavionWithHttpAuthentication(): void { - $url = "http://example.org/icon.gif\r\nLocation: http://bad.example.com/"; - \Phake::when(Arsse::$db)->subscriptionFavicon->thenReturn(""); - \Phake::when(Arsse::$db)->subscriptionFavicon(42, $this->user)->thenReturn($url); - \Phake::when(Arsse::$db)->subscriptionFavicon(2112, "jane.doe")->thenReturn($url); - \Phake::when(Arsse::$db)->subscriptionFavicon(1337, $this->user)->thenReturn($url); - \Phake::when(Arsse::$db)->subscriptionFavicon(42, null)->thenReturn($url); - \Phake::when(Arsse::$db)->subscriptionFavicon(2112, null)->thenReturn($url); - \Phake::when(Arsse::$db)->subscriptionFavicon(1337, null)->thenReturn($url); + $url = ['url' => "http://example.org/icon.gif\r\nLocation: http://bad.example.com/"]; + \Phake::when(Arsse::$db)->subscriptionIcon->thenReturn(['url' => null]); + \Phake::when(Arsse::$db)->subscriptionIcon($this->user, 42, false)->thenReturn($url); + \Phake::when(Arsse::$db)->subscriptionIcon("jane.doe", 2112, false)->thenReturn($url); + \Phake::when(Arsse::$db)->subscriptionIcon($this->user, 1337, false)->thenReturn($url); + \Phake::when(Arsse::$db)->subscriptionIcon(null, 42, false)->thenReturn($url); + \Phake::when(Arsse::$db)->subscriptionIcon(null, 2112, false)->thenReturn($url); + \Phake::when(Arsse::$db)->subscriptionIcon(null, 1337, false)->thenReturn($url); // these requests should succeed $exp = new Response(301, ['Location' => "http://example.org/icon.gif"]); $this->assertMessage($exp, $this->req("42.ico")); From 8f739cec85cdb673dadd07c286114cb86967189a Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 6 Nov 2020 10:28:28 -0500 Subject: [PATCH 021/366] Excluse empty-string URLs from icons table --- sql/MySQL/6.sql | 2 +- sql/PostgreSQL/6.sql | 2 +- sql/SQLite3/6.sql | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sql/MySQL/6.sql b/sql/MySQL/6.sql index 281467ec..4d7d4aee 100644 --- a/sql/MySQL/6.sql +++ b/sql/MySQL/6.sql @@ -30,7 +30,7 @@ create table arsse_icons( type text, data longblob ) character set utf8mb4 collate utf8mb4_unicode_ci; -insert into arsse_icons(url) select distinct favicon from arsse_feeds where favicon is not null; +insert into arsse_icons(url) select distinct favicon from arsse_feeds where favicon is not null and favicon <> ''; alter table arsse_feeds add column icon bigint unsigned; alter table arsse_feeds add constraint foreign key (icon) references arsse_icons(id) on delete set null; update arsse_feeds as f, arsse_icons as i set f.icon = i.id where f.favicon = i.url; diff --git a/sql/PostgreSQL/6.sql b/sql/PostgreSQL/6.sql index 6c128a03..c78f6d44 100644 --- a/sql/PostgreSQL/6.sql +++ b/sql/PostgreSQL/6.sql @@ -31,7 +31,7 @@ create table arsse_icons( type text, data bytea ); -insert into arsse_icons(url) select distinct favicon from arsse_feeds where favicon is not null; +insert into arsse_icons(url) select distinct favicon from arsse_feeds where favicon is not null and favicon <> ''; alter table arsse_feeds add column icon bigint references arsse_icons(id) on delete set null; update arsse_feeds as f set icon = i.id from arsse_icons as i where f.favicon = i.url; alter table arsse_feeds drop column favicon; diff --git a/sql/SQLite3/6.sql b/sql/SQLite3/6.sql index 513422d8..6e8a993e 100644 --- a/sql/SQLite3/6.sql +++ b/sql/SQLite3/6.sql @@ -40,7 +40,7 @@ create table arsse_icons( type text, -- the Content-Type of the icon, if known data blob -- the binary data of the icon itself ); -insert into arsse_icons(url) select distinct favicon from arsse_feeds where favicon is not null; +insert into arsse_icons(url) select distinct favicon from arsse_feeds where favicon is not null and favicon <> ''; create table arsse_feeds_new( -- newsfeeds, deduplicated -- users have subscriptions to these feeds in another table From b24c469dcae0e5d5933901285ee731a85810b3e6 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 6 Nov 2020 11:01:50 -0500 Subject: [PATCH 022/366] Update changelog --- CHANGELOG | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index f679cfa1..730e60a4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +Version 0.9.0 (????-??-??) +========================== + +Bug fixes: +- Use icons specified in Atom feeds when available + Version 0.8.5 (2020-10-27) ========================== From e861cca53d959133434bb621931e6f616e93cd44 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 6 Nov 2020 11:06:27 -0500 Subject: [PATCH 023/366] Integrate schema change necessary for microsub --- sql/MySQL/6.sql | 2 ++ sql/PostgreSQL/6.sql | 2 ++ sql/SQLite3/6.sql | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/sql/MySQL/6.sql b/sql/MySQL/6.sql index 4d7d4aee..6c652e82 100644 --- a/sql/MySQL/6.sql +++ b/sql/MySQL/6.sql @@ -4,6 +4,8 @@ -- Please consult the SQLite 3 schemata for commented version +alter table arsse_tokens add column data longtext default null; + alter table arsse_users add column num bigint unsigned unique; alter table arsse_users add column admin boolean not null default 0; alter table arsse_users add column lang longtext; diff --git a/sql/PostgreSQL/6.sql b/sql/PostgreSQL/6.sql index c78f6d44..f14f8c83 100644 --- a/sql/PostgreSQL/6.sql +++ b/sql/PostgreSQL/6.sql @@ -4,6 +4,8 @@ -- Please consult the SQLite 3 schemata for commented version +alter table arsse_tokens add column data text default null; + alter table arsse_users add column num bigint unique; alter table arsse_users add column admin smallint not null default 0; alter table arsse_users add column lang text; diff --git a/sql/SQLite3/6.sql b/sql/SQLite3/6.sql index 6e8a993e..8c6f73f3 100644 --- a/sql/SQLite3/6.sql +++ b/sql/SQLite3/6.sql @@ -2,6 +2,10 @@ -- Copyright 2017 J. King, Dustin Wilson et al. -- See LICENSE and AUTHORS files for details +-- Add a column to the token table to hold arbitrary class-specific data +-- This is a speculative addition to support OAuth login in the future +alter table arsse_tokens add column data text default null; + -- Add multiple columns to the users table -- In particular this adds a numeric identifier for each user, which Miniflux requires create table arsse_users_new( From 4d532cba3f45c15d4351235827ba87d10c990f57 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 6 Nov 2020 13:08:53 -0500 Subject: [PATCH 024/366] Initial Miniflux documentation --- docs/en/010_About.md | 1 + .../030_Supported_Protocols/005_Miniflux.md | 26 +++++ docs/en/030_Supported_Protocols/index.md | 1 + docs/en/040_Compatible_Clients.md | 97 +++++++++++++++++-- docs/theme/arsse/arsse.css | 2 +- docs/theme/src/arsse.scss | 4 +- 6 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 docs/en/030_Supported_Protocols/005_Miniflux.md diff --git a/docs/en/010_About.md b/docs/en/010_About.md index 615185f4..3ffc49bb 100644 --- a/docs/en/010_About.md +++ b/docs/en/010_About.md @@ -1,5 +1,6 @@ The Advanced RSS Environment (affectionately called "The Arsse") is a news aggregator server which implements multiple synchronization protocols. Unlike most other aggregator servers, The Arsse does not include a Web front-end (though one is planned as a separate project), and it relies on [existing protocols](Supported_Protocols) to maximize compatibility with [existing clients](Compatible_Clients). Supported protocols are: +- Miniflux - Nextcloud News - Tiny Tiny RSS - Fever diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md new file mode 100644 index 00000000..cf706ba5 --- /dev/null +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -0,0 +1,26 @@ +[TOC] + +# About + +
+
Supported since
+
0.9.0
+
Base URL
+
/
+
API endpoint
+
/v1/
+
Specifications
+
API Reference
+
+ +The Miniflux protocol is a well-designed protocol supporting a wide variety of operations on newsfeeds, folders (termed "categories"), and articles; it also allows for user administration, and native OPML importing and exporting. + +Architecturally it is similar to the Nextcloud News protocol, but is generally more efficient. + +# Differences + +TBD + +# Interaction with nested folders + +Tiny Tiny RSS is unique in allowing newsfeeds to be grouped into folders nested to arbitrary depth. When newsfeeds are placed into nested folders, they simply appear in the top-level folder when accessed via the Miniflux protocol. This does not affect OPML exports, where full nesting is preserved. diff --git a/docs/en/030_Supported_Protocols/index.md b/docs/en/030_Supported_Protocols/index.md index 7e58df6e..b9a9e47a 100644 --- a/docs/en/030_Supported_Protocols/index.md +++ b/docs/en/030_Supported_Protocols/index.md @@ -1,5 +1,6 @@ The Arsse was designed from the start as a server for multiple synchronization protocols which clients can make use of. Currently the following protocols are supported: +- [Miniflux](Miniflux) - [Nextcloud News](Nextcloud_News) - [Tiny Tiny RSS](Tiny_Tiny_RSS) - [Fever](Fever) diff --git a/docs/en/040_Compatible_Clients.md b/docs/en/040_Compatible_Clients.md index 9122387f..cd82678b 100644 --- a/docs/en/040_Compatible_Clients.md +++ b/docs/en/040_Compatible_Clients.md @@ -5,10 +5,11 @@ The Arsse does not at this time have any first party clients. However, because T Name OS - Protocol + Protocol Notes + Miniflux Nextcloud News Tiny Tiny RSS Fever @@ -16,16 +17,33 @@ The Arsse does not at this time have any first party clients. However, because T - Desktop + Web + + + reminiflux + + ✔ + ✘ + ✘ + ✘ + +

Three-pane alternative front-end for Minflux.

+ + + + + + Desktop FeedReader Linux + ✘ ✔ ✔ ✘ -

Excellent reader; one of the best on any platform.

+

Excellent reader; discontinued in favour of NewsFlash.

Not compatible with HTTP authentication when using TT-RSS.

@@ -33,6 +51,7 @@ The Arsse does not at this time have any first party clients. However, because T Liferea Linux ✘ + ✘ ✔ ✘ @@ -44,6 +63,7 @@ The Arsse does not at this time have any first party clients. However, because T Linux, macOS ✔ ✔ + ✔ ✘

Terminal-based client.

@@ -52,11 +72,12 @@ The Arsse does not at this time have any first party clients. However, because T NewsFlash Linux + ✔ ✘ ✘ ✔ -

Successor to FeedReader.

+

Successor to FeedReader. One of the best on any platform

@@ -64,6 +85,7 @@ The Arsse does not at this time have any first party clients. However, because T macOS ✘ ✘ + ✘ ✔

Also available for iOS.

@@ -72,11 +94,12 @@ The Arsse does not at this time have any first party clients. However, because T RSS Guard Windows, macOS, Linux + ✘ ✔ ✔ ✘ -

Very basic client; now discontinued.

+

Very basic client.

@@ -84,6 +107,7 @@ The Arsse does not at this time have any first party clients. However, because T Tiny Tiny RSS Reader Windows ✘ + ✘ ✔ ✘ @@ -93,11 +117,12 @@ The Arsse does not at this time have any first party clients. However, because T - Mobile + Mobile CloudNews iOS + ✘ ✔ ✘ ✘ @@ -109,6 +134,7 @@ The Arsse does not at this time have any first party clients. However, because T FeedMe Android ✘ + ✘ ✔ ✘ @@ -119,6 +145,7 @@ The Arsse does not at this time have any first party clients. However, because T Fiery Feeds iOS ✘ + ✘ ✔ ✔ @@ -126,9 +153,28 @@ The Arsse does not at this time have any first party clients. However, because T

Currently keeps showing items in the unread badge which have already been read.

+ + Microflux for Miniflux + Android + ✔ + ✘ + ✘ + ✘ + + + + Miniflutt + Android + ✔ + ✘ + ✘ + ✘ + + Newsout Android, iOS + ✘ ✔ ✘ ✘ @@ -139,6 +185,7 @@ The Arsse does not at this time have any first party clients. However, because T Nextcloud News Android + ✘ ✔ ✘ ✘ @@ -149,6 +196,7 @@ The Arsse does not at this time have any first party clients. However, because T OCReader Android + ✘ ✔ ✘ ✘ @@ -159,16 +207,29 @@ The Arsse does not at this time have any first party clients. However, because T Android ✘ ✘ + ✘ ✔

Fetches favicons independently.

+ + Reed + Android + ✔ + ✘ + ✘ + ✘ + +

Binaries only available from GitHub.

+ + Reeder iOS ✘ ✘ + ✘ ✔

Also available for macOS.

@@ -178,6 +239,7 @@ The Arsse does not at this time have any first party clients. However, because T Tiny Tiny RSS Android ✘ + ✘ ✔ ✘ @@ -188,6 +250,7 @@ The Arsse does not at this time have any first party clients. However, because T TTRSS-Reader Android ✘ + ✘ ✔ ✘ @@ -199,6 +262,7 @@ The Arsse does not at this time have any first party clients. However, because T iOS ✘ ✘ + ✘ ✔

Trialware with one-time purchase.

@@ -214,10 +278,11 @@ The Arsse does not at this time have any first party clients. However, because T Name OS - Protocol + Protocol Notes + Miniflux Nextcloud News Tiny Tiny RSS Fever @@ -228,15 +293,30 @@ The Arsse does not at this time have any first party clients. However, because T FeedTheMonkey Linux ✘ + ✘ ✔ ✘

+ Newsie Ubuntu Touch + ✘ ✔ ✘ ✘ @@ -249,6 +329,7 @@ The Arsse does not at this time have any first party clients. However, because T macOS ✘ ✘ + ✘ ✔

Requires purchase. Presumed to work.

@@ -259,6 +340,7 @@ The Arsse does not at this time have any first party clients. However, because T Windows ✘ ✘ + ✘ ✔

Requires manual configuration.

@@ -268,6 +350,7 @@ The Arsse does not at this time have any first party clients. However, because T tiny Reader RSS iOS ✘ + ✘ ✔ ✘ diff --git a/docs/theme/arsse/arsse.css b/docs/theme/arsse/arsse.css index 94b17ab9..9971fbaa 100644 --- a/docs/theme/arsse/arsse.css +++ b/docs/theme/arsse/arsse.css @@ -1,2 +1,2 @@ /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ -html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;font-size:14px}body{margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress,sub,sup{vertical-align:baseline}.s-content pre code:after,.s-content pre code:before,[hidden],template{display:none}a{background-color:transparent;-webkit-text-decoration-skip:objects;text-decoration:none;color:#e63c2f}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}.s-content blockquote cite,dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;clear:both;margin:1em 0;border:0;border-top:1px solid #ddd}button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:700}button,hr,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}*,:after,:before{box-sizing:border-box}@media (min-width:850px){html{font-size:16px}}body,html{height:100%;background-color:#fff;color:#15284b}.Columns__left{background-color:#e8d5d3}.Columns__right__content{padding:10px;background-color:#fff}@media (max-width:768px){html:not(.no-js) .Collapsible__content{height:0;overflow:hidden;transition:height 400ms ease-in-out}}.Collapsible__trigger{margin:12px;padding:7px 10px;background-color:transparent;border:0;float:right;background-image:none;-webkit-filter:none;filter:none;box-shadow:none}.Collapsible__trigger__bar{display:block;width:18px;height:2px;margin-top:2px;margin-bottom:3px;background-color:#e8d5d3}.Collapsible__trigger:hover{background-color:#93b7bb;box-shadow:none}.Collapsible__trigger:hover .Collapsible__trigger__bar{background-color:#15284b}@media screen and (min-width:769px){body{background-color:#15284b}.Navbar{position:fixed;z-index:1030;width:100%}.Collapsible__trigger{display:none!important}.Collapsible__content{display:block!important}.Columns{height:100%}.Columns:after,.Columns:before{content:" ";display:table}.Columns:after{clear:both}.Columns__left,.Columns__right{position:relative;min-height:1px;float:left;overflow:auto;height:100%}.Columns__left{width:25%;border-right:1px solid #e7e7e9;overflow-x:hidden}.Columns__right{width:75%}.Columns__right__content{padding:0 20px 20px;min-height:100%}}.Page{max-width:860px}body{font-family:"Cabin","Trebuchet MS",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-feature-settings:"kern" 1;-webkit-font-kerning:normal;font-kerning:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;line-height:1.618}h1,h2,h3,h4,h5,h6{font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.s-content h1,.s-content h2,.s-content h3,.s-content h4,.s-content h5,.s-content h6{cursor:text;line-height:1.4em;margin:2em 0 .5em}.s-content h1 code,.s-content h1 tt,.s-content h2 code,.s-content h2 tt,.s-content h3 code,.s-content h3 tt,.s-content h4 code,.s-content h4 tt,.s-content h5 code,.s-content h5 tt,.s-content h6 code,.s-content h6 tt{font-size:inherit}.s-content h1 i,.s-content h2 i,.s-content h3 i,.s-content h4 i,.s-content h5 i,.s-content h6 i{font-size:.7em}.s-content h1 p,.s-content h2 p,.s-content h3 p,.s-content h4 p,.s-content h5 p,.s-content h6 p{margin-top:0}.s-content h1{margin-top:0;font-size:2.618rem}.s-content h2{font-size:2rem}.s-content h3{font-size:1.618rem}.s-content h4,.s-content h5,.s-content h6,.s-content small{font-size:1.309rem}.s-content a{text-decoration:underline}.s-content p{margin-bottom:1.3em}.s-content ol,.s-content ul{padding-left:2em}.s-content ul p,.s-content ul ul{margin:0}.s-content dl{padding:0}.s-content dl dt{font-weight:700;font-style:italic;padding:0;margin:15px 0 5px}.s-content dl dt:first-child{padding:0}.s-content dl dd{margin:0 0 15px;padding:0 15px}.s-content blockquote{margin:.75em 2em;padding:.5em 1em;font-style:italic;border-left:.25em solid #15284b}.s-content blockquote cite:before{content:"\2014";padding-right:.5em}.s-content table{width:100%;padding:0;margin-bottom:1em;border-collapse:separate;border-spacing:2px;border:2px solid #b3aab1}.s-content table+table{margin-top:1em}.s-content table tr{background-color:#fff;margin:0;padding:0;border-top:0}.s-content table tr:nth-child(2n){background-color:transparent}.s-content table th{font-weight:700;background:#e8d5d3}.s-content table td,.s-content table th{margin:0;padding:.5em}.s-content blockquote>:first-child,.s-content dl dd>:first-child,.s-content dl dt>:first-child,.s-content ol>:first-child,.s-content table td>:first-child,.s-content table th>:first-child,.s-content ul>:first-child{margin-top:0}.s-content blockquote>:last-child,.s-content dl dd>:last-child,.s-content dl dt>:last-child,.s-content ol>:last-child,.s-content table td>:last-child,.s-content table th>:last-child,.s-content ul>:last-child{margin-bottom:0}.s-content img{max-width:100%;display:block;margin:0 auto}.s-content code{font-family:Monaco,Menlo,Consolas,"Lucida Console","Courier New",monospace;padding-top:.1rem;padding-bottom:.1rem;background:#f9f5f4;border-radius:0;box-shadow:none;display:inline-block;padding:.5ch;border:0}.s-content code:after,.s-content code:before{letter-spacing:-.2em;content:"\00a0"}.s-content pre{background:#f5f2f0;line-height:1.5em;overflow:auto;border:0;border-radius:0;padding:.75em 20px;margin:0 -20px 20px}.s-content pre code{margin:0;padding:0;white-space:pre;box-shadow:none}.s-content pre code,.s-content pre tt{background-color:transparent;border:0}.s-content ins,.s-content u{text-decoration:none;border-bottom:1px solid #15284b}.s-content del a,.s-content ins a,.s-content u a{color:inherit}a.Link--external:after{content:" " url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAVklEQVR4Xn3PgQkAMQhDUXfqTu7kTtkpd5RA8AInfArtQ2iRXFWT2QedAfttj2FsPIOE1eCOlEuoWWjgzYaB/IkeGOrxXhqB+uA9Bfcm0lAZuh+YIeAD+cAqSz4kCMUAAAAASUVORK5CYII=)}a.Link--broken{color:red}p{margin:0 0 1em}.Button{display:inline-block;text-align:center;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;margin-bottom:0}.Button--small{font-size:12px;line-height:1.5;border-radius:3px}.Button--default{color:#333;background-color:#fff;border-color:#ccc}.Button--default.Button--active{color:#333;background-color:#e6e6e6;border-color:#adadad}.Brand,.Nav__item a:hover{color:#15284b;text-shadow:none}.Brand,.Navbar{background-color:#e63c2f}.Brand{display:block;padding:.75em .6em;font-size:2rem;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.Navbar{box-shadow:0 1px 5px rgba(0,0,0,.25);margin-bottom:0}.CodeToggler{padding:0 20px}.CodeToggler__text{font-size:12px;line-height:1.5;padding:6px 10px 6px 0;display:inline-block;vertical-align:middle}.no-js .CodeToggler{display:none}.Nav{margin:0;padding:0}.Nav__arrow{display:inline-block;position:relative;width:16px;margin-left:-16px}.Nav__arrow:before{position:absolute;display:block;content:"";margin:-.25em 0 0 -.4em;left:50%;top:50%;border-right:.15em solid #15284b;border-top:.15em solid #15284b;transform:rotate(45deg);transition-duration:.3s}.Nav__item,.Nav__item a{display:block}.Nav__item a{margin:0;padding:6px 15px 6px 20px;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-weight:400;color:#15284b;text-shadow:none}.Nav__item a:hover{background-color:#93b7bb}.Nav .Nav{margin-left:15px}html:not(.no-js) .Nav .Nav{height:0;transition:height 400ms ease-in-out;overflow:hidden}.Nav .Nav .Nav__item a{margin:0 0 0 -15px;padding:3px 30px;font-family:"Cabin","Trebuchet MS",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;color:#15284b;opacity:.7}.HomepageButtons .Button--hero:hover,.Nav .Nav .Nav__item a:hover{opacity:1}.Nav .Nav .Nav__item--active a{color:#15284b}.Nav__item--active>a,.Nav__item--open>a{background-color:#93b7bb}.Nav__item--open>a>.Nav__arrow:before{margin-left:-.25em;transform:rotate(135deg)}.Page__header{margin:0 0 10px;padding:0}.Page__header:after,.Page__header:before{content:" ";display:table}.Page__header:after{clear:both}.Page__header h1{margin:0;padding:0;line-height:57px}.Page__header--separator{height:.6em}.Page__header a{text-decoration:none}.Page__header .EditOn,.Page__header .ModifiedDate{float:left;font-size:10px;color:gray}.Page__header .EditOn{float:right}.Links,.Twitter{padding:0 20px}.Links a{font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-weight:400;color:#15284b;line-height:2em}.Twitter{font:11px/18px "Helvetica Neue",Arial,sans-serif}.Twitter__button{text-decoration:none;display:inline-block;vertical-align:top;zoom:1;position:relative;height:20px;box-sizing:border-box;padding:1px 8px 1px 6px;background-color:#1b95e0;color:#fff;border-radius:3px;font-weight:500;cursor:pointer}.Twitter__button .Twitter__button__label{display:inline-block;vertical-align:top;zoom:1;margin-left:3px;white-space:nowrap}.Twitter__button svg{position:relative;top:2px;display:inline-block;width:14px;height:14px}.PoweredBy{padding:0 20px 1rem;font-size:1.309rem}.Search{position:relative}.Search__field{display:block;width:100%;height:34px;padding:6px 30px 6px 20px;color:#555;border-width:0 0 1px;border-bottom:1px solid #ccc;background:#fff;transition:border-color ease-in-out .15s}.Search__field:focus{border-color:#93b7bb;outline:0}.Search__icon{position:absolute;right:9px;top:9px;width:16px;height:16px}.Navbar .Search{float:right;margin:8px 20px}.Navbar .Search__field{box-shadow:inset 0 1px 1px rgba(0,0,0,.075);border-width:0;border-radius:4px;padding-left:10px}.TableOfContentsContainer{float:right;min-width:300px;max-width:25%;padding-left:1em}.TableOfContentsContainer__title{margin-bottom:0!important}.TableOfContentsContainer__content{border:1px solid #efefef;border-width:4px 2px 2px 6px}.TableOfContentsContainer__content>.TableOfContents>li+li{border-top:1px solid #ddd}ul.TableOfContents{font-size:1rem;padding-left:0;margin:0;list-style-type:none;border-left:6px solid #e8d5d3}ul.TableOfContents p{margin-bottom:0}ul.TableOfContents a{text-decoration:none;display:block;padding:.2em 0 .2em .75em}ul.TableOfContents .TableOfContents{padding-left:.75em}.Pager{padding-left:0;margin:1em 0;list-style:none;text-align:center}.Pager:after,.Pager:before{content:" ";display:table}.Pager,.Pager:after{clear:both}.Pager li{display:inline}.Pager li>a{display:inline-block;padding:5px 14px;background-color:#fff}.Pager li>a:focus,.Pager li>a:hover{text-decoration:none}.Pager--next>a{float:right}.Pager--prev>a{float:left}.Checkbox{position:relative;display:block;padding-left:30px;cursor:pointer}.Checkbox input{position:absolute;z-index:-1;opacity:0}.Checkbox__indicator{position:absolute;top:50%;left:0;width:20px;height:20px;margin-top:-10px;background:#e6e6e6}.Checkbox__indicator:after{position:absolute;display:none;content:""}.Checkbox input:focus~.Checkbox__indicator,.Checkbox:hover input~.Checkbox__indicator{background:#ccc}.Checkbox input:checked~.Checkbox__indicator{background:#15284b}.Checkbox input:checked~.Checkbox__indicator:after{display:block}.Checkbox input:checked:focus~.Checkbox__indicator,.Checkbox:hover input:not([disabled]):checked~.Checkbox__indicator{background:#93b7bb}.Checkbox input:disabled~.Checkbox__indicator{pointer-events:none;opacity:.6;background:#e6e6e6}.Checkbox .Checkbox__indicator:after{top:4px;left:8px;width:5px;height:10px;transform:rotate(45deg);border:solid #fff;border-width:0 2px 2px 0}.Checkbox input:disabled~.Checkbox__indicator:after{border-color:#7b7b7b}.Hidden{display:none}.Container{margin-right:auto;margin-left:auto}.Container--inner{width:80%;margin:0 auto}@media (min-width:1200px){.Container{width:1170px}}@media (min-width:992px){.Container{width:970px}}@media (min-width:769px){.Container{width:750px}}.Homepage{background-color:#fff;border-radius:0;border:0;color:#15284b;overflow:hidden;padding-bottom:0;margin-bottom:0;box-shadow:none}.HomepageTitle h2{width:80%;font-size:30px;margin:20px auto;text-align:center}.HomepageImage img{display:block;max-width:80%;margin:0 auto;height:auto}.HomepageButtons{padding:20px 0;background-color:#e8d5d3;text-align:center}.HomepageButtons:after,.HomepageButtons:before{content:" ";display:table}.HomepageButtons:after{clear:both}.HomepageButtons .Button--hero{padding:20px 30px;border-radius:0;text-shadow:none;opacity:.8;margin:0 10px;text-transform:uppercase;border:5px solid #15284b;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;background-image:none;-webkit-filter:none;filter:none;box-shadow:none}@media (max-width:768px){.HomepageButtons .Button--hero{display:block;margin-bottom:10px}}.HomepageButtons .Button--hero.Button--secondary{background-color:#93b7bb;color:#15284b}.HomepageButtons .Button--hero.Button--primary{background-color:#15284b;color:#e8d5d3}.HomepageContent{background-color:#fff;padding:40px 0}.HomepageContent ol li,.HomepageContent ul li{list-style:none;margin-bottom:.5em;position:relative}.HomepageContent ol li:before,.HomepageContent ul li:before{position:absolute;top:50%;left:-1.5em;content:"";width:0;height:0;border:.5em solid transparent;border-left:.5em solid #93b7bb;float:left;display:block;margin-top:-.5em}.HomepageContent .HeroText,.HomepageFooter__links li a{font-size:16px;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.HomepageContent .HeroText{font-weight:300;margin-bottom:20px;line-height:1.4}@media (min-width:769px){.HomepageContent{padding:40px 20px}.HomepageContent .HeroText{font-size:21px}.HomepageContent .Row{margin:0 -15px}.HomepageContent .Row__half,.HomepageContent .Row__quarter,.HomepageContent .Row__third{float:left;position:relative;min-height:1px;padding-left:15px;padding-right:15px}.HomepageContent .Row__third{width:33.333333%}.HomepageContent .Row__half{width:50%}.HomepageContent .Row__quarter{width:25%}}.HomepageFooter{background-color:#15284b;color:#93b7bb;border:0;box-shadow:none}.HomepageFooter:after,.HomepageFooter:before{content:" ";display:table}.HomepageFooter:after{clear:both}@media (max-width:768px){.HomepageFooter{padding:0 20px;text-align:center}.HomepageFooter .HomepageFooter__links{padding-left:0;list-style-type:none}}@media (min-width:769px){.HomepageFooter .HomepageFooter__links{float:left}.HomepageFooter .HomepageFooter__twitter{float:right}}.HomepageFooter__links,.HomepageFooter__twitter{margin:40px 0}.HomepageFooter__links li a{line-height:32px;font-weight:700}.HomepageFooter__links li a:hover{text-decoration:underline}.HomepageFooter .Twitter__button{margin-bottom:20px}@media print{*{text-shadow:none!important;color:#000!important;background:0 0!important;box-shadow:none!important}h1,h2,h3,h4,h5,h6{page-break-after:avoid;page-break-before:auto}blockquote,img,pre{page-break-inside:avoid}blockquote,pre{border:1px solid #999;font-style:italic}img{border:0}a,a:visited{text-decoration:underline}abbr[title]:after{content:" (" attr(title) ")"}q{quotes:none}.s-content a[href^="#"]:after,q:before{content:""}q:after{content:" (" attr(cite) ")"}.PageBreak{display:block;page-break-before:always}.NoPrint,.Pager,aside{display:none}.Columns__right{width:100%!important}.s-content a:after{content:" (" attr(href) ")";font-size:80%;word-wrap:break-word}h1 a[href]:after{font-size:50%}}@font-face{font-family:'League Gothic';src:url(fonts/leaguegothic.woff2) format('woff2'),url(fonts/leaguegothic.woff) format('woff');font-style:normal;font-display:swap}@font-face{font-family:'Cabin';src:url(fonts/cabin-regular.woff2) format('woff2'),url(fonts/cabin-regular.woff) format('woff');font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:'Cabin';src:url(fonts/cabin-italic.woff2) format('woff2'),url(fonts/cabin-italic.woff) format('woff');font-style:italic;font-display:swap}@font-face{font-family:'Cabin';src:url(fonts/cabin-bold.woff2) format('woff2'),url(fonts/cabin-bold.woff) format('woff');font-weight:700;font-style:normal;font-display:swap}.s-content code::after,.s-content code::before,a.Link--external::after{content:''}pre .s-content code{display:inline}.s-content table tbody,.s-content table thead{background-color:#fff}.s-content table tr:nth-child(2n) td{background-color:#f9f5f4}.s-content table td,.s-content table th{border:0}.Nav__item .Nav__item,.s-content table{font-size:1rem}.Brand,h1,h2,h3,h4,h5,h6{font-weight:400}.Button,.Pager li>a{border-radius:0}.HomepageButtons .Button--hero{font-weight:400;font-size:1.309rem}.Page__header{border-bottom:2px solid #e8d5d3}.Pager li>a{border:2px solid #e8d5d3}.Pager li>a:focus,.Pager li>a:hover{background-color:#e8d5d3}.Pager--prev a::before{content:"\2190\00a0"}.Pager--next a::after{content:"\00a0\2192"}.Navbar{height:auto;box-shadow:none}.Navbar .Brand{float:none;line-height:inherit;height:auto}.Homepage{padding-top:10px!important}.Nav__item{font-size:1.309rem}.Nav .Nav .Nav__item a .Nav__arrow:before,.Nav__arrow:before{font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;width:1ch;height:1ch}.TableOfContentsContainer__title{border-bottom:4px solid #e8d5d3}.Columns__right--full .TableOfContentsContainer .TableOfContentsContainer__content>.TableOfContents{border-right:2px solid #e8d5d3}.Columns__right--full .TableOfContentsContainer a{border-bottom:1px solid #e8d5d3}.clients thead tr:first-child th{text-align:left}.clients thead tr:first-child th:first-child{width:15%}.clients thead tr:first-child th:nth-child(3){width:50%;text-align:center}.clients thead tr+tr th{width:16.66%;text-align:center}.clients tbody td:nth-child(3),.clients tbody td:nth-child(4),.clients tbody td:nth-child(5){text-align:center}.clients tbody td.Y{color:#2c9a42}.clients tbody td.N{color:#e63c2f}.hljs,.s-content pre{background:#15284b;color:#e8d5d3}.hljs{display:block;overflow-x:auto;padding:.5em}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-comment,.hljs-quote{color:#978e9c}.hljs-addition,.hljs-keyword,.hljs-selector-tag{color:#acb39a}.hljs-doctag,.hljs-literal,.hljs-meta .hljs-meta-string,.hljs-number,.hljs-regexp,.hljs-string{color:#93b7bb}.hljs-name,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-title{color:#82b7e5}.hljs-attr,.hljs-attribute,.hljs-class .hljs-title,.hljs-template-variable,.hljs-type,.hljs-variable{color:#c5b031}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-meta .hljs-keyword,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-subst,.hljs-symbol{color:#ea8031}.hljs-built_in,.hljs-deletion{color:#e63c2f}.hljs-formula{background:#686986}@media (min-width:850px){.Columns__left{border:0}} \ No newline at end of file +html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;font-size:14px}body{margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress,sub,sup{vertical-align:baseline}.s-content pre code:after,.s-content pre code:before,[hidden],template{display:none}a{background-color:transparent;-webkit-text-decoration-skip:objects;text-decoration:none;color:#e63c2f}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}.s-content blockquote cite,dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;clear:both;margin:1em 0;border:0;border-top:1px solid #ddd}button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:700}button,hr,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}*,:after,:before{box-sizing:border-box}@media (min-width:850px){html{font-size:16px}}body,html{height:100%;background-color:#fff;color:#15284b}.Columns__left{background-color:#e8d5d3}.Columns__right__content{padding:10px;background-color:#fff}@media (max-width:768px){html:not(.no-js) .Collapsible__content{height:0;overflow:hidden;transition:height 400ms ease-in-out}}.Collapsible__trigger{margin:12px;padding:7px 10px;background-color:transparent;border:0;float:right;background-image:none;filter:none;box-shadow:none}.Collapsible__trigger__bar{display:block;width:18px;height:2px;margin-top:2px;margin-bottom:3px;background-color:#e8d5d3}.Collapsible__trigger:hover{background-color:#93b7bb;box-shadow:none}.Collapsible__trigger:hover .Collapsible__trigger__bar{background-color:#15284b}@media screen and (min-width:769px){body{background-color:#15284b}.Navbar{position:fixed;z-index:1030;width:100%}.Collapsible__trigger{display:none!important}.Collapsible__content{display:block!important}.Columns{height:100%}.Columns:after,.Columns:before{content:" ";display:table}.Columns:after{clear:both}.Columns__left,.Columns__right{position:relative;min-height:1px;float:left;overflow:auto;height:100%}.Columns__left{width:25%;border-right:1px solid #e7e7e9;overflow-x:hidden}.Columns__right{width:75%}.Columns__right__content{padding:0 20px 20px;min-height:100%}}.Page{max-width:860px}body{font-family:"Cabin","Trebuchet MS",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-feature-settings:"kern" 1;-webkit-font-kerning:normal;font-kerning:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;line-height:1.618}h1,h2,h3,h4,h5,h6{font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.s-content h1,.s-content h2,.s-content h3,.s-content h4,.s-content h5,.s-content h6{cursor:text;line-height:1.4em;margin:2em 0 .5em}.s-content h1 code,.s-content h1 tt,.s-content h2 code,.s-content h2 tt,.s-content h3 code,.s-content h3 tt,.s-content h4 code,.s-content h4 tt,.s-content h5 code,.s-content h5 tt,.s-content h6 code,.s-content h6 tt{font-size:inherit}.s-content h1 i,.s-content h2 i,.s-content h3 i,.s-content h4 i,.s-content h5 i,.s-content h6 i{font-size:.7em}.s-content h1 p,.s-content h2 p,.s-content h3 p,.s-content h4 p,.s-content h5 p,.s-content h6 p{margin-top:0}.s-content h1{margin-top:0;font-size:2.618rem}.s-content h2{font-size:2rem}.s-content h3{font-size:1.618rem}.s-content h4,.s-content h5,.s-content h6,.s-content small{font-size:1.309rem}.s-content a{text-decoration:underline}.s-content p{margin-bottom:1.3em}.s-content ol,.s-content ul{padding-left:2em}.s-content ul p,.s-content ul ul{margin:0}.s-content dl{padding:0}.s-content dl dt{font-weight:700;font-style:italic;padding:0;margin:15px 0 5px}.s-content dl dt:first-child{padding:0}.s-content dl dd{margin:0 0 15px;padding:0 15px}.s-content blockquote{margin:.75em 2em;padding:.5em 1em;font-style:italic;border-left:.25em solid #15284b}.s-content blockquote cite:before{content:"\2014";padding-right:.5em}.s-content table{width:100%;padding:0;margin-bottom:1em;border-collapse:separate;border-spacing:2px;border:2px solid #b3aab1}.s-content table+table{margin-top:1em}.s-content table tr{background-color:#fff;margin:0;padding:0;border-top:0}.s-content table tr:nth-child(2n){background-color:transparent}.s-content table th{font-weight:700;background:#e8d5d3}.s-content table td,.s-content table th{margin:0;padding:.5em}.s-content blockquote>:first-child,.s-content dl dd>:first-child,.s-content dl dt>:first-child,.s-content ol>:first-child,.s-content table td>:first-child,.s-content table th>:first-child,.s-content ul>:first-child{margin-top:0}.s-content blockquote>:last-child,.s-content dl dd>:last-child,.s-content dl dt>:last-child,.s-content ol>:last-child,.s-content table td>:last-child,.s-content table th>:last-child,.s-content ul>:last-child{margin-bottom:0}.s-content img{max-width:100%;display:block;margin:0 auto}.s-content code{font-family:Monaco,Menlo,Consolas,"Lucida Console","Courier New",monospace;padding-top:.1rem;padding-bottom:.1rem;background:#f9f5f4;border-radius:0;box-shadow:none;display:inline-block;padding:.5ch;border:0}.s-content code:after,.s-content code:before{letter-spacing:-.2em;content:"\00a0"}.s-content pre{background:#f5f2f0;line-height:1.5em;overflow:auto;border:0;border-radius:0;padding:.75em 20px;margin:0 -20px 20px}.s-content pre code{margin:0;padding:0;white-space:pre;box-shadow:none}.s-content pre code,.s-content pre tt{background-color:transparent;border:0}.s-content ins,.s-content u{text-decoration:none;border-bottom:1px solid #15284b}.s-content del a,.s-content ins a,.s-content u a{color:inherit}a.Link--external:after{content:" " url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAVklEQVR4Xn3PgQkAMQhDUXfqTu7kTtkpd5RA8AInfArtQ2iRXFWT2QedAfttj2FsPIOE1eCOlEuoWWjgzYaB/IkeGOrxXhqB+uA9Bfcm0lAZuh+YIeAD+cAqSz4kCMUAAAAASUVORK5CYII=)}a.Link--broken{color:red}p{margin:0 0 1em}.Button{display:inline-block;text-align:center;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;margin-bottom:0}.Button--small{font-size:12px;line-height:1.5;border-radius:3px}.Button--default{color:#333;background-color:#fff;border-color:#ccc}.Button--default.Button--active{color:#333;background-color:#e6e6e6;border-color:#adadad}.Brand,.Nav__item a:hover{color:#15284b;text-shadow:none}.Brand,.Navbar{background-color:#e63c2f}.Brand{display:block;padding:.75em .6em;font-size:2rem;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.Navbar{box-shadow:0 1px 5px rgba(0,0,0,.25);margin-bottom:0}.CodeToggler{padding:0 20px}.CodeToggler__text{font-size:12px;line-height:1.5;padding:6px 10px 6px 0;display:inline-block;vertical-align:middle}.no-js .CodeToggler{display:none}.Nav{margin:0;padding:0}.Nav__arrow{display:inline-block;position:relative;width:16px;margin-left:-16px}.Nav__arrow:before{position:absolute;display:block;content:"";margin:-.25em 0 0 -.4em;left:50%;top:50%;border-right:.15em solid #15284b;border-top:.15em solid #15284b;transform:rotate(45deg);transition-duration:.3s}.Nav__item,.Nav__item a{display:block}.Nav__item a{margin:0;padding:6px 15px 6px 20px;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-weight:400;color:#15284b;text-shadow:none}.Nav__item a:hover{background-color:#93b7bb}.Nav .Nav{margin-left:15px}html:not(.no-js) .Nav .Nav{height:0;transition:height 400ms ease-in-out;overflow:hidden}.Nav .Nav .Nav__item a{margin:0 0 0 -15px;padding:3px 30px;font-family:"Cabin","Trebuchet MS",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;color:#15284b;opacity:.7}.HomepageButtons .Button--hero:hover,.Nav .Nav .Nav__item a:hover{opacity:1}.Nav .Nav .Nav__item--active a{color:#15284b}.Nav__item--active>a,.Nav__item--open>a{background-color:#93b7bb}.Nav__item--open>a>.Nav__arrow:before{margin-left:-.25em;transform:rotate(135deg)}.Page__header{margin:0 0 10px;padding:0}.Page__header:after,.Page__header:before{content:" ";display:table}.Page__header:after{clear:both}.Page__header h1{margin:0;padding:0;line-height:57px}.Page__header--separator{height:.6em}.Page__header a{text-decoration:none}.Page__header .EditOn,.Page__header .ModifiedDate{float:left;font-size:10px;color:gray}.Page__header .EditOn{float:right}.Links,.Twitter{padding:0 20px}.Links a{font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-weight:400;color:#15284b;line-height:2em}.Twitter{font:11px/18px "Helvetica Neue",Arial,sans-serif}.Twitter__button{text-decoration:none;display:inline-block;vertical-align:top;zoom:1;position:relative;height:20px;box-sizing:border-box;padding:1px 8px 1px 6px;background-color:#1b95e0;color:#fff;border-radius:3px;font-weight:500;cursor:pointer}.Twitter__button .Twitter__button__label{display:inline-block;vertical-align:top;zoom:1;margin-left:3px;white-space:nowrap}.Twitter__button svg{position:relative;top:2px;display:inline-block;width:14px;height:14px}.PoweredBy{padding:0 20px 1rem;font-size:1.309rem}.Search{position:relative}.Search__field{display:block;width:100%;height:34px;padding:6px 30px 6px 20px;color:#555;border-width:0 0 1px;border-bottom:1px solid #ccc;background:#fff;transition:border-color ease-in-out .15s}.Search__field:focus{border-color:#93b7bb;outline:0}.Search__icon{position:absolute;right:9px;top:9px;width:16px;height:16px}.Navbar .Search{float:right;margin:8px 20px}.Navbar .Search__field{box-shadow:inset 0 1px 1px rgba(0,0,0,.075);border-width:0;border-radius:4px;padding-left:10px}.TableOfContentsContainer{float:right;min-width:300px;max-width:25%;padding-left:1em}.TableOfContentsContainer__title{margin-bottom:0!important}.TableOfContentsContainer__content{border:1px solid #efefef;border-width:4px 2px 2px 6px}.TableOfContentsContainer__content>.TableOfContents>li+li{border-top:1px solid #ddd}ul.TableOfContents{font-size:1rem;padding-left:0;margin:0;list-style-type:none;border-left:6px solid #e8d5d3}ul.TableOfContents p{margin-bottom:0}ul.TableOfContents a{text-decoration:none;display:block;padding:.2em 0 .2em .75em}ul.TableOfContents .TableOfContents{padding-left:.75em}.Pager{padding-left:0;margin:1em 0;list-style:none;text-align:center}.Pager:after,.Pager:before{content:" ";display:table}.Pager,.Pager:after{clear:both}.Pager li{display:inline}.Pager li>a{display:inline-block;padding:5px 14px;background-color:#fff}.Pager li>a:focus,.Pager li>a:hover{text-decoration:none}.Pager--next>a{float:right}.Pager--prev>a{float:left}.Checkbox{position:relative;display:block;padding-left:30px;cursor:pointer}.Checkbox input{position:absolute;z-index:-1;opacity:0}.Checkbox__indicator{position:absolute;top:50%;left:0;width:20px;height:20px;margin-top:-10px;background:#e6e6e6}.Checkbox__indicator:after{position:absolute;display:none;content:""}.Checkbox input:focus~.Checkbox__indicator,.Checkbox:hover input~.Checkbox__indicator{background:#ccc}.Checkbox input:checked~.Checkbox__indicator{background:#15284b}.Checkbox input:checked~.Checkbox__indicator:after{display:block}.Checkbox input:checked:focus~.Checkbox__indicator,.Checkbox:hover input:not([disabled]):checked~.Checkbox__indicator{background:#93b7bb}.Checkbox input:disabled~.Checkbox__indicator{pointer-events:none;opacity:.6;background:#e6e6e6}.Checkbox .Checkbox__indicator:after{top:4px;left:8px;width:5px;height:10px;transform:rotate(45deg);border:solid #fff;border-width:0 2px 2px 0}.Checkbox input:disabled~.Checkbox__indicator:after{border-color:#7b7b7b}.Hidden{display:none}.Container{margin-right:auto;margin-left:auto}.Container--inner{width:80%;margin:0 auto}@media (min-width:1200px){.Container{width:1170px}}@media (min-width:992px){.Container{width:970px}}@media (min-width:769px){.Container{width:750px}}.Homepage{background-color:#fff;border-radius:0;border:0;color:#15284b;overflow:hidden;padding-bottom:0;margin-bottom:0;box-shadow:none}.HomepageTitle h2{width:80%;font-size:30px;margin:20px auto;text-align:center}.HomepageImage img{display:block;max-width:80%;margin:0 auto;height:auto}.HomepageButtons{padding:20px 0;background-color:#e8d5d3;text-align:center}.HomepageButtons:after,.HomepageButtons:before{content:" ";display:table}.HomepageButtons:after{clear:both}.HomepageButtons .Button--hero{padding:20px 30px;border-radius:0;text-shadow:none;opacity:.8;margin:0 10px;text-transform:uppercase;border:5px solid #15284b;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;background-image:none;filter:none;box-shadow:none}@media (max-width:768px){.HomepageButtons .Button--hero{display:block;margin-bottom:10px}}.HomepageButtons .Button--hero.Button--secondary{background-color:#93b7bb;color:#15284b}.HomepageButtons .Button--hero.Button--primary{background-color:#15284b;color:#e8d5d3}.HomepageContent{background-color:#fff;padding:40px 0}.HomepageContent ol li,.HomepageContent ul li{list-style:none;margin-bottom:.5em;position:relative}.HomepageContent ol li:before,.HomepageContent ul li:before{position:absolute;top:50%;left:-1.5em;content:"";width:0;height:0;border:.5em solid transparent;border-left:.5em solid #93b7bb;float:left;display:block;margin-top:-.5em}.HomepageContent .HeroText,.HomepageFooter__links li a{font-size:16px;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.HomepageContent .HeroText{font-weight:300;margin-bottom:20px;line-height:1.4}@media (min-width:769px){.HomepageContent{padding:40px 20px}.HomepageContent .HeroText{font-size:21px}.HomepageContent .Row{margin:0 -15px}.HomepageContent .Row__half,.HomepageContent .Row__quarter,.HomepageContent .Row__third{float:left;position:relative;min-height:1px;padding-left:15px;padding-right:15px}.HomepageContent .Row__third{width:33.333333%}.HomepageContent .Row__half{width:50%}.HomepageContent .Row__quarter{width:25%}}.HomepageFooter{background-color:#15284b;color:#93b7bb;border:0;box-shadow:none}.HomepageFooter:after,.HomepageFooter:before{content:" ";display:table}.HomepageFooter:after{clear:both}@media (max-width:768px){.HomepageFooter{padding:0 20px;text-align:center}.HomepageFooter .HomepageFooter__links{padding-left:0;list-style-type:none}}@media (min-width:769px){.HomepageFooter .HomepageFooter__links{float:left}.HomepageFooter .HomepageFooter__twitter{float:right}}.HomepageFooter__links,.HomepageFooter__twitter{margin:40px 0}.HomepageFooter__links li a{line-height:32px;font-weight:700}.HomepageFooter__links li a:hover{text-decoration:underline}.HomepageFooter .Twitter__button{margin-bottom:20px}@media print{*{text-shadow:none!important;color:#000!important;background:0 0!important;box-shadow:none!important}h1,h2,h3,h4,h5,h6{page-break-after:avoid;page-break-before:auto}blockquote,img,pre{page-break-inside:avoid}blockquote,pre{border:1px solid #999;font-style:italic}img{border:0}a,a:visited{text-decoration:underline}abbr[title]:after{content:" (" attr(title) ")"}q{quotes:none}.s-content a[href^="#"]:after,q:before{content:""}q:after{content:" (" attr(cite) ")"}.PageBreak{display:block;page-break-before:always}.NoPrint,.Pager,aside{display:none}.Columns__right{width:100%!important}.s-content a:after{content:" (" attr(href) ")";font-size:80%;word-wrap:break-word}h1 a[href]:after{font-size:50%}}@font-face{font-family:'League Gothic';src:url(fonts/leaguegothic.woff2) format('woff2'),url(fonts/leaguegothic.woff) format('woff');font-style:normal;font-display:swap}@font-face{font-family:'Cabin';src:url(fonts/cabin-regular.woff2) format('woff2'),url(fonts/cabin-regular.woff) format('woff');font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:'Cabin';src:url(fonts/cabin-italic.woff2) format('woff2'),url(fonts/cabin-italic.woff) format('woff');font-style:italic;font-display:swap}@font-face{font-family:'Cabin';src:url(fonts/cabin-bold.woff2) format('woff2'),url(fonts/cabin-bold.woff) format('woff');font-weight:700;font-style:normal;font-display:swap}.s-content code::after,.s-content code::before,a.Link--external::after{content:''}pre .s-content code{display:inline}.s-content table tbody,.s-content table thead{background-color:#fff}.s-content table tr:nth-child(2n) td{background-color:#f9f5f4}.s-content table td,.s-content table th{border:0}.Nav__item .Nav__item,.s-content table{font-size:1rem}.Brand,h1,h2,h3,h4,h5,h6{font-weight:400}.Button,.Pager li>a{border-radius:0}.HomepageButtons .Button--hero{font-weight:400;font-size:1.309rem}.Page__header{border-bottom:2px solid #e8d5d3}.Pager li>a{border:2px solid #e8d5d3}.Pager li>a:focus,.Pager li>a:hover{background-color:#e8d5d3}.Pager--prev a::before{content:"\2190\00a0"}.Pager--next a::after{content:"\00a0\2192"}.Navbar{height:auto;box-shadow:none}.Navbar .Brand{float:none;line-height:inherit;height:auto}.Homepage{padding-top:10px!important}.Nav__item{font-size:1.309rem}.Nav .Nav .Nav__item a .Nav__arrow:before,.Nav__arrow:before{font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;width:1ch;height:1ch}.TableOfContentsContainer__title{border-bottom:4px solid #e8d5d3}.Columns__right--full .TableOfContentsContainer .TableOfContentsContainer__content>.TableOfContents{border-right:2px solid #e8d5d3}.Columns__right--full .TableOfContentsContainer a{border-bottom:1px solid #e8d5d3}.clients thead tr:first-child th{text-align:left}.clients thead tr:first-child th:first-child{width:15%}.clients thead tr:first-child th:nth-child(3){width:50%;text-align:center}.clients thead tr+tr th{width:12%;text-align:center}.clients tbody td:nth-child(3),.clients tbody td:nth-child(4),.clients tbody td:nth-child(5),.clients tbody td:nth-child(6){text-align:center}.clients tbody td.Y{color:#2c9a42}.clients tbody td.N{color:#e63c2f}.hljs,.s-content pre{background:#15284b;color:#e8d5d3}.hljs{display:block;overflow-x:auto;padding:.5em}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-comment,.hljs-quote{color:#978e9c}.hljs-addition,.hljs-keyword,.hljs-selector-tag{color:#acb39a}.hljs-doctag,.hljs-literal,.hljs-meta .hljs-meta-string,.hljs-number,.hljs-regexp,.hljs-string{color:#93b7bb}.hljs-name,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-title{color:#82b7e5}.hljs-attr,.hljs-attribute,.hljs-class .hljs-title,.hljs-template-variable,.hljs-type,.hljs-variable{color:#c5b031}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-meta .hljs-keyword,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-subst,.hljs-symbol{color:#ea8031}.hljs-built_in,.hljs-deletion{color:#e63c2f}.hljs-formula{background:#686986}@media (min-width:850px){.Columns__left{border:0}} \ No newline at end of file diff --git a/docs/theme/src/arsse.scss b/docs/theme/src/arsse.scss index 43a26c19..6f5d9d83 100644 --- a/docs/theme/src/arsse.scss +++ b/docs/theme/src/arsse.scss @@ -245,12 +245,12 @@ ul.TableOfContents { } thead tr + tr th { - width: 16.66%; + width: 12%; text-align: center; } tbody td { - &:nth-child(3), &:nth-child(4), &:nth-child(5) { + &:nth-child(3), &:nth-child(4), &:nth-child(5), &:nth-child(6) { text-align: center; } From 3d3c20de5cb7381af703a0b10de02a93dc53eab1 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 6 Nov 2020 15:57:27 -0500 Subject: [PATCH 025/366] Don't anticipate API features --- lib/Database.php | 34 +++++----------------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index b852ecc5..6732ecab 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1102,7 +1102,7 @@ class Database { $tr = $this->db->begin(); $icon = null; if ($feed->iconUrl) { - $icon = $this->iconGetByUrl($feed->iconUrl); + $icon = $this->db->prepare("SELECT id, url, type, data from arsse_icons where url = ?", "str")->run($feed->iconUrl)->getRow(); if ($icon) { // update the existing icon if necessary if ($feed->iconType !== $icon['type'] || $feed->iconData !== $icon['data']) { @@ -1260,38 +1260,14 @@ class Database { )->run($feedID, $vId, $vHashUT, $vHashUC, $vHashTC); } - /** Retrieve a feed icon by URL, for use during feed refreshing + /** Lists icons for feeds to which a user is subscribed * - * @param string $url The URL of the icon to retrieve - */ - protected function iconGetByUrl(string $url): ?array { - return $this->db->prepare("SELECT id, url, type, data from arsse_icons where url = ?", "str")->run($url)->getRow(); - } - - /** Retrieves information about an icon + * The returned information for each icon is: * - * The returned information is: - * - * - "id": The umeric identifier of the icon (not the subscription) + * - "id": The umeric identifier of the icon * - "url": The URL of the icon * - "type": The Content-Type of the icon e.g. "image/png" - * - "data": The icon itself, as a binary sring; if $withData is false this will be null - * - * @param string $user The user whose icon is to be retrieved - * @param int $subscription The numeric identifier of the icon - */ - public function iconGet(string $user, int $id): array { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } - $out = $this->db->prepare("SELECT i.id, i.url, i.type, i.data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id where s.owner = ? and i.id = ?", "str", "int")->run($user, $id)->getRow(); - if (!$out) { - throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "subscription", 'id' => $id]); - } - return $out; - } - - /** Lists icons for feeds to which a user is subscribed, with or without the binary content of the icon itself + * - "data": The icon itself, as a binary sring * * @param string $user The user whose subscription icons are to be retrieved */ From 311910795ab9fdcb4a915fc928f286c066f01369 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 6 Nov 2020 17:06:01 -0500 Subject: [PATCH 026/366] More tests for icon cache --- lib/Database.php | 2 +- lib/Service.php | 2 ++ tests/cases/Database/SeriesCleanup.php | 13 ++++++++++ tests/cases/Database/SeriesIcon.php | 34 ++++++++++++++++++-------- tests/cases/Service/TestService.php | 2 ++ 5 files changed, 42 insertions(+), 11 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 6732ecab..6844541e 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1275,7 +1275,7 @@ class Database { if (!Arsse::$user->authorize($user, __FUNCTION__)) { throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); } - return $this->db->prepare("SELECT i.id, i.url, i.type, i.data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id where s.owner = ?", "str")->run($user); + return $this->db->prepare("SELECT distinct i.id, i.url, i.type, i.data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id where s.owner = ?", "str")->run($user); } /** Deletes orphaned icons from the database diff --git a/lib/Service.php b/lib/Service.php index 597421fd..a69b12c0 100644 --- a/lib/Service.php +++ b/lib/Service.php @@ -72,6 +72,8 @@ class Service { public static function cleanupPre(): bool { // mark unsubscribed feeds as orphaned and delete orphaned feeds that are beyond their retention period Arsse::$db->feedCleanup(); + // do the same for icons + Arsse::$db->iconCleanup(); // delete expired log-in sessions Arsse::$db->sessionCleanup(); return true; diff --git a/tests/cases/Database/SeriesCleanup.php b/tests/cases/Database/SeriesCleanup.php index b31f87c7..ad1d7f14 100644 --- a/tests/cases/Database/SeriesCleanup.php +++ b/tests/cases/Database/SeriesCleanup.php @@ -66,6 +66,19 @@ trait SeriesCleanup { ["da772f8fa13c11e78667001e673b2560", "class.class", "john.doe@example.com", $soon], ], ], + 'arsse_icons' => [ + 'columns' => [ + 'id' => "int", + 'url' => "str", + 'orphaned' => "datetime", + ], + 'rows' => [ + [1,'http://localhost:8000/Icon/PNG',null], + [2,'http://localhost:8000/Icon/GIF',null], + [3,'http://localhost:8000/Icon/SVG1',null], + [4,'http://localhost:8000/Icon/SVG2',null], + ], + ], 'arsse_feeds' => [ 'columns' => [ 'id' => "int", diff --git a/tests/cases/Database/SeriesIcon.php b/tests/cases/Database/SeriesIcon.php index 7a7e348c..d54a4ab9 100644 --- a/tests/cases/Database/SeriesIcon.php +++ b/tests/cases/Database/SeriesIcon.php @@ -53,16 +53,11 @@ trait SeriesIcon { 'icon' => "int", ], 'rows' => [ - [1,"http://localhost:8000/Feed/Matching/3","Ook",0,"",$past,$past,0,null], - [2,"http://localhost:8000/Feed/Matching/1","Eek",5,"There was an error last time",$past,$future,0,null], - [3,"http://localhost:8000/Feed/Fetching/Error?code=404","Ack",0,"",$past,$now,0,null], + [1,"http://localhost:8000/Feed/Matching/3","Ook",0,"",$past,$past,0,1], + [2,"http://localhost:8000/Feed/Matching/1","Eek",5,"There was an error last time",$past,$future,0,2], + [3,"http://localhost:8000/Feed/Fetching/Error?code=404","Ack",0,"",$past,$now,0,3], [4,"http://localhost:8000/Feed/NextFetch/NotModified?t=".time(),"Ooook",0,"",$past,$past,0,null], - [5,"http://localhost:8000/Feed/Parsing/Valid","Ooook",0,"",$past,$future,0,null], - // these feeds all test icon caching - [6,"http://localhost:8000/Feed/WithIcon/PNG",null,0,"",$past,$future,0,1], // no change when updated - [7,"http://localhost:8000/Feed/WithIcon/GIF",null,0,"",$past,$future,0,1], // icon ID 2 will be assigned to feed when updated - [8,"http://localhost:8000/Feed/WithIcon/SVG1",null,0,"",$past,$future,0,3], // icon ID 3 will be modified when updated - [9,"http://localhost:8000/Feed/WithIcon/SVG2",null,0,"",$past,$future,0,null], // icon ID 4 will be created and assigned to feed when updated + [5,"http://localhost:8000/Feed/Parsing/Valid","Ooook",0,"",$past,$future,0,2], ], ], 'arsse_subscriptions' => [ @@ -77,7 +72,7 @@ trait SeriesIcon { [3,'john.doe@example.com',3], [4,'john.doe@example.com',4], [5,'john.doe@example.com',5], - [6,'jane.doe@example.com',1], + [6,'jane.doe@example.com',5], ], ], ]; @@ -86,4 +81,23 @@ trait SeriesIcon { protected function tearDownSeriesIcon(): void { unset($this->data); } + + public function testListTheIconsOfAUser() { + $exp = [ + ['id' => 1,'url' => 'http://localhost:8000/Icon/PNG', 'type' => 'image/png', 'data' => base64_decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAADUlEQVQYV2NgYGBgAAAABQABijPjAAAAAABJRU5ErkJggg==")], + ['id' => 2,'url' => 'http://localhost:8000/Icon/GIF', 'type' => 'image/gif', 'data' => base64_decode("R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==")], + ['id' => 3,'url' => 'http://localhost:8000/Icon/SVG1', 'type' => 'image/svg+xml', 'data' => ''], + ]; + $this->assertResult($exp, Arsse::$db->iconList("john.doe@example.com")); + $exp = [ + ['id' => 2,'url' => 'http://localhost:8000/Icon/GIF', 'type' => 'image/gif', 'data' => base64_decode("R0lGODlhAQABAIABAAAAAP///yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==")], + ]; + $this->assertResult($exp, Arsse::$db->iconList("jane.doe@example.com")); + } + + public function testListTheIconsOfAUserWithoutAuthority() { + \Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->iconList("jane.doe@example.com"); + } } diff --git a/tests/cases/Service/TestService.php b/tests/cases/Service/TestService.php index 804cd553..9aef50cb 100644 --- a/tests/cases/Service/TestService.php +++ b/tests/cases/Service/TestService.php @@ -43,6 +43,7 @@ class TestService extends \JKingWeb\Arsse\Test\AbstractTest { public function testPerformPreCleanup(): void { $this->assertTrue(Service::cleanupPre()); \Phake::verify(Arsse::$db)->feedCleanup(); + \Phake::verify(Arsse::$db)->iconCleanup(); \Phake::verify(Arsse::$db)->sessionCleanup(); } @@ -76,6 +77,7 @@ class TestService extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify($d)->exec(); \Phake::verify($d)->clean(); \Phake::verify(Arsse::$db)->feedCleanup(); + \Phake::verify(Arsse::$db)->iconCleanup(); \Phake::verify(Arsse::$db)->sessionCleanup(); \Phake::verify(Arsse::$db)->articleCleanup(); \Phake::verify(Arsse::$db)->metaSet("service_last_checkin", $this->anything(), "datetime"); From 1d3725341a6fae70cdfa12a9544a08873a8a5961 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 6 Nov 2020 19:56:32 -0500 Subject: [PATCH 027/366] Fix detection of Xdebug for coverage --- RoboFile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RoboFile.php b/RoboFile.php index 17456c10..31b98924 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -99,7 +99,7 @@ class RoboFile extends \Robo\Tasks { return $php; } elseif (file_exists($dir."pcov.$ext")) { return "$php -d extension=pcov.$ext -d pcov.enabled=1 -d pcov.directory=$code"; - } elseif (file_exists($dir."pcov.$ext")) { + } elseif (file_exists($dir."xdebug.$ext")) { return "$php -d zend_extension=xdebug.$ext"; } else { if (IS_WIN) { From b62c11a43eab6f103110346989ea44026802e4b2 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 7 Nov 2020 08:11:06 -0500 Subject: [PATCH 028/366] Lasts tests for icon cache; fixes #177 --- lib/Database.php | 2 +- tests/cases/Database/SeriesCleanup.php | 40 +++++++++++++++++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 6844541e..92ddb891 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1287,7 +1287,7 @@ class Database { // first unmark any icons which are no longer orphaned; an icon is considered orphaned if it is not used or only used by feeds which are themselves orphaned $this->db->query("UPDATE arsse_icons set orphaned = null where id in (select distinct icon from arsse_feeds where icon is not null and orphaned is null)"); // next mark any newly orphaned icons with the current date and time - $this->db->query("UPDATE arsse_icons set orphaned = CURRENT_TIMESTAMP where id not in (select distinct icon from arsse_feeds where icon is not null and orphaned is null)"); + $this->db->query("UPDATE arsse_icons set orphaned = CURRENT_TIMESTAMP where orphaned is null and id not in (select distinct icon from arsse_feeds where icon is not null and orphaned is null)"); // finally delete icons that have been orphaned longer than the feed retention period, if a a purge threshold has been specified $out = 0; if (Arsse::$conf->purgeFeeds) { diff --git a/tests/cases/Database/SeriesCleanup.php b/tests/cases/Database/SeriesCleanup.php index ad1d7f14..1a0e1c7e 100644 --- a/tests/cases/Database/SeriesCleanup.php +++ b/tests/cases/Database/SeriesCleanup.php @@ -73,10 +73,9 @@ trait SeriesCleanup { 'orphaned' => "datetime", ], 'rows' => [ - [1,'http://localhost:8000/Icon/PNG',null], - [2,'http://localhost:8000/Icon/GIF',null], + [1,'http://localhost:8000/Icon/PNG',$daybefore], + [2,'http://localhost:8000/Icon/GIF',$daybefore], [3,'http://localhost:8000/Icon/SVG1',null], - [4,'http://localhost:8000/Icon/SVG2',null], ], ], 'arsse_feeds' => [ @@ -86,12 +85,13 @@ trait SeriesCleanup { 'title' => "str", 'orphaned' => "datetime", 'size' => "int", + 'icon' => "int", ], 'rows' => [ - [1,"http://example.com/1","",$daybefore,2], //latest two articles should be kept - [2,"http://example.com/2","",$yesterday,0], - [3,"http://example.com/3","",null,0], - [4,"http://example.com/4","",$nowish,0], + [1,"http://example.com/1","",$daybefore,2,null], //latest two articles should be kept + [2,"http://example.com/2","",$yesterday,0,2], + [3,"http://example.com/3","",null,0,1], + [4,"http://example.com/4","",$nowish,0,null], ], ], 'arsse_subscriptions' => [ @@ -193,6 +193,32 @@ trait SeriesCleanup { $this->compareExpectations(static::$drv, $state); } + public function testCleanUpOrphanedIcons(): void { + Arsse::$db->iconCleanup(); + $now = gmdate("Y-m-d H:i:s"); + $state = $this->primeExpectations($this->data, [ + 'arsse_icons' => ["id","orphaned"], + ]); + $state['arsse_icons']['rows'][0][1] = null; + unset($state['arsse_icons']['rows'][1]); + $state['arsse_icons']['rows'][2][1] = $now; + $this->compareExpectations(static::$drv, $state); + } + + public function testCleanUpOrphanedIconsWithUnlimitedRetention(): void { + Arsse::$conf->import([ + 'purgeFeeds' => null, + ]); + Arsse::$db->iconCleanup(); + $now = gmdate("Y-m-d H:i:s"); + $state = $this->primeExpectations($this->data, [ + 'arsse_icons' => ["id","orphaned"], + ]); + $state['arsse_icons']['rows'][0][1] = null; + $state['arsse_icons']['rows'][2][1] = $now; + $this->compareExpectations(static::$drv, $state); + } + public function testCleanUpOldArticlesWithStandardRetention(): void { Arsse::$db->articleCleanup(); $state = $this->primeExpectations($this->data, [ From 9fb185a8e2265e188eea958492e4355439104f2d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 7 Nov 2020 12:00:41 -0500 Subject: [PATCH 029/366] Add TT-RSS Web client to manual --- docs/en/040_Compatible_Clients.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/en/040_Compatible_Clients.md b/docs/en/040_Compatible_Clients.md index cd82678b..b8a22499 100644 --- a/docs/en/040_Compatible_Clients.md +++ b/docs/en/040_Compatible_Clients.md @@ -30,6 +30,17 @@ The Arsse does not at this time have any first party clients. However, because T

Three-pane alternative front-end for Minflux.

+ + Tiny Tiny RSS Progressive Web App + + ✘ + ✘ + ✔ + ✘ + +

Does not (yet) support HTTP authentication.

+ + From ee050e505c4551e7ba7a6a8bc93a42d4b4a8078f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 7 Nov 2020 14:43:46 -0500 Subject: [PATCH 030/366] Add more Android clients to manual --- docs/en/040_Compatible_Clients.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/en/040_Compatible_Clients.md b/docs/en/040_Compatible_Clients.md index b8a22499..e3a88006 100644 --- a/docs/en/040_Compatible_Clients.md +++ b/docs/en/040_Compatible_Clients.md @@ -182,6 +182,15 @@ The Arsse does not at this time have any first party clients. However, because T ✘ + + NewsJet RSS + Android + ✘ + ✘ + ✔ + ✘ + + Newsout Android, iOS @@ -224,6 +233,15 @@ The Arsse does not at this time have any first party clients. However, because T

Fetches favicons independently.

+ + Readrops + Android + ✘ + ✔ + ✘ + ✘ + + Reed Android From 532ce4a502d626b0b7b23926208eb43be50039c7 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 9 Nov 2020 13:43:07 -0500 Subject: [PATCH 031/366] Prototype changes to user management The driver itself has not been expnaded; more is probably required to ensure metadata is kept in sync and users created when the internal database does not list a user an external database claims to have --- lib/AbstractException.php | 2 ++ lib/Database.php | 32 +++++++++++++++++++++++++++++ lib/User.php | 41 +++++++++++++++++++++++++++++++++++++ lib/User/ExceptionInput.php | 10 +++++++++ 4 files changed, 85 insertions(+) create mode 100644 lib/User/ExceptionInput.php diff --git a/lib/AbstractException.php b/lib/AbstractException.php index 706465e0..ebeeccc3 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -75,6 +75,8 @@ abstract class AbstractException extends \Exception { "User/Exception.authFailed" => 10412, "User/ExceptionAuthz.notAuthorized" => 10421, "User/ExceptionSession.invalid" => 10431, + "User/ExceptionInput.invalidTimezone" => 10441, + "User/ExceptionInput.invalidBoolean" => 10442, "Feed/Exception.internalError" => 10500, "Feed/Exception.invalidCertificate" => 10501, "Feed/Exception.invalidUrl" => 10502, diff --git a/lib/Database.php b/lib/Database.php index 92ddb891..6dc02ddb 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -37,6 +37,9 @@ use JKingWeb\Arsse\Misc\URL; * associations with articles. There has been an effort to keep public method * names consistent throughout, but protected methods, having different * concerns, will typically follow different conventions. + * + * Note that operations on users should be performed with the User class rather + * than the Database class directly. This is to allow for alternate user sources. */ class Database { /** The version number of the latest schema the interface is aware of */ @@ -310,6 +313,35 @@ class Database { $this->db->prepare("UPDATE arsse_users set password = ? where id = ?", "str", "str")->run($hash, $user); return true; } + + public function userPropertiesGet(string $user): array { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } elseif (!$this->userExists($user)) { + throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + } + $out = $this->db->prepare("SELECT num, admin, lang, tz, sort_asc from arsse_users where id = ?", "str")->run($user)->getRow(); + settype($out['admin'], "bool"); + settype($out['sort_asc'], "bool"); + return $out; + } + + public function userPropertiesSet(string $user, array $data): bool { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } elseif (!$this->userExists($user)) { + throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + } + $allowed = [ + 'admin' => "strict bool", + 'lang' => "str", + 'tz' => "strict str", + 'sort_asc' => "strict bool", + ]; + [$setClause, $setTypes, $setValues] = $this->generateSet($data, $allowed); + return (bool) $this->$db->prepare("UPDATE arsse_users set $setClause where user = ?", $setTypes, "str")->run($setValues, $user)->changes(); + + } /** Creates a new session for the given user and returns the session identifier */ public function sessionCreate(string $user): string { diff --git a/lib/User.php b/lib/User.php index f5299914..e39516ce 100644 --- a/lib/User.php +++ b/lib/User.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse; +use JKingWeb\Arsse\Misc\ValueInfo as V; use PasswordGenerator\Generator as PassGen; class User { @@ -120,4 +121,44 @@ class User { public function generatePassword(): string { return (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get(); } + + public function propertiesGet(string $user): array { + // unconditionally retrieve from the database to get at least the user number, and anything else the driver does not provide + $out = Arsse::$db->userPropertiesGet($user); + // layer on the driver's data + $extra = $this->u->userPropertiesGet($user); + foreach (["lang", "tz", "admin", "sort_asc"] as $k) { + if (array_key_exists($k, $extra)) { + $out[$k] = $extra[$k] ?? $out[$k]; + } + } + return $out; + } + + public function propertiesSet(string $user, array $data): bool { + $in = []; + if (array_key_exists("tz", $data)) { + if (!is_string($data['tz'])) { + throw new User\ExceptionInput("invalidTimezone"); + } elseif (!in_array($data['tz'], \DateTimeZone::listIdentifiers())) { + throw new User\ExceptionInput("invalidTimezone", $data['tz']); + } + $in['tz'] = $data['tz']; + } + foreach (["admin", "sort_asc"] as $k) { + if (array_key_exists($k, $data)) { + if (($v = V::normalize($data[$k], V::T_BOOL)) === null) { + throw new User\ExceptionInput("invalidBoolean", $k); + } + $in[$k] = $v; + } + } + if (array_key_exists("lang", $data)) { + $in['lang'] = V::normalize($data['lang'], V::T_STRING | M_NULL); + } + $out = $this->u->userPropertiesSet($user, $in); + // synchronize the internal database + Arsse::$db->userPropertiesSet($user, $in); + return $out; + } } diff --git a/lib/User/ExceptionInput.php b/lib/User/ExceptionInput.php new file mode 100644 index 00000000..aea8c131 --- /dev/null +++ b/lib/User/ExceptionInput.php @@ -0,0 +1,10 @@ + Date: Mon, 9 Nov 2020 14:47:42 -0500 Subject: [PATCH 032/366] More client compatibility updates --- docs/en/040_Compatible_Clients.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/en/040_Compatible_Clients.md b/docs/en/040_Compatible_Clients.md index e3a88006..f4585c9b 100644 --- a/docs/en/040_Compatible_Clients.md +++ b/docs/en/040_Compatible_Clients.md @@ -115,7 +115,7 @@ The Arsse does not at this time have any first party clients. However, because T - Tiny Tiny RSS Reader + Tiny Tiny RSS Reader Windows ✘ ✘ @@ -350,7 +350,6 @@ The Arsse does not at this time have any first party clients. However, because T ✘ ✘ -

Does not support HTTP authentication.

From 576d7e16a86d28a1a5c28bdf89c642e318b1b910 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 9 Nov 2020 16:49:42 -0500 Subject: [PATCH 033/366] Fix handling of bytea-typed nulls --- lib/Db/PostgreSQL/Result.php | 4 +++- tests/cases/Db/BaseResult.php | 12 ++++++++++++ tests/cases/Db/PostgreSQL/TestResult.php | 1 + tests/cases/Db/PostgreSQLPDO/TestResult.php | 1 + 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/Db/PostgreSQL/Result.php b/lib/Db/PostgreSQL/Result.php index 67a7352d..2b4d1b63 100644 --- a/lib/Db/PostgreSQL/Result.php +++ b/lib/Db/PostgreSQL/Result.php @@ -49,7 +49,9 @@ class Result extends \JKingWeb\Arsse\Db\AbstractResult { $this->cur = pg_fetch_row($this->r, null, \PGSQL_ASSOC); if ($this->cur !== false) { foreach($this->blobs as $f) { - $this->cur[$f] = hex2bin(substr($this->cur[$f], 2)); + if ($this->cur[$f]) { + $this->cur[$f] = hex2bin(substr($this->cur[$f], 2)); + } } return true; } diff --git a/tests/cases/Db/BaseResult.php b/tests/cases/Db/BaseResult.php index a43956da..e848f51b 100644 --- a/tests/cases/Db/BaseResult.php +++ b/tests/cases/Db/BaseResult.php @@ -11,6 +11,7 @@ use JKingWeb\Arsse\Db\Result; abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { protected static $insertDefault = "INSERT INTO arsse_test default values"; protected static $selectBlob = "SELECT x'DEADBEEF' as \"blob\""; + protected static $selectNullBlob = "SELECT null as \"blob\""; protected static $interface; protected $resultClass; @@ -142,4 +143,15 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { $test = new $this->resultClass(...$this->makeResult(static::$selectBlob)); $this->assertEquals($exp, $test->getValue()); } + + public function testGetNullBlobRow(): void { + $exp = ['blob' => null]; + $test = new $this->resultClass(...$this->makeResult(static::$selectNullBlob)); + $this->assertEquals($exp, $test->getRow()); + } + + public function testGetNullBlobValue(): void { + $test = new $this->resultClass(...$this->makeResult(static::$selectNullBlob)); + $this->assertNull($test->getValue()); + } } diff --git a/tests/cases/Db/PostgreSQL/TestResult.php b/tests/cases/Db/PostgreSQL/TestResult.php index 658228e0..9a4413d8 100644 --- a/tests/cases/Db/PostgreSQL/TestResult.php +++ b/tests/cases/Db/PostgreSQL/TestResult.php @@ -16,6 +16,7 @@ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { protected static $createMeta = "CREATE TABLE arsse_meta(key text primary key not null, value text)"; protected static $createTest = "CREATE TABLE arsse_test(id bigserial primary key)"; protected static $selectBlob = "SELECT '\\xDEADBEEF'::bytea as blob"; + protected static $selectNullBlob = "SELECT null::bytea as blob"; protected function makeResult(string $q): array { $set = pg_query(static::$interface, $q); diff --git a/tests/cases/Db/PostgreSQLPDO/TestResult.php b/tests/cases/Db/PostgreSQLPDO/TestResult.php index caddba71..b3d0cb33 100644 --- a/tests/cases/Db/PostgreSQLPDO/TestResult.php +++ b/tests/cases/Db/PostgreSQLPDO/TestResult.php @@ -16,6 +16,7 @@ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { protected static $createMeta = "CREATE TABLE arsse_meta(key text primary key not null, value text)"; protected static $createTest = "CREATE TABLE arsse_test(id bigserial primary key)"; protected static $selectBlob = "SELECT '\\xDEADBEEF'::bytea as blob"; + protected static $selectNullBlob = "SELECT null::bytea as blob"; protected function makeResult(string $q): array { $set = static::$interface->query($q); From 771f79323cef438a1664ac31d29ebff9d147c4cd Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 9 Nov 2020 16:51:30 -0500 Subject: [PATCH 034/366] Strip out remnants of the authorizer --- lib/AbstractException.php | 1 - lib/Database.php | 166 +----------------- lib/User.php | 31 +--- lib/User/Driver.php | 2 - lib/User/Internal/Driver.php | 4 - locale/en.php | 5 - tests/cases/Database/AbstractTest.php | 1 - tests/cases/Database/SeriesArticle.php | 42 ----- tests/cases/Database/SeriesFolder.php | 42 ----- tests/cases/Database/SeriesIcon.php | 6 - tests/cases/Database/SeriesLabel.php | 49 ------ tests/cases/Database/SeriesSession.php | 15 -- tests/cases/Database/SeriesSubscription.php | 81 +-------- tests/cases/Database/SeriesTag.php | 55 ------ tests/cases/Database/SeriesToken.php | 15 -- tests/cases/Database/SeriesUser.php | 43 ----- tests/cases/ImportExport/TestImportExport.php | 1 - tests/cases/User/TestInternal.php | 52 ++---- tests/cases/User/TestUser.php | 132 ++++---------- 19 files changed, 71 insertions(+), 672 deletions(-) diff --git a/lib/AbstractException.php b/lib/AbstractException.php index ebeeccc3..93798ca1 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -73,7 +73,6 @@ abstract class AbstractException extends \Exception { "User/Exception.alreadyExists" => 10403, "User/Exception.authMissing" => 10411, "User/Exception.authFailed" => 10412, - "User/ExceptionAuthz.notAuthorized" => 10421, "User/ExceptionSession.invalid" => 10431, "User/ExceptionInput.invalidTimezone" => 10441, "User/ExceptionInput.invalidBoolean" => 10442, diff --git a/lib/Database.php b/lib/Database.php index 6dc02ddb..eff40a52 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -242,9 +242,6 @@ class Database { /** Returns whether the specified user exists in the database */ public function userExists(string $user): bool { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } return (bool) $this->db->prepare("SELECT count(*) from arsse_users where id = ?", "str")->run($user)->getValue(); } @@ -254,9 +251,7 @@ class Database { * @param string $passwordThe user's password in cleartext. It will be stored hashed */ public function userAdd(string $user, string $password): bool { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } elseif ($this->userExists($user)) { + if ($this->userExists($user)) { throw new User\Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]); } $hash = (strlen($password) > 0) ? password_hash($password, \PASSWORD_DEFAULT) : ""; @@ -267,9 +262,6 @@ class Database { /** Removes a user from the database */ public function userRemove(string $user): bool { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } if ($this->db->prepare("DELETE from arsse_users where id = ?", "str")->run($user)->changes() < 1) { throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } @@ -279,9 +271,6 @@ class Database { /** Returns a flat, indexed array of all users in the database */ public function userList(): array { $out = []; - if (!Arsse::$user->authorize("", __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => ""]); - } foreach ($this->db->query("SELECT id from arsse_users") as $user) { $out[] = $user['id']; } @@ -290,9 +279,7 @@ class Database { /** Retrieves the hashed password of a user */ public function userPasswordGet(string $user): ?string { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } elseif (!$this->userExists($user)) { + if (!$this->userExists($user)) { throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } return $this->db->prepare("SELECT password from arsse_users where id = ?", "str")->run($user)->getValue(); @@ -304,9 +291,7 @@ class Database { * @param string $password The new password, in cleartext. The password will be stored hashed. If null is passed, the password is unset and authentication not possible */ public function userPasswordSet(string $user, string $password = null): bool { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } elseif (!$this->userExists($user)) { + if (!$this->userExists($user)) { throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } $hash = (strlen($password ?? "") > 0) ? password_hash($password, \PASSWORD_DEFAULT) : $password; @@ -315,9 +300,7 @@ class Database { } public function userPropertiesGet(string $user): array { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } elseif (!$this->userExists($user)) { + if (!$this->userExists($user)) { throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } $out = $this->db->prepare("SELECT num, admin, lang, tz, sort_asc from arsse_users where id = ?", "str")->run($user)->getRow(); @@ -327,9 +310,7 @@ class Database { } public function userPropertiesSet(string $user, array $data): bool { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } elseif (!$this->userExists($user)) { + if (!$this->userExists($user)) { throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } $allowed = [ @@ -339,16 +320,12 @@ class Database { 'sort_asc' => "strict bool", ]; [$setClause, $setTypes, $setValues] = $this->generateSet($data, $allowed); - return (bool) $this->$db->prepare("UPDATE arsse_users set $setClause where user = ?", $setTypes, "str")->run($setValues, $user)->changes(); + return (bool) $this->db->prepare("UPDATE arsse_users set $setClause where user = ?", $setTypes, "str")->run($setValues, $user)->changes(); } /** Creates a new session for the given user and returns the session identifier */ public function sessionCreate(string $user): string { - // If the user isn't authorized to perform this action then throw an exception. - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } // generate a new session ID and expiry date $id = UUID::mint()->hex; $expires = Date::add(Arsse::$conf->userSessionTimeout); @@ -367,10 +344,6 @@ class Database { * @param string|null $id The identifier of the session to destroy */ public function sessionDestroy(string $user, string $id = null): bool { - // If the user isn't authorized to perform this action then throw an exception. - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } if (is_null($id)) { // delete all sessions and report success unconditionally if no identifier was specified $this->db->prepare("DELETE FROM arsse_sessions where \"user\" = ?", "str")->run($user); @@ -424,10 +397,7 @@ class Database { * @param \DateTimeInterface|null $expires An optional expiry date and time for the token */ public function tokenCreate(string $user, string $class, string $id = null, \DateTimeInterface $expires = null): string { - // If the user isn't authorized to perform this action then throw an exception. - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } elseif (!$this->userExists($user)) { + if (!$this->userExists($user)) { throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } // generate a token if it's not provided @@ -445,10 +415,6 @@ class Database { * @param string|null $id The ID of a specific token, or null for all tokens in the class */ public function tokenRevoke(string $user, string $class, string $id = null): bool { - // If the user isn't authorized to perform this action then throw an exception. - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } if (is_null($id)) { $out = $this->db->prepare("DELETE FROM arsse_tokens where \"user\" = ? and class = ?", "str", "str")->run($user, $class)->changes(); } else { @@ -484,10 +450,6 @@ class Database { * @param array $data An associative array defining the folder */ public function folderAdd(string $user, array $data): int { - // If the user isn't authorized to perform this action then throw an exception. - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } // normalize folder's parent, if there is one $parent = array_key_exists("parent", $data) ? $this->folderValidateId($user, $data['parent'])['id'] : null; // validate the folder name and parent (if specified); this also checks for duplicates @@ -512,10 +474,6 @@ class Database { * @param boolean $recursive Whether to list all descendents (true) or only direct children (false) */ public function folderList(string $user, $parent = null, bool $recursive = true): Db\Result { - // if the user isn't authorized to perform this action then throw an exception. - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } // check to make sure the parent exists, if one is specified $parent = $this->folderValidateId($user, $parent)['id']; $q = new Query( @@ -548,9 +506,6 @@ class Database { * @param integer $id The identifier of the folder to delete */ public function folderRemove(string $user, $id): bool { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } if (!ValueInfo::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "folder", 'type' => "int > 0"]); } @@ -563,9 +518,6 @@ class Database { /** Returns the identifier, name, and parent of the given folder as an associative array */ public function folderPropertiesGet(string $user, $id): array { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } if (!ValueInfo::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "folder", 'type' => "int > 0"]); } @@ -590,9 +542,6 @@ class Database { * @param array $data An associative array of properties to modify. Anything not specified will remain unchanged */ public function folderPropertiesSet(string $user, $id, array $data): bool { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } // verify the folder belongs to the user $in = $this->folderValidateId($user, $id, true); $name = array_key_exists("name", $data); @@ -739,9 +688,6 @@ class Database { * @param boolean $discover Whether to perform newsfeed discovery if $url points to an HTML document */ public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = "", bool $discover = true): int { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } // get the ID of the underlying feed, or add it if it's not yet in the database $feedID = $this->feedAdd($url, $fetchUser, $fetchPassword, $discover); // Add the feed to the user's subscriptions and return the new subscription's ID. @@ -756,9 +702,6 @@ class Database { * @param integer|null $id The numeric identifier of a particular subscription; used internally by subscriptionPropertiesGet */ public function subscriptionList(string $user, $folder = null, bool $recursive = true, int $id = null): Db\Result { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } // validate inputs $folder = $this->folderValidateId($user, $folder)['id']; // create a complex query @@ -804,9 +747,6 @@ class Database { /** Returns the number of subscriptions in a folder, counting recursively */ public function subscriptionCount(string $user, $folder = null): int { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } // validate inputs $folder = $this->folderValidateId($user, $folder)['id']; // create a complex query @@ -829,9 +769,6 @@ class Database { * configurable retention period for newsfeeds */ public function subscriptionRemove(string $user, $id): bool { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } if (!ValueInfo::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'type' => "int > 0"]); } @@ -861,9 +798,6 @@ class Database { * - "unread": The number of unread articles associated with the subscription */ public function subscriptionPropertiesGet(string $user, $id): array { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } if (!ValueInfo::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'type' => "int > 0"]); } @@ -888,9 +822,6 @@ class Database { * @param array $data An associative array of properties to modify; any keys not specified will be left unchanged */ public function subscriptionPropertiesSet(string $user, $id, array $data): bool { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $tr = $this->db->begin(); // validate the ID $id = $this->subscriptionValidateId($user, $id, true)['id']; @@ -934,9 +865,6 @@ class Database { * @param boolean $byName Whether to return the tag names (true) instead of the numeric tag identifiers (false) */ public function subscriptionTagsGet(string $user, $id, bool $byName = false): array { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $this->subscriptionValidateId($user, $id, true); $field = !$byName ? "id" : "name"; $out = $this->db->prepare("SELECT $field from arsse_tags where id in (select tag from arsse_tag_members where subscription = ? and assigned = 1) order by $field", "int")->run($id)->getAll(); @@ -961,9 +889,6 @@ class Database { $q = new Query("SELECT i.id, i.url, i.type, $data from arsse_subscriptions as s join arsse_feeds as f on s.feed = f.id left join arsse_icons as i on f.icon = i.id"); $q->setWhere("s.id = ?", "int", $id); if (isset($user)) { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $q->setWhere("s.owner = ?", "str", $user); } $out = $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getRow(); @@ -975,9 +900,6 @@ class Database { /** Returns the time at which any of a user's subscriptions (or a specific subscription) was last refreshed, as a DateTimeImmutable object */ public function subscriptionRefreshed(string $user, int $id = null): ?\DateTimeImmutable { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $q = new Query("SELECT max(arsse_feeds.updated) from arsse_feeds join arsse_subscriptions on arsse_subscriptions.feed = arsse_feeds.id"); $q->setWhere("arsse_subscriptions.owner = ?", "str", $user); if ($id) { @@ -1304,9 +1226,6 @@ class Database { * @param string $user The user whose subscription icons are to be retrieved */ public function iconList(string $user): Db\Result { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } return $this->db->prepare("SELECT distinct i.id, i.url, i.type, i.data from arsse_icons as i join arsse_feeds as f on i.id = f.icon join arsse_subscriptions as s on s.feed = f.id where s.owner = ?", "str")->run($user); } @@ -1646,9 +1565,6 @@ class Database { * @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"], 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); @@ -1693,9 +1609,6 @@ class Database { * @param Context $context The search context */ public function articleCount(string $user, Context $context = null): int { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $context = $context ?? new Context; $q = $this->articleQuery($user, $context, []); return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); @@ -1714,9 +1627,6 @@ class Database { * @param Context $context The query context to match articles against */ public function articleMark(string $user, array $data, Context $context = null): int { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $data = [ 'read' => $data['read'] ?? null, 'starred' => $data['starred'] ?? null, @@ -1800,9 +1710,6 @@ class Database { * - "read": The count of starred articles which are read */ public function articleStarred(string $user): array { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } return $this->db->prepare( "SELECT count(*) as total, @@ -1822,9 +1729,6 @@ class Database { * @param boolean $byName Whether to return the label names (true) instead of the numeric label identifiers (false) */ public function articleLabelsGet(string $user, $id, bool $byName = false): array { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $id = $this->articleValidateId($user, $id)['article']; $field = !$byName ? "id" : "name"; $out = $this->db->prepare("SELECT $field from arsse_labels join arsse_label_members on arsse_label_members.label = arsse_labels.id where owner = ? and article = ? and assigned = 1 order by $field", "str", "int")->run($user, $id)->getAll(); @@ -1833,9 +1737,6 @@ class Database { /** Returns the author-supplied categories associated with an article */ public function articleCategoriesGet(string $user, $id): array { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $id = $this->articleValidateId($user, $id)['article']; $out = $this->db->prepare("SELECT name from arsse_categories where article = ? order by name", "int")->run($id)->getAll(); if (!$out) { @@ -1937,9 +1838,6 @@ class Database { /** Returns the numeric identifier of the most recent edition of an article matching the given context */ public function editionLatest(string $user, Context $context = null): int { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $context = $context ?? new Context; $q = new Query("SELECT max(arsse_editions.id) from arsse_editions left join arsse_articles on article = arsse_articles.id join arsse_subscriptions on arsse_articles.feed = arsse_subscriptions.feed and arsse_subscriptions.owner = ?", "str", $user); if ($context->subscription()) { @@ -1968,10 +1866,6 @@ class Database { * @param array $data An associative array defining the label's properties; currently only "name" is understood */ public function labelAdd(string $user, array $data): int { - // if the user isn't authorized to perform this action then throw an exception. - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } // validate the label name $name = array_key_exists("name", $data) ? $data['name'] : ""; $this->labelValidateName($name, true); @@ -1992,10 +1886,6 @@ class Database { * @param boolean $includeEmpty Whether to include (true) or supress (false) labels which have no articles assigned to them */ public function labelList(string $user, bool $includeEmpty = true): Db\Result { - // if the user isn't authorized to perform this action then throw an exception. - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } return $this->db->prepare( "SELECT * FROM ( SELECT @@ -2032,9 +1922,6 @@ class Database { * @param boolean $byName Whether to interpret the $id parameter as the label's name (true) or identifier (false) */ public function labelRemove(string $user, $id, bool $byName = false): bool { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $this->labelValidateId($user, $id, $byName, false); $field = $byName ? "name" : "id"; $type = $byName ? "str" : "int"; @@ -2059,9 +1946,6 @@ class Database { * @param boolean $byName Whether to interpret the $id parameter as the label's name (true) or identifier (false) */ public function labelPropertiesGet(string $user, $id, bool $byName = false): array { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $this->labelValidateId($user, $id, $byName, false); $field = $byName ? "name" : "id"; $type = $byName ? "str" : "int"; @@ -2101,9 +1985,6 @@ class Database { * @param boolean $byName Whether to interpret the $id parameter as the label's name (true) or identifier (false) */ public function labelPropertiesSet(string $user, $id, array $data, bool $byName = false): bool { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $this->labelValidateId($user, $id, $byName, false); if (isset($data['name'])) { $this->labelValidateName($data['name']); @@ -2132,9 +2013,6 @@ class Database { * @param boolean $byName Whether to interpret the $id parameter as the label's name (true) or identifier (false) */ public function labelArticlesGet(string $user, $id, bool $byName = false): array { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } // just do a syntactic check on the label ID $this->labelValidateId($user, $id, $byName, false); $field = !$byName ? "id" : "name"; @@ -2161,9 +2039,6 @@ class Database { */ public function labelArticlesSet(string $user, $id, Context $context, int $mode = self::ASSOC_ADD, bool $byName = false): int { assert(in_array($mode, [self::ASSOC_ADD, self::ASSOC_REMOVE, self::ASSOC_REPLACE]), new Exception("constantUnknown", $mode)); - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } // validate the tag ID, and get the numeric ID if matching by name $id = $this->labelValidateId($user, $id, $byName, true)['id']; // get the list of articles matching the context @@ -2269,10 +2144,6 @@ class Database { * @param array $data An associative array defining the tag's properties; currently only "name" is understood */ public function tagAdd(string $user, array $data): int { - // if the user isn't authorized to perform this action then throw an exception. - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } // validate the tag name $name = array_key_exists("name", $data) ? $data['name'] : ""; $this->tagValidateName($name, true); @@ -2292,10 +2163,6 @@ class Database { * @param boolean $includeEmpty Whether to include (true) or supress (false) tags which have no subscriptions assigned to them */ public function tagList(string $user, bool $includeEmpty = true): Db\Result { - // if the user isn't authorized to perform this action then throw an exception. - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } return $this->db->prepare( "SELECT * FROM ( SELECT @@ -2323,10 +2190,6 @@ class Database { * @param string $user The user whose tags are to be listed */ public function tagSummarize(string $user): Db\Result { - // if the user isn't authorized to perform this action then throw an exception. - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } return $this->db->prepare( "SELECT arsse_tags.id as id, @@ -2348,9 +2211,6 @@ class Database { * @param boolean $byName Whether to interpret the $id parameter as the tag's name (true) or identifier (false) */ public function tagRemove(string $user, $id, bool $byName = false): bool { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $this->tagValidateId($user, $id, $byName, false); $field = $byName ? "name" : "id"; $type = $byName ? "str" : "int"; @@ -2374,9 +2234,6 @@ class Database { * @param boolean $byName Whether to interpret the $id parameter as the tag's name (true) or identifier (false) */ public function tagPropertiesGet(string $user, $id, bool $byName = false): array { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $this->tagValidateId($user, $id, $byName, false); $field = $byName ? "name" : "id"; $type = $byName ? "str" : "int"; @@ -2404,9 +2261,6 @@ class Database { * @param boolean $byName Whether to interpret the $id parameter as the tag's name (true) or identifier (false) */ public function tagPropertiesSet(string $user, $id, array $data, bool $byName = false): bool { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } $this->tagValidateId($user, $id, $byName, false); if (isset($data['name'])) { $this->tagValidateName($data['name']); @@ -2435,9 +2289,6 @@ class Database { * @param boolean $byName Whether to interpret the $id parameter as the tag's name (true) or identifier (false) */ public function tagSubscriptionsGet(string $user, $id, bool $byName = false): array { - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } // just do a syntactic check on the tag ID $this->tagValidateId($user, $id, $byName, false); $field = !$byName ? "id" : "name"; @@ -2464,9 +2315,6 @@ class Database { */ public function tagSubscriptionsSet(string $user, $id, array $subscriptions, int $mode = self::ASSOC_ADD, bool $byName = false): int { assert(in_array($mode, [self::ASSOC_ADD, self::ASSOC_REMOVE, self::ASSOC_REPLACE]), new Exception("constantUnknown", $mode)); - if (!Arsse::$user->authorize($user, __FUNCTION__)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - } // validate the tag ID, and get the numeric ID if matching by name $id = $this->tagValidateId($user, $id, $byName, true)['id']; // an empty subscription list is a special case diff --git a/lib/User.php b/lib/User.php index e39516ce..37ae814b 100644 --- a/lib/User.php +++ b/lib/User.php @@ -27,11 +27,6 @@ class User { return (string) $this->id; } - public function authorize(string $affectedUser, string $action): bool { - // at one time there was a complicated authorization system; it exists vestigially to support a later revival if desired - return $this->u->authorize($affectedUser, $action); - } - public function auth(string $user, string $password): bool { $prevUser = $this->id; $this->id = $user; @@ -50,34 +45,18 @@ class User { } public function list(): array { - $func = "userList"; - if (!$this->authorize("", $func)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => ""]); - } return $this->u->userList(); } public function exists(string $user): bool { - $func = "userExists"; - if (!$this->authorize($user, $func)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]); - } return $this->u->userExists($user); } public function add($user, $password = null): string { - $func = "userAdd"; - if (!$this->authorize($user, $func)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]); - } return $this->u->userAdd($user, $password) ?? $this->u->userAdd($user, $this->generatePassword()); } public function remove(string $user): bool { - $func = "userRemove"; - if (!$this->authorize($user, $func)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]); - } try { return $this->u->userRemove($user); } finally { // @codeCoverageIgnore @@ -89,10 +68,6 @@ class User { } public function passwordSet(string $user, string $newPassword = null, $oldPassword = null): string { - $func = "userPasswordSet"; - if (!$this->authorize($user, $func)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]); - } $out = $this->u->userPasswordSet($user, $newPassword, $oldPassword) ?? $this->u->userPasswordSet($user, $this->generatePassword(), $oldPassword); if (Arsse::$db->userExists($user)) { // if the password change was successful and the user exists, set the internal password to the same value @@ -104,10 +79,6 @@ class User { } public function passwordUnset(string $user, $oldPassword = null): bool { - $func = "userPasswordUnset"; - if (!$this->authorize($user, $func)) { - throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]); - } $out = $this->u->userPasswordUnset($user, $oldPassword); if (Arsse::$db->userExists($user)) { // if the password change was successful and the user exists, set the internal password to the same value @@ -154,7 +125,7 @@ class User { } } if (array_key_exists("lang", $data)) { - $in['lang'] = V::normalize($data['lang'], V::T_STRING | M_NULL); + $in['lang'] = V::normalize($data['lang'], V::T_STRING | V::M_NULL); } $out = $this->u->userPropertiesSet($user, $in); // synchronize the internal database diff --git a/lib/User/Driver.php b/lib/User/Driver.php index 8faaec71..e36ca38a 100644 --- a/lib/User/Driver.php +++ b/lib/User/Driver.php @@ -17,8 +17,6 @@ interface Driver { public static function driverName(): string; // authenticates a user against their name and password public function auth(string $user, string $password): bool; - // check whether a user is authorized to perform a certain action; not currently used and subject to change - public function authorize(string $affectedUser, string $action): bool; // checks whether a user exists public function userExists(string $user): bool; // adds a user diff --git a/lib/User/Internal/Driver.php b/lib/User/Internal/Driver.php index 4fc787f1..85d6fb3f 100644 --- a/lib/User/Internal/Driver.php +++ b/lib/User/Internal/Driver.php @@ -32,10 +32,6 @@ class Driver implements \JKingWeb\Arsse\User\Driver { return password_verify($password, $hash); } - public function authorize(string $affectedUser, string $action): bool { - return true; - } - public function userExists(string $user): bool { return Arsse::$db->userExists($user); } diff --git a/locale/en.php b/locale/en.php index c19ac94b..bcc71db1 100644 --- a/locale/en.php +++ b/locale/en.php @@ -138,11 +138,6 @@ return [ 'Exception.JKingWeb/Arsse/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist', 'Exception.JKingWeb/Arsse/User/Exception.authMissing' => 'Please log in to proceed', 'Exception.JKingWeb/Arsse/User/Exception.authFailed' => 'Authentication failed', - 'Exception.JKingWeb/Arsse/User/ExceptionAuthz.notAuthorized' => - '{action, select, - userList {Authenticated user is not authorized to view the user list} - other {Authenticated user is not authorized to perform the action "{action}" on behalf of {user}} - }', 'Exception.JKingWeb/Arsse/User/ExceptionSession.invalid' => 'Session with ID {0} does not exist', 'Exception.JKingWeb/Arsse/Feed/Exception.internalError' => 'Could not download feed "{url}" because of an internal error which is probably a bug', 'Exception.JKingWeb/Arsse/Feed/Exception.invalidCertificate' => 'Could not download feed "{url}" because its server is serving an invalid SSL certificate', diff --git a/tests/cases/Database/AbstractTest.php b/tests/cases/Database/AbstractTest.php index 6e0e2ec9..5a8626ed 100644 --- a/tests/cases/Database/AbstractTest.php +++ b/tests/cases/Database/AbstractTest.php @@ -74,7 +74,6 @@ abstract class AbstractTest extends \JKingWeb\Arsse\Test\AbstractTest { Arsse::$db->driverSchemaUpdate(); // create a mock user manager Arsse::$user = \Phake::mock(User::class); - \Phake::when(Arsse::$user)->authorize->thenReturn(true); // call the series-specific setup method $setUp = "setUp".$this->series; $this->$setUp(); diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index a9354c71..4edd8c82 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -597,12 +597,6 @@ trait SeriesArticle { ]; } - public function testListArticlesWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->articleList($this->user); - } - public function testMarkNothing(): void { $this->assertSame(0, Arsse::$db->articleMark($this->user, [])); } @@ -967,12 +961,6 @@ trait SeriesArticle { $this->compareExpectations(static::$drv, $state); } - public function testMarkArticlesWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->articleMark($this->user, ['read' => false]); - } - public function testCountArticles(): void { $setSize = (new \ReflectionClassConstant(Database::class, "LIMIT_SET_SIZE"))->getValue(); $this->assertSame(2, Arsse::$db->articleCount("john.doe@example.com", (new Context)->starred(true))); @@ -981,12 +969,6 @@ trait SeriesArticle { $this->assertSame(10, Arsse::$db->articleCount("john.doe@example.com", (new Context)->articles(range(1, $setSize * 3)))); } - public function testCountArticlesWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->articleCount($this->user); - } - public function testFetchStarredCounts(): void { $exp1 = ['total' => 2, 'unread' => 1, 'read' => 1]; $exp2 = ['total' => 0, 'unread' => 0, 'read' => 0]; @@ -994,12 +976,6 @@ trait SeriesArticle { $this->assertEquals($exp2, Arsse::$db->articleStarred("jane.doe@example.com")); } - public function testFetchStarredCountsWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->articleStarred($this->user); - } - public function testFetchLatestEdition(): void { $this->assertSame(1001, Arsse::$db->editionLatest($this->user)); $this->assertSame(4, Arsse::$db->editionLatest($this->user, (new Context)->subscription(12))); @@ -1010,12 +986,6 @@ trait SeriesArticle { Arsse::$db->editionLatest($this->user, (new Context)->subscription(1)); } - public function testFetchLatestEditionWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->editionLatest($this->user); - } - public function testListTheLabelsOfAnArticle(): void { $this->assertEquals([1,2], Arsse::$db->articleLabelsGet("john.doe@example.com", 1)); $this->assertEquals([2], Arsse::$db->articleLabelsGet("john.doe@example.com", 5)); @@ -1030,12 +1000,6 @@ trait SeriesArticle { Arsse::$db->articleLabelsGet($this->user, 101); } - public function testListTheLabelsOfAnArticleWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->articleLabelsGet("john.doe@example.com", 1); - } - public function testListTheCategoriesOfAnArticle(): void { $exp = ["Fascinating", "Logical"]; $this->assertSame($exp, Arsse::$db->articleCategoriesGet($this->user, 19)); @@ -1050,12 +1014,6 @@ trait SeriesArticle { Arsse::$db->articleCategoriesGet($this->user, 101); } - public function testListTheCategoriesOfAnArticleWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->articleCategoriesGet($this->user, 19); - } - /** @dataProvider provideArrayContextOptions */ public function testUseTooFewValuesInArrayContext(string $option): void { $this->assertException("tooShort", "Db", "ExceptionInput"); diff --git a/tests/cases/Database/SeriesFolder.php b/tests/cases/Database/SeriesFolder.php index 98d12d7b..4c488ced 100644 --- a/tests/cases/Database/SeriesFolder.php +++ b/tests/cases/Database/SeriesFolder.php @@ -102,7 +102,6 @@ trait SeriesFolder { $user = "john.doe@example.com"; $folderID = $this->nextID("arsse_folders"); $this->assertSame($folderID, Arsse::$db->folderAdd($user, ['name' => "Entertainment"])); - \Phake::verify(Arsse::$user)->authorize($user, "folderAdd"); $state = $this->primeExpectations($this->data, ['arsse_folders' => ['id','owner', 'parent', 'name']]); $state['arsse_folders']['rows'][] = [$folderID, $user, null, "Entertainment"]; $this->compareExpectations(static::$drv, $state); @@ -117,7 +116,6 @@ trait SeriesFolder { $user = "john.doe@example.com"; $folderID = $this->nextID("arsse_folders"); $this->assertSame($folderID, Arsse::$db->folderAdd($user, ['name' => "GNOME", 'parent' => 2])); - \Phake::verify(Arsse::$user)->authorize($user, "folderAdd"); $state = $this->primeExpectations($this->data, ['arsse_folders' => ['id','owner', 'parent', 'name']]); $state['arsse_folders']['rows'][] = [$folderID, $user, 2, "GNOME"]; $this->compareExpectations(static::$drv, $state); @@ -153,12 +151,6 @@ trait SeriesFolder { Arsse::$db->folderAdd("john.doe@example.com", ['name' => " "]); } - public function testAddAFolderWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->folderAdd("john.doe@example.com", ['name' => "Sociology"]); - } - public function testListRootFolders(): void { $exp = [ ['id' => 5, 'name' => "Politics", 'parent' => null, 'children' => 0, 'feeds' => 2], @@ -171,9 +163,6 @@ trait SeriesFolder { $this->assertResult($exp, Arsse::$db->folderList("jane.doe@example.com", null, false)); $exp = []; $this->assertResult($exp, Arsse::$db->folderList("admin@example.net", null, false)); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderList"); - \Phake::verify(Arsse::$user)->authorize("jane.doe@example.com", "folderList"); - \Phake::verify(Arsse::$user)->authorize("admin@example.net", "folderList"); } public function testListFoldersRecursively(): void { @@ -193,8 +182,6 @@ trait SeriesFolder { $this->assertResult($exp, Arsse::$db->folderList("john.doe@example.com", 1, true)); $exp = []; $this->assertResult($exp, Arsse::$db->folderList("jane.doe@example.com", 4, true)); - \Phake::verify(Arsse::$user, \Phake::times(2))->authorize("john.doe@example.com", "folderList"); - \Phake::verify(Arsse::$user)->authorize("jane.doe@example.com", "folderList"); } public function testListFoldersOfAMissingParent(): void { @@ -207,15 +194,8 @@ trait SeriesFolder { Arsse::$db->folderList("john.doe@example.com", 4); // folder ID 4 belongs to Jane } - public function testListFoldersWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->folderList("john.doe@example.com"); - } - public function testRemoveAFolder(): void { $this->assertTrue(Arsse::$db->folderRemove("john.doe@example.com", 6)); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderRemove"); $state = $this->primeExpectations($this->data, ['arsse_folders' => ['id','owner', 'parent', 'name']]); array_pop($state['arsse_folders']['rows']); $this->compareExpectations(static::$drv, $state); @@ -223,7 +203,6 @@ trait SeriesFolder { public function testRemoveAFolderTree(): void { $this->assertTrue(Arsse::$db->folderRemove("john.doe@example.com", 1)); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderRemove"); $state = $this->primeExpectations($this->data, ['arsse_folders' => ['id','owner', 'parent', 'name']]); foreach ([0,1,2,5] as $index) { unset($state['arsse_folders']['rows'][$index]); @@ -246,12 +225,6 @@ trait SeriesFolder { Arsse::$db->folderRemove("john.doe@example.com", 4); // folder ID 4 belongs to Jane } - public function testRemoveAFolderWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->folderRemove("john.doe@example.com", 1); - } - public function testGetThePropertiesOfAFolder(): void { $exp = [ 'id' => 6, @@ -259,7 +232,6 @@ trait SeriesFolder { 'parent' => 2, ]; $this->assertArraySubset($exp, Arsse::$db->folderPropertiesGet("john.doe@example.com", 6)); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderPropertiesGet"); } public function testGetThePropertiesOfAMissingFolder(): void { @@ -277,19 +249,12 @@ trait SeriesFolder { Arsse::$db->folderPropertiesGet("john.doe@example.com", 4); // folder ID 4 belongs to Jane } - public function testGetThePropertiesOfAFolderWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->folderPropertiesGet("john.doe@example.com", 1); - } - public function testMakeNoChangesToAFolder(): void { $this->assertFalse(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, [])); } public function testRenameAFolder(): void { $this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['name' => "Opinion"])); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderPropertiesSet"); $state = $this->primeExpectations($this->data, ['arsse_folders' => ['id','owner', 'parent', 'name']]); $state['arsse_folders']['rows'][5][3] = "Opinion"; $this->compareExpectations(static::$drv, $state); @@ -316,7 +281,6 @@ trait SeriesFolder { public function testMoveAFolder(): void { $this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['parent' => 5])); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderPropertiesSet"); $state = $this->primeExpectations($this->data, ['arsse_folders' => ['id','owner', 'parent', 'name']]); $state['arsse_folders']['rows'][5][2] = 5; // parent should have changed $this->compareExpectations(static::$drv, $state); @@ -371,10 +335,4 @@ trait SeriesFolder { $this->assertException("subjectMissing", "Db", "ExceptionInput"); Arsse::$db->folderPropertiesSet("john.doe@example.com", 4, ['parent' => null]); // folder ID 4 belongs to Jane } - - public function testSetThePropertiesOfAFolderWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->folderPropertiesSet("john.doe@example.com", 1, ['parent' => null]); - } } diff --git a/tests/cases/Database/SeriesIcon.php b/tests/cases/Database/SeriesIcon.php index d54a4ab9..667651f2 100644 --- a/tests/cases/Database/SeriesIcon.php +++ b/tests/cases/Database/SeriesIcon.php @@ -94,10 +94,4 @@ trait SeriesIcon { ]; $this->assertResult($exp, Arsse::$db->iconList("jane.doe@example.com")); } - - public function testListTheIconsOfAUserWithoutAuthority() { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->iconList("jane.doe@example.com"); - } } diff --git a/tests/cases/Database/SeriesLabel.php b/tests/cases/Database/SeriesLabel.php index d66dcdb0..58f3c979 100644 --- a/tests/cases/Database/SeriesLabel.php +++ b/tests/cases/Database/SeriesLabel.php @@ -253,7 +253,6 @@ trait SeriesLabel { $user = "john.doe@example.com"; $labelID = $this->nextID("arsse_labels"); $this->assertSame($labelID, Arsse::$db->labelAdd($user, ['name' => "Entertaining"])); - \Phake::verify(Arsse::$user)->authorize($user, "labelAdd"); $state = $this->primeExpectations($this->data, $this->checkLabels); $state['arsse_labels']['rows'][] = [$labelID, $user, "Entertaining"]; $this->compareExpectations(static::$drv, $state); @@ -279,12 +278,6 @@ trait SeriesLabel { Arsse::$db->labelAdd("john.doe@example.com", ['name' => " "]); } - public function testAddALabelWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->labelAdd("john.doe@example.com", ['name' => "Boring"]); - } - public function testListLabels(): void { $exp = [ ['id' => 2, 'name' => "Fascinating", 'articles' => 3, 'read' => 1], @@ -298,18 +291,10 @@ trait SeriesLabel { $this->assertResult($exp, Arsse::$db->labelList("jane.doe@example.com")); $exp = []; $this->assertResult($exp, Arsse::$db->labelList("jane.doe@example.com", false)); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelList"); - } - - public function testListLabelsWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->labelList("john.doe@example.com"); } public function testRemoveALabel(): void { $this->assertTrue(Arsse::$db->labelRemove("john.doe@example.com", 1)); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelRemove"); $state = $this->primeExpectations($this->data, $this->checkLabels); array_shift($state['arsse_labels']['rows']); $this->compareExpectations(static::$drv, $state); @@ -317,7 +302,6 @@ trait SeriesLabel { public function testRemoveALabelByName(): void { $this->assertTrue(Arsse::$db->labelRemove("john.doe@example.com", "Interesting", true)); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelRemove"); $state = $this->primeExpectations($this->data, $this->checkLabels); array_shift($state['arsse_labels']['rows']); $this->compareExpectations(static::$drv, $state); @@ -343,12 +327,6 @@ trait SeriesLabel { Arsse::$db->labelRemove("john.doe@example.com", 3); // label ID 3 belongs to Jane } - public function testRemoveALabelWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->labelRemove("john.doe@example.com", 1); - } - public function testGetThePropertiesOfALabel(): void { $exp = [ 'id' => 2, @@ -358,7 +336,6 @@ trait SeriesLabel { ]; $this->assertArraySubset($exp, Arsse::$db->labelPropertiesGet("john.doe@example.com", 2)); $this->assertArraySubset($exp, Arsse::$db->labelPropertiesGet("john.doe@example.com", "Fascinating", true)); - \Phake::verify(Arsse::$user, \Phake::times(2))->authorize("john.doe@example.com", "labelPropertiesGet"); } public function testGetThePropertiesOfAMissingLabel(): void { @@ -381,19 +358,12 @@ trait SeriesLabel { Arsse::$db->labelPropertiesGet("john.doe@example.com", 3); // label ID 3 belongs to Jane } - public function testGetThePropertiesOfALabelWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->labelPropertiesGet("john.doe@example.com", 1); - } - public function testMakeNoChangesToALabel(): void { $this->assertFalse(Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, [])); } public function testRenameALabel(): void { $this->assertTrue(Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => "Curious"])); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelPropertiesSet"); $state = $this->primeExpectations($this->data, $this->checkLabels); $state['arsse_labels']['rows'][0][2] = "Curious"; $this->compareExpectations(static::$drv, $state); @@ -401,7 +371,6 @@ trait SeriesLabel { public function testRenameALabelByName(): void { $this->assertTrue(Arsse::$db->labelPropertiesSet("john.doe@example.com", "Interesting", ['name' => "Curious"], true)); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "labelPropertiesSet"); $state = $this->primeExpectations($this->data, $this->checkLabels); $state['arsse_labels']['rows'][0][2] = "Curious"; $this->compareExpectations(static::$drv, $state); @@ -447,12 +416,6 @@ trait SeriesLabel { Arsse::$db->labelPropertiesSet("john.doe@example.com", 3, ['name' => "Exciting"]); // label ID 3 belongs to Jane } - public function testSetThePropertiesOfALabelWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->labelPropertiesSet("john.doe@example.com", 1, ['name' => "Exciting"]); - } - public function testListLabelledArticles(): void { $exp = [1,19]; $this->assertEquals($exp, Arsse::$db->labelArticlesGet("john.doe@example.com", 1)); @@ -475,12 +438,6 @@ trait SeriesLabel { Arsse::$db->labelArticlesGet("john.doe@example.com", -1); } - public function testListLabelledArticlesWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->labelArticlesGet("john.doe@example.com", 1); - } - public function testApplyALabelToArticles(): void { Arsse::$db->labelArticlesSet("john.doe@example.com", 1, (new Context)->articles([2,5])); $state = $this->primeExpectations($this->data, $this->checkMembers); @@ -540,10 +497,4 @@ trait SeriesLabel { $state['arsse_label_members']['rows'][2][3] = 0; $this->compareExpectations(static::$drv, $state); } - - public function testApplyALabelToArticlesWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->labelArticlesSet("john.doe@example.com", 1, (new Context)->articles([2,5])); - } } diff --git a/tests/cases/Database/SeriesSession.php b/tests/cases/Database/SeriesSession.php index 163d8bf8..1db319f8 100644 --- a/tests/cases/Database/SeriesSession.php +++ b/tests/cases/Database/SeriesSession.php @@ -70,9 +70,6 @@ trait SeriesSession { $state = $this->primeExpectations($this->data, ['arsse_sessions' => ["id", "created", "expires", "user"]]); $state['arsse_sessions']['rows'][3][2] = Date::transform(Date::add(Arsse::$conf->userSessionTimeout, $now), "sql"); $this->compareExpectations(static::$drv, $state); - // session resumption should not check authorization - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertArraySubset($exp1, Arsse::$db->sessionResume("80fa94c1a11f11e78667001e673b2560")); } public function testResumeAMissingSession(): void { @@ -99,12 +96,6 @@ trait SeriesSession { $this->compareExpectations(static::$drv, $state); } - public function testCreateASessionWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->sessionCreate("jane.doe@example.com"); - } - public function testDestroyASession(): void { $user = "jane.doe@example.com"; $id = "80fa94c1a11f11e78667001e673b2560"; @@ -131,10 +122,4 @@ trait SeriesSession { $id = "80fa94c1a11f11e78667001e673b2560"; $this->assertFalse(Arsse::$db->sessionDestroy($user, $id)); } - - public function testDestroyASessionWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->sessionDestroy("jane.doe@example.com", "80fa94c1a11f11e78667001e673b2560"); - } } diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index 0075b992..749c8752 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -61,7 +61,11 @@ trait SeriesSubscription { 'next_fetch' => "datetime", 'icon' => "int", ], - 'rows' => [], // filled in the series setup + 'rows' => [ + [1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),strtotime("now"),null], + [2,"http://example.com/feed2", "eek", "", "",strtotime("now - 1 hour"),strtotime("now - 1 hour"),1], + [3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),strtotime("now + 1 hour"),null], + ], ], 'arsse_subscriptions' => [ 'columns' => [ @@ -144,11 +148,6 @@ trait SeriesSubscription { ], ], ]; - $this->data['arsse_feeds']['rows'] = [ - [1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),strtotime("now"),null], - [2,"http://example.com/feed2", "eek", "", "",strtotime("now - 1 hour"),strtotime("now - 1 hour"),1], - [3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),strtotime("now + 1 hour"),null], - ]; // initialize a partial mock of the Database object to later manipulate the feedUpdate method Arsse::$db = \Phake::partialMock(Database::class, static::$drv); $this->user = "john.doe@example.com"; @@ -163,7 +162,6 @@ trait SeriesSubscription { $subID = $this->nextID("arsse_subscriptions"); \Phake::when(Arsse::$db)->feedUpdate->thenReturn(true); $this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url)); - \Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionAdd"); \Phake::verify(Arsse::$db, \Phake::times(0))->feedUpdate(1, true); $state = $this->primeExpectations($this->data, [ 'arsse_feeds' => ['id','url','username','password'], @@ -179,7 +177,6 @@ trait SeriesSubscription { $subID = $this->nextID("arsse_subscriptions"); \Phake::when(Arsse::$db)->feedUpdate->thenReturn(true); $this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url, "", "", false)); - \Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionAdd"); \Phake::verify(Arsse::$db)->feedUpdate($feedID, true); $state = $this->primeExpectations($this->data, [ 'arsse_feeds' => ['id','url','username','password'], @@ -197,7 +194,6 @@ trait SeriesSubscription { $subID = $this->nextID("arsse_subscriptions"); \Phake::when(Arsse::$db)->feedUpdate->thenReturn(true); $this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url, "", "", true)); - \Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionAdd"); \Phake::verify(Arsse::$db)->feedUpdate($feedID, true); $state = $this->primeExpectations($this->data, [ 'arsse_feeds' => ['id','url','username','password'], @@ -216,7 +212,6 @@ trait SeriesSubscription { try { Arsse::$db->subscriptionAdd($this->user, $url, "", "", false); } finally { - \Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionAdd"); \Phake::verify(Arsse::$db)->feedUpdate($feedID, true); $state = $this->primeExpectations($this->data, [ 'arsse_feeds' => ['id','url','username','password'], @@ -246,16 +241,8 @@ trait SeriesSubscription { $this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url)); } - public function testAddASubscriptionWithoutAuthority(): void { - $url = "http://example.com/feed1"; - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->subscriptionAdd($this->user, $url); - } - public function testRemoveASubscription(): void { $this->assertTrue(Arsse::$db->subscriptionRemove($this->user, 1)); - \Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionRemove"); $state = $this->primeExpectations($this->data, [ 'arsse_feeds' => ['id','url','username','password'], 'arsse_subscriptions' => ['id','owner','feed'], @@ -280,12 +267,6 @@ trait SeriesSubscription { Arsse::$db->subscriptionRemove($this->user, 1); } - public function testRemoveASubscriptionWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->subscriptionRemove($this->user, 1); - } - public function testListSubscriptions(): void { $exp = [ [ @@ -308,9 +289,7 @@ trait SeriesSubscription { ], ]; $this->assertResult($exp, Arsse::$db->subscriptionList($this->user)); - \Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionList"); $this->assertArraySubset($exp[0], Arsse::$db->subscriptionPropertiesGet($this->user, 1)); - \Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionPropertiesGet"); $this->assertArraySubset($exp[1], Arsse::$db->subscriptionPropertiesGet($this->user, 3)); } @@ -349,12 +328,6 @@ trait SeriesSubscription { Arsse::$db->subscriptionList($this->user, 4); } - public function testListSubscriptionsWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->subscriptionList($this->user); - } - public function testCountSubscriptions(): void { $this->assertSame(2, Arsse::$db->subscriptionCount($this->user)); $this->assertSame(1, Arsse::$db->subscriptionCount($this->user, 2)); @@ -365,12 +338,6 @@ trait SeriesSubscription { Arsse::$db->subscriptionCount($this->user, 4); } - public function testCountSubscriptionsWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->subscriptionCount($this->user); - } - public function testGetThePropertiesOfAMissingSubscription(): void { $this->assertException("subjectMissing", "Db", "ExceptionInput"); Arsse::$db->subscriptionPropertiesGet($this->user, 2112); @@ -381,12 +348,6 @@ trait SeriesSubscription { Arsse::$db->subscriptionPropertiesGet($this->user, -1); } - public function testGetThePropertiesOfASubscriptionWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->subscriptionPropertiesGet($this->user, 1); - } - public function testSetThePropertiesOfASubscription(): void { Arsse::$db->subscriptionPropertiesSet($this->user, 1, [ 'title' => "Ook Ook", @@ -394,7 +355,6 @@ trait SeriesSubscription { 'pinned' => false, 'order_type' => 0, ]); - \Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionPropertiesSet"); $state = $this->primeExpectations($this->data, [ 'arsse_feeds' => ['id','url','username','password','title'], 'arsse_subscriptions' => ['id','owner','feed','title','folder','pinned','order_type'], @@ -454,22 +414,11 @@ trait SeriesSubscription { Arsse::$db->subscriptionPropertiesSet($this->user, -1, ['folder' => null]); } - public function testSetThePropertiesOfASubscriptionWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['folder' => null]); - } - public function testRetrieveTheFaviconOfASubscription(): void { $exp = "http://example.com/favicon.ico"; $this->assertSame($exp, Arsse::$db->subscriptionIcon(null, 1)['url']); $this->assertSame($exp, Arsse::$db->subscriptionIcon(null, 2)['url']); $this->assertSame(null, Arsse::$db->subscriptionIcon(null, 3)['url']); - // authorization shouldn't have any bearing on this function - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertSame($exp, Arsse::$db->subscriptionIcon(null, 1)['url']); - $this->assertSame($exp, Arsse::$db->subscriptionIcon(null, 2)['url']); - $this->assertSame(null, Arsse::$db->subscriptionIcon(null, 3)['url']); } public function testRetrieveTheFaviconOfAMissingSubscription(): void { @@ -493,14 +442,6 @@ trait SeriesSubscription { $this->assertSame(null, Arsse::$db->subscriptionIcon($user, 2)['url']); } - public function testRetrieveTheFaviconOfASubscriptionWithUserWithoutAuthority(): void { - $exp = "http://example.com/favicon.ico"; - $user = "john.doe@example.com"; - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->subscriptionIcon($user, -2112); - } - public function testListTheTagsOfASubscription(): void { $this->assertEquals([1,2], Arsse::$db->subscriptionTagsGet("john.doe@example.com", 1)); $this->assertEquals([2], Arsse::$db->subscriptionTagsGet("john.doe@example.com", 3)); @@ -513,12 +454,6 @@ trait SeriesSubscription { Arsse::$db->subscriptionTagsGet($this->user, 101); } - public function testListTheTagsOfASubscriptionWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->subscriptionTagsGet("john.doe@example.com", 1); - } - public function testGetRefreshTimeOfASubscription(): void { $user = "john.doe@example.com"; $this->assertTime(strtotime("now + 1 hour"), Arsse::$db->subscriptionRefreshed($user)); @@ -529,10 +464,4 @@ trait SeriesSubscription { $this->assertException("subjectMissing", "Db", "ExceptionInput"); $this->assertTime(strtotime("now - 1 hour"), Arsse::$db->subscriptionRefreshed("john.doe@example.com", 2)); } - - public function testGetRefreshTimeOfASubscriptionWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - $this->assertTime(strtotime("now + 1 hour"), Arsse::$db->subscriptionRefreshed("john.doe@example.com")); - } } diff --git a/tests/cases/Database/SeriesTag.php b/tests/cases/Database/SeriesTag.php index 3c4b4ac8..1f2ea9cd 100644 --- a/tests/cases/Database/SeriesTag.php +++ b/tests/cases/Database/SeriesTag.php @@ -113,7 +113,6 @@ trait SeriesTag { $user = "john.doe@example.com"; $tagID = $this->nextID("arsse_tags"); $this->assertSame($tagID, Arsse::$db->tagAdd($user, ['name' => "Entertaining"])); - \Phake::verify(Arsse::$user)->authorize($user, "tagAdd"); $state = $this->primeExpectations($this->data, $this->checkTags); $state['arsse_tags']['rows'][] = [$tagID, $user, "Entertaining"]; $this->compareExpectations(static::$drv, $state); @@ -139,12 +138,6 @@ trait SeriesTag { Arsse::$db->tagAdd("john.doe@example.com", ['name' => " "]); } - public function testAddATagWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->tagAdd("john.doe@example.com", ['name' => "Boring"]); - } - public function testListTags(): void { $exp = [ ['id' => 2, 'name' => "Fascinating"], @@ -158,18 +151,10 @@ trait SeriesTag { $this->assertResult($exp, Arsse::$db->tagList("jane.doe@example.com")); $exp = []; $this->assertResult($exp, Arsse::$db->tagList("jane.doe@example.com", false)); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "tagList"); - } - - public function testListTagsWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->tagList("john.doe@example.com"); } public function testRemoveATag(): void { $this->assertTrue(Arsse::$db->tagRemove("john.doe@example.com", 1)); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "tagRemove"); $state = $this->primeExpectations($this->data, $this->checkTags); array_shift($state['arsse_tags']['rows']); $this->compareExpectations(static::$drv, $state); @@ -177,7 +162,6 @@ trait SeriesTag { public function testRemoveATagByName(): void { $this->assertTrue(Arsse::$db->tagRemove("john.doe@example.com", "Interesting", true)); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "tagRemove"); $state = $this->primeExpectations($this->data, $this->checkTags); array_shift($state['arsse_tags']['rows']); $this->compareExpectations(static::$drv, $state); @@ -203,12 +187,6 @@ trait SeriesTag { Arsse::$db->tagRemove("john.doe@example.com", 3); // tag ID 3 belongs to Jane } - public function testRemoveATagWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->tagRemove("john.doe@example.com", 1); - } - public function testGetThePropertiesOfATag(): void { $exp = [ 'id' => 2, @@ -216,7 +194,6 @@ trait SeriesTag { ]; $this->assertArraySubset($exp, Arsse::$db->tagPropertiesGet("john.doe@example.com", 2)); $this->assertArraySubset($exp, Arsse::$db->tagPropertiesGet("john.doe@example.com", "Fascinating", true)); - \Phake::verify(Arsse::$user, \Phake::times(2))->authorize("john.doe@example.com", "tagPropertiesGet"); } public function testGetThePropertiesOfAMissingTag(): void { @@ -239,19 +216,12 @@ trait SeriesTag { Arsse::$db->tagPropertiesGet("john.doe@example.com", 3); // tag ID 3 belongs to Jane } - public function testGetThePropertiesOfATagWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->tagPropertiesGet("john.doe@example.com", 1); - } - public function testMakeNoChangesToATag(): void { $this->assertFalse(Arsse::$db->tagPropertiesSet("john.doe@example.com", 1, [])); } public function testRenameATag(): void { $this->assertTrue(Arsse::$db->tagPropertiesSet("john.doe@example.com", 1, ['name' => "Curious"])); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "tagPropertiesSet"); $state = $this->primeExpectations($this->data, $this->checkTags); $state['arsse_tags']['rows'][0][2] = "Curious"; $this->compareExpectations(static::$drv, $state); @@ -259,7 +229,6 @@ trait SeriesTag { public function testRenameATagByName(): void { $this->assertTrue(Arsse::$db->tagPropertiesSet("john.doe@example.com", "Interesting", ['name' => "Curious"], true)); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "tagPropertiesSet"); $state = $this->primeExpectations($this->data, $this->checkTags); $state['arsse_tags']['rows'][0][2] = "Curious"; $this->compareExpectations(static::$drv, $state); @@ -305,12 +274,6 @@ trait SeriesTag { Arsse::$db->tagPropertiesSet("john.doe@example.com", 3, ['name' => "Exciting"]); // tag ID 3 belongs to Jane } - public function testSetThePropertiesOfATagWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->tagPropertiesSet("john.doe@example.com", 1, ['name' => "Exciting"]); - } - public function testListTaggedSubscriptions(): void { $exp = [1,5]; $this->assertEquals($exp, Arsse::$db->tagSubscriptionsGet("john.doe@example.com", 1)); @@ -333,12 +296,6 @@ trait SeriesTag { Arsse::$db->tagSubscriptionsGet("john.doe@example.com", -1); } - public function testListTaggedSubscriptionsWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->tagSubscriptionsGet("john.doe@example.com", 1); - } - public function testApplyATagToSubscriptions(): void { Arsse::$db->tagSubscriptionsSet("john.doe@example.com", 1, [3,4]); $state = $this->primeExpectations($this->data, $this->checkMembers); @@ -399,12 +356,6 @@ trait SeriesTag { $this->compareExpectations(static::$drv, $state); } - public function testApplyATagToSubscriptionsWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->tagSubscriptionsSet("john.doe@example.com", 1, [3,4]); - } - public function testSummarizeTags(): void { $exp = [ ['id' => 1, 'name' => "Interesting", 'subscription' => 1], @@ -415,10 +366,4 @@ trait SeriesTag { ]; $this->assertResult($exp, Arsse::$db->tagSummarize("john.doe@example.com")); } - - public function testSummarizeTagsWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->tagSummarize("john.doe@example.com"); - } } diff --git a/tests/cases/Database/SeriesToken.php b/tests/cases/Database/SeriesToken.php index 267be38e..29977b3f 100644 --- a/tests/cases/Database/SeriesToken.php +++ b/tests/cases/Database/SeriesToken.php @@ -67,9 +67,6 @@ trait SeriesToken { $this->assertArraySubset($exp1, Arsse::$db->tokenLookup("fever.login", "80fa94c1a11f11e78667001e673b2560")); $this->assertArraySubset($exp2, Arsse::$db->tokenLookup("class.class", "da772f8fa13c11e78667001e673b2560")); $this->assertArraySubset($exp3, Arsse::$db->tokenLookup("class.class", "ab3b3eb8a13311e78667001e673b2560")); - // token lookup should not check authorization - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertArraySubset($exp1, Arsse::$db->tokenLookup("fever.login", "80fa94c1a11f11e78667001e673b2560")); } public function testLookUpAMissingToken(): void { @@ -106,12 +103,6 @@ trait SeriesToken { Arsse::$db->tokenCreate("fever.login", "jane.doe@example.biz"); } - public function testCreateATokenWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->tokenCreate("fever.login", "jane.doe@example.com"); - } - public function testRevokeAToken(): void { $user = "jane.doe@example.com"; $id = "80fa94c1a11f11e78667001e673b2560"; @@ -136,10 +127,4 @@ trait SeriesToken { // revoking tokens which do not exist is not an error $this->assertFalse(Arsse::$db->tokenRevoke($user, "unknown.class")); } - - public function testRevokeATokenWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->tokenRevoke("jane.doe@example.com", "fever.login"); - } } diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index 9b97fd80..3211cc9f 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -33,21 +33,12 @@ trait SeriesUser { public function testCheckThatAUserExists(): void { $this->assertTrue(Arsse::$db->userExists("jane.doe@example.com")); $this->assertFalse(Arsse::$db->userExists("jane.doe@example.org")); - \Phake::verify(Arsse::$user)->authorize("jane.doe@example.com", "userExists"); - \Phake::verify(Arsse::$user)->authorize("jane.doe@example.org", "userExists"); $this->compareExpectations(static::$drv, $this->data); } - public function testCheckThatAUserExistsWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->userExists("jane.doe@example.com"); - } - public function testGetAPassword(): void { $hash = Arsse::$db->userPasswordGet("admin@example.net"); $this->assertSame('$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW', $hash); - \Phake::verify(Arsse::$user)->authorize("admin@example.net", "userPasswordGet"); $this->assertTrue(password_verify("secret", $hash)); } @@ -56,15 +47,8 @@ trait SeriesUser { Arsse::$db->userPasswordGet("john.doe@example.org"); } - public function testGetAPasswordWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->userPasswordGet("admin@example.net"); - } - public function testAddANewUser(): void { $this->assertTrue(Arsse::$db->userAdd("john.doe@example.org", "")); - \Phake::verify(Arsse::$user)->authorize("john.doe@example.org", "userAdd"); $state = $this->primeExpectations($this->data, ['arsse_users' => ['id']]); $state['arsse_users']['rows'][] = ["john.doe@example.org"]; $this->compareExpectations(static::$drv, $state); @@ -75,15 +59,8 @@ trait SeriesUser { Arsse::$db->userAdd("john.doe@example.com", ""); } - public function testAddANewUserWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->userAdd("john.doe@example.org", ""); - } - public function testRemoveAUser(): void { $this->assertTrue(Arsse::$db->userRemove("admin@example.net")); - \Phake::verify(Arsse::$user)->authorize("admin@example.net", "userRemove"); $state = $this->primeExpectations($this->data, ['arsse_users' => ['id']]); array_shift($state['arsse_users']['rows']); $this->compareExpectations(static::$drv, $state); @@ -94,22 +71,9 @@ trait SeriesUser { Arsse::$db->userRemove("john.doe@example.org"); } - public function testRemoveAUserWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->userRemove("admin@example.net"); - } - public function testListAllUsers(): void { $users = ["admin@example.net", "jane.doe@example.com", "john.doe@example.com"]; $this->assertSame($users, Arsse::$db->userList()); - \Phake::verify(Arsse::$user)->authorize("", "userList"); - } - - public function testListAllUsersWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->userList(); } /** @@ -122,7 +86,6 @@ trait SeriesUser { $this->assertTrue(Arsse::$db->userPasswordSet($user, $pass)); $hash = Arsse::$db->userPasswordGet($user); $this->assertNotEquals("", $hash); - \Phake::verify(Arsse::$user)->authorize($user, "userPasswordSet"); $this->assertTrue(password_verify($pass, $hash), "Failed verifying password of $user '$pass' against hash '$hash'."); } @@ -137,10 +100,4 @@ trait SeriesUser { $this->assertException("doesNotExist", "User"); Arsse::$db->userPasswordSet("john.doe@example.org", "secret"); } - - public function testSetAPasswordWithoutAuthority(): void { - \Phake::when(Arsse::$user)->authorize->thenReturn(false); - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - Arsse::$db->userPasswordSet("john.doe@example.com", "secret"); - } } diff --git a/tests/cases/ImportExport/TestImportExport.php b/tests/cases/ImportExport/TestImportExport.php index af0b0fe0..64eaf905 100644 --- a/tests/cases/ImportExport/TestImportExport.php +++ b/tests/cases/ImportExport/TestImportExport.php @@ -28,7 +28,6 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest { // create a mock user manager Arsse::$user = \Phake::mock(\JKingWeb\Arsse\User::class); \Phake::when(Arsse::$user)->exists->thenReturn(true); - \Phake::when(Arsse::$user)->authorize->thenReturn(true); // create a mock Import/Export processor $this->proc = \Phake::partialMock(AbstractImportExport::class); // initialize an SQLite memeory database diff --git a/tests/cases/User/TestInternal.php b/tests/cases/User/TestInternal.php index 4333771d..6a88b4dc 100644 --- a/tests/cases/User/TestInternal.php +++ b/tests/cases/User/TestInternal.php @@ -33,16 +33,12 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { * @dataProvider provideAuthentication * @group slow */ - public function testAuthenticateAUser(bool $authorized, string $user, $password, bool $exp): void { - if ($authorized) { - \Phake::when(Arsse::$db)->userPasswordGet("john.doe@example.com")->thenReturn('$2y$10$1zbqRJhxM8uUjeSBPp4IhO90xrqK0XjEh9Z16iIYEFRV4U.zeAFom'); // hash of "secret" - \Phake::when(Arsse::$db)->userPasswordGet("jane.doe@example.com")->thenReturn('$2y$10$bK1ljXfTSyc2D.NYvT.Eq..OpehLRXVbglW.23ihVuyhgwJCd.7Im'); // hash of "superman" - \Phake::when(Arsse::$db)->userPasswordGet("owen.hardy@example.com")->thenReturn(""); - \Phake::when(Arsse::$db)->userPasswordGet("kira.nerys@example.com")->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist")); - \Phake::when(Arsse::$db)->userPasswordGet("007@example.com")->thenReturn(null); - } else { - \Phake::when(Arsse::$db)->userPasswordGet->thenThrow(new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")); - } + public function testAuthenticateAUser(string $user, $password, bool $exp): void { + \Phake::when(Arsse::$db)->userPasswordGet("john.doe@example.com")->thenReturn('$2y$10$1zbqRJhxM8uUjeSBPp4IhO90xrqK0XjEh9Z16iIYEFRV4U.zeAFom'); // hash of "secret" + \Phake::when(Arsse::$db)->userPasswordGet("jane.doe@example.com")->thenReturn('$2y$10$bK1ljXfTSyc2D.NYvT.Eq..OpehLRXVbglW.23ihVuyhgwJCd.7Im'); // hash of "superman" + \Phake::when(Arsse::$db)->userPasswordGet("owen.hardy@example.com")->thenReturn(""); + \Phake::when(Arsse::$db)->userPasswordGet("kira.nerys@example.com")->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist")); + \Phake::when(Arsse::$db)->userPasswordGet("007@example.com")->thenReturn(null); $this->assertSame($exp, (new Driver)->auth($user, $password)); } @@ -53,32 +49,22 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { $kira = "kira.nerys@example.com"; $bond = "007@example.com"; return [ - [false, $john, "secret", false], - [false, $jane, "superman", false], - [false, $owen, "", false], - [false, $kira, "ashalla", false], - [false, $bond, "", false], - [true, $john, "secret", true], - [true, $jane, "superman", true], - [true, $owen, "", true], - [true, $kira, "ashalla", false], - [true, $john, "top secret", false], - [true, $jane, "clark kent", false], - [true, $owen, "watchmaker", false], - [true, $kira, "singha", false], - [true, $john, "", false], - [true, $jane, "", false], - [true, $kira, "", false], - [true, $bond, "for England", false], - [true, $bond, "", false], + [$john, "secret", true], + [$jane, "superman", true], + [$owen, "", true], + [$kira, "ashalla", false], + [$john, "top secret", false], + [$jane, "clark kent", false], + [$owen, "watchmaker", false], + [$kira, "singha", false], + [$john, "", false], + [$jane, "", false], + [$kira, "", false], + [$bond, "for England", false], + [$bond, "", false], ]; } - public function testAuthorizeAnAction(): void { - \Phake::verifyNoFurtherInteraction(Arsse::$db); - $this->assertTrue((new Driver)->authorize("someone", "something")); - } - public function testListUsers(): void { $john = "john.doe@example.com"; $jane = "jane.doe@example.com"; diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 93b5ee72..80be0e6f 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -69,13 +69,9 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideUserList */ - public function testListUsers(bool $authorized, $exp): void { + public function testListUsers($exp): void { $u = new User($this->drv); - \Phake::when($this->drv)->authorize->thenReturn($authorized); \Phake::when($this->drv)->userList->thenReturn(["john.doe@example.com", "jane.doe@example.com"]); - if ($exp instanceof Exception) { - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - } $this->assertSame($exp, $u->list()); } @@ -83,20 +79,15 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $john = "john.doe@example.com"; $jane = "jane.doe@example.com"; return [ - [false, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")], - [true, [$john, $jane]], + [[$john, $jane]], ]; } /** @dataProvider provideExistence */ - public function testCheckThatAUserExists(bool $authorized, string $user, $exp): void { + public function testCheckThatAUserExists(string $user, $exp): void { $u = new User($this->drv); - \Phake::when($this->drv)->authorize->thenReturn($authorized); \Phake::when($this->drv)->userExists("john.doe@example.com")->thenReturn(true); \Phake::when($this->drv)->userExists("jane.doe@example.com")->thenReturn(false); - if ($exp instanceof Exception) { - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - } $this->assertSame($exp, $u->exists($user)); } @@ -104,48 +95,35 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $john = "john.doe@example.com"; $jane = "jane.doe@example.com"; return [ - [false, $john, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")], - [false, $jane, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")], - [true, $john, true], - [true, $jane, false], + [$john, true], + [$jane, false], ]; } /** @dataProvider provideAdditions */ - public function testAddAUser(bool $authorized, string $user, $password, $exp): void { + public function testAddAUser(string $user, $password, $exp): void { $u = new User($this->drv); - \Phake::when($this->drv)->authorize->thenReturn($authorized); \Phake::when($this->drv)->userAdd("john.doe@example.com", $this->anything())->thenThrow(new \JKingWeb\Arsse\User\Exception("alreadyExists")); \Phake::when($this->drv)->userAdd("jane.doe@example.com", $this->anything())->thenReturnCallback(function($user, $pass) { return $pass ?? "random password"; }); if ($exp instanceof Exception) { - if ($exp instanceof \JKingWeb\Arsse\User\ExceptionAuthz) { - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - } else { - $this->assertException("alreadyExists", "User"); - } + $this->assertException("alreadyExists", "User"); } $this->assertSame($exp, $u->add($user, $password)); } /** @dataProvider provideAdditions */ - public function testAddAUserWithARandomPassword(bool $authorized, string $user, $password, $exp): void { + public function testAddAUserWithARandomPassword(string $user, $password, $exp): void { $u = \Phake::partialMock(User::class, $this->drv); - \Phake::when($this->drv)->authorize->thenReturn($authorized); \Phake::when($this->drv)->userAdd($this->anything(), $this->isNull())->thenReturn(null); \Phake::when($this->drv)->userAdd("john.doe@example.com", $this->logicalNot($this->isNull()))->thenThrow(new \JKingWeb\Arsse\User\Exception("alreadyExists")); \Phake::when($this->drv)->userAdd("jane.doe@example.com", $this->logicalNot($this->isNull()))->thenReturnCallback(function($user, $pass) { return $pass; }); if ($exp instanceof Exception) { - if ($exp instanceof \JKingWeb\Arsse\User\ExceptionAuthz) { - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - $calls = 0; - } else { - $this->assertException("alreadyExists", "User"); - $calls = 2; - } + $this->assertException("alreadyExists", "User"); + $calls = 2; } else { $calls = 4; } @@ -163,34 +141,27 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $john = "john.doe@example.com"; $jane = "jane.doe@example.com"; return [ - [false, $john, "secret", new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")], - [false, $jane, "superman", new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")], - [true, $john, "secret", new \JKingWeb\Arsse\User\Exception("alreadyExists")], - [true, $jane, "superman", "superman"], - [true, $jane, null, "random password"], + [$john, "secret", new \JKingWeb\Arsse\User\Exception("alreadyExists")], + [$jane, "superman", "superman"], + [$jane, null, "random password"], ]; } /** @dataProvider provideRemovals */ - public function testRemoveAUser(bool $authorized, string $user, bool $exists, $exp): void { + public function testRemoveAUser(string $user, bool $exists, $exp): void { $u = new User($this->drv); - \Phake::when($this->drv)->authorize->thenReturn($authorized); \Phake::when($this->drv)->userRemove("john.doe@example.com")->thenReturn(true); \Phake::when($this->drv)->userRemove("jane.doe@example.com")->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist")); \Phake::when(Arsse::$db)->userExists->thenReturn($exists); \Phake::when(Arsse::$db)->userRemove->thenReturn(true); if ($exp instanceof Exception) { - if ($exp instanceof \JKingWeb\Arsse\User\ExceptionAuthz) { - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - } else { - $this->assertException("doesNotExist", "User"); - } + $this->assertException("doesNotExist", "User"); } try { $this->assertSame($exp, $u->remove($user)); } finally { - \Phake::verify(Arsse::$db, \Phake::times((int) $authorized))->userExists($user); - \Phake::verify(Arsse::$db, \Phake::times((int) ($authorized && $exists)))->userRemove($user); + \Phake::verify(Arsse::$db, \Phake::times(1))->userExists($user); + \Phake::verify(Arsse::$db, \Phake::times((int) $exists))->userRemove($user); } } @@ -198,32 +169,23 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $john = "john.doe@example.com"; $jane = "jane.doe@example.com"; return [ - [false, $john, true, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")], - [false, $john, false, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")], - [false, $jane, true, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")], - [false, $jane, false, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")], - [true, $john, true, true], - [true, $john, false, true], - [true, $jane, true, new \JKingWeb\Arsse\User\Exception("doesNotExist")], - [true, $jane, false, new \JKingWeb\Arsse\User\Exception("doesNotExist")], + [$john, true, true], + [$john, false, true], + [$jane, true, new \JKingWeb\Arsse\User\Exception("doesNotExist")], + [$jane, false, new \JKingWeb\Arsse\User\Exception("doesNotExist")], ]; } /** @dataProvider providePasswordChanges */ - public function testChangeAPassword(bool $authorized, string $user, $password, bool $exists, $exp): void { + public function testChangeAPassword(string $user, $password, bool $exists, $exp): void { $u = new User($this->drv); - \Phake::when($this->drv)->authorize->thenReturn($authorized); \Phake::when($this->drv)->userPasswordSet("john.doe@example.com", $this->anything(), $this->anything())->thenReturnCallback(function($user, $pass, $old) { return $pass ?? "random password"; }); \Phake::when($this->drv)->userPasswordSet("jane.doe@example.com", $this->anything(), $this->anything())->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist")); \Phake::when(Arsse::$db)->userExists->thenReturn($exists); if ($exp instanceof Exception) { - if ($exp instanceof \JKingWeb\Arsse\User\ExceptionAuthz) { - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - } else { - $this->assertException("doesNotExist", "User"); - } + $this->assertException("doesNotExist", "User"); $calls = 0; } else { $calls = 1; @@ -237,9 +199,8 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider providePasswordChanges */ - public function testChangeAPasswordToARandomPassword(bool $authorized, string $user, $password, bool $exists, $exp): void { + public function testChangeAPasswordToARandomPassword(string $user, $password, bool $exists, $exp): void { $u = \Phake::partialMock(User::class, $this->drv); - \Phake::when($this->drv)->authorize->thenReturn($authorized); \Phake::when($this->drv)->userPasswordSet($this->anything(), $this->isNull(), $this->anything())->thenReturn(null); \Phake::when($this->drv)->userPasswordSet("john.doe@example.com", $this->logicalNot($this->isNull()), $this->anything())->thenReturnCallback(function($user, $pass, $old) { return $pass ?? "random password"; @@ -247,13 +208,8 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::when($this->drv)->userPasswordSet("jane.doe@example.com", $this->logicalNot($this->isNull()), $this->anything())->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist")); \Phake::when(Arsse::$db)->userExists->thenReturn($exists); if ($exp instanceof Exception) { - if ($exp instanceof \JKingWeb\Arsse\User\ExceptionAuthz) { - $this->assertException("notAuthorized", "User", "ExceptionAuthz"); - $calls = 0; - } else { - $this->assertException("doesNotExist", "User"); - $calls = 2; - } + $this->assertException("doesNotExist", "User"); + $calls = 2; } else { $calls = 4; } @@ -278,19 +234,16 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $john = "john.doe@example.com"; $jane = "jane.doe@example.com"; return [ - [false, $john, "secret", true, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")], - [false, $jane, "superman", false, new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized")], - [true, $john, "superman", true, "superman"], - [true, $john, null, true, "random password"], - [true, $john, "superman", false, "superman"], - [true, $john, null, false, "random password"], - [true, $jane, "secret", true, new \JKingWeb\Arsse\User\Exception("doesNotExist")], + [$john, "superman", true, "superman"], + [$john, null, true, "random password"], + [$john, "superman", false, "superman"], + [$john, null, false, "random password"], + [$jane, "secret", true, new \JKingWeb\Arsse\User\Exception("doesNotExist")], ]; } /** @dataProvider providePasswordClearings */ - public function testClearAPassword(bool $authorized, bool $exists, string $user, $exp): void { - \Phake::when($this->drv)->authorize->thenReturn($authorized); + public function testClearAPassword(bool $exists, string $user, $exp): void { \Phake::when($this->drv)->userPasswordUnset->thenReturn(true); \Phake::when($this->drv)->userPasswordUnset("jane.doe@example.net", null)->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist")); \Phake::when(Arsse::$db)->userExists->thenReturn($exists); @@ -303,26 +256,19 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame($exp, $u->passwordUnset($user)); } } finally { - \Phake::verify(Arsse::$db, \Phake::times((int) ($authorized && $exists && is_bool($exp))))->userPasswordSet($user, null); + \Phake::verify(Arsse::$db, \Phake::times((int) ($exists && is_bool($exp))))->userPasswordSet($user, null); } } public function providePasswordClearings(): iterable { - $forbidden = new \JKingWeb\Arsse\User\ExceptionAuthz("notAuthorized"); $missing = new \JKingWeb\Arsse\User\Exception("doesNotExist"); return [ - [false, true, "jane.doe@example.com", $forbidden], - [false, true, "john.doe@example.com", $forbidden], - [false, true, "jane.doe@example.net", $forbidden], - [false, false, "jane.doe@example.com", $forbidden], - [false, false, "john.doe@example.com", $forbidden], - [false, false, "jane.doe@example.net", $forbidden], - [true, true, "jane.doe@example.com", true], - [true, true, "john.doe@example.com", true], - [true, true, "jane.doe@example.net", $missing], - [true, false, "jane.doe@example.com", true], - [true, false, "john.doe@example.com", true], - [true, false, "jane.doe@example.net", $missing], + [true, "jane.doe@example.com", true], + [true, "john.doe@example.com", true], + [true, "jane.doe@example.net", $missing], + [false, "jane.doe@example.com", true], + [false, "john.doe@example.com", true], + [false, "jane.doe@example.net", $missing], ]; } } From 5a17efc7b5e5000189354009585b2179099b98bc Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 9 Nov 2020 18:14:03 -0500 Subject: [PATCH 035/366] Clean up user driver API - It is no longer assumed a driver knows whether a user exists - The $password param is now required (but nullable when setting --- lib/ImportExport/AbstractImportExport.php | 2 +- lib/ImportExport/OPML.php | 2 +- lib/User.php | 4 -- lib/User/Driver.php | 55 +++++++++++++------ lib/User/ExceptionAuthz.php | 10 ---- lib/User/Internal/Driver.php | 10 ++-- tests/cases/ImportExport/TestImportExport.php | 4 +- tests/cases/ImportExport/TestOPML.php | 5 +- tests/cases/User/TestInternal.php | 26 ++------- tests/cases/User/TestUser.php | 17 ------ 10 files changed, 55 insertions(+), 80 deletions(-) delete mode 100644 lib/User/ExceptionAuthz.php diff --git a/lib/ImportExport/AbstractImportExport.php b/lib/ImportExport/AbstractImportExport.php index 22c1f2b1..72064825 100644 --- a/lib/ImportExport/AbstractImportExport.php +++ b/lib/ImportExport/AbstractImportExport.php @@ -13,7 +13,7 @@ use JKingWeb\Arsse\User\Exception as UserException; abstract class AbstractImportExport { public function import(string $user, string $data, bool $flat = false, bool $replace = false): bool { - if (!Arsse::$user->exists($user)) { + if (!Arsse::$db->userExists($user)) { throw new UserException("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } // first extract useful information from the input diff --git a/lib/ImportExport/OPML.php b/lib/ImportExport/OPML.php index 30a3cc51..30cb4f56 100644 --- a/lib/ImportExport/OPML.php +++ b/lib/ImportExport/OPML.php @@ -91,7 +91,7 @@ class OPML extends AbstractImportExport { } public function export(string $user, bool $flat = false): string { - if (!Arsse::$user->exists($user)) { + if (!Arsse::$db->userExists($user)) { throw new UserException("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } $tags = []; diff --git a/lib/User.php b/lib/User.php index 37ae814b..56e716b7 100644 --- a/lib/User.php +++ b/lib/User.php @@ -48,10 +48,6 @@ class User { return $this->u->userList(); } - public function exists(string $user): bool { - return $this->u->userExists($user); - } - public function add($user, $password = null): string { return $this->u->userAdd($user, $password) ?? $this->u->userAdd($user, $this->generatePassword()); } diff --git a/lib/User/Driver.php b/lib/User/Driver.php index e36ca38a..6bfa25b5 100644 --- a/lib/User/Driver.php +++ b/lib/User/Driver.php @@ -7,26 +7,49 @@ declare(strict_types=1); namespace JKingWeb\Arsse\User; interface Driver { - public const FUNC_NOT_IMPLEMENTED = 0; - public const FUNC_INTERNAL = 1; - public const FUNC_EXTERNAL = 2; - - // returns an instance of a class implementing this interface. public function __construct(); - // returns a human-friendly name for the driver (for display in installer, for example) + + /** Returns a human-friendly name for the driver (for display in installer, for example) */ public static function driverName(): string; - // authenticates a user against their name and password + + /** Authenticates a user against their name and password */ public function auth(string $user, string $password): bool; - // checks whether a user exists - public function userExists(string $user): bool; - // adds a user - public function userAdd(string $user, string $password = null); - // removes a user + + /** Adds a new user and returns their password + * + * When given no password the implementation may return null; the user + * manager will then generate a random password and try again with that + * password. Alternatively the implementation may generate its own + * password if desired + * + * @param string $user The username to create + * @param string|null $password The cleartext password to assign to the user, or null to generate a random password + */ + public function userAdd(string $user, string $password = null): ?string; + + /** Removes a user */ public function userRemove(string $user): bool; - // lists all users + + /** Lists all users */ public function userList(): array; - // sets a user's password; if the driver does not require the old password, it may be ignored - public function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null); - // removes a user's password; this makes authentication fail unconditionally + + /** sets a user's password + * + * When given no password the implementation may return null; the user + * manager will then generate a random password and try again with that + * password. Alternatively the implementation may generate its own + * password if desired + * + * @param string $user The user for whom to change the password + * @param string|null $password The cleartext password to assign to the user, or null to generate a random password + * @param string|null $oldPassword The user's previous password, if known + */ + public function userPasswordSet(string $user, ?string $newPassword, string $oldPassword = null); + + /** removes a user's password; this makes authentication fail unconditionally + * + * @param string $user The user for whom to change the password + * @param string|null $oldPassword The user's previous password, if known + */ public function userPasswordUnset(string $user, string $oldPassword = null): bool; } diff --git a/lib/User/ExceptionAuthz.php b/lib/User/ExceptionAuthz.php deleted file mode 100644 index 2d16f594..00000000 --- a/lib/User/ExceptionAuthz.php +++ /dev/null @@ -1,10 +0,0 @@ -userExists($user); - } - public function userAdd(string $user, string $password = null): ?string { if (isset($password)) { // only add the user if the password is not null; the user manager will retry with a generated password if null is returned @@ -52,7 +48,7 @@ class Driver implements \JKingWeb\Arsse\User\Driver { return Arsse::$db->userList(); } - public function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): ?string { + public function userPasswordSet(string $user, ?string $newPassword, string $oldPassword = null): ?string { // do nothing: the internal database is updated regardless of what the driver does (assuming it does not throw an exception) return $newPassword; } @@ -70,4 +66,8 @@ class Driver implements \JKingWeb\Arsse\User\Driver { protected function userPasswordGet(string $user): ?string { return Arsse::$db->userPasswordGet($user); } + + protected function userExists(string $user): bool { + return Arsse::$db->userExists($user); + } } diff --git a/tests/cases/ImportExport/TestImportExport.php b/tests/cases/ImportExport/TestImportExport.php index 64eaf905..1a899c4c 100644 --- a/tests/cases/ImportExport/TestImportExport.php +++ b/tests/cases/ImportExport/TestImportExport.php @@ -27,7 +27,6 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest { self::clearData(); // create a mock user manager Arsse::$user = \Phake::mock(\JKingWeb\Arsse\User::class); - \Phake::when(Arsse::$user)->exists->thenReturn(true); // create a mock Import/Export processor $this->proc = \Phake::partialMock(AbstractImportExport::class); // initialize an SQLite memeory database @@ -147,9 +146,8 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest { } public function testImportForAMissingUser(): void { - \Phake::when(Arsse::$user)->exists->thenReturn(false); $this->assertException("doesNotExist", "User"); - $this->proc->import("john.doe@example.com", "", false, false); + $this->proc->import("no.one@example.com", "", false, false); } public function testImportWithInvalidFolder(): void { diff --git a/tests/cases/ImportExport/TestOPML.php b/tests/cases/ImportExport/TestOPML.php index 1e65fa1e..36caa77c 100644 --- a/tests/cases/ImportExport/TestOPML.php +++ b/tests/cases/ImportExport/TestOPML.php @@ -82,8 +82,7 @@ OPML_EXPORT_SERIALIZATION; public function setUp(): void { self::clearData(); Arsse::$db = \Phake::mock(\JKingWeb\Arsse\Database::class); - Arsse::$user = \Phake::mock(\JKingWeb\Arsse\User::class); - \Phake::when(Arsse::$user)->exists->thenReturn(true); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); } public function testExportToOpml(): void { @@ -101,7 +100,7 @@ OPML_EXPORT_SERIALIZATION; } public function testExportToOpmlAMissingUser(): void { - \Phake::when(Arsse::$user)->exists->thenReturn(false); + \Phake::when(Arsse::$db)->userExists->thenReturn(false); $this->assertException("doesNotExist", "User"); (new OPML)->export("john.doe@example.com"); } diff --git a/tests/cases/User/TestInternal.php b/tests/cases/User/TestInternal.php index 6a88b4dc..21587f3d 100644 --- a/tests/cases/User/TestInternal.php +++ b/tests/cases/User/TestInternal.php @@ -75,18 +75,6 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db, \Phake::times(2))->userList; } - public function testCheckThatAUserExists(): void { - $john = "john.doe@example.com"; - $jane = "jane.doe@example.com"; - \Phake::when(Arsse::$db)->userExists($john)->thenReturn(true); - \Phake::when(Arsse::$db)->userExists($jane)->thenReturn(false); - $driver = new Driver; - $this->assertTrue($driver->userExists($john)); - \Phake::verify(Arsse::$db)->userExists($john); - $this->assertFalse($driver->userExists($jane)); - \Phake::verify(Arsse::$db)->userExists($jane); - } - public function testAddAUser(): void { $john = "john.doe@example.com"; \Phake::when(Arsse::$db)->userAdd->thenReturnCallback(function($user, $pass) { @@ -119,20 +107,18 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verifyNoFurtherInteraction(Arsse::$db); $this->assertSame("superman", (new Driver)->userPasswordSet($john, "superman")); $this->assertSame(null, (new Driver)->userPasswordSet($john, null)); + \Phake::verify(Arsse::$db, \Phake::times(0))->userPasswordSet; } public function testUnsetAPassword(): void { - $drv = \Phake::partialMock(Driver::class); - \Phake::when($drv)->userExists->thenReturn(true); - \Phake::verifyNoFurtherInteraction(Arsse::$db); - $this->assertTrue($drv->userPasswordUnset("john.doe@example.com")); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertTrue((new Driver)->userPasswordUnset("john.doe@example.com")); + \Phake::verify(Arsse::$db, \Phake::times(0))->userPasswordUnset; } public function testUnsetAPasswordForAMssingUser(): void { - $drv = \Phake::partialMock(Driver::class); - \Phake::when($drv)->userExists->thenReturn(false); - \Phake::verifyNoFurtherInteraction(Arsse::$db); + \Phake::when(Arsse::$db)->userExists->thenReturn(false); $this->assertException("doesNotExist", "User"); - $drv->userPasswordUnset("john.doe@example.com"); + (new Driver)->userPasswordUnset("john.doe@example.com"); } } diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 80be0e6f..759241c2 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -83,23 +83,6 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { ]; } - /** @dataProvider provideExistence */ - public function testCheckThatAUserExists(string $user, $exp): void { - $u = new User($this->drv); - \Phake::when($this->drv)->userExists("john.doe@example.com")->thenReturn(true); - \Phake::when($this->drv)->userExists("jane.doe@example.com")->thenReturn(false); - $this->assertSame($exp, $u->exists($user)); - } - - public function provideExistence(): iterable { - $john = "john.doe@example.com"; - $jane = "jane.doe@example.com"; - return [ - [$john, true], - [$jane, false], - ]; - } - /** @dataProvider provideAdditions */ public function testAddAUser(string $user, $password, $exp): void { $u = new User($this->drv); From eb2fe522bf529207956d56a50f5c693b3d28085d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 10 Nov 2020 17:09:59 -0500 Subject: [PATCH 036/366] Last bits of the new user metadata handling --- lib/User.php | 6 +++--- lib/User/Driver.php | 29 +++++++++++++++++++++++++++-- lib/User/Internal/Driver.php | 18 ++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/lib/User.php b/lib/User.php index 56e716b7..afe4920c 100644 --- a/lib/User.php +++ b/lib/User.php @@ -90,10 +90,10 @@ class User { } public function propertiesGet(string $user): array { + $extra = $this->u->userPropertiesGet($user); // unconditionally retrieve from the database to get at least the user number, and anything else the driver does not provide $out = Arsse::$db->userPropertiesGet($user); // layer on the driver's data - $extra = $this->u->userPropertiesGet($user); foreach (["lang", "tz", "admin", "sort_asc"] as $k) { if (array_key_exists($k, $extra)) { $out[$k] = $extra[$k] ?? $out[$k]; @@ -102,7 +102,7 @@ class User { return $out; } - public function propertiesSet(string $user, array $data): bool { + public function propertiesSet(string $user, array $data): array { $in = []; if (array_key_exists("tz", $data)) { if (!is_string($data['tz'])) { @@ -125,7 +125,7 @@ class User { } $out = $this->u->userPropertiesSet($user, $in); // synchronize the internal database - Arsse::$db->userPropertiesSet($user, $in); + Arsse::$db->userPropertiesSet($user, $out); return $out; } } diff --git a/lib/User/Driver.php b/lib/User/Driver.php index 6bfa25b5..dbf8ad68 100644 --- a/lib/User/Driver.php +++ b/lib/User/Driver.php @@ -33,7 +33,7 @@ interface Driver { /** Lists all users */ public function userList(): array; - /** sets a user's password + /** Sets a user's password * * When given no password the implementation may return null; the user * manager will then generate a random password and try again with that @@ -46,10 +46,35 @@ interface Driver { */ public function userPasswordSet(string $user, ?string $newPassword, string $oldPassword = null); - /** removes a user's password; this makes authentication fail unconditionally + /** Removes a user's password; this makes authentication fail unconditionally * * @param string $user The user for whom to change the password * @param string|null $oldPassword The user's previous password, if known */ public function userPasswordUnset(string $user, string $oldPassword = null): bool; + + /** Retrieves metadata about a user + * + * Any expected keys not returned by the driver are taken from the internal + * database instead; the expected keys at this time are: + * + * - admin: A boolean denoting whether the user has administrator privileges + * - lang: A BCP 47 language tag e.g. "en", "hy-Latn-IT-arevela" + * - tz: A zoneinfo timezone e.g. "Asia/Jakarta", "America/Argentina/La_Rioja" + * - sort_asc: A boolean denoting whether the user prefers articles to be sorted oldest-first + * + * Any other keys will be ignored. + */ + public function userPropertiesGet(string $user): array; + + /** Sets metadata about a user + * + * Output should be the same as the input, unless input is changed prior to storage + * (if it is, for instance, normalized in some way), which which case the changes + * should be reflected in the output. + * + * @param string $user The user for which to set metadata + * @param array $data The input data; see userPropertiesGet for keys + */ + public function userPropertiesSet(string $user, array $data): array; } diff --git a/lib/User/Internal/Driver.php b/lib/User/Internal/Driver.php index 5114498e..6b8e4438 100644 --- a/lib/User/Internal/Driver.php +++ b/lib/User/Internal/Driver.php @@ -70,4 +70,22 @@ class Driver implements \JKingWeb\Arsse\User\Driver { protected function userExists(string $user): bool { return Arsse::$db->userExists($user); } + + public function userPropertiesGet(string $user): array { + // do nothing: the internal database will retrieve everything for us + if (!$this->userExists($user)) { + throw new Exception("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); + } else { + return []; + } + } + + public function userPropertiesSet(string $user, array $data): array { + // do nothing: the internal database will set everything for us + if (!$this->userExists($user)) { + throw new Exception("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); + } else { + return $data; + } + } } From dde9d7a28a37066b124d6ad41c99464ac9dc1953 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 11 Nov 2020 18:50:27 -0500 Subject: [PATCH 037/366] Refinements to user manager A greater effort is made to keep the internal database synchronized --- lib/User.php | 21 ++++++++++++++++++++- lib/User/ExceptionNotImplemented.php | 10 ---------- 2 files changed, 20 insertions(+), 11 deletions(-) delete mode 100644 lib/User/ExceptionNotImplemented.php diff --git a/lib/User.php b/lib/User.php index afe4920c..8ebee8a5 100644 --- a/lib/User.php +++ b/lib/User.php @@ -49,7 +49,12 @@ class User { } public function add($user, $password = null): string { - return $this->u->userAdd($user, $password) ?? $this->u->userAdd($user, $this->generatePassword()); + $out = $this->u->userAdd($user, $password) ?? $this->u->userAdd($user, $this->generatePassword()); + // synchronize the internal database + if (!Arsse::$db->userExists($user)) { + Arsse::$db->userAdd($user, $out); + } + return $out; } public function remove(string $user): bool { @@ -70,6 +75,9 @@ class User { Arsse::$db->userPasswordSet($user, $out); // also invalidate any current sessions for the user Arsse::$db->sessionDestroy($user); + } else { + // if the user does not exist, add it with the new password + Arsse::$db->userAdd($user, $out); } return $out; } @@ -81,6 +89,10 @@ class User { Arsse::$db->userPasswordSet($user, null); // also invalidate any current sessions for the user Arsse::$db->sessionDestroy($user); + } else { + // if the user does not exist + Arsse::$db->userAdd($user, ""); + Arsse::$db->userPasswordSet($user, null); } return $out; } @@ -91,6 +103,10 @@ class User { public function propertiesGet(string $user): array { $extra = $this->u->userPropertiesGet($user); + // synchronize the internal database + if (!Arsse::$db->userExists($user)) { + Arsse::$db->userAdd($user, $this->generatePassword()); + } // unconditionally retrieve from the database to get at least the user number, and anything else the driver does not provide $out = Arsse::$db->userPropertiesGet($user); // layer on the driver's data @@ -125,6 +141,9 @@ class User { } $out = $this->u->userPropertiesSet($user, $in); // synchronize the internal database + if (!Arsse::$db->userExists($user)) { + Arsse::$db->userAdd($user, $this->generatePassword()); + } Arsse::$db->userPropertiesSet($user, $out); return $out; } diff --git a/lib/User/ExceptionNotImplemented.php b/lib/User/ExceptionNotImplemented.php deleted file mode 100644 index 12518ac7..00000000 --- a/lib/User/ExceptionNotImplemented.php +++ /dev/null @@ -1,10 +0,0 @@ - Date: Fri, 13 Nov 2020 19:30:23 -0500 Subject: [PATCH 038/366] Tests for new user functionality in Database --- lib/Database.php | 10 ++++-- tests/cases/Database/SeriesUser.php | 56 +++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index eff40a52..a531f23d 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -300,10 +300,11 @@ class Database { } public function userPropertiesGet(string $user): array { - if (!$this->userExists($user)) { + $out = $this->db->prepare("SELECT num, admin, lang, tz, sort_asc from arsse_users where id = ?", "str")->run($user)->getRow(); + if (!$out) { throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } - $out = $this->db->prepare("SELECT num, admin, lang, tz, sort_asc from arsse_users where id = ?", "str")->run($user)->getRow(); + settype($out['num'], "int"); settype($out['admin'], "bool"); settype($out['sort_asc'], "bool"); return $out; @@ -320,7 +321,10 @@ class Database { 'sort_asc' => "strict bool", ]; [$setClause, $setTypes, $setValues] = $this->generateSet($data, $allowed); - return (bool) $this->db->prepare("UPDATE arsse_users set $setClause where user = ?", $setTypes, "str")->run($setValues, $user)->changes(); + if (!$setClause) { + return false; + } + return (bool) $this->db->prepare("UPDATE arsse_users set $setClause where id = ?", $setTypes, "str")->run($setValues, $user)->changes(); } diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index 3211cc9f..10bd2582 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -16,11 +16,15 @@ trait SeriesUser { 'id' => 'str', 'password' => 'str', 'num' => 'int', + 'admin' => 'bool', + 'lang' => 'str', + 'tz' => 'str', + 'sort_asc' => 'bool', ], 'rows' => [ - ["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW',1], // password is hash of "secret" - ["jane.doe@example.com", "",2], - ["john.doe@example.com", "",3], + ["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW',1, 1, "en", "America/Toronto", 0], // password is hash of "secret" + ["jane.doe@example.com", "",2, 0, "fr", "Asia/Kuala_Lumpur", 1], + ["john.doe@example.com", "",3, 0, null, "Etc/UTC", 0], ], ], ]; @@ -100,4 +104,50 @@ trait SeriesUser { $this->assertException("doesNotExist", "User"); Arsse::$db->userPasswordSet("john.doe@example.org", "secret"); } + + /** @dataProvider provideMetaData */ + public function testGetMetadata(string $user, array $exp): void { + $this->assertSame($exp, Arsse::$db->userPropertiesGet($user)); + } + + public function provideMetadata() { + return [ + ["admin@example.net", ['num' => 1, 'admin' => true, 'lang' => "en", 'tz' => "America/Toronto", 'sort_asc' => false]], + ["jane.doe@example.com", ['num' => 2, 'admin' => false, 'lang' => "fr", 'tz' => "Asia/Kuala_Lumpur", 'sort_asc' => true]], + ["john.doe@example.com", ['num' => 3, 'admin' => false, 'lang' => null, 'tz' => "Etc/UTC", 'sort_asc' => false]], + ]; + } + + public function testGetTheMetadataOfAMissingUser(): void { + $this->assertException("doesNotExist", "User"); + Arsse::$db->userPropertiesGet("john.doe@example.org"); + } + + public function testSetMetadata(): void { + $in = [ + 'admin' => true, + 'lang' => "en-ca", + 'tz' => "Atlantic/Reykjavik", + 'sort_asc' => true, + ]; + $this->assertTrue(Arsse::$db->userPropertiesSet("john.doe@example.com", $in)); + $state = $this->primeExpectations($this->data, ['arsse_users' => ['id', 'num', 'admin', 'lang', 'tz', 'sort_asc']]); + $state['arsse_users']['rows'][2] = ["john.doe@example.com", 3, 1, "en-ca", "Atlantic/Reykjavik", 1]; + $this->compareExpectations(static::$drv, $state); + } + + public function testSetNoMetadata(): void { + $in = [ + 'num' => 2112, + 'blah' => "bloo" + ]; + $this->assertFalse(Arsse::$db->userPropertiesSet("john.doe@example.com", $in)); + $state = $this->primeExpectations($this->data, ['arsse_users' => ['id', 'num', 'admin', 'lang', 'tz', 'sort_asc']]); + $this->compareExpectations(static::$drv, $state); + } + + public function testSetTheMetadataOfAMissingUser(): void { + $this->assertException("doesNotExist", "User"); + Arsse::$db->userPropertiesSet("john.doe@example.org", ['admin' => true]); + } } From 351f97251273fe14b859caac6a938a355f9ea384 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 13 Nov 2020 21:41:27 -0500 Subject: [PATCH 039/366] Tests for internal user driver --- tests/cases/User/TestInternal.php | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/cases/User/TestInternal.php b/tests/cases/User/TestInternal.php index 21587f3d..cd231a0a 100644 --- a/tests/cases/User/TestInternal.php +++ b/tests/cases/User/TestInternal.php @@ -121,4 +121,41 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertException("doesNotExist", "User"); (new Driver)->userPasswordUnset("john.doe@example.com"); } + + public function testGetUserProperties(): void { + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertSame([], (new Driver)->userPropertiesGet("john.doe@example.com")); + \Phake::verify(Arsse::$db)->userExists("john.doe@example.com"); + \Phake::verifyNoFurtherInteraction(Arsse::$db); + } + + public function testGetPropertiesForAMissingUser(): void { + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertException("doesNotExist", "User"); + try { + (new Driver)->userPropertiesGet("john.doe@example.com"); + } finally { + \Phake::verify(Arsse::$db)->userExists("john.doe@example.com"); + \Phake::verifyNoFurtherInteraction(Arsse::$db); + } + } + + public function testSetUserProperties(): void { + $in = ['admin' => true]; + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertSame($in, (new Driver)->userPropertiesSet("john.doe@example.com", $in)); + \Phake::verify(Arsse::$db)->userExists("john.doe@example.com"); + \Phake::verifyNoFurtherInteraction(Arsse::$db); + } + + public function testSetPropertiesForAMissingUser(): void { + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertException("doesNotExist", "User"); + try { + (new Driver)->userPropertiesSet("john.doe@example.com", ['admin' => true]); + } finally { + \Phake::verify(Arsse::$db)->userExists("john.doe@example.com"); + \Phake::verifyNoFurtherInteraction(Arsse::$db); + } + } } From 7f2117adaaff3fcd544e56342add5bfd54e9783a Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 15 Nov 2020 16:24:26 -0500 Subject: [PATCH 040/366] Differentiate between duplicate/missing users and other failure modes --- lib/AbstractException.php | 5 +- lib/Database.php | 22 +- lib/ImportExport/AbstractImportExport.php | 2 +- lib/ImportExport/OPML.php | 2 +- lib/REST/Fever/API.php | 2 +- lib/User.php | 15 +- lib/User/ExceptionConflict.php | 10 + lib/User/Internal/Driver.php | 10 +- locale/en.php | 4 +- tests/cases/CLI/TestCLI.php | 8 +- tests/cases/Database/SeriesToken.php | 2 +- tests/cases/Database/SeriesUser.php | 14 +- tests/cases/ImportExport/TestImportExport.php | 2 +- tests/cases/ImportExport/TestOPML.php | 2 +- tests/cases/REST/Fever/TestUser.php | 2 +- tests/cases/User/TestInternal.php | 12 +- tests/cases/User/TestUser.php | 210 ++++-------------- 17 files changed, 109 insertions(+), 215 deletions(-) create mode 100644 lib/User/ExceptionConflict.php diff --git a/lib/AbstractException.php b/lib/AbstractException.php index 93798ca1..22b6eb52 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -68,11 +68,10 @@ abstract class AbstractException extends \Exception { "Conf/Exception.typeMismatch" => 10311, "Conf/Exception.semanticMismatch" => 10312, "Conf/Exception.ambiguousDefault" => 10313, - "User/Exception.functionNotImplemented" => 10401, - "User/Exception.doesNotExist" => 10402, - "User/Exception.alreadyExists" => 10403, "User/Exception.authMissing" => 10411, "User/Exception.authFailed" => 10412, + "User/ExceptionConflict.doesNotExist" => 10402, + "User/ExceptionConflict.alreadyExists" => 10403, "User/ExceptionSession.invalid" => 10431, "User/ExceptionInput.invalidTimezone" => 10441, "User/ExceptionInput.invalidBoolean" => 10442, diff --git a/lib/Database.php b/lib/Database.php index a531f23d..660fbc4a 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -248,11 +248,11 @@ class Database { /** Adds a user to the database * * @param string $user The user to add - * @param string $passwordThe user's password in cleartext. It will be stored hashed + * @param string|null $passwordThe user's password in cleartext. It will be stored hashed */ - public function userAdd(string $user, string $password): bool { + public function userAdd(string $user, ?string $password): bool { if ($this->userExists($user)) { - throw new User\Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]); + throw new User\ExceptionConflict("alreadyExists", ["action" => __FUNCTION__, "user" => $user]); } $hash = (strlen($password) > 0) ? password_hash($password, \PASSWORD_DEFAULT) : ""; // NOTE: This roundabout construction (with 'select' rather than 'values') is required by MySQL, because MySQL is riddled with pitfalls and exceptions @@ -263,7 +263,7 @@ class Database { /** Removes a user from the database */ public function userRemove(string $user): bool { if ($this->db->prepare("DELETE from arsse_users where id = ?", "str")->run($user)->changes() < 1) { - throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } return true; } @@ -280,7 +280,7 @@ class Database { /** Retrieves the hashed password of a user */ public function userPasswordGet(string $user): ?string { if (!$this->userExists($user)) { - throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } return $this->db->prepare("SELECT password from arsse_users where id = ?", "str")->run($user)->getValue(); } @@ -288,11 +288,11 @@ class Database { /** Sets the password of an existing user * * @param string $user The user for whom to set the password - * @param string $password The new password, in cleartext. The password will be stored hashed. If null is passed, the password is unset and authentication not possible + * @param string|null $password The new password, in cleartext. The password will be stored hashed. If null is passed, the password is unset and authentication not possible */ - public function userPasswordSet(string $user, string $password = null): bool { + public function userPasswordSet(string $user, ?string $password): bool { if (!$this->userExists($user)) { - throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } $hash = (strlen($password ?? "") > 0) ? password_hash($password, \PASSWORD_DEFAULT) : $password; $this->db->prepare("UPDATE arsse_users set password = ? where id = ?", "str", "str")->run($hash, $user); @@ -302,7 +302,7 @@ class Database { public function userPropertiesGet(string $user): array { $out = $this->db->prepare("SELECT num, admin, lang, tz, sort_asc from arsse_users where id = ?", "str")->run($user)->getRow(); if (!$out) { - throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } settype($out['num'], "int"); settype($out['admin'], "bool"); @@ -312,7 +312,7 @@ class Database { public function userPropertiesSet(string $user, array $data): bool { if (!$this->userExists($user)) { - throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } $allowed = [ 'admin' => "strict bool", @@ -402,7 +402,7 @@ class Database { */ public function tokenCreate(string $user, string $class, string $id = null, \DateTimeInterface $expires = null): string { if (!$this->userExists($user)) { - throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } // generate a token if it's not provided $id = $id ?? UUID::mint()->hex; diff --git a/lib/ImportExport/AbstractImportExport.php b/lib/ImportExport/AbstractImportExport.php index 72064825..6f0496fd 100644 --- a/lib/ImportExport/AbstractImportExport.php +++ b/lib/ImportExport/AbstractImportExport.php @@ -9,7 +9,7 @@ namespace JKingWeb\Arsse\ImportExport; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Db\ExceptionInput as InputException; -use JKingWeb\Arsse\User\Exception as UserException; +use JKingWeb\Arsse\User\ExceptionConflict as UserException; abstract class AbstractImportExport { public function import(string $user, string $data, bool $flat = false, bool $replace = false): bool { diff --git a/lib/ImportExport/OPML.php b/lib/ImportExport/OPML.php index 30cb4f56..85d136cf 100644 --- a/lib/ImportExport/OPML.php +++ b/lib/ImportExport/OPML.php @@ -7,7 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\ImportExport; use JKingWeb\Arsse\Arsse; -use JKingWeb\Arsse\User\Exception as UserException; +use JKingWeb\Arsse\User\ExceptionConflict as UserException; class OPML extends AbstractImportExport { protected function parse(string $opml, bool $flat): array { diff --git a/lib/REST/Fever/API.php b/lib/REST/Fever/API.php index 1901397f..3382e6cb 100644 --- a/lib/REST/Fever/API.php +++ b/lib/REST/Fever/API.php @@ -241,7 +241,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { try { // verify the supplied hash is valid $s = Arsse::$db->TokenLookup("fever.login", $hash); - } catch (\JKingWeb\Arsse\Db\ExceptionInput $e) { + } catch (ExceptionInput $e) { return false; } // set the user name diff --git a/lib/User.php b/lib/User.php index 8ebee8a5..ffb6d4aa 100644 --- a/lib/User.php +++ b/lib/User.php @@ -48,11 +48,14 @@ class User { return $this->u->userList(); } - public function add($user, $password = null): string { - $out = $this->u->userAdd($user, $password) ?? $this->u->userAdd($user, $this->generatePassword()); - // synchronize the internal database - if (!Arsse::$db->userExists($user)) { - Arsse::$db->userAdd($user, $out); + public function add(string $user, ?string $password = null): string { + try { + $out = $this->u->userAdd($user, $password) ?? $this->u->userAdd($user, $this->generatePassword()); + } finally { + // synchronize the internal database + if (!Arsse::$db->userExists($user)) { + Arsse::$db->userAdd($user, $out ?? null); + } } return $out; } @@ -68,7 +71,7 @@ class User { } } - public function passwordSet(string $user, string $newPassword = null, $oldPassword = null): string { + public function passwordSet(string $user, ?string $newPassword, $oldPassword = null): string { $out = $this->u->userPasswordSet($user, $newPassword, $oldPassword) ?? $this->u->userPasswordSet($user, $this->generatePassword(), $oldPassword); if (Arsse::$db->userExists($user)) { // if the password change was successful and the user exists, set the internal password to the same value diff --git a/lib/User/ExceptionConflict.php b/lib/User/ExceptionConflict.php new file mode 100644 index 00000000..4fa1bbfd --- /dev/null +++ b/lib/User/ExceptionConflict.php @@ -0,0 +1,10 @@ +userExists($user)) { - throw new Exception("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); + throw new ExceptionConflict("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); } else { return true; } @@ -74,7 +74,7 @@ class Driver implements \JKingWeb\Arsse\User\Driver { public function userPropertiesGet(string $user): array { // do nothing: the internal database will retrieve everything for us if (!$this->userExists($user)) { - throw new Exception("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); + throw new ExceptionConflict("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); } else { return []; } @@ -83,7 +83,7 @@ class Driver implements \JKingWeb\Arsse\User\Driver { public function userPropertiesSet(string $user, array $data): array { // do nothing: the internal database will set everything for us if (!$this->userExists($user)) { - throw new Exception("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); + throw new ExceptionConflict("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); } else { return $data; } diff --git a/locale/en.php b/locale/en.php index bcc71db1..e5ada184 100644 --- a/locale/en.php +++ b/locale/en.php @@ -134,8 +134,8 @@ return [ 'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineTypeViolation' => '{0}', 'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general' => '{0}', 'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.logicalLock' => 'Database is locked', - 'Exception.JKingWeb/Arsse/User/Exception.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists', - 'Exception.JKingWeb/Arsse/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist', + 'Exception.JKingWeb/Arsse/User/ExceptionConflict.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists', + 'Exception.JKingWeb/Arsse/User/ExceptionConflict.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist', 'Exception.JKingWeb/Arsse/User/Exception.authMissing' => 'Please log in to proceed', 'Exception.JKingWeb/Arsse/User/Exception.authFailed' => 'Authentication failed', 'Exception.JKingWeb/Arsse/User/ExceptionSession.invalid' => 'Session with ID {0} does not exist', diff --git a/tests/cases/CLI/TestCLI.php b/tests/cases/CLI/TestCLI.php index e6c19e2d..671fbb91 100644 --- a/tests/cases/CLI/TestCLI.php +++ b/tests/cases/CLI/TestCLI.php @@ -142,7 +142,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { Arsse::$user->method("add")->will($this->returnCallback(function($user, $pass = null) { switch ($user) { case "john.doe@example.com": - throw new \JKingWeb\Arsse\User\Exception("alreadyExists"); + throw new \JKingWeb\Arsse\User\ExceptionConflict("alreadyExists"); case "jane.doe@example.com": return is_null($pass) ? "random password" : $pass; } @@ -200,7 +200,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { if ($user === "john.doe@example.com") { return true; } - throw new \JKingWeb\Arsse\User\Exception("doesNotExist"); + throw new \JKingWeb\Arsse\User\ExceptionConflict("doesNotExist"); })); $this->assertConsole($this->cli, $cmd, $exitStatus, $output); } @@ -217,7 +217,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { $passwordChange = function($user, $pass = null) { switch ($user) { case "jane.doe@example.com": - throw new \JKingWeb\Arsse\User\Exception("doesNotExist"); + throw new \JKingWeb\Arsse\User\ExceptionConflict("doesNotExist"); case "john.doe@example.com": return is_null($pass) ? "random password" : $pass; } @@ -247,7 +247,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { $passwordClear = function($user) { switch ($user) { case "jane.doe@example.com": - throw new \JKingWeb\Arsse\User\Exception("doesNotExist"); + throw new \JKingWeb\Arsse\User\ExceptionConflict("doesNotExist"); case "john.doe@example.com": return true; } diff --git a/tests/cases/Database/SeriesToken.php b/tests/cases/Database/SeriesToken.php index 29977b3f..3f766aa3 100644 --- a/tests/cases/Database/SeriesToken.php +++ b/tests/cases/Database/SeriesToken.php @@ -99,7 +99,7 @@ trait SeriesToken { } public function testCreateATokenForAMissingUser(): void { - $this->assertException("doesNotExist", "User"); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); Arsse::$db->tokenCreate("fever.login", "jane.doe@example.biz"); } diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index 10bd2582..350fa272 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -47,7 +47,7 @@ trait SeriesUser { } public function testGetThePasswordOfAMissingUser(): void { - $this->assertException("doesNotExist", "User"); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); Arsse::$db->userPasswordGet("john.doe@example.org"); } @@ -59,7 +59,7 @@ trait SeriesUser { } public function testAddAnExistingUser(): void { - $this->assertException("alreadyExists", "User"); + $this->assertException("alreadyExists", "User", "ExceptionConflict"); Arsse::$db->userAdd("john.doe@example.com", ""); } @@ -71,7 +71,7 @@ trait SeriesUser { } public function testRemoveAMissingUser(): void { - $this->assertException("doesNotExist", "User"); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); Arsse::$db->userRemove("john.doe@example.org"); } @@ -101,7 +101,7 @@ trait SeriesUser { } public function testSetThePasswordOfAMissingUser(): void { - $this->assertException("doesNotExist", "User"); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); Arsse::$db->userPasswordSet("john.doe@example.org", "secret"); } @@ -110,7 +110,7 @@ trait SeriesUser { $this->assertSame($exp, Arsse::$db->userPropertiesGet($user)); } - public function provideMetadata() { + public function provideMetadata(): iterable { return [ ["admin@example.net", ['num' => 1, 'admin' => true, 'lang' => "en", 'tz' => "America/Toronto", 'sort_asc' => false]], ["jane.doe@example.com", ['num' => 2, 'admin' => false, 'lang' => "fr", 'tz' => "Asia/Kuala_Lumpur", 'sort_asc' => true]], @@ -119,7 +119,7 @@ trait SeriesUser { } public function testGetTheMetadataOfAMissingUser(): void { - $this->assertException("doesNotExist", "User"); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); Arsse::$db->userPropertiesGet("john.doe@example.org"); } @@ -147,7 +147,7 @@ trait SeriesUser { } public function testSetTheMetadataOfAMissingUser(): void { - $this->assertException("doesNotExist", "User"); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); Arsse::$db->userPropertiesSet("john.doe@example.org", ['admin' => true]); } } diff --git a/tests/cases/ImportExport/TestImportExport.php b/tests/cases/ImportExport/TestImportExport.php index 1a899c4c..e113d83c 100644 --- a/tests/cases/ImportExport/TestImportExport.php +++ b/tests/cases/ImportExport/TestImportExport.php @@ -146,7 +146,7 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest { } public function testImportForAMissingUser(): void { - $this->assertException("doesNotExist", "User"); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); $this->proc->import("no.one@example.com", "", false, false); } diff --git a/tests/cases/ImportExport/TestOPML.php b/tests/cases/ImportExport/TestOPML.php index 36caa77c..3c61688e 100644 --- a/tests/cases/ImportExport/TestOPML.php +++ b/tests/cases/ImportExport/TestOPML.php @@ -101,7 +101,7 @@ OPML_EXPORT_SERIALIZATION; public function testExportToOpmlAMissingUser(): void { \Phake::when(Arsse::$db)->userExists->thenReturn(false); - $this->assertException("doesNotExist", "User"); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); (new OPML)->export("john.doe@example.com"); } diff --git a/tests/cases/REST/Fever/TestUser.php b/tests/cases/REST/Fever/TestUser.php index d6bab6df..2764c420 100644 --- a/tests/cases/REST/Fever/TestUser.php +++ b/tests/cases/REST/Fever/TestUser.php @@ -10,7 +10,7 @@ use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\User; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Db\ExceptionInput; -use JKingWeb\Arsse\User\Exception as UserException; +use JKingWeb\Arsse\User\ExceptionConflict as UserException; use JKingWeb\Arsse\Db\Transaction; use JKingWeb\Arsse\REST\Fever\User as FeverUser; diff --git a/tests/cases/User/TestInternal.php b/tests/cases/User/TestInternal.php index cd231a0a..21b38b5f 100644 --- a/tests/cases/User/TestInternal.php +++ b/tests/cases/User/TestInternal.php @@ -37,7 +37,7 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::when(Arsse::$db)->userPasswordGet("john.doe@example.com")->thenReturn('$2y$10$1zbqRJhxM8uUjeSBPp4IhO90xrqK0XjEh9Z16iIYEFRV4U.zeAFom'); // hash of "secret" \Phake::when(Arsse::$db)->userPasswordGet("jane.doe@example.com")->thenReturn('$2y$10$bK1ljXfTSyc2D.NYvT.Eq..OpehLRXVbglW.23ihVuyhgwJCd.7Im'); // hash of "superman" \Phake::when(Arsse::$db)->userPasswordGet("owen.hardy@example.com")->thenReturn(""); - \Phake::when(Arsse::$db)->userPasswordGet("kira.nerys@example.com")->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist")); + \Phake::when(Arsse::$db)->userPasswordGet("kira.nerys@example.com")->thenThrow(new \JKingWeb\Arsse\User\ExceptionConflict("doesNotExist")); \Phake::when(Arsse::$db)->userPasswordGet("007@example.com")->thenReturn(null); $this->assertSame($exp, (new Driver)->auth($user, $password)); } @@ -90,11 +90,11 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { public function testRemoveAUser(): void { $john = "john.doe@example.com"; - \Phake::when(Arsse::$db)->userRemove->thenReturn(true)->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist")); + \Phake::when(Arsse::$db)->userRemove->thenReturn(true)->thenThrow(new \JKingWeb\Arsse\User\ExceptionConflict("doesNotExist")); $driver = new Driver; $this->assertTrue($driver->userRemove($john)); \Phake::verify(Arsse::$db, \Phake::times(1))->userRemove($john); - $this->assertException("doesNotExist", "User"); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); try { $this->assertFalse($driver->userRemove($john)); } finally { @@ -118,7 +118,7 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { public function testUnsetAPasswordForAMssingUser(): void { \Phake::when(Arsse::$db)->userExists->thenReturn(false); - $this->assertException("doesNotExist", "User"); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); (new Driver)->userPasswordUnset("john.doe@example.com"); } @@ -131,7 +131,7 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { public function testGetPropertiesForAMissingUser(): void { \Phake::when(Arsse::$db)->userExists->thenReturn(false); - $this->assertException("doesNotExist", "User"); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); try { (new Driver)->userPropertiesGet("john.doe@example.com"); } finally { @@ -150,7 +150,7 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { public function testSetPropertiesForAMissingUser(): void { \Phake::when(Arsse::$db)->userExists->thenReturn(false); - $this->assertException("doesNotExist", "User"); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); try { (new Driver)->userPropertiesSet("john.doe@example.com", ['admin' => true]); } finally { diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 759241c2..97a93523 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -10,6 +10,7 @@ use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\User; use JKingWeb\Arsse\AbstractException as Exception; +use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\Driver; /** @covers \JKingWeb\Arsse\User */ @@ -23,6 +24,11 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { // create a mock user driver $this->drv = \Phake::mock(Driver::class); } + + public function tearDown(): void { + \Phake::verifyNoOtherInteractions($this->drv); + \Phake::verifyNoOtherInteractions(Arsse::$db); + } public function testConstruct(): void { $this->assertInstanceOf(User::class, new User($this->drv)); @@ -49,6 +55,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $u = new User($this->drv); $this->assertSame($exp, $u->auth($user, $password)); $this->assertNull($u->id); + \Phake::verify($this->drv, \Phake::times((int) !$preAuth))->auth($user, $password); \Phake::verify(Arsse::$db, \Phake::times($exp ? 1 : 0))->userExists($user); \Phake::verify(Arsse::$db, \Phake::times($exp && $user === "jane.doe@example.com" ? 1 : 0))->userAdd($user, $password); } @@ -68,190 +75,65 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { ]; } - /** @dataProvider provideUserList */ - public function testListUsers($exp): void { + public function testListUsers(): void { + $exp = ["john.doe@example.com", "jane.doe@example.com"]; $u = new User($this->drv); \Phake::when($this->drv)->userList->thenReturn(["john.doe@example.com", "jane.doe@example.com"]); $this->assertSame($exp, $u->list()); + \Phake::verify($this->drv)->userList(); } - public function provideUserList(): iterable { - $john = "john.doe@example.com"; - $jane = "jane.doe@example.com"; - return [ - [[$john, $jane]], - ]; - } - - /** @dataProvider provideAdditions */ - public function testAddAUser(string $user, $password, $exp): void { + public function testAddAUser(): void { + $user = "ohn.doe@example.com"; + $pass = "secret"; $u = new User($this->drv); - \Phake::when($this->drv)->userAdd("john.doe@example.com", $this->anything())->thenThrow(new \JKingWeb\Arsse\User\Exception("alreadyExists")); - \Phake::when($this->drv)->userAdd("jane.doe@example.com", $this->anything())->thenReturnCallback(function($user, $pass) { - return $pass ?? "random password"; - }); - if ($exp instanceof Exception) { - $this->assertException("alreadyExists", "User"); - } - $this->assertSame($exp, $u->add($user, $password)); + \Phake::when($this->drv)->userAdd->thenReturn($pass); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertSame($pass, $u->add($user, $pass)); + \Phake::verify($this->drv)->userAdd($user, $pass); + \Phake::verify(Arsse::$db)->userExists($user); } - /** @dataProvider provideAdditions */ - public function testAddAUserWithARandomPassword(string $user, $password, $exp): void { - $u = \Phake::partialMock(User::class, $this->drv); - \Phake::when($this->drv)->userAdd($this->anything(), $this->isNull())->thenReturn(null); - \Phake::when($this->drv)->userAdd("john.doe@example.com", $this->logicalNot($this->isNull()))->thenThrow(new \JKingWeb\Arsse\User\Exception("alreadyExists")); - \Phake::when($this->drv)->userAdd("jane.doe@example.com", $this->logicalNot($this->isNull()))->thenReturnCallback(function($user, $pass) { - return $pass; - }); - if ($exp instanceof Exception) { - $this->assertException("alreadyExists", "User"); - $calls = 2; - } else { - $calls = 4; - } - try { - $pass1 = $u->add($user, null); - $pass2 = $u->add($user, null); - $this->assertNotEquals($pass1, $pass2); - } finally { - \Phake::verify($this->drv, \Phake::times($calls))->userAdd; - \Phake::verify($u, \Phake::times($calls / 2))->generatePassword; - } - } - - public function provideAdditions(): iterable { - $john = "john.doe@example.com"; - $jane = "jane.doe@example.com"; - return [ - [$john, "secret", new \JKingWeb\Arsse\User\Exception("alreadyExists")], - [$jane, "superman", "superman"], - [$jane, null, "random password"], - ]; - } - - /** @dataProvider provideRemovals */ - public function testRemoveAUser(string $user, bool $exists, $exp): void { + public function testAddAUserWeDoNotKnow(): void { + $user = "ohn.doe@example.com"; + $pass = "secret"; $u = new User($this->drv); - \Phake::when($this->drv)->userRemove("john.doe@example.com")->thenReturn(true); - \Phake::when($this->drv)->userRemove("jane.doe@example.com")->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist")); - \Phake::when(Arsse::$db)->userExists->thenReturn($exists); - \Phake::when(Arsse::$db)->userRemove->thenReturn(true); - if ($exp instanceof Exception) { - $this->assertException("doesNotExist", "User"); - } - try { - $this->assertSame($exp, $u->remove($user)); - } finally { - \Phake::verify(Arsse::$db, \Phake::times(1))->userExists($user); - \Phake::verify(Arsse::$db, \Phake::times((int) $exists))->userRemove($user); - } + \Phake::when($this->drv)->userAdd->thenReturn($pass); + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertSame($pass, $u->add($user, $pass)); + \Phake::verify($this->drv)->userAdd($user, $pass); + \Phake::verify(Arsse::$db)->userExists($user); + \Phake::verify(Arsse::$db)->userAdd($user, $pass); } - public function provideRemovals(): iterable { - $john = "john.doe@example.com"; - $jane = "jane.doe@example.com"; - return [ - [$john, true, true], - [$john, false, true], - [$jane, true, new \JKingWeb\Arsse\User\Exception("doesNotExist")], - [$jane, false, new \JKingWeb\Arsse\User\Exception("doesNotExist")], - ]; - } - - /** @dataProvider providePasswordChanges */ - public function testChangeAPassword(string $user, $password, bool $exists, $exp): void { + public function testAddADuplicateUser(): void { + $user = "ohn.doe@example.com"; + $pass = "secret"; $u = new User($this->drv); - \Phake::when($this->drv)->userPasswordSet("john.doe@example.com", $this->anything(), $this->anything())->thenReturnCallback(function($user, $pass, $old) { - return $pass ?? "random password"; - }); - \Phake::when($this->drv)->userPasswordSet("jane.doe@example.com", $this->anything(), $this->anything())->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist")); - \Phake::when(Arsse::$db)->userExists->thenReturn($exists); - if ($exp instanceof Exception) { - $this->assertException("doesNotExist", "User"); - $calls = 0; - } else { - $calls = 1; - } + \Phake::when($this->drv)->userAdd->thenThrow(new ExceptionConflict("alreadyExists")); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertException("alreadyExists", "User", "ExceptionConflict"); try { - $this->assertSame($exp, $u->passwordSet($user, $password)); + $u->add($user, $pass); } finally { - \Phake::verify(Arsse::$db, \Phake::times($calls))->userExists($user); - \Phake::verify(Arsse::$db, \Phake::times($exists ? $calls : 0))->userPasswordSet($user, $password ?? "random password", null); + \Phake::verify(Arsse::$db)->userExists($user); + \Phake::verify($this->drv)->userAdd($user, $pass); } } - /** @dataProvider providePasswordChanges */ - public function testChangeAPasswordToARandomPassword(string $user, $password, bool $exists, $exp): void { - $u = \Phake::partialMock(User::class, $this->drv); - \Phake::when($this->drv)->userPasswordSet($this->anything(), $this->isNull(), $this->anything())->thenReturn(null); - \Phake::when($this->drv)->userPasswordSet("john.doe@example.com", $this->logicalNot($this->isNull()), $this->anything())->thenReturnCallback(function($user, $pass, $old) { - return $pass ?? "random password"; - }); - \Phake::when($this->drv)->userPasswordSet("jane.doe@example.com", $this->logicalNot($this->isNull()), $this->anything())->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist")); - \Phake::when(Arsse::$db)->userExists->thenReturn($exists); - if ($exp instanceof Exception) { - $this->assertException("doesNotExist", "User"); - $calls = 2; - } else { - $calls = 4; - } - try { - $pass1 = $u->passwordSet($user, null); - $pass2 = $u->passwordSet($user, null); - $this->assertNotEquals($pass1, $pass2); - } finally { - \Phake::verify($this->drv, \Phake::times($calls))->userPasswordSet; - \Phake::verify($u, \Phake::times($calls / 2))->generatePassword; - \Phake::verify(Arsse::$db, \Phake::times($calls == 4 ? 2 : 0))->userExists($user); - if ($calls == 4) { - \Phake::verify(Arsse::$db, \Phake::times($exists ? 1 : 0))->userPasswordSet($user, $pass1, null); - \Phake::verify(Arsse::$db, \Phake::times($exists ? 1 : 0))->userPasswordSet($user, $pass2, null); - } else { - \Phake::verify(Arsse::$db, \Phake::times(0))->userPasswordSet; - } - } - } - - public function providePasswordChanges(): iterable { - $john = "john.doe@example.com"; - $jane = "jane.doe@example.com"; - return [ - [$john, "superman", true, "superman"], - [$john, null, true, "random password"], - [$john, "superman", false, "superman"], - [$john, null, false, "random password"], - [$jane, "secret", true, new \JKingWeb\Arsse\User\Exception("doesNotExist")], - ]; - } - - /** @dataProvider providePasswordClearings */ - public function testClearAPassword(bool $exists, string $user, $exp): void { - \Phake::when($this->drv)->userPasswordUnset->thenReturn(true); - \Phake::when($this->drv)->userPasswordUnset("jane.doe@example.net", null)->thenThrow(new \JKingWeb\Arsse\User\Exception("doesNotExist")); - \Phake::when(Arsse::$db)->userExists->thenReturn($exists); + public function testAddADuplicateUserWeDoNotKnow(): void { + $user = "ohn.doe@example.com"; + $pass = "secret"; $u = new User($this->drv); + \Phake::when($this->drv)->userAdd->thenThrow(new ExceptionConflict("alreadyExists")); + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertException("alreadyExists", "User", "ExceptionConflict"); try { - if ($exp instanceof \JKingWeb\Arsse\AbstractException) { - $this->assertException($exp); - $u->passwordUnset($user); - } else { - $this->assertSame($exp, $u->passwordUnset($user)); - } + $u->add($user, $pass); } finally { - \Phake::verify(Arsse::$db, \Phake::times((int) ($exists && is_bool($exp))))->userPasswordSet($user, null); + \Phake::verify(Arsse::$db)->userExists($user); + \Phake::verify(Arsse::$db)->userAdd($user, null); + \Phake::verify($this->drv)->userAdd($user, $pass); } } - - public function providePasswordClearings(): iterable { - $missing = new \JKingWeb\Arsse\User\Exception("doesNotExist"); - return [ - [true, "jane.doe@example.com", true], - [true, "john.doe@example.com", true], - [true, "jane.doe@example.net", $missing], - [false, "jane.doe@example.com", true], - [false, "john.doe@example.com", true], - [false, "jane.doe@example.net", $missing], - ]; - } } From 27d9c046d53de240206840a1147088a97ef2cce9 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 16 Nov 2020 00:11:19 -0500 Subject: [PATCH 041/366] More work on user management --- CHANGELOG | 4 ++ lib/AbstractException.php | 1 + lib/User.php | 28 +++++++-- locale/en.php | 1 + tests/cases/User/TestUser.php | 115 ++++++++++++++++++++++++++++++++-- 5 files changed, 139 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 730e60a4..3b65066b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,10 @@ Version 0.9.0 (????-??-??) Bug fixes: - Use icons specified in Atom feeds when available +Changes: +- Explicitly forbid U+003A COLON in usernames, for compatibility with HTTP + Basic authentication + Version 0.8.5 (2020-10-27) ========================== diff --git a/lib/AbstractException.php b/lib/AbstractException.php index 22b6eb52..d26b3cd3 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -75,6 +75,7 @@ abstract class AbstractException extends \Exception { "User/ExceptionSession.invalid" => 10431, "User/ExceptionInput.invalidTimezone" => 10441, "User/ExceptionInput.invalidBoolean" => 10442, + "User/ExceptionInput.invalidUsername" => 10443, "Feed/Exception.internalError" => 10500, "Feed/Exception.invalidCertificate" => 10501, "Feed/Exception.invalidUrl" => 10502, diff --git a/lib/User.php b/lib/User.php index ffb6d4aa..2fec130f 100644 --- a/lib/User.php +++ b/lib/User.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse; use JKingWeb\Arsse\Misc\ValueInfo as V; +use JKingWeb\Arsse\User\ExceptionConflict as Conflict; use PasswordGenerator\Generator as PassGen; class User { @@ -49,26 +50,41 @@ class User { } public function add(string $user, ?string $password = null): string { + // ensure the user name does not contain any U+003A COLON characters, as + // this is incompatible with HTTP Basic authentication + if (strpos($user, ":") !== false) { + throw new User\ExceptionInput("invalidUsername", "U+003A COLON"); + } try { $out = $this->u->userAdd($user, $password) ?? $this->u->userAdd($user, $this->generatePassword()); - } finally { - // synchronize the internal database + } catch (Conflict $e) { if (!Arsse::$db->userExists($user)) { - Arsse::$db->userAdd($user, $out ?? null); + Arsse::$db->userAdd($user, null); } + throw $e; + } + // synchronize the internal database + if (!Arsse::$db->userExists($user)) { + Arsse::$db->userAdd($user, $out); } return $out; } + public function remove(string $user): bool { try { - return $this->u->userRemove($user); - } finally { // @codeCoverageIgnore + $out = $this->u->userRemove($user); + } catch (Conflict $e) { if (Arsse::$db->userExists($user)) { - // if the user was removed and we (still) have it in the internal database, remove it there Arsse::$db->userRemove($user); } + throw $e; } + if (Arsse::$db->userExists($user)) { + // if the user was removed and we (still) have it in the internal database, remove it there + Arsse::$db->userRemove($user); + } + return $out; } public function passwordSet(string $user, ?string $newPassword, $oldPassword = null): string { diff --git a/locale/en.php b/locale/en.php index e5ada184..2791c5b1 100644 --- a/locale/en.php +++ b/locale/en.php @@ -139,6 +139,7 @@ return [ 'Exception.JKingWeb/Arsse/User/Exception.authMissing' => 'Please log in to proceed', 'Exception.JKingWeb/Arsse/User/Exception.authFailed' => 'Authentication failed', 'Exception.JKingWeb/Arsse/User/ExceptionSession.invalid' => 'Session with ID {0} does not exist', + 'Exception.JKingWeb/Arsse/User/ExceptionInput.invalidUsername' => 'User names may not contain the Unicode character {0}', 'Exception.JKingWeb/Arsse/Feed/Exception.internalError' => 'Could not download feed "{url}" because of an internal error which is probably a bug', 'Exception.JKingWeb/Arsse/Feed/Exception.invalidCertificate' => 'Could not download feed "{url}" because its server is serving an invalid SSL certificate', 'Exception.JKingWeb/Arsse/Feed/Exception.invalidUrl' => 'Feed URL "{url}" is invalid', diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 97a93523..9e34a2e6 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -11,6 +11,7 @@ use JKingWeb\Arsse\Database; use JKingWeb\Arsse\User; use JKingWeb\Arsse\AbstractException as Exception; use JKingWeb\Arsse\User\ExceptionConflict; +use JKingWeb\Arsse\User\ExceptionInput; use JKingWeb\Arsse\User\Driver; /** @covers \JKingWeb\Arsse\User */ @@ -84,7 +85,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { } public function testAddAUser(): void { - $user = "ohn.doe@example.com"; + $user = "john.doe@example.com"; $pass = "secret"; $u = new User($this->drv); \Phake::when($this->drv)->userAdd->thenReturn($pass); @@ -95,7 +96,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { } public function testAddAUserWeDoNotKnow(): void { - $user = "ohn.doe@example.com"; + $user = "john.doe@example.com"; $pass = "secret"; $u = new User($this->drv); \Phake::when($this->drv)->userAdd->thenReturn($pass); @@ -107,7 +108,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { } public function testAddADuplicateUser(): void { - $user = "ohn.doe@example.com"; + $user = "john.doe@example.com"; $pass = "secret"; $u = new User($this->drv); \Phake::when($this->drv)->userAdd->thenThrow(new ExceptionConflict("alreadyExists")); @@ -122,7 +123,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { } public function testAddADuplicateUserWeDoNotKnow(): void { - $user = "ohn.doe@example.com"; + $user = "john.doe@example.com"; $pass = "secret"; $u = new User($this->drv); \Phake::when($this->drv)->userAdd->thenThrow(new ExceptionConflict("alreadyExists")); @@ -136,4 +137,110 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify($this->drv)->userAdd($user, $pass); } } + + public function testAddAnInvalidUser(): void { + $user = "john:doe@example.com"; + $pass = "secret"; + $u = new User($this->drv); + \Phake::when($this->drv)->userAdd->thenThrow(new ExceptionInput("invalidUsername")); + $this->assertException("invalidUsername", "User", "ExceptionInput"); + $u->add($user, $pass); + } + + public function testAddAUserWithARandomPassword(): void { + $user = "john.doe@example.com"; + $pass = "random password"; + $u = \Phake::partialMock(User::class, $this->drv); + \Phake::when($u)->generatePassword->thenReturn($pass); + \Phake::when($this->drv)->userAdd->thenReturn(null)->thenReturn($pass); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertSame($pass, $u->add($user)); + \Phake::verify($this->drv)->userAdd($user, null); + \Phake::verify($this->drv)->userAdd($user, $pass); + \Phake::verify(Arsse::$db)->userExists($user); + } + + public function testRemoveAUser(): void { + $user = "john.doe@example.com"; + $pass = "secret"; + $u = new User($this->drv); + \Phake::when($this->drv)->userRemove->thenReturn(true); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertTrue($u->remove($user)); + \Phake::verify(Arsse::$db)->userExists($user); + \Phake::verify(Arsse::$db)->userRemove($user); + \Phake::verify($this->drv)->userRemove($user); + } + + public function testRemoveAUserWeDoNotKnow(): void { + $user = "john.doe@example.com"; + $pass = "secret"; + $u = new User($this->drv); + \Phake::when($this->drv)->userRemove->thenReturn(true); + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertTrue($u->remove($user)); + \Phake::verify(Arsse::$db)->userExists($user); + \Phake::verify($this->drv)->userRemove($user); + } + + public function testRemoveAMissingUser(): void { + $user = "john.doe@example.com"; + $pass = "secret"; + $u = new User($this->drv); + \Phake::when($this->drv)->userRemove->thenThrow(new ExceptionConflict("doesNotExist")); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); + try { + $u->remove($user); + } finally { + \Phake::verify(Arsse::$db)->userExists($user); + \Phake::verify(Arsse::$db)->userRemove($user); + \Phake::verify($this->drv)->userRemove($user); + } + } + + public function testRemoveAMissingUserWeDoNotKnow(): void { + $user = "john.doe@example.com"; + $pass = "secret"; + $u = new User($this->drv); + \Phake::when($this->drv)->userRemove->thenThrow(new ExceptionConflict("doesNotExist")); + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); + try { + $u->remove($user); + } finally { + \Phake::verify(Arsse::$db)->userExists($user); + \Phake::verify($this->drv)->userRemove($user); + } + } + + public function testSetAPassword(): void { + $user = "john.doe@example.com"; + $pass = "secret"; + $u = new User($this->drv); + \Phake::when($this->drv)->userPasswordSet->thenReturn($pass); + \Phake::when(Arsse::$db)->userPasswordSet->thenReturn($pass); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertSame($pass, $u->passwordSet($user, $pass)); + \Phake::verify($this->drv)->userPasswordSet($user, $pass, null); + \Phake::verify(Arsse::$db)->userPasswordSet($user, $pass, null); + \Phake::verify(Arsse::$db)->sessionDestroy($user); + \Phake::verify(Arsse::$db)->userExists($user); + } + + public function testSetARandomPassword(): void { + $user = "john.doe@example.com"; + $pass = "random password"; + $u = \Phake::partialMock(User::class, $this->drv); + \Phake::when($u)->generatePassword->thenReturn($pass); + \Phake::when($this->drv)->userPasswordSet->thenReturn(null)->thenReturn($pass); + \Phake::when(Arsse::$db)->userPasswordSet->thenReturn($pass); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertSame($pass, $u->passwordSet($user, null)); + \Phake::verify($this->drv)->userPasswordSet($user, null, null); + \Phake::verify($this->drv)->userPasswordSet($user, $pass, null); + \Phake::verify(Arsse::$db)->userPasswordSet($user, $pass, null); + \Phake::verify(Arsse::$db)->sessionDestroy($user); + \Phake::verify(Arsse::$db)->userExists($user); + } } From 180b4ecc9b27c3f4c9aa5d78da361ed6b31f3fa5 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 16 Nov 2020 10:24:06 -0500 Subject: [PATCH 042/366] More user tests --- lib/Database.php | 2 +- lib/User.php | 19 ++++--- tests/cases/User/TestUser.php | 101 ++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 10 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 660fbc4a..bd0d25b0 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -248,7 +248,7 @@ class Database { /** Adds a user to the database * * @param string $user The user to add - * @param string|null $passwordThe user's password in cleartext. It will be stored hashed + * @param string|null $passwordThe user's password in cleartext. It will be stored hashed. If null is provided the user will not be able to log in */ public function userAdd(string $user, ?string $password): bool { if ($this->userExists($user)) { diff --git a/lib/User.php b/lib/User.php index 2fec130f..1ef83176 100644 --- a/lib/User.php +++ b/lib/User.php @@ -108,10 +108,6 @@ class User { Arsse::$db->userPasswordSet($user, null); // also invalidate any current sessions for the user Arsse::$db->sessionDestroy($user); - } else { - // if the user does not exist - Arsse::$db->userAdd($user, ""); - Arsse::$db->userPasswordSet($user, null); } return $out; } @@ -119,24 +115,29 @@ class User { public function generatePassword(): string { return (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get(); } - + public function propertiesGet(string $user): array { $extra = $this->u->userPropertiesGet($user); // synchronize the internal database if (!Arsse::$db->userExists($user)) { - Arsse::$db->userAdd($user, $this->generatePassword()); + Arsse::$db->userAdd($user, null); + Arsse::$db->userPropertiesSet($user, $extra); } - // unconditionally retrieve from the database to get at least the user number, and anything else the driver does not provide + // retrieve from the database to get at least the user number, and anything else the driver does not provide $out = Arsse::$db->userPropertiesGet($user); // layer on the driver's data - foreach (["lang", "tz", "admin", "sort_asc"] as $k) { + foreach (["tz", "admin", "sort_asc"] as $k) { if (array_key_exists($k, $extra)) { $out[$k] = $extra[$k] ?? $out[$k]; } } + // treat language specially since it may legitimately be null + if (array_key_exists("lang", $extra)) { + $out['lang'] = $extra['lang']; + } return $out; } - + public function propertiesSet(string $user, array $data): array { $in = []; if (array_key_exists("tz", $data)) { diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 9e34a2e6..313a0541 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -243,4 +243,105 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->sessionDestroy($user); \Phake::verify(Arsse::$db)->userExists($user); } + + public function testSetAPasswordForAUserWeDoNotKnow(): void { + $user = "john.doe@example.com"; + $pass = "secret"; + $u = new User($this->drv); + \Phake::when($this->drv)->userPasswordSet->thenReturn($pass); + \Phake::when(Arsse::$db)->userPasswordSet->thenReturn($pass); + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertSame($pass, $u->passwordSet($user, $pass)); + \Phake::verify($this->drv)->userPasswordSet($user, $pass, null); + \Phake::verify(Arsse::$db)->userAdd($user, $pass); + \Phake::verify(Arsse::$db)->userExists($user); + } + + public function testSetARandomPasswordForAUserWeDoNotKnow(): void { + $user = "john.doe@example.com"; + $pass = "random password"; + $u = \Phake::partialMock(User::class, $this->drv); + \Phake::when($u)->generatePassword->thenReturn($pass); + \Phake::when($this->drv)->userPasswordSet->thenReturn(null)->thenReturn($pass); + \Phake::when(Arsse::$db)->userPasswordSet->thenReturn($pass); + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertSame($pass, $u->passwordSet($user, null)); + \Phake::verify($this->drv)->userPasswordSet($user, null, null); + \Phake::verify($this->drv)->userPasswordSet($user, $pass, null); + \Phake::verify(Arsse::$db)->userAdd($user, $pass); + \Phake::verify(Arsse::$db)->userExists($user); + } + + public function testSetARandomPasswordForAMissingUser(): void { + $user = "john.doe@example.com"; + $pass = "random password"; + $u = \Phake::partialMock(User::class, $this->drv); + \Phake::when($u)->generatePassword->thenReturn($pass); + \Phake::when($this->drv)->userPasswordSet->thenThrow(new ExceptionConflict("doesNotExist")); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); + try { + $u->passwordSet($user, null); + } finally { + \Phake::verify($this->drv)->userPasswordSet($user, null, null); + } + } + + public function testUnsetAPassword(): void { + $user = "john.doe@example.com"; + $u = new User($this->drv); + \Phake::when($this->drv)->userPasswordUnset->thenReturn(true); + \Phake::when(Arsse::$db)->userPasswordUnset->thenReturn(true); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertTrue($u->passwordUnset($user)); + \Phake::verify($this->drv)->userPasswordUnset($user, null); + \Phake::verify(Arsse::$db)->userPasswordSet($user, null); + \Phake::verify(Arsse::$db)->sessionDestroy($user); + \Phake::verify(Arsse::$db)->userExists($user); + } + + public function testUnsetAPasswordForAUserWeDoNotKnow(): void { + $user = "john.doe@example.com"; + $u = new User($this->drv); + \Phake::when($this->drv)->userPasswordUnset->thenReturn(true); + \Phake::when(Arsse::$db)->userPasswordUnset->thenReturn(true); + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertTrue($u->passwordUnset($user)); + \Phake::verify($this->drv)->userPasswordUnset($user, null); + \Phake::verify(Arsse::$db)->userExists($user); + } + + public function testUnsetAPasswordForAMissingUser(): void { + $user = "john.doe@example.com"; + $u = new User($this->drv); + \Phake::when($this->drv)->userPasswordUnset->thenThrow(new ExceptionConflict("doesNotExist")); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); + try { + $u->passwordUnset($user); + } finally { + \Phake::verify($this->drv)->userPasswordUnset($user, null); + } + } + + /** @dataProvider provideProperties */ + public function testGetThePropertiesOfAUser(array $exp, array $base, array $extra): void { + $user = "john.doe@example.com"; + $u = new User($this->drv); + \Phake::when($this->drv)->userPropertiesGet->thenReturn($extra); + \Phake::when(Arsse::$db)->userPropertiesGet->thenReturn($base); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertSame($exp, $u->propertiesGet($user)); + \Phake::verify($this->drv)->userPropertiesGet($user); + \Phake::verify(Arsse::$db)->userPropertiesGet($user); + \Phake::verify(Arsse::$db)->userExists($user); + } + + public function provideProperties(): iterable { + $defaults = ['num' => 1, 'admin' => false, 'lang' => null, 'tz' => "Etc/UTC", 'sort_asc' => false]; + return [ + [$defaults, $defaults, []], + [$defaults, $defaults, ['num' => 2112, 'blah' => "bloo"]], + [['num' => 1, 'admin' => true, 'lang' => "fr", 'tz' => "America/Toronto", 'sort_asc' => true], $defaults, ['admin' => true, 'lang' => "fr", 'tz' => "America/Toronto", 'sort_asc' => true]], + [['num' => 1, 'admin' => true, 'lang' => null, 'tz' => "America/Toronto", 'sort_asc' => true], ['num' => 1, 'admin' => true, 'lang' => "fr", 'tz' => "America/Toronto", 'sort_asc' => true], ['lang' => null]], + ]; + } } From e16df90bae6868bcb0a438c7358e779baf94fac1 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 16 Nov 2020 10:26:14 -0500 Subject: [PATCH 043/366] Style fixes --- lib/Database.php | 21 ++++++++++----------- lib/Db/PostgreSQL/PDOResult.php | 2 +- lib/Db/PostgreSQL/Result.php | 2 +- lib/REST/Miniflux/V1.php | 5 ----- lib/User.php | 1 - lib/User/Driver.php | 26 +++++++++++++------------- tests/cases/Database/SeriesCleanup.php | 4 ++-- tests/cases/Database/SeriesUser.php | 18 +++++++++--------- tests/cases/Db/BaseUpdate.php | 5 +++-- tests/cases/User/TestInternal.php | 8 ++++---- tests/cases/User/TestUser.php | 11 +++++------ tests/docroot/Icon/SVG1.php | 2 +- tests/docroot/Icon/SVG2.php | 2 +- 13 files changed, 50 insertions(+), 57 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index bd0d25b0..8bcd8298 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -37,7 +37,7 @@ use JKingWeb\Arsse\Misc\URL; * associations with articles. There has been an effort to keep public method * names consistent throughout, but protected methods, having different * concerns, will typically follow different conventions. - * + * * Note that operations on users should be performed with the User class rather * than the Database class directly. This is to allow for alternate user sources. */ @@ -298,7 +298,7 @@ class Database { $this->db->prepare("UPDATE arsse_users set password = ? where id = ?", "str", "str")->run($hash, $user); return true; } - + public function userPropertiesGet(string $user): array { $out = $this->db->prepare("SELECT num, admin, lang, tz, sort_asc from arsse_users where id = ?", "str")->run($user)->getRow(); if (!$out) { @@ -309,7 +309,7 @@ class Database { settype($out['sort_asc'], "bool"); return $out; } - + public function userPropertiesSet(string $user, array $data): bool { if (!$this->userExists($user)) { throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); @@ -325,7 +325,6 @@ class Database { return false; } return (bool) $this->db->prepare("UPDATE arsse_users set $setClause where id = ?", $setTypes, "str")->run($setValues, $user)->changes(); - } /** Creates a new session for the given user and returns the session identifier */ @@ -874,16 +873,16 @@ class Database { $out = $this->db->prepare("SELECT $field from arsse_tags where id in (select tag from arsse_tag_members where subscription = ? and assigned = 1) order by $field", "int")->run($id)->getAll(); return $out ? array_column($out, $field) : []; } - + /** Retrieves detailed information about the icon for a subscription. - * + * * The returned information is: - * + * * - "id": The umeric identifier of the icon (not the subscription) * - "url": The URL of the icon * - "type": The Content-Type of the icon e.g. "image/png" * - "data": The icon itself, as a binary sring; if $withData is false this will be null - * + * * @param string|null $user The user who owns the subscription being queried; using null here is supported for TT-RSS and SHOULD NOT be used elsewhere as it leaks information * @param int $subscription The numeric identifier of the subscription * @param bool $includeData Whether to include the binary data of the icon itself in the result @@ -1219,14 +1218,14 @@ class Database { } /** Lists icons for feeds to which a user is subscribed - * + * * The returned information for each icon is: - * + * * - "id": The umeric identifier of the icon * - "url": The URL of the icon * - "type": The Content-Type of the icon e.g. "image/png" * - "data": The icon itself, as a binary sring - * + * * @param string $user The user whose subscription icons are to be retrieved */ public function iconList(string $user): Db\Result { diff --git a/lib/Db/PostgreSQL/PDOResult.php b/lib/Db/PostgreSQL/PDOResult.php index 91fe4c09..4920776f 100644 --- a/lib/Db/PostgreSQL/PDOResult.php +++ b/lib/Db/PostgreSQL/PDOResult.php @@ -13,7 +13,7 @@ class PDOResult extends \JKingWeb\Arsse\Db\PDOResult { public function valid() { $this->cur = $this->set->fetch(\PDO::FETCH_ASSOC); if ($this->cur !== false) { - foreach($this->cur as $k => $v) { + foreach ($this->cur as $k => $v) { if (is_resource($v)) { $this->cur[$k] = stream_get_contents($v); fclose($v); diff --git a/lib/Db/PostgreSQL/Result.php b/lib/Db/PostgreSQL/Result.php index 2b4d1b63..7200ac35 100644 --- a/lib/Db/PostgreSQL/Result.php +++ b/lib/Db/PostgreSQL/Result.php @@ -48,7 +48,7 @@ class Result extends \JKingWeb\Arsse\Db\AbstractResult { public function valid() { $this->cur = pg_fetch_row($this->r, null, \PGSQL_ASSOC); if ($this->cur !== false) { - foreach($this->blobs as $f) { + foreach ($this->blobs as $f) { if ($this->cur[$f]) { $this->cur[$f] = hex2bin(substr($this->cur[$f], 2)); } diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 9edff158..74873afd 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -7,19 +7,14 @@ declare(strict_types=1); namespace JKingWeb\Arsse\REST\Miniflux; use JKingWeb\Arsse\Arsse; -use JKingWeb\Arsse\Service; -use JKingWeb\Arsse\Context\Context; use JKingWeb\Arsse\Misc\ValueInfo; use JKingWeb\Arsse\AbstractException; -use JKingWeb\Arsse\Db\ExceptionInput; -use JKingWeb\Arsse\Feed\Exception as FeedException; use JKingWeb\Arsse\Misc\HTTP; use JKingWeb\Arsse\REST\Exception; use JKingWeb\Arsse\REST\Exception404; use JKingWeb\Arsse\REST\Exception405; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; -use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\EmptyResponse; class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { diff --git a/lib/User.php b/lib/User.php index 1ef83176..c44a2068 100644 --- a/lib/User.php +++ b/lib/User.php @@ -69,7 +69,6 @@ class User { } return $out; } - public function remove(string $user): bool { try { diff --git a/lib/User/Driver.php b/lib/User/Driver.php index dbf8ad68..5da6a0ca 100644 --- a/lib/User/Driver.php +++ b/lib/User/Driver.php @@ -16,12 +16,12 @@ interface Driver { public function auth(string $user, string $password): bool; /** Adds a new user and returns their password - * + * * When given no password the implementation may return null; the user * manager will then generate a random password and try again with that - * password. Alternatively the implementation may generate its own + * password. Alternatively the implementation may generate its own * password if desired - * + * * @param string $user The username to create * @param string|null $password The cleartext password to assign to the user, or null to generate a random password */ @@ -34,45 +34,45 @@ interface Driver { public function userList(): array; /** Sets a user's password - * + * * When given no password the implementation may return null; the user * manager will then generate a random password and try again with that - * password. Alternatively the implementation may generate its own + * password. Alternatively the implementation may generate its own * password if desired - * + * * @param string $user The user for whom to change the password * @param string|null $password The cleartext password to assign to the user, or null to generate a random password * @param string|null $oldPassword The user's previous password, if known */ public function userPasswordSet(string $user, ?string $newPassword, string $oldPassword = null); - /** Removes a user's password; this makes authentication fail unconditionally - * + /** Removes a user's password; this makes authentication fail unconditionally + * * @param string $user The user for whom to change the password * @param string|null $oldPassword The user's previous password, if known */ public function userPasswordUnset(string $user, string $oldPassword = null): bool; /** Retrieves metadata about a user - * + * * Any expected keys not returned by the driver are taken from the internal * database instead; the expected keys at this time are: - * + * * - admin: A boolean denoting whether the user has administrator privileges * - lang: A BCP 47 language tag e.g. "en", "hy-Latn-IT-arevela" * - tz: A zoneinfo timezone e.g. "Asia/Jakarta", "America/Argentina/La_Rioja" * - sort_asc: A boolean denoting whether the user prefers articles to be sorted oldest-first - * + * * Any other keys will be ignored. */ public function userPropertiesGet(string $user): array; /** Sets metadata about a user - * + * * Output should be the same as the input, unless input is changed prior to storage * (if it is, for instance, normalized in some way), which which case the changes * should be reflected in the output. - * + * * @param string $user The user for which to set metadata * @param array $data The input data; see userPropertiesGet for keys */ diff --git a/tests/cases/Database/SeriesCleanup.php b/tests/cases/Database/SeriesCleanup.php index 1a0e1c7e..cdbb66a0 100644 --- a/tests/cases/Database/SeriesCleanup.php +++ b/tests/cases/Database/SeriesCleanup.php @@ -68,8 +68,8 @@ trait SeriesCleanup { ], 'arsse_icons' => [ 'columns' => [ - 'id' => "int", - 'url' => "str", + 'id' => "int", + 'url' => "str", 'orphaned' => "datetime", ], 'rows' => [ diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index 350fa272..0c13012b 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -104,12 +104,12 @@ trait SeriesUser { $this->assertException("doesNotExist", "User", "ExceptionConflict"); Arsse::$db->userPasswordSet("john.doe@example.org", "secret"); } - + /** @dataProvider provideMetaData */ public function testGetMetadata(string $user, array $exp): void { $this->assertSame($exp, Arsse::$db->userPropertiesGet($user)); } - + public function provideMetadata(): iterable { return [ ["admin@example.net", ['num' => 1, 'admin' => true, 'lang' => "en", 'tz' => "America/Toronto", 'sort_asc' => false]], @@ -122,12 +122,12 @@ trait SeriesUser { $this->assertException("doesNotExist", "User", "ExceptionConflict"); Arsse::$db->userPropertiesGet("john.doe@example.org"); } - + public function testSetMetadata(): void { $in = [ - 'admin' => true, - 'lang' => "en-ca", - 'tz' => "Atlantic/Reykjavik", + 'admin' => true, + 'lang' => "en-ca", + 'tz' => "Atlantic/Reykjavik", 'sort_asc' => true, ]; $this->assertTrue(Arsse::$db->userPropertiesSet("john.doe@example.com", $in)); @@ -135,11 +135,11 @@ trait SeriesUser { $state['arsse_users']['rows'][2] = ["john.doe@example.com", 3, 1, "en-ca", "Atlantic/Reykjavik", 1]; $this->compareExpectations(static::$drv, $state); } - + public function testSetNoMetadata(): void { $in = [ - 'num' => 2112, - 'blah' => "bloo" + 'num' => 2112, + 'blah' => "bloo", ]; $this->assertFalse(Arsse::$db->userPropertiesSet("john.doe@example.com", $in)); $state = $this->primeExpectations($this->data, ['arsse_users' => ['id', 'num', 'admin', 'lang', 'tz', 'sort_asc']]); diff --git a/tests/cases/Db/BaseUpdate.php b/tests/cases/Db/BaseUpdate.php index ba93687e..bce4dbcf 100644 --- a/tests/cases/Db/BaseUpdate.php +++ b/tests/cases/Db/BaseUpdate.php @@ -134,10 +134,11 @@ class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest { $this->drv->schemaUpdate(Database::SCHEMA_VERSION); $this->assertTrue($this->drv->maintenance()); } - + public function testUpdateTo7(): void { $this->drv->schemaUpdate(6); - $this->drv->exec(<<drv->exec( + <<assertException("doesNotExist", "User", "ExceptionConflict"); (new Driver)->userPasswordUnset("john.doe@example.com"); } - + public function testGetUserProperties(): void { \Phake::when(Arsse::$db)->userExists->thenReturn(true); $this->assertSame([], (new Driver)->userPropertiesGet("john.doe@example.com")); \Phake::verify(Arsse::$db)->userExists("john.doe@example.com"); \Phake::verifyNoFurtherInteraction(Arsse::$db); } - + public function testGetPropertiesForAMissingUser(): void { \Phake::when(Arsse::$db)->userExists->thenReturn(false); $this->assertException("doesNotExist", "User", "ExceptionConflict"); @@ -139,7 +139,7 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verifyNoFurtherInteraction(Arsse::$db); } } - + public function testSetUserProperties(): void { $in = ['admin' => true]; \Phake::when(Arsse::$db)->userExists->thenReturn(true); @@ -147,7 +147,7 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->userExists("john.doe@example.com"); \Phake::verifyNoFurtherInteraction(Arsse::$db); } - + public function testSetPropertiesForAMissingUser(): void { \Phake::when(Arsse::$db)->userExists->thenReturn(false); $this->assertException("doesNotExist", "User", "ExceptionConflict"); diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 313a0541..e958c557 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -9,7 +9,6 @@ namespace JKingWeb\Arsse\TestCase\User; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\User; -use JKingWeb\Arsse\AbstractException as Exception; use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\ExceptionInput; use JKingWeb\Arsse\User\Driver; @@ -25,7 +24,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { // create a mock user driver $this->drv = \Phake::mock(Driver::class); } - + public function tearDown(): void { \Phake::verifyNoOtherInteractions($this->drv); \Phake::verifyNoOtherInteractions(Arsse::$db); @@ -159,7 +158,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify($this->drv)->userAdd($user, $pass); \Phake::verify(Arsse::$db)->userExists($user); } - + public function testRemoveAUser(): void { $user = "john.doe@example.com"; $pass = "secret"; @@ -171,7 +170,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->userRemove($user); \Phake::verify($this->drv)->userRemove($user); } - + public function testRemoveAUserWeDoNotKnow(): void { $user = "john.doe@example.com"; $pass = "secret"; @@ -182,7 +181,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->userExists($user); \Phake::verify($this->drv)->userRemove($user); } - + public function testRemoveAMissingUser(): void { $user = "john.doe@example.com"; $pass = "secret"; @@ -198,7 +197,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify($this->drv)->userRemove($user); } } - + public function testRemoveAMissingUserWeDoNotKnow(): void { $user = "john.doe@example.com"; $pass = "secret"; diff --git a/tests/docroot/Icon/SVG1.php b/tests/docroot/Icon/SVG1.php index 0543d91a..5c90d375 100644 --- a/tests/docroot/Icon/SVG1.php +++ b/tests/docroot/Icon/SVG1.php @@ -1,4 +1,4 @@ "image/svg+xml", - 'content' => '' + 'content' => '', ]; diff --git a/tests/docroot/Icon/SVG2.php b/tests/docroot/Icon/SVG2.php index 4ade7ce4..e5260cf3 100644 --- a/tests/docroot/Icon/SVG2.php +++ b/tests/docroot/Icon/SVG2.php @@ -1,4 +1,4 @@ "image/svg+xml", - 'content' => '' + 'content' => '', ]; From d3ebb1bd56cd85d055ccc042faeae4a1279e68fa Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 17 Nov 2020 16:23:36 -0500 Subject: [PATCH 044/366] Last set of tests for user management. Fixes #180 --- lib/User.php | 11 ++-- locale/en.php | 2 + tests/cases/User/TestUser.php | 111 ++++++++++++++++++++++++++++++++-- 3 files changed, 114 insertions(+), 10 deletions(-) diff --git a/lib/User.php b/lib/User.php index c44a2068..0a70e5d2 100644 --- a/lib/User.php +++ b/lib/User.php @@ -69,6 +69,7 @@ class User { } return $out; } + public function remove(string $user): bool { try { @@ -141,15 +142,15 @@ class User { $in = []; if (array_key_exists("tz", $data)) { if (!is_string($data['tz'])) { - throw new User\ExceptionInput("invalidTimezone"); - } elseif (!in_array($data['tz'], \DateTimeZone::listIdentifiers())) { - throw new User\ExceptionInput("invalidTimezone", $data['tz']); + throw new User\ExceptionInput("invalidTimezone", ['field' => "tz", 'value' => ""]); + } elseif(!@timezone_open($data['tz'])) { + throw new User\ExceptionInput("invalidTimezone", ['field' => "tz", 'value' => $data['tz']]); } $in['tz'] = $data['tz']; } foreach (["admin", "sort_asc"] as $k) { if (array_key_exists($k, $data)) { - if (($v = V::normalize($data[$k], V::T_BOOL)) === null) { + if (($v = V::normalize($data[$k], V::T_BOOL | V::M_DROP)) === null) { throw new User\ExceptionInput("invalidBoolean", $k); } $in[$k] = $v; @@ -161,7 +162,7 @@ class User { $out = $this->u->userPropertiesSet($user, $in); // synchronize the internal database if (!Arsse::$db->userExists($user)) { - Arsse::$db->userAdd($user, $this->generatePassword()); + Arsse::$db->userAdd($user, null); } Arsse::$db->userPropertiesSet($user, $out); return $out; diff --git a/locale/en.php b/locale/en.php index 2791c5b1..66a03eee 100644 --- a/locale/en.php +++ b/locale/en.php @@ -140,6 +140,8 @@ return [ 'Exception.JKingWeb/Arsse/User/Exception.authFailed' => 'Authentication failed', 'Exception.JKingWeb/Arsse/User/ExceptionSession.invalid' => 'Session with ID {0} does not exist', 'Exception.JKingWeb/Arsse/User/ExceptionInput.invalidUsername' => 'User names may not contain the Unicode character {0}', + 'Exception.JKingWeb/Arsse/User/ExceptionInput.invalidBoolean' => 'User property "{0}" must be a boolean value (true or false)', + 'Exception.JKingWeb/Arsse/User/ExceptionInput.invalidTimezone' => 'User property "{field}" must be a valid zoneinfo timezone', 'Exception.JKingWeb/Arsse/Feed/Exception.internalError' => 'Could not download feed "{url}" because of an internal error which is probably a bug', 'Exception.JKingWeb/Arsse/Feed/Exception.invalidCertificate' => 'Could not download feed "{url}" because its server is serving an invalid SSL certificate', 'Exception.JKingWeb/Arsse/Feed/Exception.invalidUrl' => 'Feed URL "{url}" is invalid', diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index e958c557..310a0a33 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -9,6 +9,7 @@ namespace JKingWeb\Arsse\TestCase\User; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\User; +use JKingWeb\Arsse\AbstractException as Exception; use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\ExceptionInput; use JKingWeb\Arsse\User\Driver; @@ -24,7 +25,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { // create a mock user driver $this->drv = \Phake::mock(Driver::class); } - + public function tearDown(): void { \Phake::verifyNoOtherInteractions($this->drv); \Phake::verifyNoOtherInteractions(Arsse::$db); @@ -42,6 +43,13 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $u->id = null; $this->assertSame("", (string) $u); } + + public function testGeneratePasswords(): void { + $u = new User($this->drv); + $pass1 = $u->generatePassword(); + $pass2 = $u->generatePassword(); + $this->assertNotEquals($pass1, $pass2); + } /** @dataProvider provideAuthentication */ public function testAuthenticateAUser(bool $preAuth, string $user, string $password, bool $exp): void { @@ -158,7 +166,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify($this->drv)->userAdd($user, $pass); \Phake::verify(Arsse::$db)->userExists($user); } - + public function testRemoveAUser(): void { $user = "john.doe@example.com"; $pass = "secret"; @@ -170,7 +178,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->userRemove($user); \Phake::verify($this->drv)->userRemove($user); } - + public function testRemoveAUserWeDoNotKnow(): void { $user = "john.doe@example.com"; $pass = "secret"; @@ -181,7 +189,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->userExists($user); \Phake::verify($this->drv)->userRemove($user); } - + public function testRemoveAMissingUser(): void { $user = "john.doe@example.com"; $pass = "secret"; @@ -197,7 +205,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify($this->drv)->userRemove($user); } } - + public function testRemoveAMissingUserWeDoNotKnow(): void { $user = "john.doe@example.com"; $pass = "secret"; @@ -343,4 +351,97 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { [['num' => 1, 'admin' => true, 'lang' => null, 'tz' => "America/Toronto", 'sort_asc' => true], ['num' => 1, 'admin' => true, 'lang' => "fr", 'tz' => "America/Toronto", 'sort_asc' => true], ['lang' => null]], ]; } + + public function testGetThePropertiesOfAUserWeDoNotKnow(): void { + $user = "john.doe@example.com"; + $extra = ['tz' => "Europe/Istanbul"]; + $base = ['num' => 47, 'admin' => false, 'lang' => null, 'tz' => "Etc/UTC", 'sort_asc' => false]; + $exp = ['num' => 47, 'admin' => false, 'lang' => null, 'tz' => "Europe/Istanbul", 'sort_asc' => false]; + $u = new User($this->drv); + \Phake::when($this->drv)->userPropertiesGet->thenReturn($extra); + \Phake::when(Arsse::$db)->userPropertiesGet->thenReturn($base); + \Phake::when(Arsse::$db)->userAdd->thenReturn(true); + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertSame($exp, $u->propertiesGet($user)); + \Phake::verify($this->drv)->userPropertiesGet($user); + \Phake::verify(Arsse::$db)->userPropertiesGet($user); + \Phake::verify(Arsse::$db)->userPropertiesSet($user, $extra); + \Phake::verify(Arsse::$db)->userAdd($user, null); + \Phake::verify(Arsse::$db)->userExists($user); + } + + public function testGetThePropertiesOfAMissingUser(): void { + $user = "john.doe@example.com"; + $u = new User($this->drv); + \Phake::when($this->drv)->userPropertiesGet->thenThrow(new ExceptionConflict("doesNotExist")); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); + try { + $u->propertiesGet($user); + } finally { + \Phake::verify($this->drv)->userPropertiesGet($user); + } + } + + /** @dataProvider providePropertyChanges */ + public function testSetThePropertiesOfAUser(array $in, $out): void { + $user = "john.doe@example.com"; + $u = new User($this->drv); + if ($out instanceof \Exception) { + $this->assertException($out); + $u->propertiesSet($user, $in); + } else { + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + \Phake::when($this->drv)->userPropertiesSet->thenReturn($out); + \Phake::when(Arsse::$db)->userPropertiesSet->thenReturn(true); + $this->assertSame($out, $u->propertiesSet($user, $in)); + \Phake::verify($this->drv)->userPropertiesSet($user, $in); + \Phake::verify(Arsse::$db)->userPropertiesSet($user, $out); + \Phake::verify(Arsse::$db)->userExists($user); + } + } + + /** @dataProvider providePropertyChanges */ + public function testSetThePropertiesOfAUserWeDoNotKnow(array $in, $out): void { + $user = "john.doe@example.com"; + $u = new User($this->drv); + if ($out instanceof \Exception) { + $this->assertException($out); + $u->propertiesSet($user, $in); + } else { + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + \Phake::when($this->drv)->userPropertiesSet->thenReturn($out); + \Phake::when(Arsse::$db)->userPropertiesSet->thenReturn(true); + $this->assertSame($out, $u->propertiesSet($user, $in)); + \Phake::verify($this->drv)->userPropertiesSet($user, $in); + \Phake::verify(Arsse::$db)->userPropertiesSet($user, $out); + \Phake::verify(Arsse::$db)->userExists($user); + \Phake::verify(Arsse::$db)->userAdd($user, null); + } + } + + public function providePropertyChanges(): iterable { + return [ + [['admin' => true], ['admin' => true]], + [['admin' => 2], new ExceptionInput("invalidBoolean")], + [['sort_asc' => 2], new ExceptionInput("invalidBoolean")], + [['tz' => "Etc/UTC"], ['tz' => "Etc/UTC"]], + [['tz' => "Etc/blah"], new ExceptionInput("invalidTimezone")], + [['tz' => false], new ExceptionInput("invalidTimezone")], + [['lang' => "en-ca"], ['lang' => "en-CA"]], + [['lang' => null], ['lang' => null]], + ]; + } + + public function testSetThePropertiesOfAMissingUser(): void { + $user = "john.doe@example.com"; + $in = ['admin' => true]; + $u = new User($this->drv); + \Phake::when($this->drv)->userPropertiesSet->thenThrow(new ExceptionConflict("doesNotExist")); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); + try { + $u->propertiesSet($user, $in); + } finally { + \Phake::verify($this->drv)->userPropertiesSet($user, $in); + } + } } From d4bcdcdaddec14ed5bbc8e64d59091765953ef4f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 18 Nov 2020 10:01:20 -0500 Subject: [PATCH 045/366] Fix TTRSS coverage --- tests/cases/REST/TinyTinyRSS/TestIcon.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/cases/REST/TinyTinyRSS/TestIcon.php b/tests/cases/REST/TinyTinyRSS/TestIcon.php index 5341238f..18735af7 100644 --- a/tests/cases/REST/TinyTinyRSS/TestIcon.php +++ b/tests/cases/REST/TinyTinyRSS/TestIcon.php @@ -9,6 +9,7 @@ namespace JKingWeb\Arsse\TestCase\REST\TinyTinyRSS; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\User; use JKingWeb\Arsse\Database; +use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\REST\TinyTinyRSS\Icon; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\EmptyResponse as Response; @@ -49,6 +50,7 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest { public function testRetrieveFavion(): void { \Phake::when(Arsse::$db)->subscriptionIcon->thenReturn(['url' => null]); + \Phake::when(Arsse::$db)->subscriptionIcon($this->anything(), 1123, false)->thenThrow(new ExceptionInput("subjectMissing")); \Phake::when(Arsse::$db)->subscriptionIcon($this->anything(), 42, false)->thenReturn(['url' => "http://example.com/favicon.ico"]); \Phake::when(Arsse::$db)->subscriptionIcon($this->anything(), 2112, false)->thenReturn(['url' => "http://example.net/logo.png"]); \Phake::when(Arsse::$db)->subscriptionIcon($this->anything(), 1337, false)->thenReturn(['url' => "http://example.org/icon.gif\r\nLocation: http://bad.example.com/"]); @@ -65,6 +67,7 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertMessage($exp, $this->req("ook")); $this->assertMessage($exp, $this->req("47.ico")); $this->assertMessage($exp, $this->req("2112.png")); + $this->assertMessage($exp, $this->req("1123.ico")); // only GET is allowed $exp = new Response(405, ['Allow' => "GET"]); $this->assertMessage($exp, $this->req("2112.ico", "PUT")); From f6cd2b87ce3a01f319eb01418f95d4a9de3b2410 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 18 Nov 2020 11:25:28 -0500 Subject: [PATCH 046/366] Port token data from Microsub branch --- lib/Database.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 8bcd8298..ace75a83 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -398,15 +398,16 @@ class Database { * @param string $class The class of the token e.g. the protocol name * @param string|null $id The value of the token; if none is provided a UUID will be generated * @param \DateTimeInterface|null $expires An optional expiry date and time for the token + * @param string $data Application-specific data associated with a token */ - public function tokenCreate(string $user, string $class, string $id = null, \DateTimeInterface $expires = null): string { + public function tokenCreate(string $user, string $class, string $id = null, \DateTimeInterface $expires = null, string $data = null): string { if (!$this->userExists($user)) { throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } // generate a token if it's not provided $id = $id ?? UUID::mint()->hex; // save the token to the database - $this->db->prepare("INSERT INTO arsse_tokens(id,class,\"user\",expires) values(?,?,?,?)", "str", "str", "str", "datetime")->run($id, $class, $user, $expires); + $this->db->prepare("INSERT INTO arsse_tokens(id,class,\"user\",expires,data) values(?,?,?,?,?)", "str", "str", "str", "datetime", "str")->run($id, $class, $user, $expires, $data); // return the ID return $id; } @@ -428,7 +429,7 @@ class Database { /** Look up data associated with a token */ public function tokenLookup(string $class, string $id): array { - $out = $this->db->prepare("SELECT id,class,\"user\",created,expires from arsse_tokens where class = ? and id = ? and (expires is null or expires > CURRENT_TIMESTAMP)", "str", "str")->run($class, $id)->getRow(); + $out = $this->db->prepare("SELECT id,class,\"user\",created,expires,data from arsse_tokens where class = ? and id = ? and (expires is null or expires > CURRENT_TIMESTAMP)", "str", "str")->run($class, $id)->getRow(); if (!$out) { throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "token", 'id' => $id]); } From 06dee77bac15a3fb0b91976d3b234c0e6af63eea Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 23 Nov 2020 09:31:50 -0500 Subject: [PATCH 047/366] First tests for Miniflux --- lib/Database.php | 9 +- lib/REST.php | 12 +- lib/REST/Miniflux/ErrorResponse.php | 19 +++ lib/REST/Miniflux/Status.php | 37 ++++++ lib/REST/Miniflux/V1.php | 56 +++++++-- lib/User.php | 3 +- locale/en.php | 3 + tests/cases/Database/SeriesToken.php | 21 +++- .../cases/REST/Miniflux/TestErrorResponse.php | 22 ++++ tests/cases/REST/Miniflux/TestStatus.php | 34 +++++ tests/cases/REST/Miniflux/TestV1.php | 118 ++++++++++++++++++ tests/cases/User/TestUser.php | 21 ++-- tests/phpunit.dist.xml | 6 +- 13 files changed, 328 insertions(+), 33 deletions(-) create mode 100644 lib/REST/Miniflux/ErrorResponse.php create mode 100644 lib/REST/Miniflux/Status.php create mode 100644 tests/cases/REST/Miniflux/TestErrorResponse.php create mode 100644 tests/cases/REST/Miniflux/TestStatus.php create mode 100644 tests/cases/REST/Miniflux/TestV1.php diff --git a/lib/Database.php b/lib/Database.php index ace75a83..760a0de4 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -400,7 +400,7 @@ class Database { * @param \DateTimeInterface|null $expires An optional expiry date and time for the token * @param string $data Application-specific data associated with a token */ - public function tokenCreate(string $user, string $class, string $id = null, \DateTimeInterface $expires = null, string $data = null): string { + public function tokenCreate(string $user, string $class, string $id = null, ?\DateTimeInterface $expires = null, string $data = null): string { if (!$this->userExists($user)) { throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } @@ -418,7 +418,7 @@ class Database { * @param string $class The class of the token e.g. the protocol name * @param string|null $id The ID of a specific token, or null for all tokens in the class */ - public function tokenRevoke(string $user, string $class, string $id = null): bool { + public function tokenRevoke(string $user, string $class, ?string $id = null): bool { if (is_null($id)) { $out = $this->db->prepare("DELETE FROM arsse_tokens where \"user\" = ? and class = ?", "str", "str")->run($user, $class)->changes(); } else { @@ -436,6 +436,11 @@ class Database { return $out; } + /** List tokens associated with a user */ + public function tokenList(string $user, string $class): Db\Result { + return $this->db->prepare("SELECT id,created,expires,data from arsse_tokens where class = ? and user = ? and (expires is null or expires > CURRENT_TIMESTAMP)", "str", "str")->run($class, $user); + } + /** Deletes expires tokens from the database, returning the number of deleted tokens */ public function tokenCleanup(): int { return $this->db->query("DELETE FROM arsse_tokens where expires < CURRENT_TIMESTAMP")->changes(); diff --git a/lib/REST.php b/lib/REST.php index 011d27df..4f1f4bdd 100644 --- a/lib/REST.php +++ b/lib/REST.php @@ -42,9 +42,19 @@ class REST { ], 'miniflux' => [ // Miniflux https://miniflux.app/docs/api.html 'match' => '/v1/', - 'strip' => '/v1', + 'strip' => '', 'class' => REST\Miniflux\V1::class, ], + 'miniflux-version' => [ // Miniflux version report + 'match' => '/version', + 'strip' => '', + 'class' => REST\Miniflux\Status::class, + ], + 'miniflux-healthcheck' => [ // Miniflux health check + 'match' => '/healthcheck', + 'strip' => '', + 'class' => REST\Miniflux\Status::class, + ], // Other candidates: // Microsub https://indieweb.org/Microsub // Google Reader http://feedhq.readthedocs.io/en/latest/api/index.html diff --git a/lib/REST/Miniflux/ErrorResponse.php b/lib/REST/Miniflux/ErrorResponse.php new file mode 100644 index 00000000..1cf467ee --- /dev/null +++ b/lib/REST/Miniflux/ErrorResponse.php @@ -0,0 +1,19 @@ + Arsse::$lang->msg("API.Miniflux.Error.".$msg, $data)]; + parent::__construct($data, $status, $headers, $encodingOptions); + } +} diff --git a/lib/REST/Miniflux/Status.php b/lib/REST/Miniflux/Status.php new file mode 100644 index 00000000..367a7a65 --- /dev/null +++ b/lib/REST/Miniflux/Status.php @@ -0,0 +1,37 @@ +getRequestTarget())['path'] ?? ""; + if (!in_array($target, ["/version", "/healthcheck"])) { + return new EmptyResponse(404); + } + $method = $req->getMethod(); + if ($method === "OPTIONS") { + return new EmptyResponse(204, ['Allow' => "HEAD, GET"]); + } elseif ($method !== "GET") { + return new EmptyResponse(405, ['Allow' => "HEAD, GET"]); + } + $out = ""; + if ($target === "/version") { + $out = V1::VERSION; + } elseif ($target === "/healthcheck") { + $out = "OK"; + } + return new TextResponse($out); + } +} diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 74873afd..45d61914 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -13,6 +13,7 @@ use JKingWeb\Arsse\Misc\HTTP; use JKingWeb\Arsse\REST\Exception; use JKingWeb\Arsse\REST\Exception404; use JKingWeb\Arsse\REST\Exception405; +use JKingWeb\Arsse\User\ExceptionConflict as UserException; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\EmptyResponse; @@ -20,6 +21,8 @@ use Laminas\Diactoros\Response\EmptyResponse; class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected const ACCEPTED_TYPES_OPML = ["text/xml", "application/xml", "text/x-opml"]; protected const ACCEPTED_TYPES_JSON = ["application/json", "text/json"]; + public const VERSION = "2.0.25"; + protected $paths = [ '/categories' => ['GET' => "getCategories", 'POST' => "createCategory"], '/categories/1' => ['PUT' => "updateCategory", 'DELETE' => "deleteCategory"], @@ -35,25 +38,41 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { '/feeds/1/icon' => ['GET' => "getFeedIcon"], '/feeds/1/refresh' => ['PUT' => "refreshFeed"], '/feeds/refresh' => ['PUT' => "refreshAllFeeds"], - '/healthcheck' => ['GET' => "healthCheck"], '/import' => ['POST' => "opmlImport"], '/me' => ['GET' => "getCurrentUser"], '/users' => ['GET' => "getUsers", 'POST' => "createUser"], '/users/1' => ['GET' => "getUser", 'PUT' => "updateUser", 'DELETE' => "deleteUser"], '/users/*' => ['GET' => "getUser"], - '/version' => ['GET' => "getVersion"], ]; public function __construct() { } - public function dispatch(ServerRequestInterface $req): ResponseInterface { - // try to authenticate + protected function authenticate(ServerRequestInterface $req): bool { + // first check any tokens; this is what Miniflux does + foreach ($req->getHeader("X-Auth-Token") as $t) { + if (strlen($t)) { + // a non-empty header is authoritative, so we'll stop here one way or the other + try { + $d = Arsse::$db->tokenLookup("miniflux.login", $t); + } catch (ExceptionInput $e) { + return false; + } + Arsse::$user->id = $d->user; + return true; + } + } + // next check HTTP auth if ($req->getAttribute("authenticated", false)) { Arsse::$user->id = $req->getAttribute("authenticatedUser"); - } else { - // TODO: Handle X-Auth-Token authentication - return new EmptyResponse(401); + } + return false; + } + + public function dispatch(ServerRequestInterface $req): ResponseInterface { + // try to authenticate + if (!$this->authenticate($req)) { + return new ErrorResponse("401", 401); } // get the request path only; this is assumed to already be normalized $target = parse_url($req->getRequestTarget())['path'] ?? ""; @@ -65,17 +84,14 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $func = $this->chooseCall($target, $method); if ($func === "opmlImport") { if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) { - return new EmptyResponse(415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]); + return new ErrorResponse(415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]); } $data = (string) $req->getBody(); } elseif ($method === "POST" || $method === "PUT") { - if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_JSON])) { - return new EmptyResponse(415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_JSON)]); - } $data = @json_decode($data, true); if (json_last_error() !== \JSON_ERROR_NONE) { // if the body could not be parsed as JSON, return "400 Bad Request" - return new EmptyResponse(400); + return new ErrorResponse(["invalidBodyJSON", json_last_error_msg()], 400); } } else { $data = null; @@ -154,4 +170,20 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { throw new Exception404(); } } + + public static function tokenGenerate(string $user, string $label): string { + $t = base64_encode(random_bytes(24)); + return Arsse::$db->tokenCreate($user, "miniflux.login", $t, null, $label); + } + + public static function tokenList(string $user): array { + if (!Arsse::$db->userExists($user)) { + throw new UserException("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + } + $out = []; + foreach (Arsse::$db->tokenList($user, "miniflux.login") as $r) { + $out[] = ['label' => $r['data'], 'id' => $r['id']]; + } + return $out; + } } diff --git a/lib/User.php b/lib/User.php index 0a70e5d2..e8359bc0 100644 --- a/lib/User.php +++ b/lib/User.php @@ -69,7 +69,6 @@ class User { } return $out; } - public function remove(string $user): bool { try { @@ -143,7 +142,7 @@ class User { if (array_key_exists("tz", $data)) { if (!is_string($data['tz'])) { throw new User\ExceptionInput("invalidTimezone", ['field' => "tz", 'value' => ""]); - } elseif(!@timezone_open($data['tz'])) { + } elseif (!@timezone_open($data['tz'])) { throw new User\ExceptionInput("invalidTimezone", ['field' => "tz", 'value' => $data['tz']]); } $in['tz'] = $data['tz']; diff --git a/locale/en.php b/locale/en.php index 66a03eee..c0dea555 100644 --- a/locale/en.php +++ b/locale/en.php @@ -7,6 +7,9 @@ return [ 'CLI.Auth.Success' => 'Authentication successful', 'CLI.Auth.Failure' => 'Authentication failed', + 'API.Miniflux.Error.401' => 'Access Unauthorized', + 'API.Miniflux.Error.invalidBodyJSON' => 'Invalid JSON payload: {0}', + 'API.TTRSS.Category.Uncategorized' => 'Uncategorized', 'API.TTRSS.Category.Special' => 'Special', 'API.TTRSS.Category.Labels' => 'Labels', diff --git a/tests/cases/Database/SeriesToken.php b/tests/cases/Database/SeriesToken.php index 3f766aa3..7a14ed0d 100644 --- a/tests/cases/Database/SeriesToken.php +++ b/tests/cases/Database/SeriesToken.php @@ -33,12 +33,16 @@ trait SeriesToken { 'class' => "str", 'user' => "str", 'expires' => "datetime", + 'data' => "str", ], 'rows' => [ - ["80fa94c1a11f11e78667001e673b2560", "fever.login", "jane.doe@example.com", $faroff], - ["27c6de8da13311e78667001e673b2560", "fever.login", "jane.doe@example.com", $past], // expired - ["ab3b3eb8a13311e78667001e673b2560", "class.class", "jane.doe@example.com", null], - ["da772f8fa13c11e78667001e673b2560", "class.class", "john.doe@example.com", $future], + ["80fa94c1a11f11e78667001e673b2560", "fever.login", "jane.doe@example.com", $faroff, null], + ["27c6de8da13311e78667001e673b2560", "fever.login", "jane.doe@example.com", $past, null], // expired + ["ab3b3eb8a13311e78667001e673b2560", "class.class", "jane.doe@example.com", null, null], + ["da772f8fa13c11e78667001e673b2560", "class.class", "john.doe@example.com", $future, null], + ["A", "miniflux.login", "jane.doe@example.com", null, "Label 1"], + ["B", "miniflux.login", "jane.doe@example.com", null, "Label 2"], + ["C", "miniflux.login", "john.doe@example.com", null, "Label 1"], ], ], ]; @@ -127,4 +131,13 @@ trait SeriesToken { // revoking tokens which do not exist is not an error $this->assertFalse(Arsse::$db->tokenRevoke($user, "unknown.class")); } + + public function testListTokens(): void { + $user = "jane.doe@example.com"; + $exp = [ + ['id' => "A", 'data' => "Label 1"], + ['id' => "B", 'data' => "Label 2"], + ]; + $this->assertResult($exp, Arsse::$db->tokenList($user, "miniflux.login")); + } } diff --git a/tests/cases/REST/Miniflux/TestErrorResponse.php b/tests/cases/REST/Miniflux/TestErrorResponse.php new file mode 100644 index 00000000..23d6e286 --- /dev/null +++ b/tests/cases/REST/Miniflux/TestErrorResponse.php @@ -0,0 +1,22 @@ +assertSame('{"error_message":"Access Unauthorized"}', (string) $act->getBody()); + } + + public function testCreateVariableResponse(): void { + $act = new ErrorResponse(["invalidBodyJSON", "Doh!"], 401); + $this->assertSame('{"error_message":"Invalid JSON payload: Doh!"}', (string) $act->getBody()); + } +} diff --git a/tests/cases/REST/Miniflux/TestStatus.php b/tests/cases/REST/Miniflux/TestStatus.php new file mode 100644 index 00000000..bcf81d18 --- /dev/null +++ b/tests/cases/REST/Miniflux/TestStatus.php @@ -0,0 +1,34 @@ +dispatch($this->serverRequest($method, $url, "")); + $this->assertMessage($exp, $act); + } + + public function provideRequests(): iterable { + return [ + ["/version", "GET", new TextResponse(V1::VERSION)], + ["/version", "POST", new EmptyResponse(405, ['Allow' => "HEAD, GET"])], + ["/version", "OPTIONS", new EmptyResponse(204, ['Allow' => "HEAD, GET"])], + ["/healthcheck", "GET", new TextResponse("OK")], + ["/healthcheck", "POST", new EmptyResponse(405, ['Allow' => "HEAD, GET"])], + ["/healthcheck", "OPTIONS", new EmptyResponse(204, ['Allow' => "HEAD, GET"])], + ["/version/", "GET", new EmptyResponse(404)], + ]; + } +} diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php new file mode 100644 index 00000000..7d20a0a8 --- /dev/null +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -0,0 +1,118 @@ + */ +class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { + protected $h; + protected $transaction; + + protected function req(string $method, string $target, $data = "", array $headers = [], bool $authenticated = true, bool $body = true): ResponseInterface { + $prefix = "/v1"; + $url = $prefix.$target; + if ($body) { + $params = []; + } else { + $params = $data; + $data = []; + } + $req = $this->serverRequest($method, $url, $prefix, $headers, [], $data, "application/json", $params, $authenticated ? "john.doe@example.com" : ""); + return $this->h->dispatch($req); + } + + public function setUp(): void { + self::clearData(); + self::setConf(); + // create a mock user manager + Arsse::$user = \Phake::mock(User::class); + Arsse::$user->id = "john.doe@example.com"; + // create a mock database interface + Arsse::$db = \Phake::mock(Database::class); + $this->transaction = \Phake::mock(Transaction::class); + \Phake::when(Arsse::$db)->begin->thenReturn($this->transaction); + //initialize a handler + $this->h = new V1(); + } + + public function tearDown(): void { + self::clearData(); + } + + protected function v($value) { + return $value; + } + + public function testSendAuthenticationChallenge(): void { + $exp = new EmptyResponse(401); + $this->assertMessage($exp, $this->req("GET", "/", "", [], false)); + } + + /** @dataProvider provideInvalidPaths */ + public function testRespondToInvalidPaths($path, $method, $code, $allow = null): void { + $exp = new EmptyResponse($code, $allow ? ['Allow' => $allow] : []); + $this->assertMessage($exp, $this->req($method, $path)); + } + + public function provideInvalidPaths(): array { + return [ + ["/", "GET", 404], + ["/", "POST", 404], + ["/", "PUT", 404], + ["/", "DELETE", 404], + ["/", "OPTIONS", 404], + ["/version/invalid", "GET", 404], + ["/version/invalid", "POST", 404], + ["/version/invalid", "PUT", 404], + ["/version/invalid", "DELETE", 404], + ["/version/invalid", "OPTIONS", 404], + ["/folders/1/invalid", "GET", 404], + ["/folders/1/invalid", "POST", 404], + ["/folders/1/invalid", "PUT", 404], + ["/folders/1/invalid", "DELETE", 404], + ["/folders/1/invalid", "OPTIONS", 404], + ["/version", "POST", 405, "GET"], + ["/version", "PUT", 405, "GET"], + ["/version", "DELETE", 405, "GET"], + ["/folders", "PUT", 405, "GET, POST"], + ["/folders", "DELETE", 405, "GET, POST"], + ["/folders/1", "GET", 405, "PUT, DELETE"], + ["/folders/1", "POST", 405, "PUT, DELETE"], + ]; + } + + public function testRespondToInvalidInputTypes(): void { + $exp = new EmptyResponse(415, ['Accept' => "application/json"]); + $this->assertMessage($exp, $this->req("PUT", "/folders/1", '', ['Content-Type' => "application/xml"])); + $exp = new EmptyResponse(400); + $this->assertMessage($exp, $this->req("PUT", "/folders/1", '')); + $this->assertMessage($exp, $this->req("PUT", "/folders/1", '', ['Content-Type' => null])); + } + + /** @dataProvider provideOptionsRequests */ + public function testRespondToOptionsRequests(string $url, string $allow, string $accept): void { + $exp = new EmptyResponse(204, [ + 'Allow' => $allow, + 'Accept' => $accept, + ]); + $this->assertMessage($exp, $this->req("OPTIONS", $url)); + } + + public function provideOptionsRequests(): array { + return [ + ["/feeds", "HEAD,GET,POST", "application/json"], + ["/feeds/2112", "DELETE", "application/json"], + ["/user", "HEAD,GET", "application/json"], + ]; + } +} diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 310a0a33..8863d5fc 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -9,7 +9,6 @@ namespace JKingWeb\Arsse\TestCase\User; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\User; -use JKingWeb\Arsse\AbstractException as Exception; use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\ExceptionInput; use JKingWeb\Arsse\User\Driver; @@ -25,7 +24,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { // create a mock user driver $this->drv = \Phake::mock(Driver::class); } - + public function tearDown(): void { \Phake::verifyNoOtherInteractions($this->drv); \Phake::verifyNoOtherInteractions(Arsse::$db); @@ -43,7 +42,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $u->id = null; $this->assertSame("", (string) $u); } - + public function testGeneratePasswords(): void { $u = new User($this->drv); $pass1 = $u->generatePassword(); @@ -166,7 +165,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify($this->drv)->userAdd($user, $pass); \Phake::verify(Arsse::$db)->userExists($user); } - + public function testRemoveAUser(): void { $user = "john.doe@example.com"; $pass = "secret"; @@ -178,7 +177,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->userRemove($user); \Phake::verify($this->drv)->userRemove($user); } - + public function testRemoveAUserWeDoNotKnow(): void { $user = "john.doe@example.com"; $pass = "secret"; @@ -189,7 +188,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->userExists($user); \Phake::verify($this->drv)->userRemove($user); } - + public function testRemoveAMissingUser(): void { $user = "john.doe@example.com"; $pass = "secret"; @@ -205,7 +204,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify($this->drv)->userRemove($user); } } - + public function testRemoveAMissingUserWeDoNotKnow(): void { $user = "john.doe@example.com"; $pass = "secret"; @@ -381,7 +380,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify($this->drv)->userPropertiesGet($user); } } - + /** @dataProvider providePropertyChanges */ public function testSetThePropertiesOfAUser(array $in, $out): void { $user = "john.doe@example.com"; @@ -399,7 +398,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->userExists($user); } } - + /** @dataProvider providePropertyChanges */ public function testSetThePropertiesOfAUserWeDoNotKnow(array $in, $out): void { $user = "john.doe@example.com"; @@ -418,7 +417,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->userAdd($user, null); } } - + public function providePropertyChanges(): iterable { return [ [['admin' => true], ['admin' => true]], @@ -431,7 +430,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { [['lang' => null], ['lang' => null]], ]; } - + public function testSetThePropertiesOfAMissingUser(): void { $user = "john.doe@example.com"; $in = ['admin' => true]; diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml index 0c6f8a76..a46fe77d 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -112,10 +112,14 @@ cases/REST/TestREST.php + + cases/REST/Miniflux/TestErrorResponse.php + cases/REST/Miniflux/TestStatus.php + cases/REST/NextcloudNews/TestVersions.php cases/REST/NextcloudNews/TestV1_2.php - cases/REST/NextcloudNews/PDO/TestV1_2.php + cases/REST/NextcloudNews/PDO/TestV1_2.php cases/REST/TinyTinyRSS/TestSearch.php From 90117b5cd72d42141e83601a65136eda4f75ffbd Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 26 Nov 2020 08:42:35 -0500 Subject: [PATCH 048/366] Fix Miniflux strip value --- lib/REST.php | 2 +- tests/cases/REST/Miniflux/TestV1.php | 23 ++--------------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/lib/REST.php b/lib/REST.php index 4f1f4bdd..eea62746 100644 --- a/lib/REST.php +++ b/lib/REST.php @@ -42,7 +42,7 @@ class REST { ], 'miniflux' => [ // Miniflux https://miniflux.app/docs/api.html 'match' => '/v1/', - 'strip' => '', + 'strip' => '/v1', 'class' => REST\Miniflux\V1::class, ], 'miniflux-version' => [ // Miniflux version report diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 7d20a0a8..db8c34d9 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -53,7 +53,8 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { return $value; } - public function testSendAuthenticationChallenge(): void { + /** @dataProvider provideAuthResponses */ + public function testAuthenticateAUser(): void { $exp = new EmptyResponse(401); $this->assertMessage($exp, $this->req("GET", "/", "", [], false)); } @@ -67,27 +68,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function provideInvalidPaths(): array { return [ ["/", "GET", 404], - ["/", "POST", 404], - ["/", "PUT", 404], - ["/", "DELETE", 404], - ["/", "OPTIONS", 404], - ["/version/invalid", "GET", 404], - ["/version/invalid", "POST", 404], - ["/version/invalid", "PUT", 404], - ["/version/invalid", "DELETE", 404], - ["/version/invalid", "OPTIONS", 404], - ["/folders/1/invalid", "GET", 404], - ["/folders/1/invalid", "POST", 404], - ["/folders/1/invalid", "PUT", 404], - ["/folders/1/invalid", "DELETE", 404], - ["/folders/1/invalid", "OPTIONS", 404], ["/version", "POST", 405, "GET"], - ["/version", "PUT", 405, "GET"], - ["/version", "DELETE", 405, "GET"], - ["/folders", "PUT", 405, "GET, POST"], - ["/folders", "DELETE", 405, "GET, POST"], - ["/folders/1", "GET", 405, "PUT, DELETE"], - ["/folders/1", "POST", 405, "PUT, DELETE"], ]; } From 8c059773bb1fe64f4a2461c7d3eceb5f34c487ee Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 30 Nov 2020 10:51:39 -0500 Subject: [PATCH 049/366] Update tooling --- composer.lock | 93 ++++++++- vendor-bin/csfixer/composer.lock | 279 ++++++++++++++++----------- vendor-bin/daux/composer.lock | 321 ++++++++++++++++++++++--------- vendor-bin/phpunit/composer.lock | 217 +++++++++++++++++---- vendor-bin/robo/composer.lock | 205 +++++++++++++------- 5 files changed, 807 insertions(+), 308 deletions(-) diff --git a/composer.lock b/composer.lock index 77d43df5..f69d4dbb 100644 --- a/composer.lock +++ b/composer.lock @@ -50,6 +50,10 @@ "cli", "docs" ], + "support": { + "issues": "https://github.com/docopt/docopt.php/issues", + "source": "https://github.com/docopt/docopt.php/tree/1.0.4" + }, "time": "2019-12-03T02:48:46+00:00" }, { @@ -117,6 +121,10 @@ "rest", "web service" ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/6.5" + }, "time": "2020-06-16T21:01:06+00:00" }, { @@ -168,6 +176,10 @@ "keywords": [ "promise" ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.4.0" + }, "time": "2020-09-30T07:37:28+00:00" }, { @@ -239,6 +251,10 @@ "uri", "url" ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.7.0" + }, "time": "2020-09-30T07:37:11+00:00" }, { @@ -279,6 +295,10 @@ } ], "description": "Password generator for generating policy-compliant passwords.", + "support": { + "issues": "https://github.com/hosteurope/password-generator/issues", + "source": "https://github.com/hosteurope/password-generator/tree/master" + }, "time": "2016-12-08T09:32:12+00:00" }, { @@ -324,6 +344,10 @@ "keywords": [ "uuid" ], + "support": { + "issues": "https://github.com/JKingweb/DrUUID/issues", + "source": "https://github.com/JKingweb/DrUUID/tree/3.0.0" + }, "time": "2017-02-09T14:17:01+00:00" }, { @@ -399,6 +423,10 @@ "rfc7234", "validation" ], + "support": { + "issues": "https://github.com/Kevinrob/guzzle-cache-middleware/issues", + "source": "https://github.com/Kevinrob/guzzle-cache-middleware/tree/master" + }, "time": "2017-08-17T12:23:43+00:00" }, { @@ -484,6 +512,14 @@ "psr-17", "psr-7" ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-diactoros/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-diactoros/issues", + "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", + "source": "https://github.com/laminas/laminas-diactoros" + }, "funding": [ { "url": "https://funding.communitybridge.org/projects/laminas-project", @@ -549,6 +585,14 @@ "psr-15", "psr-7" ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-httphandlerrunner/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-httphandlerrunner/issues", + "rss": "https://github.com/laminas/laminas-httphandlerrunner/releases.atom", + "source": "https://github.com/laminas/laminas-httphandlerrunner" + }, "funding": [ { "url": "https://funding.communitybridge.org/projects/laminas-project", @@ -605,6 +649,14 @@ "security", "xml" ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-xml/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-xml/issues", + "rss": "https://github.com/laminas/laminas-xml/releases.atom", + "source": "https://github.com/laminas/laminas-xml" + }, "time": "2019-12-31T18:05:42+00:00" }, { @@ -653,6 +705,12 @@ "laminas", "zf" ], + "support": { + "forum": "https://discourse.laminas.dev/", + "issues": "https://github.com/laminas/laminas-zendframework-bridge/issues", + "rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom", + "source": "https://github.com/laminas/laminas-zendframework-bridge" + }, "funding": [ { "url": "https://funding.communitybridge.org/projects/laminas-project", @@ -721,6 +779,9 @@ ], "description": "RSS/Atom parsing library", "homepage": "https://github.com/nicolus/picoFeed", + "support": { + "source": "https://github.com/nicolus/picoFeed/tree/0.1.43" + }, "time": "2020-09-15T07:28:23+00:00" }, { @@ -773,6 +834,9 @@ "request", "response" ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, "time": "2019-04-30T12:38:16+00:00" }, { @@ -823,6 +887,9 @@ "request", "response" ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, "time": "2016-08-06T14:39:51+00:00" }, { @@ -876,6 +943,10 @@ "response", "server" ], + "support": { + "issues": "https://github.com/php-fig/http-server-handler/issues", + "source": "https://github.com/php-fig/http-server-handler/tree/master" + }, "time": "2018-10-30T16:46:14+00:00" }, { @@ -923,6 +994,9 @@ "psr", "psr-3" ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.3" + }, "time": "2020-03-23T09:12:05+00:00" }, { @@ -963,6 +1037,10 @@ } ], "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, "time": "2019-03-08T08:55:37+00:00" }, { @@ -1033,6 +1111,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1114,6 +1195,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1187,6 +1271,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1249,6 +1336,10 @@ "isolation", "tool" ], + "support": { + "issues": "https://github.com/bamarni/composer-bin-plugin/issues", + "source": "https://github.com/bamarni/composer-bin-plugin/tree/master" + }, "time": "2020-05-03T08:27:20+00:00" } ], @@ -1268,5 +1359,5 @@ "platform-overrides": { "php": "7.1.33" }, - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.0.0" } diff --git a/vendor-bin/csfixer/composer.lock b/vendor-bin/csfixer/composer.lock index dc5ddc50..f3fbaff0 100644 --- a/vendor-bin/csfixer/composer.lock +++ b/vendor-bin/csfixer/composer.lock @@ -9,28 +9,29 @@ "packages-dev": [ { "name": "composer/semver", - "version": "1.7.1", + "version": "3.2.4", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "38276325bd896f90dfcfe30029aa5db40df387a7" + "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/38276325bd896f90dfcfe30029aa5db40df387a7", - "reference": "38276325bd896f90dfcfe30029aa5db40df387a7", + "url": "https://api.github.com/repos/composer/semver/zipball/a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", + "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.5 || ^5.0.5" + "phpstan/phpstan": "^0.12.54", + "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { @@ -66,6 +67,11 @@ "validation", "versioning" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.2.4" + }, "funding": [ { "url": "https://packagist.com", @@ -80,20 +86,20 @@ "type": "tidelift" } ], - "time": "2020-09-27T13:13:07+00:00" + "time": "2020-11-13T08:59:24+00:00" }, { "name": "composer/xdebug-handler", - "version": "1.4.4", + "version": "1.4.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6e076a124f7ee146f2487554a94b6a19a74887ba" + "reference": "f28d44c286812c714741478d968104c5e604a1d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6e076a124f7ee146f2487554a94b6a19a74887ba", - "reference": "6e076a124f7ee146f2487554a94b6a19a74887ba", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f28d44c286812c714741478d968104c5e604a1d4", + "reference": "f28d44c286812c714741478d968104c5e604a1d4", "shasum": "" }, "require": { @@ -124,6 +130,11 @@ "Xdebug", "performance" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/1.4.5" + }, "funding": [ { "url": "https://packagist.com", @@ -138,20 +149,20 @@ "type": "tidelift" } ], - "time": "2020-10-24T12:39:10+00:00" + "time": "2020-11-13T08:04:11+00:00" }, { "name": "doctrine/annotations", - "version": "1.11.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "88fb6fb1dae011de24dd6b632811c1ff5c2928f5" + "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/88fb6fb1dae011de24dd6b632811c1ff5c2928f5", - "reference": "88fb6fb1dae011de24dd6b632811c1ff5c2928f5", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/ce77a7ba1770462cd705a91a151b6c3746f9c6ad", + "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad", "shasum": "" }, "require": { @@ -209,7 +220,11 @@ "docblock", "parser" ], - "time": "2020-10-17T22:05:33+00:00" + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.11.1" + }, + "time": "2020-10-26T10:28:16+00:00" }, { "name": "doctrine/lexer", @@ -271,6 +286,10 @@ "parser", "php" ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.1" + }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -289,27 +308,27 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.16.4", + "version": "v2.16.7", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13" + "reference": "4e35806a6d7d8510d6842ae932e8832363d22c87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/1023c3458137ab052f6ff1e09621a721bfdeca13", - "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/4e35806a6d7d8510d6842ae932e8832363d22c87", + "reference": "4e35806a6d7d8510d6842ae932e8832363d22c87", "shasum": "" }, "require": { - "composer/semver": "^1.4", + "composer/semver": "^1.4 || ^2.0 || ^3.0", "composer/xdebug-handler": "^1.2", "doctrine/annotations": "^1.2", "ext-json": "*", "ext-tokenizer": "*", - "php": "^5.6 || ^7.0", + "php": "^7.1", "php-cs-fixer/diff": "^1.3", - "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0", + "symfony/console": "^3.4.43 || ^4.1.6 || ^5.0", "symfony/event-dispatcher": "^3.0 || ^4.0 || ^5.0", "symfony/filesystem": "^3.0 || ^4.0 || ^5.0", "symfony/finder": "^3.0 || ^4.0 || ^5.0", @@ -322,14 +341,14 @@ "require-dev": { "johnkary/phpunit-speedtrap": "^1.1 || ^2.0 || ^3.0", "justinrainbow/json-schema": "^5.0", - "keradus/cli-executor": "^1.2", + "keradus/cli-executor": "^1.4", "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^2.1", + "php-coveralls/php-coveralls": "^2.4.1", "php-cs-fixer/accessible-object": "^1.0", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.1", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.1", - "phpunitgoodpractices/traits": "^1.8", + "phpunitgoodpractices/traits": "^1.9.1", "symfony/phpunit-bridge": "^5.1", "symfony/yaml": "^3.0 || ^4.0 || ^5.0" }, @@ -376,13 +395,17 @@ } ], "description": "A tool to automatically fix PHP code style", + "support": { + "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.16.7" + }, "funding": [ { "url": "https://github.com/keradus", "type": "github" } ], - "time": "2020-06-27T23:57:46+00:00" + "time": "2020-10-27T22:44:27+00:00" }, { "name": "php-cs-fixer/diff", @@ -433,6 +456,10 @@ "keywords": [ "diff" ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/diff/issues", + "source": "https://github.com/PHP-CS-Fixer/diff/tree/v1.3.1" + }, "time": "2020-10-14T08:39:05+00:00" }, { @@ -482,6 +509,10 @@ "container-interop", "psr" ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/master" + }, "time": "2017-02-14T16:28:37+00:00" }, { @@ -528,6 +559,10 @@ "psr", "psr-14" ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, "time": "2019-01-08T18:20:26+00:00" }, { @@ -575,20 +610,23 @@ "psr", "psr-3" ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.3" + }, "time": "2020-03-23T09:12:05+00:00" }, { "name": "symfony/console", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "ae789a8a2ad189ce7e8216942cdb9b77319f5eb8" + "reference": "e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/ae789a8a2ad189ce7e8216942cdb9b77319f5eb8", - "reference": "ae789a8a2ad189ce7e8216942cdb9b77319f5eb8", + "url": "https://api.github.com/repos/symfony/console/zipball/e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e", + "reference": "e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e", "shasum": "" }, "require": { @@ -625,11 +663,6 @@ "symfony/process": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Console\\": "" @@ -654,6 +687,9 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/console/tree/v5.1.8" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -668,7 +704,7 @@ "type": "tidelift" } ], - "time": "2020-10-07T15:23:00+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/deprecation-contracts", @@ -718,6 +754,9 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/master" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -736,16 +775,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "d5de97d6af175a9e8131c546db054ca32842dd0f" + "reference": "26f4edae48c913fc183a3da0553fe63bdfbd361a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d5de97d6af175a9e8131c546db054ca32842dd0f", - "reference": "d5de97d6af175a9e8131c546db054ca32842dd0f", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/26f4edae48c913fc183a3da0553fe63bdfbd361a", + "reference": "26f4edae48c913fc183a3da0553fe63bdfbd361a", "shasum": "" }, "require": { @@ -776,11 +815,6 @@ "symfony/http-kernel": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" @@ -805,6 +839,9 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v5.1.8" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -819,7 +856,7 @@ "type": "tidelift" } ], - "time": "2020-09-18T14:27:32+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -881,6 +918,9 @@ "interoperability", "standards" ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.2.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -899,16 +939,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "1a8697545a8d87b9f2f6b1d32414199cc5e20aae" + "reference": "df08650ea7aee2d925380069c131a66124d79177" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/1a8697545a8d87b9f2f6b1d32414199cc5e20aae", - "reference": "1a8697545a8d87b9f2f6b1d32414199cc5e20aae", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/df08650ea7aee2d925380069c131a66124d79177", + "reference": "df08650ea7aee2d925380069c131a66124d79177", "shasum": "" }, "require": { @@ -916,11 +956,6 @@ "symfony/polyfill-ctype": "~1.8" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Filesystem\\": "" @@ -945,6 +980,9 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.1.8" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -959,31 +997,26 @@ "type": "tidelift" } ], - "time": "2020-09-27T14:02:37+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/finder", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8" + "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8", - "reference": "2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8", + "url": "https://api.github.com/repos/symfony/finder/zipball/e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", + "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", "shasum": "" }, "require": { "php": ">=7.2.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Finder\\": "" @@ -1008,6 +1041,9 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v5.1.8" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1022,20 +1058,20 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:23:27+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "4c7e155bf7d93ea4ba3824d5a14476694a5278dd" + "reference": "c6a02905e4ffc7a1498e8ee019db2b477cd1cc02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/4c7e155bf7d93ea4ba3824d5a14476694a5278dd", - "reference": "4c7e155bf7d93ea4ba3824d5a14476694a5278dd", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/c6a02905e4ffc7a1498e8ee019db2b477cd1cc02", + "reference": "c6a02905e4ffc7a1498e8ee019db2b477cd1cc02", "shasum": "" }, "require": { @@ -1044,11 +1080,6 @@ "symfony/polyfill-php80": "^1.15" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\OptionsResolver\\": "" @@ -1078,6 +1109,9 @@ "configuration", "options" ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v5.1.8" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1092,7 +1126,7 @@ "type": "tidelift" } ], - "time": "2020-09-27T03:44:28+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1154,6 +1188,9 @@ "polyfill", "portable" ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1232,6 +1269,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1313,6 +1353,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1390,6 +1433,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1455,6 +1501,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php70/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1528,6 +1577,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1604,6 +1656,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1684,6 +1739,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1702,16 +1760,16 @@ }, { "name": "symfony/process", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d3a2e64866169586502f0cd9cab69135ad12cee9" + "reference": "f00872c3f6804150d6a0f73b4151daab96248101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d3a2e64866169586502f0cd9cab69135ad12cee9", - "reference": "d3a2e64866169586502f0cd9cab69135ad12cee9", + "url": "https://api.github.com/repos/symfony/process/zipball/f00872c3f6804150d6a0f73b4151daab96248101", + "reference": "f00872c3f6804150d6a0f73b4151daab96248101", "shasum": "" }, "require": { @@ -1719,11 +1777,6 @@ "symfony/polyfill-php80": "^1.15" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" @@ -1748,6 +1801,9 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v5.1.8" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1762,7 +1818,7 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:23:27+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/service-contracts", @@ -1824,6 +1880,9 @@ "interoperability", "standards" ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/master" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1842,16 +1901,16 @@ }, { "name": "symfony/stopwatch", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323" + "reference": "3d9f57c89011f0266e6b1d469e5c0110513859d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/0f7c58cf81dbb5dd67d423a89d577524a2ec0323", - "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/3d9f57c89011f0266e6b1d469e5c0110513859d5", + "reference": "3d9f57c89011f0266e6b1d469e5c0110513859d5", "shasum": "" }, "require": { @@ -1859,11 +1918,6 @@ "symfony/service-contracts": "^1.0|^2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Stopwatch\\": "" @@ -1888,6 +1942,9 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v5.1.8" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1902,20 +1959,20 @@ "type": "tidelift" } ], - "time": "2020-05-20T17:43:50+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/string", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "4a9afe9d07bac506f75bcee8ed3ce76da5a9343e" + "reference": "a97573e960303db71be0dd8fda9be3bca5e0feea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/4a9afe9d07bac506f75bcee8ed3ce76da5a9343e", - "reference": "4a9afe9d07bac506f75bcee8ed3ce76da5a9343e", + "url": "https://api.github.com/repos/symfony/string/zipball/a97573e960303db71be0dd8fda9be3bca5e0feea", + "reference": "a97573e960303db71be0dd8fda9be3bca5e0feea", "shasum": "" }, "require": { @@ -1933,11 +1990,6 @@ "symfony/var-exporter": "^4.4|^5.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\String\\": "" @@ -1973,6 +2025,9 @@ "utf-8", "utf8" ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.1.8" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1987,7 +2042,7 @@ "type": "tidelift" } ], - "time": "2020-09-15T12:23:47+00:00" + "time": "2020-10-24T12:01:57+00:00" } ], "aliases": [], @@ -1997,5 +2052,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.0.0" } diff --git a/vendor-bin/daux/composer.lock b/vendor-bin/daux/composer.lock index 14ef3c4f..8e08bd22 100644 --- a/vendor-bin/daux/composer.lock +++ b/vendor-bin/daux/composer.lock @@ -76,6 +76,10 @@ "markdown", "md" ], + "support": { + "issues": "https://github.com/dauxio/daux.io/issues", + "source": "https://github.com/dauxio/daux.io/tree/master" + }, "time": "2019-09-23T20:10:07+00:00" }, { @@ -143,6 +147,10 @@ "rest", "web service" ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/6.5" + }, "time": "2020-06-16T21:01:06+00:00" }, { @@ -194,6 +202,10 @@ "keywords": [ "promise" ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.4.0" + }, "time": "2020-09-30T07:37:28+00:00" }, { @@ -265,6 +277,10 @@ "uri", "url" ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.7.0" + }, "time": "2020-09-30T07:37:11+00:00" }, { @@ -334,6 +350,12 @@ "markdown", "parser" ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, "time": "2019-03-28T13:52:31+00:00" }, { @@ -389,20 +411,24 @@ "templating", "views" ], + "support": { + "issues": "https://github.com/thephpleague/plates/issues", + "source": "https://github.com/thephpleague/plates/tree/master" + }, "time": "2016-12-28T00:14:17+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.10.1", + "version": "1.10.2", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", "shasum": "" }, "require": { @@ -437,13 +463,17 @@ "object", "object graph" ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", "type": "tidelift" } ], - "time": "2020-06-29T13:22:24+00:00" + "time": "2020-11-13T09:40:50+00:00" }, { "name": "psr/container", @@ -492,6 +522,10 @@ "container-interop", "psr" ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/master" + }, "time": "2017-02-14T16:28:37+00:00" }, { @@ -542,6 +576,9 @@ "request", "response" ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, "time": "2016-08-06T14:39:51+00:00" }, { @@ -582,20 +619,24 @@ } ], "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, "time": "2019-03-08T08:55:37+00:00" }, { "name": "scrivo/highlight.php", - "version": "v9.18.1.3", + "version": "v9.18.1.5", "source": { "type": "git", "url": "https://github.com/scrivo/highlight.php.git", - "reference": "6a1699707b099081f20a488ac1f92d682181018c" + "reference": "fa75a865928a4a5d49e5e77faca6bd2f2410baaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/6a1699707b099081f20a488ac1f92d682181018c", - "reference": "6a1699707b099081f20a488ac1f92d682181018c", + "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/fa75a865928a4a5d49e5e77faca6bd2f2410baaf", + "reference": "fa75a865928a4a5d49e5e77faca6bd2f2410baaf", "shasum": "" }, "require": { @@ -651,26 +692,30 @@ "highlight.php", "syntax" ], + "support": { + "issues": "https://github.com/scrivo/highlight.php/issues", + "source": "https://github.com/scrivo/highlight.php" + }, "funding": [ { "url": "https://github.com/allejo", "type": "github" } ], - "time": "2020-10-16T07:43:22+00:00" + "time": "2020-11-22T06:07:40+00:00" }, { "name": "symfony/console", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "90933b39c7b312fc3ceaa1ddeac7eb48cb953124" + "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/90933b39c7b312fc3ceaa1ddeac7eb48cb953124", - "reference": "90933b39c7b312fc3ceaa1ddeac7eb48cb953124", + "url": "https://api.github.com/repos/symfony/console/zipball/20f73dd143a5815d475e0838ff867bce1eebd9d5", + "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5", "shasum": "" }, "require": { @@ -705,11 +750,6 @@ "symfony/process": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Console\\": "" @@ -734,6 +774,9 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/console/tree/v4.4.16" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -748,20 +791,87 @@ "type": "tidelift" } ], - "time": "2020-09-15T07:58:55+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { - "name": "symfony/http-foundation", - "version": "v4.4.15", + "name": "symfony/deprecation-contracts", + "version": "v2.2.0", "source": { "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "10683b407c3b6087c64619ebc97a87e36ea62c92" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/10683b407c3b6087c64619ebc97a87e36ea62c92", - "reference": "10683b407c3b6087c64619ebc97a87e36ea62c92", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/master" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-09-07T11:33:47+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v4.4.16", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "827a00811ef699e809a201ceafac0b2b246bf38a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/827a00811ef699e809a201ceafac0b2b246bf38a", + "reference": "827a00811ef699e809a201ceafac0b2b246bf38a", "shasum": "" }, "require": { @@ -774,11 +884,6 @@ "symfony/expression-language": "^3.4|^4.0|^5.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\HttpFoundation\\": "" @@ -803,6 +908,9 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v4.4.16" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -817,20 +925,20 @@ "type": "tidelift" } ], - "time": "2020-09-27T14:14:06+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/intl", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "9381fd69ce6407041185aa6f1bafbf7d65f0e66a" + "reference": "e353c6c37afa1ff90739b3941f60ff9fa650eec3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/9381fd69ce6407041185aa6f1bafbf7d65f0e66a", - "reference": "9381fd69ce6407041185aa6f1bafbf7d65f0e66a", + "url": "https://api.github.com/repos/symfony/intl/zipball/e353c6c37afa1ff90739b3941f60ff9fa650eec3", + "reference": "e353c6c37afa1ff90739b3941f60ff9fa650eec3", "shasum": "" }, "require": { @@ -845,11 +953,6 @@ "ext-intl": "to use the component with locales other than \"en\"" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Intl\\": "" @@ -893,6 +996,9 @@ "l10n", "localization" ], + "support": { + "source": "https://github.com/symfony/intl/tree/v5.1.8" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -907,20 +1013,20 @@ "type": "tidelift" } ], - "time": "2020-09-27T03:44:28+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/mime", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "4404d6545125863561721514ad9388db2661eec5" + "reference": "f5485a92c24d4bcfc2f3fc648744fb398482ff1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/4404d6545125863561721514ad9388db2661eec5", - "reference": "4404d6545125863561721514ad9388db2661eec5", + "url": "https://api.github.com/repos/symfony/mime/zipball/f5485a92c24d4bcfc2f3fc648744fb398482ff1b", + "reference": "f5485a92c24d4bcfc2f3fc648744fb398482ff1b", "shasum": "" }, "require": { @@ -937,11 +1043,6 @@ "symfony/dependency-injection": "^4.4|^5.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Mime\\": "" @@ -970,6 +1071,9 @@ "mime", "mime-type" ], + "support": { + "source": "https://github.com/symfony/mime/tree/v5.1.8" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -984,7 +1088,7 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:23:27+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1046,6 +1150,9 @@ "polyfill", "portable" ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1122,6 +1229,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1206,6 +1316,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1287,6 +1400,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1364,6 +1480,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1437,6 +1556,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1513,6 +1635,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1593,6 +1718,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1611,27 +1739,22 @@ }, { "name": "symfony/process", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "9b887acc522935f77555ae8813495958c7771ba7" + "reference": "2f4b049fb80ca5e9874615a2a85dc2a502090f05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/9b887acc522935f77555ae8813495958c7771ba7", - "reference": "9b887acc522935f77555ae8813495958c7771ba7", + "url": "https://api.github.com/repos/symfony/process/zipball/2f4b049fb80ca5e9874615a2a85dc2a502090f05", + "reference": "2f4b049fb80ca5e9874615a2a85dc2a502090f05", "shasum": "" }, "require": { "php": ">=7.1.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" @@ -1656,6 +1779,9 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v4.4.16" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1670,7 +1796,7 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:08:58+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/service-contracts", @@ -1732,6 +1858,9 @@ "interoperability", "standards" ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/master" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1750,37 +1879,36 @@ }, { "name": "symfony/yaml", - "version": "v4.4.15", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "c7885964b1eceb70b0981556d0a9b01d2d97c8d1" + "reference": "f284e032c3cefefb9943792132251b79a6127ca6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c7885964b1eceb70b0981556d0a9b01d2d97c8d1", - "reference": "c7885964b1eceb70b0981556d0a9b01d2d97c8d1", + "url": "https://api.github.com/repos/symfony/yaml/zipball/f284e032c3cefefb9943792132251b79a6127ca6", + "reference": "f284e032c3cefefb9943792132251b79a6127ca6", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/polyfill-ctype": "~1.8" }, "conflict": { - "symfony/console": "<3.4" + "symfony/console": "<4.4" }, "require-dev": { - "symfony/console": "^3.4|^4.0|^5.0" + "symfony/console": "^4.4|^5.0" }, "suggest": { "symfony/console": "For validating YAML files using the lint command" }, + "bin": [ + "Resources/bin/yaml-lint" + ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Yaml\\": "" @@ -1805,6 +1933,9 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v5.1.8" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1819,7 +1950,7 @@ "type": "tidelift" } ], - "time": "2020-09-27T03:36:23+00:00" + "time": "2020-10-24T12:03:25+00:00" }, { "name": "webuni/commonmark-table-extension", @@ -1877,36 +2008,38 @@ "markdown", "table" ], + "support": { + "issues": "https://github.com/webuni/commonmark-table-extension/issues", + "source": "https://github.com/webuni/commonmark-table-extension/tree/0.9.0" + }, "abandoned": "league/commonmark", "time": "2018-11-28T11:29:11+00:00" }, { "name": "webuni/front-matter", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/webuni/front-matter.git", - "reference": "c7d1c51f9864ff015365ce515374e63bcd3b558e" + "reference": "dd2f623ac169b52e4eb261f3aaf4973cd2b0c368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webuni/front-matter/zipball/c7d1c51f9864ff015365ce515374e63bcd3b558e", - "reference": "c7d1c51f9864ff015365ce515374e63bcd3b558e", + "url": "https://api.github.com/repos/webuni/front-matter/zipball/dd2f623ac169b52e4eb261f3aaf4973cd2b0c368", + "reference": "dd2f623ac169b52e4eb261f3aaf4973cd2b0c368", "shasum": "" }, "require": { - "php": "^5.6|^7.0", - "symfony/yaml": "^2.3|^3.0|^4.0" + "php": "^7.2 || ^8.0", + "symfony/yaml": "^3.0 || ^4.0 || ^5.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.9", + "ext-json": "*", + "league/commonmark": "^1.4", "mthaml/mthaml": "^1.3", - "nette/neon": "^2.2", - "phpunit/phpunit": "^5.7|^6.0|^7.0", - "symfony/var-dumper": "^3.0|^4.0", - "twig/twig": "^1.27|^2.0", - "vimeo/psalm": "^1.0", - "yosymfony/toml": "~0.3|^1.0" + "nette/neon": "^2.2 || ^3.0", + "twig/twig": "^3.0", + "yosymfony/toml": "^1.0" }, "suggest": { "nette/neon": "If you want to use NEON as front matter", @@ -1930,7 +2063,8 @@ "authors": [ { "name": "Martin Hasoň", - "email": "martin.hason@gmail.com" + "email": "martin.hason@gmail.com", + "homepage": "https://www.martinhason.cz" }, { "name": "Webuni s.r.o.", @@ -1940,13 +2074,18 @@ "description": "Front matter parser and dumper for PHP", "homepage": "https://github.com/webuni/front-matter", "keywords": [ + "commonmark", "front-matter", "json", "neon", "toml", "yaml" ], - "time": "2018-03-20T13:36:33+00:00" + "support": { + "issues": "https://github.com/webuni/front-matter/issues", + "source": "https://github.com/webuni/front-matter/tree/1.2.0" + }, + "time": "2020-11-04T21:04:28+00:00" } ], "aliases": [], @@ -1956,5 +2095,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.0.0" } diff --git a/vendor-bin/phpunit/composer.lock b/vendor-bin/phpunit/composer.lock index 938fc8f3..e963b995 100644 --- a/vendor-bin/phpunit/composer.lock +++ b/vendor-bin/phpunit/composer.lock @@ -55,6 +55,10 @@ "parse", "split" ], + "support": { + "issues": "https://github.com/clue/php-arguments/issues", + "source": "https://github.com/clue/php-arguments/tree/v2.0.0" + }, "time": "2016-12-18T14:37:39+00:00" }, { @@ -96,40 +100,39 @@ } ], "description": "This package provides Array Subset and related asserts once depracated in PHPunit 8", + "support": { + "issues": "https://github.com/rdohms/phpunit-arraysubset-asserts/issues", + "source": "https://github.com/rdohms/phpunit-arraysubset-asserts/tree/compat/phpunit8" + }, "time": "2020-02-18T21:20:04+00:00" }, { "name": "doctrine/instantiator", - "version": "1.3.1", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", - "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", + "doctrine/coding-standard": "^8.0", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.13", - "phpstan/phpstan-phpunit": "^0.11", - "phpstan/phpstan-shim": "^0.11", - "phpunit/phpunit": "^7.0" + "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" @@ -143,7 +146,7 @@ { "name": "Marco Pivetta", "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" + "homepage": "https://ocramius.github.io/" } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", @@ -152,6 +155,10 @@ "constructor", "instantiate" ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -166,7 +173,7 @@ "type": "tidelift" } ], - "time": "2020-05-29T17:27:14+00:00" + "time": "2020-11-10T18:47:58+00:00" }, { "name": "mikey179/vfsstream", @@ -212,20 +219,25 @@ ], "description": "Virtual file system to mock the real file system in unit tests.", "homepage": "http://vfs.bovigo.org/", + "support": { + "issues": "https://github.com/bovigo/vfsStream/issues", + "source": "https://github.com/bovigo/vfsStream/tree/master", + "wiki": "https://github.com/bovigo/vfsStream/wiki" + }, "time": "2019-10-30T15:31:00+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.10.1", + "version": "1.10.2", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", "shasum": "" }, "require": { @@ -260,13 +272,17 @@ "object", "object graph" ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", "type": "tidelift" } ], - "time": "2020-06-29T13:22:24+00:00" + "time": "2020-11-13T09:40:50+00:00" }, { "name": "phake/phake", @@ -324,6 +340,10 @@ "mock", "testing" ], + "support": { + "issues": "https://github.com/mlively/Phake/issues", + "source": "https://github.com/mlively/Phake/tree/v3.1.8" + }, "time": "2020-05-11T18:43:26+00:00" }, { @@ -379,6 +399,10 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/master" + }, "time": "2018-07-08T19:23:20+00:00" }, { @@ -426,6 +450,10 @@ } ], "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/master" + }, "time": "2018-07-08T19:19:57+00:00" }, { @@ -475,6 +503,10 @@ "reflection", "static analysis" ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, "time": "2020-06-27T09:03:43+00:00" }, { @@ -527,6 +559,10 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + }, "time": "2020-09-03T19:13:55+00:00" }, { @@ -572,6 +608,10 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + }, "time": "2020-09-17T18:55:26+00:00" }, { @@ -635,20 +675,24 @@ "spy", "stub" ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/1.12.1" + }, "time": "2020-09-29T09:10:42+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "7.0.10", + "version": "7.0.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf" + "reference": "52f55786aa2e52c26cd9e2db20aff2981e0f7399" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf", - "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/52f55786aa2e52c26cd9e2db20aff2981e0f7399", + "reference": "52f55786aa2e52c26cd9e2db20aff2981e0f7399", "shasum": "" }, "require": { @@ -698,7 +742,17 @@ "testing", "xunit" ], - "time": "2019-11-20T13:55:58+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-27T06:08:35+00:00" }, { "name": "phpunit/php-file-iterator", @@ -748,6 +802,10 @@ "filesystem", "iterator" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.2" + }, "time": "2018-09-13T20:33:42+00:00" }, { @@ -789,6 +847,10 @@ "keywords": [ "template" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + }, "time": "2015-06-21T13:50:34+00:00" }, { @@ -838,6 +900,10 @@ "keywords": [ "timer" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/master" + }, "time": "2019-06-07T04:22:29+00:00" }, { @@ -887,44 +953,48 @@ "keywords": [ "tokenizer" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.1" + }, "abandoned": true, "time": "2019-09-17T06:23:10+00:00" }, { "name": "phpunit/phpunit", - "version": "8.5.8", + "version": "8.5.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997" + "reference": "3123601e3b29339b20129acc3f989cfec3274566" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/34c18baa6a44f1d1fbf0338907139e9dce95b997", - "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3123601e3b29339b20129acc3f989cfec3274566", + "reference": "3123601e3b29339b20129acc3f989cfec3274566", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.2.0", + "doctrine/instantiator": "^1.3.1", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.9.1", + "myclabs/deep-copy": "^1.10.0", "phar-io/manifest": "^1.0.3", "phar-io/version": "^2.0.1", "php": "^7.2", - "phpspec/prophecy": "^1.8.1", - "phpunit/php-code-coverage": "^7.0.7", + "phpspec/prophecy": "^1.10.3", + "phpunit/php-code-coverage": "^7.0.12", "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", "phpunit/php-timer": "^2.1.2", "sebastian/comparator": "^3.0.2", "sebastian/diff": "^3.0.2", - "sebastian/environment": "^4.2.2", - "sebastian/exporter": "^3.1.1", + "sebastian/environment": "^4.2.3", + "sebastian/exporter": "^3.1.2", "sebastian/global-state": "^3.0.0", "sebastian/object-enumerator": "^3.0.3", "sebastian/resource-operations": "^2.0.1", @@ -971,6 +1041,10 @@ "testing", "xunit" ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.11" + }, "funding": [ { "url": "https://phpunit.de/donate.html", @@ -981,7 +1055,7 @@ "type": "github" } ], - "time": "2020-06-22T07:06:58+00:00" + "time": "2020-11-27T12:46:45+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -1026,6 +1100,10 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/master" + }, "time": "2017-03-04T06:30:41+00:00" }, { @@ -1090,6 +1168,10 @@ "compare", "equality" ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/master" + }, "time": "2018-07-12T15:12:46+00:00" }, { @@ -1146,6 +1228,10 @@ "unidiff", "unified diff" ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/master" + }, "time": "2019-02-04T06:01:07+00:00" }, { @@ -1199,6 +1285,10 @@ "environment", "hhvm" ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/4.2.3" + }, "time": "2019-11-20T08:46:58+00:00" }, { @@ -1266,6 +1356,10 @@ "export", "exporter" ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/master" + }, "time": "2019-09-14T09:02:43+00:00" }, { @@ -1320,6 +1414,10 @@ "keywords": [ "global state" ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/master" + }, "time": "2019-02-01T05:30:01+00:00" }, { @@ -1367,6 +1465,10 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/master" + }, "time": "2017-08-03T12:35:26+00:00" }, { @@ -1412,6 +1514,10 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/master" + }, "time": "2017-03-29T09:07:27+00:00" }, { @@ -1465,6 +1571,10 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/master" + }, "time": "2017-03-03T06:23:57+00:00" }, { @@ -1507,6 +1617,10 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/master" + }, "time": "2018-10-04T04:07:39+00:00" }, { @@ -1553,6 +1667,10 @@ ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/master" + }, "time": "2019-07-02T08:10:15+00:00" }, { @@ -1596,6 +1714,10 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/master" + }, "time": "2016-10-03T07:35:21+00:00" }, { @@ -1658,6 +1780,9 @@ "polyfill", "portable" ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1712,6 +1837,10 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/master" + }, "funding": [ { "url": "https://github.com/theseer", @@ -1767,6 +1896,10 @@ "check", "validate" ], + "support": { + "issues": "https://github.com/webmozart/assert/issues", + "source": "https://github.com/webmozart/assert/tree/master" + }, "time": "2020-07-08T17:02:28+00:00" }, { @@ -1814,6 +1947,10 @@ } ], "description": "A PHP implementation of Ant's glob.", + "support": { + "issues": "https://github.com/webmozart/glob/issues", + "source": "https://github.com/webmozart/glob/tree/master" + }, "time": "2015-12-29T11:14:33+00:00" }, { @@ -1860,6 +1997,10 @@ } ], "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.", + "support": { + "issues": "https://github.com/webmozart/path-util/issues", + "source": "https://github.com/webmozart/path-util/tree/2.3.0" + }, "time": "2015-12-17T08:42:14+00:00" } ], @@ -1870,5 +2011,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.0.0" } diff --git a/vendor-bin/robo/composer.lock b/vendor-bin/robo/composer.lock index 5fbe6804..c558c227 100644 --- a/vendor-bin/robo/composer.lock +++ b/vendor-bin/robo/composer.lock @@ -69,6 +69,10 @@ } ], "description": "Initialize Symfony Console commands from annotated command class methods.", + "support": { + "issues": "https://github.com/consolidation/annotated-command/issues", + "source": "https://github.com/consolidation/annotated-command/tree/4.2.3" + }, "time": "2020-10-03T14:28:42+00:00" }, { @@ -150,6 +154,10 @@ } ], "description": "Provide configuration services for a commandline tool.", + "support": { + "issues": "https://github.com/consolidation/config/issues", + "source": "https://github.com/consolidation/config/tree/master" + }, "time": "2019-03-03T19:37:04+00:00" }, { @@ -211,6 +219,10 @@ } ], "description": "Improved Psr-3 / Psr\\Log logger based on Symfony Console components.", + "support": { + "issues": "https://github.com/consolidation/log/issues", + "source": "https://github.com/consolidation/log/tree/2.0.1" + }, "time": "2020-05-27T17:06:13+00:00" }, { @@ -278,6 +290,10 @@ } ], "description": "Format text by applying transformations provided by plug-in formatters.", + "support": { + "issues": "https://github.com/consolidation/output-formatters/issues", + "source": "https://github.com/consolidation/output-formatters/tree/4.1.1" + }, "time": "2020-05-27T20:51:17+00:00" }, { @@ -393,6 +409,10 @@ } ], "description": "Modern task runner", + "support": { + "issues": "https://github.com/consolidation/Robo/issues", + "source": "https://github.com/consolidation/Robo/tree/1.4.13" + }, "time": "2020-10-11T04:51:34+00:00" }, { @@ -443,6 +463,10 @@ } ], "description": "Provides a self:update command for Symfony Console applications.", + "support": { + "issues": "https://github.com/consolidation/self-update/issues", + "source": "https://github.com/consolidation/self-update/tree/1.2.0" + }, "time": "2020-04-13T02:49:20+00:00" }, { @@ -474,6 +498,10 @@ ], "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", "homepage": "https://github.com/container-interop/container-interop", + "support": { + "issues": "https://github.com/container-interop/container-interop/issues", + "source": "https://github.com/container-interop/container-interop/tree/master" + }, "abandoned": "psr/container", "time": "2017-02-14T19:40:03+00:00" }, @@ -534,6 +562,10 @@ "dot", "notation" ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/master" + }, "time": "2017-01-20T21:14:22+00:00" }, { @@ -581,6 +613,10 @@ } ], "description": "Expands internal property references in PHP arrays file.", + "support": { + "issues": "https://github.com/grasmash/expander/issues", + "source": "https://github.com/grasmash/expander/tree/master" + }, "time": "2017-12-21T22:14:55+00:00" }, { @@ -629,6 +665,10 @@ } ], "description": "Expands internal property references in a yaml file.", + "support": { + "issues": "https://github.com/grasmash/yaml-expander/issues", + "source": "https://github.com/grasmash/yaml-expander/tree/master" + }, "time": "2017-12-16T16:06:03+00:00" }, { @@ -694,20 +734,24 @@ "provider", "service" ], + "support": { + "issues": "https://github.com/thephpleague/container/issues", + "source": "https://github.com/thephpleague/container/tree/2.x" + }, "time": "2017-05-10T09:20:27+00:00" }, { "name": "pear/archive_tar", - "version": "1.4.10", + "version": "1.4.11", "source": { "type": "git", "url": "https://github.com/pear/Archive_Tar.git", - "reference": "bbb4f10f71a1da2715ec6d9a683f4f23c507a49b" + "reference": "17d355cb7d3c4ff08e5729f29cd7660145208d9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pear/Archive_Tar/zipball/bbb4f10f71a1da2715ec6d9a683f4f23c507a49b", - "reference": "bbb4f10f71a1da2715ec6d9a683f4f23c507a49b", + "url": "https://api.github.com/repos/pear/Archive_Tar/zipball/17d355cb7d3c4ff08e5729f29cd7660145208d9d", + "reference": "17d355cb7d3c4ff08e5729f29cd7660145208d9d", "shasum": "" }, "require": { @@ -760,7 +804,11 @@ "archive", "tar" ], - "time": "2020-09-15T14:13:23+00:00" + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Archive_Tar", + "source": "https://github.com/pear/Archive_Tar" + }, + "time": "2020-11-19T22:10:24+00:00" }, { "name": "pear/console_getopt", @@ -807,6 +855,10 @@ } ], "description": "More info available on: http://pear.php.net/package/Console_Getopt", + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Console_Getopt", + "source": "https://github.com/pear/Console_Getopt" + }, "time": "2019-11-20T18:27:48+00:00" }, { @@ -851,6 +903,10 @@ } ], "description": "Minimal set of PEAR core files to be used as composer dependency", + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR", + "source": "https://github.com/pear/pear-core-minimal" + }, "time": "2019-11-19T19:00:24+00:00" }, { @@ -906,6 +962,10 @@ "keywords": [ "exception" ], + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", + "source": "https://github.com/pear/PEAR_Exception" + }, "time": "2019-12-10T10:24:42+00:00" }, { @@ -955,6 +1015,10 @@ "container-interop", "psr" ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/master" + }, "time": "2017-02-14T16:28:37+00:00" }, { @@ -1002,20 +1066,23 @@ "psr", "psr-3" ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.3" + }, "time": "2020-03-23T09:12:05+00:00" }, { "name": "symfony/console", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "90933b39c7b312fc3ceaa1ddeac7eb48cb953124" + "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/90933b39c7b312fc3ceaa1ddeac7eb48cb953124", - "reference": "90933b39c7b312fc3ceaa1ddeac7eb48cb953124", + "url": "https://api.github.com/repos/symfony/console/zipball/20f73dd143a5815d475e0838ff867bce1eebd9d5", + "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5", "shasum": "" }, "require": { @@ -1050,11 +1117,6 @@ "symfony/process": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Console\\": "" @@ -1079,6 +1141,9 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/console/tree/v4.4.16" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1093,20 +1158,20 @@ "type": "tidelift" } ], - "time": "2020-09-15T07:58:55+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "e17bb5e0663dc725f7cdcafc932132735b4725cd" + "reference": "4204f13d2d0b7ad09454f221bb2195fccdf1fe98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e17bb5e0663dc725f7cdcafc932132735b4725cd", - "reference": "e17bb5e0663dc725f7cdcafc932132735b4725cd", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4204f13d2d0b7ad09454f221bb2195fccdf1fe98", + "reference": "4204f13d2d0b7ad09454f221bb2195fccdf1fe98", "shasum": "" }, "require": { @@ -1135,11 +1200,6 @@ "symfony/http-kernel": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" @@ -1164,6 +1224,9 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.16" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1178,7 +1241,7 @@ "type": "tidelift" } ], - "time": "2020-09-18T14:07:46+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -1240,6 +1303,9 @@ "interoperability", "standards" ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v1.1.9" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1258,16 +1324,16 @@ }, { "name": "symfony/filesystem", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "ebc51494739d3b081ea543ed7c462fa73a4f74db" + "reference": "e74b873395b7213d44d1397bd4a605cd1632a68a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/ebc51494739d3b081ea543ed7c462fa73a4f74db", - "reference": "ebc51494739d3b081ea543ed7c462fa73a4f74db", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/e74b873395b7213d44d1397bd4a605cd1632a68a", + "reference": "e74b873395b7213d44d1397bd4a605cd1632a68a", "shasum": "" }, "require": { @@ -1275,11 +1341,6 @@ "symfony/polyfill-ctype": "~1.8" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Filesystem\\": "" @@ -1304,6 +1365,9 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v4.4.16" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1318,31 +1382,26 @@ "type": "tidelift" } ], - "time": "2020-09-27T13:54:16+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/finder", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8" + "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8", - "reference": "2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8", + "url": "https://api.github.com/repos/symfony/finder/zipball/e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", + "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", "shasum": "" }, "require": { "php": ">=7.2.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Finder\\": "" @@ -1367,6 +1426,9 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v5.1.8" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1381,7 +1443,7 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:23:27+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1443,6 +1505,9 @@ "polyfill", "portable" ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1520,6 +1585,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1596,6 +1664,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1676,6 +1747,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.20.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1694,27 +1768,22 @@ }, { "name": "symfony/process", - "version": "v3.4.45", + "version": "v3.4.47", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "46a862d0f334e51c1ed831b49cbe12863ffd5475" + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/46a862d0f334e51c1ed831b49cbe12863ffd5475", - "reference": "46a862d0f334e51c1ed831b49cbe12863ffd5475", + "url": "https://api.github.com/repos/symfony/process/zipball/b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca", "shasum": "" }, "require": { "php": "^5.5.9|>=7.0.8" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" @@ -1739,6 +1808,9 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v3.4.47" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1753,7 +1825,7 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:06:40+00:00" + "time": "2020-10-24T10:57:07+00:00" }, { "name": "symfony/service-contracts", @@ -1815,6 +1887,9 @@ "interoperability", "standards" ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/master" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1833,16 +1908,16 @@ }, { "name": "symfony/yaml", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "c7885964b1eceb70b0981556d0a9b01d2d97c8d1" + "reference": "543cb4dbd45ed803f08a9a65f27fb149b5dd20c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c7885964b1eceb70b0981556d0a9b01d2d97c8d1", - "reference": "c7885964b1eceb70b0981556d0a9b01d2d97c8d1", + "url": "https://api.github.com/repos/symfony/yaml/zipball/543cb4dbd45ed803f08a9a65f27fb149b5dd20c2", + "reference": "543cb4dbd45ed803f08a9a65f27fb149b5dd20c2", "shasum": "" }, "require": { @@ -1859,11 +1934,6 @@ "symfony/console": "For validating YAML files using the lint command" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Yaml\\": "" @@ -1888,6 +1958,9 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v4.4.16" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1902,7 +1975,7 @@ "type": "tidelift" } ], - "time": "2020-09-27T03:36:23+00:00" + "time": "2020-10-24T11:50:19+00:00" } ], "aliases": [], @@ -1912,5 +1985,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.0.0" } From def07bb1ad2a16178e3915ba3132d30321dc3bd3 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 30 Nov 2020 10:52:32 -0500 Subject: [PATCH 050/366] Tests for Miniflux authentication This appears to match Miniflux's behaviour --- lib/REST/Miniflux/V1.php | 21 ++++++----- tests/cases/REST/Miniflux/TestV1.php | 52 ++++++++++++++++++++++++---- tests/phpunit.dist.xml | 1 + 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 45d61914..3860c20b 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -7,12 +7,14 @@ declare(strict_types=1); namespace JKingWeb\Arsse\REST\Miniflux; use JKingWeb\Arsse\Arsse; -use JKingWeb\Arsse\Misc\ValueInfo; use JKingWeb\Arsse\AbstractException; +use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Misc\HTTP; +use JKingWeb\Arsse\Misc\ValueInfo; use JKingWeb\Arsse\REST\Exception; use JKingWeb\Arsse\REST\Exception404; use JKingWeb\Arsse\REST\Exception405; +use JKingWeb\Arsse\REST\Exception501; use JKingWeb\Arsse\User\ExceptionConflict as UserException; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; @@ -21,6 +23,7 @@ use Laminas\Diactoros\Response\EmptyResponse; class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected const ACCEPTED_TYPES_OPML = ["text/xml", "application/xml", "text/x-opml"]; protected const ACCEPTED_TYPES_JSON = ["application/json", "text/json"]; + protected const TOKEN_LENGTH = 32; public const VERSION = "2.0.25"; protected $paths = [ @@ -50,21 +53,22 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function authenticate(ServerRequestInterface $req): bool { // first check any tokens; this is what Miniflux does - foreach ($req->getHeader("X-Auth-Token") as $t) { - if (strlen($t)) { - // a non-empty header is authoritative, so we'll stop here one way or the other + if ($req->hasHeader("X-Auth-Token")) { + $t = $req->getHeader("X-Auth-Token")[0]; // consider only the first token + if (strlen($t)) { // and only if it is not blank try { $d = Arsse::$db->tokenLookup("miniflux.login", $t); } catch (ExceptionInput $e) { return false; } - Arsse::$user->id = $d->user; + Arsse::$user->id = $d['user']; return true; } } // next check HTTP auth if ($req->getAttribute("authenticated", false)) { Arsse::$user->id = $req->getAttribute("authenticatedUser"); + return true; } return false; } @@ -84,11 +88,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $func = $this->chooseCall($target, $method); if ($func === "opmlImport") { if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) { - return new ErrorResponse(415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]); + return new ErrorResponse("", 415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]); } $data = (string) $req->getBody(); } elseif ($method === "POST" || $method === "PUT") { - $data = @json_decode($data, true); + $data = @json_decode((string) $req->getBody(), true); if (json_last_error() !== \JSON_ERROR_NONE) { // if the body could not be parsed as JSON, return "400 Bad Request" return new ErrorResponse(["invalidBodyJSON", json_last_error_msg()], 400); @@ -172,7 +176,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } public static function tokenGenerate(string $user, string $label): string { - $t = base64_encode(random_bytes(24)); + // Miniflux produces tokens in base64url alphabet + $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); return Arsse::$db->tokenCreate($user, "miniflux.login", $t, null, $label); } diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index db8c34d9..de35c276 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -10,13 +10,19 @@ use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\User; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Db\Transaction; +use JKingWeb\Arsse\Db\ExceptionInput; +use JKingWeb\Arsse\REST\Exception404; use JKingWeb\Arsse\REST\Miniflux\V1; +use JKingWeb\Arsse\REST\Miniflux\ErrorResponse; use Psr\Http\Message\ResponseInterface; +use Laminas\Diactoros\Response\JsonResponse as Response; +use Laminas\Diactoros\Response\EmptyResponse; /** @covers \JKingWeb\Arsse\REST\Miniflux\V1 */ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { protected $h; protected $transaction; + protected $token = "Tk2o9YubmZIL2fm2w8Z4KlDEQJz532fNSOcTG0s2_xc="; protected function req(string $method, string $target, $data = "", array $headers = [], bool $authenticated = true, bool $body = true): ResponseInterface { $prefix = "/v1"; @@ -54,13 +60,47 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideAuthResponses */ - public function testAuthenticateAUser(): void { - $exp = new EmptyResponse(401); - $this->assertMessage($exp, $this->req("GET", "/", "", [], false)); + public function testAuthenticateAUser($token, bool $auth, bool $success): void { + $exp = new ErrorResponse("401", 401); + $user = "john.doe@example.com"; + if ($token !== null) { + $headers = ['X-Auth-Token' => $token]; + } else { + $headers = []; + } + Arsse::$user->id = null; + \Phake::when(Arsse::$db)->tokenLookup->thenThrow(new ExceptionInput("subjectMissing")); + \Phake::when(Arsse::$db)->tokenLookup("miniflux.login", $this->token)->thenReturn(['user' => $user]); + if ($success) { + $this->expectExceptionObject(new Exception404); + try { + $this->req("GET", "/", "", $headers, $auth); + } finally { + $this->assertSame($user, Arsse::$user->id); + } + } else { + $this->assertMessage($exp, $this->req("GET", "/", "", $headers, $auth)); + $this->assertNull(Arsse::$user->id); + } + } + + public function provideAuthResponses(): iterable { + return [ + [null, false, false], + [null, true, true], + [$this->token, false, true], + [[$this->token, "BOGUS"], false, true], + ["", true, true], + [["", "BOGUS"], true, true], + ["NOT A TOKEN", false, false], + ["NOT A TOKEN", true, false], + [["BOGUS", $this->token], false, false], + [["", $this->token], false, false], + ]; } /** @dataProvider provideInvalidPaths */ - public function testRespondToInvalidPaths($path, $method, $code, $allow = null): void { + public function xtestRespondToInvalidPaths($path, $method, $code, $allow = null): void { $exp = new EmptyResponse($code, $allow ? ['Allow' => $allow] : []); $this->assertMessage($exp, $this->req($method, $path)); } @@ -72,7 +112,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ]; } - public function testRespondToInvalidInputTypes(): void { + public function xtestRespondToInvalidInputTypes(): void { $exp = new EmptyResponse(415, ['Accept' => "application/json"]); $this->assertMessage($exp, $this->req("PUT", "/folders/1", '', ['Content-Type' => "application/xml"])); $exp = new EmptyResponse(400); @@ -81,7 +121,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideOptionsRequests */ - public function testRespondToOptionsRequests(string $url, string $allow, string $accept): void { + public function xtestRespondToOptionsRequests(string $url, string $allow, string $accept): void { $exp = new EmptyResponse(204, [ 'Allow' => $allow, 'Accept' => $accept, diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml index a46fe77d..18486652 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -115,6 +115,7 @@ cases/REST/Miniflux/TestErrorResponse.php cases/REST/Miniflux/TestStatus.php + cases/REST/Miniflux/TestV1.php cases/REST/NextcloudNews/TestVersions.php From 7fa5523a7d1743a8563e828970950727e2842c8d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 1 Dec 2020 11:06:29 -0500 Subject: [PATCH 051/366] Simplify handling of invalid paths and methods --- lib/REST/Exception404.php | 10 ---------- lib/REST/Exception405.php | 10 ---------- lib/REST/Miniflux/V1.php | 19 ++++++++----------- lib/REST/NextcloudNews/V1_2.php | 25 +++++++++---------------- tests/cases/REST/Miniflux/TestV1.php | 20 +++++--------------- 5 files changed, 22 insertions(+), 62 deletions(-) delete mode 100644 lib/REST/Exception404.php delete mode 100644 lib/REST/Exception405.php diff --git a/lib/REST/Exception404.php b/lib/REST/Exception404.php deleted file mode 100644 index 8bee1922..00000000 --- a/lib/REST/Exception404.php +++ /dev/null @@ -1,10 +0,0 @@ -handleHTTPOptions($target); } $func = $this->chooseCall($target, $method); + if ($func instanceof ResponseInterface) { + return $func; + } if ($func === "opmlImport") { if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) { return new ErrorResponse("", 415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]); @@ -149,7 +149,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } - protected function chooseCall(string $url, string $method): string { + protected function chooseCall(string $url, string $method) { // // normalize the URL path: change any IDs to 1 for easier comparison $url = $this->normalizePathIds($url); // normalize the HTTP method to uppercase @@ -160,18 +160,15 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { // if the path is supported, make sure the method is allowed if (isset($this->paths[$url][$method])) { // if it is allowed, return the object method to run, assuming the method exists - if (method_exists($this, $this->paths[$url][$method])) { - return $this->paths[$url][$method]; - } else { - throw new Exception501(); // @codeCoverageIgnore - } + assert(method_exists($this, $this->paths[$url][$method]), new \Exception("Method is not implemented")); + return $this->paths[$url][$method]; } else { // otherwise return 405 - throw new Exception405(implode(", ", array_keys($this->paths[$url]))); + return new EmptyResponse(405, ['Allow' => implode(", ", array_keys($this->paths[$url]))]); } } else { // if the path is not supported, return 404 - throw new Exception404(); + return new EmptyResponse(404); } } diff --git a/lib/REST/NextcloudNews/V1_2.php b/lib/REST/NextcloudNews/V1_2.php index 4741e839..7cefe13e 100644 --- a/lib/REST/NextcloudNews/V1_2.php +++ b/lib/REST/NextcloudNews/V1_2.php @@ -15,9 +15,6 @@ use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Feed\Exception as FeedException; use JKingWeb\Arsse\Misc\HTTP; use JKingWeb\Arsse\REST\Exception; -use JKingWeb\Arsse\REST\Exception404; -use JKingWeb\Arsse\REST\Exception405; -use JKingWeb\Arsse\REST\Exception501; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\JsonResponse as Response; @@ -110,15 +107,14 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // merge GET and POST data, and normalize it. POST parameters are preferred over GET parameters $data = $this->normalizeInput(array_merge($req->getQueryParams(), $data), $this->validInput, "unix"); // check to make sure the requested function is implemented + $func = $this->chooseCall($target, $req->getMethod()); + if ($func instanceof ResponseInterface) { + return $func; + } // dispatch try { - $func = $this->chooseCall($target, $req->getMethod()); $path = explode("/", ltrim($target, "/")); return $this->$func($path, $data); - } catch (Exception404 $e) { - return new EmptyResponse(404); - } catch (Exception405 $e) { - return new EmptyResponse(405, ['Allow' => $e->getMessage()]); // @codeCoverageIgnoreStart } catch (Exception $e) { // if there was a REST exception return 400 @@ -141,7 +137,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return implode("/", $path); } - protected function chooseCall(string $url, string $method): string { + protected function chooseCall(string $url, string $method) { // // normalize the URL path: change any IDs to 1 for easier comparison $url = $this->normalizePathIds($url); // normalize the HTTP method to uppercase @@ -152,18 +148,15 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // if the path is supported, make sure the method is allowed if (isset($this->paths[$url][$method])) { // if it is allowed, return the object method to run, assuming the method exists - if (method_exists($this, $this->paths[$url][$method])) { - return $this->paths[$url][$method]; - } else { - throw new Exception501(); // @codeCoverageIgnore - } + assert(method_exists($this, $this->paths[$url][$method]), new \Exception("Method is not implemented")); + return $this->paths[$url][$method]; } else { // otherwise return 405 - throw new Exception405(implode(", ", array_keys($this->paths[$url]))); + return new EmptyResponse(405, ['Allow' => implode(", ", array_keys($this->paths[$url]))]); } } else { // if the path is not supported, return 404 - throw new Exception404(); + return new EmptyResponse(404); } } diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index de35c276..c62f0c03 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -11,7 +11,6 @@ use JKingWeb\Arsse\User; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Db\Transaction; use JKingWeb\Arsse\Db\ExceptionInput; -use JKingWeb\Arsse\REST\Exception404; use JKingWeb\Arsse\REST\Miniflux\V1; use JKingWeb\Arsse\REST\Miniflux\ErrorResponse; use Psr\Http\Message\ResponseInterface; @@ -61,7 +60,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideAuthResponses */ public function testAuthenticateAUser($token, bool $auth, bool $success): void { - $exp = new ErrorResponse("401", 401); + $exp = $success ? new EmptyResponse(404) : new ErrorResponse("401", 401); $user = "john.doe@example.com"; if ($token !== null) { $headers = ['X-Auth-Token' => $token]; @@ -71,17 +70,8 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { Arsse::$user->id = null; \Phake::when(Arsse::$db)->tokenLookup->thenThrow(new ExceptionInput("subjectMissing")); \Phake::when(Arsse::$db)->tokenLookup("miniflux.login", $this->token)->thenReturn(['user' => $user]); - if ($success) { - $this->expectExceptionObject(new Exception404); - try { - $this->req("GET", "/", "", $headers, $auth); - } finally { - $this->assertSame($user, Arsse::$user->id); - } - } else { - $this->assertMessage($exp, $this->req("GET", "/", "", $headers, $auth)); - $this->assertNull(Arsse::$user->id); - } + $this->assertMessage($exp, $this->req("GET", "/", "", $headers, $auth)); + $this->assertSame($success ? $user : null, Arsse::$user->id); } public function provideAuthResponses(): iterable { @@ -100,7 +90,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideInvalidPaths */ - public function xtestRespondToInvalidPaths($path, $method, $code, $allow = null): void { + public function testRespondToInvalidPaths($path, $method, $code, $allow = null): void { $exp = new EmptyResponse($code, $allow ? ['Allow' => $allow] : []); $this->assertMessage($exp, $this->req($method, $path)); } @@ -108,7 +98,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function provideInvalidPaths(): array { return [ ["/", "GET", 404], - ["/version", "POST", 405, "GET"], + ["/me", "POST", 405, "GET"], ]; } From 2a0d6e659996997a6798b96d4f8089eca4d500e1 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 1 Dec 2020 12:08:45 -0500 Subject: [PATCH 052/366] OPTIONS tests --- lib/REST/Miniflux/V1.php | 6 +++--- tests/cases/REST/Miniflux/TestV1.php | 22 +++++++++------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 887d61f7..c2e84dd7 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -18,8 +18,8 @@ use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\EmptyResponse; class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { - protected const ACCEPTED_TYPES_OPML = ["text/xml", "application/xml", "text/x-opml"]; - protected const ACCEPTED_TYPES_JSON = ["application/json", "text/json"]; + protected const ACCEPTED_TYPES_OPML = ["application/xml", "text/xml", "text/x-opml"]; + protected const ACCEPTED_TYPES_JSON = ["application/json"]; protected const TOKEN_LENGTH = 32; public const VERSION = "2.0.25"; @@ -140,7 +140,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { array_unshift($allowed, "HEAD"); } return new EmptyResponse(204, [ - 'Allow' => implode(",", $allowed), + 'Allow' => implode(", ", $allowed), 'Accept' => implode(", ", $url === "/import" ? self::ACCEPTED_TYPES_OPML : self::ACCEPTED_TYPES_JSON), ]); } else { diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index c62f0c03..a66a8900 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\TestCase\REST\NextcloudNews; +namespace JKingWeb\Arsse\TestCase\REST\Miniflux; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\User; @@ -98,20 +98,14 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function provideInvalidPaths(): array { return [ ["/", "GET", 404], + ["/", "OPTIONS", 404], ["/me", "POST", 405, "GET"], + ["/me/", "GET", 404], ]; } - public function xtestRespondToInvalidInputTypes(): void { - $exp = new EmptyResponse(415, ['Accept' => "application/json"]); - $this->assertMessage($exp, $this->req("PUT", "/folders/1", '', ['Content-Type' => "application/xml"])); - $exp = new EmptyResponse(400); - $this->assertMessage($exp, $this->req("PUT", "/folders/1", '')); - $this->assertMessage($exp, $this->req("PUT", "/folders/1", '', ['Content-Type' => null])); - } - /** @dataProvider provideOptionsRequests */ - public function xtestRespondToOptionsRequests(string $url, string $allow, string $accept): void { + public function testRespondToOptionsRequests(string $url, string $allow, string $accept): void { $exp = new EmptyResponse(204, [ 'Allow' => $allow, 'Accept' => $accept, @@ -121,9 +115,11 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function provideOptionsRequests(): array { return [ - ["/feeds", "HEAD,GET,POST", "application/json"], - ["/feeds/2112", "DELETE", "application/json"], - ["/user", "HEAD,GET", "application/json"], + ["/feeds", "HEAD, GET, POST", "application/json"], + ["/feeds/2112", "HEAD, GET, PUT, DELETE", "application/json"], + ["/me", "HEAD, GET", "application/json"], + ["/users/someone", "HEAD, GET", "application/json"], + ["/import", "POST", "application/xml, text/xml, text/x-opml"], ]; } } From 669e17a1f67c044a7c6e475f81c6c7c86058b667 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 1 Dec 2020 17:12:19 -0500 Subject: [PATCH 053/366] Add ability to discover multiple feeds --- lib/Feed.php | 11 +++++++++++ tests/cases/Feed/TestFeed.php | 21 +++++++++++++++++++++ tests/docroot/Feed/Discovery/Missing.php | 3 +++ tests/docroot/Feed/Discovery/Valid.php | 1 + 4 files changed, 36 insertions(+) create mode 100644 tests/docroot/Feed/Discovery/Missing.php diff --git a/lib/Feed.php b/lib/Feed.php index dffbccb9..81256a63 100644 --- a/lib/Feed.php +++ b/lib/Feed.php @@ -47,6 +47,17 @@ class Feed { return $out; } + public static function discoverAll(string $url, string $username = '', string $password = ''): array { + // fetch the candidate feed + $f = self::download($url, "", "", $username, $password); + if ($f->reader->detectFormat($f->getContent())) { + // if the prospective URL is a feed, use it + return [$url]; + } else { + return $f->reader->find($f->getUrl(), $f->getContent()); + } + } + public function __construct(int $feedID = null, string $url, string $lastModified = '', string $etag = '', string $username = '', string $password = '', bool $scrape = false) { // fetch the feed $this->resource = self::download($url, $lastModified, $etag, $username, $password); diff --git a/tests/cases/Feed/TestFeed.php b/tests/cases/Feed/TestFeed.php index a5036d28..f9a422e4 100644 --- a/tests/cases/Feed/TestFeed.php +++ b/tests/cases/Feed/TestFeed.php @@ -150,6 +150,27 @@ class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { Feed::discover($this->base."Discovery/Invalid"); } + public function testDiscoverAMissingFeed(): void { + $this->assertException("invalidUrl", "Feed"); + Feed::discover($this->base."Discovery/Missing"); + } + + public function testDiscoverMultipleFeedsSuccessfully(): void { + $exp1 = [$this->base."Discovery/Feed", $this->base."Discovery/Missing"]; + $exp2 = [$this->base."Discovery/Feed"]; + $this->assertSame($exp1, Feed::discoverAll($this->base."Discovery/Valid")); + $this->assertSame($exp2, Feed::discoverAll($this->base."Discovery/Feed")); + } + + public function testDiscoverMultipleFeedsUnsuccessfully(): void { + $this->assertSame([], Feed::discoverAll($this->base."Discovery/Invalid")); + } + + public function testDiscoverMultipleMissingFeeds(): void { + $this->assertException("invalidUrl", "Feed"); + Feed::discoverAll($this->base."Discovery/Missing"); + } + public function testParseEntityExpansionAttack(): void { $this->assertException("xmlEntity", "Feed"); new Feed(null, $this->base."Parsing/XEEAttack"); diff --git a/tests/docroot/Feed/Discovery/Missing.php b/tests/docroot/Feed/Discovery/Missing.php new file mode 100644 index 00000000..666eb036 --- /dev/null +++ b/tests/docroot/Feed/Discovery/Missing.php @@ -0,0 +1,3 @@ + 404, +]; diff --git a/tests/docroot/Feed/Discovery/Valid.php b/tests/docroot/Feed/Discovery/Valid.php index 9f34f716..af7b9c17 100644 --- a/tests/docroot/Feed/Discovery/Valid.php +++ b/tests/docroot/Feed/Discovery/Valid.php @@ -4,6 +4,7 @@ Example article + MESSAGE_BODY ]; From 94154d43543f4b53414c058a364086ae1c734e69 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 2 Dec 2020 18:00:27 -0500 Subject: [PATCH 054/366] Implement Miniflux feed discovery --- lib/REST/Miniflux/V1.php | 59 +++++++++++++++++++++++++--- lib/REST/NextcloudNews/V1_2.php | 1 - locale/en.php | 5 +++ tests/cases/REST/Miniflux/TestV1.php | 17 ++++++++ 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index c2e84dd7..321716da 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -7,21 +7,31 @@ declare(strict_types=1); namespace JKingWeb\Arsse\REST\Miniflux; use JKingWeb\Arsse\Arsse; +use JKingWeb\Arsse\Feed; +use JKingWeb\Arsse\Feed\Exception as FeedException; use JKingWeb\Arsse\AbstractException; use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Misc\HTTP; -use JKingWeb\Arsse\Misc\ValueInfo; +use JKingWeb\Arsse\Misc\ValueInfo as V; use JKingWeb\Arsse\REST\Exception; use JKingWeb\Arsse\User\ExceptionConflict as UserException; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\EmptyResponse; +use Laminas\Diactoros\Response\JsonResponse as Response; class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { + public const VERSION = "2.0.25"; + protected const ACCEPTED_TYPES_OPML = ["application/xml", "text/xml", "text/x-opml"]; protected const ACCEPTED_TYPES_JSON = ["application/json"]; protected const TOKEN_LENGTH = 32; - public const VERSION = "2.0.25"; + protected const VALID_JSON = [ + 'url' => "string", + 'username' => "string", + 'password' => "string", + 'user_agent' => "string", + ]; protected $paths = [ '/categories' => ['GET' => "getCategories", 'POST' => "createCategory"], @@ -86,6 +96,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if ($func instanceof ResponseInterface) { return $func; } + $data = []; + $query = []; if ($func === "opmlImport") { if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) { return new ErrorResponse("", 415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]); @@ -97,12 +109,16 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { // if the body could not be parsed as JSON, return "400 Bad Request" return new ErrorResponse(["invalidBodyJSON", json_last_error_msg()], 400); } - } else { - $data = null; + $data = $this->normalizeBody((array) $data); + if ($data instanceof ResponseInterface) { + return $data; + } + } elseif ($method === "GET") { + $query = $req->getQueryParams(); } try { $path = explode("/", ltrim($target, "/")); - return $this->$func($path, $req->getQueryParams(), $data); + return $this->$func($path, $query, $data); // @codeCoverageIgnoreStart } catch (Exception $e) { // if there was a REST exception return 400 @@ -118,7 +134,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $path = explode("/", $url); // any path components which are database IDs (integers greater than zero) should be replaced with "1", for easier comparison (we don't care about the specific ID) for ($a = 0; $a < sizeof($path); $a++) { - if (ValueInfo::id($path[$a])) { + if (V::id($path[$a])) { $path[$a] = "1"; } } @@ -172,6 +188,37 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } + protected function normalizeBody(array $body) { + // Miniflux does not attempt to coerce values into different types + foreach (self::VALID_JSON as $k => $t) { + if (!isset($body[$k])) { + $body[$k] = null; + } elseif (gettype($body[$k]) !== $t) { + return new ErrorResponse(["invalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])]); + } + } + return $body; + } + + protected function discoverSubscriptions(array $path, array $query, array $data) { + try { + $list = Feed::discoverAll((string) $data['url'], (string) $data['username'], (string) $data['password']); + } catch (FeedException $e) { + $msg = [ + 10502 => "fetch404", + 10506 => "fetch403", + 10507 => "fetch401", + ][$e->getCode()] ?? "fetchOther"; + return new ErrorResponse($msg, 500); + } + $out = []; + foreach($list as $url) { + // TODO: This needs to be refined once PicoFeed is replaced + $out[] = ['title' => "Feed", 'type' => "rss", 'url' => $url]; + } + return new Response($out); + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokens in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); diff --git a/lib/REST/NextcloudNews/V1_2.php b/lib/REST/NextcloudNews/V1_2.php index 7cefe13e..c73ea8c7 100644 --- a/lib/REST/NextcloudNews/V1_2.php +++ b/lib/REST/NextcloudNews/V1_2.php @@ -22,7 +22,6 @@ use Laminas\Diactoros\Response\EmptyResponse; class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { public const VERSION = "11.0.5"; - protected const REALM = "Nextcloud News API v1-2"; protected const ACCEPTED_TYPE = "application/json"; protected $dateFormat = "unix"; diff --git a/locale/en.php b/locale/en.php index c0dea555..f58d7b49 100644 --- a/locale/en.php +++ b/locale/en.php @@ -9,6 +9,11 @@ return [ 'API.Miniflux.Error.401' => 'Access Unauthorized', 'API.Miniflux.Error.invalidBodyJSON' => 'Invalid JSON payload: {0}', + 'API.Miniflux.Error.invalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}', + 'API.Miniflux.Error.fetch404' => 'Resource not found (404), this feed doesn\'t exists anymore, check the feed URL', + 'API.Miniflux.Error.fetch401' => 'You are not authorized to access this resource (invalid username/password)', + 'API.Miniflux.Error.fetch403' => 'Unable to fetch this resource (Status Code = 403)', + 'API.Miniflux.Error.fetchOther' => 'Unable to fetch this resource', 'API.TTRSS.Category.Uncategorized' => 'Uncategorized', 'API.TTRSS.Category.Special' => 'Special', diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index a66a8900..e94d1450 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -122,4 +122,21 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ["/import", "POST", "application/xml, text/xml, text/x-opml"], ]; } + + public function testRejectBadlyTypedData(): void { + $exp = new ErrorResponse(["invalidInputType", 'field' => "url", 'expected' => "string", 'actual' => "integer"], 400); + $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => 2112])); + } + + public function testDiscoverFeeds(): void { + $exp = new Response([ + ['title' => "Feed", 'type' => "rss", 'url' => "http://localhost:8000/Feed/Discovery/Feed"], + ['title' => "Feed", 'type' => "rss", 'url' => "http://localhost:8000/Feed/Discovery/Missing"], + ]); + $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Valid"])); + $exp = new Response([]); + $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Invalid"])); + $exp = new ErrorResponse("fetch404", 500); + $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Missing"])); + } } From 0f3e0411f0eeafecaf502b0b47e6c87c219b7cc7 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 3 Dec 2020 15:15:23 -0500 Subject: [PATCH 055/366] Document some differences frrom Miniflux --- docs/en/030_Supported_Protocols/005_Miniflux.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index cf706ba5..04e53e2c 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -13,13 +13,22 @@
API Reference
-The Miniflux protocol is a well-designed protocol supporting a wide variety of operations on newsfeeds, folders (termed "categories"), and articles; it also allows for user administration, and native OPML importing and exporting. +The Miniflux protocol is a well-designed protocol supporting a wide variety of operations on newsfeeds, folders (termed "categories"), and articles; it also allows for user administration, and native OPML importing and exporting. Architecturally it is similar to the Nextcloud News protocol, but is generally more efficient. -Architecturally it is similar to the Nextcloud News protocol, but is generally more efficient. +Miniflux version 2.0.25 is emulated, though not all features are implemented + +# Missing features + +- JSON Feed format is not suported +- Various feed-related features are not supported; attempting to use them has no effect + - Rewrite rules and scraper rules + - Custom User-Agent strings + - The `disabled`, `ignore_http_cache`, and `fetch_via_proxy` flags + - Changing the URL, username, or password of a feed # Differences -TBD +- Only the URL should be considered reliable in feed discovery results # Interaction with nested folders From 978929aabdb9d7c1c1af4e33a6dfab570763895c Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 5 Dec 2020 11:01:44 -0500 Subject: [PATCH 056/366] WIP redesign of user properties --- lib/Database.php | 119 +++++++++++++++------------- lib/User.php | 56 +++++++------ lib/User/Driver.php | 2 +- sql/MySQL/6.sql | 11 ++- sql/PostgreSQL/6.sql | 10 ++- sql/SQLite3/6.sql | 16 +++- tests/cases/Database/SeriesUser.php | 24 ++++-- 7 files changed, 142 insertions(+), 96 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 760a0de4..9135b203 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -11,7 +11,7 @@ use JKingWeb\Arsse\Db\Statement; use JKingWeb\Arsse\Misc\Query; use JKingWeb\Arsse\Context\Context; use JKingWeb\Arsse\Misc\Date; -use JKingWeb\Arsse\Misc\ValueInfo; +use JKingWeb\Arsse\Misc\ValueInfo as V; use JKingWeb\Arsse\Misc\URL; /** The high-level interface with the database @@ -149,7 +149,7 @@ class Database { $count = 0; $convType = Db\AbstractStatement::TYPE_NORM_MAP[Statement::TYPES[$type]]; foreach ($values as $v) { - $v = ValueInfo::normalize($v, $convType, null, "sql"); + $v = V::normalize($v, $convType, null, "sql"); if (is_null($v)) { // nulls are pointless to have continue; @@ -161,7 +161,7 @@ class Database { $clause[] = $this->db->literalString($v); } } else { - $clause[] = ValueInfo::normalize($v, ValueInfo::T_STRING, null, "sql"); + $clause[] = V::normalize($v, V::T_STRING, null, "sql"); } $count++; } @@ -299,32 +299,43 @@ class Database { return true; } - public function userPropertiesGet(string $user): array { - $out = $this->db->prepare("SELECT num, admin, lang, tz, sort_asc from arsse_users where id = ?", "str")->run($user)->getRow(); - if (!$out) { + public function userPropertiesGet(string $user, bool $includeLarge = true): array { + $meta = $this->db->prepareArray( + "SELECT \"key\", value from arsse_user_meta where owner = ? and \"key\" not in ('num', 'admin') + union all select 'num', num from arsse_users where id = ? + union all select 'admin', admin from arsse_users where id = ?", + ["str", "str", "str"] + )->run($user)->getRow(); + if (!$meta) { throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } - settype($out['num'], "int"); - settype($out['admin'], "bool"); - settype($out['sort_asc'], "bool"); - return $out; + $meta = array_combine(array_column($meta, "key"), array_column($meta, "value")); + settype($meta['num'], "integer"); + return $meta; } public function userPropertiesSet(string $user, array $data): bool { if (!$this->userExists($user)) { throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } - $allowed = [ - 'admin' => "strict bool", - 'lang' => "str", - 'tz' => "strict str", - 'sort_asc' => "strict bool", - ]; - [$setClause, $setTypes, $setValues] = $this->generateSet($data, $allowed); - if (!$setClause) { - return false; + $update = $this->db->prepare("UPDATE arsse_user_meta set value = ? where owner = ? and \"key\" = ?", "str", "str", "str"); + $insert = ["INSERT INTO arsse_user_meta values(?, ?, ?)", "str", "strict str", "str"]; + foreach ($data as $k => $v) { + if ($k === "admin") { + $this->db->prepare("UPDATE arsse_users SET admin = ? where user = ?", "bool", "str")->run($v, $user); + } elseif ($k === "num") { + continue; + } else { + $success = $update->run($v, $user, $k)->changes(); + if (!$success) { + if (!$insert instanceof Db\Statement) { + $insert = $this->db->prepare(...$insert); + } + $insert->run($user, $k, $v); + } + } } - return (bool) $this->db->prepare("UPDATE arsse_users set $setClause where id = ?", $setTypes, "str")->run($setValues, $user)->changes(); + return true; } /** Creates a new session for the given user and returns the session identifier */ @@ -515,7 +526,7 @@ class Database { * @param integer $id The identifier of the folder to delete */ public function folderRemove(string $user, $id): bool { - if (!ValueInfo::id($id)) { + if (!V::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "folder", 'type' => "int > 0"]); } $changes = $this->db->prepare("DELETE FROM arsse_folders where owner = ? and id = ?", "str", "int")->run($user, $id)->changes(); @@ -527,7 +538,7 @@ class Database { /** Returns the identifier, name, and parent of the given folder as an associative array */ public function folderPropertiesGet(string $user, $id): array { - if (!ValueInfo::id($id)) { + if (!V::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "folder", 'type' => "int > 0"]); } $props = $this->db->prepare("SELECT id,name,parent from arsse_folders where owner = ? and id = ?", "str", "int")->run($user, $id)->getRow(); @@ -593,7 +604,7 @@ class Database { */ protected function folderValidateId(string $user, $id = null, bool $subject = false): array { // if the specified ID is not a non-negative integer (or null), this will always fail - if (!ValueInfo::id($id, true)) { + if (!V::id($id, true)) { throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "folder", 'type' => "int >= 0"]); } // if a null or zero ID is specified this is a no-op @@ -615,13 +626,13 @@ class Database { // the root cannot be moved throw new Db\ExceptionInput("circularDependence", $errData); } - $info = ValueInfo::int($parent); + $info = V::int($parent); // the root is always a valid parent - if ($info & (ValueInfo::NULL | ValueInfo::ZERO)) { + if ($info & (V::NULL | V::ZERO)) { $parent = null; } else { // if a negative integer or non-integer is specified this will always fail - if (!($info & ValueInfo::VALID) || (($info & ValueInfo::NEG))) { + if (!($info & V::VALID) || (($info & V::NEG))) { throw new Db\ExceptionInput("idMissing", $errData); } $parent = (int) $parent; @@ -668,12 +679,12 @@ class Database { * @param integer|null $parent The parent folder context in which to check for duplication */ protected function folderValidateName($name, bool $checkDuplicates = false, $parent = null): bool { - $info = ValueInfo::str($name); - if ($info & (ValueInfo::NULL | ValueInfo::EMPTY)) { + $info = V::str($name); + if ($info & (V::NULL | V::EMPTY)) { throw new Db\ExceptionInput("missing", ["action" => $this->caller(), "field" => "name"]); - } elseif ($info & ValueInfo::WHITE) { + } elseif ($info & V::WHITE) { throw new Db\ExceptionInput("whitespace", ["action" => $this->caller(), "field" => "name"]); - } elseif (!($info & ValueInfo::VALID)) { + } elseif (!($info & V::VALID)) { throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "name", 'type' => "string"]); } elseif ($checkDuplicates) { // make sure that a folder with the same prospective name and parent does not already exist: if the parent is null, @@ -778,7 +789,7 @@ class Database { * configurable retention period for newsfeeds */ public function subscriptionRemove(string $user, $id): bool { - if (!ValueInfo::id($id)) { + if (!V::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'type' => "int > 0"]); } $changes = $this->db->prepare("DELETE from arsse_subscriptions where owner = ? and id = ?", "str", "int")->run($user, $id)->changes(); @@ -807,7 +818,7 @@ class Database { * - "unread": The number of unread articles associated with the subscription */ public function subscriptionPropertiesGet(string $user, $id): array { - if (!ValueInfo::id($id)) { + if (!V::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'type' => "int > 0"]); } $sub = $this->subscriptionList($user, null, true, (int) $id)->getRow(); @@ -841,12 +852,12 @@ class Database { if (array_key_exists("title", $data)) { // if the title is null, this signals intended use of the default title; otherwise make sure it's not effectively an empty string if (!is_null($data['title'])) { - $info = ValueInfo::str($data['title']); - if ($info & ValueInfo::EMPTY) { + $info = V::str($data['title']); + if ($info & V::EMPTY) { throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "title"]); - } elseif ($info & ValueInfo::WHITE) { + } elseif ($info & V::WHITE) { throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "title"]); - } elseif (!($info & ValueInfo::VALID)) { + } elseif (!($info & V::VALID)) { throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "title", 'type' => "string"]); } } @@ -918,7 +929,7 @@ class Database { if (!$out && $id) { throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]); } - return ValueInfo::normalize($out, ValueInfo::T_DATE | ValueInfo::M_NULL, "sql"); + return V::normalize($out, V::T_DATE | V::M_NULL, "sql"); } /** Ensures the specified subscription exists and raises an exception otherwise @@ -930,7 +941,7 @@ class Database { * @param boolean $subject Whether the subscription is the subject (true) rather than the object (false) of the operation being performed; this only affects the semantics of the error message if validation fails */ protected function subscriptionValidateId(string $user, $id, bool $subject = false): array { - if (!ValueInfo::id($id)) { + if (!V::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "feed", 'type' => "int > 0"]); } $out = $this->db->prepare("SELECT id,feed from arsse_subscriptions where id = ? and owner = ?", "int", "str")->run($id, $user)->getRow(); @@ -988,7 +999,7 @@ class Database { */ public function feedUpdate($feedID, bool $throwError = false): bool { // check to make sure the feed exists - if (!ValueInfo::id($feedID)) { + if (!V::id($feedID)) { throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID, 'type' => "int > 0"]); } $f = $this->db->prepare("SELECT url, username, password, modified, etag, err_count, scrape FROM arsse_feeds where id = ?", "int")->run($feedID)->getRow(); @@ -1328,7 +1339,7 @@ class Database { } else { // normalize requested output and sorting columns $norm = function($v) { - return trim(strtolower(ValueInfo::normalize($v, ValueInfo::T_STRING))); + return trim(strtolower(V::normalize($v, V::T_STRING))); }; $cols = array_map($norm, $cols); // make an output column list @@ -1798,7 +1809,7 @@ class Database { * @param integer $id The identifier of the article to validate */ protected function articleValidateId(string $user, $id): array { - if (!ValueInfo::id($id)) { + if (!V::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "article", 'type' => "int > 0"]); // @codeCoverageIgnore } $out = $this->db->prepare( @@ -1825,7 +1836,7 @@ class Database { * @param integer $id The identifier of the edition to validate */ protected function articleValidateEdition(string $user, int $id): array { - if (!ValueInfo::id($id)) { + if (!V::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "edition", 'type' => "int > 0"]); // @codeCoverageIgnore } $out = $this->db->prepare( @@ -2109,10 +2120,10 @@ class Database { * @param boolean $subject Whether the label is the subject (true) rather than the object (false) of the operation being performed; this only affects the semantics of the error message if validation fails */ protected function labelValidateId(string $user, $id, bool $byName, bool $checkDb = true, bool $subject = false): array { - if (!$byName && !ValueInfo::id($id)) { + if (!$byName && !V::id($id)) { // if we're not referring to a label by name and the ID is invalid, throw an exception throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "label", 'type' => "int > 0"]); - } elseif ($byName && !(ValueInfo::str($id) & ValueInfo::VALID)) { + } elseif ($byName && !(V::str($id) & V::VALID)) { // otherwise if we are referring to a label by name but the ID is not a string, also throw an exception throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "label", 'type' => "string"]); } elseif ($checkDb) { @@ -2133,12 +2144,12 @@ class Database { /** Ensures a prospective label name is syntactically valid and raises an exception otherwise */ protected function labelValidateName($name): bool { - $info = ValueInfo::str($name); - if ($info & (ValueInfo::NULL | ValueInfo::EMPTY)) { + $info = V::str($name); + if ($info & (V::NULL | V::EMPTY)) { throw new Db\ExceptionInput("missing", ["action" => $this->caller(), "field" => "name"]); - } elseif ($info & ValueInfo::WHITE) { + } elseif ($info & V::WHITE) { throw new Db\ExceptionInput("whitespace", ["action" => $this->caller(), "field" => "name"]); - } elseif (!($info & ValueInfo::VALID)) { + } elseif (!($info & V::VALID)) { throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "name", 'type' => "string"]); } else { return true; @@ -2381,10 +2392,10 @@ class Database { * @param boolean $subject Whether the tag is the subject (true) rather than the object (false) of the operation being performed; this only affects the semantics of the error message if validation fails */ protected function tagValidateId(string $user, $id, bool $byName, bool $checkDb = true, bool $subject = false): array { - if (!$byName && !ValueInfo::id($id)) { + if (!$byName && !V::id($id)) { // if we're not referring to a tag by name and the ID is invalid, throw an exception throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "tag", 'type' => "int > 0"]); - } elseif ($byName && !(ValueInfo::str($id) & ValueInfo::VALID)) { + } elseif ($byName && !(V::str($id) & V::VALID)) { // otherwise if we are referring to a tag by name but the ID is not a string, also throw an exception throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "tag", 'type' => "string"]); } elseif ($checkDb) { @@ -2405,12 +2416,12 @@ class Database { /** Ensures a prospective tag name is syntactically valid and raises an exception otherwise */ protected function tagValidateName($name): bool { - $info = ValueInfo::str($name); - if ($info & (ValueInfo::NULL | ValueInfo::EMPTY)) { + $info = V::str($name); + if ($info & (V::NULL | V::EMPTY)) { throw new Db\ExceptionInput("missing", ["action" => $this->caller(), "field" => "name"]); - } elseif ($info & ValueInfo::WHITE) { + } elseif ($info & V::WHITE) { throw new Db\ExceptionInput("whitespace", ["action" => $this->caller(), "field" => "name"]); - } elseif (!($info & ValueInfo::VALID)) { + } elseif (!($info & V::VALID)) { throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "name", 'type' => "string"]); } else { return true; diff --git a/lib/User.php b/lib/User.php index e8359bc0..48d7c27d 100644 --- a/lib/User.php +++ b/lib/User.php @@ -14,6 +14,18 @@ class User { public const DRIVER_NAMES = [ 'internal' => \JKingWeb\Arsse\User\Internal\Driver::class, ]; + public const PROPERTIES = [ + 'admin' => V::T_BOOL, + 'lang' => V::T_STRING, + 'tz' => V::T_STRING, + 'sort_asc' => V::T_BOOL, + 'theme' => V::T_STRING, + 'page_size' => V::T_INT, // greater than zero + 'shortcuts' => V::T_BOOL, + 'gestures' => V::T_BOOL, + 'stylesheet' => V::T_STRING, + 'reading_time' => V::T_BOOL, + ]; public $id = null; @@ -115,48 +127,42 @@ class User { return (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get(); } - public function propertiesGet(string $user): array { - $extra = $this->u->userPropertiesGet($user); + public function propertiesGet(string $user, bool $includeLarge = true): array { + $extra = $this->u->userPropertiesGet($user, $includeLarge); // synchronize the internal database if (!Arsse::$db->userExists($user)) { Arsse::$db->userAdd($user, null); Arsse::$db->userPropertiesSet($user, $extra); } // retrieve from the database to get at least the user number, and anything else the driver does not provide - $out = Arsse::$db->userPropertiesGet($user); - // layer on the driver's data - foreach (["tz", "admin", "sort_asc"] as $k) { + $meta = Arsse::$db->userPropertiesGet($user); + // combine all the data + $out = ['num' => $meta['num']]; + foreach (self::PROPERTIES as $k => $t) { if (array_key_exists($k, $extra)) { - $out[$k] = $extra[$k] ?? $out[$k]; + $v = $extra[$k]; + } elseif (array_key_exists($k, $meta)) { + $v = $meta[$k]; + } else { + $v = null; } - } - // treat language specially since it may legitimately be null - if (array_key_exists("lang", $extra)) { - $out['lang'] = $extra['lang']; + $out[$k] = V::normalize($v, $t | V::M_NULL); } return $out; } public function propertiesSet(string $user, array $data): array { $in = []; - if (array_key_exists("tz", $data)) { - if (!is_string($data['tz'])) { - throw new User\ExceptionInput("invalidTimezone", ['field' => "tz", 'value' => ""]); - } elseif (!@timezone_open($data['tz'])) { - throw new User\ExceptionInput("invalidTimezone", ['field' => "tz", 'value' => $data['tz']]); - } - $in['tz'] = $data['tz']; - } - foreach (["admin", "sort_asc"] as $k) { + foreach (self::PROPERTIES as $k => $t) { if (array_key_exists($k, $data)) { - if (($v = V::normalize($data[$k], V::T_BOOL | V::M_DROP)) === null) { - throw new User\ExceptionInput("invalidBoolean", $k); - } - $in[$k] = $v; + // TODO: handle type mistmatch exception + $in[$k] = V::normalize($data[$k], $t | V::M_NULL | V::M_STRICT); } } - if (array_key_exists("lang", $data)) { - $in['lang'] = V::normalize($data['lang'], V::T_STRING | V::M_NULL); + if (isset($in['tz']) && !@timezone_open($in['tz'])) { + throw new User\ExceptionInput("invalidTimezone", ['field' => "tz", 'value' => $in['tz']]); + } elseif (isset($in['page_size']) && $in['page_size'] < 1) { + throw new User\ExceptionInput("invalidNonZeroInteger", ['field' => "page_size"]); } $out = $this->u->userPropertiesSet($user, $in); // synchronize the internal database diff --git a/lib/User/Driver.php b/lib/User/Driver.php index 5da6a0ca..e0d949c7 100644 --- a/lib/User/Driver.php +++ b/lib/User/Driver.php @@ -65,7 +65,7 @@ interface Driver { * * Any other keys will be ignored. */ - public function userPropertiesGet(string $user): array; + public function userPropertiesGet(string $user, bool $includeLarge = true): array; /** Sets metadata about a user * diff --git a/sql/MySQL/6.sql b/sql/MySQL/6.sql index 6c652e82..aeb7c126 100644 --- a/sql/MySQL/6.sql +++ b/sql/MySQL/6.sql @@ -8,9 +8,6 @@ alter table arsse_tokens add column data longtext default null; alter table arsse_users add column num bigint unsigned unique; alter table arsse_users add column admin boolean not null default 0; -alter table arsse_users add column lang longtext; -alter table arsse_users add column tz varchar(44) not null default 'Etc/UTC'; -alter table arsse_users add column sort_asc boolean not null default 0; create temporary table arsse_users_existing( id text not null, num serial primary key @@ -22,6 +19,14 @@ where u.id = n.id; drop table arsse_users_existing; alter table arsse_users modify num bigint unsigned not null; +create table arsse_user_meta( + owner varchar(255) not null, + "key" varchar(255) not null, + value longtext, + foreign key(owner) references arsse_users(id) on delete cascade on update cascade, + primary key(owner,key) +); + create table arsse_icons( id serial primary key, url varchar(767) unique not null, diff --git a/sql/PostgreSQL/6.sql b/sql/PostgreSQL/6.sql index f14f8c83..0b405a27 100644 --- a/sql/PostgreSQL/6.sql +++ b/sql/PostgreSQL/6.sql @@ -8,9 +8,6 @@ alter table arsse_tokens add column data text default null; alter table arsse_users add column num bigint unique; alter table arsse_users add column admin smallint not null default 0; -alter table arsse_users add column lang text; -alter table arsse_users add column tz text not null default 'Etc/UTC'; -alter table arsse_users add column sort_asc smallint not null default 0; create temp table arsse_users_existing( id text not null, num bigserial @@ -23,6 +20,13 @@ where u.id = e.id; drop table arsse_users_existing; alter table arsse_users alter column num set not null; +create table arsse_user_meta( + owner text not null references arsse_users(id) on delete cascade on update cascade, + key text not null, + value text, + primary key(owner,key) +); + create table arsse_icons( id bigserial primary key, url text unique not null, diff --git a/sql/SQLite3/6.sql b/sql/SQLite3/6.sql index 8c6f73f3..5f067229 100644 --- a/sql/SQLite3/6.sql +++ b/sql/SQLite3/6.sql @@ -6,7 +6,7 @@ -- This is a speculative addition to support OAuth login in the future alter table arsse_tokens add column data text default null; --- Add multiple columns to the users table +-- Add num and admin columns to the users table -- In particular this adds a numeric identifier for each user, which Miniflux requires create table arsse_users_new( -- users @@ -14,9 +14,6 @@ create table arsse_users_new( password text, -- password, salted and hashed; if using external authentication this would be blank num integer unique not null, -- numeric identfier used by Miniflux admin boolean not null default 0, -- Whether the user is an administrator - lang text, -- The user's chosen language code e.g. 'en', 'fr-ca'; null uses the system default - tz text not null default 'Etc/UTC', -- The user's chosen time zone, in zoneinfo format - sort_asc boolean not null default 0 -- Whether the user prefers to sort articles in ascending order ) without rowid; create temp table arsse_users_existing( id text not null, @@ -31,6 +28,17 @@ drop table arsse_users; drop table arsse_users_existing; alter table arsse_users_new rename to arsse_users; +-- Add a table for other user metadata +create table arsse_user_meta( + -- Metadata for users + -- It is up to individual applications (i.e. the client protocols) to cooperate with names and types + owner text not null references arsse_users(id) on delete cascade on update cascade, -- the user to whom the metadata belongs + key text not null, -- metadata key + value text, -- metadata value + primary key(owner,key) +) without rowid; + + -- Add a separate table for feed icons and replace their URLs in the feeds table with their IDs create table arsse_icons( -- Icons associated with feeds diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index 0c13012b..a3d5507c 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -17,14 +17,26 @@ trait SeriesUser { 'password' => 'str', 'num' => 'int', 'admin' => 'bool', - 'lang' => 'str', - 'tz' => 'str', - 'sort_asc' => 'bool', ], 'rows' => [ - ["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW',1, 1, "en", "America/Toronto", 0], // password is hash of "secret" - ["jane.doe@example.com", "",2, 0, "fr", "Asia/Kuala_Lumpur", 1], - ["john.doe@example.com", "",3, 0, null, "Etc/UTC", 0], + ["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW', 1, 1], // password is hash of "secret" + ["jane.doe@example.com", "", 2, 0], + ["john.doe@example.com", "", 3, 0], + ], + ], + 'arsse_user_meta' => [ + 'columns' => [ + 'owner' => "str", + 'key' => "str", + 'value' => "str", + ], + 'rows' => [ + ["admin@example.net", "lang", "en"], + ["admin@example.net", "tz", "America/Toronto"], + ["admin@example.net", "sort", "desc"], + ["jane.doe@example.com", "lang", "fr"], + ["jane.doe@example.com", "tz", "Asia/Kuala_Lumpur"], + ["jane.doe@example.com", "sort", "asc"], ], ], ]; From fcf1260dab61336c7e0a2316e913cf418048997f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 5 Dec 2020 22:13:48 -0500 Subject: [PATCH 057/366] Adjust database portion of user property manager --- lib/Database.php | 13 ++++++++---- lib/User.php | 1 + lib/User/Internal/Driver.php | 2 +- sql/SQLite3/6.sql | 2 +- tests/cases/Database/SeriesUser.php | 31 ++++++++++++++++++----------- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 9135b203..9c4cf7f0 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -300,12 +300,17 @@ class Database { } public function userPropertiesGet(string $user, bool $includeLarge = true): array { + $exclude = ["num", "admin"]; + if (!$includeLarge) { + $exclude = array_merge($exclude, User::PROPERTIES_LARGE); + } + [$inClause, $inTypes, $inValues] = $this->generateIn($exclude, "str"); $meta = $this->db->prepareArray( - "SELECT \"key\", value from arsse_user_meta where owner = ? and \"key\" not in ('num', 'admin') + "SELECT \"key\", value from arsse_user_meta where owner = ? and \"key\" not in ($inClause) union all select 'num', num from arsse_users where id = ? union all select 'admin', admin from arsse_users where id = ?", - ["str", "str", "str"] - )->run($user)->getRow(); + ["str", $inTypes, "str", "str"] + )->run($user, $inValues, $user, $user)->getAll(); if (!$meta) { throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } @@ -322,7 +327,7 @@ class Database { $insert = ["INSERT INTO arsse_user_meta values(?, ?, ?)", "str", "strict str", "str"]; foreach ($data as $k => $v) { if ($k === "admin") { - $this->db->prepare("UPDATE arsse_users SET admin = ? where user = ?", "bool", "str")->run($v, $user); + $this->db->prepare("UPDATE arsse_users SET admin = ? where id = ?", "bool", "str")->run($v, $user); } elseif ($k === "num") { continue; } else { diff --git a/lib/User.php b/lib/User.php index 48d7c27d..124fd238 100644 --- a/lib/User.php +++ b/lib/User.php @@ -26,6 +26,7 @@ class User { 'stylesheet' => V::T_STRING, 'reading_time' => V::T_BOOL, ]; + public const PROPERTIES_LARGE = ["stylesheet"]; public $id = null; diff --git a/lib/User/Internal/Driver.php b/lib/User/Internal/Driver.php index c6d0a989..27486fb1 100644 --- a/lib/User/Internal/Driver.php +++ b/lib/User/Internal/Driver.php @@ -71,7 +71,7 @@ class Driver implements \JKingWeb\Arsse\User\Driver { return Arsse::$db->userExists($user); } - public function userPropertiesGet(string $user): array { + public function userPropertiesGet(string $user, bool $includeLarge = true): array { // do nothing: the internal database will retrieve everything for us if (!$this->userExists($user)) { throw new ExceptionConflict("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); diff --git a/sql/SQLite3/6.sql b/sql/SQLite3/6.sql index 5f067229..9e86182d 100644 --- a/sql/SQLite3/6.sql +++ b/sql/SQLite3/6.sql @@ -13,7 +13,7 @@ create table arsse_users_new( id text primary key not null collate nocase, -- user id password text, -- password, salted and hashed; if using external authentication this would be blank num integer unique not null, -- numeric identfier used by Miniflux - admin boolean not null default 0, -- Whether the user is an administrator + admin boolean not null default 0 -- Whether the user is an administrator ) without rowid; create temp table arsse_users_existing( id text not null, diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index a3d5507c..bf78cf86 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -33,10 +33,11 @@ trait SeriesUser { 'rows' => [ ["admin@example.net", "lang", "en"], ["admin@example.net", "tz", "America/Toronto"], - ["admin@example.net", "sort", "desc"], + ["admin@example.net", "sort_asc", "0"], ["jane.doe@example.com", "lang", "fr"], ["jane.doe@example.com", "tz", "Asia/Kuala_Lumpur"], - ["jane.doe@example.com", "sort", "asc"], + ["jane.doe@example.com", "sort_asc", "1"], + ["john.doe@example.com", "stylesheet", "body {background:lightgray}"], ], ], ]; @@ -118,15 +119,18 @@ trait SeriesUser { } /** @dataProvider provideMetaData */ - public function testGetMetadata(string $user, array $exp): void { - $this->assertSame($exp, Arsse::$db->userPropertiesGet($user)); + public function testGetMetadata(string $user, bool $includeLarge, array $exp): void { + $this->assertSame($exp, Arsse::$db->userPropertiesGet($user, $includeLarge)); } public function provideMetadata(): iterable { return [ - ["admin@example.net", ['num' => 1, 'admin' => true, 'lang' => "en", 'tz' => "America/Toronto", 'sort_asc' => false]], - ["jane.doe@example.com", ['num' => 2, 'admin' => false, 'lang' => "fr", 'tz' => "Asia/Kuala_Lumpur", 'sort_asc' => true]], - ["john.doe@example.com", ['num' => 3, 'admin' => false, 'lang' => null, 'tz' => "Etc/UTC", 'sort_asc' => false]], + ["admin@example.net", true, ['lang' => "en", 'sort_asc' => "0", 'tz' => "America/Toronto", 'num' => 1, 'admin' => '1']], + ["jane.doe@example.com", true, ['lang' => "fr", 'sort_asc' => "1", 'tz' => "Asia/Kuala_Lumpur", 'num' => 2, 'admin' => '0']], + ["john.doe@example.com", true, ['stylesheet' => "body {background:lightgray}", 'num' => 3, 'admin' => '0']], + ["admin@example.net", false, ['lang' => "en", 'sort_asc' => "0", 'tz' => "America/Toronto", 'num' => 1, 'admin' => '1']], + ["jane.doe@example.com", false, ['lang' => "fr", 'sort_asc' => "1", 'tz' => "Asia/Kuala_Lumpur", 'num' => 2, 'admin' => '0']], + ["john.doe@example.com", false, ['num' => 3, 'admin' => '0']], ]; } @@ -143,18 +147,21 @@ trait SeriesUser { 'sort_asc' => true, ]; $this->assertTrue(Arsse::$db->userPropertiesSet("john.doe@example.com", $in)); - $state = $this->primeExpectations($this->data, ['arsse_users' => ['id', 'num', 'admin', 'lang', 'tz', 'sort_asc']]); - $state['arsse_users']['rows'][2] = ["john.doe@example.com", 3, 1, "en-ca", "Atlantic/Reykjavik", 1]; + $state = $this->primeExpectations($this->data, ['arsse_users' => ['id', 'num', 'admin'], 'arsse_user_meta' => ["owner", "key", "value"]]); + $state['arsse_users']['rows'][2][2] = 1; + $state['arsse_user_meta']['rows'][] = ["john.doe@example.com", "lang", "en-ca"]; + $state['arsse_user_meta']['rows'][] = ["john.doe@example.com", "tz", "Atlantic/Reykjavik"]; + $state['arsse_user_meta']['rows'][] = ["john.doe@example.com", "sort_asc", "1"]; $this->compareExpectations(static::$drv, $state); } public function testSetNoMetadata(): void { $in = [ 'num' => 2112, - 'blah' => "bloo", + 'stylesheet' => "body {background:lightgray}", ]; - $this->assertFalse(Arsse::$db->userPropertiesSet("john.doe@example.com", $in)); - $state = $this->primeExpectations($this->data, ['arsse_users' => ['id', 'num', 'admin', 'lang', 'tz', 'sort_asc']]); + $this->assertTrue(Arsse::$db->userPropertiesSet("john.doe@example.com", $in)); + $state = $this->primeExpectations($this->data, ['arsse_users' => ['id', 'num', 'admin'], 'arsse_user_meta' => ["owner", "key", "value"]]); $this->compareExpectations(static::$drv, $state); } From a4312434218f3113ecbb81497eae0aadbb2dc87e Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 6 Dec 2020 13:17:19 -0500 Subject: [PATCH 058/366] Fixes for MySQL and PostgreSQL --- lib/Database.php | 21 +++++++++------------ lib/Db/MySQL/Statement.php | 6 +++++- sql/MySQL/6.sql | 4 ++-- tests/cases/Database/SeriesUser.php | 12 ++++++------ 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 9c4cf7f0..f01338a8 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -300,24 +300,21 @@ class Database { } public function userPropertiesGet(string $user, bool $includeLarge = true): array { + $basic = $this->db->prepare("SELECT num, admin from arsse_users where id = ?", "str")->run($user)->getRow(); + if (!$basic) { + throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + } $exclude = ["num", "admin"]; if (!$includeLarge) { $exclude = array_merge($exclude, User::PROPERTIES_LARGE); } [$inClause, $inTypes, $inValues] = $this->generateIn($exclude, "str"); - $meta = $this->db->prepareArray( - "SELECT \"key\", value from arsse_user_meta where owner = ? and \"key\" not in ($inClause) - union all select 'num', num from arsse_users where id = ? - union all select 'admin', admin from arsse_users where id = ?", - ["str", $inTypes, "str", "str"] - )->run($user, $inValues, $user, $user)->getAll(); - if (!$meta) { - throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); - } - $meta = array_combine(array_column($meta, "key"), array_column($meta, "value")); + $meta = $this->db->prepare("SELECT \"key\", value from arsse_user_meta where owner = ? and \"key\" not in ($inClause) order by \"key\"", "str", $inTypes)->run($user, $inValues)->getAll(); + $meta = array_merge($basic, array_combine(array_column($meta, "key"), array_column($meta, "value"))); settype($meta['num'], "integer"); + settype($meta['admin'], "integer"); return $meta; - } + } public function userPropertiesSet(string $user, array $data): bool { if (!$this->userExists($user)) { @@ -454,7 +451,7 @@ class Database { /** List tokens associated with a user */ public function tokenList(string $user, string $class): Db\Result { - return $this->db->prepare("SELECT id,created,expires,data from arsse_tokens where class = ? and user = ? and (expires is null or expires > CURRENT_TIMESTAMP)", "str", "str")->run($class, $user); + return $this->db->prepare("SELECT id,created,expires,data from arsse_tokens where class = ? and \"user\" = ? and (expires is null or expires > CURRENT_TIMESTAMP)", "str", "str")->run($class, $user); } /** Deletes expires tokens from the database, returning the number of deleted tokens */ diff --git a/lib/Db/MySQL/Statement.php b/lib/Db/MySQL/Statement.php index 057225a6..aba3542b 100644 --- a/lib/Db/MySQL/Statement.php +++ b/lib/Db/MySQL/Statement.php @@ -84,7 +84,11 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement { } // create a result-set instance $r = $this->st->get_result(); - $changes = $this->st->affected_rows; + if (preg_match("\d+", mysqli_info($this->db), $m)) { + $changes = (int) $m[0]; + } else { + $changes = 0; + } $lastId = $this->st->insert_id; return new Result($r, [$changes, $lastId], $this); } diff --git a/sql/MySQL/6.sql b/sql/MySQL/6.sql index aeb7c126..23ba5edb 100644 --- a/sql/MySQL/6.sql +++ b/sql/MySQL/6.sql @@ -24,8 +24,8 @@ create table arsse_user_meta( "key" varchar(255) not null, value longtext, foreign key(owner) references arsse_users(id) on delete cascade on update cascade, - primary key(owner,key) -); + primary key(owner,"key") +) character set utf8mb4 collate utf8mb4_unicode_ci; create table arsse_icons( id serial primary key, diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index bf78cf86..9ca140c7 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -125,12 +125,12 @@ trait SeriesUser { public function provideMetadata(): iterable { return [ - ["admin@example.net", true, ['lang' => "en", 'sort_asc' => "0", 'tz' => "America/Toronto", 'num' => 1, 'admin' => '1']], - ["jane.doe@example.com", true, ['lang' => "fr", 'sort_asc' => "1", 'tz' => "Asia/Kuala_Lumpur", 'num' => 2, 'admin' => '0']], - ["john.doe@example.com", true, ['stylesheet' => "body {background:lightgray}", 'num' => 3, 'admin' => '0']], - ["admin@example.net", false, ['lang' => "en", 'sort_asc' => "0", 'tz' => "America/Toronto", 'num' => 1, 'admin' => '1']], - ["jane.doe@example.com", false, ['lang' => "fr", 'sort_asc' => "1", 'tz' => "Asia/Kuala_Lumpur", 'num' => 2, 'admin' => '0']], - ["john.doe@example.com", false, ['num' => 3, 'admin' => '0']], + ["admin@example.net", true, ['num' => 1, 'admin' => 1, 'lang' => "en", 'sort_asc' => "0", 'tz' => "America/Toronto"]], + ["jane.doe@example.com", true, ['num' => 2, 'admin' => 0, 'lang' => "fr", 'sort_asc' => "1", 'tz' => "Asia/Kuala_Lumpur"]], + ["john.doe@example.com", true, ['num' => 3, 'admin' => 0, 'stylesheet' => "body {background:lightgray}"]], + ["admin@example.net", false, ['num' => 1, 'admin' => 1, 'lang' => "en", 'sort_asc' => "0", 'tz' => "America/Toronto"]], + ["jane.doe@example.com", false, ['num' => 2, 'admin' => 0, 'lang' => "fr", 'sort_asc' => "1", 'tz' => "Asia/Kuala_Lumpur"]], + ["john.doe@example.com", false, ['num' => 3, 'admin' => 0]], ]; } From ce68566fcb8d235e75ad048cb2ee6ab5d400d0a2 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 6 Dec 2020 20:27:20 -0500 Subject: [PATCH 059/366] Hopefully fix MySQL --- lib/Database.php | 2 +- lib/Db/MySQL/Statement.php | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index f01338a8..ecf1edea 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -321,7 +321,7 @@ class Database { throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } $update = $this->db->prepare("UPDATE arsse_user_meta set value = ? where owner = ? and \"key\" = ?", "str", "str", "str"); - $insert = ["INSERT INTO arsse_user_meta values(?, ?, ?)", "str", "strict str", "str"]; + $insert = ["INSERT INTO arsse_user_meta select ?, ?, ? where not exists(select 1 from arsse_user_meta where owner = ? and \"key\" = ?)", "str", "strict str", "str", "str", "strict str"]; foreach ($data as $k => $v) { if ($k === "admin") { $this->db->prepare("UPDATE arsse_users SET admin = ? where id = ?", "bool", "str")->run($v, $user); diff --git a/lib/Db/MySQL/Statement.php b/lib/Db/MySQL/Statement.php index aba3542b..057225a6 100644 --- a/lib/Db/MySQL/Statement.php +++ b/lib/Db/MySQL/Statement.php @@ -84,11 +84,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement { } // create a result-set instance $r = $this->st->get_result(); - if (preg_match("\d+", mysqli_info($this->db), $m)) { - $changes = (int) $m[0]; - } else { - $changes = 0; - } + $changes = $this->st->affected_rows; $lastId = $this->st->insert_id; return new Result($r, [$changes, $lastId], $this); } From e9d449a8ba76373f88c8cb785093683b48b00978 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 7 Dec 2020 00:07:10 -0500 Subject: [PATCH 060/366] Fix user manager and tests --- lib/AbstractException.php | 5 +++-- lib/User.php | 9 ++++++--- locale/en.php | 14 +++++++++++++- tests/cases/User/TestUser.php | 19 +++++++++++-------- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/lib/AbstractException.php b/lib/AbstractException.php index d26b3cd3..73a17076 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -74,8 +74,9 @@ abstract class AbstractException extends \Exception { "User/ExceptionConflict.alreadyExists" => 10403, "User/ExceptionSession.invalid" => 10431, "User/ExceptionInput.invalidTimezone" => 10441, - "User/ExceptionInput.invalidBoolean" => 10442, - "User/ExceptionInput.invalidUsername" => 10443, + "User/ExceptionInput.invalidValue" => 10442, + "User/ExceptionInput.invalidNonZeroInteger" => 10443, + "User/ExceptionInput.invalidUsername" => 10444, "Feed/Exception.internalError" => 10500, "Feed/Exception.invalidCertificate" => 10501, "Feed/Exception.invalidUrl" => 10502, diff --git a/lib/User.php b/lib/User.php index 124fd238..bf457a95 100644 --- a/lib/User.php +++ b/lib/User.php @@ -136,7 +136,7 @@ class User { Arsse::$db->userPropertiesSet($user, $extra); } // retrieve from the database to get at least the user number, and anything else the driver does not provide - $meta = Arsse::$db->userPropertiesGet($user); + $meta = Arsse::$db->userPropertiesGet($user, $includeLarge); // combine all the data $out = ['num' => $meta['num']]; foreach (self::PROPERTIES as $k => $t) { @@ -156,8 +156,11 @@ class User { $in = []; foreach (self::PROPERTIES as $k => $t) { if (array_key_exists($k, $data)) { - // TODO: handle type mistmatch exception - $in[$k] = V::normalize($data[$k], $t | V::M_NULL | V::M_STRICT); + try { + $in[$k] = V::normalize($data[$k], $t | V::M_NULL | V::M_STRICT); + } catch (\JKingWeb\Arsse\ExceptionType $e) { + throw new User\ExceptionInput("invalidValue", ['field' => $k, 'type' => $t], $e); + } } } if (isset($in['tz']) && !@timezone_open($in['tz'])) { diff --git a/locale/en.php b/locale/en.php index f58d7b49..cbf3d793 100644 --- a/locale/en.php +++ b/locale/en.php @@ -148,8 +148,20 @@ return [ 'Exception.JKingWeb/Arsse/User/Exception.authFailed' => 'Authentication failed', 'Exception.JKingWeb/Arsse/User/ExceptionSession.invalid' => 'Session with ID {0} does not exist', 'Exception.JKingWeb/Arsse/User/ExceptionInput.invalidUsername' => 'User names may not contain the Unicode character {0}', - 'Exception.JKingWeb/Arsse/User/ExceptionInput.invalidBoolean' => 'User property "{0}" must be a boolean value (true or false)', + 'Exception.JKingWeb/Arsse/User/ExceptionInput.invalidValue' => + 'User property "{field}" must be {type, select, + 1 {null} + 2 {true or false} + 3 {an integer} + 4 {a real number} + 5 {a DateTime object} + 6 {a string} + 7 {an array} + 8 {a DateInterval object} + other {another type} + }', 'Exception.JKingWeb/Arsse/User/ExceptionInput.invalidTimezone' => 'User property "{field}" must be a valid zoneinfo timezone', + 'Exception.JKingWeb/Arsse/User/ExceptionInput.invalidNonZeroInteger' => 'User property "{field}" must be greater than zero', 'Exception.JKingWeb/Arsse/Feed/Exception.internalError' => 'Could not download feed "{url}" because of an internal error which is probably a bug', 'Exception.JKingWeb/Arsse/Feed/Exception.invalidCertificate' => 'Could not download feed "{url}" because its server is serving an invalid SSL certificate', 'Exception.JKingWeb/Arsse/Feed/Exception.invalidUrl' => 'Feed URL "{url}" is invalid', diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 8863d5fc..84228cac 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -331,13 +331,14 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideProperties */ public function testGetThePropertiesOfAUser(array $exp, array $base, array $extra): void { $user = "john.doe@example.com"; + $exp = array_merge(['num' => null], array_combine(array_keys(User::PROPERTIES), array_fill(0, sizeof(User::PROPERTIES), null)), $exp); $u = new User($this->drv); \Phake::when($this->drv)->userPropertiesGet->thenReturn($extra); \Phake::when(Arsse::$db)->userPropertiesGet->thenReturn($base); \Phake::when(Arsse::$db)->userExists->thenReturn(true); $this->assertSame($exp, $u->propertiesGet($user)); - \Phake::verify($this->drv)->userPropertiesGet($user); - \Phake::verify(Arsse::$db)->userPropertiesGet($user); + \Phake::verify($this->drv)->userPropertiesGet($user, true); + \Phake::verify(Arsse::$db)->userPropertiesGet($user, true); \Phake::verify(Arsse::$db)->userExists($user); } @@ -356,14 +357,15 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $extra = ['tz' => "Europe/Istanbul"]; $base = ['num' => 47, 'admin' => false, 'lang' => null, 'tz' => "Etc/UTC", 'sort_asc' => false]; $exp = ['num' => 47, 'admin' => false, 'lang' => null, 'tz' => "Europe/Istanbul", 'sort_asc' => false]; + $exp = array_merge(['num' => null], array_combine(array_keys(User::PROPERTIES), array_fill(0, sizeof(User::PROPERTIES), null)), $exp); $u = new User($this->drv); \Phake::when($this->drv)->userPropertiesGet->thenReturn($extra); \Phake::when(Arsse::$db)->userPropertiesGet->thenReturn($base); \Phake::when(Arsse::$db)->userAdd->thenReturn(true); \Phake::when(Arsse::$db)->userExists->thenReturn(false); $this->assertSame($exp, $u->propertiesGet($user)); - \Phake::verify($this->drv)->userPropertiesGet($user); - \Phake::verify(Arsse::$db)->userPropertiesGet($user); + \Phake::verify($this->drv)->userPropertiesGet($user, true); + \Phake::verify(Arsse::$db)->userPropertiesGet($user, true); \Phake::verify(Arsse::$db)->userPropertiesSet($user, $extra); \Phake::verify(Arsse::$db)->userAdd($user, null); \Phake::verify(Arsse::$db)->userExists($user); @@ -377,7 +379,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { try { $u->propertiesGet($user); } finally { - \Phake::verify($this->drv)->userPropertiesGet($user); + \Phake::verify($this->drv)->userPropertiesGet($user, true); } } @@ -421,13 +423,14 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { public function providePropertyChanges(): iterable { return [ [['admin' => true], ['admin' => true]], - [['admin' => 2], new ExceptionInput("invalidBoolean")], - [['sort_asc' => 2], new ExceptionInput("invalidBoolean")], + [['admin' => 2], new ExceptionInput("invalidValue")], + [['sort_asc' => 2], new ExceptionInput("invalidValue")], [['tz' => "Etc/UTC"], ['tz' => "Etc/UTC"]], [['tz' => "Etc/blah"], new ExceptionInput("invalidTimezone")], - [['tz' => false], new ExceptionInput("invalidTimezone")], + [['tz' => false], new ExceptionInput("invalidValue")], [['lang' => "en-ca"], ['lang' => "en-CA"]], [['lang' => null], ['lang' => null]], + [['page_size' => 0], new ExceptionInput("invalidNonZeroInteger")] ]; } From 2eedf7d38c3d00ef2c5402f734f28dab13994ea4 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 7 Dec 2020 09:52:42 -0500 Subject: [PATCH 061/366] Finally fix MySQL --- lib/Database.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index ecf1edea..f961f7dd 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -320,23 +320,24 @@ class Database { if (!$this->userExists($user)) { throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } + $tr = $this->begin(); + $find = $this->db->prepare("SELECT count(*) from arsse_user_meta where owner = ? and \"key\" = ?", "str", "strict str"); $update = $this->db->prepare("UPDATE arsse_user_meta set value = ? where owner = ? and \"key\" = ?", "str", "str", "str"); - $insert = ["INSERT INTO arsse_user_meta select ?, ?, ? where not exists(select 1 from arsse_user_meta where owner = ? and \"key\" = ?)", "str", "strict str", "str", "str", "strict str"]; + $insert = $this->db->prepare("INSERT INTO arsse_user_meta values(?, ?, ?)", "str", "strict str", "str"); foreach ($data as $k => $v) { if ($k === "admin") { $this->db->prepare("UPDATE arsse_users SET admin = ? where id = ?", "bool", "str")->run($v, $user); } elseif ($k === "num") { continue; } else { - $success = $update->run($v, $user, $k)->changes(); - if (!$success) { - if (!$insert instanceof Db\Statement) { - $insert = $this->db->prepare(...$insert); - } + if ($find->run($user, $k)->getValue()) { + $update->run($v, $user, $k); + } else { $insert->run($user, $k, $v); } } } + $tr->commit(); return true; } From d85988f09d2dc8d564ee7b15c5199f5e40c8e18f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 8 Dec 2020 15:34:31 -0500 Subject: [PATCH 062/366] Prototype Miniflux user querying --- lib/Misc/Date.php | 2 +- lib/REST/Miniflux/V1.php | 94 +++++++++++++++++++++++++++++++++++----- locale/en.php | 2 + 3 files changed, 85 insertions(+), 13 deletions(-) diff --git a/lib/Misc/Date.php b/lib/Misc/Date.php index 6972ea5e..6384f4f3 100644 --- a/lib/Misc/Date.php +++ b/lib/Misc/Date.php @@ -6,7 +6,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Misc; -class Date { +abstract class Date { public static function transform($date, string $outFormat = null, string $inFormat = null) { $date = ValueInfo::normalize($date, ValueInfo::T_DATE, $inFormat); if (!$date) { diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 321716da..2f2685fc 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -12,6 +12,7 @@ use JKingWeb\Arsse\Feed\Exception as FeedException; use JKingWeb\Arsse\AbstractException; use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Misc\HTTP; +use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\ValueInfo as V; use JKingWeb\Arsse\REST\Exception; use JKingWeb\Arsse\User\ExceptionConflict as UserException; @@ -32,8 +33,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'password' => "string", 'user_agent' => "string", ]; - - protected $paths = [ + protected const PATHS = [ '/categories' => ['GET' => "getCategories", 'POST' => "createCategory"], '/categories/1' => ['PUT' => "updateCategory", 'DELETE' => "deleteCategory"], '/discover' => ['POST' => "discoverSubscriptions"], @@ -42,7 +42,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { '/entries/1/bookmark' => ['PUT' => "toggleEntryBookmark"], '/export' => ['GET' => "opmlExport"], '/feeds' => ['GET' => "getFeeds", 'POST' => "createFeed"], - '/feeds/1' => ['GET' => "getFeed", 'PUT' => "updateFeed", 'DELETE' => "removeFeed"], + '/feeds/1' => ['GET' => "getFeed", 'PUT' => "updateFeed", 'DELETE' => "removeFeed"], '/feeds/1/entries/1' => ['GET' => "getFeedEntry"], '/feeds/1/entries' => ['GET' => "getFeedEntries"], '/feeds/1/icon' => ['GET' => "getFeedIcon"], @@ -51,8 +51,16 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { '/import' => ['POST' => "opmlImport"], '/me' => ['GET' => "getCurrentUser"], '/users' => ['GET' => "getUsers", 'POST' => "createUser"], - '/users/1' => ['GET' => "getUser", 'PUT' => "updateUser", 'DELETE' => "deleteUser"], - '/users/*' => ['GET' => "getUser"], + '/users/1' => ['GET' => "getUserByNum", 'PUT' => "updateUserByNum", 'DELETE' => "deleteUser"], + '/users/*' => ['GET' => "getUserById"], + ]; + protected const ADMIN_FUNCTIONS = [ + 'getUsers' => true, + 'getUserByNum' => true, + 'getUserById' => true, + 'createUser' => true, + 'updateUserByNum' => true, + 'deleteUser' => true, ]; public function __construct() { @@ -80,6 +88,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return false; } + protected function isAdmin(): bool { + return (bool) Arsse::$user->propertiesGet(Arsse::$user->id, false)['admin']; + } + + public function dispatch(ServerRequestInterface $req): ResponseInterface { // try to authenticate if (!$this->authenticate($req)) { @@ -96,6 +109,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if ($func instanceof ResponseInterface) { return $func; } + if ((self::ADMIN_FUNCTIONS[$func] ?? false) && !$this->isAdmin()) { + return new ErrorResponse("403", 403); + } $data = []; $query = []; if ($func === "opmlImport") { @@ -148,9 +164,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function handleHTTPOptions(string $url): ResponseInterface { // normalize the URL path: change any IDs to 1 for easier comparison $url = $this->normalizePathIDs($url); - if (isset($this->paths[$url])) { + if (isset(self::PATHS[$url])) { // if the path is supported, respond with the allowed methods and other metadata - $allowed = array_keys($this->paths[$url]); + $allowed = array_keys(self::PATHS[$url]); // if GET is allowed, so is HEAD if (in_array("GET", $allowed)) { array_unshift($allowed, "HEAD"); @@ -172,15 +188,15 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $method = strtoupper($method); // we now evaluate the supplied URL against every supported path for the selected scope // the URL is evaluated as an array so as to avoid decoded escapes turning invalid URLs into valid ones - if (isset($this->paths[$url])) { + if (isset(self::PATHS[$url])) { // if the path is supported, make sure the method is allowed - if (isset($this->paths[$url][$method])) { + if (isset(self::PATHS[$url][$method])) { // if it is allowed, return the object method to run, assuming the method exists - assert(method_exists($this, $this->paths[$url][$method]), new \Exception("Method is not implemented")); - return $this->paths[$url][$method]; + assert(method_exists($this, self::PATHS[$url][$method]), new \Exception("Method is not implemented")); + return self::PATHS[$url][$method]; } else { // otherwise return 405 - return new EmptyResponse(405, ['Allow' => implode(", ", array_keys($this->paths[$url]))]); + return new EmptyResponse(405, ['Allow' => implode(", ", array_keys(self::PATHS[$url]))]); } } else { // if the path is not supported, return 404 @@ -200,6 +216,40 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return $body; } + protected function listUsers(array $users, bool $reportMissing): array { + $out = []; + $now = Date::transform("now", "iso8601m"); + foreach ($users as $u) { + try { + $info = Arsse::$user->propertiesGet($u, true); + } catch (UserException $e) { + if ($reportMissing) { + throw $e; + } else { + continue; + } + } + $out[] = [ + 'id' => $info['num'], + 'username' => $u, + 'is_admin' => $info['admin'] ?? false, + 'theme' => $info['theme'] ?? "light_serif", + 'language' => $info['lang'] ?? "en_US", + 'timezone' => $info['tz'] ?? "UTC", + 'entry_sorting_direction' => ($info['sort_asc'] ?? false) ? "asc" : "desc", + 'entries_per_page' => $info['page_size'] ?? 100, + 'keyboard_shortcuts' => $info['shortcuts'] ?? true, + 'show_reading_time' => $info['reading_time'] ?? true, + 'last_login_at' => $now, + 'entry_swipe' => $info['swipe'] ?? true, + 'extra' => [ + 'custom_css' => $info['stylesheet'] ?? "", + ], + ]; + } + return $out; + } + protected function discoverSubscriptions(array $path, array $query, array $data) { try { $list = Feed::discoverAll((string) $data['url'], (string) $data['username'], (string) $data['password']); @@ -219,6 +269,26 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($out); } + protected function getUsers(array $path, array $query, array $data) { + return new Response($this->listUsers(Arsse::$user->list(), false)); + } + + protected function getUserById(array $path, array $query, array $data) { + try { + return $this->listUsers([$path[1]], true)[0] ?? []; + } catch (UserException $e) { + return new ErrorResponse("404", 404); + } + } + + protected function getUserByNum(array $path, array $query, array $data) { + return $this->listUsers([Arsse::$user->id], false)[0] ?? []; + } + + protected function getCurrentUser(array $path, array $query, array $data) { + return new Response($this->listUsers([Arsse::$user->id], false)[0] ?? new \stdClass); + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokens in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); diff --git a/locale/en.php b/locale/en.php index cbf3d793..75b52e5f 100644 --- a/locale/en.php +++ b/locale/en.php @@ -8,6 +8,8 @@ return [ 'CLI.Auth.Failure' => 'Authentication failed', 'API.Miniflux.Error.401' => 'Access Unauthorized', + 'API.Miniflux.Error.403' => 'Access Forbidden', + 'API.Miniflux.Error.404' => 'Resource Not Found', 'API.Miniflux.Error.invalidBodyJSON' => 'Invalid JSON payload: {0}', 'API.Miniflux.Error.invalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}', 'API.Miniflux.Error.fetch404' => 'Resource not found (404), this feed doesn\'t exists anymore, check the feed URL', From 5c8365554129fb64761690b2d3a1b076a5270a61 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 8 Dec 2020 16:10:23 -0500 Subject: [PATCH 063/366] Add modification timestamp to user metadata --- lib/Database.php | 4 ++-- sql/MySQL/6.sql | 1 + sql/PostgreSQL/6.sql | 1 + sql/SQLite3/6.sql | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index f961f7dd..b2a7aa38 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -322,8 +322,8 @@ class Database { } $tr = $this->begin(); $find = $this->db->prepare("SELECT count(*) from arsse_user_meta where owner = ? and \"key\" = ?", "str", "strict str"); - $update = $this->db->prepare("UPDATE arsse_user_meta set value = ? where owner = ? and \"key\" = ?", "str", "str", "str"); - $insert = $this->db->prepare("INSERT INTO arsse_user_meta values(?, ?, ?)", "str", "strict str", "str"); + $update = $this->db->prepare("UPDATE arsse_user_meta set value = ?, modified = CURRENT_TIMESTAMP where owner = ? and \"key\" = ?", "str", "str", "str"); + $insert = $this->db->prepare("INSERT INTO arsse_user_meta(owner, \"key\", value) values(?, ?, ?)", "str", "strict str", "str"); foreach ($data as $k => $v) { if ($k === "admin") { $this->db->prepare("UPDATE arsse_users SET admin = ? where id = ?", "bool", "str")->run($v, $user); diff --git a/sql/MySQL/6.sql b/sql/MySQL/6.sql index 23ba5edb..36b2d6e4 100644 --- a/sql/MySQL/6.sql +++ b/sql/MySQL/6.sql @@ -22,6 +22,7 @@ alter table arsse_users modify num bigint unsigned not null; create table arsse_user_meta( owner varchar(255) not null, "key" varchar(255) not null, + modified datetime(0) not null default CURRENT_TIMESTAMP, value longtext, foreign key(owner) references arsse_users(id) on delete cascade on update cascade, primary key(owner,"key") diff --git a/sql/PostgreSQL/6.sql b/sql/PostgreSQL/6.sql index 0b405a27..a32eb0c0 100644 --- a/sql/PostgreSQL/6.sql +++ b/sql/PostgreSQL/6.sql @@ -23,6 +23,7 @@ alter table arsse_users alter column num set not null; create table arsse_user_meta( owner text not null references arsse_users(id) on delete cascade on update cascade, key text not null, + modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, value text, primary key(owner,key) ); diff --git a/sql/SQLite3/6.sql b/sql/SQLite3/6.sql index 9e86182d..81e9e821 100644 --- a/sql/SQLite3/6.sql +++ b/sql/SQLite3/6.sql @@ -34,6 +34,7 @@ create table arsse_user_meta( -- It is up to individual applications (i.e. the client protocols) to cooperate with names and types owner text not null references arsse_users(id) on delete cascade on update cascade, -- the user to whom the metadata belongs key text not null, -- metadata key + modified text not null default CURRENT_TIMESTAMP, -- time at which the metadata was last changed value text, -- metadata value primary key(owner,key) ) without rowid; From 7c841b5fc2423b1ae0e8a65b853cdf6c3bd5dcad Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 9 Dec 2020 23:39:29 -0500 Subject: [PATCH 064/366] Test for listing users --- lib/REST/Miniflux/V1.php | 9 +++- tests/cases/REST/Miniflux/TestV1.php | 63 +++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 2f2685fc..5b1f51df 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -66,6 +66,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { public function __construct() { } + /** @codeCoverageIgnore */ + protected function now(): \DateTimeImmutable { + return Date::normalize("now"); + } + protected function authenticate(ServerRequestInterface $req): bool { // first check any tokens; this is what Miniflux does if ($req->hasHeader("X-Auth-Token")) { @@ -218,7 +223,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function listUsers(array $users, bool $reportMissing): array { $out = []; - $now = Date::transform("now", "iso8601m"); + $now = Date::transform($this->now(), "iso8601m"); foreach ($users as $u) { try { $info = Arsse::$user->propertiesGet($u, true); @@ -275,7 +280,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function getUserById(array $path, array $query, array $data) { try { - return $this->listUsers([$path[1]], true)[0] ?? []; + return new Response($this->listUsers([$path[1]], true)[0] ?? new \stdClass); } catch (UserException $e) { return new ErrorResponse("404", 404); } diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index e94d1450..401c1bf9 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -11,8 +11,10 @@ use JKingWeb\Arsse\User; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Db\Transaction; use JKingWeb\Arsse\Db\ExceptionInput; +use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\REST\Miniflux\V1; use JKingWeb\Arsse\REST\Miniflux\ErrorResponse; +use JKingWeb\Arsse\User\ExceptionConflict; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\EmptyResponse; @@ -41,7 +43,6 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { self::setConf(); // create a mock user manager Arsse::$user = \Phake::mock(User::class); - Arsse::$user->id = "john.doe@example.com"; // create a mock database interface Arsse::$db = \Phake::mock(Database::class); $this->transaction = \Phake::mock(Transaction::class); @@ -139,4 +140,64 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { $exp = new ErrorResponse("fetch404", 500); $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Missing"])); } + + public function testQueryUsers(): void { + $now = Date::normalize("now"); + $u = [ + ['num'=> 1, 'admin' => true, 'theme' => "custom", 'lang' => "fr_CA", 'tz' => "Asia/Gaza", 'sort_asc' => true, 'page_size' => 200, 'shortcuts' => false, 'reading_time' => false, 'swipe' => false, 'stylesheet' => "p {}"], + ['num'=> 2, 'admin' => false, 'theme' => null, 'lang' => null, 'tz' => null, 'sort_asc' => null, 'page_size' => null, 'shortcuts' => null, 'reading_time' => null, 'swipe' => null, 'stylesheet' => null], + new ExceptionConflict("doesNotExist"), + ]; + $exp = [ + [ + 'id' => 1, + 'username' => "john.doe@example.com", + 'is_admin' => true, + 'theme' => "custom", + 'language' => "fr_CA", + 'timezone' => "Asia/Gaza", + 'entry_sorting_direction' => "asc", + 'entries_per_page' => 200, + 'keyboard_shortcuts' => false, + 'show_reading_time' => false, + 'last_login_at' => Date::transform($now, "iso8601m"), + 'entry_swipe' => false, + 'extra' => [ + 'custom_css' => "p {}", + ], + ], + [ + 'id' => 2, + 'username' => "jane.doe@example.com", + 'is_admin' => false, + 'theme' => "light_serif", + 'language' => "en_US", + 'timezone' => "UTC", + 'entry_sorting_direction' => "desc", + 'entries_per_page' => 100, + 'keyboard_shortcuts' => true, + 'show_reading_time' => true, + 'last_login_at' => Date::transform($now, "iso8601m"), + 'entry_swipe' => true, + 'extra' => [ + 'custom_css' => "", + ], + ] + ]; + // FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("list")->willReturn(["john.doe@example.com", "jane.doe@example.com", "admin@example.com"]); + Arsse::$user->method("propertiesGet")->willReturnCallback(function(string $user, bool $includeLerge = true) use ($u) { + if ($user === "john.doe@example.com") { + return $u[0]; + } elseif ($user === "jane.doe@example.com") { + return $u[1]; + }else { + throw $u[2]; + } + }); + $this->h = $this->createPartialMock(V1::class, ["now"]); + $this->h->method("now")->willReturn($now); + $this->assertMessage(new Response($exp), $this->req("GET", "/users")); + } } From ebdfad535c0abe9704397ec51c666f9d24708f38 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 10 Dec 2020 20:08:00 -0500 Subject: [PATCH 065/366] More Miniflux user tests Also added user lookup functionality --- lib/Database.php | 9 +++++++++ lib/REST/Miniflux/V1.php | 7 ++++++- lib/User.php | 5 +++++ tests/cases/Database/SeriesUser.php | 11 +++++++++++ tests/cases/REST/Miniflux/TestV1.php | 22 +++++++++++++++++++++- tests/cases/User/TestUser.php | 9 +++++++++ 6 files changed, 61 insertions(+), 2 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index b2a7aa38..799968fb 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -245,6 +245,15 @@ class Database { return (bool) $this->db->prepare("SELECT count(*) from arsse_users where id = ?", "str")->run($user)->getValue(); } + /** Returns the username associated with a user number */ + public function userLookup(int $num): string { + $out = $this->db->prepare("SELECT id from arsse_users where num = ?", "int")->run($num)->getValue(); + if ($out === null) { + throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $num]); + } + return $out; + } + /** Adds a user to the database * * @param string $user The user to add diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 5b1f51df..1107b600 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -287,7 +287,12 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function getUserByNum(array $path, array $query, array $data) { - return $this->listUsers([Arsse::$user->id], false)[0] ?? []; + try { + $user = Arsse::$user->lookup((int) $path[1]); + return new Response($this->listUsers([$user], true)[0] ?? new \stdClass); + } catch (UserException $e) { + return new ErrorResponse("404", 404); + } } protected function getCurrentUser(array $path, array $query, array $data) { diff --git a/lib/User.php b/lib/User.php index bf457a95..5ab1b111 100644 --- a/lib/User.php +++ b/lib/User.php @@ -62,6 +62,11 @@ class User { return $this->u->userList(); } + public function lookup(int $num): string { + // the user number is always stored in the internal database, so the user driver is not called here + return Arsse::$db->userLookup($num); + } + public function add(string $user, ?string $password = null): string { // ensure the user name does not contain any U+003A COLON characters, as // this is incompatible with HTTP Basic authentication diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index 9ca140c7..7ed01822 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -169,4 +169,15 @@ trait SeriesUser { $this->assertException("doesNotExist", "User", "ExceptionConflict"); Arsse::$db->userPropertiesSet("john.doe@example.org", ['admin' => true]); } + + public function testLookUpAUserByNumber(): void { + $this->assertSame("admin@example.net", Arsse::$db->userLookup(1)); + $this->assertSame("jane.doe@example.com", Arsse::$db->userLookup(2)); + $this->assertSame("john.doe@example.com", Arsse::$db->userLookup(3)); + } + + public function testLookUpAMissingUserByNumber(): void { + $this->assertException("doesNotExist", "User", "ExceptionConflict"); + Arsse::$db->userLookup(2112); + } } diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 401c1bf9..065b2762 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -192,12 +192,32 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { return $u[0]; } elseif ($user === "jane.doe@example.com") { return $u[1]; - }else { + } else { + throw $u[2]; + } + }); + Arsse::$user->method("lookup")->willReturnCallback(function(int $num) use ($u) { + if ($num === 1) { + return "john.doe@example.com"; + } elseif ($num === 2) { + return "jane.doe@example.com"; + } else { throw $u[2]; } }); $this->h = $this->createPartialMock(V1::class, ["now"]); $this->h->method("now")->willReturn($now); + // list all users $this->assertMessage(new Response($exp), $this->req("GET", "/users")); + // fetch John + $this->assertMessage(new Response($exp[0]), $this->req("GET", "/me")); + $this->assertMessage(new Response($exp[0]), $this->req("GET", "/users/john.doe@example.com")); + $this->assertMessage(new Response($exp[0]), $this->req("GET", "/users/1")); + // fetch Jane + $this->assertMessage(new Response($exp[1]), $this->req("GET", "/users/jane.doe@example.com")); + $this->assertMessage(new Response($exp[1]), $this->req("GET", "/users/2")); + // fetch no one + $this->assertMessage(new ErrorResponse("404", 404), $this->req("GET", "/users/jack.doe@example.com")); + $this->assertMessage(new ErrorResponse("404", 404), $this->req("GET", "/users/47")); } } diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 84228cac..b7d1266e 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -90,6 +90,15 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify($this->drv)->userList(); } + public function testLookUpAUserByNumber(): void { + $exp = "john.doe@example.com"; + $u = new User($this->drv); + \Phake::when(Arsse::$db)->userLookup->thenReturn($exp); + $this->assertSame($exp, $u->lookup(2112)); + \Phake::verify(Arsse::$db)->userLookup(2112); + } + + public function testAddAUser(): void { $user = "john.doe@example.com"; $pass = "secret"; From 4b7369838113daade4b0fc871a1a852b4eaff8f2 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 10 Dec 2020 23:19:26 -0500 Subject: [PATCH 066/366] More user query tests --- tests/cases/REST/Miniflux/TestV1.php | 122 +++++++++++++++------------ 1 file changed, 68 insertions(+), 54 deletions(-) diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 065b2762..89316153 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -21,11 +21,49 @@ use Laminas\Diactoros\Response\EmptyResponse; /** @covers \JKingWeb\Arsse\REST\Miniflux\V1 */ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { + protected const NOW = "2020-12-09T22:35:10.023419Z"; + protected $h; protected $transaction; protected $token = "Tk2o9YubmZIL2fm2w8Z4KlDEQJz532fNSOcTG0s2_xc="; + protected $users = [ + [ + 'id' => 1, + 'username' => "john.doe@example.com", + 'is_admin' => true, + 'theme' => "custom", + 'language' => "fr_CA", + 'timezone' => "Asia/Gaza", + 'entry_sorting_direction' => "asc", + 'entries_per_page' => 200, + 'keyboard_shortcuts' => false, + 'show_reading_time' => false, + 'last_login_at' => self::NOW, + 'entry_swipe' => false, + 'extra' => [ + 'custom_css' => "p {}", + ], + ], + [ + 'id' => 2, + 'username' => "jane.doe@example.com", + 'is_admin' => false, + 'theme' => "light_serif", + 'language' => "en_US", + 'timezone' => "UTC", + 'entry_sorting_direction' => "desc", + 'entries_per_page' => 100, + 'keyboard_shortcuts' => true, + 'show_reading_time' => true, + 'last_login_at' => self::NOW, + 'entry_swipe' => true, + 'extra' => [ + 'custom_css' => "", + ], + ] + ]; - protected function req(string $method, string $target, $data = "", array $headers = [], bool $authenticated = true, bool $body = true): ResponseInterface { + protected function req(string $method, string $target, $data = "", array $headers = [], ?string $user = "john.doe@example.com", bool $body = true): ResponseInterface { $prefix = "/v1"; $url = $prefix.$target; if ($body) { @@ -34,7 +72,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { $params = $data; $data = []; } - $req = $this->serverRequest($method, $url, $prefix, $headers, [], $data, "application/json", $params, $authenticated ? "john.doe@example.com" : ""); + $req = $this->serverRequest($method, $url, $prefix, $headers, [], $data, "application/json", $params, $user); return $this->h->dispatch($req); } @@ -71,7 +109,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { Arsse::$user->id = null; \Phake::when(Arsse::$db)->tokenLookup->thenThrow(new ExceptionInput("subjectMissing")); \Phake::when(Arsse::$db)->tokenLookup("miniflux.login", $this->token)->thenReturn(['user' => $user]); - $this->assertMessage($exp, $this->req("GET", "/", "", $headers, $auth)); + $this->assertMessage($exp, $this->req("GET", "/", "", $headers, $auth ? "john.doe@example.com" : null)); $this->assertSame($success ? $user : null, Arsse::$user->id); } @@ -141,49 +179,14 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Missing"])); } - public function testQueryUsers(): void { - $now = Date::normalize("now"); + /** @dataProvider provideUserQueries */ + public function testQueryUsers(bool $admin, string $route, ResponseInterface $exp): void { $u = [ ['num'=> 1, 'admin' => true, 'theme' => "custom", 'lang' => "fr_CA", 'tz' => "Asia/Gaza", 'sort_asc' => true, 'page_size' => 200, 'shortcuts' => false, 'reading_time' => false, 'swipe' => false, 'stylesheet' => "p {}"], ['num'=> 2, 'admin' => false, 'theme' => null, 'lang' => null, 'tz' => null, 'sort_asc' => null, 'page_size' => null, 'shortcuts' => null, 'reading_time' => null, 'swipe' => null, 'stylesheet' => null], new ExceptionConflict("doesNotExist"), ]; - $exp = [ - [ - 'id' => 1, - 'username' => "john.doe@example.com", - 'is_admin' => true, - 'theme' => "custom", - 'language' => "fr_CA", - 'timezone' => "Asia/Gaza", - 'entry_sorting_direction' => "asc", - 'entries_per_page' => 200, - 'keyboard_shortcuts' => false, - 'show_reading_time' => false, - 'last_login_at' => Date::transform($now, "iso8601m"), - 'entry_swipe' => false, - 'extra' => [ - 'custom_css' => "p {}", - ], - ], - [ - 'id' => 2, - 'username' => "jane.doe@example.com", - 'is_admin' => false, - 'theme' => "light_serif", - 'language' => "en_US", - 'timezone' => "UTC", - 'entry_sorting_direction' => "desc", - 'entries_per_page' => 100, - 'keyboard_shortcuts' => true, - 'show_reading_time' => true, - 'last_login_at' => Date::transform($now, "iso8601m"), - 'entry_swipe' => true, - 'extra' => [ - 'custom_css' => "", - ], - ] - ]; + $user = $admin ? "john.doe@example.com" : "jane.doe@example.com"; // FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead Arsse::$user = $this->createMock(User::class); Arsse::$user->method("list")->willReturn(["john.doe@example.com", "jane.doe@example.com", "admin@example.com"]); @@ -206,18 +209,29 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } }); $this->h = $this->createPartialMock(V1::class, ["now"]); - $this->h->method("now")->willReturn($now); - // list all users - $this->assertMessage(new Response($exp), $this->req("GET", "/users")); - // fetch John - $this->assertMessage(new Response($exp[0]), $this->req("GET", "/me")); - $this->assertMessage(new Response($exp[0]), $this->req("GET", "/users/john.doe@example.com")); - $this->assertMessage(new Response($exp[0]), $this->req("GET", "/users/1")); - // fetch Jane - $this->assertMessage(new Response($exp[1]), $this->req("GET", "/users/jane.doe@example.com")); - $this->assertMessage(new Response($exp[1]), $this->req("GET", "/users/2")); - // fetch no one - $this->assertMessage(new ErrorResponse("404", 404), $this->req("GET", "/users/jack.doe@example.com")); - $this->assertMessage(new ErrorResponse("404", 404), $this->req("GET", "/users/47")); + $this->h->method("now")->willReturn(Date::normalize(self::NOW)); + $this->assertMessage($exp, $this->req("GET", $route, "", [], $user)); + } + + public function provideUserQueries(): iterable { + self::clearData(); + return [ + [true, "/users", new Response($this->users)], + [true, "/me", new Response($this->users[0])], + [true, "/users/john.doe@example.com", new Response($this->users[0])], + [true, "/users/1", new Response($this->users[0])], + [true, "/users/jane.doe@example.com", new Response($this->users[1])], + [true, "/users/2", new Response($this->users[1])], + [true, "/users/jack.doe@example.com", new ErrorResponse("404", 404)], + [true, "/users/47", new ErrorResponse("404", 404)], + [false, "/users", new ErrorResponse("403", 403)], + [false, "/me", new Response($this->users[1])], + [false, "/users/john.doe@example.com", new ErrorResponse("403", 403)], + [false, "/users/1", new ErrorResponse("403", 403)], + [false, "/users/jane.doe@example.com", new ErrorResponse("403", 403)], + [false, "/users/2", new ErrorResponse("403", 403)], + [false, "/users/jack.doe@example.com", new ErrorResponse("403", 403)], + [false, "/users/47", new ErrorResponse("403", 403)], + ]; } } From 2e6c5d2ad23e41269762fd356df9ce1f0740e1f8 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 11 Dec 2020 13:31:35 -0500 Subject: [PATCH 067/366] Query Miniflux categories --- lib/REST/Miniflux/V1.php | 25 +++++++++++++++++++------ lib/User.php | 21 +++++++++++---------- locale/en.php | 13 +++++++------ tests/cases/REST/Miniflux/TestV1.php | 4 ++-- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 1107b600..ec82dcaa 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -128,7 +128,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $data = @json_decode((string) $req->getBody(), true); if (json_last_error() !== \JSON_ERROR_NONE) { // if the body could not be parsed as JSON, return "400 Bad Request" - return new ErrorResponse(["invalidBodyJSON", json_last_error_msg()], 400); + return new ErrorResponse(["InvalidBodyJSON", json_last_error_msg()], 400); } $data = $this->normalizeBody((array) $data); if ($data instanceof ResponseInterface) { @@ -215,7 +215,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if (!isset($body[$k])) { $body[$k] = null; } elseif (gettype($body[$k]) !== $t) { - return new ErrorResponse(["invalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])]); + return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])]); } } return $body; @@ -260,10 +260,10 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $list = Feed::discoverAll((string) $data['url'], (string) $data['username'], (string) $data['password']); } catch (FeedException $e) { $msg = [ - 10502 => "fetch404", - 10506 => "fetch403", - 10507 => "fetch401", - ][$e->getCode()] ?? "fetchOther"; + 10502 => "Fetch404", + 10506 => "Fetch403", + 10507 => "Fetch401", + ][$e->getCode()] ?? "FetchOther"; return new ErrorResponse($msg, 500); } $out = []; @@ -299,6 +299,19 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($this->listUsers([Arsse::$user->id], false)[0] ?? new \stdClass); } + protected function getCategories(array $path, array $query, array $data) { + $out = []; + $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); + // add the root folder as a category + $out[] = ['id' => 1, 'title' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName"), 'user_id' => $meta['num']]; + // add other top folders as categories + foreach (Arsse::$db->folderList(Arsse::$user->id, null, false) as $f) { + // always add 1 to the ID since the root folder will always be 1 instead of 0. + $out[] = ['id' => $f['id'] + 1, 'title' => $f['name'], 'user_id' => $meta['num']]; + } + return new Response($out); + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokens in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); diff --git a/lib/User.php b/lib/User.php index 5ab1b111..df9a49d9 100644 --- a/lib/User.php +++ b/lib/User.php @@ -15,16 +15,17 @@ class User { 'internal' => \JKingWeb\Arsse\User\Internal\Driver::class, ]; public const PROPERTIES = [ - 'admin' => V::T_BOOL, - 'lang' => V::T_STRING, - 'tz' => V::T_STRING, - 'sort_asc' => V::T_BOOL, - 'theme' => V::T_STRING, - 'page_size' => V::T_INT, // greater than zero - 'shortcuts' => V::T_BOOL, - 'gestures' => V::T_BOOL, - 'stylesheet' => V::T_STRING, - 'reading_time' => V::T_BOOL, + 'admin' => V::T_BOOL, + 'lang' => V::T_STRING, + 'tz' => V::T_STRING, + 'sort_asc' => V::T_BOOL, + 'theme' => V::T_STRING, + 'page_size' => V::T_INT, // greater than zero + 'shortcuts' => V::T_BOOL, + 'gestures' => V::T_BOOL, + 'stylesheet' => V::T_STRING, + 'reading_time' => V::T_BOOL, + 'root_folder_name' => V::T_STRING, ]; public const PROPERTIES_LARGE = ["stylesheet"]; diff --git a/locale/en.php b/locale/en.php index 75b52e5f..0e03fd97 100644 --- a/locale/en.php +++ b/locale/en.php @@ -7,15 +7,16 @@ return [ 'CLI.Auth.Success' => 'Authentication successful', 'CLI.Auth.Failure' => 'Authentication failed', + 'API.Miniflux.DefaultCategoryName' => "All", 'API.Miniflux.Error.401' => 'Access Unauthorized', 'API.Miniflux.Error.403' => 'Access Forbidden', 'API.Miniflux.Error.404' => 'Resource Not Found', - 'API.Miniflux.Error.invalidBodyJSON' => 'Invalid JSON payload: {0}', - 'API.Miniflux.Error.invalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}', - 'API.Miniflux.Error.fetch404' => 'Resource not found (404), this feed doesn\'t exists anymore, check the feed URL', - 'API.Miniflux.Error.fetch401' => 'You are not authorized to access this resource (invalid username/password)', - 'API.Miniflux.Error.fetch403' => 'Unable to fetch this resource (Status Code = 403)', - 'API.Miniflux.Error.fetchOther' => 'Unable to fetch this resource', + 'API.Miniflux.Error.InvalidBodyJSON' => 'Invalid JSON payload: {0}', + 'API.Miniflux.Error.InvalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}', + 'API.Miniflux.Error.Fetch404' => 'Resource not found (404), this feed doesn\'t exists anymore, check the feed URL', + 'API.Miniflux.Error.Fetch401' => 'You are not authorized to access this resource (invalid username/password)', + 'API.Miniflux.Error.Fetch403' => 'Unable to fetch this resource (Status Code = 403)', + 'API.Miniflux.Error.FetchOther' => 'Unable to fetch this resource', 'API.TTRSS.Category.Uncategorized' => 'Uncategorized', 'API.TTRSS.Category.Special' => 'Special', diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 89316153..4ac27344 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -163,7 +163,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } public function testRejectBadlyTypedData(): void { - $exp = new ErrorResponse(["invalidInputType", 'field' => "url", 'expected' => "string", 'actual' => "integer"], 400); + $exp = new ErrorResponse(["InvalidInputType", 'field' => "url", 'expected' => "string", 'actual' => "integer"], 400); $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => 2112])); } @@ -175,7 +175,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Valid"])); $exp = new Response([]); $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Invalid"])); - $exp = new ErrorResponse("fetch404", 500); + $exp = new ErrorResponse("Fetch404", 500); $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Missing"])); } From 3ebb46f48e79fadfc4e0c70bb76a35c1d5f9aecf Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 11 Dec 2020 23:47:13 -0500 Subject: [PATCH 068/366] Some work on categories --- .../030_Supported_Protocols/005_Miniflux.md | 15 ++- lib/REST/Miniflux/V1.php | 95 ++++++++++++++----- locale/en.php | 4 +- .../cases/REST/Miniflux/TestErrorResponse.php | 2 +- tests/cases/REST/Miniflux/TestV1.php | 53 ++++++++++- 5 files changed, 135 insertions(+), 34 deletions(-) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index 04e53e2c..0d5792e9 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -13,9 +13,9 @@
API Reference
-The Miniflux protocol is a well-designed protocol supporting a wide variety of operations on newsfeeds, folders (termed "categories"), and articles; it also allows for user administration, and native OPML importing and exporting. Architecturally it is similar to the Nextcloud News protocol, but is generally more efficient. +The Miniflux protocol is a fairly well-designed protocol supporting a wide variety of operations on newsfeeds, folders (termed "categories"), and articles; it also allows for user administration, and native OPML importing and exporting. Architecturally it is similar to the Nextcloud News protocol, but is generally more efficient and has more capabilities. -Miniflux version 2.0.25 is emulated, though not all features are implemented +Miniflux version 2.0.26 is emulated, though not all features are implemented # Missing features @@ -28,8 +28,15 @@ Miniflux version 2.0.25 is emulated, though not all features are implemented # Differences +- Various error messages differ due to significant implementation differences - Only the URL should be considered reliable in feed discovery results +- The "All" category is treated specially (see below for details) +- Category names consisting only of whitespace are rejected along with the empty string -# Interaction with nested folders +# Special handling of the "All" category -Tiny Tiny RSS is unique in allowing newsfeeds to be grouped into folders nested to arbitrary depth. When newsfeeds are placed into nested folders, they simply appear in the top-level folder when accessed via the Miniflux protocol. This does not affect OPML exports, where full nesting is preserved. +Nextcloud News' root folder and Tiny Tiny RSS' "Uncategorized" catgory are mapped to Miniflux's initial "All" category. This Miniflux category can be renamed, but it cannot be deleted. Attempting to do so will delete the child feeds it contains, but not the category itself. + +# Interaction with nested categories + +Tiny Tiny RSS is unique in allowing newsfeeds to be grouped into categories nested to arbitrary depth. When newsfeeds are placed into nested categories, they simply appear in the top-level category when accessed via the Miniflux protocol. This does not affect OPML exports, where full nesting is preserved. diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index ec82dcaa..fd6baa9c 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -32,27 +32,31 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'username' => "string", 'password' => "string", 'user_agent' => "string", + 'title' => "string", ]; protected const PATHS = [ - '/categories' => ['GET' => "getCategories", 'POST' => "createCategory"], - '/categories/1' => ['PUT' => "updateCategory", 'DELETE' => "deleteCategory"], - '/discover' => ['POST' => "discoverSubscriptions"], - '/entries' => ['GET' => "getEntries", 'PUT' => "updateEntries"], - '/entries/1' => ['GET' => "getEntry"], - '/entries/1/bookmark' => ['PUT' => "toggleEntryBookmark"], - '/export' => ['GET' => "opmlExport"], - '/feeds' => ['GET' => "getFeeds", 'POST' => "createFeed"], - '/feeds/1' => ['GET' => "getFeed", 'PUT' => "updateFeed", 'DELETE' => "removeFeed"], - '/feeds/1/entries/1' => ['GET' => "getFeedEntry"], - '/feeds/1/entries' => ['GET' => "getFeedEntries"], - '/feeds/1/icon' => ['GET' => "getFeedIcon"], - '/feeds/1/refresh' => ['PUT' => "refreshFeed"], - '/feeds/refresh' => ['PUT' => "refreshAllFeeds"], - '/import' => ['POST' => "opmlImport"], - '/me' => ['GET' => "getCurrentUser"], - '/users' => ['GET' => "getUsers", 'POST' => "createUser"], - '/users/1' => ['GET' => "getUserByNum", 'PUT' => "updateUserByNum", 'DELETE' => "deleteUser"], - '/users/*' => ['GET' => "getUserById"], + '/categories' => ['GET' => "getCategories", 'POST' => "createCategory"], + '/categories/1' => ['PUT' => "updateCategory", 'DELETE' => "deleteCategory"], + '/categories/1/mark-all-as-read' => ['PUT' => "markCategory"], + '/discover' => ['POST' => "discoverSubscriptions"], + '/entries' => ['GET' => "getEntries", 'PUT' => "updateEntries"], + '/entries/1' => ['GET' => "getEntry"], + '/entries/1/bookmark' => ['PUT' => "toggleEntryBookmark"], + '/export' => ['GET' => "opmlExport"], + '/feeds' => ['GET' => "getFeeds", 'POST' => "createFeed"], + '/feeds/1' => ['GET' => "getFeed", 'PUT' => "updateFeed", 'DELETE' => "removeFeed"], + '/feeds/1/mark-all-as-read' => ['PUT' => "markFeed"], + '/feeds/1/entries/1' => ['GET' => "getFeedEntry"], + '/feeds/1/entries' => ['GET' => "getFeedEntries"], + '/feeds/1/icon' => ['GET' => "getFeedIcon"], + '/feeds/1/refresh' => ['PUT' => "refreshFeed"], + '/feeds/refresh' => ['PUT' => "refreshAllFeeds"], + '/import' => ['POST' => "opmlImport"], + '/me' => ['GET' => "getCurrentUser"], + '/users' => ['GET' => "getUsers", 'POST' => "createUser"], + '/users/1' => ['GET' => "getUserByNum", 'PUT' => "updateUserByNum", 'DELETE' => "deleteUser"], + '/users/1/mark-all-as-read' => ['PUT' => "markAll"], + '/users/*' => ['GET' => "getUserById"], ]; protected const ADMIN_FUNCTIONS = [ 'getUsers' => true, @@ -85,7 +89,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return true; } } - // next check HTTP auth + // next check HTTP auth if ($req->getAttribute("authenticated", false)) { Arsse::$user->id = $req->getAttribute("authenticatedUser"); return true; @@ -255,7 +259,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return $out; } - protected function discoverSubscriptions(array $path, array $query, array $data) { + protected function discoverSubscriptions(array $path, array $query, array $data): ResponseInterface { try { $list = Feed::discoverAll((string) $data['url'], (string) $data['username'], (string) $data['password']); } catch (FeedException $e) { @@ -274,11 +278,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($out); } - protected function getUsers(array $path, array $query, array $data) { + protected function getUsers(array $path, array $query, array $data): ResponseInterface { return new Response($this->listUsers(Arsse::$user->list(), false)); } - protected function getUserById(array $path, array $query, array $data) { + protected function getUserById(array $path, array $query, array $data): ResponseInterface { try { return new Response($this->listUsers([$path[1]], true)[0] ?? new \stdClass); } catch (UserException $e) { @@ -286,7 +290,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } - protected function getUserByNum(array $path, array $query, array $data) { + protected function getUserByNum(array $path, array $query, array $data): ResponseInterface { try { $user = Arsse::$user->lookup((int) $path[1]); return new Response($this->listUsers([$user], true)[0] ?? new \stdClass); @@ -295,11 +299,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } - protected function getCurrentUser(array $path, array $query, array $data) { + protected function getCurrentUser(array $path, array $query, array $data): ResponseInterface { return new Response($this->listUsers([Arsse::$user->id], false)[0] ?? new \stdClass); } - protected function getCategories(array $path, array $query, array $data) { + protected function getCategories(array $path, array $query, array $data): ResponseInterface { $out = []; $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); // add the root folder as a category @@ -312,6 +316,45 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($out); } + protected function createCategory(array $path, array $query, array $data): ResponseInterface { + try { + $id = Arsse::$db->folderAdd(Arsse::$user->id, ['name' => (string) $data['title']]); + } catch (ExceptionInput $e) { + if ($e->getCode() === 10236) { + return new ErrorResponse(["DuplicateCategory", 'title' => $data['title']], 500); + } else { + return new ErrorResponse(["InvalidCategory", 'title' => $data['title']], 500); + } + } + $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); + return new Response(['id' => $id + 1, 'title' => $data['title'], 'user_id' => $meta['num']]); + } + + protected function updateCategory(array $path, array $query, array $data): ResponseInterface { + $folder = $path[1] - 1; + $title = $data['title'] ?? ""; + try { + if ($folder === 0) { + if (!strlen(trim($title))) { + throw new ExceptionInput("whitespace"); + } + $title = Arsse::$user->propertiesSet(Arsse::$user->id, ['root_folder_name' => $title])['root_folder_name']; + } else { + Arsse::$db->folderPropertiesSet(Arsse::$user->id, $folder, ['name' => $title]); + } + } catch (ExceptionInput $e) { + if ($e->getCode() === 10236) { + return new ErrorResponse(["DuplicateCategory", 'title' => $title], 500); + } elseif ($e->getCode === 10239) { + return new ErrorResponse("404", 404); + } else { + return new ErrorResponse(["InvalidCategory", 'title' => $title], 500); + } + } + $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); + return new Response(['id' => (int) $path[1], 'title' => $title, 'user_id' => $meta['num']]); + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokens in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); diff --git a/locale/en.php b/locale/en.php index 0e03fd97..acdaa601 100644 --- a/locale/en.php +++ b/locale/en.php @@ -17,7 +17,9 @@ return [ 'API.Miniflux.Error.Fetch401' => 'You are not authorized to access this resource (invalid username/password)', 'API.Miniflux.Error.Fetch403' => 'Unable to fetch this resource (Status Code = 403)', 'API.Miniflux.Error.FetchOther' => 'Unable to fetch this resource', - + 'API.Miniflux.Error.DuplicateCategory' => 'Category "{title}" already exists', + 'API.Miniflux.Error.InvalidCategory' => 'Invalid category title "{title}"', + 'API.TTRSS.Category.Uncategorized' => 'Uncategorized', 'API.TTRSS.Category.Special' => 'Special', 'API.TTRSS.Category.Labels' => 'Labels', diff --git a/tests/cases/REST/Miniflux/TestErrorResponse.php b/tests/cases/REST/Miniflux/TestErrorResponse.php index 23d6e286..5852b4d0 100644 --- a/tests/cases/REST/Miniflux/TestErrorResponse.php +++ b/tests/cases/REST/Miniflux/TestErrorResponse.php @@ -16,7 +16,7 @@ class TestErrorResponse extends \JKingWeb\Arsse\Test\AbstractTest { } public function testCreateVariableResponse(): void { - $act = new ErrorResponse(["invalidBodyJSON", "Doh!"], 401); + $act = new ErrorResponse(["InvalidBodyJSON", "Doh!"], 401); $this->assertSame('{"error_message":"Invalid JSON payload: Doh!"}', (string) $act->getBody()); } } diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 4ac27344..02599480 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -18,6 +18,7 @@ use JKingWeb\Arsse\User\ExceptionConflict; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\EmptyResponse; +use JKingWeb\Arsse\Test\Result; /** @covers \JKingWeb\Arsse\REST\Miniflux\V1 */ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { @@ -79,8 +80,9 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp(): void { self::clearData(); self::setConf(); - // create a mock user manager - Arsse::$user = \Phake::mock(User::class); + // create a mock user manager; we use a PHPUnitmock because Phake for reasons unknown is unable to mock the User class correctly, sometimes + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("propertiesGet")->willReturn(['num' => 42, 'admin' => false, 'root_folder_name' => null]); // create a mock database interface Arsse::$db = \Phake::mock(Database::class); $this->transaction = \Phake::mock(Transaction::class); @@ -234,4 +236,51 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { [false, "/users/47", new ErrorResponse("403", 403)], ]; } + + public function testListCategories(): void { + \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ + ['id' => 1, 'name' => "Science"], + ['id' => 20, 'name' => "Technology"], + ]))); + $exp = new Response([ + ['id' => 1, 'title' => "All", 'user_id' => 42], + ['id' => 2, 'title' => "Science", 'user_id' => 42], + ['id' => 21, 'title' => "Technology", 'user_id' => 42], + ]); + $this->assertMessage($exp, $this->req("GET", "/categories")); + \Phake::verify(Arsse::$db)->folderList("john.doe@example.com", null, false); + // run test again with a renamed root folder + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("propertiesGet")->willReturn(['num' => 47, 'admin' => false, 'root_folder_name' => "Uncategorized"]); + $exp = new Response([ + ['id' => 1, 'title' => "Uncategorized", 'user_id' => 47], + ['id' => 2, 'title' => "Science", 'user_id' => 47], + ['id' => 21, 'title' => "Technology", 'user_id' => 47], + ]); + $this->assertMessage($exp, $this->req("GET", "/categories")); + } + + /** @dataProvider provideCategoryAdditions */ + public function testAddACategory($title, ResponseInterface $exp): void { + if (!strlen((string) $title)) { + \Phake::when(Arsse::$db)->folderAdd->thenThrow(new ExceptionInput("missing")); + } elseif (!strlen(trim((string) $title))) { + \Phake::when(Arsse::$db)->folderAdd->thenThrow(new ExceptionInput("whitespace")); + } elseif ($title === "Duplicate") { + \Phake::when(Arsse::$db)->folderAdd->thenThrow(new ExceptionInput("constraintViolation")); + } else { + \Phake::when(Arsse::$db)->folderAdd->thenReturn(2111); + } + $this->assertMessage($exp, $this->req("POST", "/categories", ['title' => $title])); + } + + public function provideCategoryAdditions(): iterable { + return [ + ["New", new Response(['id' => 2112, 'title' => "New", 'user_id' => 42])], + ["Duplicate", new ErrorResponse(["DuplicateCategory", 'title' => "Duplicate"], 500)], + ["", new ErrorResponse(["InvalidCategory", 'title' => ""], 500)], + [" ", new ErrorResponse(["InvalidCategory", 'title' => " "], 500)], + [false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 400)], + ]; + } } From eb079166de569b0f91c8491985a2840c276083af Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 13 Dec 2020 12:56:57 -0500 Subject: [PATCH 069/366] Tests for category renaming --- .../030_Supported_Protocols/005_Miniflux.md | 1 + lib/REST/Miniflux/V1.php | 4 +- tests/cases/REST/Miniflux/TestV1.php | 40 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index 0d5792e9..84e7d283 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -29,6 +29,7 @@ Miniflux version 2.0.26 is emulated, though not all features are implemented # Differences - Various error messages differ due to significant implementation differences +- `PUT` requests which return a body respond with `200 OK` rather than `201 Created` - Only the URL should be considered reliable in feed discovery results - The "All" category is treated specially (see below for details) - Category names consisting only of whitespace are rejected along with the empty string diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index fd6baa9c..d84a7e9b 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -331,10 +331,12 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function updateCategory(array $path, array $query, array $data): ResponseInterface { + // category IDs in Miniflux are always greater than 1; we have folder 0, so we decrement category IDs by 1 to get the folder ID $folder = $path[1] - 1; $title = $data['title'] ?? ""; try { if ($folder === 0) { + // folder 0 doesn't actually exist in the database, so its name is kept as user metadata if (!strlen(trim($title))) { throw new ExceptionInput("whitespace"); } @@ -345,7 +347,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } catch (ExceptionInput $e) { if ($e->getCode() === 10236) { return new ErrorResponse(["DuplicateCategory", 'title' => $title], 500); - } elseif ($e->getCode === 10239) { + } elseif ($e->getCode() === 10239) { return new ErrorResponse("404", 404); } else { return new ErrorResponse(["InvalidCategory", 'title' => $title], 500); diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 02599480..f9bb423c 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -283,4 +283,44 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { [false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 400)], ]; } + + /** @dataProvider provideCategoryUpdates */ + public function testRenameACategory(int $id, $title, ResponseInterface $exp): void { + Arsse::$user->method("propertiesSet")->willReturn(['root_folder_name' => $title]); + if (!in_array($id, [1,2])) { + \Phake::when(Arsse::$db)->folderPropertiesSet->thenThrow(new ExceptionInput("subjectMissing")); + } elseif (!strlen((string) $title)) { + \Phake::when(Arsse::$db)->folderPropertiesSet->thenThrow(new ExceptionInput("missing")); + } elseif (!strlen(trim((string) $title))) { + \Phake::when(Arsse::$db)->folderPropertiesSet->thenThrow(new ExceptionInput("whitespace")); + } elseif ($title === "Duplicate") { + \Phake::when(Arsse::$db)->folderPropertiesSet->thenThrow(new ExceptionInput("constraintViolation")); + } else { + \Phake::when(Arsse::$db)->folderPropertiesSet->thenReturn(true); + } + if ($id === 1) { + $times = (int) (is_string($title) && strlen(trim($title))); + Arsse::$user->expects($this->exactly($times))->method("propertiesSet")->with("john.doe@example.com", ['root_folder_name' => $title]); + } + $this->assertMessage($exp, $this->req("PUT", "/categories/$id", ['title' => $title])); + if ($id !== 1 && is_string($title)) { + \Phake::verify(Arsse::$db)->folderPropertiesSet("john.doe@example.com", $id - 1, ['name' => $title]); + } + } + + public function provideCategoryUpdates(): iterable { + return [ + [3, "New", new ErrorResponse("404", 404)], + [2, "New", new Response(['id' => 2, 'title' => "New", 'user_id' => 42])], + [2, "Duplicate", new ErrorResponse(["DuplicateCategory", 'title' => "Duplicate"], 500)], + [2, "", new ErrorResponse(["InvalidCategory", 'title' => ""], 500)], + [2, " ", new ErrorResponse(["InvalidCategory", 'title' => " "], 500)], + [2, false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 400)], + [1, "New", new Response(['id' => 1, 'title' => "New", 'user_id' => 42])], + [1, "Duplicate", new Response(['id' => 1, 'title' => "Duplicate", 'user_id' => 42])], // This is allowed because the name of the root folder is only a duplicate in circumstances where it is used + [1, "", new ErrorResponse(["InvalidCategory", 'title' => ""], 500)], + [1, " ", new ErrorResponse(["InvalidCategory", 'title' => " "], 500)], + [1, false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 400)], + ]; + } } From 5124f76b70e9ea9323f046b1d2c3b1a64ebe750c Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 13 Dec 2020 22:10:34 -0500 Subject: [PATCH 070/366] Implementcategory deletion --- lib/REST/Miniflux/V1.php | 22 ++++++++- tests/cases/REST/Miniflux/TestV1.php | 73 +++++++++++++++++----------- tests/lib/AbstractTest.php | 6 ++- 3 files changed, 70 insertions(+), 31 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index d84a7e9b..19f0d0b9 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -347,7 +347,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } catch (ExceptionInput $e) { if ($e->getCode() === 10236) { return new ErrorResponse(["DuplicateCategory", 'title' => $title], 500); - } elseif ($e->getCode() === 10239) { + } elseif (in_array($e->getCode(), [10237, 10239])) { return new ErrorResponse("404", 404); } else { return new ErrorResponse(["InvalidCategory", 'title' => $title], 500); @@ -357,6 +357,26 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(['id' => (int) $path[1], 'title' => $title, 'user_id' => $meta['num']]); } + protected function deleteCategory(array $path, array $query, array $data): ResponseInterface { + try { + $folder = $path[1] - 1; + if ($folder !== 0) { + Arsse::$db->folderRemove(Arsse::$user->id, $folder); + } else { + // if we're deleting from the root folder, delete each child subscription individually + // otherwise we'd be deleting the entire tree + $tr = Arsse::$db->begin(); + foreach (Arsse::$db->subscriptionList(Arsse::$user->id, null, false) as $sub) { + Arsse::$db->subscriptionRemove(Arsse::$user->id, $sub['id']); + } + $tr->commit(); + } + } catch (ExceptionInput $e) { + return new ErrorResponse("404", 404); + } + return new EmptyResponse(204); + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokens in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index f9bb423c..e2f1033f 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -285,42 +285,59 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideCategoryUpdates */ - public function testRenameACategory(int $id, $title, ResponseInterface $exp): void { + public function testRenameACategory(int $id, $title, $out, ResponseInterface $exp): void { Arsse::$user->method("propertiesSet")->willReturn(['root_folder_name' => $title]); - if (!in_array($id, [1,2])) { - \Phake::when(Arsse::$db)->folderPropertiesSet->thenThrow(new ExceptionInput("subjectMissing")); - } elseif (!strlen((string) $title)) { - \Phake::when(Arsse::$db)->folderPropertiesSet->thenThrow(new ExceptionInput("missing")); - } elseif (!strlen(trim((string) $title))) { - \Phake::when(Arsse::$db)->folderPropertiesSet->thenThrow(new ExceptionInput("whitespace")); - } elseif ($title === "Duplicate") { - \Phake::when(Arsse::$db)->folderPropertiesSet->thenThrow(new ExceptionInput("constraintViolation")); + if (is_string($out)) { + \Phake::when(Arsse::$db)->folderPropertiesSet->thenThrow(new ExceptionInput($out)); } else { - \Phake::when(Arsse::$db)->folderPropertiesSet->thenReturn(true); - } - if ($id === 1) { - $times = (int) (is_string($title) && strlen(trim($title))); - Arsse::$user->expects($this->exactly($times))->method("propertiesSet")->with("john.doe@example.com", ['root_folder_name' => $title]); + \Phake::when(Arsse::$db)->folderPropertiesSet->thenReturn($out); } + $times = (int) ($id === 1 && is_string($title) && strlen(trim($title))); + Arsse::$user->expects($this->exactly($times))->method("propertiesSet")->with("john.doe@example.com", ['root_folder_name' => $title]); $this->assertMessage($exp, $this->req("PUT", "/categories/$id", ['title' => $title])); - if ($id !== 1 && is_string($title)) { - \Phake::verify(Arsse::$db)->folderPropertiesSet("john.doe@example.com", $id - 1, ['name' => $title]); - } + $times = (int) ($id !== 1 && is_string($title)); + \Phake::verify(Arsse::$db, \Phake::times($times))->folderPropertiesSet("john.doe@example.com", $id - 1, ['name' => $title]); } public function provideCategoryUpdates(): iterable { return [ - [3, "New", new ErrorResponse("404", 404)], - [2, "New", new Response(['id' => 2, 'title' => "New", 'user_id' => 42])], - [2, "Duplicate", new ErrorResponse(["DuplicateCategory", 'title' => "Duplicate"], 500)], - [2, "", new ErrorResponse(["InvalidCategory", 'title' => ""], 500)], - [2, " ", new ErrorResponse(["InvalidCategory", 'title' => " "], 500)], - [2, false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 400)], - [1, "New", new Response(['id' => 1, 'title' => "New", 'user_id' => 42])], - [1, "Duplicate", new Response(['id' => 1, 'title' => "Duplicate", 'user_id' => 42])], // This is allowed because the name of the root folder is only a duplicate in circumstances where it is used - [1, "", new ErrorResponse(["InvalidCategory", 'title' => ""], 500)], - [1, " ", new ErrorResponse(["InvalidCategory", 'title' => " "], 500)], - [1, false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 400)], + [3, "New", "subjectMissing", new ErrorResponse("404", 404)], + [2, "New", true, new Response(['id' => 2, 'title' => "New", 'user_id' => 42])], + [2, "Duplicate", "constraintViolation", new ErrorResponse(["DuplicateCategory", 'title' => "Duplicate"], 500)], + [2, "", "missing", new ErrorResponse(["InvalidCategory", 'title' => ""], 500)], + [2, " ", "whitespace", new ErrorResponse(["InvalidCategory", 'title' => " "], 500)], + [2, false, "subjectMissing", new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 400)], + [1, "New", true, new Response(['id' => 1, 'title' => "New", 'user_id' => 42])], + [1, "Duplicate", "constraintViolation", new Response(['id' => 1, 'title' => "Duplicate", 'user_id' => 42])], // This is allowed because the name of the root folder is only a duplicate in circumstances where it is used + [1, "", "missing", new ErrorResponse(["InvalidCategory", 'title' => ""], 500)], + [1, " ", "whitespace", new ErrorResponse(["InvalidCategory", 'title' => " "], 500)], + [1, false, false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 400)], ]; } + + public function testDeleteARealCategory(): void { + \Phake::when(Arsse::$db)->folderRemove->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing")); + $this->assertMessage(new EmptyResponse(204), $this->req("DELETE", "/categories/2112")); + \Phake::verify(Arsse::$db)->folderRemove("john.doe@example.com", 2111); + $this->assertMessage(new ErrorResponse("404", 404), $this->req("DELETE", "/categories/47")); + \Phake::verify(Arsse::$db)->folderRemove("john.doe@example.com", 46); + } + + public function testDeleteTheSpecialCategory(): void { + \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v([ + ['id' => 1], + ['id' => 47], + ['id' => 2112], + ]))); + \Phake::when(Arsse::$db)->subscriptionRemove->thenReturn(true); + $this->assertMessage(new EmptyResponse(204), $this->req("DELETE", "/categories/1")); + \Phake::inOrder( + \Phake::verify(Arsse::$db)->begin(), + \Phake::verify(Arsse::$db)->subscriptionList("john.doe@example.com", null, false), + \Phake::verify(Arsse::$db)->subscriptionRemove("john.doe@example.com", 1), + \Phake::verify(Arsse::$db)->subscriptionRemove("john.doe@example.com", 47), + \Phake::verify(Arsse::$db)->subscriptionRemove("john.doe@example.com", 2112), + \Phake::verify($this->transaction)->commit() + ); + } } diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index ed172430..54cde189 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -154,7 +154,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { protected function assertMessage(MessageInterface $exp, MessageInterface $act, string $text = ''): void { if ($exp instanceof ResponseInterface) { $this->assertInstanceOf(ResponseInterface::class, $act, $text); - $this->assertEquals($exp->getStatusCode(), $act->getStatusCode(), $text); + $this->assertSame($exp->getStatusCode(), $act->getStatusCode(), $text); } elseif ($exp instanceof RequestInterface) { if ($exp instanceof ServerRequestInterface) { $this->assertInstanceOf(ServerRequestInterface::class, $act, $text); @@ -165,12 +165,14 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { $this->assertSame($exp->getRequestTarget(), $act->getRequestTarget(), $text); } if ($exp instanceof JsonResponse) { + $this->assertInstanceOf(JsonResponse::class, $act, $text); $this->assertEquals($exp->getPayload(), $act->getPayload(), $text); $this->assertSame($exp->getPayload(), $act->getPayload(), $text); } elseif ($exp instanceof XmlResponse) { + $this->assertInstanceOf(XmlResponse::class, $act, $text); $this->assertXmlStringEqualsXmlString((string) $exp->getBody(), (string) $act->getBody(), $text); } else { - $this->assertEquals((string) $exp->getBody(), (string) $act->getBody(), $text); + $this->assertSame((string) $exp->getBody(), (string) $act->getBody(), $text); } $this->assertEquals($exp->getHeaders(), $act->getHeaders(), $text); } From 95a2018e755997af6d3fe484ba3e1bc42a670072 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 14 Dec 2020 12:41:09 -0500 Subject: [PATCH 071/366] Implement caategory marking as read --- lib/REST/Miniflux/V1.php | 270 +++++++++++++++++---------- lib/REST/NextcloudNews/V1_2.php | 1 - tests/cases/REST/Miniflux/TestV1.php | 13 ++ 3 files changed, 185 insertions(+), 99 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 19f0d0b9..99c6a9b2 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -10,6 +10,7 @@ use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Feed; use JKingWeb\Arsse\Feed\Exception as FeedException; use JKingWeb\Arsse\AbstractException; +use JKingWeb\Arsse\Context\Context; use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Misc\HTTP; use JKingWeb\Arsse\Misc\Date; @@ -34,37 +35,82 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'user_agent' => "string", 'title' => "string", ]; - protected const PATHS = [ - '/categories' => ['GET' => "getCategories", 'POST' => "createCategory"], - '/categories/1' => ['PUT' => "updateCategory", 'DELETE' => "deleteCategory"], - '/categories/1/mark-all-as-read' => ['PUT' => "markCategory"], - '/discover' => ['POST' => "discoverSubscriptions"], - '/entries' => ['GET' => "getEntries", 'PUT' => "updateEntries"], - '/entries/1' => ['GET' => "getEntry"], - '/entries/1/bookmark' => ['PUT' => "toggleEntryBookmark"], - '/export' => ['GET' => "opmlExport"], - '/feeds' => ['GET' => "getFeeds", 'POST' => "createFeed"], - '/feeds/1' => ['GET' => "getFeed", 'PUT' => "updateFeed", 'DELETE' => "removeFeed"], - '/feeds/1/mark-all-as-read' => ['PUT' => "markFeed"], - '/feeds/1/entries/1' => ['GET' => "getFeedEntry"], - '/feeds/1/entries' => ['GET' => "getFeedEntries"], - '/feeds/1/icon' => ['GET' => "getFeedIcon"], - '/feeds/1/refresh' => ['PUT' => "refreshFeed"], - '/feeds/refresh' => ['PUT' => "refreshAllFeeds"], - '/import' => ['POST' => "opmlImport"], - '/me' => ['GET' => "getCurrentUser"], - '/users' => ['GET' => "getUsers", 'POST' => "createUser"], - '/users/1' => ['GET' => "getUserByNum", 'PUT' => "updateUserByNum", 'DELETE' => "deleteUser"], - '/users/1/mark-all-as-read' => ['PUT' => "markAll"], - '/users/*' => ['GET' => "getUserById"], - ]; - protected const ADMIN_FUNCTIONS = [ - 'getUsers' => true, - 'getUserByNum' => true, - 'getUserById' => true, - 'createUser' => true, - 'updateUserByNum' => true, - 'deleteUser' => true, + protected const CALLS = [ // handler method Admin Path Body Query + '/categories' => [ + 'GET' => ["getCategories", false, false, false, false], + 'POST' => ["createCategory", false, false, true, false], + ], + '/categories/1' => [ + 'PUT' => ["updateCategory", false, true, true, false], + 'DELETE' => ["deleteCategory", false, true, false, false], + ], + '/categories/1/mark-all-as-read' => [ + 'PUT' => ["markCategory", false, true, false, false], + ], + '/discover' => [ + 'POST' => ["discoverSubscriptions", false, false, true, false], + ], + '/entries' => [ + 'GET' => ["getEntries", false, false, false, true], + 'PUT' => ["updateEntries", false, false, true, false], + ], + '/entries/1' => [ + 'GET' => ["getEntry", false, true, false, false], + ], + '/entries/1/bookmark' => [ + 'PUT' => ["toggleEntryBookmark", false, true, false, false], + ], + '/export' => [ + 'GET' => ["opmlExport", false, false, false, false], + ], + '/feeds' => [ + 'GET' => ["getFeeds", false, false, false, false], + 'POST' => ["createFeed", false, false, true, false], + ], + '/feeds/1' => [ + 'GET' => ["getFeed", false, true, false, false], + 'PUT' => ["updateFeed", false, true, true, false], + 'DELETE' => ["deleteFeed", false, true, false, false], + ], + '/feeds/1/entries' => [ + 'GET' => ["getFeedEntries", false, true, false, false], + ], + '/feeds/1/entries/1' => [ + 'GET' => ["getFeedEntry", false, true, false, false], + ], + '/feeds/1/icon' => [ + 'GET' => ["getFeedIcon", false, true, false, false], + ], + '/feeds/1/mark-all-as-read' => [ + 'PUT' => ["markFeed", false, true, false, false], + ], + '/feeds/1/refresh' => [ + 'PUT' => ["refreshFeed", false, true, false, false], + ], + '/feeds/refresh' => [ + 'PUT' => ["refreshAllFeeds", false, false, false, false], + ], + '/import' => [ + 'POST' => ["opmlImport", false, false, true, false], + ], + '/me' => [ + 'GET' => ["getCurrentUser", false, false, false, false], + ], + '/users' => [ + 'GET' => ["getUsers", true, false, false, false], + 'POST' => ["createUser", true, false, true, false], + ], + '/users/1' => [ + 'GET' => ["getUserByNum", true, true, false, false], + 'PUT' => ["updateUserByNum", true, true, true, false], + 'DELETE' => ["deleteUserByNum", true, true, false, false], + ], + '/users/1/mark-all-as-read' => [ + 'PUT' => ["markUserByNum", false, true, false, false], + ], + '/users/*' => [ + 'GET' => ["getUserById", true, true, false, false], + ], ]; public function __construct() { @@ -117,33 +163,45 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $func = $this->chooseCall($target, $method); if ($func instanceof ResponseInterface) { return $func; + } else { + [$func, $reqAdmin, $reqPath, $reqBody, $reqQuery] = $func; } - if ((self::ADMIN_FUNCTIONS[$func] ?? false) && !$this->isAdmin()) { + if ($reqAdmin && !$this->isAdmin()) { return new ErrorResponse("403", 403); } - $data = []; - $query = []; - if ($func === "opmlImport") { - if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) { - return new ErrorResponse("", 415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]); + $args = []; + if ($reqPath) { + $args[] = explode("/", ltrim($target, "/")); + } + if ($reqBody) { + if ($func === "opmlImport") { + if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) { + return new ErrorResponse("", 415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]); + } + $args[] = (string) $req->getBody(); + } else { + $data = (string) $req->getBody(); + if (strlen($data)) { + $data = @json_decode($data, true); + if (json_last_error() !== \JSON_ERROR_NONE) { + // if the body could not be parsed as JSON, return "400 Bad Request" + return new ErrorResponse(["InvalidBodyJSON", json_last_error_msg()], 400); + } + } else { + $data = []; + } + $data = $this->normalizeBody((array) $data); + if ($data instanceof ResponseInterface) { + return $data; + } } - $data = (string) $req->getBody(); - } elseif ($method === "POST" || $method === "PUT") { - $data = @json_decode((string) $req->getBody(), true); - if (json_last_error() !== \JSON_ERROR_NONE) { - // if the body could not be parsed as JSON, return "400 Bad Request" - return new ErrorResponse(["InvalidBodyJSON", json_last_error_msg()], 400); - } - $data = $this->normalizeBody((array) $data); - if ($data instanceof ResponseInterface) { - return $data; - } - } elseif ($method === "GET") { - $query = $req->getQueryParams(); + $args[] = $data; + } + if ($reqQuery) { + $args[] = $req->getQueryParams(); } try { - $path = explode("/", ltrim($target, "/")); - return $this->$func($path, $query, $data); + return $this->$func(...$args); // @codeCoverageIgnoreStart } catch (Exception $e) { // if there was a REST exception return 400 @@ -155,6 +213,28 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { // @codeCoverageIgnoreEnd } + protected function chooseCall(string $url, string $method) { + // // normalize the URL path: change any IDs to 1 for easier comparison + $url = $this->normalizePathIds($url); + // normalize the HTTP method to uppercase + $method = strtoupper($method); + // we now evaluate the supplied URL against every supported path for the selected scope + if (isset(self::CALLS[$url])) { + // if the path is supported, make sure the method is allowed + if (isset(self::CALLS[$url][$method])) { + // if it is allowed, return the object method to run, assuming the method exists + assert(method_exists($this, self::CALLS[$url][$method][0]), new \Exception("Method is not implemented")); + return self::CALLS[$url][$method]; + } else { + // otherwise return 405 + return new EmptyResponse(405, ['Allow' => implode(", ", array_keys(self::CALLS[$url]))]); + } + } else { + // if the path is not supported, return 404 + return new EmptyResponse(404); + } + } + protected function normalizePathIds(string $url): string { $path = explode("/", $url); // any path components which are database IDs (integers greater than zero) should be replaced with "1", for easier comparison (we don't care about the specific ID) @@ -170,12 +250,24 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return implode("/", $path); } + protected function normalizeBody(array $body) { + // Miniflux does not attempt to coerce values into different types + foreach (self::VALID_JSON as $k => $t) { + if (!isset($body[$k])) { + $body[$k] = null; + } elseif (gettype($body[$k]) !== $t) { + return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])]); + } + } + return $body; + } + protected function handleHTTPOptions(string $url): ResponseInterface { // normalize the URL path: change any IDs to 1 for easier comparison $url = $this->normalizePathIDs($url); - if (isset(self::PATHS[$url])) { + if (isset(self::CALLS[$url])) { // if the path is supported, respond with the allowed methods and other metadata - $allowed = array_keys(self::PATHS[$url]); + $allowed = array_keys(self::CALLS[$url]); // if GET is allowed, so is HEAD if (in_array("GET", $allowed)) { array_unshift($allowed, "HEAD"); @@ -190,41 +282,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } - protected function chooseCall(string $url, string $method) { - // // normalize the URL path: change any IDs to 1 for easier comparison - $url = $this->normalizePathIds($url); - // normalize the HTTP method to uppercase - $method = strtoupper($method); - // we now evaluate the supplied URL against every supported path for the selected scope - // the URL is evaluated as an array so as to avoid decoded escapes turning invalid URLs into valid ones - if (isset(self::PATHS[$url])) { - // if the path is supported, make sure the method is allowed - if (isset(self::PATHS[$url][$method])) { - // if it is allowed, return the object method to run, assuming the method exists - assert(method_exists($this, self::PATHS[$url][$method]), new \Exception("Method is not implemented")); - return self::PATHS[$url][$method]; - } else { - // otherwise return 405 - return new EmptyResponse(405, ['Allow' => implode(", ", array_keys(self::PATHS[$url]))]); - } - } else { - // if the path is not supported, return 404 - return new EmptyResponse(404); - } - } - - protected function normalizeBody(array $body) { - // Miniflux does not attempt to coerce values into different types - foreach (self::VALID_JSON as $k => $t) { - if (!isset($body[$k])) { - $body[$k] = null; - } elseif (gettype($body[$k]) !== $t) { - return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])]); - } - } - return $body; - } - protected function listUsers(array $users, bool $reportMissing): array { $out = []; $now = Date::transform($this->now(), "iso8601m"); @@ -259,7 +316,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return $out; } - protected function discoverSubscriptions(array $path, array $query, array $data): ResponseInterface { + protected function discoverSubscriptions(array $data): ResponseInterface { try { $list = Feed::discoverAll((string) $data['url'], (string) $data['username'], (string) $data['password']); } catch (FeedException $e) { @@ -278,11 +335,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($out); } - protected function getUsers(array $path, array $query, array $data): ResponseInterface { + protected function getUsers(): ResponseInterface { return new Response($this->listUsers(Arsse::$user->list(), false)); } - protected function getUserById(array $path, array $query, array $data): ResponseInterface { + protected function getUserById(array $path): ResponseInterface { try { return new Response($this->listUsers([$path[1]], true)[0] ?? new \stdClass); } catch (UserException $e) { @@ -290,7 +347,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } - protected function getUserByNum(array $path, array $query, array $data): ResponseInterface { + protected function getUserByNum(array $path): ResponseInterface { try { $user = Arsse::$user->lookup((int) $path[1]); return new Response($this->listUsers([$user], true)[0] ?? new \stdClass); @@ -299,11 +356,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } - protected function getCurrentUser(array $path, array $query, array $data): ResponseInterface { + protected function getCurrentUser(): ResponseInterface { return new Response($this->listUsers([Arsse::$user->id], false)[0] ?? new \stdClass); } - protected function getCategories(array $path, array $query, array $data): ResponseInterface { + protected function getCategories(): ResponseInterface { $out = []; $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); // add the root folder as a category @@ -316,7 +373,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($out); } - protected function createCategory(array $path, array $query, array $data): ResponseInterface { + protected function createCategory(array $data): ResponseInterface { try { $id = Arsse::$db->folderAdd(Arsse::$user->id, ['name' => (string) $data['title']]); } catch (ExceptionInput $e) { @@ -330,7 +387,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(['id' => $id + 1, 'title' => $data['title'], 'user_id' => $meta['num']]); } - protected function updateCategory(array $path, array $query, array $data): ResponseInterface { + protected function updateCategory(array $path, array $data): ResponseInterface { // category IDs in Miniflux are always greater than 1; we have folder 0, so we decrement category IDs by 1 to get the folder ID $folder = $path[1] - 1; $title = $data['title'] ?? ""; @@ -357,7 +414,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(['id' => (int) $path[1], 'title' => $title, 'user_id' => $meta['num']]); } - protected function deleteCategory(array $path, array $query, array $data): ResponseInterface { + protected function deleteCategory(array $path): ResponseInterface { try { $folder = $path[1] - 1; if ($folder !== 0) { @@ -377,6 +434,23 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } + protected function markCategory(array $path): ResponseInterface { + $folder = $path[1] - 1; + $c = new Context; + if ($folder === 0) { + // if we're marking the root folder don't also mark its child folders, since Miniflux organizes it as a peer of other folders + $c = $c->folderShallow($folder); + } else { + $c = $c->folder($folder); + } + try { + Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c); + } catch (ExceptionInput $e) { + return new ErrorResponse("404", 404); + } + return new EmptyResponse(204); + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokens in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); diff --git a/lib/REST/NextcloudNews/V1_2.php b/lib/REST/NextcloudNews/V1_2.php index c73ea8c7..5c5a9443 100644 --- a/lib/REST/NextcloudNews/V1_2.php +++ b/lib/REST/NextcloudNews/V1_2.php @@ -142,7 +142,6 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // normalize the HTTP method to uppercase $method = strtoupper($method); // we now evaluate the supplied URL against every supported path for the selected scope - // the URL is evaluated as an array so as to avoid decoded escapes turning invalid URLs into valid ones if (isset($this->paths[$url])) { // if the path is supported, make sure the method is allowed if (isset($this->paths[$url][$method])) { diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index e2f1033f..3778a547 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\REST\Miniflux; use JKingWeb\Arsse\Arsse; +use JKingWeb\Arsse\Context\Context; use JKingWeb\Arsse\User; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Db\Transaction; @@ -340,4 +341,16 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify($this->transaction)->commit() ); } + + public function testMarkACategoryAsRead(): void { + \Phake::when(Arsse::$db)->articleMark->thenReturn(1)->thenReturn(1)->thenThrow(new ExceptionInput("idMissing")); + $this->assertMessage(new EmptyResponse(204), $this->req("PUT", "/categories/2/mark-all-as-read")); + $this->assertMessage(new EmptyResponse(204), $this->req("PUT", "/categories/1/mark-all-as-read")); + $this->assertMessage(new ErrorResponse("404", 404), $this->req("PUT", "/categories/2112/mark-all-as-read")); + \Phake::inOrder( + \Phake::verify(Arsse::$db)->articleMark("john.doe@example.com", ['read' => true], (new Context)->folder(1)), + \Phake::verify(Arsse::$db)->articleMark("john.doe@example.com", ['read' => true], (new Context)->folderShallow(0)), + \Phake::verify(Arsse::$db)->articleMark("john.doe@example.com", ['read' => true], (new Context)->folder(2111)) + ); + } } From c43d0dcae3686e7627c53565e37bed05bbd14c04 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 14 Dec 2020 20:09:38 -0500 Subject: [PATCH 072/366] Groundwork for filtering rules --- docs/en/030_Supported_Protocols/005_Miniflux.md | 9 ++++++++- sql/MySQL/6.sql | 3 +++ sql/PostgreSQL/6.sql | 3 +++ sql/SQLite3/6.sql | 7 +++++-- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index 84e7d283..b29c0825 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -10,7 +10,7 @@
API endpoint
/v1/
Specifications
-
API Reference
+
API Reference, Filtering Rules
The Miniflux protocol is a fairly well-designed protocol supporting a wide variety of operations on newsfeeds, folders (termed "categories"), and articles; it also allows for user administration, and native OPML importing and exporting. Architecturally it is similar to the Nextcloud News protocol, but is generally more efficient and has more capabilities. @@ -33,6 +33,13 @@ Miniflux version 2.0.26 is emulated, though not all features are implemented - Only the URL should be considered reliable in feed discovery results - The "All" category is treated specially (see below for details) - Category names consisting only of whitespace are rejected along with the empty string +- Filtering rules may not function identically (see below for details) + +# Behaviour of filtering (block and keep) rules + +The Miniflux documentation gives only a brief example of a pattern for its filtering rules; the allowed syntax is described in full [in Google's documentation for RE2](https://github.com/google/re2/wiki/Syntax). Being a PHP application, The Arsse instead accepts [PCRE syntax](http://www.pcre.org/original/doc/html/pcresyntax.html) (or since PHP 7.3 [PCRE2 syntax](https://www.pcre.org/current/doc/html/pcre2syntax.html)), specifically in UTF-8 mode. Delimiters should not be included, and slashes should not be escaped; anchors may be used if desired. For example `^(?i)RE/MAX$` is a valid pattern. + +For convenience the patterns are tested after collapsing whitespace. Unlike Miniflux, The Arsse tests the patterns against an article's author-supplied categories if they do not match its title. # Special handling of the "All" category diff --git a/sql/MySQL/6.sql b/sql/MySQL/6.sql index 36b2d6e4..9370e274 100644 --- a/sql/MySQL/6.sql +++ b/sql/MySQL/6.sql @@ -6,6 +6,9 @@ alter table arsse_tokens add column data longtext default null; +alter table arsse_subscriptions add column keep_rule longtext default null; +alter table arsse_subscriptions add column block_rule longtext default null; + alter table arsse_users add column num bigint unsigned unique; alter table arsse_users add column admin boolean not null default 0; create temporary table arsse_users_existing( diff --git a/sql/PostgreSQL/6.sql b/sql/PostgreSQL/6.sql index a32eb0c0..f936b87a 100644 --- a/sql/PostgreSQL/6.sql +++ b/sql/PostgreSQL/6.sql @@ -6,6 +6,9 @@ alter table arsse_tokens add column data text default null; +alter table arsse_subscriptions add column keep_rule text default null; +alter table arsse_subscriptions add column block_rule text default null; + alter table arsse_users add column num bigint unique; alter table arsse_users add column admin smallint not null default 0; create temp table arsse_users_existing( diff --git a/sql/SQLite3/6.sql b/sql/SQLite3/6.sql index 81e9e821..752c0568 100644 --- a/sql/SQLite3/6.sql +++ b/sql/SQLite3/6.sql @@ -6,8 +6,11 @@ -- This is a speculative addition to support OAuth login in the future alter table arsse_tokens add column data text default null; --- Add num and admin columns to the users table --- In particular this adds a numeric identifier for each user, which Miniflux requires +-- Add columns to subscriptions to store "keep" and "block" filtering rules from Miniflux +alter table arsse_subscriptions add column keep_rule text default null; +alter table arsse_subscriptions add column block_rule text default null; + +-- Add numeric identifier and admin columns to the users table create table arsse_users_new( -- users id text primary key not null collate nocase, -- user id From d5cd5b6a17503c5774d45a9c0ebe92dc5f220ea9 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 15 Dec 2020 13:20:03 -0500 Subject: [PATCH 073/366] Implement hidden marks Tests are still needed --- lib/Context/Context.php | 5 +++++ lib/Database.php | 33 +++++++++++++++++++++++--------- sql/MySQL/6.sql | 1 + sql/PostgreSQL/6.sql | 1 + sql/SQLite3/6.sql | 4 +++- tests/cases/Misc/TestContext.php | 1 + 6 files changed, 35 insertions(+), 10 deletions(-) diff --git a/lib/Context/Context.php b/lib/Context/Context.php index fb1236a3..8e1b699c 100644 --- a/lib/Context/Context.php +++ b/lib/Context/Context.php @@ -13,6 +13,7 @@ class Context extends ExclusionContext { public $offset = 0; public $unread; public $starred; + public $hidden; public $labelled; public $annotated; @@ -46,6 +47,10 @@ class Context extends ExclusionContext { return $this->act(__FUNCTION__, func_num_args(), $spec); } + public function hidden(bool $spec = null) { + return $this->act(__FUNCTION__, func_num_args(), $spec); + } + public function labelled(bool $spec = null) { return $this->act(__FUNCTION__, func_num_args(), $spec); } diff --git a/lib/Database.php b/lib/Database.php index 799968fb..30a126f4 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -741,7 +741,7 @@ class Database { "SELECT s.id as id, s.feed as feed, - f.url,source,folder,pinned,err_count,err_msg,order_type,added, + f.url,source,folder,pinned,err_count,err_msg,order_type,added,keep_rule,block_rule, f.updated as updated, f.modified as edited, s.modified as modified, @@ -762,8 +762,8 @@ class Database { // topmost folders belonging to the user $q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders where owner = ? and parent is null union all select id,top from arsse_folders join topmost on parent=f_id", ["str"], [$user]); if ($id) { - // this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder // if an ID is specified, add a suitable WHERE condition and bindings + // this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder $q->setWhere("s.id = ?", "int", $id); } elseif ($folder && $recursive) { // if a folder is specified and we're listing recursively, add a common table expression to list it and its children so that we select from the entire subtree @@ -1194,6 +1194,19 @@ class Database { return $out; } + /** Retrieves the set of filters users have applied to a given feed + * + * Each record includes the following keys: + * + * - "owner": The user for whom to apply the filters + * - "sub": The subscription ID which ties the user to the feed + * - "keep": The "keep" rule; any articles which fail to match this rule are hidden + * - "block": The block rule; any article which matches this rule are hidden + */ + public function feedRulesGet(int $feedID): Db\Result { + return $this->db->prepare("SELECT owner, id as sub, keep_rule as keep, block_rule as block from arsse_subscriptions where feed = ? and (coalesce(keep_rule, '') || coalesce(block_rule, '')) <> ''", "int")->run($feedID); + } + /** Retrieves various identifiers for the latest $count articles in the given newsfeed. The identifiers are: * * - "id": The database record key for the article @@ -1652,6 +1665,7 @@ class Database { * * - "read": Whether the article should be marked as read (true) or unread (false) * - "starred": Whether the article should (true) or should not (false) be marked as starred/favourite + * - "hidden": Whether the article should (true) or should not (false) be suppressed from normal listings; this is normally set by the system rather than the user directly * - "note": A string containing a freeform plain-text note for the article * * @param string $user The user who owns the articles to be modified @@ -1662,22 +1676,23 @@ class Database { $data = [ 'read' => $data['read'] ?? null, 'starred' => $data['starred'] ?? null, + 'hidden' => $data['hidden'] ?? null, 'note' => $data['note'] ?? null, ]; - if (!isset($data['read']) && !isset($data['starred']) && !isset($data['note'])) { + if (!isset($data['read']) && !isset($data['starred']) && !isset($data['hidden']) && !isset($data['note'])) { return 0; } $context = $context ?? new Context; $tr = $this->begin(); $out = 0; - if ($data['read'] || $data['starred'] || strlen($data['note'] ?? "")) { + if ($data['read'] || $data['starred'] || $data['hidden'] || strlen($data['note'] ?? "")) { // first prepare a query to insert any missing marks rows for the articles we want to mark // but only insert new mark records if we're setting at least one "positive" mark $q = $this->articleQuery($user, $context, ["id", "subscription", "note"]); - $q->setWhere("arsse_marks.starred is null"); // null means there is no marks row for the article + $q->setWhere("arsse_marks.starred is null"); // null means there is no marks row for the article, because the column is defined not-null $this->db->prepare("INSERT INTO arsse_marks(article,subscription,note) ".$q->getQuery(), $q->getTypes())->run($q->getValues()); } - if (isset($data['read']) && (isset($data['starred']) || isset($data['note'])) && ($context->edition() || $context->editions())) { + if (isset($data['read']) && (isset($data['starred']) || isset($data['hidden']) || isset($data['note'])) && ($context->edition() || $context->editions())) { // if marking by edition both read and something else, do separate marks for starred and note than for read // marking as read is ignored if the edition is not the latest, but the same is not true of the other two marks $this->db->query("UPDATE arsse_marks set touched = 0 where touched <> 0"); @@ -1693,7 +1708,7 @@ class Database { } else { $context->articles($this->editionArticle(...$context->editions))->editions(null); } - // set starred and/or note marks (unless all requested editions actually do not exist) + // set starred, hidden, and/or note marks (unless all requested editions actually do not exist) if ($context->article || $context->articles) { $q = $this->articleQuery($user, $context, ["id", "subscription"]); $q->setWhere("(arsse_marks.note <> coalesce(?,arsse_marks.note) or arsse_marks.starred <> coalesce(?,arsse_marks.starred))", ["str", "bool"], [$data['note'], $data['starred']]); @@ -1701,7 +1716,7 @@ class Database { $data = array_filter($data, function($v) { return isset($v); }); - [$set, $setTypes, $setValues] = $this->generateSet($data, ['starred' => "bool", 'note' => "str"]); + [$set, $setTypes, $setValues] = $this->generateSet($data, ['starred' => "bool", 'hidden' => "bool", 'note' => "str"]); $q->setBody("UPDATE arsse_marks set touched = 1, $set where article in(select article from target_articles) and subscription in(select distinct subscription from target_articles)", $setTypes, $setValues); $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); } @@ -1725,7 +1740,7 @@ class Database { $data = array_filter($data, function($v) { return isset($v); }); - [$set, $setTypes, $setValues] = $this->generateSet($data, ['read' => "bool", 'starred' => "bool", 'note' => "str"]); + [$set, $setTypes, $setValues] = $this->generateSet($data, ['read' => "bool", 'starred' => "bool", 'hidden' => "bool", 'note' => "str"]); $q->setBody("UPDATE arsse_marks set $set, modified = CURRENT_TIMESTAMP where article in(select article from target_articles) and subscription in(select distinct subscription from target_articles)", $setTypes, $setValues); $out = $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes(); } diff --git a/sql/MySQL/6.sql b/sql/MySQL/6.sql index 9370e274..c2f8b532 100644 --- a/sql/MySQL/6.sql +++ b/sql/MySQL/6.sql @@ -8,6 +8,7 @@ alter table arsse_tokens add column data longtext default null; alter table arsse_subscriptions add column keep_rule longtext default null; alter table arsse_subscriptions add column block_rule longtext default null; +alter table arsse_marks add column hidden boolean not null default 0; alter table arsse_users add column num bigint unsigned unique; alter table arsse_users add column admin boolean not null default 0; diff --git a/sql/PostgreSQL/6.sql b/sql/PostgreSQL/6.sql index f936b87a..a27b87a6 100644 --- a/sql/PostgreSQL/6.sql +++ b/sql/PostgreSQL/6.sql @@ -8,6 +8,7 @@ alter table arsse_tokens add column data text default null; alter table arsse_subscriptions add column keep_rule text default null; alter table arsse_subscriptions add column block_rule text default null; +alter table arsse_marks add column hidden smallint not null default 0; alter table arsse_users add column num bigint unique; alter table arsse_users add column admin smallint not null default 0; diff --git a/sql/SQLite3/6.sql b/sql/SQLite3/6.sql index 752c0568..3c5f3589 100644 --- a/sql/SQLite3/6.sql +++ b/sql/SQLite3/6.sql @@ -6,9 +6,11 @@ -- This is a speculative addition to support OAuth login in the future alter table arsse_tokens add column data text default null; --- Add columns to subscriptions to store "keep" and "block" filtering rules from Miniflux +-- Add columns to subscriptions to store "keep" and "block" filtering rules from Miniflux, +-- as well as a column to mark articles as hidden for users alter table arsse_subscriptions add column keep_rule text default null; alter table arsse_subscriptions add column block_rule text default null; +alter table arsse_marks add column hidden boolean not null default 0; -- Add numeric identifier and admin columns to the users table create table arsse_users_new( diff --git a/tests/cases/Misc/TestContext.php b/tests/cases/Misc/TestContext.php index 037ca8e1..46ecaaff 100644 --- a/tests/cases/Misc/TestContext.php +++ b/tests/cases/Misc/TestContext.php @@ -46,6 +46,7 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest { 'oldestEdition' => 1337, 'unread' => true, 'starred' => true, + 'hidden' => true, 'modifiedSince' => new \DateTime(), 'notModifiedSince' => new \DateTime(), 'markedSince' => new \DateTime(), From 8ae3740d5fefab94a5435c93f63c2f189553bb36 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 15 Dec 2020 19:28:51 -0500 Subject: [PATCH 074/366] Implement querying articles by hidden mark --- lib/Database.php | 2 ++ tests/cases/Database/SeriesArticle.php | 30 +++++++++++++++----------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 30a126f4..466a0362 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1312,6 +1312,7 @@ class Database { 'folder' => "coalesce(arsse_subscriptions.folder,0)", 'subscription' => "arsse_subscriptions.id", 'feed' => "arsse_subscriptions.feed", + 'hidden' => "coalesce(arsse_marks.hidden,0)", 'starred' => "coalesce(arsse_marks.starred,0)", 'unread' => "abs(coalesce(arsse_marks.read,0) - 1)", 'note' => "coalesce(arsse_marks.note,'')", @@ -1417,6 +1418,7 @@ class Database { "subscriptions" => ["subscription", "in", "int", ""], "unread" => ["unread", "=", "bool", ""], "starred" => ["starred", "=", "bool", ""], + "hidden" => ["hidden", "=", "bool", ""], ]; foreach ($options as $m => [$col, $op, $type, $pair]) { if (!$context->$m()) { diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 4edd8c82..a930caea 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -235,21 +235,23 @@ trait SeriesArticle { 'starred' => "bool", 'modified' => "datetime", 'note' => "str", + 'hidden' => "bool", ], 'rows' => [ - [1, 1,1,1,'2000-01-01 00:00:00',''], - [5, 19,1,0,'2016-01-01 00:00:00',''], - [5, 20,0,1,'2005-01-01 00:00:00',''], - [7, 20,1,0,'2010-01-01 00:00:00',''], - [8, 102,1,0,'2000-01-02 02:00:00','Note 2'], - [9, 103,0,1,'2000-01-03 03:00:00','Note 3'], - [9, 104,1,1,'2000-01-04 04:00:00','Note 4'], - [10,105,0,0,'2000-01-05 05:00:00',''], - [11, 19,0,0,'2017-01-01 00:00:00','ook'], - [11, 20,1,0,'2017-01-01 00:00:00','eek'], - [12, 3,0,1,'2017-01-01 00:00:00','ack'], - [12, 4,1,1,'2017-01-01 00:00:00','ach'], - [1, 2,0,0,'2010-01-01 00:00:00','Some Note'], + [1, 1,1,1,'2000-01-01 00:00:00','',0], + [5, 19,1,0,'2016-01-01 00:00:00','',0], + [5, 20,0,1,'2005-01-01 00:00:00','',0], + [7, 20,1,0,'2010-01-01 00:00:00','',0], + [8, 102,1,0,'2000-01-02 02:00:00','Note 2',0], + [9, 103,0,1,'2000-01-03 03:00:00','Note 3',0], + [9, 104,1,1,'2000-01-04 04:00:00','Note 4',0], + [10,105,0,0,'2000-01-05 05:00:00','',0], + [11, 19,0,0,'2017-01-01 00:00:00','ook',0], + [11, 20,1,0,'2017-01-01 00:00:00','eek',0], + [12, 3,0,1,'2017-01-01 00:00:00','ack',0], + [12, 4,1,1,'2017-01-01 00:00:00','ach',0], + [1, 2,0,0,'2010-01-01 00:00:00','Some Note',0], + [3, 5,0,0,'2000-01-01 00:00:00','',1], ], ], 'arsse_categories' => [ // author-supplied categories @@ -443,6 +445,8 @@ trait SeriesArticle { 'Starred and Read in subscription' => [(new Context)->starred(true)->unread(false)->subscription(5), []], 'Annotated' => [(new Context)->annotated(true), [2]], 'Not annotated' => [(new Context)->annotated(false), [1,3,4,5,6,7,8,19,20]], + 'Hidden' => [(new Context)->hidden(true), [5]], + 'Not hidden' => [(new Context)->hidden(false), [1,2,3,4,6,7,8,19,20]], 'Labelled' => [(new Context)->labelled(true), [1,5,8,19,20]], 'Not labelled' => [(new Context)->labelled(false), [2,3,4,6,7]], 'Not after edition 999' => [(new Context)->subscription(5)->latestEdition(999), [19]], From ffc98daff3a62f06b93ea5a67b71a4cfd7724c06 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 15 Dec 2020 19:50:26 -0500 Subject: [PATCH 075/366] Adjust article marking tests to account for new hidden mark --- tests/cases/Database/SeriesArticle.php | 86 +++++++++++++------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index a930caea..37dc10bf 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -407,7 +407,7 @@ trait SeriesArticle { "content", "media_url", "media_type", "note", ]; - $this->checkTables = ['arsse_marks' => ["subscription","article","read","starred","modified","note"]]; + $this->checkTables = ['arsse_marks' => ["subscription", "article", "read", "starred", "modified", "note", "hidden"]]; $this->user = "john.doe@example.net"; } @@ -624,10 +624,10 @@ trait SeriesArticle { $state['arsse_marks']['rows'][8][4] = $now; $state['arsse_marks']['rows'][10][2] = 1; $state['arsse_marks']['rows'][10][4] = $now; - $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'']; - $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'']; - $state['arsse_marks']['rows'][] = [14,7,1,0,$now,'']; - $state['arsse_marks']['rows'][] = [14,8,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'',0]; + $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,7,1,0,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,8,1,0,$now,'',0]; $this->compareExpectations(static::$drv, $state); } @@ -650,10 +650,10 @@ trait SeriesArticle { $state['arsse_marks']['rows'][8][4] = $now; $state['arsse_marks']['rows'][9][3] = 1; $state['arsse_marks']['rows'][9][4] = $now; - $state['arsse_marks']['rows'][] = [13,5,0,1,$now,'']; - $state['arsse_marks']['rows'][] = [13,6,0,1,$now,'']; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; - $state['arsse_marks']['rows'][] = [14,8,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [13,5,0,1,$now,'',0]; + $state['arsse_marks']['rows'][] = [13,6,0,1,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,8,0,1,$now,'',0]; $this->compareExpectations(static::$drv, $state); } @@ -682,10 +682,10 @@ trait SeriesArticle { $state['arsse_marks']['rows'][9][4] = $now; $state['arsse_marks']['rows'][10][2] = 1; $state['arsse_marks']['rows'][10][4] = $now; - $state['arsse_marks']['rows'][] = [13,5,1,1,$now,'']; - $state['arsse_marks']['rows'][] = [13,6,1,1,$now,'']; - $state['arsse_marks']['rows'][] = [14,7,1,1,$now,'']; - $state['arsse_marks']['rows'][] = [14,8,1,1,$now,'']; + $state['arsse_marks']['rows'][] = [13,5,1,1,$now,'',0]; + $state['arsse_marks']['rows'][] = [13,6,1,1,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,7,1,1,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,8,1,1,$now,'',0]; $this->compareExpectations(static::$drv, $state); } @@ -700,10 +700,10 @@ trait SeriesArticle { $state['arsse_marks']['rows'][9][4] = $now; $state['arsse_marks']['rows'][11][2] = 0; $state['arsse_marks']['rows'][11][4] = $now; - $state['arsse_marks']['rows'][] = [13,5,0,1,$now,'']; - $state['arsse_marks']['rows'][] = [13,6,0,1,$now,'']; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; - $state['arsse_marks']['rows'][] = [14,8,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [13,5,0,1,$now,'',0]; + $state['arsse_marks']['rows'][] = [13,6,0,1,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,8,0,1,$now,'',0]; $this->compareExpectations(static::$drv, $state); } @@ -718,10 +718,10 @@ trait SeriesArticle { $state['arsse_marks']['rows'][10][4] = $now; $state['arsse_marks']['rows'][11][3] = 0; $state['arsse_marks']['rows'][11][4] = $now; - $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'']; - $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'']; - $state['arsse_marks']['rows'][] = [14,7,1,0,$now,'']; - $state['arsse_marks']['rows'][] = [14,8,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'',0]; + $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,7,1,0,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,8,1,0,$now,'',0]; $this->compareExpectations(static::$drv, $state); } @@ -737,10 +737,10 @@ trait SeriesArticle { $state['arsse_marks']['rows'][10][4] = $now; $state['arsse_marks']['rows'][11][5] = "New note"; $state['arsse_marks']['rows'][11][4] = $now; - $state['arsse_marks']['rows'][] = [13,5,0,0,$now,'New note']; - $state['arsse_marks']['rows'][] = [13,6,0,0,$now,'New note']; - $state['arsse_marks']['rows'][] = [14,7,0,0,$now,'New note']; - $state['arsse_marks']['rows'][] = [14,8,0,0,$now,'New note']; + $state['arsse_marks']['rows'][] = [13,5,0,0,$now,'New note',0]; + $state['arsse_marks']['rows'][] = [13,6,0,0,$now,'New note',0]; + $state['arsse_marks']['rows'][] = [14,7,0,0,$now,'New note',0]; + $state['arsse_marks']['rows'][] = [14,8,0,0,$now,'New note',0]; $this->compareExpectations(static::$drv, $state); } @@ -748,10 +748,10 @@ trait SeriesArticle { Arsse::$db->articleMark($this->user, ['read' => true], (new Context)->folder(7)); $now = Date::transform(time(), "sql"); $state = $this->primeExpectations($this->data, $this->checkTables); - $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'']; - $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'']; - $state['arsse_marks']['rows'][] = [14,7,1,0,$now,'']; - $state['arsse_marks']['rows'][] = [14,8,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'',0]; + $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,7,1,0,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,8,1,0,$now,'',0]; $this->compareExpectations(static::$drv, $state); } @@ -759,8 +759,8 @@ trait SeriesArticle { Arsse::$db->articleMark($this->user, ['read' => true], (new Context)->folder(8)); $now = Date::transform(time(), "sql"); $state = $this->primeExpectations($this->data, $this->checkTables); - $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'']; - $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'',0]; + $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'',0]; $this->compareExpectations(static::$drv, $state); } @@ -773,8 +773,8 @@ trait SeriesArticle { Arsse::$db->articleMark($this->user, ['read' => true], (new Context)->subscription(13)); $now = Date::transform(time(), "sql"); $state = $this->primeExpectations($this->data, $this->checkTables); - $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'']; - $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'']; + $state['arsse_marks']['rows'][] = [13,5,1,0,$now,'',0]; + $state['arsse_marks']['rows'][] = [13,6,1,0,$now,'',0]; $this->compareExpectations(static::$drv, $state); } @@ -798,7 +798,7 @@ trait SeriesArticle { $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][9][3] = 1; $state['arsse_marks']['rows'][9][4] = $now; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'',0]; $this->compareExpectations(static::$drv, $state); } @@ -811,7 +811,7 @@ trait SeriesArticle { $state['arsse_marks']['rows'][9][4] = $now; $state['arsse_marks']['rows'][11][2] = 0; $state['arsse_marks']['rows'][11][4] = $now; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'',0]; $this->compareExpectations(static::$drv, $state); } @@ -840,7 +840,7 @@ trait SeriesArticle { $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][9][3] = 1; $state['arsse_marks']['rows'][9][4] = $now; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'',0]; $this->compareExpectations(static::$drv, $state); } @@ -878,7 +878,7 @@ trait SeriesArticle { $state['arsse_marks']['rows'][9][4] = $now; $state['arsse_marks']['rows'][11][2] = 0; $state['arsse_marks']['rows'][11][4] = $now; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'',0]; $this->compareExpectations(static::$drv, $state); } @@ -938,10 +938,10 @@ trait SeriesArticle { $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][8][3] = 1; $state['arsse_marks']['rows'][8][4] = $now; - $state['arsse_marks']['rows'][] = [13,5,0,1,$now,'']; - $state['arsse_marks']['rows'][] = [13,6,0,1,$now,'']; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; - $state['arsse_marks']['rows'][] = [14,8,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [13,5,0,1,$now,'',0]; + $state['arsse_marks']['rows'][] = [13,6,0,1,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,8,0,1,$now,'',0]; $this->compareExpectations(static::$drv, $state); } @@ -960,8 +960,8 @@ trait SeriesArticle { Arsse::$db->articleMark($this->user, ['starred' => true], (new Context)->notMarkedSince('2000-01-01T00:00:00Z')); $now = Date::transform(time(), "sql"); $state = $this->primeExpectations($this->data, $this->checkTables); - $state['arsse_marks']['rows'][] = [13,5,0,1,$now,'']; - $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'']; + $state['arsse_marks']['rows'][] = [13,5,0,1,$now,'',0]; + $state['arsse_marks']['rows'][] = [14,7,0,1,$now,'',0]; $this->compareExpectations(static::$drv, $state); } From 86c4a30744fe838a86572031e6dd5a616ec76104 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 17 Dec 2020 18:12:52 -0500 Subject: [PATCH 076/366] Adjust articleStarred function to discount hidden --- lib/Database.php | 4 ++-- tests/cases/Database/SeriesArticle.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 466a0362..55c21e0f 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1750,7 +1750,7 @@ class Database { return $out; } - /** Returns statistics about the articles starred by the given user + /** Returns statistics about the articles starred by the given user. Hidden articles are excluded * * The associative array returned has the following keys: * @@ -1765,7 +1765,7 @@ class Database { coalesce(sum(abs(\"read\" - 1)),0) as unread, coalesce(sum(\"read\"),0) as \"read\" FROM ( - select \"read\" from arsse_marks where starred = 1 and subscription in (select id from arsse_subscriptions where owner = ?) + select \"read\" from arsse_marks where starred = 1 and hidden <> 1 and subscription in (select id from arsse_subscriptions where owner = ?) ) as starred_data", "str" )->run($user)->getRow(); diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 37dc10bf..a6b6bdbd 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -252,6 +252,7 @@ trait SeriesArticle { [12, 4,1,1,'2017-01-01 00:00:00','ach',0], [1, 2,0,0,'2010-01-01 00:00:00','Some Note',0], [3, 5,0,0,'2000-01-01 00:00:00','',1], + [6, 1,0,1,'2010-01-01 00:00:00','',1], ], ], 'arsse_categories' => [ // author-supplied categories @@ -969,7 +970,7 @@ trait SeriesArticle { $setSize = (new \ReflectionClassConstant(Database::class, "LIMIT_SET_SIZE"))->getValue(); $this->assertSame(2, Arsse::$db->articleCount("john.doe@example.com", (new Context)->starred(true))); $this->assertSame(4, Arsse::$db->articleCount("john.doe@example.com", (new Context)->folder(1))); - $this->assertSame(0, Arsse::$db->articleCount("jane.doe@example.com", (new Context)->starred(true))); + $this->assertSame(1, Arsse::$db->articleCount("jane.doe@example.com", (new Context)->starred(true))); $this->assertSame(10, Arsse::$db->articleCount("john.doe@example.com", (new Context)->articles(range(1, $setSize * 3)))); } From 97010d882290f3098f961f32df10f8e6d4c17073 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 19 Dec 2020 10:59:40 -0500 Subject: [PATCH 077/366] Tests for marking articles hidden --- lib/Database.php | 4 +- tests/cases/Database/SeriesArticle.php | 113 ++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 3 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 55c21e0f..08e3f050 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1713,7 +1713,7 @@ class Database { // set starred, hidden, and/or note marks (unless all requested editions actually do not exist) if ($context->article || $context->articles) { $q = $this->articleQuery($user, $context, ["id", "subscription"]); - $q->setWhere("(arsse_marks.note <> coalesce(?,arsse_marks.note) or arsse_marks.starred <> coalesce(?,arsse_marks.starred))", ["str", "bool"], [$data['note'], $data['starred']]); + $q->setWhere("(arsse_marks.note <> coalesce(?,arsse_marks.note) or arsse_marks.starred <> coalesce(?,arsse_marks.starred) or arsse_marks.hidden <> coalesce(?,arsse_marks.hidden))", ["str", "bool", "bool"], [$data['note'], $data['starred'], $data['hidden']]); $q->pushCTE("target_articles(article,subscription)"); $data = array_filter($data, function($v) { return isset($v); @@ -1737,7 +1737,7 @@ class Database { } } $q = $this->articleQuery($user, $context, ["id", "subscription"]); - $q->setWhere("(arsse_marks.note <> coalesce(?,arsse_marks.note) or arsse_marks.starred <> coalesce(?,arsse_marks.starred) or arsse_marks.read <> coalesce(?,arsse_marks.read))", ["str", "bool", "bool"], [$data['note'], $data['starred'], $data['read']]); + $q->setWhere("(arsse_marks.note <> coalesce(?,arsse_marks.note) or arsse_marks.starred <> coalesce(?,arsse_marks.starred) or arsse_marks.read <> coalesce(?,arsse_marks.read) or arsse_marks.hidden <> coalesce(?,arsse_marks.hidden))", ["str", "bool", "bool", "bool"], [$data['note'], $data['starred'], $data['read'], $data['hidden']]); $q->pushCTE("target_articles(article,subscription)"); $data = array_filter($data, function($v) { return isset($v); diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index a6b6bdbd..0653b580 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -253,6 +253,7 @@ trait SeriesArticle { [1, 2,0,0,'2010-01-01 00:00:00','Some Note',0], [3, 5,0,0,'2000-01-01 00:00:00','',1], [6, 1,0,1,'2010-01-01 00:00:00','',1], + [6, 2,1,0,'2010-01-01 00:00:00','',1], ], ], 'arsse_categories' => [ // author-supplied categories @@ -1035,4 +1036,114 @@ trait SeriesArticle { yield [$method]; } } -} + + public function testMarkAllArticlesNotHidden(): void { + Arsse::$db->articleMark("jane.doe@example.com", ['hidden' => false]); + $now = Date::transform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][14][6] = 0; + $state['arsse_marks']['rows'][14][4] = $now; + $state['arsse_marks']['rows'][15][6] = 0; + $state['arsse_marks']['rows'][15][4] = $now; + $this->compareExpectations(static::$drv, $state); + } + + public function testMarkAllArticlesHidden(): void { + Arsse::$db->articleMark("jane.doe@example.com", ['hidden' => true]); + $now = Date::transform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][3][6] = 1; + $state['arsse_marks']['rows'][3][4] = $now; + $state['arsse_marks']['rows'][] = [7,19,0,0,$now,'',1]; + $this->compareExpectations(static::$drv, $state); + } + + public function testMarkAllArticlesUnreadAndNotHidden(): void { + Arsse::$db->articleMark("jane.doe@example.com", ['read' => false, 'hidden' => false]); + $now = Date::transform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][3][2] = 0; + $state['arsse_marks']['rows'][3][4] = $now; + $state['arsse_marks']['rows'][14][6] = 0; + $state['arsse_marks']['rows'][14][4] = $now; + $state['arsse_marks']['rows'][15][2] = 0; + $state['arsse_marks']['rows'][15][6] = 0; + $state['arsse_marks']['rows'][15][4] = $now; + $this->compareExpectations(static::$drv, $state); + } + + public function testMarkAllArticlesReadAndHidden(): void { + Arsse::$db->articleMark("jane.doe@example.com", ['read' => true, 'hidden' => true]); + $now = Date::transform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][3][6] = 1; + $state['arsse_marks']['rows'][3][4] = $now; + $state['arsse_marks']['rows'][14][2] = 1; + $state['arsse_marks']['rows'][14][4] = $now; + $state['arsse_marks']['rows'][] = [7,19,1,0,$now,'',1]; + $this->compareExpectations(static::$drv, $state); + } + + public function testMarkAllArticlesUnreadAndHidden(): void { + Arsse::$db->articleMark("jane.doe@example.com", ['read' => false,'hidden' => true]); + $now = Date::transform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][3][2] = 0; + $state['arsse_marks']['rows'][3][6] = 1; + $state['arsse_marks']['rows'][3][4] = $now; + $state['arsse_marks']['rows'][15][2] = 0; + $state['arsse_marks']['rows'][15][4] = $now; + $state['arsse_marks']['rows'][] = [7,19,0,0,$now,'',1]; + $this->compareExpectations(static::$drv, $state); + } + + public function testMarkAllArticlesReadAndNotHidden(): void { + Arsse::$db->articleMark("jane.doe@example.com", ['read' => true,'hidden' => false]); + $now = Date::transform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][14][2] = 1; + $state['arsse_marks']['rows'][14][6] = 0; + $state['arsse_marks']['rows'][14][4] = $now; + $state['arsse_marks']['rows'][15][6] = 0; + $state['arsse_marks']['rows'][15][4] = $now; + $state['arsse_marks']['rows'][] = [7,19,1,0,$now,'',0]; + $this->compareExpectations(static::$drv, $state); + } + + public function testMarkMultipleEditionsUnreadAndHiddenWithStale(): void { + Arsse::$db->articleMark("jane.doe@example.com", ['read' => false,'hidden' => true], (new Context)->editions([1,2,19,20])); + $now = Date::transform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][3][6] = 1; + $state['arsse_marks']['rows'][3][4] = $now; + $state['arsse_marks']['rows'][15][2] = 0; + $state['arsse_marks']['rows'][15][6] = 1; + $state['arsse_marks']['rows'][15][4] = $now; + $state['arsse_marks']['rows'][] = [7,19,0,0,$now,'',1]; + $this->compareExpectations(static::$drv, $state); + } + + public function testMarkAStaleEditionHidden(): void { + Arsse::$db->articleMark("jane.doe@example.com", ['hidden' => true], (new Context)->edition(20)); + $now = Date::transform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][3][6] = 1; + $state['arsse_marks']['rows'][3][4] = $now; + $this->compareExpectations(static::$drv, $state); + } + + public function testMarkAStaleEditionUnreadAndHidden(): void { + Arsse::$db->articleMark("jane.doe@example.com", ['read' => false,'hidden' => true], (new Context)->edition(20)); // only starred is changed + $now = Date::transform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][3][6] = 1; + $state['arsse_marks']['rows'][3][4] = $now; + $this->compareExpectations(static::$drv, $state); + } + + public function testMarkAStaleEditionUnreadAndNotHidden(): void { + Arsse::$db->articleMark("jane.doe@example.com", ['read' => false,'hidden' => false], (new Context)->edition(20)); // no changes occur + $state = $this->primeExpectations($this->data, $this->checkTables); + $this->compareExpectations(static::$drv, $state); + } +} \ No newline at end of file From 8527c83976e7a06980d85b72794a6bdcba33bf0a Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 20 Dec 2020 11:55:36 -0500 Subject: [PATCH 078/366] Exclude hiddens from subscription unread count Also fix a bug that would result in the unread count being null if no marks existed --- CHANGELOG | 1 + lib/Database.php | 11 +++++++++-- tests/cases/Database/SeriesSubscription.php | 19 +++++++++++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3b65066b..a3847c4f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Version 0.9.0 (????-??-??) Bug fixes: - Use icons specified in Atom feeds when available +- Do not return null as subscription unread count Changes: - Explicitly forbid U+003A COLON in usernames, for compatibility with HTTP diff --git a/lib/Database.php b/lib/Database.php index 08e3f050..9ad0eb2c 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -748,13 +748,20 @@ class Database { i.url as favicon, t.top as top_folder, coalesce(s.title, f.title) as title, - (articles - marked) as unread + coalesce((articles - hidden - marked + hidden_marked), articles) as unread FROM arsse_subscriptions as s left join topmost as t on t.f_id = s.folder join arsse_feeds as f on f.id = s.feed left join arsse_icons as i on i.id = f.icon left join (select feed, count(*) as articles from arsse_articles group by feed) as article_stats on article_stats.feed = s.feed - left join (select subscription, sum(\"read\") as marked from arsse_marks group by subscription) as mark_stats on mark_stats.subscription = s.id" + left join ( + select + subscription, + sum(cast((\"read\" = 1 and hidden = 0) as integer)) as marked, + sum(cast((\"read\" = 0 and hidden = 1) as integer)) as hidden, + sum(cast((\"read\" = 1 and hidden = 1) as integer)) as hidden_marked + from arsse_marks group by subscription + ) as mark_stats on mark_stats.subscription = s.id" ); $q->setWhere("s.owner = ?", ["str"], [$user]); $nocase = $this->db->sqlToken("nocase"); diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index 749c8752..0e14a7bf 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -21,8 +21,9 @@ trait SeriesSubscription { 'num' => 'int', ], 'rows' => [ - ["jane.doe@example.com", "",1], - ["john.doe@example.com", "",2], + ["jane.doe@example.com", "", 1], + ["john.doe@example.com", "", 2], + ["jill.doe@example.com", "", 3] ], ], 'arsse_folders' => [ @@ -81,6 +82,7 @@ trait SeriesSubscription { [1,"john.doe@example.com",2,null,null,1,2], [2,"jane.doe@example.com",2,null,null,0,0], [3,"john.doe@example.com",3,"Ook",2,0,1], + [4,"jill.doe@example.com",2,null,null,0,0], ], ], 'arsse_tags' => [ @@ -291,6 +293,19 @@ trait SeriesSubscription { $this->assertResult($exp, Arsse::$db->subscriptionList($this->user)); $this->assertArraySubset($exp[0], Arsse::$db->subscriptionPropertiesGet($this->user, 1)); $this->assertArraySubset($exp[1], Arsse::$db->subscriptionPropertiesGet($this->user, 3)); + // test that an absence of marks does not corrupt unread count + $exp = [ + [ + 'url' => "http://example.com/feed2", + 'title' => "eek", + 'folder' => null, + 'top_folder' => null, + 'unread' => 5, + 'pinned' => 0, + 'order_type' => 0, + ], + ]; + $this->assertResult($exp, Arsse::$db->subscriptionList("jill.doe@example.com")); } public function testListSubscriptionsInAFolder(): void { From f0bfe1fdff9c868327511009c2564153198bc443 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 20 Dec 2020 17:34:32 -0500 Subject: [PATCH 079/366] Simplify editionLatest Database method Also adjust label querying to take hidden marks into account --- lib/Database.php | 76 +++++++++++++++----------- tests/cases/Database/SeriesArticle.php | 7 ++- tests/cases/Database/SeriesLabel.php | 27 +++++---- 3 files changed, 64 insertions(+), 46 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 9ad0eb2c..32e88d33 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -79,7 +79,11 @@ class Database { /** Returns the bare name of the calling context's calling method, when __FUNCTION__ is not appropriate */ protected function caller(): string { - return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function']; + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4); + if ($trace[2]['function'] === "articleQuery") { + return $trace[3]['function']; + } + return $trace[2]['function']; } /** Returns the current (actual) schema version of the database; compared against self::SCHEMA_VERSION to know when an upgrade is required */ @@ -748,7 +752,7 @@ class Database { i.url as favicon, t.top as top_folder, coalesce(s.title, f.title) as title, - coalesce((articles - hidden - marked + hidden_marked), articles) as unread + coalesce((articles - hidden - marked), articles) as unread FROM arsse_subscriptions as s left join topmost as t on t.f_id = s.folder join arsse_feeds as f on f.id = s.feed @@ -757,9 +761,8 @@ class Database { left join ( select subscription, - sum(cast((\"read\" = 1 and hidden = 0) as integer)) as marked, - sum(cast((\"read\" = 0 and hidden = 1) as integer)) as hidden, - sum(cast((\"read\" = 1 and hidden = 1) as integer)) as hidden_marked + sum(hidden) as hidden, + sum(cast((\"read\" = 1 and hidden = 0) as integer)) as marked from arsse_marks group by subscription ) as mark_stats on mark_stats.subscription = s.id" ); @@ -1206,12 +1209,11 @@ class Database { * Each record includes the following keys: * * - "owner": The user for whom to apply the filters - * - "sub": The subscription ID which ties the user to the feed * - "keep": The "keep" rule; any articles which fail to match this rule are hidden * - "block": The block rule; any article which matches this rule are hidden */ public function feedRulesGet(int $feedID): Db\Result { - return $this->db->prepare("SELECT owner, id as sub, keep_rule as keep, block_rule as block from arsse_subscriptions where feed = ? and (coalesce(keep_rule, '') || coalesce(block_rule, '')) <> ''", "int")->run($feedID); + return $this->db->prepare("SELECT owner, keep_rule as keep, block_rule as block from arsse_subscriptions where feed = ? and (coalesce(keep_rule, '') || coalesce(block_rule, '')) <> ''", "int")->run($feedID); } /** Retrieves various identifiers for the latest $count articles in the given newsfeed. The identifiers are: @@ -1310,6 +1312,7 @@ class Database { return [ 'id' => "arsse_articles.id", 'edition' => "latest_editions.edition", + 'latest_edition' => "max(latest_editions.edition)", 'url' => "arsse_articles.url", 'title' => "arsse_articles.title", 'author' => "arsse_articles.author", @@ -1385,6 +1388,7 @@ class Database { } $outColumns = implode(",", $outColumns); } + assert(strlen($outColumns) > 0, new \Exception("No input columns matched whitelist")); // define the basic query, to which we add lots of stuff where necessary $q = new Query( "SELECT @@ -1895,14 +1899,8 @@ class Database { /** Returns the numeric identifier of the most recent edition of an article matching the given context */ public function editionLatest(string $user, Context $context = null): int { $context = $context ?? new Context; - $q = new Query("SELECT max(arsse_editions.id) from arsse_editions left join arsse_articles on article = arsse_articles.id join arsse_subscriptions on arsse_articles.feed = arsse_subscriptions.feed and arsse_subscriptions.owner = ?", "str", $user); - if ($context->subscription()) { - // if a subscription is specified, make sure it exists - $this->subscriptionValidateId($user, $context->subscription); - // a simple WHERE clause is required here - $q->setWhere("arsse_subscriptions.id = ?", "int", $context->subscription); - } - return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); + $q = $this->articleQuery($user, $context, ["latest_edition"]); + return (int) $this->db->prepare((string) $q, $q->getTypes())->run($q->getValues())->getValue(); } /** Returns a map between all the given edition identifiers and their associated article identifiers */ @@ -1945,14 +1943,19 @@ class Database { return $this->db->prepare( "SELECT * FROM ( SELECT - id,name,coalesce(articles,0) as articles,coalesce(marked,0) as \"read\" + id, + name, + coalesce(articles - coalesce(hidden, 0), 0) as articles, + coalesce(marked, 0) as \"read\" from arsse_labels left join ( SELECT label, sum(assigned) as articles from arsse_label_members group by label ) as label_stats on label_stats.label = arsse_labels.id left join ( - SELECT - label, sum(\"read\") as marked + SELECT + label, + sum(hidden) as hidden, + sum(cast((\"read\" = 1 and hidden = 0) as integer)) as marked from arsse_marks join arsse_subscriptions on arsse_subscriptions.id = arsse_marks.subscription join arsse_label_members on arsse_label_members.article = arsse_marks.article @@ -2007,14 +2010,19 @@ class Database { $type = $byName ? "str" : "int"; $out = $this->db->prepare( "SELECT - id,name,coalesce(articles,0) as articles,coalesce(marked,0) as \"read\" + id, + name, + coalesce(articles - coalesce(hidden, 0), 0) as articles, + coalesce(marked, 0) as \"read\" FROM arsse_labels left join ( SELECT label, sum(assigned) as articles from arsse_label_members group by label ) as label_stats on label_stats.label = arsse_labels.id left join ( - SELECT - label, sum(\"read\") as marked + SELECT + label, + sum(hidden) as hidden, + sum(cast((\"read\" = 1 and hidden = 0) as integer)) as marked from arsse_marks join arsse_subscriptions on arsse_subscriptions.id = arsse_marks.subscription join arsse_label_members on arsse_label_members.article = arsse_marks.article @@ -2069,19 +2077,25 @@ class Database { * @param boolean $byName Whether to interpret the $id parameter as the label's name (true) or identifier (false) */ public function labelArticlesGet(string $user, $id, bool $byName = false): array { - // just do a syntactic check on the label ID - $this->labelValidateId($user, $id, $byName, false); - $field = !$byName ? "id" : "name"; - $type = !$byName ? "int" : "str"; - $out = $this->db->prepare("SELECT article from arsse_label_members join arsse_labels on label = id where assigned = 1 and $field = ? and owner = ? order by article", $type, "str")->run($id, $user)->getAll(); + $c = (new Context)->hidden(false); + if ($byName) { + $c->labelName($id); + } else { + $c->label($id); + } + try { + $q = $this->articleQuery($user, $c); + $out = $this->db->prepare((string) $q, $q->getTypes())->run($q->getValues())->getAll(); + } catch (Db\ExceptionInput $e) { + if ($e->getCode() === 10235) { + throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "label", 'id' => $id]); + } + throw $e; + } if (!$out) { - // if no results were returned, do a full validation on the label ID - $this->labelValidateId($user, $id, $byName, true, true); - // if the validation passes, return the empty result return $out; } else { - // flatten the result to return just the article IDs in a simple array - return array_column($out, "article"); + return array_column($out, "id"); } } diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 0653b580..033bbfde 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -251,7 +251,7 @@ trait SeriesArticle { [12, 3,0,1,'2017-01-01 00:00:00','ack',0], [12, 4,1,1,'2017-01-01 00:00:00','ach',0], [1, 2,0,0,'2010-01-01 00:00:00','Some Note',0], - [3, 5,0,0,'2000-01-01 00:00:00','',1], + [3, 6,0,0,'2000-01-01 00:00:00','',1], [6, 1,0,1,'2010-01-01 00:00:00','',1], [6, 2,1,0,'2010-01-01 00:00:00','',1], ], @@ -447,8 +447,8 @@ trait SeriesArticle { 'Starred and Read in subscription' => [(new Context)->starred(true)->unread(false)->subscription(5), []], 'Annotated' => [(new Context)->annotated(true), [2]], 'Not annotated' => [(new Context)->annotated(false), [1,3,4,5,6,7,8,19,20]], - 'Hidden' => [(new Context)->hidden(true), [5]], - 'Not hidden' => [(new Context)->hidden(false), [1,2,3,4,6,7,8,19,20]], + 'Hidden' => [(new Context)->hidden(true), [6]], + 'Not hidden' => [(new Context)->hidden(false), [1,2,3,4,5,7,8,19,20]], 'Labelled' => [(new Context)->labelled(true), [1,5,8,19,20]], 'Not labelled' => [(new Context)->labelled(false), [2,3,4,6,7]], 'Not after edition 999' => [(new Context)->subscription(5)->latestEdition(999), [19]], @@ -985,6 +985,7 @@ trait SeriesArticle { public function testFetchLatestEdition(): void { $this->assertSame(1001, Arsse::$db->editionLatest($this->user)); $this->assertSame(4, Arsse::$db->editionLatest($this->user, (new Context)->subscription(12))); + $this->assertSame(5, Arsse::$db->editionLatest("john.doe@example.com", (new Context)->subscription(3)->hidden(false))); } public function testFetchLatestEditionOfMissingSubscription(): void { diff --git a/tests/cases/Database/SeriesLabel.php b/tests/cases/Database/SeriesLabel.php index 58f3c979..ec82613c 100644 --- a/tests/cases/Database/SeriesLabel.php +++ b/tests/cases/Database/SeriesLabel.php @@ -194,20 +194,22 @@ trait SeriesLabel { 'read' => "bool", 'starred' => "bool", 'modified' => "datetime", + 'hidden' => "bool", ], 'rows' => [ - [1, 1,1,1,'2000-01-01 00:00:00'], - [5, 19,1,0,'2000-01-01 00:00:00'], - [5, 20,0,1,'2010-01-01 00:00:00'], - [7, 20,1,0,'2010-01-01 00:00:00'], - [8, 102,1,0,'2000-01-02 02:00:00'], - [9, 103,0,1,'2000-01-03 03:00:00'], - [9, 104,1,1,'2000-01-04 04:00:00'], - [10,105,0,0,'2000-01-05 05:00:00'], - [11, 19,0,0,'2017-01-01 00:00:00'], - [11, 20,1,0,'2017-01-01 00:00:00'], - [12, 3,0,1,'2017-01-01 00:00:00'], - [12, 4,1,1,'2017-01-01 00:00:00'], + [1, 1,1,1,'2000-01-01 00:00:00',0], + [5, 19,1,0,'2000-01-01 00:00:00',0], + [5, 20,0,1,'2010-01-01 00:00:00',0], + [7, 20,1,0,'2010-01-01 00:00:00',0], + [8, 102,1,0,'2000-01-02 02:00:00',0], + [9, 103,0,1,'2000-01-03 03:00:00',0], + [9, 104,1,1,'2000-01-04 04:00:00',0], + [10,105,0,0,'2000-01-05 05:00:00',0], + [11, 19,0,0,'2017-01-01 00:00:00',0], + [11, 20,1,0,'2017-01-01 00:00:00',0], + [12, 3,0,1,'2017-01-01 00:00:00',0], + [12, 4,1,1,'2017-01-01 00:00:00',0], + [4, 8,0,0,'2000-01-02 02:00:00',1] ], ], 'arsse_labels' => [ @@ -237,6 +239,7 @@ trait SeriesLabel { [2,20,5,1], [1, 5,3,0], [2, 5,3,1], + [2, 8,4,1], ], ], ]; From b2fae336e8dbb5175d47db58dbae6bed71a1a781 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 20 Dec 2020 17:42:28 -0500 Subject: [PATCH 080/366] Adjust Nextcloud News to ignore hidden items --- lib/REST/NextcloudNews/V1_2.php | 10 +- tests/cases/REST/NextcloudNews/TestV1_2.php | 109 ++++++++++---------- 2 files changed, 57 insertions(+), 62 deletions(-) diff --git a/lib/REST/NextcloudNews/V1_2.php b/lib/REST/NextcloudNews/V1_2.php index 5c5a9443..32405f87 100644 --- a/lib/REST/NextcloudNews/V1_2.php +++ b/lib/REST/NextcloudNews/V1_2.php @@ -333,7 +333,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(422); } // build the context - $c = new Context; + $c = (new Context)->hidden(false); $c->latestEdition((int) $data['newestItemId']); $c->folder((int) $url[1]); // perform the operation @@ -400,7 +400,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { $feed = Arsse::$db->subscriptionPropertiesGet(Arsse::$user->id, $id); $feed = $this->feedTranslate($feed); $out = ['feeds' => [$feed]]; - $newest = Arsse::$db->editionLatest(Arsse::$user->id, (new Context)->subscription($id)); + $newest = Arsse::$db->editionLatest(Arsse::$user->id, (new Context)->subscription($id)->hidden(false)); if ($newest) { $out['newestItemId'] = $newest; } @@ -482,7 +482,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(422); } // build the context - $c = new Context; + $c = (new Context)->hidden(false); $c->latestEdition((int) $data['newestItemId']); $c->subscription((int) $url[1]); // perform the operation @@ -498,7 +498,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // list articles and their properties protected function articleList(array $url, array $data): ResponseInterface { // set the context options supplied by the client - $c = new Context; + $c = (new Context)->hidden(false); // set the batch size if ($data['batchSize'] > 0) { $c->limit($data['batchSize']); @@ -578,7 +578,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(422); } // build the context - $c = new Context; + $c = (new Context)->hidden(false); $c->latestEdition((int) $data['newestItemId']); // perform the operation Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c); diff --git a/tests/cases/REST/NextcloudNews/TestV1_2.php b/tests/cases/REST/NextcloudNews/TestV1_2.php index 5e8c7d13..a88eb81e 100644 --- a/tests/cases/REST/NextcloudNews/TestV1_2.php +++ b/tests/cases/REST/NextcloudNews/TestV1_2.php @@ -524,7 +524,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db, \Phake::times(0))->editionLatest; } else { \Phake::verify(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, $id); - \Phake::verify(Arsse::$db)->editionLatest(Arsse::$user->id, (new Context)->subscription($id)); + \Phake::verify(Arsse::$db)->editionLatest(Arsse::$user->id, (new Context)->subscription($id)->hidden(false)); if ($input['folderId'] ?? 0) { \Phake::verify(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, $id, ['folder' => (int) $input['folderId']]); } else { @@ -650,65 +650,60 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[4]))); } - public function testListArticles(): void { - $t = new \DateTime; - $in = [ - ['type' => 0, 'id' => 42], // type=0 => subscription/feed - ['type' => 1, 'id' => 2112], // type=1 => folder - ['type' => 0, 'id' => -1], // type=0 => subscription/feed; invalid ID - ['type' => 1, 'id' => -1], // type=1 => folder; invalid ID - ['type' => 2, 'id' => 0], // type=2 => starred - ['type' => 3, 'id' => 0], // type=3 => all (default); base context - ['oldestFirst' => true, 'batchSize' => 10, 'offset' => 5], - ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 5], - ['getRead' => true], // base context - ['getRead' => false], - ['lastModified' => $t->getTimestamp()], - ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 0], // offset=0 should not set the latestEdition context + /** @dataProvider provideArticleQueries */ + public function testListArticles(string $url, array $in, Context $c, $out, ResponseInterface $exp): void { + if ($out instanceof \Exception) { + \Phake::when(Arsse::$db)->articleList->thenThrow($out); + } else { + \Phake::when(Arsse::$db)->articleList->thenReturn($out); + } + $this->assertMessage($exp, $this->req("GET", $url, $in)); + $columns = ["edition", "guid", "id", "url", "title", "author", "edited_date", "content", "media_type", "media_url", "subscription", "unread", "starred", "modified_date", "fingerprint"]; + $order = ($in['oldestFirst'] ?? false) ? "edition" : "edition desc"; + \Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, $c, $columns, [$order]); + } + + public function provideArticleQueries(): iterable { + $c = (new Context)->hidden(false); + $t = Date::normalize(time()); + $out = new Result($this->v($this->articles['db'])); + $r200 = new Response(['items' => $this->articles['rest']]); + $r422 = new EmptyResponse(422); + return [ + ["/items", [], clone $c, $out, $r200], + ["/items", ['type' => 0, 'id' => 42], (clone $c)->subscription(42), new ExceptionInput("idMissing"), $r422], + ["/items", ['type' => 1, 'id' => 2112], (clone $c)->folder(2112), new ExceptionInput("idMissing"), $r422], + ["/items", ['type' => 0, 'id' => -1], (clone $c)->subscription(-1), new ExceptionInput("typeViolation"), $r422], + ["/items", ['type' => 1, 'id' => -1], (clone $c)->folder(-1), new ExceptionInput("typeViolation"), $r422], + ["/items", ['type' => 2, 'id' => 0], (clone $c)->starred(true), $out, $r200], + ["/items", ['type' => 3, 'id' => 0], clone $c, $out, $r200], + ["/items", ['getRead' => true], clone $c, $out, $r200], + ["/items", ['getRead' => false], (clone $c)->unread(true), $out, $r200], + ["/items", ['lastModified' => $t->getTimestamp()], (clone $c)->markedSince($t), $out, $r200], + ["/items", ['oldestFirst' => true, 'batchSize' => 10, 'offset' => 5], (clone $c)->oldestEdition(6)->limit(10), $out, $r200], + ["/items", ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 5], (clone $c)->latestEdition(4)->limit(5), $out, $r200], + ["/items", ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 0], (clone $c)->limit(5), $out, $r200], + ["/items/updated", [], clone $c, $out, $r200], + ["/items/updated", ['type' => 0, 'id' => 42], (clone $c)->subscription(42), new ExceptionInput("idMissing"), $r422], + ["/items/updated", ['type' => 1, 'id' => 2112], (clone $c)->folder(2112), new ExceptionInput("idMissing"), $r422], + ["/items/updated", ['type' => 0, 'id' => -1], (clone $c)->subscription(-1), new ExceptionInput("typeViolation"), $r422], + ["/items/updated", ['type' => 1, 'id' => -1], (clone $c)->folder(-1), new ExceptionInput("typeViolation"), $r422], + ["/items/updated", ['type' => 2, 'id' => 0], (clone $c)->starred(true), $out, $r200], + ["/items/updated", ['type' => 3, 'id' => 0], clone $c, $out, $r200], + ["/items/updated", ['getRead' => true], clone $c, $out, $r200], + ["/items/updated", ['getRead' => false], (clone $c)->unread(true), $out, $r200], + ["/items/updated", ['lastModified' => $t->getTimestamp()], (clone $c)->markedSince($t), $out, $r200], + ["/items/updated", ['oldestFirst' => true, 'batchSize' => 10, 'offset' => 5], (clone $c)->oldestEdition(6)->limit(10), $out, $r200], + ["/items/updated", ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 5], (clone $c)->latestEdition(4)->limit(5), $out, $r200], + ["/items/updated", ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 0], (clone $c)->limit(5), $out, $r200], ]; - \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 - $this->assertMessage($exp, $this->req("GET", "/items/updated")); // second instance of base context - // check error conditions - $exp = new EmptyResponse(422); - $this->assertMessage($exp, $this->req("GET", "/items", json_encode($in[0]))); - $this->assertMessage($exp, $this->req("GET", "/items", json_encode($in[1]))); - $this->assertMessage($exp, $this->req("GET", "/items", json_encode($in[2]))); - $this->assertMessage($exp, $this->req("GET", "/items", json_encode($in[3]))); - // simply run through the remainder of the input for later method verification - $this->req("GET", "/items", json_encode($in[4])); - $this->req("GET", "/items", json_encode($in[5])); // third instance of base context - $this->req("GET", "/items", json_encode($in[6])); - $this->req("GET", "/items", json_encode($in[7])); - $this->req("GET", "/items", json_encode($in[8])); // fourth instance of base context - $this->req("GET", "/items", json_encode($in[9])); - $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, $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(): void { $read = ['read' => true]; $in = json_encode(['newestItemId' => 2112]); - \Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->folder(1)->latestEdition(2112))->thenReturn(42); - \Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->folder(42)->latestEdition(2112))->thenThrow(new ExceptionInput("idMissing")); // folder doesn't exist + \Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->folder(1)->latestEdition(2112)->hidden(false))->thenReturn(42); + \Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->folder(42)->latestEdition(2112)->hidden(false))->thenThrow(new ExceptionInput("idMissing")); // folder doesn't exist $exp = new EmptyResponse(204); $this->assertMessage($exp, $this->req("PUT", "/folders/1/read", $in)); $this->assertMessage($exp, $this->req("PUT", "/folders/1/read?newestItemId=2112")); @@ -722,8 +717,8 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { public function testMarkASubscriptionRead(): void { $read = ['read' => true]; $in = json_encode(['newestItemId' => 2112]); - \Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->subscription(1)->latestEdition(2112))->thenReturn(42); - \Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->subscription(42)->latestEdition(2112))->thenThrow(new ExceptionInput("idMissing")); // subscription doesn't exist + \Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->subscription(1)->latestEdition(2112)->hidden(false))->thenReturn(42); + \Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->subscription(42)->latestEdition(2112)->hidden(false))->thenThrow(new ExceptionInput("idMissing")); // subscription doesn't exist $exp = new EmptyResponse(204); $this->assertMessage($exp, $this->req("PUT", "/feeds/1/read", $in)); $this->assertMessage($exp, $this->req("PUT", "/feeds/1/read?newestItemId=2112")); @@ -890,6 +885,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)->starred(true), $this->anything(), ["edition"]); + \Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->starred(true)->hidden(false), $this->anything(), ["edition"]); } } From b7ce6f5c790f0aa60dd16af29c9cf8e806c2172d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 20 Dec 2020 19:32:07 -0500 Subject: [PATCH 081/366] Adjust Fever to ignore hidden items --- lib/REST/Fever/API.php | 23 ++++++------ tests/cases/REST/Fever/TestAPI.php | 60 +++++++++++++++--------------- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/lib/REST/Fever/API.php b/lib/REST/Fever/API.php index 3382e6cb..2d5fcc1c 100644 --- a/lib/REST/Fever/API.php +++ b/lib/REST/Fever/API.php @@ -161,17 +161,17 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } if ($G['items']) { $out['items'] = $this->getItems($G); - $out['total_items'] = Arsse::$db->articleCount(Arsse::$user->id); + $out['total_items'] = Arsse::$db->articleCount(Arsse::$user->id, (new Context)->hidden(false)); } 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)); + $out['unread_item_ids'] = $this->getItemIds((new Context)->unread(true)->hidden(false)); } if ($G['saved_item_ids'] || $listSaved) { - $out['saved_item_ids'] = $this->getItemIds((new Context)->starred(true)); + $out['saved_item_ids'] = $this->getItemIds((new Context)->starred(true)->hidden(false)); } return $out; } @@ -263,17 +263,18 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { case "group": if ($id > 0) { // concrete groups - $c->tag($id); + $c->tag($id)->hidden(false); } 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 + // only exclude hidden articles + $c->hidden(false); } break; case "feed": - $c->subscription($id); + $c->subscription($id)->hidden(false); break; default: return $listSaved; @@ -308,7 +309,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function setUnread(): void { - $lastUnread = Arsse::$db->articleList(Arsse::$user->id, (new Context)->limit(1), ["marked_date"], ["marked_date desc"])->getValue(); + $lastUnread = Arsse::$db->articleList(Arsse::$user->id, (new Context)->hidden(false)->limit(1), ["marked_date"], ["marked_date desc"])->getValue(); if (!$lastUnread) { // there are no articles return; @@ -316,7 +317,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // 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; + $c = (new Context)->hidden(false); $lastUnread = Date::normalize($lastUnread, "sql"); $since = Date::sub("PT15S", $lastUnread); $c->unread(false)->markedSince($since); @@ -373,11 +374,11 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function getItems(array $G): array { - $c = (new Context)->limit(50); + $c = (new Context)->hidden(false)->limit(50); $reverse = false; // handle the standard options if ($G['with_ids']) { - $c->articles(explode(",", $G['with_ids'])); + $c->articles(explode(",", $G['with_ids']))->hidden(null); } elseif ($G['max_id']) { $c->latestArticle($G['max_id'] - 1); $reverse = true; @@ -410,7 +411,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { return $out; } - protected function getItemIds(Context $c = null): string { + protected function getItemIds(Context $c): string { $out = []; foreach (Arsse::$db->articleList(Arsse::$user->id, $c) as $r) { $out[] = (int) $r['id']; diff --git a/tests/cases/REST/Fever/TestAPI.php b/tests/cases/REST/Fever/TestAPI.php index d0632c97..1aa77ba3 100644 --- a/tests/cases/REST/Fever/TestAPI.php +++ b/tests/cases/REST/Fever/TestAPI.php @@ -303,7 +303,7 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { $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); + \Phake::when(Arsse::$db)->articleCount(Arsse::$user->id, (new Context)->hidden(false))->thenReturn(1024); $exp = new JsonResponse([ 'items' => $this->articles['rest'], 'total_items' => 1024, @@ -316,24 +316,24 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { public function provideItemListContexts(): iterable { $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", (clone $c)->hidden(false), false], + ["items&group_ids=1,2,3,4", (clone $c)->tags([1,2,3,4])->hidden(false), false], + ["items&feed_ids=1,2,3,4", (clone $c)->subscriptions([1,2,3,4])->hidden(false), 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&since_id=1", (clone $c)->oldestArticle(2)->hidden(false), false], + ["items&max_id=2", (clone $c)->latestArticle(1)->hidden(false), 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], + ["items&max_id=3&since_id=6", (clone $c)->latestArticle(2)->hidden(false), true], + ["items&feed_ids=1,2,3,4&since_id=6", (clone $c)->subscriptions([1,2,3,4])->oldestArticle(7)->hidden(false), false], ]; } public function testListItemIds(): void { $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)->articleList(Arsse::$user->id, (new Context)->starred(true)->hidden(false))->thenReturn(new Result($saved)); + \Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->unread(true)->hidden(false))->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"]); @@ -350,8 +350,8 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { public function testSetMarks(string $post, Context $c, array $data, array $out): void { $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)->articleList(Arsse::$user->id, (new Context)->starred(true)->hidden(false))->thenReturn(new Result($saved)); + \Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->unread(true)->hidden(false))->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); @@ -368,8 +368,8 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { public function testSetMarksWithQuery(string $get, Context $c, array $data, array $out): void { $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)->articleList(Arsse::$user->id, (new Context)->starred(true)->hidden(false))->thenReturn(new Result($saved)); + \Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->unread(true)->hidden(false))->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); @@ -395,20 +395,20 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { ["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=feed&as=read&id=5", (new Context)->subscription(5)->hidden(false), $markRead, $listUnread], + ["mark=feed&as=unread&id=42", (new Context)->subscription(42)->hidden(false), $markUnread, $listUnread], + ["mark=feed&as=saved&id=5", (new Context)->subscription(5)->hidden(false), $markSaved, $listSaved], + ["mark=feed&as=unsaved&id=42", (new Context)->subscription(42)->hidden(false), $markUnsaved, $listSaved], + ["mark=group&as=read&id=5", (new Context)->tag(5)->hidden(false), $markRead, $listUnread], + ["mark=group&as=unread&id=42", (new Context)->tag(42)->hidden(false), $markUnread, $listUnread], + ["mark=group&as=saved&id=5", (new Context)->tag(5)->hidden(false), $markSaved, $listSaved], + ["mark=group&as=unsaved&id=42", (new Context)->tag(42)->hidden(false), $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=0", (new Context)->hidden(false), $markRead, $listUnread], + ["mark=group&as=unread&id=0", (new Context)->hidden(false), $markUnread, $listUnread], + ["mark=group&as=saved&id=0", (new Context)->hidden(false), $markSaved, $listSaved], + ["mark=group&as=unsaved&id=0", (new Context)->hidden(false), $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], @@ -466,14 +466,14 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { public function testUndoReadMarks(): void { $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)->articleList(Arsse::$user->id, (new Context)->limit(1)->hidden(false), ["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)->hidden(false))->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([])); + \Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, ['read' => false], (new Context)->unread(false)->markedSince("1999-12-31T23:59:45Z")->hidden(false)); + \Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->limit(1)->hidden(false), ["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 From f33359f3e3d45800f99561364234adeef4cbc985 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 20 Dec 2020 22:30:59 -0500 Subject: [PATCH 082/366] Move some Miniflux features to abstract handler --- lib/REST/AbstractHandler.php | 10 ++++++++++ lib/REST/Miniflux/V1.php | 10 ---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/REST/AbstractHandler.php b/lib/REST/AbstractHandler.php index 6060da42..f0e39e79 100644 --- a/lib/REST/AbstractHandler.php +++ b/lib/REST/AbstractHandler.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\REST; +use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\ValueInfo; use Psr\Http\Message\ServerRequestInterface; @@ -15,6 +16,15 @@ abstract class AbstractHandler implements Handler { abstract public function __construct(); abstract public function dispatch(ServerRequestInterface $req): ResponseInterface; + /** @codeCoverageIgnore */ + protected function now(): \DateTimeImmutable { + return Date::normalize("now"); + } + + protected function isAdmin(): bool { + return (bool) Arsse::$user->propertiesGet(Arsse::$user->id, false)['admin']; + } + protected function fieldMapNames(array $data, array $map): array { $out = []; foreach ($map as $to => $from) { diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 99c6a9b2..5474dc0b 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -116,11 +116,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { public function __construct() { } - /** @codeCoverageIgnore */ - protected function now(): \DateTimeImmutable { - return Date::normalize("now"); - } - protected function authenticate(ServerRequestInterface $req): bool { // first check any tokens; this is what Miniflux does if ($req->hasHeader("X-Auth-Token")) { @@ -143,11 +138,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return false; } - protected function isAdmin(): bool { - return (bool) Arsse::$user->propertiesGet(Arsse::$user->id, false)['admin']; - } - - public function dispatch(ServerRequestInterface $req): ResponseInterface { // try to authenticate if (!$this->authenticate($req)) { From ade04022106f8a1ff99535ccd1d0a21649b13b2e Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 21 Dec 2020 21:49:57 -0500 Subject: [PATCH 083/366] Adjust TT-RSS to ignore hidden items --- lib/REST/TinyTinyRSS/API.php | 42 +-- tests/cases/REST/TinyTinyRSS/TestAPI.php | 317 ++++++++--------------- 2 files changed, 130 insertions(+), 229 deletions(-) diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index 2df402a0..9f8ea590 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -26,6 +26,7 @@ use Laminas\Diactoros\Response\EmptyResponse; class API extends \JKingWeb\Arsse\REST\AbstractHandler { public const LEVEL = 14; // emulated API level public const VERSION = "17.4"; // emulated TT-RSS version + protected const LABEL_OFFSET = 1024; // offset below zero at which labels begin, counting down protected const LIMIT_ARTICLES = 200; // maximum number of articles returned by getHeadlines protected const LIMIT_EXCERPT = 100; // maximum length of excerpts in getHeadlines, counted in grapheme units @@ -81,6 +82,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { 'mode' => ValueInfo::T_INT, // whether to set, clear, or toggle the selected state in `updateArticle` 'data' => ValueInfo::T_STRING, // note text in `updateArticle` if setting a note ]; + protected const VIEW_MODES = ["all_articles", "adaptive", "unread", "marked", "has_note", "published"]; // generic error construct protected const FATAL_ERR = [ 'seq' => null, @@ -234,7 +236,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { public function opGetCounters(array $data): array { $user = Arsse::$user->id; $starred = Arsse::$db->articleStarred($user); - $fresh = Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H"))); + $fresh = Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H", $this->now()))->hidden(false)); $countAll = 0; $countSubs = 0; $feeds = []; @@ -339,7 +341,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { 'id' => "FEED:".self::FEED_FRESH, 'bare_id' => self::FEED_FRESH, 'icon' => "images/fresh.png", - 'unread' => Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H"))), + 'unread' => Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H", $this->now()))->hidden(false)), ], $tSpecial), array_merge([ // Starred articles 'name' => Arsse::$lang->msg("API.TTRSS.Feed.Starred"), @@ -391,7 +393,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { ]; $unread += ($l['articles'] - $l['read']); } - // if there are labels, all the label category, + // if there are labels, add the "Labels" category, if ($items) { $out[] = [ 'name' => Arsse::$lang->msg("API.TTRSS.Category.Labels"), @@ -523,7 +525,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // FIXME: this is pretty inefficient $f = $map[self::CAT_SPECIAL]; $cats[$f]['unread'] += Arsse::$db->articleStarred($user)['unread']; // starred - $cats[$f]['unread'] += Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H"))); // fresh + $cats[$f]['unread'] += Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H", $this->now()))->hidden(false)); // fresh if (!$read) { // if we're only including unread entries, remove any categories with zero unread items (this will by definition also exclude empties) $count = sizeof($cats); @@ -675,8 +677,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { if ($cat == self::CAT_ALL || $cat == self::CAT_SPECIAL) { // gather some statistics $starred = Arsse::$db->articleStarred($user)['unread']; - $fresh = Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H"))); - $global = Arsse::$db->articleCount($user, (new Context)->unread(true)); + $fresh = Arsse::$db->articleCount($user, (new Context)->unread(true)->modifiedSince(Date::sub("PT24H", $this->now()))->hidden(false)); + $global = Arsse::$db->articleCount($user, (new Context)->unread(true)->hidden(false)); $published = 0; // TODO: if the Published feed is implemented, the getFeeds method needs to be adjusted accordingly $archived = 0; // the archived feed is non-functional in the TT-RSS protocol itself // build the list; exclude anything with zero unread if requested @@ -737,7 +739,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // NOTE: the list is a flat one: it includes children, but not other descendents foreach (Arsse::$db->folderList($user, $cat, false) as $c) { // get the number of unread for the category and its descendents; those with zero unread are excluded in "unread-only" mode - $count = Arsse::$db->articleCount($user, (new Context)->unread(true)->folder((int) $c['id'])); + $count = Arsse::$db->articleCount($user, (new Context)->unread(true)->folder((int) $c['id'])->hidden(false)); if (!$unread || $count) { $out[] = [ 'id' => (int) $c['id'], @@ -1037,7 +1039,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { $cat = $data['is_cat'] ?? false; $out = ['status' => "OK"]; // first prepare the context; unsupported contexts simply return early - $c = new Context; + $c = (new Context)->hidden(false); if ($cat) { // categories switch ($id) { case self::CAT_SPECIAL: @@ -1073,7 +1075,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // TODO: if the Published feed is implemented, the catchup function needs to be modified accordingly return $out; case self::FEED_FRESH: - $c->modifiedSince(Date::sub("PT24H")); + $c->modifiedSince(Date::sub("PT24H", $this->now())); break; case self::FEED_ALL: // no context needed here @@ -1188,6 +1190,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { "id", "guid", "title", + "author", "url", "unread", "starred", @@ -1296,10 +1299,14 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { "subscription", "subscription_title", "note", - ($data['show_content'] || $data['show_excerpt']) ? "content" : "", - ($data['include_attachments']) ? "media_url": "", - ($data['include_attachments']) ? "media_type": "", ]; + if ($data['show_content'] || $data['show_excerpt']) { + $columns[] = "content"; + } + if ($data['include_attachments']) { + $columns[] = "media_url"; + $columns[] = "media_type"; + } foreach ($this->fetchArticles($data, $columns) as $article) { $row = [ 'id' => (int) $article['id'], @@ -1387,9 +1394,10 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { $id = $data['feed_id']; $cat = $data['is_cat'] ?? false; $shallow = !($data['include_nested'] ?? false); - $viewMode = in_array($data['view_mode'], ["all_articles", "adaptive", "unread", "marked", "has_note", "published"]) ? $data['view_mode'] : "all_articles"; + $viewMode = in_array($data['view_mode'], self::VIEW_MODES) ? $data['view_mode'] : "all_articles"; + assert(in_array($viewMode, self::VIEW_MODES), new \JKingWeb\Arsse\Exception("constantUnknown", $viewMode)); // prepare the context; unsupported, invalid, or inherently empty contexts return synthetic empty result sets - $c = new Context; + $c = (new Context)->hidden(false); $tr = Arsse::$db->begin(); // start with the feed or category ID if ($cat) { // categories @@ -1433,13 +1441,13 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // TODO: if the Published feed is implemented, the headline function needs to be modified accordingly return new ResultEmpty; case self::FEED_FRESH: - $c->modifiedSince(Date::sub("PT24H"))->unread(true); + $c->modifiedSince(Date::sub("PT24H", $this->now()))->unread(true); break; case self::FEED_ALL: // no context needed here break; case self::FEED_READ: - $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 + $c->markedSince(Date::sub("PT24H", $this->now()))->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 @@ -1477,8 +1485,6 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // not implemented // TODO: if the Published feed is implemented, the headline function needs to be modified accordingly return new ResultEmpty; - default: - throw new \JKingWeb\Arsse\Exception("constantUnknown", $viewMode); // @codeCoverageIgnore } // handle the search string, if any if (isset($data['search'])) { diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index d5ca279f..abcbdd9b 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -23,6 +23,8 @@ use Laminas\Diactoros\Response\EmptyResponse; /** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\API * @covers \JKingWeb\Arsse\REST\TinyTinyRSS\Exception */ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { + protected const NOW = "2020-12-21T23:09:17.189065Z"; + protected $h; protected $folders = [ ['id' => 5, 'parent' => 3, 'children' => 0, 'feeds' => 1, 'name' => "Local"], @@ -1113,7 +1115,7 @@ LONG_STRING; \Phake::when(Arsse::$db)->folderList($this->anything(), null, false)->thenReturn(new Result($this->v($this->topFolders))); \Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($this->v($this->subscriptions))); \Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result($this->v($this->labels))); - \Phake::when(Arsse::$db)->articleCount($this->anything(), $this->equalTo((new Context)->unread(true)->modifiedSince(Date::sub("PT24H")), 2))->thenReturn(7); + \Phake::when(Arsse::$db)->articleCount($this->anything(), $this->equalTo((new Context)->hidden(false)->unread(true)->modifiedSince(Date::sub("PT24H")), 2))->thenReturn(7); \Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn($this->v($this->starred)); $exp = [ [ @@ -1177,7 +1179,7 @@ LONG_STRING; \Phake::when(Arsse::$db)->folderList($this->anything())->thenReturn(new Result($this->v($this->folders))); \Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($this->v($this->subscriptions))); \Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result($this->v($this->usedLabels))); - \Phake::when(Arsse::$db)->articleCount($this->anything(), $this->equalTo((new Context)->unread(true)->modifiedSince(Date::sub("PT24H")), 2))->thenReturn(7); + \Phake::when(Arsse::$db)->articleCount($this->anything(), $this->equalTo((new Context)->hidden(false)->unread(true)->modifiedSince(Date::sub("PT24H")), 2))->thenReturn(7); \Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn($this->v($this->starred)); $exp = [ ['id' => "global-unread", 'counter' => 35], @@ -1298,7 +1300,7 @@ LONG_STRING; \Phake::when(Arsse::$db)->folderList($this->anything(), null, true)->thenReturn(new Result($this->v($this->folders))); \Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($this->v($this->subscriptions))); \Phake::when(Arsse::$db)->labelList($this->anything(), true)->thenReturn(new Result($this->v($this->labels))); - \Phake::when(Arsse::$db)->articleCount($this->anything(), $this->equalTo((new Context)->unread(true)->modifiedSince(Date::sub("PT24H")), 2))->thenReturn(7); + \Phake::when(Arsse::$db)->articleCount($this->anything(), $this->equalTo((new Context)->hidden(false)->unread(true)->modifiedSince(Date::sub("PT24H")), 2))->thenReturn(7); \Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn($this->v($this->starred)); // the expectations are packed tightly since they're very verbose; one can use var_export() (or convert to JSON) to pretty-print them $exp = ['categories' => ['identifier' => 'id','label' => 'name','items' => [['name' => 'Special','id' => 'CAT:-1','bare_id' => -1,'type' => 'category','unread' => 0,'items' => [['name' => 'All articles','id' => 'FEED:-4','bare_id' => -4,'icon' => 'images/folder.png','unread' => 35,'type' => 'feed','auxcounter' => 0,'error' => '','updated' => ''],['name' => 'Fresh articles','id' => 'FEED:-3','bare_id' => -3,'icon' => 'images/fresh.png','unread' => 7,'type' => 'feed','auxcounter' => 0,'error' => '','updated' => ''],['name' => 'Starred articles','id' => 'FEED:-1','bare_id' => -1,'icon' => 'images/star.png','unread' => 4,'type' => 'feed','auxcounter' => 0,'error' => '','updated' => ''],['name' => 'Published articles','id' => 'FEED:-2','bare_id' => -2,'icon' => 'images/feed.png','unread' => 0,'type' => 'feed','auxcounter' => 0,'error' => '','updated' => ''],['name' => 'Archived articles','id' => 'FEED:0','bare_id' => 0,'icon' => 'images/archive.png','unread' => 0,'type' => 'feed','auxcounter' => 0,'error' => '','updated' => ''],['name' => 'Recently read','id' => 'FEED:-6','bare_id' => -6,'icon' => 'images/time.png','unread' => 0,'type' => 'feed','auxcounter' => 0,'error' => '','updated' => '']]],['name' => 'Labels','id' => 'CAT:-2','bare_id' => -2,'type' => 'category','unread' => 6,'items' => [['name' => 'Fascinating','id' => 'FEED:-1027','bare_id' => -1027,'unread' => 0,'icon' => 'images/label.png','type' => 'feed','auxcounter' => 0,'error' => '','updated' => '','fg_color' => '','bg_color' => ''],['name' => 'Interesting','id' => 'FEED:-1029','bare_id' => -1029,'unread' => 0,'icon' => 'images/label.png','type' => 'feed','auxcounter' => 0,'error' => '','updated' => '','fg_color' => '','bg_color' => ''],['name' => 'Logical','id' => 'FEED:-1025','bare_id' => -1025,'unread' => 0,'icon' => 'images/label.png','type' => 'feed','auxcounter' => 0,'error' => '','updated' => '','fg_color' => '','bg_color' => '']]],['name' => 'Photography','id' => 'CAT:4','bare_id' => 4,'parent_id' => null,'type' => 'category','auxcounter' => 0,'unread' => 0,'child_unread' => 0,'checkbox' => false,'param' => '(0 feeds)','items' => []],['name' => 'Politics','id' => 'CAT:3','bare_id' => 3,'parent_id' => null,'type' => 'category','auxcounter' => 0,'unread' => 0,'child_unread' => 0,'checkbox' => false,'param' => '(3 feeds)','items' => [['name' => 'Local','id' => 'CAT:5','bare_id' => 5,'parent_id' => 3,'type' => 'category','auxcounter' => 0,'unread' => 0,'child_unread' => 0,'checkbox' => false,'param' => '(1 feed)','items' => [['name' => 'Toronto Star','id' => 'FEED:2','bare_id' => 2,'icon' => 'feed-icons/2.ico','error' => 'oops','param' => '2011-11-11T11:11:11Z','unread' => 0,'auxcounter' => 0,'checkbox' => false]]],['name' => 'National','id' => 'CAT:6','bare_id' => 6,'parent_id' => 3,'type' => 'category','auxcounter' => 0,'unread' => 0,'child_unread' => 0,'checkbox' => false,'param' => '(2 feeds)','items' => [['name' => 'CBC News','id' => 'FEED:4','bare_id' => 4,'icon' => 'feed-icons/4.ico','error' => '','param' => '2017-10-09T15:58:34Z','unread' => 0,'auxcounter' => 0,'checkbox' => false],['name' => 'Ottawa Citizen','id' => 'FEED:5','bare_id' => 5,'icon' => false,'error' => '','param' => '2017-07-07T17:07:17Z','unread' => 0,'auxcounter' => 0,'checkbox' => false]]]]],['name' => 'Science','id' => 'CAT:1','bare_id' => 1,'parent_id' => null,'type' => 'category','auxcounter' => 0,'unread' => 0,'child_unread' => 0,'checkbox' => false,'param' => '(2 feeds)','items' => [['name' => 'Rocketry','id' => 'CAT:2','bare_id' => 2,'parent_id' => 1,'type' => 'category','auxcounter' => 0,'unread' => 0,'child_unread' => 0,'checkbox' => false,'param' => '(1 feed)','items' => [['name' => 'NASA JPL','id' => 'FEED:1','bare_id' => 1,'icon' => false,'error' => '','param' => '2017-09-15T22:54:16Z','unread' => 0,'auxcounter' => 0,'checkbox' => false]]],['name' => 'Ars Technica','id' => 'FEED:3','bare_id' => 3,'icon' => 'feed-icons/3.ico','error' => 'argh','param' => '2016-05-23T06:40:02Z','unread' => 0,'auxcounter' => 0,'checkbox' => false]]],['name' => 'Uncategorized','id' => 'CAT:0','bare_id' => 0,'type' => 'category','auxcounter' => 0,'unread' => 0,'child_unread' => 0,'checkbox' => false,'parent_id' => null,'param' => '(1 feed)','items' => [['name' => 'Eurogamer','id' => 'FEED:6','bare_id' => 6,'icon' => 'feed-icons/6.ico','error' => '','param' => '2010-02-12T20:08:47Z','unread' => 0,'auxcounter' => 0,'checkbox' => false]]]]]]; @@ -1343,19 +1345,19 @@ LONG_STRING; for ($a = 0; $a < sizeof($in2); $a++) { $this->assertMessage($exp, $this->req($in2[$a]), "Test $a failed"); } - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], new Context); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->starred(true)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->label(1088)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->subscription(2112)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->folder(42)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->folderShallow(0)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->labelled(true)); + \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->hidden(false)); + \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->starred(true)->hidden(false)); + \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->label(1088)->hidden(false)); + \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->subscription(2112)->hidden(false)); + \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->folder(42)->hidden(false)); + \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->folderShallow(0)->hidden(false)); + \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->labelled(true)->hidden(false)); // verify the time-based mock $t = Date::sub("PT24H"); for ($a = 0; $a < sizeof($in3); $a++) { $this->assertMessage($exp, $this->req($in3[$a]), "Test $a failed"); } - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], $this->equalTo((new Context)->modifiedSince($t), 2)); // within two seconds + \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], $this->equalTo((new Context)->hidden(false)->modifiedSince($t), 2)); // within two seconds } public function testRetrieveFeedList(): void { @@ -1385,8 +1387,8 @@ LONG_STRING; ]; // statistical mocks \Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn($this->v($this->starred)); - \Phake::when(Arsse::$db)->articleCount($this->anything(), $this->equalTo((new Context)->unread(true)->modifiedSince(Date::sub("PT24H")), 2))->thenReturn(7); - \Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(35); + \Phake::when(Arsse::$db)->articleCount($this->anything(), $this->equalTo((new Context)->unread(true)->hidden(false)->modifiedSince(Date::sub("PT24H")), 2))->thenReturn(7); + \Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true)->hidden(false))->thenReturn(35); // label mocks \Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result($this->v($this->labels))); \Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result($this->v($this->usedLabels))); @@ -1400,7 +1402,7 @@ LONG_STRING; \Phake::when(Arsse::$db)->folderList($this->anything(), null, false)->thenReturn(new Result($this->v($this->filterFolders(null)))); foreach ($this->folders as $f) { \Phake::when(Arsse::$db)->folderList($this->anything(), $f['id'], false)->thenReturn(new Result($this->v($this->filterFolders($f['id'])))); - \Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true)->folder($f['id']))->thenReturn($this->reduceFolders($f['id'])); + \Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true)->hidden(false)->folder($f['id']))->thenReturn($this->reduceFolders($f['id'])); \Phake::when(Arsse::$db)->subscriptionList($this->anything(), $f['id'], false)->thenReturn(new Result($this->v($this->filterSubs($f['id'])))); } $exp = [ @@ -1694,206 +1696,99 @@ LONG_STRING; $this->assertMessage($this->respGood([$exp[0]]), $this->req($in[5])); } - public function testRetrieveCompactHeadlines(): void { - $in1 = [ - // erroneous input - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx"], - // empty results - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2, 'is_cat' => true], // is_cat is not used in getCompactHeadlines - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'view_mode' => "published"], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6, 'view_mode' => "unread"], - // non-empty results - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'view_mode' => "adaptive"], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112, 'view_mode' => "adaptive"], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112, 'view_mode' => "unread"], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'view_mode' => "marked"], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'view_mode' => "has_note"], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'limit' => 5], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'skip' => 2], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'limit' => 5, 'skip' => 2], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'since_id' => 47], - ]; - $in2 = [ - // time-based contexts, handled separately - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6, 'view_mode' => "adaptive"], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3], - ['op' => "getCompactHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3, 'view_mode' => "marked"], - ]; - \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); - \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([]), - $this->respGood([]), - $this->respGood([]), - $this->respGood([]), - $this->respGood([]), - $this->respGood([]), - $this->respGood([['id' => 101],['id' => 102]]), - $this->respGood([['id' => 1]]), - $this->respGood([['id' => 2]]), - $this->respGood([['id' => 3]]), - $this->respGood([['id' => 2]]), // the result is 2 rather than 4 because there are no unread, so the unread context is not used - $this->respGood([['id' => 4]]), - $this->respGood([['id' => 5]]), - $this->respGood([['id' => 6]]), - $this->respGood([['id' => 7]]), - $this->respGood([['id' => 8]]), - $this->respGood([['id' => 9]]), - $this->respGood([['id' => 10]]), - ]; - $out2 = [ - $this->respGood([['id' => 1001]]), - $this->respGood([['id' => 1001]]), - $this->respGood([['id' => 1002]]), - $this->respGood([['id' => 1003]]), - ]; - for ($a = 0; $a < sizeof($in1); $a++) { - $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"], ["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"); - } - } - - public function testRetrieveFullHeadlines(): void { - $in1 = [ - // empty results - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1, 'is_cat' => true], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'view_mode' => "published"], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6, 'view_mode' => "unread"], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'view_mode' => "unread", 'search' => "unread:false"], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'search' => "pub:true"], - ]; - $in2 = [ - // simple context tests - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx"], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'view_mode' => "adaptive"], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112, 'view_mode' => "adaptive"], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112, 'view_mode' => "unread"], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'view_mode' => "marked"], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'view_mode' => "has_note"], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'limit' => 5], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'skip' => 2], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'limit' => 5, 'skip' => 2], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'since_id' => 47], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3, 'is_cat' => true], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'is_cat' => true], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2, 'is_cat' => true], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0, 'is_cat' => true], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'is_cat' => true], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'is_cat' => true, 'include_nested' => true], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'order_by' => "feed_dates"], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'order_by' => "date_reverse"], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'search' => "interesting"], - ]; - $in3 = [ - // time-based context tests - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6, 'view_mode' => "adaptive"], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3], - ['op' => "getHeadlines", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3, 'view_mode' => "marked"], - ]; - \Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result($this->v($this->labels))); + /** @dataProvider provideHeadlines */ + public function testRetrieveHeadlines(bool $full, array $in, $out, Context $c, array $fields, array $order, ResponseInterface $exp): void { + $base = ['op' => $full ? "getHeadlines" : "getCompactHeadlines", 'sid' => "PriestsOfSyrinx"]; + $in = array_merge($base, $in); + $this->h = \Phake::partialMock(API::class); + \Phake::when($this->h)->now->thenReturn(Date::normalize(self::NOW)); + \Phake::when(Arsse::$db)->labelList->thenReturn(new Result($this->v($this->labels))); \Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result($this->v($this->usedLabels))); \Phake::when(Arsse::$db)->articleLabelsGet->thenReturn([]); \Phake::when(Arsse::$db)->articleLabelsGet($this->anything(), 2112)->thenReturn($this->v([1,3])); \Phake::when(Arsse::$db)->articleCategoriesGet->thenReturn([]); \Phake::when(Arsse::$db)->articleCategoriesGet($this->anything(), 2112)->thenReturn(["Boring","Illogical"]); - \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); - \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), - $this->outputHeadlines(1), - $this->outputHeadlines(2), - $this->outputHeadlines(3), - $this->outputHeadlines(2), // the result is 2 rather than 4 because there are no unread, so the unread context is not used - $this->outputHeadlines(4), - $this->outputHeadlines(5), - $this->outputHeadlines(6), - $this->outputHeadlines(7), - $this->outputHeadlines(8), - $this->outputHeadlines(9), - $this->outputHeadlines(10), - $this->outputHeadlines(11), - $this->outputHeadlines(11), - $this->outputHeadlines(12), - $this->outputHeadlines(13), - $this->outputHeadlines(14), - $this->outputHeadlines(15), - $this->outputHeadlines(11), // defaulting sorting is not fully implemented - $this->outputHeadlines(16), - $this->outputHeadlines(17), + \Phake::when(Arsse::$db)->articleCount->thenReturn(2); + if ($out instanceof \Exception) { + \Phake::when(Arsse::$db)->articleList->thenThrow($out); + } else { + \Phake::when(Arsse::$db)->articleList->thenReturn($out); + } + $this->assertMessage($exp, $this->req($in)); + if ($out) { + \Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, $c, $fields, $order); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->articleList; + } + } + + public function provideHeadlines(): iterable { + $t = Date::normalize(self::NOW); + $c = (new Context)->hidden(false)->limit(200); + $out = $this->generateHeadlines(47); + $gone = new ExceptionInput("idMissing"); + $comp = new Result($this->v([['id' => 47], ['id' => 2112]])); + $expFull = $this->outputHeadlines(47); + $expComp = $this->respGood([['id' => 47], ['id' => 2112]]); + $fields = ["id", "guid", "title", "author", "url", "unread", "starred", "edited_date", "published_date", "subscription", "subscription_title", "note"]; + $sort = ["edited_date desc"]; + return [ + [true, [], null, $c, [], [], $this->respErr("INCORRECT_USAGE")], + [true, ['feed_id' => 0], null, $c, [], [], $this->respGood([])], + [true, ['feed_id' => -1], $out, (clone $c)->starred(true), $fields, ["marked_date desc"], $expFull], + [true, ['feed_id' => -2], null, $c, [], [], $this->respGood([])], + [true, ['feed_id' => -4], $out, $c, $fields, $sort, $expFull], + [true, ['feed_id' => 2112], $gone, (clone $c)->subscription(2112), $fields, $sort, $this->respGood([])], + [true, ['feed_id' => -2112], $out, (clone $c)->label(1088), $fields, $sort, $expFull], + [true, ['feed_id' => -4, 'view_mode' => "adaptive"], $out, (clone $c)->unread(true), $fields, $sort, $expFull], + [true, ['feed_id' => -4, 'view_mode' => "published"], null, $c, [], [], $this->respGood([])], + [true, ['feed_id' => -2112, 'view_mode' => "adaptive"], $out, (clone $c)->label(1088)->unread(true), $fields, $sort, $expFull], + [true, ['feed_id' => -2112, 'view_mode' => "unread"], $out, (clone $c)->label(1088)->unread(true), $fields, $sort, $expFull], + [true, ['feed_id' => 42, 'view_mode' => "marked"], $out, (clone $c)->subscription(42)->starred(true), $fields, $sort, $expFull], + [true, ['feed_id' => 42, 'view_mode' => "has_note"], $out, (clone $c)->subscription(42)->annotated(true), $fields, $sort, $expFull], + [true, ['feed_id' => 42, 'view_mode' => "unread", 'search' => "unread:false"], null, $c, [], [], $this->respGood([])], + [true, ['feed_id' => 42, 'search' => "pub:true"], null, $c, [], [], $this->respGood([])], + [true, ['feed_id' => -4, 'limit' => 5], $out, (clone $c)->limit(5), $fields, $sort, $expFull], + [true, ['feed_id' => -4, 'skip' => 2], $out, (clone $c)->offset(2), $fields, $sort, $expFull], + [true, ['feed_id' => -4, 'limit' => 5, 'skip' => 2], $out, (clone $c)->limit(5)->offset(2), $fields, $sort, $expFull], + [true, ['feed_id' => -4, 'since_id' => 47], $out, (clone $c)->oldestArticle(48), $fields, $sort, $expFull], + [true, ['feed_id' => -3, 'is_cat' => true], $out, $c, $fields, $sort, $expFull], + [true, ['feed_id' => -4, 'is_cat' => true], $out, $c, $fields, $sort, $expFull], + [true, ['feed_id' => -2, 'is_cat' => true], $out, (clone $c)->labelled(true), $fields, $sort, $expFull], + [true, ['feed_id' => -1, 'is_cat' => true], null, $c, [], [], $this->respGood([])], + [true, ['feed_id' => 0, 'is_cat' => true], $out, (clone $c)->folderShallow(0), $fields, $sort, $expFull], + [true, ['feed_id' => 0, 'is_cat' => true, 'include_nested' => true], $out, (clone $c)->folderShallow(0), $fields, $sort, $expFull], + [true, ['feed_id' => 42, 'is_cat' => true], $out, (clone $c)->folderShallow(42), $fields, $sort, $expFull], + [true, ['feed_id' => 42, 'is_cat' => true, 'include_nested' => true], $out, (clone $c)->folder(42), $fields, $sort, $expFull], + [true, ['feed_id' => -4, 'order_by' => "feed_dates"], $out, $c, $fields, $sort, $expFull], + [true, ['feed_id' => -4, 'order_by' => "date_reverse"], $out, $c, $fields, ["edited_date"], $expFull], + [true, ['feed_id' => 42, 'search' => "interesting"], $out, (clone $c)->subscription(42)->searchTerms(["interesting"]), $fields, $sort, $expFull], + [true, ['feed_id' => -6], $out, (clone $c)->unread(false)->markedSince(Date::sub("PT24H", $t)), $fields, ["marked_date desc"], $expFull], + [true, ['feed_id' => -6, 'view_mode' => "unread"], null, $c, $fields, $sort, $this->respGood([])], + [true, ['feed_id' => -3], $out, (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H", $t)), $fields, $sort, $expFull], + [true, ['feed_id' => -3, 'view_mode' => "marked"], $out, (clone $c)->unread(true)->starred(true)->modifiedSince(Date::sub("PT24H", $t)), $fields, $sort, $expFull], + [false, [], null, (clone $c)->limit(null), [], [], $this->respErr("INCORRECT_USAGE")], + [false, ['feed_id' => 0], null, (clone $c)->limit(null), [], [], $this->respGood([])], + [false, ['feed_id' => -1], $comp, (clone $c)->limit(null)->starred(true), ["id"], ["marked_date desc"], $expComp], + [false, ['feed_id' => -2], null, (clone $c)->limit(null), [], [], $this->respGood([])], + [false, ['feed_id' => -4], $comp, (clone $c)->limit(null), ["id"], $sort, $expComp], + [false, ['feed_id' => 2112], $gone, (clone $c)->limit(null)->subscription(2112), ["id"], $sort, $this->respGood([])], + [false, ['feed_id' => -2112], $comp, (clone $c)->limit(null)->label(1088), ["id"], $sort, $expComp], + [false, ['feed_id' => -4, 'view_mode' => "adaptive"], $comp, (clone $c)->limit(null)->unread(true), ["id"], $sort, $expComp], + [false, ['feed_id' => -4, 'view_mode' => "published"], null, (clone $c)->limit(null), [], [], $this->respGood([])], + [false, ['feed_id' => -2112, 'view_mode' => "adaptive"], $comp, (clone $c)->limit(null)->label(1088)->unread(true), ["id"], $sort, $expComp], + [false, ['feed_id' => -2112, 'view_mode' => "unread"], $comp, (clone $c)->limit(null)->label(1088)->unread(true), ["id"], $sort, $expComp], + [false, ['feed_id' => 42, 'view_mode' => "marked"], $comp, (clone $c)->limit(null)->subscription(42)->starred(true), ["id"], $sort, $expComp], + [false, ['feed_id' => 42, 'view_mode' => "has_note"], $comp, (clone $c)->limit(null)->subscription(42)->annotated(true), ["id"], $sort, $expComp], + [false, ['feed_id' => -4, 'limit' => 5], $comp, (clone $c)->limit(5), ["id"], $sort, $expComp], + [false, ['feed_id' => -4, 'skip' => 2], $comp, (clone $c)->limit(null)->offset(2), ["id"], $sort, $expComp], + [false, ['feed_id' => -4, 'limit' => 5, 'skip' => 2], $comp, (clone $c)->limit(5)->offset(2), ["id"], $sort, $expComp], + [false, ['feed_id' => -4, 'since_id' => 47], $comp, (clone $c)->limit(null)->oldestArticle(48), ["id"], $sort, $expComp], + [false, ['feed_id' => -6], $comp, (clone $c)->limit(null)->unread(false)->markedSince(Date::sub("PT24H", $t)), ["id"], ["marked_date desc"], $expComp], + [false, ['feed_id' => -6, 'view_mode' => "unread"], null, (clone $c)->limit(null), ["id"], $sort, $this->respGood([])], + [false, ['feed_id' => -3], $comp, (clone $c)->limit(null)->unread(true)->modifiedSince(Date::sub("PT24H", $t)), ["id"], $sort, $expComp], + [false, ['feed_id' => -3, 'view_mode' => "marked"], $comp, (clone $c)->limit(null)->unread(true)->starred(true)->modifiedSince(Date::sub("PT24H", $t)), ["id"], $sort, $expComp], ]; - $out3 = [ - $this->outputHeadlines(1001), - $this->outputHeadlines(1001), - $this->outputHeadlines(1002), - $this->outputHeadlines(1003), - ]; - for ($a = 0; $a < sizeof($in1); $a++) { - $this->assertMessage($this->respGood([]), $this->req($in1[$a]), "Test $a failed"); - } - for ($a = 0; $a < sizeof($in2); $a++) { - $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(), ["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"); - } } public function testRetrieveFullHeadlinesCheckingExtraFields(): void { @@ -1919,7 +1814,7 @@ LONG_STRING; \Phake::when(Arsse::$db)->articleCategoriesGet($this->anything(), 2112)->thenReturn(["Boring","Illogical"]); \Phake::when(Arsse::$db)->articleList->thenReturn($this->generateHeadlines(1)); \Phake::when(Arsse::$db)->articleCount->thenReturn(0); - \Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1); + \Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true)->hidden(false))->thenReturn(1); // sanity check; this makes sure extra fields are not included in default situations $test = $this->req($in[0]); $this->assertMessage($this->outputHeadlines(1), $test); @@ -1970,7 +1865,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)->subscription(2112), $this->anything(), ["edited_date desc"])->thenThrow(new ExceptionInput("subjectMissing")); + \Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->limit(200)->subscription(2112)->hidden(false), $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], @@ -1985,7 +1880,7 @@ LONG_STRING; ]); $this->assertMessage($exp, $test); // test 'include_header' with skip - \Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->limit(1)->subscription(42), $this->anything(), ["edited_date desc"])->thenReturn($this->generateHeadlines(1867)); + \Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->limit(1)->subscription(42)->hidden(false), $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], From a81760e39d0caadf7d26506856123b0a4d554a8d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 22 Dec 2020 15:17:18 -0500 Subject: [PATCH 084/366] Aggressivly clean up hidden articles Notably, starred articles are cleaned up if hidden --- lib/Database.php | 54 +++++++++++++++++++------- tests/cases/Database/SeriesCleanup.php | 16 ++++---- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 32e88d33..d193b576 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1809,21 +1809,49 @@ class Database { /** Deletes from the database articles which are beyond the configured clean-up threshold */ public function articleCleanup(): bool { - $query = $this->db->prepare( + $query = $this->db->prepareArray( "WITH RECURSIVE - exempt_articles as (SELECT id from arsse_articles join (SELECT article, max(id) as edition from arsse_editions group by article) as latest_editions on arsse_articles.id = latest_editions.article where feed = ? order by edition desc limit ?), - target_articles as ( - select id from arsse_articles - left join (select article, sum(starred) as starred, sum(\"read\") as \"read\", max(arsse_marks.modified) as marked_date from arsse_marks join arsse_subscriptions on arsse_subscriptions.id = arsse_marks.subscription group by article) as mark_stats on mark_stats.article = arsse_articles.id - left join (select feed, count(*) as subs from arsse_subscriptions group by feed) as feed_stats on feed_stats.feed = arsse_articles.feed - where arsse_articles.feed = ? and coalesce(starred,0) = 0 and (coalesce(marked_date,modified) <= ? or (coalesce(\"read\",0) = coalesce(subs,0) and coalesce(marked_date,modified) <= ?)) - ) + exempt_articles as ( + SELECT + id + from arsse_articles join ( + SELECT article, max(id) as edition from arsse_editions group by article + ) as latest_editions on arsse_articles.id = latest_editions.article + where feed = ? order by edition desc limit ? + ), + target_articles as ( + SELECT + id + from arsse_articles + join ( + select + feed, + count(*) as subs + from arsse_subscriptions + where feed = ? + group by feed + ) as feed_stats on feed_stats.feed = arsse_articles.feed + left join ( + select + article, + sum(cast((starred = 1 and hidden = 0) as integer)) as starred, + sum(cast((\"read\" = 1 or hidden = 1) as integer)) as \"read\", + max(arsse_marks.modified) as marked_date + from arsse_marks + group by article + ) as mark_stats on mark_stats.article = arsse_articles.id + where + coalesce(starred,0) = 0 + and ( + coalesce(marked_date,modified) <= ? + or ( + coalesce(\"read\",0) = coalesce(subs,0) + and coalesce(marked_date,modified) <= ? + ) + ) + ) DELETE FROM arsse_articles WHERE id not in (select id from exempt_articles) and id in (select id from target_articles)", - "int", - "int", - "int", - "datetime", - "datetime" + ["int", "int", "int", "datetime", "datetime"] ); $limitRead = null; $limitUnread = null; diff --git a/tests/cases/Database/SeriesCleanup.php b/tests/cases/Database/SeriesCleanup.php index cdbb66a0..d863a644 100644 --- a/tests/cases/Database/SeriesCleanup.php +++ b/tests/cases/Database/SeriesCleanup.php @@ -148,16 +148,18 @@ trait SeriesCleanup { 'subscription' => "int", 'read' => "bool", 'starred' => "bool", + 'hidden' => "bool", 'modified' => "datetime", ], 'rows' => [ - [3,1,0,1,$weeksago], - [4,1,1,0,$daysago], - [6,1,1,0,$nowish], - [6,2,1,0,$weeksago], - [8,1,1,0,$weeksago], - [9,1,1,0,$daysago], - [9,2,1,0,$daysago], + [3,1,0,1,0,$weeksago], + [4,1,1,0,0,$daysago], + [6,1,1,0,0,$nowish], + [6,2,1,0,0,$weeksago], + [7,2,0,1,1,$weeksago], // hidden takes precedence over starred + [8,1,1,0,0,$weeksago], + [9,1,1,0,0,$daysago], + [9,2,0,0,1,$daysago], // hidden is the same as read for the purposes of cleanup ], ], ]; From d66cf32c1f12f4ebb9f0a986d207fa229f7befdc Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 22 Dec 2020 16:13:12 -0500 Subject: [PATCH 085/366] Style fixes --- lib/Database.php | 149 ++++++++------------ lib/REST/Fever/API.php | 2 +- lib/REST/Miniflux/V1.php | 8 +- tests/cases/Database/SeriesArticle.php | 2 +- tests/cases/Database/SeriesLabel.php | 2 +- tests/cases/Database/SeriesSubscription.php | 2 +- tests/cases/Database/SeriesUser.php | 2 +- tests/cases/REST/Miniflux/TestV1.php | 6 +- tests/cases/REST/TinyTinyRSS/TestAPI.php | 2 +- tests/cases/User/TestUser.php | 3 +- 10 files changed, 73 insertions(+), 105 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index d193b576..6663e0ff 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -327,7 +327,7 @@ class Database { settype($meta['num'], "integer"); settype($meta['admin'], "integer"); return $meta; - } + } public function userPropertiesSet(string $user, array $data): bool { if (!$this->userExists($user)) { @@ -660,20 +660,27 @@ class Database { // make sure both that the prospective parent exists, and that the it is not one of its children (a circular dependence); // also make sure that a folder with the same prospective name and parent does not already exist: if the parent is null, // SQL will happily accept duplicates (null is not unique), so we must do this check ourselves - $p = $this->db->prepare( + $p = $this->db->prepareArray( "WITH RECURSIVE - target as (select ? as userid, ? as source, ? as dest, ? as new_name), - folders as (SELECT id from arsse_folders join target on owner = userid and coalesce(parent,0) = source union all select arsse_folders.id as id from arsse_folders join folders on arsse_folders.parent=folders.id) - ". - "SELECT - case when ((select dest from target) is null or exists(select id from arsse_folders join target on owner = userid and coalesce(id,0) = coalesce(dest,0))) then 1 else 0 end as extant, - case when not exists(select id from folders where id = coalesce((select dest from target),0)) then 1 else 0 end as valid, - case when not exists(select id from arsse_folders join target on coalesce(parent,0) = coalesce(dest,0) and name = coalesce((select new_name from target),(select name from arsse_folders join target on id = source))) then 1 else 0 end as available - ", - "str", - "strict int", - "int", - "str" + target as ( + SELECT ? as userid, ? as source, ? as dest, ? as new_name + ), + folders as ( + SELECT id from arsse_folders join target on owner = userid and coalesce(parent,0) = source + union all + select arsse_folders.id as id from arsse_folders join folders on arsse_folders.parent=folders.id + ) + SELECT + case when + ((select dest from target) is null or exists(select id from arsse_folders join target on owner = userid and coalesce(id,0) = coalesce(dest,0))) + then 1 else 0 end as extant, + case when + not exists(select id from folders where id = coalesce((select dest from target),0)) + then 1 else 0 end as valid, + case when + not exists(select id from arsse_folders join target on coalesce(parent,0) = coalesce(dest,0) and name = coalesce((select new_name from target),(select name from arsse_folders join target on id = source))) + then 1 else 0 end as available", + ["str", "strict int", "int", "str"] )->run($user, $id, $parent, $name)->getRow(); if (!$p['extant']) { // if the parent doesn't exist or doesn't below to the user, throw an exception @@ -757,7 +764,13 @@ class Database { left join topmost as t on t.f_id = s.folder join arsse_feeds as f on f.id = s.feed left join arsse_icons as i on i.id = f.icon - left join (select feed, count(*) as articles from arsse_articles group by feed) as article_stats on article_stats.feed = s.feed + left join ( + select + feed, + count(*) as articles + from arsse_articles + group by feed + ) as article_stats on article_stats.feed = s.feed left join ( select subscription, @@ -1042,11 +1055,9 @@ class Database { } } catch (Feed\Exception $e) { // update the database with the resultant error and the next fetch time, incrementing the error count - $this->db->prepare( + $this->db->prepareArray( "UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ?, err_count = err_count + 1, err_msg = ? WHERE id = ?", - 'datetime', - 'str', - 'int' + ['datetime', 'str', 'int'] )->run(Feed::nextFetchOnError($f['err_count']), $e->getMessage(), $feedID); if ($throwError) { throw $e; @@ -1060,38 +1071,18 @@ class Database { $qInsertEdition = $this->db->prepare("INSERT INTO arsse_editions(article) values(?)", 'int'); } if (sizeof($feed->newItems)) { - $qInsertArticle = $this->db->prepare( + $qInsertArticle = $this->db->prepareArray( "INSERT INTO arsse_articles(url,title,author,published,edited,guid,content,url_title_hash,url_content_hash,title_content_hash,feed) values(?,?,?,?,?,?,?,?,?,?,?)", - 'str', - 'str', - 'str', - 'datetime', - 'datetime', - 'str', - 'str', - 'str', - 'str', - 'str', - 'int' + ['str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str', 'int'] ); } if (sizeof($feed->changedItems)) { $qDeleteEnclosures = $this->db->prepare("DELETE FROM arsse_enclosures WHERE article = ?", 'int'); $qDeleteCategories = $this->db->prepare("DELETE FROM arsse_categories WHERE article = ?", 'int'); $qClearReadMarks = $this->db->prepare("UPDATE arsse_marks SET \"read\" = 0, modified = CURRENT_TIMESTAMP WHERE article = ? and \"read\" = 1", 'int'); - $qUpdateArticle = $this->db->prepare( + $qUpdateArticle = $this->db->prepareArray( "UPDATE arsse_articles SET url = ?, title = ?, author = ?, published = ?, edited = ?, modified = CURRENT_TIMESTAMP, guid = ?, content = ?, url_title_hash = ?, url_content_hash = ?, title_content_hash = ? WHERE id = ?", - 'str', - 'str', - 'str', - 'datetime', - 'datetime', - 'str', - 'str', - 'str', - 'str', - 'str', - 'int' + ['str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str', 'int'] ); } // determine if the feed icon needs to be updated, and update it if appropriate @@ -1159,16 +1150,9 @@ class Database { $qClearReadMarks->run($articleID); } // lastly update the feed database itself with updated information. - $this->db->prepare( + $this->db->prepareArray( "UPDATE arsse_feeds SET title = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ?, icon = ? WHERE id = ?", - 'str', - 'str', - 'datetime', - 'strict str', - 'datetime', - 'int', - 'int', - 'int' + ['str', 'str', 'datetime', 'strict str', 'datetime', 'int', 'int', 'int'] )->run( $feed->data->title, $feed->data->siteUrl, @@ -1205,9 +1189,9 @@ class Database { } /** Retrieves the set of filters users have applied to a given feed - * + * * Each record includes the following keys: - * + * * - "owner": The user for whom to apply the filters * - "keep": The "keep" rule; any articles which fail to match this rule are hidden * - "block": The block rule; any article which matches this rule are hidden @@ -1258,13 +1242,9 @@ class Database { [$cHashUC, $tHashUC, $vHashUC] = $this->generateIn($hashesUC, "str"); [$cHashTC, $tHashTC, $vHashTC] = $this->generateIn($hashesTC, "str"); // perform the query - return $this->db->prepare( + return $this->db->prepareArray( "SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed = ? and (guid in($cId) or url_title_hash in($cHashUT) or url_content_hash in($cHashUC) or title_content_hash in($cHashTC))", - 'int', - $tId, - $tHashUT, - $tHashUC, - $tHashTC + ['int', $tId, $tHashUT, $tHashUC, $tHashTC] )->run($feedID, $vId, $vHashUT, $vHashUC, $vHashTC); } @@ -1880,15 +1860,14 @@ class Database { if (!V::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "article", 'type' => "int > 0"]); // @codeCoverageIgnore } - $out = $this->db->prepare( + $out = $this->db->prepareArray( "SELECT articles.article as article, max(arsse_editions.id) as edition from ( select arsse_articles.id as article FROM arsse_articles join arsse_subscriptions on arsse_subscriptions.feed = arsse_articles.feed WHERE arsse_articles.id = ? and arsse_subscriptions.owner = ? ) as articles join arsse_editions on arsse_editions.article = articles.article group by articles.article", - "int", - "str" + ["int", "str"] )->run($id, $user)->getRow(); if (!$out) { throw new Db\ExceptionInput("subjectMissing", ["action" => $this->caller(), "field" => "article", 'id' => $id]); @@ -1907,7 +1886,7 @@ class Database { if (!V::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "edition", 'type' => "int > 0"]); // @codeCoverageIgnore } - $out = $this->db->prepare( + $out = $this->db->prepareArray( "SELECT arsse_editions.id, arsse_editions.article, edition_stats.edition as current from arsse_editions @@ -1915,8 +1894,7 @@ class Database { join arsse_subscriptions on arsse_subscriptions.feed = arsse_articles.feed join (select article, max(id) as edition from arsse_editions group by article) as edition_stats on edition_stats.article = arsse_editions.article where arsse_editions.id = ? and arsse_subscriptions.owner = ?", - "int", - "str" + ["int", "str"] )->run($id, $user)->getRow(); if (!$out) { throw new Db\ExceptionInput("subjectMissing", ["action" => $this->caller(), "field" => "edition", 'id' => $id]); @@ -1968,7 +1946,7 @@ class Database { * @param boolean $includeEmpty Whether to include (true) or supress (false) labels which have no articles assigned to them */ public function labelList(string $user, bool $includeEmpty = true): Db\Result { - return $this->db->prepare( + return $this->db->prepareArray( "SELECT * FROM ( SELECT id, @@ -1992,11 +1970,8 @@ class Database { ) as mark_stats on mark_stats.label = arsse_labels.id WHERE owner = ? ) as label_data - where articles >= ? order by name - ", - "str", - "str", - "int" + where articles >= ? order by name", + ["str", "str", "int"] )->run($user, $user, !$includeEmpty); } @@ -2036,7 +2011,7 @@ class Database { $this->labelValidateId($user, $id, $byName, false); $field = $byName ? "name" : "id"; $type = $byName ? "str" : "int"; - $out = $this->db->prepare( + $out = $this->db->prepareArray( "SELECT id, name, @@ -2057,11 +2032,8 @@ class Database { where arsse_subscriptions.owner = ? group by label ) as mark_stats on mark_stats.label = arsse_labels.id - WHERE $field = ? and owner = ? - ", - "str", - $type, - "str" + WHERE $field = ? and owner = ?", + ["str", $type, "str"] )->run($user, $id, $user)->getRow(); if (!$out) { throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "label", 'id' => $id]); @@ -2113,6 +2085,7 @@ class Database { } try { $q = $this->articleQuery($user, $c); + $q->setOrder("id"); $out = $this->db->prepare((string) $q, $q->getTypes())->run($q->getValues())->getAll(); } catch (Db\ExceptionInput $e) { if ($e->getCode() === 10235) { @@ -2261,7 +2234,7 @@ class Database { * @param boolean $includeEmpty Whether to include (true) or supress (false) tags which have no subscriptions assigned to them */ public function tagList(string $user, bool $includeEmpty = true): Db\Result { - return $this->db->prepare( + return $this->db->prepareArray( "SELECT * FROM ( SELECT id,name,coalesce(subscriptions,0) as subscriptions @@ -2269,10 +2242,8 @@ class Database { left join (SELECT tag, sum(assigned) as subscriptions from arsse_tag_members group by tag) as tag_stats on tag_stats.tag = arsse_tags.id WHERE owner = ? ) as tag_data - where subscriptions >= ? order by name - ", - "str", - "int" + where subscriptions >= ? order by name", + ["str", "int"] )->run($user, !$includeEmpty); } @@ -2288,7 +2259,7 @@ class Database { * @param string $user The user whose tags are to be listed */ public function tagSummarize(string $user): Db\Result { - return $this->db->prepare( + return $this->db->prepareArray( "SELECT arsse_tags.id as id, arsse_tags.name as name, @@ -2296,7 +2267,7 @@ class Database { FROM arsse_tag_members join arsse_tags on arsse_tags.id = arsse_tag_members.tag WHERE arsse_tags.owner = ? and assigned = 1", - "str" + ["str"] )->run($user); } @@ -2335,15 +2306,13 @@ class Database { $this->tagValidateId($user, $id, $byName, false); $field = $byName ? "name" : "id"; $type = $byName ? "str" : "int"; - $out = $this->db->prepare( + $out = $this->db->prepareArray( "SELECT id,name,coalesce(subscriptions,0) as subscriptions FROM arsse_tags left join (SELECT tag, sum(assigned) as subscriptions from arsse_tag_members group by tag) as tag_stats on tag_stats.tag = arsse_tags.id - WHERE $field = ? and owner = ? - ", - $type, - "str" + WHERE $field = ? and owner = ?", + [$type, "str"] )->run($id, $user)->getRow(); if (!$out) { throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "tag", 'id' => $id]); diff --git a/lib/REST/Fever/API.php b/lib/REST/Fever/API.php index 2d5fcc1c..8c94a8dd 100644 --- a/lib/REST/Fever/API.php +++ b/lib/REST/Fever/API.php @@ -270,7 +270,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } else { // group zero is the "Kindling" supergroup i.e. all feeds // only exclude hidden articles - $c->hidden(false); + $c->hidden(false); } break; case "feed": diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 5474dc0b..bf3da2ea 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -130,7 +130,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return true; } } - // next check HTTP auth + // next check HTTP auth if ($req->getAttribute("authenticated", false)) { Arsse::$user->id = $req->getAttribute("authenticatedUser"); return true; @@ -318,7 +318,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse($msg, 500); } $out = []; - foreach($list as $url) { + foreach ($list as $url) { // TODO: This needs to be refined once PicoFeed is replaced $out[] = ['title' => "Feed", 'type' => "rss", 'url' => $url]; } @@ -345,7 +345,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("404", 404); } } - + protected function getCurrentUser(): ResponseInterface { return new Response($this->listUsers([Arsse::$user->id], false)[0] ?? new \stdClass); } @@ -411,7 +411,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { Arsse::$db->folderRemove(Arsse::$user->id, $folder); } else { // if we're deleting from the root folder, delete each child subscription individually - // otherwise we'd be deleting the entire tree + // otherwise we'd be deleting the entire tree $tr = Arsse::$db->begin(); foreach (Arsse::$db->subscriptionList(Arsse::$user->id, null, false) as $sub) { Arsse::$db->subscriptionRemove(Arsse::$user->id, $sub['id']); diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 033bbfde..c444977f 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -1147,4 +1147,4 @@ trait SeriesArticle { $state = $this->primeExpectations($this->data, $this->checkTables); $this->compareExpectations(static::$drv, $state); } -} \ No newline at end of file +} diff --git a/tests/cases/Database/SeriesLabel.php b/tests/cases/Database/SeriesLabel.php index ec82613c..4a4fac66 100644 --- a/tests/cases/Database/SeriesLabel.php +++ b/tests/cases/Database/SeriesLabel.php @@ -209,7 +209,7 @@ trait SeriesLabel { [11, 20,1,0,'2017-01-01 00:00:00',0], [12, 3,0,1,'2017-01-01 00:00:00',0], [12, 4,1,1,'2017-01-01 00:00:00',0], - [4, 8,0,0,'2000-01-02 02:00:00',1] + [4, 8,0,0,'2000-01-02 02:00:00',1], ], ], 'arsse_labels' => [ diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index 0e14a7bf..7fe700a8 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -23,7 +23,7 @@ trait SeriesSubscription { 'rows' => [ ["jane.doe@example.com", "", 1], ["john.doe@example.com", "", 2], - ["jill.doe@example.com", "", 3] + ["jill.doe@example.com", "", 3], ], ], 'arsse_folders' => [ diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index 7ed01822..af591d48 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -157,7 +157,7 @@ trait SeriesUser { public function testSetNoMetadata(): void { $in = [ - 'num' => 2112, + 'num' => 2112, 'stylesheet' => "body {background:lightgray}", ]; $this->assertTrue(Arsse::$db->userPropertiesSet("john.doe@example.com", $in)); diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 3778a547..918a3da6 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -62,7 +62,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { 'extra' => [ 'custom_css' => "", ], - ] + ], ]; protected function req(string $method, string $target, $data = "", array $headers = [], ?string $user = "john.doe@example.com", bool $body = true): ResponseInterface { @@ -185,8 +185,8 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideUserQueries */ public function testQueryUsers(bool $admin, string $route, ResponseInterface $exp): void { $u = [ - ['num'=> 1, 'admin' => true, 'theme' => "custom", 'lang' => "fr_CA", 'tz' => "Asia/Gaza", 'sort_asc' => true, 'page_size' => 200, 'shortcuts' => false, 'reading_time' => false, 'swipe' => false, 'stylesheet' => "p {}"], - ['num'=> 2, 'admin' => false, 'theme' => null, 'lang' => null, 'tz' => null, 'sort_asc' => null, 'page_size' => null, 'shortcuts' => null, 'reading_time' => null, 'swipe' => null, 'stylesheet' => null], + ['num' => 1, 'admin' => true, 'theme' => "custom", 'lang' => "fr_CA", 'tz' => "Asia/Gaza", 'sort_asc' => true, 'page_size' => 200, 'shortcuts' => false, 'reading_time' => false, 'swipe' => false, 'stylesheet' => "p {}"], + ['num' => 2, 'admin' => false, 'theme' => null, 'lang' => null, 'tz' => null, 'sort_asc' => null, 'page_size' => null, 'shortcuts' => null, 'reading_time' => null, 'swipe' => null, 'stylesheet' => null], new ExceptionConflict("doesNotExist"), ]; $user = $admin ? "john.doe@example.com" : "jane.doe@example.com"; diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index abcbdd9b..923380dd 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -1716,7 +1716,7 @@ LONG_STRING; } $this->assertMessage($exp, $this->req($in)); if ($out) { - \Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, $c, $fields, $order); + \Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, $c, $fields, $order); } else { \Phake::verify(Arsse::$db, \Phake::times(0))->articleList; } diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index b7d1266e..597a1583 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -98,7 +98,6 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->userLookup(2112); } - public function testAddAUser(): void { $user = "john.doe@example.com"; $pass = "secret"; @@ -439,7 +438,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { [['tz' => false], new ExceptionInput("invalidValue")], [['lang' => "en-ca"], ['lang' => "en-CA"]], [['lang' => null], ['lang' => null]], - [['page_size' => 0], new ExceptionInput("invalidNonZeroInteger")] + [['page_size' => 0], new ExceptionInput("invalidNonZeroInteger")], ]; } From 88cf3c6dae2cbb66ba9042ea59d791479a5c7c68 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 23 Dec 2020 09:38:22 -0500 Subject: [PATCH 086/366] Test filter rule retrieval --- lib/Database.php | 2 +- tests/cases/Database/SeriesFeed.php | 36 +++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 6663e0ff..1d5405aa 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1197,7 +1197,7 @@ class Database { * - "block": The block rule; any article which matches this rule are hidden */ public function feedRulesGet(int $feedID): Db\Result { - return $this->db->prepare("SELECT owner, keep_rule as keep, block_rule as block from arsse_subscriptions where feed = ? and (coalesce(keep_rule, '') || coalesce(block_rule, '')) <> ''", "int")->run($feedID); + return $this->db->prepare("SELECT owner, coalesce(keep_rule, '') as keep, coalesce(block_rule, '') as block from arsse_subscriptions where feed = ? and (keep || block) <> '' order by owner", "int")->run($feedID); } /** Retrieves various identifiers for the latest $count articles in the given newsfeed. The identifiers are: diff --git a/tests/cases/Database/SeriesFeed.php b/tests/cases/Database/SeriesFeed.php index f79c8cc9..8f17694a 100644 --- a/tests/cases/Database/SeriesFeed.php +++ b/tests/cases/Database/SeriesFeed.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Database; use JKingWeb\Arsse\Arsse; +use JKingWeb\Arsse\Test\Result; trait SeriesFeed { protected function setUpSeriesFeed(): void { @@ -67,17 +68,19 @@ trait SeriesFeed { ], 'arsse_subscriptions' => [ 'columns' => [ - 'id' => "int", - 'owner' => "str", - 'feed' => "int", + 'id' => "int", + 'owner' => "str", + 'feed' => "int", + 'keep_rule' => "str", + 'block_rule' => "str", ], 'rows' => [ - [1,'john.doe@example.com',1], - [2,'john.doe@example.com',2], - [3,'john.doe@example.com',3], - [4,'john.doe@example.com',4], - [5,'john.doe@example.com',5], - [6,'jane.doe@example.com',1], + [1,'john.doe@example.com',1,null,'^Sport$'], + [2,'john.doe@example.com',2,null,null], + [3,'john.doe@example.com',3,'\w+',null], + [4,'john.doe@example.com',4,null,null], + [5,'john.doe@example.com',5,null,'and/or'], + [6,'jane.doe@example.com',1,'^(?i)[a-z]+','bluberry'], ], ], 'arsse_articles' => [ @@ -200,6 +203,21 @@ trait SeriesFeed { $this->assertResult([['id' => 1]], Arsse::$db->feedMatchIds(1, ['e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda'])); // this ID appears in both feed 1 and feed 2; only one result should be returned } + /** @dataProvider provideFilterRules */ + public function testGetRules(int $in, array $exp): void { + $this->assertResult($exp, Arsse::$db->feedRulesGet($in)); + } + + public function provideFilterRules(): iterable { + return [ + [1, [['owner' => "john.doe@example.com", 'keep' => "", 'block' => "^Sport$"], ['owner' => "jane.doe@example.com", 'keep' => "^(?i)[a-z]+", 'block' => "bluberry"]]], + [2, []], + [3, [['owner' => "john.doe@example.com", 'keep' => '\w+', 'block' => ""]]], + [4, []], + [5, [['owner' => "john.doe@example.com", 'keep' => "", 'block' => "and/or"]]], + ]; + } + public function testUpdateAFeed(): void { // update a valid feed with both new and changed items Arsse::$db->feedUpdate(1); From 5ec04d33c621f201db40eb25af34e1084b6c2b09 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 25 Dec 2020 17:47:36 -0500 Subject: [PATCH 087/366] Add backend functionality to rename users --- lib/Database.php | 14 ++++++++ lib/User.php | 27 ++++++++++++++++ lib/User/Driver.php | 9 +++++- lib/User/Internal/Driver.php | 23 ++++++++++--- tests/cases/Database/SeriesUser.php | 25 +++++++++++++++ tests/cases/User/TestInternal.php | 24 +++++++++++++- tests/cases/User/TestUser.php | 50 +++++++++++++++++++++++++++-- 7 files changed, 164 insertions(+), 8 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 1d5405aa..95ccc611 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -273,6 +273,20 @@ class Database { return true; } + public function userRename(string $user, string $name): bool { + if ($user === $name) { + return false; + } + try { + if (!$this->db->prepare("UPDATE arsse_users set id = ? where id = ?", "str", "str")->run($name, $user)->changes()) { + throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + } + } catch (Db\ExceptionInput $e) { + throw new User\ExceptionConflict("alreadyExists", ["action" => __FUNCTION__, "user" => $name], $e); + } + return true; + } + /** Removes a user from the database */ public function userRemove(string $user): bool { if ($this->db->prepare("DELETE from arsse_users where id = ?", "str")->run($user)->changes() < 1) { diff --git a/lib/User.php b/lib/User.php index df9a49d9..1c7979bc 100644 --- a/lib/User.php +++ b/lib/User.php @@ -42,6 +42,21 @@ class User { return (string) $this->id; } + public function begin(): Db\Transaction { + /* TODO: A proper implementation of this would return a meta-transaction + object which would contain both a user-manager transaction (when + applicable) and a database transaction, and commit or roll back both + as the situation calls. + + In theory, an external user driver would probably have to implement its + own approximation of atomic transactions and rollback. In practice the + only driver is the internal one, which is always backed by an ACID + database; the added complexity is thus being deferred until such time + as it is actually needed for a concrete implementation. + */ + return Arsse::$db->begin(); + } + public function auth(string $user, string $password): bool { $prevUser = $this->id; $this->id = $user; @@ -89,6 +104,18 @@ class User { return $out; } + public function rename(string $user, string $newName): bool { + if ($this->u->userRename($user, $newName)) { + if (!Arsse::$db->userExists($user)) { + Arsse::$db->userAdd($newName, null); + return true; + } else { + return Arsse::$db->userRename($user, $newName); + } + } + return false; + } + public function remove(string $user): bool { try { $out = $this->u->userRemove($user); diff --git a/lib/User/Driver.php b/lib/User/Driver.php index e0d949c7..d4b73706 100644 --- a/lib/User/Driver.php +++ b/lib/User/Driver.php @@ -27,6 +27,13 @@ interface Driver { */ public function userAdd(string $user, string $password = null): ?string; + /** Renames a user + * + * The implementation must retain all user metadata as well as the + * user's password + */ + public function userRename(string $user, string $newName): bool; + /** Removes a user */ public function userRemove(string $user): bool; @@ -44,7 +51,7 @@ interface Driver { * @param string|null $password The cleartext password to assign to the user, or null to generate a random password * @param string|null $oldPassword The user's previous password, if known */ - public function userPasswordSet(string $user, ?string $newPassword, string $oldPassword = null); + public function userPasswordSet(string $user, ?string $newPassword, string $oldPassword = null): ?string; /** Removes a user's password; this makes authentication fail unconditionally * diff --git a/lib/User/Internal/Driver.php b/lib/User/Internal/Driver.php index 27486fb1..80f16bb3 100644 --- a/lib/User/Internal/Driver.php +++ b/lib/User/Internal/Driver.php @@ -40,6 +40,16 @@ class Driver implements \JKingWeb\Arsse\User\Driver { return $password; } + public function userRename(string $user, string $newName): bool { + // do nothing: the internal database is updated regardless of what the driver does (assuming it does not throw an exception) + // throw an exception if the user does not exist + if (!$this->userExists($user)) { + throw new ExceptionConflict("doesNotExist", ['action' => __FUNCTION__, 'user' => $user]); + } else { + return !($user === $newName); + } + } + public function userRemove(string $user): bool { return Arsse::$db->userRemove($user); } @@ -50,14 +60,19 @@ class Driver implements \JKingWeb\Arsse\User\Driver { public function userPasswordSet(string $user, ?string $newPassword, string $oldPassword = null): ?string { // do nothing: the internal database is updated regardless of what the driver does (assuming it does not throw an exception) - return $newPassword; + // throw an exception if the user does not exist + if (!$this->userExists($user)) { + throw new ExceptionConflict("doesNotExist", ['action' => __FUNCTION__, 'user' => $user]); + } else { + return $newPassword; + } } public function userPasswordUnset(string $user, string $oldPassword = null): bool { // do nothing: the internal database is updated regardless of what the driver does (assuming it does not throw an exception) // throw an exception if the user does not exist if (!$this->userExists($user)) { - throw new ExceptionConflict("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); + throw new ExceptionConflict("doesNotExist", ['action' => __FUNCTION__, 'user' => $user]); } else { return true; } @@ -74,7 +89,7 @@ class Driver implements \JKingWeb\Arsse\User\Driver { public function userPropertiesGet(string $user, bool $includeLarge = true): array { // do nothing: the internal database will retrieve everything for us if (!$this->userExists($user)) { - throw new ExceptionConflict("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); + throw new ExceptionConflict("doesNotExist", ['action' => __FUNCTION__, 'user' => $user]); } else { return []; } @@ -83,7 +98,7 @@ class Driver implements \JKingWeb\Arsse\User\Driver { public function userPropertiesSet(string $user, array $data): array { // do nothing: the internal database will set everything for us if (!$this->userExists($user)) { - throw new ExceptionConflict("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); + throw new ExceptionConflict("doesNotExist", ['action' => __FUNCTION__, 'user' => $user]); } else { return $data; } diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index af591d48..0cd4ffb7 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -180,4 +180,29 @@ trait SeriesUser { $this->assertException("doesNotExist", "User", "ExceptionConflict"); Arsse::$db->userLookup(2112); } + + public function testRenameAUser(): void { + $this->assertTrue(Arsse::$db->userRename("john.doe@example.com", "juan.doe@example.com")); + $state = $this->primeExpectations($this->data, [ + 'arsse_users' => ['id', 'num'], + 'arsse_user_meta' => ["owner", "key", "value"] + ]); + $state['arsse_users']['rows'][2][0] = "juan.doe@example.com"; + $state['arsse_user_meta']['rows'][6][0] = "juan.doe@example.com"; + $this->compareExpectations(static::$drv, $state); + } + + public function testRenameAUserToTheSameName(): void { + $this->assertFalse(Arsse::$db->userRename("john.doe@example.com", "john.doe@example.com")); + } + + public function testRenameAMissingUser(): void { + $this->assertException("doesNotExist", "User", "ExceptionConflict"); + Arsse::$db->userRename("juan.doe@example.com", "john.doe@example.com"); + } + + public function testRenameAUserToADuplicateName(): void { + $this->assertException("alreadyExists", "User", "ExceptionConflict"); + Arsse::$db->userRename("john.doe@example.com", "jane.doe@example.com"); + } } diff --git a/tests/cases/User/TestInternal.php b/tests/cases/User/TestInternal.php index c7038352..858a8765 100644 --- a/tests/cases/User/TestInternal.php +++ b/tests/cases/User/TestInternal.php @@ -9,6 +9,7 @@ namespace JKingWeb\Arsse\TestCase\User; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\User\Driver as DriverInterface; +use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\Internal\Driver; /** @covers \JKingWeb\Arsse\User\Internal\Driver */ @@ -88,6 +89,21 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->userAdd; } + public function testRenameAUser(): void { + $john = "john.doe@example.com"; + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertTrue((new Driver)->userRename($john, "jane.doe@example.com")); + $this->assertFalse((new Driver)->userRename($john, $john)); + \Phake::verify(Arsse::$db, \Phake::times(2))->userExists($john); + } + + public function testRenameAMissingUser(): void { + $john = "john.doe@example.com"; + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); + (new Driver)->userRename($john, "jane.doe@example.com"); + } + public function testRemoveAUser(): void { $john = "john.doe@example.com"; \Phake::when(Arsse::$db)->userRemove->thenReturn(true)->thenThrow(new \JKingWeb\Arsse\User\ExceptionConflict("doesNotExist")); @@ -104,12 +120,18 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { public function testSetAPassword(): void { $john = "john.doe@example.com"; - \Phake::verifyNoFurtherInteraction(Arsse::$db); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); $this->assertSame("superman", (new Driver)->userPasswordSet($john, "superman")); $this->assertSame(null, (new Driver)->userPasswordSet($john, null)); \Phake::verify(Arsse::$db, \Phake::times(0))->userPasswordSet; } + public function testSetAPasswordForAMssingUser(): void { + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); + (new Driver)->userPasswordSet("john.doe@example.com", "secret"); + } + public function testUnsetAPassword(): void { \Phake::when(Arsse::$db)->userExists->thenReturn(true); $this->assertTrue((new Driver)->userPasswordUnset("john.doe@example.com")); diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 597a1583..e42832e9 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -9,6 +9,7 @@ namespace JKingWeb\Arsse\TestCase\User; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\User; +use JKingWeb\Arsse\Db\Transaction; use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\ExceptionInput; use JKingWeb\Arsse\User\Driver; @@ -43,6 +44,13 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame("", (string) $u); } + public function testStartATransaction(): void { + \Phake::when(Arsse::$db)->begin->thenReturn(\Phake::mock(Transaction::class)); + $u = new User($this->drv); + $this->assertInstanceOf(Transaction::class, $u->begin()); + \Phake::verify(Arsse::$db)->begin(); + } + public function testGeneratePasswords(): void { $u = new User($this->drv); $pass1 = $u->generatePassword(); @@ -174,9 +182,48 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->userExists($user); } + public function testRenameAUser(): void { + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + \Phake::when(Arsse::$db)->userAdd->thenReturn(true); + \Phake::when(Arsse::$db)->userRename->thenReturn(true); + \Phake::when($this->drv)->userRename->thenReturn(true); + $u = new User($this->drv); + $old = "john.doe@example.com"; + $new = "jane.doe@example.com"; + $this->assertTrue($u->rename($old, $new)); + \Phake::verify($this->drv)->userRename($old, $new); + \Phake::verify(Arsse::$db)->userExists($old); + \Phake::verify(Arsse::$db)->userRename($old, $new); + } + + public function testRenameAUserWeDoNotKnow(): void { + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + \Phake::when(Arsse::$db)->userAdd->thenReturn(true); + \Phake::when(Arsse::$db)->userRename->thenReturn(true); + \Phake::when($this->drv)->userRename->thenReturn(true); + $u = new User($this->drv); + $old = "john.doe@example.com"; + $new = "jane.doe@example.com"; + $this->assertTrue($u->rename($old, $new)); + \Phake::verify($this->drv)->userRename($old, $new); + \Phake::verify(Arsse::$db)->userExists($old); + \Phake::verify(Arsse::$db)->userAdd($new, null); + } + + public function testRenameAUserWithoutEffect(): void { + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + \Phake::when(Arsse::$db)->userAdd->thenReturn(true); + \Phake::when(Arsse::$db)->userRename->thenReturn(true); + \Phake::when($this->drv)->userRename->thenReturn(false); + $u = new User($this->drv); + $old = "john.doe@example.com"; + $new = "jane.doe@example.com"; + $this->assertFalse($u->rename($old, $old)); + \Phake::verify($this->drv)->userRename($old, $old); + } + public function testRemoveAUser(): void { $user = "john.doe@example.com"; - $pass = "secret"; $u = new User($this->drv); \Phake::when($this->drv)->userRemove->thenReturn(true); \Phake::when(Arsse::$db)->userExists->thenReturn(true); @@ -188,7 +235,6 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { public function testRemoveAUserWeDoNotKnow(): void { $user = "john.doe@example.com"; - $pass = "secret"; $u = new User($this->drv); \Phake::when($this->drv)->userRemove->thenReturn(true); \Phake::when(Arsse::$db)->userExists->thenReturn(false); From 405f3af257f3ef4645e560fbb62697d2330a50a5 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 25 Dec 2020 22:22:37 -0500 Subject: [PATCH 088/366] Invalidate sessions and Fever passwords when renaming users --- lib/User.php | 9 +++++++-- tests/cases/User/TestUser.php | 26 ++++++++++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/User.php b/lib/User.php index 1c7979bc..d0bbbf80 100644 --- a/lib/User.php +++ b/lib/User.php @@ -106,12 +106,17 @@ class User { public function rename(string $user, string $newName): bool { if ($this->u->userRename($user, $newName)) { + $tr = Arsse::$db->begin(); if (!Arsse::$db->userExists($user)) { Arsse::$db->userAdd($newName, null); - return true; } else { - return Arsse::$db->userRename($user, $newName); + Arsse::$db->userRename($user, $newName); + // invalidate any sessions and Fever passwords + Arsse::$db->sessionDestroy($newName); + Arsse::$db->tokenRevoke($newName, "fever.login"); } + $tr->commit(); + return true; } return false; } diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index e42832e9..c2a2645d 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -183,6 +183,8 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { } public function testRenameAUser(): void { + $tr = \Phake::mock(Transaction::class); + \Phake::when(Arsse::$db)->begin->thenReturn($tr); \Phake::when(Arsse::$db)->userExists->thenReturn(true); \Phake::when(Arsse::$db)->userAdd->thenReturn(true); \Phake::when(Arsse::$db)->userRename->thenReturn(true); @@ -191,12 +193,20 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $old = "john.doe@example.com"; $new = "jane.doe@example.com"; $this->assertTrue($u->rename($old, $new)); - \Phake::verify($this->drv)->userRename($old, $new); - \Phake::verify(Arsse::$db)->userExists($old); - \Phake::verify(Arsse::$db)->userRename($old, $new); + \Phake::inOrder( + \Phake::verify($this->drv)->userRename($old, $new), + \Phake::verify(Arsse::$db)->begin(), + \Phake::verify(Arsse::$db)->userExists($old), + \Phake::verify(Arsse::$db)->userRename($old, $new), + \Phake::verify(Arsse::$db)->sessionDestroy($new), + \Phake::verify(Arsse::$db)->tokenRevoke($new, "fever.login"), + \Phake::verify($tr)->commit() + ); } public function testRenameAUserWeDoNotKnow(): void { + $tr = \Phake::mock(Transaction::class); + \Phake::when(Arsse::$db)->begin->thenReturn($tr); \Phake::when(Arsse::$db)->userExists->thenReturn(false); \Phake::when(Arsse::$db)->userAdd->thenReturn(true); \Phake::when(Arsse::$db)->userRename->thenReturn(true); @@ -205,9 +215,13 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $old = "john.doe@example.com"; $new = "jane.doe@example.com"; $this->assertTrue($u->rename($old, $new)); - \Phake::verify($this->drv)->userRename($old, $new); - \Phake::verify(Arsse::$db)->userExists($old); - \Phake::verify(Arsse::$db)->userAdd($new, null); + \Phake::inOrder( + \Phake::verify($this->drv)->userRename($old, $new), + \Phake::verify(Arsse::$db)->begin(), + \Phake::verify(Arsse::$db)->userExists($old), + \Phake::verify(Arsse::$db)->userAdd($new, null), + \Phake::verify($tr)->commit() + ); } public function testRenameAUserWithoutEffect(): void { From 2946d950f2559dea004465866681c00725adc60c Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 27 Dec 2020 10:08:00 -0500 Subject: [PATCH 089/366] Forbid more user names - Control characters are now forbidden - Controls and colons are now also forbidden when renaming --- CHANGELOG | 9 +++++---- lib/User.php | 13 ++++++++++--- tests/cases/User/TestUser.php | 27 +++++++++++++++++++++------ 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a3847c4f..8580d40b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,13 +1,14 @@ Version 0.9.0 (????-??-??) ========================== +New features: +- Support for the Miniflux protocol (see manual for details) + Bug fixes: - Use icons specified in Atom feeds when available - Do not return null as subscription unread count - -Changes: -- Explicitly forbid U+003A COLON in usernames, for compatibility with HTTP - Basic authentication +- Explicitly forbid U+003A COLON and control characters in usernames, for + compatibility with RFC 7617 Version 0.8.5 (2020-10-27) ========================== diff --git a/lib/User.php b/lib/User.php index d0bbbf80..accec103 100644 --- a/lib/User.php +++ b/lib/User.php @@ -84,10 +84,11 @@ class User { } public function add(string $user, ?string $password = null): string { - // ensure the user name does not contain any U+003A COLON characters, as + // ensure the user name does not contain any U+003A COLON or control characters, as // this is incompatible with HTTP Basic authentication - if (strpos($user, ":") !== false) { - throw new User\ExceptionInput("invalidUsername", "U+003A COLON"); + if (preg_match("/[\x{00}-\x{1F}\x{7F}:]/", $user, $m)) { + $c = ord($m[0]); + throw new User\ExceptionInput("invalidUsername", "U+".str_pad((string) $c, 4, "0", \STR_PAD_LEFT)." ".\IntlChar::charName($c, \IntlChar::EXTENDED_CHAR_NAME)); } try { $out = $this->u->userAdd($user, $password) ?? $this->u->userAdd($user, $this->generatePassword()); @@ -105,6 +106,12 @@ class User { } public function rename(string $user, string $newName): bool { + // ensure the new user name does not contain any U+003A COLON or + // control characters, as this is incompatible with HTTP Basic authentication + if (preg_match("/[\x{00}-\x{1F}\x{7F}:]/", $newName, $m)) { + $c = ord($m[0]); + throw new User\ExceptionInput("invalidUsername", "U+".str_pad((string) $c, 4, "0", \STR_PAD_LEFT)." ".\IntlChar::charName($c, \IntlChar::EXTENDED_CHAR_NAME)); + } if ($this->u->userRename($user, $newName)) { $tr = Arsse::$db->begin(); if (!Arsse::$db->userExists($user)) { diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index c2a2645d..7c87e0c3 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -160,13 +160,22 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { } } - public function testAddAnInvalidUser(): void { - $user = "john:doe@example.com"; - $pass = "secret"; + /** @dataProvider provideInvalidUserNames */ + public function testAddAnInvalidUser(string $user): void { $u = new User($this->drv); - \Phake::when($this->drv)->userAdd->thenThrow(new ExceptionInput("invalidUsername")); $this->assertException("invalidUsername", "User", "ExceptionInput"); - $u->add($user, $pass); + $u->add($user, "secret"); + } + + public function provideInvalidUserNames(): iterable { + // output names with control characters + foreach (array_merge(range(0x00, 0x1F), [0x7F]) as $ord) { + yield [chr($ord)]; + yield ["john".chr($ord)."doe@example.com"]; + } + // also handle colons + yield [":"]; + yield ["john:doe@example.com"]; } public function testAddAUserWithARandomPassword(): void { @@ -231,11 +240,17 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::when($this->drv)->userRename->thenReturn(false); $u = new User($this->drv); $old = "john.doe@example.com"; - $new = "jane.doe@example.com"; $this->assertFalse($u->rename($old, $old)); \Phake::verify($this->drv)->userRename($old, $old); } + /** @dataProvider provideInvalidUserNames */ + public function testRenameAUserToAnInvalidName(string $new): void { + $u = new User($this->drv); + $this->assertException("invalidUsername", "User", "ExceptionInput"); + $u->rename("john.doe@example.com", $new); + } + public function testRemoveAUser(): void { $user = "john.doe@example.com"; $u = new User($this->drv); From f58005640a76c801a59b74f3fda6c70b14fc4660 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 28 Dec 2020 08:12:30 -0500 Subject: [PATCH 090/366] Prototype user modification --- lib/REST/Miniflux/V1.php | 126 +++++++++++++++++++++++---- locale/en.php | 6 ++ tests/cases/REST/Miniflux/TestV1.php | 15 ++-- 3 files changed, 122 insertions(+), 25 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index bf3da2ea..4822adfa 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -16,7 +16,8 @@ use JKingWeb\Arsse\Misc\HTTP; use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\ValueInfo as V; use JKingWeb\Arsse\REST\Exception; -use JKingWeb\Arsse\User\ExceptionConflict as UserException; +use JKingWeb\Arsse\User\ExceptionConflict; +use JKingWeb\Arsse\User\Exception as UserException; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\EmptyResponse; @@ -29,12 +30,26 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected const ACCEPTED_TYPES_JSON = ["application/json"]; protected const TOKEN_LENGTH = 32; protected const VALID_JSON = [ + // user properties which map directly to Arsse user metadata are listed separately 'url' => "string", 'username' => "string", 'password' => "string", 'user_agent' => "string", 'title' => "string", ]; + protected const USER_META_MAP = [ + // Miniflux ID // Arsse ID Default value Extra + 'is_admin' => ["admin", false, false], + 'theme' => ["theme", "light_serif", false], + 'language' => ["lang", "en_US", false], + 'timezone' => ["tz", "UTC", false], + 'entry_sorting_direction' => ["sort_asc", false, false], + 'entries_per_page' => ["page_size", 100, false], + 'keyboard_shortcuts' => ["shortcuts", true, false], + 'show_reading_time' => ["reading_time", true, false], + 'entry_swipe' => ["swipe", true, false], + 'custom_css' => ["stylesheet", "", true], + ]; protected const CALLS = [ // handler method Admin Path Body Query '/categories' => [ 'GET' => ["getCategories", false, false, false, false], @@ -102,7 +117,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { ], '/users/1' => [ 'GET' => ["getUserByNum", true, true, false, false], - 'PUT' => ["updateUserByNum", true, true, true, false], + 'PUT' => ["updateUserByNum", false, true, true, false], // requires admin for users other than self 'DELETE' => ["deleteUserByNum", true, true, false, false], ], '/users/1/mark-all-as-read' => [ @@ -246,7 +261,17 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if (!isset($body[$k])) { $body[$k] = null; } elseif (gettype($body[$k]) !== $t) { - return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])]); + return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422); + } + } + foreach (self::USER_META_MAP as $k => [,$d,]) { + $t = gettype($d); + if (!isset($body[$k])) { + $body[$k] = null; + } elseif (gettype($body[$k]) !== $t) { + return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422); + } elseif ($k === "entry_sorting_direction" && !in_array($body[$k], ["asc", "desc"])) { + return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422); } } return $body; @@ -285,23 +310,23 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { continue; } } - $out[] = [ + $entry = [ 'id' => $info['num'], 'username' => $u, - 'is_admin' => $info['admin'] ?? false, - 'theme' => $info['theme'] ?? "light_serif", - 'language' => $info['lang'] ?? "en_US", - 'timezone' => $info['tz'] ?? "UTC", - 'entry_sorting_direction' => ($info['sort_asc'] ?? false) ? "asc" : "desc", - 'entries_per_page' => $info['page_size'] ?? 100, - 'keyboard_shortcuts' => $info['shortcuts'] ?? true, - 'show_reading_time' => $info['reading_time'] ?? true, 'last_login_at' => $now, - 'entry_swipe' => $info['swipe'] ?? true, - 'extra' => [ - 'custom_css' => $info['stylesheet'] ?? "", - ], ]; + foreach (self::USER_META_MAP as $ext => [$int, $default, $extra]) { + if (!$extra) { + $entry[$ext] = $info[$int] ?? $default; + } else { + if (!isset($entry['extra'])) { + $entry['extra'] = []; + } + $entry['extra'][$ext] = $info[$int] ?? $default; + } + } + $entry['entry_sorting_direction'] = ($entry['entry_sorting_direction']) ? "asc" : "desc"; + $out[] = $entry; } return $out; } @@ -326,6 +351,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function getUsers(): ResponseInterface { + $tr = Arsse::$user->begin(); return new Response($this->listUsers(Arsse::$user->list(), false)); } @@ -350,6 +376,70 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($this->listUsers([Arsse::$user->id], false)[0] ?? new \stdClass); } + protected function updateUserByNum(array $data, array $path): ResponseInterface { + try { + if (!$this->isAdmin()) { + // this function is restricted to admins unless the affected user and calling user are the same + if (Arsse::$db->userLookup((int) $path[1]) !== Arsse::$user->id) { + return new ErrorResponse("403", 403); + } elseif ($data['is_admin']) { + // non-admins should not be able to set themselves as admin + return new ErrorResponse("InvalidElevation"); + } + $user = Arsse::$user->id; + } else { + $user = Arsse::$db->userLookup((int) $path[1]); + } + } catch (ExceptionConflict $e) { + return new ErrorResponse("404", 404); + } + // map Miniflux properties to internal metadata properties + $in = []; + foreach (self::USER_META_MAP as $i => [$o,,]) { + if (isset($data[$i])) { + if ($i === "entry_sorting_direction") { + $in[$o] = $data[$i] === "asc"; + } else { + $in[$o] = $data[$i]; + } + } + } + // make any requested changes + try { + $tr = Arsse::$user->begin(); + if (isset($data['username'])) { + Arsse::$user->rename($user, $data['username']); + $user = $data['username']; + } + if (isset($data['password'])) { + Arsse::$user->passwordSet($user, $data['password']); + } + if ($in) { + Arsse::$user->propertiesSet($user, $in); + } + // read out the newly-modified user and commit the changes + $out = $this->listUsers([$user], true)[0]; + $tr->commit(); + } catch (UserException $e) { + switch ($e->getCode()) { + case 10403: + return new ErrorResponse(["DuplicateUser", 'user' => $data['username']], 409); + case 20441: + return new ErrorResponse(["InvalidTimeone", 'tz' => $data['timezone']], 422); + case 10443: + return new ErrorResponse("InvalidPageSize", 422); + case 10444: + return new ErrorResponse(["InvalidUsername", $e->getMessage()], 422); + } + throw $e; // @codeCoverageIgnore + } + // add the input password if a password change was requested + if (isset($data['password'])) { + $out['password'] = $data['password']; + } + return new Response($out); + } + protected function getCategories(): ResponseInterface { $out = []; $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); @@ -374,7 +464,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); - return new Response(['id' => $id + 1, 'title' => $data['title'], 'user_id' => $meta['num']]); + return new Response(['id' => $id + 1, 'title' => $data['title'], 'user_id' => $meta['num']], 201); } protected function updateCategory(array $path, array $data): ResponseInterface { @@ -449,7 +539,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { public static function tokenList(string $user): array { if (!Arsse::$db->userExists($user)) { - throw new UserException("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + throw new ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); } $out = []; foreach (Arsse::$db->tokenList($user, "miniflux.login") as $r) { diff --git a/locale/en.php b/locale/en.php index acdaa601..b0cfe82d 100644 --- a/locale/en.php +++ b/locale/en.php @@ -13,12 +13,18 @@ return [ 'API.Miniflux.Error.404' => 'Resource Not Found', 'API.Miniflux.Error.InvalidBodyJSON' => 'Invalid JSON payload: {0}', 'API.Miniflux.Error.InvalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}', + 'API.Miniflux.Error.InvalidInputValue' => 'Supplied value is not valid for input key "{field}"', 'API.Miniflux.Error.Fetch404' => 'Resource not found (404), this feed doesn\'t exists anymore, check the feed URL', 'API.Miniflux.Error.Fetch401' => 'You are not authorized to access this resource (invalid username/password)', 'API.Miniflux.Error.Fetch403' => 'Unable to fetch this resource (Status Code = 403)', 'API.Miniflux.Error.FetchOther' => 'Unable to fetch this resource', 'API.Miniflux.Error.DuplicateCategory' => 'Category "{title}" already exists', 'API.Miniflux.Error.InvalidCategory' => 'Invalid category title "{title}"', + 'API.Miniflux.Error.InvalidElevation' => 'Only administrators can change permissions of standard users', + 'API.Miniflux.Error.DuplicateUser' => 'The user name "{user}" already exists', + 'API.Miniflux.Error.InvalidUser' => '{0}', + 'API.Miniflux.Error.InvalidTimezone' => 'Specified time zone "{tz}" is invalid', + 'API.Miniflux.Error.InvalidPageSize' => 'Page size must be greater than zero', 'API.TTRSS.Category.Uncategorized' => 'Uncategorized', 'API.TTRSS.Category.Special' => 'Special', diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 918a3da6..0b9f68d3 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -16,6 +16,7 @@ use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\REST\Miniflux\V1; use JKingWeb\Arsse\REST\Miniflux\ErrorResponse; use JKingWeb\Arsse\User\ExceptionConflict; +use JKingWeb\Arsse\User\Exception; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\EmptyResponse; @@ -32,6 +33,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { [ 'id' => 1, 'username' => "john.doe@example.com", + 'last_login_at' => self::NOW, 'is_admin' => true, 'theme' => "custom", 'language' => "fr_CA", @@ -40,7 +42,6 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { 'entries_per_page' => 200, 'keyboard_shortcuts' => false, 'show_reading_time' => false, - 'last_login_at' => self::NOW, 'entry_swipe' => false, 'extra' => [ 'custom_css' => "p {}", @@ -49,6 +50,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { [ 'id' => 2, 'username' => "jane.doe@example.com", + 'last_login_at' => self::NOW, 'is_admin' => false, 'theme' => "light_serif", 'language' => "en_US", @@ -57,7 +59,6 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { 'entries_per_page' => 100, 'keyboard_shortcuts' => true, 'show_reading_time' => true, - 'last_login_at' => self::NOW, 'entry_swipe' => true, 'extra' => [ 'custom_css' => "", @@ -166,7 +167,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } public function testRejectBadlyTypedData(): void { - $exp = new ErrorResponse(["InvalidInputType", 'field' => "url", 'expected' => "string", 'actual' => "integer"], 400); + $exp = new ErrorResponse(["InvalidInputType", 'field' => "url", 'expected' => "string", 'actual' => "integer"], 422); $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => 2112])); } @@ -277,11 +278,11 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function provideCategoryAdditions(): iterable { return [ - ["New", new Response(['id' => 2112, 'title' => "New", 'user_id' => 42])], + ["New", new Response(['id' => 2112, 'title' => "New", 'user_id' => 42], 201)], ["Duplicate", new ErrorResponse(["DuplicateCategory", 'title' => "Duplicate"], 500)], ["", new ErrorResponse(["InvalidCategory", 'title' => ""], 500)], [" ", new ErrorResponse(["InvalidCategory", 'title' => " "], 500)], - [false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 400)], + [false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"],422)], ]; } @@ -307,12 +308,12 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { [2, "Duplicate", "constraintViolation", new ErrorResponse(["DuplicateCategory", 'title' => "Duplicate"], 500)], [2, "", "missing", new ErrorResponse(["InvalidCategory", 'title' => ""], 500)], [2, " ", "whitespace", new ErrorResponse(["InvalidCategory", 'title' => " "], 500)], - [2, false, "subjectMissing", new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 400)], + [2, false, "subjectMissing", new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"],422)], [1, "New", true, new Response(['id' => 1, 'title' => "New", 'user_id' => 42])], [1, "Duplicate", "constraintViolation", new Response(['id' => 1, 'title' => "Duplicate", 'user_id' => 42])], // This is allowed because the name of the root folder is only a duplicate in circumstances where it is used [1, "", "missing", new ErrorResponse(["InvalidCategory", 'title' => ""], 500)], [1, " ", "whitespace", new ErrorResponse(["InvalidCategory", 'title' => " "], 500)], - [1, false, false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 400)], + [1, false, false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 422)], ]; } From 67f577d573423ba3fab0070b80cbd86ca763889d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 28 Dec 2020 08:43:54 -0500 Subject: [PATCH 091/366] Bump emulated Miniflux version --- lib/REST/Miniflux/V1.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 4822adfa..a532304a 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -24,7 +24,7 @@ use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\JsonResponse as Response; class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { - public const VERSION = "2.0.25"; + public const VERSION = "2.0.26"; protected const ACCEPTED_TYPES_OPML = ["application/xml", "text/xml", "text/x-opml"]; protected const ACCEPTED_TYPES_JSON = ["application/json"]; From cc648e1c3a7c5ce8fb38907a0ae649d99a1817cf Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 28 Dec 2020 11:42:36 -0500 Subject: [PATCH 092/366] Update tooling --- composer.lock | 154 +--------- vendor-bin/csfixer/composer.lock | 501 ++++--------------------------- vendor-bin/daux/composer.lock | 437 ++++----------------------- vendor-bin/phpunit/composer.lock | 485 +++++++++--------------------- vendor-bin/robo/composer.lock | 411 ++++--------------------- 5 files changed, 322 insertions(+), 1666 deletions(-) diff --git a/composer.lock b/composer.lock index f69d4dbb..b3e19e88 100644 --- a/composer.lock +++ b/composer.lock @@ -50,10 +50,6 @@ "cli", "docs" ], - "support": { - "issues": "https://github.com/docopt/docopt.php/issues", - "source": "https://github.com/docopt/docopt.php/tree/1.0.4" - }, "time": "2019-12-03T02:48:46+00:00" }, { @@ -121,10 +117,6 @@ "rest", "web service" ], - "support": { - "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/6.5" - }, "time": "2020-06-16T21:01:06+00:00" }, { @@ -176,10 +168,6 @@ "keywords": [ "promise" ], - "support": { - "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.4.0" - }, "time": "2020-09-30T07:37:28+00:00" }, { @@ -251,10 +239,6 @@ "uri", "url" ], - "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.7.0" - }, "time": "2020-09-30T07:37:11+00:00" }, { @@ -295,10 +279,6 @@ } ], "description": "Password generator for generating policy-compliant passwords.", - "support": { - "issues": "https://github.com/hosteurope/password-generator/issues", - "source": "https://github.com/hosteurope/password-generator/tree/master" - }, "time": "2016-12-08T09:32:12+00:00" }, { @@ -344,10 +324,6 @@ "keywords": [ "uuid" ], - "support": { - "issues": "https://github.com/JKingweb/DrUUID/issues", - "source": "https://github.com/JKingweb/DrUUID/tree/3.0.0" - }, "time": "2017-02-09T14:17:01+00:00" }, { @@ -423,10 +399,6 @@ "rfc7234", "validation" ], - "support": { - "issues": "https://github.com/Kevinrob/guzzle-cache-middleware/issues", - "source": "https://github.com/Kevinrob/guzzle-cache-middleware/tree/master" - }, "time": "2017-08-17T12:23:43+00:00" }, { @@ -512,20 +484,6 @@ "psr-17", "psr-7" ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-diactoros/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-diactoros/issues", - "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", - "source": "https://github.com/laminas/laminas-diactoros" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], "time": "2020-09-03T14:29:41+00:00" }, { @@ -585,20 +543,6 @@ "psr-15", "psr-7" ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-httphandlerrunner/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-httphandlerrunner/issues", - "rss": "https://github.com/laminas/laminas-httphandlerrunner/releases.atom", - "source": "https://github.com/laminas/laminas-httphandlerrunner" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], "time": "2020-06-03T15:52:17+00:00" }, { @@ -649,14 +593,6 @@ "security", "xml" ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-xml/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-xml/issues", - "rss": "https://github.com/laminas/laminas-xml/releases.atom", - "source": "https://github.com/laminas/laminas-xml" - }, "time": "2019-12-31T18:05:42+00:00" }, { @@ -705,18 +641,6 @@ "laminas", "zf" ], - "support": { - "forum": "https://discourse.laminas.dev/", - "issues": "https://github.com/laminas/laminas-zendframework-bridge/issues", - "rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom", - "source": "https://github.com/laminas/laminas-zendframework-bridge" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], "time": "2020-09-14T14:23:00+00:00" }, { @@ -779,9 +703,6 @@ ], "description": "RSS/Atom parsing library", "homepage": "https://github.com/nicolus/picoFeed", - "support": { - "source": "https://github.com/nicolus/picoFeed/tree/0.1.43" - }, "time": "2020-09-15T07:28:23+00:00" }, { @@ -834,9 +755,6 @@ "request", "response" ], - "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" - }, "time": "2019-04-30T12:38:16+00:00" }, { @@ -887,9 +805,6 @@ "request", "response" ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/master" - }, "time": "2016-08-06T14:39:51+00:00" }, { @@ -943,10 +858,6 @@ "response", "server" ], - "support": { - "issues": "https://github.com/php-fig/http-server-handler/issues", - "source": "https://github.com/php-fig/http-server-handler/tree/master" - }, "time": "2018-10-30T16:46:14+00:00" }, { @@ -994,9 +905,6 @@ "psr", "psr-3" ], - "support": { - "source": "https://github.com/php-fig/log/tree/1.1.3" - }, "time": "2020-03-23T09:12:05+00:00" }, { @@ -1037,10 +945,6 @@ } ], "description": "A polyfill for getallheaders.", - "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" - }, "time": "2019-03-08T08:55:37+00:00" }, { @@ -1111,23 +1015,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1195,23 +1082,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1271,23 +1141,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" } ], @@ -1336,10 +1189,6 @@ "isolation", "tool" ], - "support": { - "issues": "https://github.com/bamarni/composer-bin-plugin/issues", - "source": "https://github.com/bamarni/composer-bin-plugin/tree/master" - }, "time": "2020-05-03T08:27:20+00:00" } ], @@ -1358,6 +1207,5 @@ "platform-dev": [], "platform-overrides": { "php": "7.1.33" - }, - "plugin-api-version": "2.0.0" + } } diff --git a/vendor-bin/csfixer/composer.lock b/vendor-bin/csfixer/composer.lock index f3fbaff0..17bfa2ef 100644 --- a/vendor-bin/csfixer/composer.lock +++ b/vendor-bin/csfixer/composer.lock @@ -72,20 +72,6 @@ "issues": "https://github.com/composer/semver/issues", "source": "https://github.com/composer/semver/tree/3.2.4" }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], "time": "2020-11-13T08:59:24+00:00" }, { @@ -135,20 +121,6 @@ "issues": "https://github.com/composer/xdebug-handler/issues", "source": "https://github.com/composer/xdebug-handler/tree/1.4.5" }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], "time": "2020-11-13T08:04:11+00:00" }, { @@ -286,38 +258,20 @@ "parser", "php" ], - "support": { - "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.1" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" - } - ], "time": "2020-05-25T17:44:05+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.16.7", + "version": "v2.17.3", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "4e35806a6d7d8510d6842ae932e8832363d22c87" + "reference": "bd32f5dd72cdfc7b53f54077f980e144bfa2f595" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/4e35806a6d7d8510d6842ae932e8832363d22c87", - "reference": "4e35806a6d7d8510d6842ae932e8832363d22c87", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/bd32f5dd72cdfc7b53f54077f980e144bfa2f595", + "reference": "bd32f5dd72cdfc7b53f54077f980e144bfa2f595", "shasum": "" }, "require": { @@ -326,7 +280,7 @@ "doctrine/annotations": "^1.2", "ext-json": "*", "ext-tokenizer": "*", - "php": "^7.1", + "php": "^5.6 || ^7.0 || ^8.0", "php-cs-fixer/diff": "^1.3", "symfony/console": "^3.4.43 || ^4.1.6 || ^5.0", "symfony/event-dispatcher": "^3.0 || ^4.0 || ^5.0", @@ -343,12 +297,15 @@ "justinrainbow/json-schema": "^5.0", "keradus/cli-executor": "^1.4", "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^2.4.1", + "php-coveralls/php-coveralls": "^2.4.2", "php-cs-fixer/accessible-object": "^1.0", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", - "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.1", + "phpspec/prophecy-phpunit": "^1.1 || ^2.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.13 || ^9.4.4 <9.5", + "phpunitgoodpractices/polyfill": "^1.5", "phpunitgoodpractices/traits": "^1.9.1", + "sanmai/phpunit-legacy-adapter": "^6.4 || ^8.2.1", "symfony/phpunit-bridge": "^5.1", "symfony/yaml": "^3.0 || ^4.0 || ^5.0" }, @@ -395,17 +352,7 @@ } ], "description": "A tool to automatically fix PHP code style", - "support": { - "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.16.7" - }, - "funding": [ - { - "url": "https://github.com/keradus", - "type": "github" - } - ], - "time": "2020-10-27T22:44:27+00:00" + "time": "2020-12-24T11:14:44+00:00" }, { "name": "php-cs-fixer/diff", @@ -456,10 +403,6 @@ "keywords": [ "diff" ], - "support": { - "issues": "https://github.com/PHP-CS-Fixer/diff/issues", - "source": "https://github.com/PHP-CS-Fixer/diff/tree/v1.3.1" - }, "time": "2020-10-14T08:39:05+00:00" }, { @@ -509,10 +452,6 @@ "container-interop", "psr" ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/master" - }, "time": "2017-02-14T16:28:37+00:00" }, { @@ -559,10 +498,6 @@ "psr", "psr-14" ], - "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" - }, "time": "2019-01-08T18:20:26+00:00" }, { @@ -610,23 +545,20 @@ "psr", "psr-3" ], - "support": { - "source": "https://github.com/php-fig/log/tree/1.1.3" - }, "time": "2020-03-23T09:12:05+00:00" }, { "name": "symfony/console", - "version": "v5.1.8", + "version": "v5.2.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e" + "reference": "47c02526c532fb381374dab26df05e7313978976" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e", - "reference": "e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e", + "url": "https://api.github.com/repos/symfony/console/zipball/47c02526c532fb381374dab26df05e7313978976", + "reference": "47c02526c532fb381374dab26df05e7313978976", "shasum": "" }, "require": { @@ -687,24 +619,13 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/console/tree/v5.1.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } + "keywords": [ + "cli", + "command line", + "console", + "terminal" ], - "time": "2020-10-24T12:01:57+00:00" + "time": "2020-12-18T08:03:05+00:00" }, { "name": "symfony/deprecation-contracts", @@ -754,37 +675,20 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/master" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-09-07T11:33:47+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.1.8", + "version": "v5.2.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "26f4edae48c913fc183a3da0553fe63bdfbd361a" + "reference": "1c93f7a1dff592c252574c79a8635a8a80856042" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/26f4edae48c913fc183a3da0553fe63bdfbd361a", - "reference": "26f4edae48c913fc183a3da0553fe63bdfbd361a", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/1c93f7a1dff592c252574c79a8635a8a80856042", + "reference": "1c93f7a1dff592c252574c79a8635a8a80856042", "shasum": "" }, "require": { @@ -839,24 +743,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.1.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T12:01:57+00:00" + "time": "2020-12-18T08:03:05+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -918,37 +805,20 @@ "interoperability", "standards" ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.2.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-09-07T11:33:47+00:00" }, { "name": "symfony/filesystem", - "version": "v5.1.8", + "version": "v5.2.1", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "df08650ea7aee2d925380069c131a66124d79177" + "reference": "fa8f8cab6b65e2d99a118e082935344c5ba8c60d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/df08650ea7aee2d925380069c131a66124d79177", - "reference": "df08650ea7aee2d925380069c131a66124d79177", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/fa8f8cab6b65e2d99a118e082935344c5ba8c60d", + "reference": "fa8f8cab6b65e2d99a118e082935344c5ba8c60d", "shasum": "" }, "require": { @@ -980,37 +850,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.1.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T12:01:57+00:00" + "time": "2020-11-30T17:05:38+00:00" }, { "name": "symfony/finder", - "version": "v5.1.8", + "version": "v5.2.1", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0" + "reference": "0b9231a5922fd7287ba5b411893c0ecd2733e5ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", - "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", + "url": "https://api.github.com/repos/symfony/finder/zipball/0b9231a5922fd7287ba5b411893c0ecd2733e5ba", + "reference": "0b9231a5922fd7287ba5b411893c0ecd2733e5ba", "shasum": "" }, "require": { @@ -1041,42 +894,26 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v5.1.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T12:01:57+00:00" + "time": "2020-12-08T17:02:38+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.1.8", + "version": "v5.2.1", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "c6a02905e4ffc7a1498e8ee019db2b477cd1cc02" + "reference": "87a2a4a766244e796dd9cb9d6f58c123358cd986" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/c6a02905e4ffc7a1498e8ee019db2b477cd1cc02", - "reference": "c6a02905e4ffc7a1498e8ee019db2b477cd1cc02", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/87a2a4a766244e796dd9cb9d6f58c123358cd986", + "reference": "87a2a4a766244e796dd9cb9d6f58c123358cd986", "shasum": "" }, "require": { "php": ">=7.2.5", "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-php73": "~1.0", "symfony/polyfill-php80": "^1.15" }, "type": "library", @@ -1109,24 +946,7 @@ "configuration", "options" ], - "support": { - "source": "https://github.com/symfony/options-resolver/tree/v5.1.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T12:01:57+00:00" + "time": "2020-10-24T12:08:07+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1188,23 +1008,6 @@ "polyfill", "portable" ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1269,23 +1072,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1353,23 +1139,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1433,23 +1202,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1501,23 +1253,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php70/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1577,23 +1312,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1656,23 +1374,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1739,37 +1440,20 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/process", - "version": "v5.1.8", + "version": "v5.2.1", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f00872c3f6804150d6a0f73b4151daab96248101" + "reference": "bd8815b8b6705298beaa384f04fabd459c10bedd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f00872c3f6804150d6a0f73b4151daab96248101", - "reference": "f00872c3f6804150d6a0f73b4151daab96248101", + "url": "https://api.github.com/repos/symfony/process/zipball/bd8815b8b6705298beaa384f04fabd459c10bedd", + "reference": "bd8815b8b6705298beaa384f04fabd459c10bedd", "shasum": "" }, "require": { @@ -1801,24 +1485,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v5.1.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T12:01:57+00:00" + "time": "2020-12-08T17:03:37+00:00" }, { "name": "symfony/service-contracts", @@ -1880,37 +1547,20 @@ "interoperability", "standards" ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/master" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-09-07T11:33:47+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.1.8", + "version": "v5.2.1", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "3d9f57c89011f0266e6b1d469e5c0110513859d5" + "reference": "2b105c0354f39a63038a1d8bf776ee92852813af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/3d9f57c89011f0266e6b1d469e5c0110513859d5", - "reference": "3d9f57c89011f0266e6b1d469e5c0110513859d5", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/2b105c0354f39a63038a1d8bf776ee92852813af", + "reference": "2b105c0354f39a63038a1d8bf776ee92852813af", "shasum": "" }, "require": { @@ -1942,37 +1592,20 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.1.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T12:01:57+00:00" + "time": "2020-11-01T16:14:45+00:00" }, { "name": "symfony/string", - "version": "v5.1.8", + "version": "v5.2.1", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "a97573e960303db71be0dd8fda9be3bca5e0feea" + "reference": "5bd67751d2e3f7d6f770c9154b8fbcb2aa05f7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/a97573e960303db71be0dd8fda9be3bca5e0feea", - "reference": "a97573e960303db71be0dd8fda9be3bca5e0feea", + "url": "https://api.github.com/repos/symfony/string/zipball/5bd67751d2e3f7d6f770c9154b8fbcb2aa05f7ed", + "reference": "5bd67751d2e3f7d6f770c9154b8fbcb2aa05f7ed", "shasum": "" }, "require": { @@ -2025,24 +1658,7 @@ "utf-8", "utf8" ], - "support": { - "source": "https://github.com/symfony/string/tree/v5.1.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T12:01:57+00:00" + "time": "2020-12-05T07:33:16+00:00" } ], "aliases": [], @@ -2051,6 +1667,5 @@ "prefer-stable": false, "prefer-lowest": false, "platform": [], - "platform-dev": [], - "plugin-api-version": "2.0.0" + "platform-dev": [] } diff --git a/vendor-bin/daux/composer.lock b/vendor-bin/daux/composer.lock index 8e08bd22..35bc4c60 100644 --- a/vendor-bin/daux/composer.lock +++ b/vendor-bin/daux/composer.lock @@ -76,10 +76,6 @@ "markdown", "md" ], - "support": { - "issues": "https://github.com/dauxio/daux.io/issues", - "source": "https://github.com/dauxio/daux.io/tree/master" - }, "time": "2019-09-23T20:10:07+00:00" }, { @@ -147,10 +143,6 @@ "rest", "web service" ], - "support": { - "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/6.5" - }, "time": "2020-06-16T21:01:06+00:00" }, { @@ -202,10 +194,6 @@ "keywords": [ "promise" ], - "support": { - "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.4.0" - }, "time": "2020-09-30T07:37:28+00:00" }, { @@ -277,10 +265,6 @@ "uri", "url" ], - "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.7.0" - }, "time": "2020-09-30T07:37:11+00:00" }, { @@ -350,35 +334,29 @@ "markdown", "parser" ], - "support": { - "docs": "https://commonmark.thephpleague.com/", - "issues": "https://github.com/thephpleague/commonmark/issues", - "rss": "https://github.com/thephpleague/commonmark/releases.atom", - "source": "https://github.com/thephpleague/commonmark" - }, "time": "2019-03-28T13:52:31+00:00" }, { "name": "league/plates", - "version": "3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/thephpleague/plates.git", - "reference": "b1684b6f127714497a0ef927ce42c0b44b45a8af" + "reference": "6d3ee31199b536a4e003b34a356ca20f6f75496a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/plates/zipball/b1684b6f127714497a0ef927ce42c0b44b45a8af", - "reference": "b1684b6f127714497a0ef927ce42c0b44b45a8af", + "url": "https://api.github.com/repos/thephpleague/plates/zipball/6d3ee31199b536a4e003b34a356ca20f6f75496a", + "reference": "6d3ee31199b536a4e003b34a356ca20f6f75496a", "shasum": "" }, "require": { - "php": "^5.3 | ^7.0" + "php": "^7.0|^8.0" }, "require-dev": { - "mikey179/vfsstream": "^1.4", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~1.5" + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.5" }, "type": "library", "extra": { @@ -400,10 +378,15 @@ "name": "Jonathan Reinink", "email": "jonathan@reinink.ca", "role": "Developer" + }, + { + "name": "RJ Garcia", + "email": "ragboyjr@icloud.com", + "role": "Developer" } ], "description": "Plates, the native PHP template system that's fast, easy to use and easy to extend.", - "homepage": "http://platesphp.com", + "homepage": "https://platesphp.com", "keywords": [ "league", "package", @@ -411,11 +394,7 @@ "templating", "views" ], - "support": { - "issues": "https://github.com/thephpleague/plates/issues", - "source": "https://github.com/thephpleague/plates/tree/master" - }, - "time": "2016-12-28T00:14:17+00:00" + "time": "2020-12-25T05:00:37+00:00" }, { "name": "myclabs/deep-copy", @@ -467,12 +446,6 @@ "issues": "https://github.com/myclabs/DeepCopy/issues", "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], "time": "2020-11-13T09:40:50+00:00" }, { @@ -522,10 +495,6 @@ "container-interop", "psr" ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/master" - }, "time": "2017-02-14T16:28:37+00:00" }, { @@ -576,9 +545,6 @@ "request", "response" ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/master" - }, "time": "2016-08-06T14:39:51+00:00" }, { @@ -619,24 +585,20 @@ } ], "description": "A polyfill for getallheaders.", - "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" - }, "time": "2019-03-08T08:55:37+00:00" }, { "name": "scrivo/highlight.php", - "version": "v9.18.1.5", + "version": "v9.18.1.6", "source": { "type": "git", "url": "https://github.com/scrivo/highlight.php.git", - "reference": "fa75a865928a4a5d49e5e77faca6bd2f2410baaf" + "reference": "44a3d4136edb5ad8551590bf90f437db80b2d466" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/fa75a865928a4a5d49e5e77faca6bd2f2410baaf", - "reference": "fa75a865928a4a5d49e5e77faca6bd2f2410baaf", + "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/44a3d4136edb5ad8551590bf90f437db80b2d466", + "reference": "44a3d4136edb5ad8551590bf90f437db80b2d466", "shasum": "" }, "require": { @@ -650,9 +612,6 @@ "symfony/finder": "^2.8|^3.4", "symfony/var-dumper": "^2.8|^3.4" }, - "suggest": { - "ext-dom": "Needed to make use of the features in the utilities namespace" - }, "type": "library", "autoload": { "psr-0": { @@ -692,30 +651,20 @@ "highlight.php", "syntax" ], - "support": { - "issues": "https://github.com/scrivo/highlight.php/issues", - "source": "https://github.com/scrivo/highlight.php" - }, - "funding": [ - { - "url": "https://github.com/allejo", - "type": "github" - } - ], - "time": "2020-11-22T06:07:40+00:00" + "time": "2020-12-22T19:20:29+00:00" }, { "name": "symfony/console", - "version": "v4.4.16", + "version": "v4.4.18", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5" + "reference": "12e071278e396cc3e1c149857337e9e192deca0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/20f73dd143a5815d475e0838ff867bce1eebd9d5", - "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5", + "url": "https://api.github.com/repos/symfony/console/zipball/12e071278e396cc3e1c149857337e9e192deca0b", + "reference": "12e071278e396cc3e1c149857337e9e192deca0b", "shasum": "" }, "require": { @@ -774,24 +723,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/console/tree/v4.4.16" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T11:50:19+00:00" + "time": "2020-12-18T07:41:31+00:00" }, { "name": "symfony/deprecation-contracts", @@ -844,40 +776,27 @@ "support": { "source": "https://github.com/symfony/deprecation-contracts/tree/master" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-09-07T11:33:47+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.4.16", + "version": "v4.4.18", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "827a00811ef699e809a201ceafac0b2b246bf38a" + "reference": "5ebda66b51612516bf338d5f87da2f37ff74cf34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/827a00811ef699e809a201ceafac0b2b246bf38a", - "reference": "827a00811ef699e809a201ceafac0b2b246bf38a", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5ebda66b51612516bf338d5f87da2f37ff74cf34", + "reference": "5ebda66b51612516bf338d5f87da2f37ff74cf34", "shasum": "" }, "require": { "php": ">=7.1.3", "symfony/mime": "^4.3|^5.0", - "symfony/polyfill-mbstring": "~1.1" + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.15" }, "require-dev": { "predis/predis": "~1.0", @@ -908,37 +827,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/http-foundation/tree/v4.4.16" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T11:50:19+00:00" + "time": "2020-12-18T07:41:31+00:00" }, { "name": "symfony/intl", - "version": "v5.1.8", + "version": "v5.2.1", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "e353c6c37afa1ff90739b3941f60ff9fa650eec3" + "reference": "53927f98c9201fe5db3cfc4d574b1f4039020297" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/e353c6c37afa1ff90739b3941f60ff9fa650eec3", - "reference": "e353c6c37afa1ff90739b3941f60ff9fa650eec3", + "url": "https://api.github.com/repos/symfony/intl/zipball/53927f98c9201fe5db3cfc4d574b1f4039020297", + "reference": "53927f98c9201fe5db3cfc4d574b1f4039020297", "shasum": "" }, "require": { @@ -996,41 +898,25 @@ "l10n", "localization" ], - "support": { - "source": "https://github.com/symfony/intl/tree/v5.1.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T12:01:57+00:00" + "time": "2020-12-14T10:10:03+00:00" }, { "name": "symfony/mime", - "version": "v5.1.8", + "version": "v5.2.1", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "f5485a92c24d4bcfc2f3fc648744fb398482ff1b" + "reference": "de97005aef7426ba008c46ba840fc301df577ada" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/f5485a92c24d4bcfc2f3fc648744fb398482ff1b", - "reference": "f5485a92c24d4bcfc2f3fc648744fb398482ff1b", + "url": "https://api.github.com/repos/symfony/mime/zipball/de97005aef7426ba008c46ba840fc301df577ada", + "reference": "de97005aef7426ba008c46ba840fc301df577ada", "shasum": "" }, "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0", "symfony/polyfill-php80": "^1.15" @@ -1040,7 +926,11 @@ }, "require-dev": { "egulias/email-validator": "^2.1.10", - "symfony/dependency-injection": "^4.4|^5.0" + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/property-access": "^4.4|^5.1", + "symfony/property-info": "^4.4|^5.1", + "symfony/serializer": "^5.2" }, "type": "library", "autoload": { @@ -1071,24 +961,7 @@ "mime", "mime-type" ], - "support": { - "source": "https://github.com/symfony/mime/tree/v5.1.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T12:01:57+00:00" + "time": "2020-12-09T18:54:12+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1150,23 +1023,6 @@ "polyfill", "portable" ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1229,23 +1085,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1316,23 +1155,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1400,23 +1222,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1480,23 +1285,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1556,23 +1344,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1635,23 +1406,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1718,37 +1472,20 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/process", - "version": "v4.4.16", + "version": "v4.4.18", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "2f4b049fb80ca5e9874615a2a85dc2a502090f05" + "reference": "075316ff72233ce3d04a9743414292e834f2cb4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/2f4b049fb80ca5e9874615a2a85dc2a502090f05", - "reference": "2f4b049fb80ca5e9874615a2a85dc2a502090f05", + "url": "https://api.github.com/repos/symfony/process/zipball/075316ff72233ce3d04a9743414292e834f2cb4a", + "reference": "075316ff72233ce3d04a9743414292e834f2cb4a", "shasum": "" }, "require": { @@ -1779,24 +1516,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v4.4.16" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T11:50:19+00:00" + "time": "2020-12-08T16:59:59+00:00" }, { "name": "symfony/service-contracts", @@ -1858,37 +1578,20 @@ "interoperability", "standards" ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/master" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-09-07T11:33:47+00:00" }, { "name": "symfony/yaml", - "version": "v5.1.8", + "version": "v5.2.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "f284e032c3cefefb9943792132251b79a6127ca6" + "reference": "290ea5e03b8cf9b42c783163123f54441fb06939" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/f284e032c3cefefb9943792132251b79a6127ca6", - "reference": "f284e032c3cefefb9943792132251b79a6127ca6", + "url": "https://api.github.com/repos/symfony/yaml/zipball/290ea5e03b8cf9b42c783163123f54441fb06939", + "reference": "290ea5e03b8cf9b42c783163123f54441fb06939", "shasum": "" }, "require": { @@ -1933,24 +1636,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/yaml/tree/v5.1.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T12:03:25+00:00" + "time": "2020-12-08T17:02:38+00:00" }, { "name": "webuni/commonmark-table-extension", @@ -2008,10 +1694,6 @@ "markdown", "table" ], - "support": { - "issues": "https://github.com/webuni/commonmark-table-extension/issues", - "source": "https://github.com/webuni/commonmark-table-extension/tree/0.9.0" - }, "abandoned": "league/commonmark", "time": "2018-11-28T11:29:11+00:00" }, @@ -2094,6 +1776,5 @@ "prefer-stable": false, "prefer-lowest": false, "platform": [], - "platform-dev": [], - "plugin-api-version": "2.0.0" + "platform-dev": [] } diff --git a/vendor-bin/phpunit/composer.lock b/vendor-bin/phpunit/composer.lock index e963b995..45e101b1 100644 --- a/vendor-bin/phpunit/composer.lock +++ b/vendor-bin/phpunit/composer.lock @@ -9,21 +9,24 @@ "packages-dev": [ { "name": "clue/arguments", - "version": "v2.0.0", + "version": "v2.1.0", "source": { "type": "git", - "url": "https://github.com/clue/php-arguments.git", - "reference": "eb8356918bc51ac7e595e4ad92a2bc1c1d2754c2" + "url": "https://github.com/clue/arguments.git", + "reference": "87f2c4bc2ff602173bc52f5935a9c3b70d8c996d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/php-arguments/zipball/eb8356918bc51ac7e595e4ad92a2bc1c1d2754c2", - "reference": "eb8356918bc51ac7e595e4ad92a2bc1c1d2754c2", + "url": "https://api.github.com/repos/clue/arguments/zipball/87f2c4bc2ff602173bc52f5935a9c3b70d8c996d", + "reference": "87f2c4bc2ff602173bc52f5935a9c3b70d8c996d", "shasum": "" }, "require": { "php": ">=5.3" }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + }, "type": "library", "autoload": { "files": [ @@ -40,11 +43,11 @@ "authors": [ { "name": "Christian Lück", - "email": "christian@lueck.tv" + "email": "christian@clue.engineering" } ], "description": "The simple way to split your command line string into an array of command arguments in PHP.", - "homepage": "https://github.com/clue/php-arguments", + "homepage": "https://github.com/clue/arguments", "keywords": [ "args", "arguments", @@ -55,11 +58,7 @@ "parse", "split" ], - "support": { - "issues": "https://github.com/clue/php-arguments/issues", - "source": "https://github.com/clue/php-arguments/tree/v2.0.0" - }, - "time": "2016-12-18T14:37:39+00:00" + "time": "2020-12-08T13:02:50+00:00" }, { "name": "dms/phpunit-arraysubset-asserts", @@ -100,10 +99,6 @@ } ], "description": "This package provides Array Subset and related asserts once depracated in PHPunit 8", - "support": { - "issues": "https://github.com/rdohms/phpunit-arraysubset-asserts/issues", - "source": "https://github.com/rdohms/phpunit-arraysubset-asserts/tree/compat/phpunit8" - }, "time": "2020-02-18T21:20:04+00:00" }, { @@ -159,20 +154,6 @@ "issues": "https://github.com/doctrine/instantiator/issues", "source": "https://github.com/doctrine/instantiator/tree/1.4.0" }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], "time": "2020-11-10T18:47:58+00:00" }, { @@ -219,11 +200,6 @@ ], "description": "Virtual file system to mock the real file system in unit tests.", "homepage": "http://vfs.bovigo.org/", - "support": { - "issues": "https://github.com/bovigo/vfsStream/issues", - "source": "https://github.com/bovigo/vfsStream/tree/master", - "wiki": "https://github.com/bovigo/vfsStream/wiki" - }, "time": "2019-10-30T15:31:00+00:00" }, { @@ -276,12 +252,6 @@ "issues": "https://github.com/myclabs/DeepCopy/issues", "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], "time": "2020-11-13T09:40:50+00:00" }, { @@ -340,36 +310,33 @@ "mock", "testing" ], - "support": { - "issues": "https://github.com/mlively/Phake/issues", - "source": "https://github.com/mlively/Phake/tree/v3.1.8" - }, "time": "2020-05-11T18:43:26+00:00" }, { "name": "phar-io/manifest", - "version": "1.0.3", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", - "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", "shasum": "" }, "require": { "ext-dom": "*", "ext-phar": "*", - "phar-io/version": "^2.0", - "php": "^5.6 || ^7.0" + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -399,28 +366,24 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" - }, - "time": "2018-07-08T19:23:20+00:00" + "time": "2020-06-27T14:33:11+00:00" }, { "name": "phar-io/version", - "version": "2.0.1", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + "reference": "e4782611070e50613683d2b9a57730e9a3ba5451" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", - "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "url": "https://api.github.com/repos/phar-io/version/zipball/e4782611070e50613683d2b9a57730e9a3ba5451", + "reference": "e4782611070e50613683d2b9a57730e9a3ba5451", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { @@ -450,11 +413,7 @@ } ], "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/master" - }, - "time": "2018-07-08T19:19:57+00:00" + "time": "2020-12-13T23:18:30+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -503,10 +462,6 @@ "reflection", "static analysis" ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, "time": "2020-06-27T09:03:43+00:00" }, { @@ -559,10 +514,6 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" - }, "time": "2020-09-03T19:13:55+00:00" }, { @@ -608,24 +559,20 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" - }, "time": "2020-09-17T18:55:26+00:00" }, { "name": "phpspec/prophecy", - "version": "1.12.1", + "version": "1.12.2", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d" + "reference": "245710e971a030f42e08f4912863805570f23d39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d", - "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/245710e971a030f42e08f4912863805570f23d39", + "reference": "245710e971a030f42e08f4912863805570f23d39", "shasum": "" }, "require": { @@ -637,7 +584,7 @@ }, "require-dev": { "phpspec/phpspec": "^6.0", - "phpunit/phpunit": "^8.0 || ^9.0 <9.3" + "phpunit/phpunit": "^8.0 || ^9.0" }, "type": "library", "extra": { @@ -675,33 +622,29 @@ "spy", "stub" ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/1.12.1" - }, - "time": "2020-09-29T09:10:42+00:00" + "time": "2020-12-19T10:15:11+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "7.0.12", + "version": "7.0.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "52f55786aa2e52c26cd9e2db20aff2981e0f7399" + "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/52f55786aa2e52c26cd9e2db20aff2981e0f7399", - "reference": "52f55786aa2e52c26cd9e2db20aff2981e0f7399", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bb7c9a210c72e4709cdde67f8b7362f672f2225c", + "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^7.2", + "php": ">=7.2", "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.1.1", + "phpunit/php-token-stream": "^3.1.1 || ^4.0", "sebastian/code-unit-reverse-lookup": "^1.0.1", "sebastian/environment": "^4.2.2", "sebastian/version": "^2.0.1", @@ -742,37 +685,27 @@ "testing", "xunit" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.12" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-11-27T06:08:35+00:00" + "time": "2020-12-02T13:39:03+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "050bedf145a257b1ff02746c31894800e5122946" + "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", - "reference": "050bedf145a257b1ff02746c31894800e5122946", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4b49fb70f067272b659ef0174ff9ca40fdaa6357", + "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^7.1" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { @@ -802,11 +735,7 @@ "filesystem", "iterator" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.2" - }, - "time": "2018-09-13T20:33:42+00:00" + "time": "2020-11-30T08:25:21+00:00" }, { "name": "phpunit/php-text-template", @@ -847,31 +776,27 @@ "keywords": [ "template" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" - }, "time": "2015-06-21T13:50:34+00:00" }, { "name": "phpunit/php-timer", - "version": "2.1.2", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", - "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662", + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { @@ -900,37 +825,33 @@ "keywords": [ "timer" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/master" - }, - "time": "2019-06-07T04:22:29+00:00" + "time": "2020-11-30T08:20:02+00:00" }, { "name": "phpunit/php-token-stream", - "version": "3.1.1", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", - "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/a853a0e183b9db7eed023d7933a858fa1c8d25a3", + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": "^7.1" + "php": "^7.3 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -953,25 +874,21 @@ "keywords": [ "tokenizer" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", - "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.1" - }, "abandoned": true, - "time": "2019-09-17T06:23:10+00:00" + "time": "2020-08-04T08:28:15+00:00" }, { "name": "phpunit/phpunit", - "version": "8.5.11", + "version": "8.5.13", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3123601e3b29339b20129acc3f989cfec3274566" + "reference": "8e86be391a58104ef86037ba8a846524528d784e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3123601e3b29339b20129acc3f989cfec3274566", - "reference": "3123601e3b29339b20129acc3f989cfec3274566", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8e86be391a58104ef86037ba8a846524528d784e", + "reference": "8e86be391a58104ef86037ba8a846524528d784e", "shasum": "" }, "require": { @@ -983,9 +900,9 @@ "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.10.0", - "phar-io/manifest": "^1.0.3", - "phar-io/version": "^2.0.1", - "php": "^7.2", + "phar-io/manifest": "^2.0.1", + "phar-io/version": "^3.0.2", + "php": ">=7.2", "phpspec/prophecy": "^1.10.3", "phpunit/php-code-coverage": "^7.0.12", "phpunit/php-file-iterator": "^2.0.2", @@ -1041,41 +958,27 @@ "testing", "xunit" ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.11" - }, - "funding": [ - { - "url": "https://phpunit.de/donate.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-11-27T12:46:45+00:00" + "time": "2020-12-01T04:53:52+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": ">=5.6" }, "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.0" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { @@ -1100,33 +1003,29 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/master" - }, - "time": "2017-03-04T06:30:41+00:00" + "time": "2020-11-30T08:15:22+00:00" }, { "name": "sebastian/comparator", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" + "reference": "1071dfcef776a57013124ff35e1fc41ccd294758" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", - "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758", + "reference": "1071dfcef776a57013124ff35e1fc41ccd294758", "shasum": "" }, "require": { - "php": "^7.1", + "php": ">=7.1", "sebastian/diff": "^3.0", "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "^7.1" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { @@ -1144,6 +1043,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -1155,10 +1058,6 @@ { "name": "Bernhard Schussek", "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" } ], "description": "Provides the functionality to compare PHP values for equality", @@ -1168,28 +1067,24 @@ "compare", "equality" ], - "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/master" - }, - "time": "2018-07-12T15:12:46+00:00" + "time": "2020-11-30T08:04:30+00:00" }, { "name": "sebastian/diff", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", - "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.1" }, "require-dev": { "phpunit/phpunit": "^7.5 || ^8.0", @@ -1211,13 +1106,13 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" } ], "description": "Diff implementation", @@ -1228,28 +1123,24 @@ "unidiff", "unified diff" ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/master" - }, - "time": "2019-02-04T06:01:07+00:00" + "time": "2020-11-30T07:59:04+00:00" }, { "name": "sebastian/environment", - "version": "4.2.3", + "version": "4.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368" + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368", - "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.1" }, "require-dev": { "phpunit/phpunit": "^7.5" @@ -1285,28 +1176,24 @@ "environment", "hhvm" ], - "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/4.2.3" - }, - "time": "2019-11-20T08:46:58+00:00" + "time": "2020-11-30T07:53:42+00:00" }, { "name": "sebastian/exporter", - "version": "3.1.2", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" + "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", - "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/6b853149eab67d4da22291d36f5b0631c0fd856e", + "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e", "shasum": "" }, "require": { - "php": "^7.0", + "php": ">=7.0", "sebastian/recursion-context": "^3.0" }, "require-dev": { @@ -1356,28 +1243,24 @@ "export", "exporter" ], - "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/master" - }, - "time": "2019-09-14T09:02:43+00:00" + "time": "2020-11-30T07:47:53+00:00" }, { "name": "sebastian/global-state", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" + "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", - "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/474fb9edb7ab891665d3bfc6317f42a0a150454b", + "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b", "shasum": "" }, "require": { - "php": "^7.2", + "php": ">=7.2", "sebastian/object-reflector": "^1.1.1", "sebastian/recursion-context": "^3.0" }, @@ -1414,28 +1297,24 @@ "keywords": [ "global state" ], - "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/master" - }, - "time": "2019-02-01T05:30:01+00:00" + "time": "2020-11-30T07:43:24+00:00" }, { "name": "sebastian/object-enumerator", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", "shasum": "" }, "require": { - "php": "^7.0", + "php": ">=7.0", "sebastian/object-reflector": "^1.1.1", "sebastian/recursion-context": "^3.0" }, @@ -1465,28 +1344,24 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/master" - }, - "time": "2017-08-03T12:35:26+00:00" + "time": "2020-11-30T07:40:27+00:00" }, { "name": "sebastian/object-reflector", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "773f97c67f28de00d397be301821b06708fca0be" + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", - "reference": "773f97c67f28de00d397be301821b06708fca0be", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", "shasum": "" }, "require": { - "php": "^7.0" + "php": ">=7.0" }, "require-dev": { "phpunit/phpunit": "^6.0" @@ -1514,28 +1389,24 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/master" - }, - "time": "2017-03-29T09:07:27+00:00" + "time": "2020-11-30T07:37:18+00:00" }, { "name": "sebastian/recursion-context", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb", + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb", "shasum": "" }, "require": { - "php": "^7.0" + "php": ">=7.0" }, "require-dev": { "phpunit/phpunit": "^6.0" @@ -1556,14 +1427,14 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, { "name": "Adam Harvey", "email": "aharvey@php.net" @@ -1571,28 +1442,24 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/master" - }, - "time": "2017-03-03T06:23:57+00:00" + "time": "2020-11-30T07:34:24+00:00" }, { "name": "sebastian/resource-operations", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", - "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3", + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.1" }, "type": "library", "extra": { @@ -1617,28 +1484,24 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/master" - }, - "time": "2018-10-04T04:07:39+00:00" + "time": "2020-11-30T07:30:19+00:00" }, { "name": "sebastian/type", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3" + "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3", - "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0150cfbc4495ed2df3872fb31b26781e4e077eb4", + "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4", "shasum": "" }, "require": { - "php": "^7.2" + "php": ">=7.2" }, "require-dev": { "phpunit/phpunit": "^8.2" @@ -1667,11 +1530,7 @@ ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", - "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/master" - }, - "time": "2019-07-02T08:10:15+00:00" + "time": "2020-11-30T07:25:11+00:00" }, { "name": "sebastian/version", @@ -1714,10 +1573,6 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/master" - }, "time": "2016-10-03T07:35:21+00:00" }, { @@ -1780,23 +1635,6 @@ "polyfill", "portable" ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1837,16 +1675,6 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/master" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], "time": "2020-07-12T23:59:07+00:00" }, { @@ -1896,10 +1724,6 @@ "check", "validate" ], - "support": { - "issues": "https://github.com/webmozart/assert/issues", - "source": "https://github.com/webmozart/assert/tree/master" - }, "time": "2020-07-08T17:02:28+00:00" }, { @@ -1947,10 +1771,6 @@ } ], "description": "A PHP implementation of Ant's glob.", - "support": { - "issues": "https://github.com/webmozart/glob/issues", - "source": "https://github.com/webmozart/glob/tree/master" - }, "time": "2015-12-29T11:14:33+00:00" }, { @@ -1997,10 +1817,6 @@ } ], "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.", - "support": { - "issues": "https://github.com/webmozart/path-util/issues", - "source": "https://github.com/webmozart/path-util/tree/2.3.0" - }, "time": "2015-12-17T08:42:14+00:00" } ], @@ -2010,6 +1826,5 @@ "prefer-stable": false, "prefer-lowest": false, "platform": [], - "platform-dev": [], - "plugin-api-version": "2.0.0" + "platform-dev": [] } diff --git a/vendor-bin/robo/composer.lock b/vendor-bin/robo/composer.lock index c558c227..d4395718 100644 --- a/vendor-bin/robo/composer.lock +++ b/vendor-bin/robo/composer.lock @@ -9,46 +9,33 @@ "packages-dev": [ { "name": "consolidation/annotated-command", - "version": "4.2.3", + "version": "4.2.4", "source": { "type": "git", "url": "https://github.com/consolidation/annotated-command.git", - "reference": "4b596872f24c39d9c04d7b3adb6bc51baa1f2fd5" + "reference": "ec297e05cb86557671c2d6cbb1bebba6c7ae2c60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/4b596872f24c39d9c04d7b3adb6bc51baa1f2fd5", - "reference": "4b596872f24c39d9c04d7b3adb6bc51baa1f2fd5", + "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/ec297e05cb86557671c2d6cbb1bebba6c7ae2c60", + "reference": "ec297e05cb86557671c2d6cbb1bebba6c7ae2c60", "shasum": "" }, "require": { "consolidation/output-formatters": "^4.1.1", "php": ">=7.1.3", "psr/log": "^1|^2", - "symfony/console": "^4.4.8|^5", + "symfony/console": "^4.4.8|~5.1.0", "symfony/event-dispatcher": "^4.4.8|^5", "symfony/finder": "^4.4.8|^5" }, "require-dev": { - "g1a/composer-test-scenarios": "^3", - "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^6", - "squizlabs/php_codesniffer": "^3" + "phpunit/phpunit": ">=7.5.20", + "squizlabs/php_codesniffer": "^3", + "yoast/phpunit-polyfills": "^0.2.0" }, "type": "library", "extra": { - "scenarios": { - "symfony4": { - "require": { - "symfony/console": "^4.0" - }, - "config": { - "platform": { - "php": "7.1.3" - } - } - } - }, "branch-alias": { "dev-main": "4.x-dev" } @@ -69,11 +56,7 @@ } ], "description": "Initialize Symfony Console commands from annotated command class methods.", - "support": { - "issues": "https://github.com/consolidation/annotated-command/issues", - "source": "https://github.com/consolidation/annotated-command/tree/4.2.3" - }, - "time": "2020-10-03T14:28:42+00:00" + "time": "2020-12-10T16:56:39+00:00" }, { "name": "consolidation/config", @@ -154,24 +137,20 @@ } ], "description": "Provide configuration services for a commandline tool.", - "support": { - "issues": "https://github.com/consolidation/config/issues", - "source": "https://github.com/consolidation/config/tree/master" - }, "time": "2019-03-03T19:37:04+00:00" }, { "name": "consolidation/log", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/consolidation/log.git", - "reference": "ba0bf6af1fbd09ed4dc18fc2f27b12ceff487cbf" + "reference": "82a2aaaa621a7b976e50a745a8d249d5085ee2b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/log/zipball/ba0bf6af1fbd09ed4dc18fc2f27b12ceff487cbf", - "reference": "ba0bf6af1fbd09ed4dc18fc2f27b12ceff487cbf", + "url": "https://api.github.com/repos/consolidation/log/zipball/82a2aaaa621a7b976e50a745a8d249d5085ee2b1", + "reference": "82a2aaaa621a7b976e50a745a8d249d5085ee2b1", "shasum": "" }, "require": { @@ -180,27 +159,14 @@ "symfony/console": "^4|^5" }, "require-dev": { - "g1a/composer-test-scenarios": "^3", - "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^6", - "squizlabs/php_codesniffer": "^3" + "phpunit/phpunit": ">=7.5.20", + "squizlabs/php_codesniffer": "^3", + "yoast/phpunit-polyfills": "^0.2.0" }, "type": "library", "extra": { - "scenarios": { - "symfony4": { - "require-dev": { - "symfony/console": "^4" - }, - "config": { - "platform": { - "php": "7.1.3" - } - } - } - }, "branch-alias": { - "dev-master": "2.x-dev" + "dev-main": "2.x-dev" } }, "autoload": { @@ -219,24 +185,20 @@ } ], "description": "Improved Psr-3 / Psr\\Log logger based on Symfony Console components.", - "support": { - "issues": "https://github.com/consolidation/log/issues", - "source": "https://github.com/consolidation/log/tree/2.0.1" - }, - "time": "2020-05-27T17:06:13+00:00" + "time": "2020-12-10T16:26:23+00:00" }, { "name": "consolidation/output-formatters", - "version": "4.1.1", + "version": "4.1.2", "source": { "type": "git", "url": "https://github.com/consolidation/output-formatters.git", - "reference": "9deeddd6a916d0a756b216a8b40ce1016e17c0b9" + "reference": "5821e6ae076bf690058a4de6c94dce97398a69c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/9deeddd6a916d0a756b216a8b40ce1016e17c0b9", - "reference": "9deeddd6a916d0a756b216a8b40ce1016e17c0b9", + "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/5821e6ae076bf690058a4de6c94dce97398a69c9", + "reference": "5821e6ae076bf690058a4de6c94dce97398a69c9", "shasum": "" }, "require": { @@ -246,32 +208,20 @@ "symfony/finder": "^4|^5" }, "require-dev": { - "g1a/composer-test-scenarios": "^3", - "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^6", + "php-coveralls/php-coveralls": "^2.4.2", + "phpunit/phpunit": ">=7", "squizlabs/php_codesniffer": "^3", "symfony/var-dumper": "^4", - "symfony/yaml": "^4" + "symfony/yaml": "^4", + "yoast/phpunit-polyfills": "^0.2.0" }, "suggest": { "symfony/var-dumper": "For using the var_dump formatter" }, "type": "library", "extra": { - "scenarios": { - "symfony4": { - "require": { - "symfony/console": "^4.0" - }, - "config": { - "platform": { - "php": "7.1.3" - } - } - } - }, "branch-alias": { - "dev-master": "4.x-dev" + "dev-main": "4.x-dev" } }, "autoload": { @@ -290,11 +240,7 @@ } ], "description": "Format text by applying transformations provided by plug-in formatters.", - "support": { - "issues": "https://github.com/consolidation/output-formatters/issues", - "source": "https://github.com/consolidation/output-formatters/tree/4.1.1" - }, - "time": "2020-05-27T20:51:17+00:00" + "time": "2020-12-12T19:04:59+00:00" }, { "name": "consolidation/robo", @@ -409,10 +355,6 @@ } ], "description": "Modern task runner", - "support": { - "issues": "https://github.com/consolidation/Robo/issues", - "source": "https://github.com/consolidation/Robo/tree/1.4.13" - }, "time": "2020-10-11T04:51:34+00:00" }, { @@ -463,10 +405,6 @@ } ], "description": "Provides a self:update command for Symfony Console applications.", - "support": { - "issues": "https://github.com/consolidation/self-update/issues", - "source": "https://github.com/consolidation/self-update/tree/1.2.0" - }, "time": "2020-04-13T02:49:20+00:00" }, { @@ -498,10 +436,6 @@ ], "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", "homepage": "https://github.com/container-interop/container-interop", - "support": { - "issues": "https://github.com/container-interop/container-interop/issues", - "source": "https://github.com/container-interop/container-interop/tree/master" - }, "abandoned": "psr/container", "time": "2017-02-14T19:40:03+00:00" }, @@ -562,10 +496,6 @@ "dot", "notation" ], - "support": { - "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", - "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/master" - }, "time": "2017-01-20T21:14:22+00:00" }, { @@ -613,10 +543,6 @@ } ], "description": "Expands internal property references in PHP arrays file.", - "support": { - "issues": "https://github.com/grasmash/expander/issues", - "source": "https://github.com/grasmash/expander/tree/master" - }, "time": "2017-12-21T22:14:55+00:00" }, { @@ -665,10 +591,6 @@ } ], "description": "Expands internal property references in a yaml file.", - "support": { - "issues": "https://github.com/grasmash/yaml-expander/issues", - "source": "https://github.com/grasmash/yaml-expander/tree/master" - }, "time": "2017-12-16T16:06:03+00:00" }, { @@ -734,10 +656,6 @@ "provider", "service" ], - "support": { - "issues": "https://github.com/thephpleague/container/issues", - "source": "https://github.com/thephpleague/container/tree/2.x" - }, "time": "2017-05-10T09:20:27+00:00" }, { @@ -855,10 +773,6 @@ } ], "description": "More info available on: http://pear.php.net/package/Console_Getopt", - "support": { - "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Console_Getopt", - "source": "https://github.com/pear/Console_Getopt" - }, "time": "2019-11-20T18:27:48+00:00" }, { @@ -903,10 +817,6 @@ } ], "description": "Minimal set of PEAR core files to be used as composer dependency", - "support": { - "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR", - "source": "https://github.com/pear/pear-core-minimal" - }, "time": "2019-11-19T19:00:24+00:00" }, { @@ -962,10 +872,6 @@ "keywords": [ "exception" ], - "support": { - "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", - "source": "https://github.com/pear/PEAR_Exception" - }, "time": "2019-12-10T10:24:42+00:00" }, { @@ -1015,10 +921,6 @@ "container-interop", "psr" ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/master" - }, "time": "2017-02-14T16:28:37+00:00" }, { @@ -1066,23 +968,20 @@ "psr", "psr-3" ], - "support": { - "source": "https://github.com/php-fig/log/tree/1.1.3" - }, "time": "2020-03-23T09:12:05+00:00" }, { "name": "symfony/console", - "version": "v4.4.16", + "version": "v4.4.18", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5" + "reference": "12e071278e396cc3e1c149857337e9e192deca0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/20f73dd143a5815d475e0838ff867bce1eebd9d5", - "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5", + "url": "https://api.github.com/repos/symfony/console/zipball/12e071278e396cc3e1c149857337e9e192deca0b", + "reference": "12e071278e396cc3e1c149857337e9e192deca0b", "shasum": "" }, "require": { @@ -1141,37 +1040,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/console/tree/v4.4.16" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T11:50:19+00:00" + "time": "2020-12-18T07:41:31+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.4.16", + "version": "v4.4.18", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "4204f13d2d0b7ad09454f221bb2195fccdf1fe98" + "reference": "5d4c874b0eb1c32d40328a09dbc37307a5a910b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4204f13d2d0b7ad09454f221bb2195fccdf1fe98", - "reference": "4204f13d2d0b7ad09454f221bb2195fccdf1fe98", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/5d4c874b0eb1c32d40328a09dbc37307a5a910b0", + "reference": "5d4c874b0eb1c32d40328a09dbc37307a5a910b0", "shasum": "" }, "require": { @@ -1224,24 +1106,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.16" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T11:50:19+00:00" + "time": "2020-12-18T07:41:31+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -1303,37 +1168,20 @@ "interoperability", "standards" ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v1.1.9" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-06T13:19:58+00:00" }, { "name": "symfony/filesystem", - "version": "v4.4.16", + "version": "v4.4.18", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "e74b873395b7213d44d1397bd4a605cd1632a68a" + "reference": "d99fbef7e0f69bf162ae6131b31132fa3cc4bcbe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/e74b873395b7213d44d1397bd4a605cd1632a68a", - "reference": "e74b873395b7213d44d1397bd4a605cd1632a68a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d99fbef7e0f69bf162ae6131b31132fa3cc4bcbe", + "reference": "d99fbef7e0f69bf162ae6131b31132fa3cc4bcbe", "shasum": "" }, "require": { @@ -1365,37 +1213,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v4.4.16" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T11:50:19+00:00" + "time": "2020-11-30T13:04:35+00:00" }, { "name": "symfony/finder", - "version": "v5.1.8", + "version": "v5.2.1", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0" + "reference": "0b9231a5922fd7287ba5b411893c0ecd2733e5ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", - "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", + "url": "https://api.github.com/repos/symfony/finder/zipball/0b9231a5922fd7287ba5b411893c0ecd2733e5ba", + "reference": "0b9231a5922fd7287ba5b411893c0ecd2733e5ba", "shasum": "" }, "require": { @@ -1426,24 +1257,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v5.1.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T12:01:57+00:00" + "time": "2020-12-08T17:02:38+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1505,23 +1319,6 @@ "polyfill", "portable" ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1585,23 +1382,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1664,23 +1444,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1747,23 +1510,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.20.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-23T14:02:19+00:00" }, { @@ -1811,20 +1557,6 @@ "support": { "source": "https://github.com/symfony/process/tree/v3.4.47" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-10-24T10:57:07+00:00" }, { @@ -1887,37 +1619,20 @@ "interoperability", "standards" ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/master" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-09-07T11:33:47+00:00" }, { "name": "symfony/yaml", - "version": "v4.4.16", + "version": "v4.4.18", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "543cb4dbd45ed803f08a9a65f27fb149b5dd20c2" + "reference": "bbce94f14d73732340740366fcbe63363663a403" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/543cb4dbd45ed803f08a9a65f27fb149b5dd20c2", - "reference": "543cb4dbd45ed803f08a9a65f27fb149b5dd20c2", + "url": "https://api.github.com/repos/symfony/yaml/zipball/bbce94f14d73732340740366fcbe63363663a403", + "reference": "bbce94f14d73732340740366fcbe63363663a403", "shasum": "" }, "require": { @@ -1958,24 +1673,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/yaml/tree/v4.4.16" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-10-24T11:50:19+00:00" + "time": "2020-12-08T16:59:59+00:00" } ], "aliases": [], @@ -1984,6 +1682,5 @@ "prefer-stable": false, "prefer-lowest": false, "platform": [], - "platform-dev": [], - "plugin-api-version": "2.0.0" + "platform-dev": [] } From ee0c3c9449e8d9ad489e849f871188d9fa4779de Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 30 Dec 2020 17:01:17 -0500 Subject: [PATCH 093/366] Tests and fixes for user modification --- lib/REST/Miniflux/V1.php | 45 +++++++------- locale/en.php | 3 - tests/cases/REST/Miniflux/TestV1.php | 90 ++++++++++++++++++++++++++-- 3 files changed, 110 insertions(+), 28 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index a532304a..08b69799 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -268,10 +268,12 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $t = gettype($d); if (!isset($body[$k])) { $body[$k] = null; + } elseif ($k === "entry_sorting_direction") { + if (!in_array($body[$k], ["asc", "desc"])) { + return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422); + } } elseif (gettype($body[$k]) !== $t) { return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422); - } elseif ($k === "entry_sorting_direction" && !in_array($body[$k], ["asc", "desc"])) { - return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422); } } return $body; @@ -376,22 +378,23 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($this->listUsers([Arsse::$user->id], false)[0] ?? new \stdClass); } - protected function updateUserByNum(array $data, array $path): ResponseInterface { - try { - if (!$this->isAdmin()) { - // this function is restricted to admins unless the affected user and calling user are the same - if (Arsse::$db->userLookup((int) $path[1]) !== Arsse::$user->id) { - return new ErrorResponse("403", 403); - } elseif ($data['is_admin']) { - // non-admins should not be able to set themselves as admin - return new ErrorResponse("InvalidElevation"); - } - $user = Arsse::$user->id; - } else { - $user = Arsse::$db->userLookup((int) $path[1]); + protected function updateUserByNum(array $path, array $data): ResponseInterface { + // this function is restricted to admins unless the affected user and calling user are the same + $user = Arsse::$user->propertiesGet(Arsse::$user->id, false); + if (((int) $path[1]) === $user['num']) { + if ($data['is_admin'] && !$user['admin']) { + // non-admins should not be able to set themselves as admin + return new ErrorResponse("InvalidElevation", 403); + } + $user = Arsse::$user->id; + } elseif (!$user['admin']) { + return new ErrorResponse("403", 403); + } else { + try { + $user = Arsse::$user->lookup((int) $path[1]); + } catch (ExceptionConflict $e) { + return new ErrorResponse("404", 404); } - } catch (ExceptionConflict $e) { - return new ErrorResponse("404", 404); } // map Miniflux properties to internal metadata properties $in = []; @@ -424,12 +427,12 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { switch ($e->getCode()) { case 10403: return new ErrorResponse(["DuplicateUser", 'user' => $data['username']], 409); - case 20441: - return new ErrorResponse(["InvalidTimeone", 'tz' => $data['timezone']], 422); + case 10441: + return new ErrorResponse(["InvalidInputValue", 'field' => "timezone"], 422); case 10443: - return new ErrorResponse("InvalidPageSize", 422); + return new ErrorResponse(["InvalidInputValue", 'field' => "entries_per_page"], 422); case 10444: - return new ErrorResponse(["InvalidUsername", $e->getMessage()], 422); + return new ErrorResponse(["InvalidInputValue", 'field' => "username"], 422); } throw $e; // @codeCoverageIgnore } diff --git a/locale/en.php b/locale/en.php index b0cfe82d..6944023c 100644 --- a/locale/en.php +++ b/locale/en.php @@ -22,9 +22,6 @@ return [ 'API.Miniflux.Error.InvalidCategory' => 'Invalid category title "{title}"', 'API.Miniflux.Error.InvalidElevation' => 'Only administrators can change permissions of standard users', 'API.Miniflux.Error.DuplicateUser' => 'The user name "{user}" already exists', - 'API.Miniflux.Error.InvalidUser' => '{0}', - 'API.Miniflux.Error.InvalidTimezone' => 'Specified time zone "{tz}" is invalid', - 'API.Miniflux.Error.InvalidPageSize' => 'Page size must be greater than zero', 'API.TTRSS.Category.Uncategorized' => 'Uncategorized', 'API.TTRSS.Category.Special' => 'Special', diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 0b9f68d3..8495d62d 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -16,7 +16,7 @@ use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\REST\Miniflux\V1; use JKingWeb\Arsse\REST\Miniflux\ErrorResponse; use JKingWeb\Arsse\User\ExceptionConflict; -use JKingWeb\Arsse\User\Exception; +use JKingWeb\Arsse\User\ExceptionInput as UserExceptionInput; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\EmptyResponse; @@ -82,13 +82,14 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp(): void { self::clearData(); self::setConf(); - // create a mock user manager; we use a PHPUnitmock because Phake for reasons unknown is unable to mock the User class correctly, sometimes - Arsse::$user = $this->createMock(User::class); - Arsse::$user->method("propertiesGet")->willReturn(['num' => 42, 'admin' => false, 'root_folder_name' => null]); // create a mock database interface Arsse::$db = \Phake::mock(Database::class); $this->transaction = \Phake::mock(Transaction::class); \Phake::when(Arsse::$db)->begin->thenReturn($this->transaction); + // create a mock user manager; we use a PHPUnitmock because Phake for reasons unknown is unable to mock the User class correctly, sometimes + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("propertiesGet")->willReturn(['num' => 42, 'admin' => false, 'root_folder_name' => null]); + Arsse::$user->method("begin")->willReturn($this->transaction); //initialize a handler $this->h = new V1(); } @@ -239,6 +240,87 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ]; } + /** @dataProvider provideUserModifications */ + public function testModifyAUser(bool $admin, string $url, array $body, $in1, $out1, $in2, $out2, $in3, $out3, ResponseInterface $exp): void { + $this->h = $this->createPartialMock(V1::class, ["now"]); + $this->h->method("now")->willReturn(Date::normalize(self::NOW)); + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("begin")->willReturn($this->transaction); + Arsse::$user->method("propertiesGet")->willReturnCallback(function(string $u, bool $includeLarge) use ($admin) { + if ($u === "john.doe@example.com" || $u === "ook") { + return ['num' => 2, 'admin' => $admin]; + } else { + return ['num' => 1, 'admin' => true]; + } + }); + Arsse::$user->method("lookup")->willReturnCallback(function(int $u) { + if ($u === 1) { + return "jane.doe@example.com"; + } elseif ($u === 2) { + return "john.doe@example.com"; + } else { + throw new ExceptionConflict("doesNotExist"); + } + }); + if ($out1 instanceof \Exception) { + Arsse::$user->method("rename")->willThrowException($out1); + } else { + Arsse::$user->method("rename")->willReturn($out1 ?? false); + } + if ($out2 instanceof \Exception) { + Arsse::$user->method("passwordSet")->willThrowException($out2); + } else { + Arsse::$user->method("passwordSet")->willReturn($out2 ?? ""); + } + if ($out3 instanceof \Exception) { + Arsse::$user->method("propertiesSet")->willThrowException($out3); + } else { + Arsse::$user->method("propertiesSet")->willReturn($out3 ?? []); + } + $user = $url === "/users/1" ? "jane.doe@example.com" : "john.doe@example.com"; + if ($in1 === null) { + Arsse::$user->expects($this->exactly(0))->method("rename"); + } else { + Arsse::$user->expects($this->exactly(1))->method("rename")->with($user, $in1); + $user = $in1; + } + if ($in2 === null) { + Arsse::$user->expects($this->exactly(0))->method("passwordSet"); + } else { + Arsse::$user->expects($this->exactly(1))->method("passwordSet")->with($user, $in2); + } + if ($in3 === null) { + Arsse::$user->expects($this->exactly(0))->method("propertiesSet"); + } else { + Arsse::$user->expects($this->exactly(1))->method("propertiesSet")->with($user, $in3); + } + $this->assertMessage($exp, $this->req("PUT", $url, $body)); + } + + public function provideUserModifications(): iterable { + $out1 = ['num' => 2, 'admin' => false]; + $out2 = ['num' => 1, 'admin' => false]; + $resp1 = array_merge($this->users[1], ['username' => "john.doe@example.com"]); + $resp2 = array_merge($this->users[1], ['id' => 1, 'is_admin' => true]); + return [ + [false, "/users/1", ['is_admin' => 0], null, null, null, null, null, null, new ErrorResponse(["InvalidInputType", 'field' => "is_admin", 'expected' => "boolean", 'actual' => "integer"], 422)], + [false, "/users/1", ['entry_sorting_direction' => "bad"], null, null, null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "entry_sorting_direction"], 422)], + [false, "/users/1", ['theme' => "stark"], null, null, null, null, null, null, new ErrorResponse("403", 403)], + [false, "/users/2", ['is_admin' => true], null, null, null, null, null, null, new ErrorResponse("InvalidElevation", 403)], + [false, "/users/2", ['language' => "fr_CA"], null, null, null, null, ['lang' => "fr_CA"], $out1, new Response($resp1)], + [false, "/users/2", ['entry_sorting_direction' => "asc"], null, null, null, null, ['sort_asc' => true], $out1, new Response($resp1)], + [false, "/users/2", ['entry_sorting_direction' => "desc"], null, null, null, null, ['sort_asc' => false], $out1, new Response($resp1)], + [false, "/users/2", ['entries_per_page' => -1], null, null, null, null, ['page_size' => -1], new UserExceptionInput("invalidNonZeroInteger"), new ErrorResponse(["InvalidInputValue", 'field' => "entries_per_page"], 422)], + [false, "/users/2", ['timezone' => "Ook"], null, null, null, null, ['tz' => "Ook"], new UserExceptionInput("invalidTimezone"), new ErrorResponse(["InvalidInputValue", 'field' => "timezone"], 422)], + [false, "/users/2", ['username' => "j:k"], "j:k", new UserExceptionInput("invalidUsername"), null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "username"], 422)], + [false, "/users/2", ['username' => "ook"], "ook", new ExceptionConflict("alreadyExists"), null, null, null, null, new ErrorResponse(["DuplicateUser", 'user' => "ook"], 409)], + [false, "/users/2", ['password' => "ook"], null, null, "ook", "ook", null, null, new Response(array_merge($resp1, ['password' => "ook"]))], + [false, "/users/2", ['username' => "ook", 'password' => "ook"], "ook", true, "ook", "ook", null, null, new Response(array_merge($resp1, ['username' => "ook", 'password' => "ook"]))], + [true, "/users/1", ['theme' => "stark"], null, null, null, null, ['theme' => "stark"], $out2, new Response($resp2)], + [true, "/users/3", ['theme' => "stark"], null, null, null, null, null, null, new ErrorResponse("404", 404)], + ]; + } + public function testListCategories(): void { \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ ['id' => 1, 'name' => "Science"], From 197922f92fe7f718283f8818874fa08ed4d7600d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 31 Dec 2020 13:57:36 -0500 Subject: [PATCH 094/366] Implement Miniflux user creation --- lib/REST/Miniflux/V1.php | 75 ++++++++++++++++++++-------- locale/en.php | 1 + tests/cases/REST/Miniflux/TestV1.php | 55 ++++++++++++++++++++ 3 files changed, 111 insertions(+), 20 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 08b69799..c020b8a3 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -333,6 +333,33 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return $out; } + protected function editUser(string $user, array $data): array { + // map Miniflux properties to internal metadata properties + $in = []; + foreach (self::USER_META_MAP as $i => [$o,,]) { + if (isset($data[$i])) { + if ($i === "entry_sorting_direction") { + $in[$o] = $data[$i] === "asc"; + } else { + $in[$o] = $data[$i]; + } + } + } + // make any requested changes + $tr = Arsse::$user->begin(); + if ($in) { + Arsse::$user->propertiesSet($user, $in); + } + // read out the newly-modified user and commit the changes + $out = $this->listUsers([$user], true)[0]; + $tr->commit(); + // add the input password if a password change was requested + if (isset($data['password'])) { + $out['password'] = $data['password']; + } + return $out; + } + protected function discoverSubscriptions(array $data): ResponseInterface { try { $list = Feed::discoverAll((string) $data['url'], (string) $data['username'], (string) $data['password']); @@ -378,6 +405,33 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($this->listUsers([Arsse::$user->id], false)[0] ?? new \stdClass); } + protected function createUser(array $data): ResponseInterface { + if ($data['username'] === null) { + return new ErrorResponse(["MissingInputValue", 'field' => "username"], 422); + } elseif ($data['password'] === null) { + return new ErrorResponse(["MissingInputValue", 'field' => "password"], 422); + } + try { + $tr = Arsse::$user->begin(); + $data['password'] = Arsse::$user->add($data['username'], $data['password']); + $out = $this->editUser($data['username'], $data); + $tr->commit(); + } catch (UserException $e) { + switch ($e->getCode()) { + case 10403: + return new ErrorResponse(["DuplicateUser", 'user' => $data['username']], 409); + case 10441: + return new ErrorResponse(["InvalidInputValue", 'field' => "timezone"], 422); + case 10443: + return new ErrorResponse(["InvalidInputValue", 'field' => "entries_per_page"], 422); + case 10444: + return new ErrorResponse(["InvalidInputValue", 'field' => "username"], 422); + } + throw $e; // @codeCoverageIgnore + } + return new Response($out, 201); + } + protected function updateUserByNum(array $path, array $data): ResponseInterface { // this function is restricted to admins unless the affected user and calling user are the same $user = Arsse::$user->propertiesGet(Arsse::$user->id, false); @@ -396,17 +450,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("404", 404); } } - // map Miniflux properties to internal metadata properties - $in = []; - foreach (self::USER_META_MAP as $i => [$o,,]) { - if (isset($data[$i])) { - if ($i === "entry_sorting_direction") { - $in[$o] = $data[$i] === "asc"; - } else { - $in[$o] = $data[$i]; - } - } - } // make any requested changes try { $tr = Arsse::$user->begin(); @@ -417,11 +460,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if (isset($data['password'])) { Arsse::$user->passwordSet($user, $data['password']); } - if ($in) { - Arsse::$user->propertiesSet($user, $in); - } - // read out the newly-modified user and commit the changes - $out = $this->listUsers([$user], true)[0]; + $out = $this->editUser($user, $data); $tr->commit(); } catch (UserException $e) { switch ($e->getCode()) { @@ -436,10 +475,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } throw $e; // @codeCoverageIgnore } - // add the input password if a password change was requested - if (isset($data['password'])) { - $out['password'] = $data['password']; - } return new Response($out); } diff --git a/locale/en.php b/locale/en.php index 6944023c..d67547aa 100644 --- a/locale/en.php +++ b/locale/en.php @@ -11,6 +11,7 @@ return [ 'API.Miniflux.Error.401' => 'Access Unauthorized', 'API.Miniflux.Error.403' => 'Access Forbidden', 'API.Miniflux.Error.404' => 'Resource Not Found', + 'API.Miniflux.Error.MissingInputValue' => 'Required key "{field}" was not present in input', 'API.Miniflux.Error.InvalidBodyJSON' => 'Invalid JSON payload: {0}', 'API.Miniflux.Error.InvalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}', 'API.Miniflux.Error.InvalidInputValue' => 'Supplied value is not valid for input key "{field}"', diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 8495d62d..ce962727 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -321,6 +321,61 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ]; } + /** @dataProvider provideUserAdditions */ + public function testAddAUser(array $body, $in1, $out1, $in2, $out2, ResponseInterface $exp): void { + $this->h = $this->createPartialMock(V1::class, ["now"]); + $this->h->method("now")->willReturn(Date::normalize(self::NOW)); + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("begin")->willReturn($this->transaction); + Arsse::$user->method("propertiesGet")->willReturnCallback(function(string $u, bool $includeLarge) { + if ($u === "john.doe@example.com") { + return ['num' => 1, 'admin' => true]; + } else { + return ['num' => 2, 'admin' => false]; + } + }); + if ($out1 instanceof \Exception) { + Arsse::$user->method("add")->willThrowException($out1); + } else { + Arsse::$user->method("add")->willReturn($in1[1] ?? ""); + } + if ($out2 instanceof \Exception) { + Arsse::$user->method("propertiesSet")->willThrowException($out2); + } else { + Arsse::$user->method("propertiesSet")->willReturn($out2 ?? []); + } + if ($in1 === null) { + Arsse::$user->expects($this->exactly(0))->method("add"); + } else { + Arsse::$user->expects($this->exactly(1))->method("add")->with(...($in1 ?? [])); + } + if ($in2 === null) { + Arsse::$user->expects($this->exactly(0))->method("propertiesSet"); + } else { + Arsse::$user->expects($this->exactly(1))->method("propertiesSet")->with($body['username'], $in2); + } + $this->assertMessage($exp, $this->req("POST", "/users", $body)); + } + + public function provideUserAdditions(): iterable { + $resp1 = array_merge($this->users[1], ['username' => "ook", 'password' => "eek"]); + return [ + [[], null, null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "username"], 422)], + [['username' => "ook"], null, null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "password"], 422)], + [['username' => "ook", 'password' => "eek"], ["ook", "eek"], new ExceptionConflict("alreadyExists"), null, null, new ErrorResponse(["DuplicateUser", 'user' => "ook"], 409)], + [['username' => "j:k", 'password' => "eek"], ["j:k", "eek"], new UserExceptionInput("invalidUsername"), null, null, new ErrorResponse(["InvalidInputValue", 'field' => "username"], 422)], + [['username' => "ook", 'password' => "eek", 'timezone' => "ook"], ["ook", "eek"], "eek", ['tz' => "ook"], new UserExceptionInput("invalidTimezone"), new ErrorResponse(["InvalidInputValue", 'field' => "timezone"], 422)], + [['username' => "ook", 'password' => "eek", 'entries_per_page' => -1], ["ook", "eek"], "eek", ['page_size' => -1], new UserExceptionInput("invalidNonZeroInteger"), new ErrorResponse(["InvalidInputValue", 'field' => "entries_per_page"], 422)], + [['username' => "ook", 'password' => "eek", 'theme' => "default"], ["ook", "eek"], "eek", ['theme' => "default"], ['theme' => "default"], new Response($resp1, 201)], + ]; + } + + public function testAddAUserWithoutAuthority(): void { + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("propertiesGet")->willReturn(['num' => 1, 'admin' => false]); + $this->assertMessage(new ErrorResponse("403", 403), $this->req("POST", "/users", [])); + } + public function testListCategories(): void { \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ ['id' => 1, 'name' => "Science"], From bf95b134bd020a9e8043848a1aa0b94586ae28a3 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 31 Dec 2020 15:46:47 -0500 Subject: [PATCH 095/366] Fix up error codes for category changes --- lib/REST/Miniflux/V1.php | 90 ++++++++++++++-------------- tests/cases/REST/Miniflux/TestV1.php | 21 ++++--- 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index c020b8a3..94cdab0e 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -50,81 +50,81 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'entry_swipe' => ["swipe", true, false], 'custom_css' => ["stylesheet", "", true], ]; - protected const CALLS = [ // handler method Admin Path Body Query + protected const CALLS = [ // handler method Admin Path Body Query Required fields '/categories' => [ - 'GET' => ["getCategories", false, false, false, false], - 'POST' => ["createCategory", false, false, true, false], + 'GET' => ["getCategories", false, false, false, false, []], + 'POST' => ["createCategory", false, false, true, false, ["title"]], ], '/categories/1' => [ - 'PUT' => ["updateCategory", false, true, true, false], - 'DELETE' => ["deleteCategory", false, true, false, false], + 'PUT' => ["updateCategory", false, true, true, false, ["title"]], // title is effectively required since no other field can be changed + 'DELETE' => ["deleteCategory", false, true, false, false, []], ], '/categories/1/mark-all-as-read' => [ - 'PUT' => ["markCategory", false, true, false, false], + 'PUT' => ["markCategory", false, true, false, false, []], ], '/discover' => [ - 'POST' => ["discoverSubscriptions", false, false, true, false], + 'POST' => ["discoverSubscriptions", false, false, true, false, ["url"]], ], '/entries' => [ - 'GET' => ["getEntries", false, false, false, true], - 'PUT' => ["updateEntries", false, false, true, false], + 'GET' => ["getEntries", false, false, false, true, []], + 'PUT' => ["updateEntries", false, false, true, false, []], ], '/entries/1' => [ - 'GET' => ["getEntry", false, true, false, false], + 'GET' => ["getEntry", false, true, false, false, []], ], '/entries/1/bookmark' => [ - 'PUT' => ["toggleEntryBookmark", false, true, false, false], + 'PUT' => ["toggleEntryBookmark", false, true, false, false, []], ], '/export' => [ - 'GET' => ["opmlExport", false, false, false, false], + 'GET' => ["opmlExport", false, false, false, false, []], ], '/feeds' => [ - 'GET' => ["getFeeds", false, false, false, false], - 'POST' => ["createFeed", false, false, true, false], + 'GET' => ["getFeeds", false, false, false, false, []], + 'POST' => ["createFeed", false, false, true, false, []], ], '/feeds/1' => [ - 'GET' => ["getFeed", false, true, false, false], - 'PUT' => ["updateFeed", false, true, true, false], - 'DELETE' => ["deleteFeed", false, true, false, false], + 'GET' => ["getFeed", false, true, false, false, []], + 'PUT' => ["updateFeed", false, true, true, false, []], + 'DELETE' => ["deleteFeed", false, true, false, false, []], ], '/feeds/1/entries' => [ - 'GET' => ["getFeedEntries", false, true, false, false], + 'GET' => ["getFeedEntries", false, true, false, false, []], ], '/feeds/1/entries/1' => [ - 'GET' => ["getFeedEntry", false, true, false, false], + 'GET' => ["getFeedEntry", false, true, false, false, []], ], '/feeds/1/icon' => [ - 'GET' => ["getFeedIcon", false, true, false, false], + 'GET' => ["getFeedIcon", false, true, false, false, []], ], '/feeds/1/mark-all-as-read' => [ - 'PUT' => ["markFeed", false, true, false, false], + 'PUT' => ["markFeed", false, true, false, false, []], ], '/feeds/1/refresh' => [ - 'PUT' => ["refreshFeed", false, true, false, false], + 'PUT' => ["refreshFeed", false, true, false, false, []], ], '/feeds/refresh' => [ - 'PUT' => ["refreshAllFeeds", false, false, false, false], + 'PUT' => ["refreshAllFeeds", false, false, false, false, []], ], '/import' => [ - 'POST' => ["opmlImport", false, false, true, false], + 'POST' => ["opmlImport", false, false, true, false, []], ], '/me' => [ - 'GET' => ["getCurrentUser", false, false, false, false], + 'GET' => ["getCurrentUser", false, false, false, false, []], ], '/users' => [ - 'GET' => ["getUsers", true, false, false, false], - 'POST' => ["createUser", true, false, true, false], + 'GET' => ["getUsers", true, false, false, false, []], + 'POST' => ["createUser", true, false, true, false, ["username", "password"]], ], '/users/1' => [ - 'GET' => ["getUserByNum", true, true, false, false], - 'PUT' => ["updateUserByNum", false, true, true, false], // requires admin for users other than self - 'DELETE' => ["deleteUserByNum", true, true, false, false], + 'GET' => ["getUserByNum", true, true, false, false, []], + 'PUT' => ["updateUserByNum", false, true, true, false, []], // requires admin for users other than self + 'DELETE' => ["deleteUserByNum", true, true, false, false, []], ], '/users/1/mark-all-as-read' => [ - 'PUT' => ["markUserByNum", false, true, false, false], + 'PUT' => ["markUserByNum", false, true, false, false, []], ], '/users/*' => [ - 'GET' => ["getUserById", true, true, false, false], + 'GET' => ["getUserById", true, true, false, false, []], ], ]; @@ -169,7 +169,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if ($func instanceof ResponseInterface) { return $func; } else { - [$func, $reqAdmin, $reqPath, $reqBody, $reqQuery] = $func; + [$func, $reqAdmin, $reqPath, $reqBody, $reqQuery, $reqFields] = $func; } if ($reqAdmin && !$this->isAdmin()) { return new ErrorResponse("403", 403); @@ -195,7 +195,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } else { $data = []; } - $data = $this->normalizeBody((array) $data); + $data = $this->normalizeBody((array) $data, $reqFields); if ($data instanceof ResponseInterface) { return $data; } @@ -255,7 +255,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return implode("/", $path); } - protected function normalizeBody(array $body) { + protected function normalizeBody(array $body, array $req) { // Miniflux does not attempt to coerce values into different types foreach (self::VALID_JSON as $k => $t) { if (!isset($body[$k])) { @@ -264,6 +264,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422); } } + //normalize user-specific input foreach (self::USER_META_MAP as $k => [,$d,]) { $t = gettype($d); if (!isset($body[$k])) { @@ -276,6 +277,12 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422); } } + // check for any missing required values + foreach ($req as $k) { + if (!isset($body[$k])) { + return new ErrorResponse(["MissingInputValue", 'field' => $k], 422); + } + } return $body; } @@ -406,11 +413,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function createUser(array $data): ResponseInterface { - if ($data['username'] === null) { - return new ErrorResponse(["MissingInputValue", 'field' => "username"], 422); - } elseif ($data['password'] === null) { - return new ErrorResponse(["MissingInputValue", 'field' => "password"], 422); - } try { $tr = Arsse::$user->begin(); $data['password'] = Arsse::$user->add($data['username'], $data['password']); @@ -496,9 +498,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $id = Arsse::$db->folderAdd(Arsse::$user->id, ['name' => (string) $data['title']]); } catch (ExceptionInput $e) { if ($e->getCode() === 10236) { - return new ErrorResponse(["DuplicateCategory", 'title' => $data['title']], 500); + return new ErrorResponse(["DuplicateCategory", 'title' => $data['title']], 409); } else { - return new ErrorResponse(["InvalidCategory", 'title' => $data['title']], 500); + return new ErrorResponse(["InvalidCategory", 'title' => $data['title']], 422); } } $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); @@ -521,11 +523,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } catch (ExceptionInput $e) { if ($e->getCode() === 10236) { - return new ErrorResponse(["DuplicateCategory", 'title' => $title], 500); + return new ErrorResponse(["DuplicateCategory", 'title' => $title], 409); } elseif (in_array($e->getCode(), [10237, 10239])) { return new ErrorResponse("404", 404); } else { - return new ErrorResponse(["InvalidCategory", 'title' => $title], 500); + return new ErrorResponse(["InvalidCategory", 'title' => $title], 422); } } $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index ce962727..94fad0d8 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -416,9 +416,10 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function provideCategoryAdditions(): iterable { return [ ["New", new Response(['id' => 2112, 'title' => "New", 'user_id' => 42], 201)], - ["Duplicate", new ErrorResponse(["DuplicateCategory", 'title' => "Duplicate"], 500)], - ["", new ErrorResponse(["InvalidCategory", 'title' => ""], 500)], - [" ", new ErrorResponse(["InvalidCategory", 'title' => " "], 500)], + ["Duplicate", new ErrorResponse(["DuplicateCategory", 'title' => "Duplicate"], 409)], + ["", new ErrorResponse(["InvalidCategory", 'title' => ""], 422)], + [" ", new ErrorResponse(["InvalidCategory", 'title' => " "], 422)], + [null, new ErrorResponse(["MissingInputValue", 'field' => "title"], 422)], [false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"],422)], ]; } @@ -442,14 +443,16 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { return [ [3, "New", "subjectMissing", new ErrorResponse("404", 404)], [2, "New", true, new Response(['id' => 2, 'title' => "New", 'user_id' => 42])], - [2, "Duplicate", "constraintViolation", new ErrorResponse(["DuplicateCategory", 'title' => "Duplicate"], 500)], - [2, "", "missing", new ErrorResponse(["InvalidCategory", 'title' => ""], 500)], - [2, " ", "whitespace", new ErrorResponse(["InvalidCategory", 'title' => " "], 500)], - [2, false, "subjectMissing", new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"],422)], + [2, "Duplicate", "constraintViolation", new ErrorResponse(["DuplicateCategory", 'title' => "Duplicate"], 409)], + [2, "", "missing", new ErrorResponse(["InvalidCategory", 'title' => ""], 422)], + [2, " ", "whitespace", new ErrorResponse(["InvalidCategory", 'title' => " "], 422)], + [2, null, "missing", new ErrorResponse(["MissingInputValue", 'field' => "title"], 422)], + [2, false, "subjectMissing", new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 422)], [1, "New", true, new Response(['id' => 1, 'title' => "New", 'user_id' => 42])], [1, "Duplicate", "constraintViolation", new Response(['id' => 1, 'title' => "Duplicate", 'user_id' => 42])], // This is allowed because the name of the root folder is only a duplicate in circumstances where it is used - [1, "", "missing", new ErrorResponse(["InvalidCategory", 'title' => ""], 500)], - [1, " ", "whitespace", new ErrorResponse(["InvalidCategory", 'title' => " "], 500)], + [1, "", "missing", new ErrorResponse(["InvalidCategory", 'title' => ""], 422)], + [1, " ", "whitespace", new ErrorResponse(["InvalidCategory", 'title' => " "], 422)], + [1, null, "missing", new ErrorResponse(["MissingInputValue", 'field' => "title"], 422)], [1, false, false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 422)], ]; } From 31f0539dc0b4bc2588802f983531d95794b47474 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 31 Dec 2020 17:03:08 -0500 Subject: [PATCH 096/366] Implement Miniflux user deletion --- lib/REST/Miniflux/V1.php | 9 +++++++++ tests/cases/REST/Miniflux/TestV1.php | 28 ++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 94cdab0e..12f509a3 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -480,6 +480,15 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($out); } + protected function deleteUserByNum(array $path): ResponseInterface { + try { + Arsse::$user->remove(Arsse::$user->lookup((int) $path[1])); + } catch (ExceptionConflict $e) { + return new ErrorResponse("404", 404); + } + return new EmptyResponse(204); + } + protected function getCategories(): ResponseInterface { $out = []; $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 94fad0d8..3851ab32 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -371,11 +371,35 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } public function testAddAUserWithoutAuthority(): void { - Arsse::$user = $this->createMock(User::class); - Arsse::$user->method("propertiesGet")->willReturn(['num' => 1, 'admin' => false]); $this->assertMessage(new ErrorResponse("403", 403), $this->req("POST", "/users", [])); } + public function testDeleteAUser(): void { + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("propertiesGet")->willReturn(['admin' => true]); + Arsse::$user->method("lookup")->willReturn("john.doe@example.com"); + Arsse::$user->method("remove")->willReturn(true); + Arsse::$user->expects($this->exactly(1))->method("lookup")->with(2112); + Arsse::$user->expects($this->exactly(1))->method("remove")->with("john.doe@example.com"); + $this->assertMessage(new EmptyResponse(204), $this->req("DELETE", "/users/2112")); + } + + public function testDeleteAMissingUser(): void { + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("propertiesGet")->willReturn(['admin' => true]); + Arsse::$user->method("lookup")->willThrowException(new ExceptionConflict("doesNotExist")); + Arsse::$user->method("remove")->willReturn(true); + Arsse::$user->expects($this->exactly(1))->method("lookup")->with(2112); + Arsse::$user->expects($this->exactly(0))->method("remove"); + $this->assertMessage(new ErrorResponse("404", 404), $this->req("DELETE", "/users/2112")); + } + + public function testDeleteAUserWithoutAuthority(): void { + Arsse::$user->expects($this->exactly(0))->method("lookup"); + Arsse::$user->expects($this->exactly(0))->method("remove"); + $this->assertMessage(new ErrorResponse("403", 403), $this->req("DELETE", "/users/2112")); + } + public function testListCategories(): void { \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ ['id' => 1, 'name' => "Science"], From 7e173327149d86443969940481c8e13285bc245c Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 31 Dec 2020 17:50:40 -0500 Subject: [PATCH 097/366] Implement marking all as read for Miniflux --- lib/REST/Miniflux/V1.php | 10 ++++++++++ tests/cases/REST/Miniflux/TestV1.php | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 12f509a3..9ad2882c 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -489,6 +489,16 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } + protected function markUserByNum(array $path): ResponseInterface { + // this function is restricted to the logged-in user + $user = Arsse::$user->propertiesGet(Arsse::$user->id, false); + if (((int) $path[1]) !== $user['num']) { + return new ErrorResponse("403", 403); + } + Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], (new Context)->hidden(false)); + return new EmptyResponse(204); + } + protected function getCategories(): ResponseInterface { $out = []; $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 3851ab32..255d9a55 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -400,6 +400,13 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertMessage(new ErrorResponse("403", 403), $this->req("DELETE", "/users/2112")); } + public function testMarkAllArticlesAsRead(): void { + \Phake::when(Arsse::$db)->articleMark->thenReturn(true); + $this->assertMessage(new ErrorResponse("403", 403), $this->req("PUT", "/users/1/mark-all-as-read")); + $this->assertMessage(new EmptyResponse(204), $this->req("PUT", "/users/42/mark-all-as-read")); + \Phake::verify(Arsse::$db)->articleMark("john.doe@example.com", ['read' => true], (new Context)->hidden(false)); + } + public function testListCategories(): void { \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ ['id' => 1, 'name' => "Science"], From ffc5579a7a874219cb57f09cc7ae75b2c95d0c44 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 3 Jan 2021 16:41:15 -0500 Subject: [PATCH 098/366] Partial implementation of filter rule handling --- lib/AbstractException.php | 1 + lib/Feed.php | 35 ++++++++++++++++++++++++----------- lib/Rule/Exception.php | 10 ++++++++++ lib/Rule/Rule.php | 31 +++++++++++++++++++++++++++++++ locale/en.php | 1 + tests/cases/Misc/TestRule.php | 22 ++++++++++++++++++++++ tests/phpunit.dist.xml | 1 + 7 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 lib/Rule/Exception.php create mode 100644 lib/Rule/Rule.php create mode 100644 tests/cases/Misc/TestRule.php diff --git a/lib/AbstractException.php b/lib/AbstractException.php index 73a17076..5a575c34 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -100,6 +100,7 @@ abstract class AbstractException extends \Exception { "ImportExport/Exception.invalidFolderName" => 10613, "ImportExport/Exception.invalidFolderCopy" => 10614, "ImportExport/Exception.invalidTagName" => 10615, + "Rule/Exception.invalidPattern" => 10701, ]; public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) { diff --git a/lib/Feed.php b/lib/Feed.php index 81256a63..da01d2f0 100644 --- a/lib/Feed.php +++ b/lib/Feed.php @@ -79,10 +79,14 @@ class Feed { // we only really care if articles have been modified; if there are no new articles, act as if the feed is unchanged if (!sizeof($this->newItems) && !sizeof($this->changedItems)) { $this->modified = false; - } - // if requested, scrape full content for any new and changed items - if ($scrape) { - $this->scrape(); + } else { + if ($feedID) { + $this->computeFilterRules($feedID); + } + // if requested, scrape full content for any new and changed items + if ($scrape) { + $this->scrape(); + } } } // compute the time at which the feed should next be fetched @@ -119,7 +123,7 @@ class Feed { } } - protected function parse(): bool { + protected function parse(): void { try { $feed = $this->resource->reader->getParser( $this->resource->getUrl(), @@ -222,7 +226,6 @@ class Feed { sort($f->categories); } $this->data = $feed; - return true; } protected function deduplicateItems(array $items): array { @@ -269,13 +272,13 @@ class Feed { return $out; } - protected function matchToDatabase(int $feedID = null): bool { + protected function matchToDatabase(int $feedID = null): void { // first perform deduplication on items $items = $this->deduplicateItems($this->data->items); // if we haven't been given a database feed ID to check against, all items are new if (is_null($feedID)) { $this->newItems = $items; - return true; + return; } // get as many of the latest articles in the database as there are in the feed $articles = Arsse::$db->feedMatchLatest($feedID, sizeof($items))->getAll(); @@ -303,7 +306,6 @@ class Feed { // merge the two change-lists, preserving keys $this->changedItems = array_combine(array_merge(array_keys($this->changedItems), array_keys($changed)), array_merge($this->changedItems, $changed)); } - return true; } protected function matchItems(array $items, array $articles): array { @@ -438,7 +440,7 @@ class Feed { return $dates; } - protected function scrape(): bool { + protected function scrape(): void { $scraper = new Scraper(self::configure()); foreach (array_merge($this->newItems, $this->changedItems) as $item) { $scraper->setUrl($item->url); @@ -447,6 +449,17 @@ class Feed { $item->content = $scraper->getFilteredContent(); } } - return true; + } + + protected function computeFilterRules(int $feedID): void { + return; + $rules = Arsse::$db->feedRulesGet($feedID); + foreach ($rules as $r) { + $keep = ""; + $block = ""; + if (strlen($r['keep'])) { + + } + } } } diff --git a/lib/Rule/Exception.php b/lib/Rule/Exception.php new file mode 100644 index 00000000..e3c66645 --- /dev/null +++ b/lib/Rule/Exception.php @@ -0,0 +1,10 @@ +", $pattern, $m, \PREG_OFFSET_CAPTURE)) { + // where necessary escape our chosen delimiter (backtick) in reverse order + foreach (array_reverse($m[0]) as [,$pos]) { + // count the number of backslashes preceding the delimiter character + $count = 0; + $p = $pos; + while ($p-- && $pattern[$p] === "\\" && ++$count); + // if the number is even (including zero), add a backslash + if ($count % 2 === 0) { + $pattern = substr($pattern, 0, $pos)."\\".substr($pattern, $pos); + } + } + } + // add the delimiters and test the pattern + $pattern = "`$pattern`u"; + if (@preg_match($pattern, "") === false) { + throw new Exception("invalidPattern"); + } + return $pattern; + } +} \ No newline at end of file diff --git a/locale/en.php b/locale/en.php index d67547aa..1927eaf7 100644 --- a/locale/en.php +++ b/locale/en.php @@ -194,4 +194,5 @@ return [ 'Exception.JKingWeb/Arsse/ImportExport/Exception.invalidFolderName' => 'Input data contains an invalid folder name', 'Exception.JKingWeb/Arsse/ImportExport/Exception.invalidFolderCopy' => 'Input data contains multiple folders of the same name under the same parent', 'Exception.JKingWeb/Arsse/ImportExport/Exception.invalidTagName' => 'Input data contains an invalid tag name', + 'Exception.JKingWeb/Arsse/Rule/Exception.invalidPattern' => 'Specified rule pattern is invalid' ]; diff --git a/tests/cases/Misc/TestRule.php b/tests/cases/Misc/TestRule.php new file mode 100644 index 00000000..804b1724 --- /dev/null +++ b/tests/cases/Misc/TestRule.php @@ -0,0 +1,22 @@ +assertSame($exp, Rule::prep("`..`..\\`..\\\\`..")); + } + + public function testPrepareAnInvalidPattern(): void { + $this->assertException("invalidPattern", "Rule"); + Rule::prep("["); + } +} \ No newline at end of file diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml index 18486652..0875bf54 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -51,6 +51,7 @@ cases/Misc/TestContext.php cases/Misc/TestURL.php cases/Misc/TestHTTP.php + cases/Misc/TestRule.php cases/User/TestInternal.php From b12f87e231fa2c318bd9fb75cdb9fc1885195f42 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 3 Jan 2021 16:51:25 -0500 Subject: [PATCH 099/366] Support Xdebug 3.x for coverage --- RoboFile.php | 4 ++-- tests/bootstrap.php | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/RoboFile.php b/RoboFile.php index 31b98924..0f1a349d 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -96,11 +96,11 @@ class RoboFile extends \Robo\Tasks { if (extension_loaded("pcov")) { return "$php -d pcov.enabled=1 -d pcov.directory=$code"; } elseif (extension_loaded("xdebug")) { - return $php; + return "$php -d xdebug.mode=coverage"; } elseif (file_exists($dir."pcov.$ext")) { return "$php -d extension=pcov.$ext -d pcov.enabled=1 -d pcov.directory=$code"; } elseif (file_exists($dir."xdebug.$ext")) { - return "$php -d zend_extension=xdebug.$ext"; + return "$php -d zend_extension=xdebug.$ext -d xdebug.mode=coverage"; } else { if (IS_WIN) { $dbg = dirname(\PHP_BINARY)."\\phpdbg.exe"; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 2e4c2514..c8f36508 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -16,5 +16,9 @@ error_reporting(\E_ALL); require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php"; if (function_exists("xdebug_set_filter")) { - xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, \XDEBUG_PATH_WHITELIST, [BASE."lib/"]); + if (defined("XDEBUG_PATH_INCLUDE")) { + xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, XDEBUG_PATH_INCLUDE, [BASE."lib/"]); + } else { + xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, XDEBUG_PATH_WHITELIST, [BASE."lib/"]); + } } From 47ae65b9d368bfcb6585c6ce1fa828baebd05d17 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 3 Jan 2021 22:15:39 -0500 Subject: [PATCH 100/366] Function to apply filter rules --- lib/Rule/Rule.php | 59 ++++++++++++++++++++++++++++++++++- tests/cases/Misc/TestRule.php | 30 +++++++++++++++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/lib/Rule/Rule.php b/lib/Rule/Rule.php index 90b0bbec..5f387f1d 100644 --- a/lib/Rule/Rule.php +++ b/lib/Rule/Rule.php @@ -28,4 +28,61 @@ abstract class Rule { } return $pattern; } -} \ No newline at end of file + + public static function validate(string $pattern): bool { + try { + static::prep($pattern); + } catch (Exception $e) { + return false; + } + return true; + } + + /** applies keep and block rules against the title and categories of an article + * + * Returns true if the article is to be kept, and false if it is to be suppressed + */ + public static function apply(string $keepRule, string $blockRule, string $title, array $categories = []): bool { + // if neither rule is processed we should keep + $keep = true; + // add the title to the front of the category array + array_unshift($categories, $title); + // process the keep rule if it exists + if (strlen($keepRule)) { + try { + $rule = static::prep($keepRule); + } catch (Exception $e) { + return true; + } + // if a keep rule is specified the default state is now not to keep + $keep = false; + foreach ($categories as $str) { + if (is_string($str)) { + if (preg_match($rule, $str)) { + // keep if the keep-rule matches one of the strings + $keep = true; + break; + } + } + } + } + // process the block rule if the keep rule was matched + if ($keep && strlen($blockRule)) { + try { + $rule = static::prep($blockRule); + } catch (Exception $e) { + return true; + } + foreach ($categories as $str) { + if (is_string($str)) { + if (preg_match($rule, $str)) { + // do not keep if the block-rule matches one of the strings + $keep = false; + break; + } + } + } + } + return $keep; + } +} diff --git a/tests/cases/Misc/TestRule.php b/tests/cases/Misc/TestRule.php index 804b1724..9df68380 100644 --- a/tests/cases/Misc/TestRule.php +++ b/tests/cases/Misc/TestRule.php @@ -7,16 +7,44 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Misc; use JKingWeb\Arsse\Rule\Rule; +use JKingWeb\Arsse\Rule\Exception; /** @covers \JKingWeb\Arsse\Rule\Rule */ class TestRule extends \JKingWeb\Arsse\Test\AbstractTest { public function testPrepareAPattern(): void { $exp = "`\\`..\\`..\\`..\\\\\\`..`u"; + $this->assertTrue(Rule::validate("`..`..\\`..\\\\`..")); $this->assertSame($exp, Rule::prep("`..`..\\`..\\\\`..")); } public function testPrepareAnInvalidPattern(): void { + $this->assertFalse(Rule::validate("[")); $this->assertException("invalidPattern", "Rule"); Rule::prep("["); } -} \ No newline at end of file + + /** @dataProvider provideApplications */ + public function testApplyRules(string $keepRule, string $blockRule, string $title, array $categories, $exp): void { + if ($exp instanceof \Exception) { + $this->assertException($exp); + Rule::apply($keepRule, $blockRule, $title, $categories); + } else { + $this->assertSame($exp, Rule::apply($keepRule, $blockRule, $title, $categories)); + } + } + + public function provideApplications(): iterable { + return [ + ["", "", "Title", ["Dummy", "Category"], true], + ["^Title$", "", "Title", ["Dummy", "Category"], true], + ["^Category$", "", "Title", ["Dummy", "Category"], true], + ["^Naught$", "", "Title", ["Dummy", "Category"], false], + ["", "^Title$", "Title", ["Dummy", "Category"], false], + ["", "^Category$", "Title", ["Dummy", "Category"], false], + ["", "^Naught$", "Title", ["Dummy", "Category"], true], + ["^Category$", "^Category$", "Title", ["Dummy", "Category"], false], + ["[", "", "Title", ["Dummy", "Category"], true], + ["", "[", "Title", ["Dummy", "Category"], true], + ]; + } +} From 461e25605273167c7a355c6f67575f14f7e42a55 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 7 Jan 2021 10:12:38 -0500 Subject: [PATCH 101/366] Work around MySQL syntax weirdness Also improve test for token translation to actually test that the translated tokens are accepted by the database system --- lib/Database.php | 16 ++++++++++------ lib/Db/Driver.php | 1 + lib/Db/MySQL/Driver.php | 2 ++ tests/cases/Db/BaseDriver.php | 21 ++++++++++++++------- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 95ccc611..343e2cec 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -762,6 +762,7 @@ class Database { // validate inputs $folder = $this->folderValidateId($user, $folder)['id']; // create a complex query + $integer = $this->db->sqlToken("integer"); $q = new Query( "SELECT s.id as id, @@ -789,7 +790,7 @@ class Database { select subscription, sum(hidden) as hidden, - sum(cast((\"read\" = 1 and hidden = 0) as integer)) as marked + sum(cast((\"read\" = 1 and hidden = 0) as $integer)) as marked from arsse_marks group by subscription ) as mark_stats on mark_stats.subscription = s.id" ); @@ -1211,7 +1212,7 @@ class Database { * - "block": The block rule; any article which matches this rule are hidden */ public function feedRulesGet(int $feedID): Db\Result { - return $this->db->prepare("SELECT owner, coalesce(keep_rule, '') as keep, coalesce(block_rule, '') as block from arsse_subscriptions where feed = ? and (keep || block) <> '' order by owner", "int")->run($feedID); + return $this->db->prepare("SELECT owner, coalesce(keep_rule, '') as keep, coalesce(block_rule, '') as block from arsse_subscriptions where feed = ? and (coalesce(keep_rule, '') || coalesce(block_rule, '')) <> '' order by owner", "int")->run($feedID); } /** Retrieves various identifiers for the latest $count articles in the given newsfeed. The identifiers are: @@ -1803,6 +1804,7 @@ class Database { /** Deletes from the database articles which are beyond the configured clean-up threshold */ public function articleCleanup(): bool { + $integer = $this->db->sqlToken("integer"); $query = $this->db->prepareArray( "WITH RECURSIVE exempt_articles as ( @@ -1828,8 +1830,8 @@ class Database { left join ( select article, - sum(cast((starred = 1 and hidden = 0) as integer)) as starred, - sum(cast((\"read\" = 1 or hidden = 1) as integer)) as \"read\", + sum(cast((starred = 1 and hidden = 0) as $integer)) as starred, + sum(cast((\"read\" = 1 or hidden = 1) as $integer)) as \"read\", max(arsse_marks.modified) as marked_date from arsse_marks group by article @@ -1960,6 +1962,7 @@ class Database { * @param boolean $includeEmpty Whether to include (true) or supress (false) labels which have no articles assigned to them */ public function labelList(string $user, bool $includeEmpty = true): Db\Result { + $integer = $this->db->sqlToken("integer"); return $this->db->prepareArray( "SELECT * FROM ( SELECT @@ -1975,7 +1978,7 @@ class Database { SELECT label, sum(hidden) as hidden, - sum(cast((\"read\" = 1 and hidden = 0) as integer)) as marked + sum(cast((\"read\" = 1 and hidden = 0) as $integer)) as marked from arsse_marks join arsse_subscriptions on arsse_subscriptions.id = arsse_marks.subscription join arsse_label_members on arsse_label_members.article = arsse_marks.article @@ -2025,6 +2028,7 @@ class Database { $this->labelValidateId($user, $id, $byName, false); $field = $byName ? "name" : "id"; $type = $byName ? "str" : "int"; + $integer = $this->db->sqlToken("integer"); $out = $this->db->prepareArray( "SELECT id, @@ -2039,7 +2043,7 @@ class Database { SELECT label, sum(hidden) as hidden, - sum(cast((\"read\" = 1 and hidden = 0) as integer)) as marked + sum(cast((\"read\" = 1 and hidden = 0) as $integer)) as marked from arsse_marks join arsse_subscriptions on arsse_subscriptions.id = arsse_marks.subscription join arsse_label_members on arsse_label_members.article = arsse_marks.article diff --git a/lib/Db/Driver.php b/lib/Db/Driver.php index 1488b1b1..d533b926 100644 --- a/lib/Db/Driver.php +++ b/lib/Db/Driver.php @@ -74,6 +74,7 @@ interface Driver { * - "greatest": the GREATEST function implemented by PostgreSQL and MySQL * - "nocase": the name of a general-purpose case-insensitive collation sequence * - "like": the case-insensitive LIKE operator + * - "integer": the integer type to use for explicit casts */ public function sqlToken(string $token): string; diff --git a/lib/Db/MySQL/Driver.php b/lib/Db/MySQL/Driver.php index 023a2819..8a82be44 100644 --- a/lib/Db/MySQL/Driver.php +++ b/lib/Db/MySQL/Driver.php @@ -81,6 +81,8 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { switch (strtolower($token)) { case "nocase": return '"utf8mb4_unicode_ci"'; + case "integer": + return "signed integer"; default: return $token; } diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index 94091ac5..665443d3 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -90,13 +90,6 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertTrue($this->drv->charsetAcceptable()); } - public function testTranslateAToken(): void { - $this->assertRegExp("/^[a-z][a-z0-9]*$/i", $this->drv->sqlToken("greatest")); - $this->assertRegExp("/^\"?[a-z][a-z0-9_\-]*\"?$/i", $this->drv->sqlToken("nocase")); - $this->assertRegExp("/^[a-z][a-z0-9]*$/i", $this->drv->sqlToken("like")); - $this->assertSame("distinct", $this->drv->sqlToken("distinct")); - } - public function testExecAValidStatement(): void { $this->assertTrue($this->drv->exec($this->create)); } @@ -386,4 +379,18 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { // this performs maintenance in the absence of tables; see BaseUpdate.php for another test with tables $this->assertTrue($this->drv->maintenance()); } + + public function testTranslateTokens(): void { + $greatest = $this->drv->sqlToken("GrEatESt"); + $nocase = $this->drv->sqlToken("noCASE"); + $like = $this->drv->sqlToken("liKe"); + $integer = $this->drv->sqlToken("InTEGer"); + + $this->assertSame("NOT_A_TOKEN", $this->drv->sqlToken("NOT_A_TOKEN")); + + $this->assertSame("Z", $this->drv->query("SELECT $greatest('Z', 'A')")->getValue()); + $this->assertSame("Z", $this->drv->query("SELECT 'Z' collate $nocase")->getValue()); + $this->assertSame("Z", $this->drv->query("SELECT 'Z' where 'Z' $like 'z'")->getValue()); + $this->assertEquals(1, $this->drv->query("SELECT CAST((1=1) as $integer)")->getValue()); + } } From 6dba8aa66b69132752c40865de91f25cba823def Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 7 Jan 2021 15:08:50 -0500 Subject: [PATCH 102/366] Fixes for rules - Whitespace is now collapsed before evaluating rules - Feed tests are fixed to retrieve a dumy set of rules - Rule evaluation during feed parsing also filled out --- lib/Feed.php | 14 +++++++++----- lib/Rule/Rule.php | 10 ++++++---- tests/cases/Feed/TestFeed.php | 1 + tests/cases/Misc/TestRule.php | 22 ++++++++++++---------- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/lib/Feed.php b/lib/Feed.php index da01d2f0..6d68ea8f 100644 --- a/lib/Feed.php +++ b/lib/Feed.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse; use JKingWeb\Arsse\Misc\Date; +use JKingWeb\Arsse\Rule\Rule; use PicoFeed\PicoFeedException; use PicoFeed\Config\Config; use PicoFeed\Client\Client; @@ -25,6 +26,7 @@ class Feed { public $nextFetch; public $newItems = []; public $changedItems = []; + public $filteredItems = []; public static function discover(string $url, string $username = '', string $password = ''): string { // fetch the candidate feed @@ -452,14 +454,16 @@ class Feed { } protected function computeFilterRules(int $feedID): void { - return; $rules = Arsse::$db->feedRulesGet($feedID); foreach ($rules as $r) { - $keep = ""; - $block = ""; - if (strlen($r['keep'])) { - + $stats = ['new' => [], 'changed' => []]; + foreach ($this->newItems as $index => $item) { + $stats['new'][$index] = Rule::apply($r['keep'], $r['block'], $item->title, $item->categories); } + foreach ($this->changedItems as $index => $item) { + $stats['changed'][$index] = Rule::apply($r['keep'], $r['block'], $item->title, $item->categories); + } + $this->filteredItems[$r['owner']] = $stats; } } } diff --git a/lib/Rule/Rule.php b/lib/Rule/Rule.php index 5f387f1d..451d360f 100644 --- a/lib/Rule/Rule.php +++ b/lib/Rule/Rule.php @@ -45,8 +45,10 @@ abstract class Rule { public static function apply(string $keepRule, string $blockRule, string $title, array $categories = []): bool { // if neither rule is processed we should keep $keep = true; - // add the title to the front of the category array - array_unshift($categories, $title); + // merge and clean the data to match + $data = array_map(function($str) { + return preg_replace('/\s+/', " ", $str); + }, array_merge([$title], $categories)); // process the keep rule if it exists if (strlen($keepRule)) { try { @@ -56,7 +58,7 @@ abstract class Rule { } // if a keep rule is specified the default state is now not to keep $keep = false; - foreach ($categories as $str) { + foreach ($data as $str) { if (is_string($str)) { if (preg_match($rule, $str)) { // keep if the keep-rule matches one of the strings @@ -73,7 +75,7 @@ abstract class Rule { } catch (Exception $e) { return true; } - foreach ($categories as $str) { + foreach ($data as $str) { if (is_string($str)) { if (preg_match($rule, $str)) { // do not keep if the block-rule matches one of the strings diff --git a/tests/cases/Feed/TestFeed.php b/tests/cases/Feed/TestFeed.php index f9a422e4..b5ce6653 100644 --- a/tests/cases/Feed/TestFeed.php +++ b/tests/cases/Feed/TestFeed.php @@ -95,6 +95,7 @@ class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { self::clearData(); self::setConf(); Arsse::$db = \Phake::mock(Database::class); + \Phake::when(Arsse::$db)->feedRulesGet->thenReturn(new Result([])); } public function testParseAFeed(): void { diff --git a/tests/cases/Misc/TestRule.php b/tests/cases/Misc/TestRule.php index 9df68380..8652aa45 100644 --- a/tests/cases/Misc/TestRule.php +++ b/tests/cases/Misc/TestRule.php @@ -35,16 +35,18 @@ class TestRule extends \JKingWeb\Arsse\Test\AbstractTest { public function provideApplications(): iterable { return [ - ["", "", "Title", ["Dummy", "Category"], true], - ["^Title$", "", "Title", ["Dummy", "Category"], true], - ["^Category$", "", "Title", ["Dummy", "Category"], true], - ["^Naught$", "", "Title", ["Dummy", "Category"], false], - ["", "^Title$", "Title", ["Dummy", "Category"], false], - ["", "^Category$", "Title", ["Dummy", "Category"], false], - ["", "^Naught$", "Title", ["Dummy", "Category"], true], - ["^Category$", "^Category$", "Title", ["Dummy", "Category"], false], - ["[", "", "Title", ["Dummy", "Category"], true], - ["", "[", "Title", ["Dummy", "Category"], true], + ["", "", "Title", ["Dummy", "Category"], true], + ["^Title$", "", "Title", ["Dummy", "Category"], true], + ["^Category$", "", "Title", ["Dummy", "Category"], true], + ["^Naught$", "", "Title", ["Dummy", "Category"], false], + ["", "^Title$", "Title", ["Dummy", "Category"], false], + ["", "^Category$", "Title", ["Dummy", "Category"], false], + ["", "^Naught$", "Title", ["Dummy", "Category"], true], + ["^Category$", "^Category$", "Title", ["Dummy", "Category"], false], + ["[", "", "Title", ["Dummy", "Category"], true], + ["", "[", "Title", ["Dummy", "Category"], true], + ["", "^A B C$", "A B\nC", ["X\n Y \t \r Z"], false], + ["", "^X Y Z$", "A B\nC", ["X\n Y \t \r Z"], false], ]; } } From c1eff8479ca0ede7a417ea7e161e0057e0453c38 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 7 Jan 2021 19:49:09 -0500 Subject: [PATCH 103/366] Simplify configuration property caching --- lib/Conf.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/Conf.php b/lib/Conf.php index 2de8addb..c6fd33c1 100644 --- a/lib/Conf.php +++ b/lib/Conf.php @@ -128,16 +128,14 @@ class Conf { 'dbSQLite3Timeout' => "double", ]; - protected static $types = []; + protected $types = []; /** Creates a new configuration object * @param string $import_file Optional file to read configuration data from * @see self::importFile() */ public function __construct(string $import_file = "") { - if (!static::$types) { - static::$types = $this->propertyDiscover(); - } - foreach (array_keys(static::$types) as $prop) { + $this->types = $this->propertyDiscover(); + foreach (array_keys($this->types) as $prop) { $this->$prop = $this->propertyImport($prop, $this->$prop); } if ($import_file !== "") { @@ -273,9 +271,9 @@ class Conf { } protected function propertyImport(string $key, $value, string $file = "") { - $typeName = static::$types[$key]['name'] ?? "mixed"; - $typeConst = static::$types[$key]['const'] ?? Value::T_MIXED; - $nullable = (int) (bool) (static::$types[$key]['const'] & Value::M_NULL); + $typeName = $this->types[$key]['name'] ?? "mixed"; + $typeConst = $this->types[$key]['const'] ?? Value::T_MIXED; + $nullable = (int) (bool) ($this->types[$key]['const'] & Value::M_NULL); try { if ($typeName === "\\DateInterval") { // date intervals have special handling: if the existing value (ultimately, the default value) @@ -319,7 +317,7 @@ class Conf { } return $value; } catch (ExceptionType $e) { - $type = static::$types[$key]['const'] & ~(Value::M_STRICT | Value::M_DROP | Value::M_NULL | Value::M_ARRAY); + $type = $this->types[$key]['const'] & ~(Value::M_STRICT | Value::M_DROP | Value::M_NULL | Value::M_ARRAY); throw new Conf\Exception("typeMismatch", ['param' => $key, 'type' => self::TYPE_NAMES[$type], 'file' => $file, 'nullable' => $nullable]); } } From 4f34b4ff2968bb73f543fe2277c0c61a0a8d9783 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 8 Jan 2021 14:17:46 -0500 Subject: [PATCH 104/366] Rule refactoring - The Database class is now responsible for preparing rules - Rules are now returned in an array keyed by user - Empty strings are now passed through during rule preparation --- lib/Database.php | 28 +++++++++++++----- lib/Feed.php | 4 +-- lib/Rule/Rule.php | 45 +++++++++++++---------------- tests/cases/Database/SeriesFeed.php | 12 ++++---- tests/cases/Feed/TestFeed.php | 2 +- tests/cases/Misc/TestRule.php | 9 ++++-- 6 files changed, 57 insertions(+), 43 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 343e2cec..f748aa7c 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -13,6 +13,8 @@ use JKingWeb\Arsse\Context\Context; use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\ValueInfo as V; use JKingWeb\Arsse\Misc\URL; +use JKingWeb\Arsse\Rule\Rule; +use JKingWeb\Arsse\Rule\Exception as RuleException; /** The high-level interface with the database * @@ -1205,14 +1207,26 @@ class Database { /** Retrieves the set of filters users have applied to a given feed * - * Each record includes the following keys: - * - * - "owner": The user for whom to apply the filters - * - "keep": The "keep" rule; any articles which fail to match this rule are hidden - * - "block": The block rule; any article which matches this rule are hidden + * The result is an associative array whose keys are usernames, values + * being an array in turn with the following keys: + * + * - "keep": The "keep" rule as a prepared pattern; any articles which fail to match this rule are hidden + * - "block": The block rule as a prepared pattern; any articles which match this rule are hidden */ - public function feedRulesGet(int $feedID): Db\Result { - return $this->db->prepare("SELECT owner, coalesce(keep_rule, '') as keep, coalesce(block_rule, '') as block from arsse_subscriptions where feed = ? and (coalesce(keep_rule, '') || coalesce(block_rule, '')) <> '' order by owner", "int")->run($feedID); + public function feedRulesGet(int $feedID): array { + $out = []; + $result = $this->db->prepare("SELECT owner, coalesce(keep_rule, '') as keep, coalesce(block_rule, '') as block from arsse_subscriptions where feed = ? and (coalesce(keep_rule, '') || coalesce(block_rule, '')) <> '' order by owner", "int")->run($feedID); + foreach ($result as $row) { + try { + $keep = Rule::prep($row['keep']); + $block = Rule::prep($row['block']); + } catch (RuleException $e) { + // invalid rules should not normally appear in the database, but it's possible + continue; + } + $out[$row['owner']] = ['keep' => $keep, 'block' => $block]; + } + return $out; } /** Retrieves various identifiers for the latest $count articles in the given newsfeed. The identifiers are: diff --git a/lib/Feed.php b/lib/Feed.php index 6d68ea8f..b0e91290 100644 --- a/lib/Feed.php +++ b/lib/Feed.php @@ -455,7 +455,7 @@ class Feed { protected function computeFilterRules(int $feedID): void { $rules = Arsse::$db->feedRulesGet($feedID); - foreach ($rules as $r) { + foreach ($rules as $user => $r) { $stats = ['new' => [], 'changed' => []]; foreach ($this->newItems as $index => $item) { $stats['new'][$index] = Rule::apply($r['keep'], $r['block'], $item->title, $item->categories); @@ -463,7 +463,7 @@ class Feed { foreach ($this->changedItems as $index => $item) { $stats['changed'][$index] = Rule::apply($r['keep'], $r['block'], $item->title, $item->categories); } - $this->filteredItems[$r['owner']] = $stats; + $this->filteredItems[$user] = $stats; } } } diff --git a/lib/Rule/Rule.php b/lib/Rule/Rule.php index 451d360f..ee3c4a65 100644 --- a/lib/Rule/Rule.php +++ b/lib/Rule/Rule.php @@ -8,6 +8,9 @@ namespace JKingWeb\Arsse\Rule; abstract class Rule { public static function prep(string $pattern): string { + if (!strlen($pattern)) { + return ""; + } if (preg_match_all("<`>", $pattern, $m, \PREG_OFFSET_CAPTURE)) { // where necessary escape our chosen delimiter (backtick) in reverse order foreach (array_reverse($m[0]) as [,$pos]) { @@ -42,7 +45,13 @@ abstract class Rule { * * Returns true if the article is to be kept, and false if it is to be suppressed */ - public static function apply(string $keepRule, string $blockRule, string $title, array $categories = []): bool { + public static function apply(string $keepPattern, string $blockPattern, string $title, array $categories = []): bool { + // ensure input is valid + assert(!strlen($keepPattern) || @preg_match($keepPattern, "") !== false, new \Exception("Keep pattern is invalid")); + assert(!strlen($blockPattern) || @preg_match($blockPattern, "") !== false, new \Exception("Block pattern is invalid")); + assert(sizeof(array_filter($categories, function($v) { + return !is_string($v); + })) === 0, new \Exception("All categories must be strings")); // if neither rule is processed we should keep $keep = true; // merge and clean the data to match @@ -50,38 +59,24 @@ abstract class Rule { return preg_replace('/\s+/', " ", $str); }, array_merge([$title], $categories)); // process the keep rule if it exists - if (strlen($keepRule)) { - try { - $rule = static::prep($keepRule); - } catch (Exception $e) { - return true; - } + if (strlen($keepPattern)) { // if a keep rule is specified the default state is now not to keep $keep = false; foreach ($data as $str) { - if (is_string($str)) { - if (preg_match($rule, $str)) { - // keep if the keep-rule matches one of the strings - $keep = true; - break; - } + if (preg_match($keepPattern, $str)) { + // keep if the keep-rule matches one of the strings + $keep = true; + break; } } } // process the block rule if the keep rule was matched - if ($keep && strlen($blockRule)) { - try { - $rule = static::prep($blockRule); - } catch (Exception $e) { - return true; - } + if ($keep && strlen($blockPattern)) { foreach ($data as $str) { - if (is_string($str)) { - if (preg_match($rule, $str)) { - // do not keep if the block-rule matches one of the strings - $keep = false; - break; - } + if (preg_match($blockPattern, $str)) { + // do not keep if the block-rule matches one of the strings + $keep = false; + break; } } } diff --git a/tests/cases/Database/SeriesFeed.php b/tests/cases/Database/SeriesFeed.php index 8f17694a..65a2931f 100644 --- a/tests/cases/Database/SeriesFeed.php +++ b/tests/cases/Database/SeriesFeed.php @@ -76,9 +76,9 @@ trait SeriesFeed { ], 'rows' => [ [1,'john.doe@example.com',1,null,'^Sport$'], - [2,'john.doe@example.com',2,null,null], + [2,'john.doe@example.com',2,"",null], [3,'john.doe@example.com',3,'\w+',null], - [4,'john.doe@example.com',4,null,null], + [4,'john.doe@example.com',4,'\w+',"["], // invalid rule leads to both rules being ignored [5,'john.doe@example.com',5,null,'and/or'], [6,'jane.doe@example.com',1,'^(?i)[a-z]+','bluberry'], ], @@ -205,16 +205,16 @@ trait SeriesFeed { /** @dataProvider provideFilterRules */ public function testGetRules(int $in, array $exp): void { - $this->assertResult($exp, Arsse::$db->feedRulesGet($in)); + $this->assertSame($exp, Arsse::$db->feedRulesGet($in)); } public function provideFilterRules(): iterable { return [ - [1, [['owner' => "john.doe@example.com", 'keep' => "", 'block' => "^Sport$"], ['owner' => "jane.doe@example.com", 'keep' => "^(?i)[a-z]+", 'block' => "bluberry"]]], + [1, ['jane.doe@example.com' => ['keep' => "`^(?i)[a-z]+`u", 'block' => "`bluberry`u"], 'john.doe@example.com' => ['keep' => "", 'block' => "`^Sport$`u"]]], [2, []], - [3, [['owner' => "john.doe@example.com", 'keep' => '\w+', 'block' => ""]]], + [3, ['john.doe@example.com' => ['keep' => '`\w+`u', 'block' => ""]]], [4, []], - [5, [['owner' => "john.doe@example.com", 'keep' => "", 'block' => "and/or"]]], + [5, ['john.doe@example.com' => ['keep' => "", 'block' => "`and/or`u"]]], ]; } diff --git a/tests/cases/Feed/TestFeed.php b/tests/cases/Feed/TestFeed.php index b5ce6653..cb94c5e7 100644 --- a/tests/cases/Feed/TestFeed.php +++ b/tests/cases/Feed/TestFeed.php @@ -95,7 +95,7 @@ class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { self::clearData(); self::setConf(); Arsse::$db = \Phake::mock(Database::class); - \Phake::when(Arsse::$db)->feedRulesGet->thenReturn(new Result([])); + \Phake::when(Arsse::$db)->feedRulesGet->thenReturn([]); } public function testParseAFeed(): void { diff --git a/tests/cases/Misc/TestRule.php b/tests/cases/Misc/TestRule.php index 8652aa45..0d72f5fd 100644 --- a/tests/cases/Misc/TestRule.php +++ b/tests/cases/Misc/TestRule.php @@ -23,8 +23,15 @@ class TestRule extends \JKingWeb\Arsse\Test\AbstractTest { Rule::prep("["); } + public function testPrepareAnEmptyPattern(): void { + $this->assertTrue(Rule::validate("")); + $this->assertSame("", Rule::prep("")); + } + /** @dataProvider provideApplications */ public function testApplyRules(string $keepRule, string $blockRule, string $title, array $categories, $exp): void { + $keepRule = Rule::prep($keepRule); + $blockRule = Rule::prep($blockRule); if ($exp instanceof \Exception) { $this->assertException($exp); Rule::apply($keepRule, $blockRule, $title, $categories); @@ -43,8 +50,6 @@ class TestRule extends \JKingWeb\Arsse\Test\AbstractTest { ["", "^Category$", "Title", ["Dummy", "Category"], false], ["", "^Naught$", "Title", ["Dummy", "Category"], true], ["^Category$", "^Category$", "Title", ["Dummy", "Category"], false], - ["[", "", "Title", ["Dummy", "Category"], true], - ["", "[", "Title", ["Dummy", "Category"], true], ["", "^A B C$", "A B\nC", ["X\n Y \t \r Z"], false], ["", "^X Y Z$", "A B\nC", ["X\n Y \t \r Z"], false], ]; From 549c7bdc721002400b047a3ed33dc4a7f1fa2acb Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 8 Jan 2021 15:47:19 -0500 Subject: [PATCH 105/366] Style fixes --- lib/Database.php | 2 +- lib/REST/Miniflux/V1.php | 4 ++-- lib/Rule/Exception.php | 2 +- lib/Rule/Rule.php | 2 +- lib/User.php | 8 ++++---- lib/User/Driver.php | 6 +++--- tests/cases/Database/SeriesUser.php | 4 ++-- tests/cases/Misc/TestRule.php | 8 +------- tests/cases/REST/Miniflux/TestV1.php | 2 +- tests/cases/User/TestInternal.php | 1 - tests/cases/User/TestUser.php | 4 ++-- 11 files changed, 18 insertions(+), 25 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index f748aa7c..5e44d384 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1209,7 +1209,7 @@ class Database { * * The result is an associative array whose keys are usernames, values * being an array in turn with the following keys: - * + * * - "keep": The "keep" rule as a prepared pattern; any articles which fail to match this rule are hidden * - "block": The block rule as a prepared pattern; any articles which match this rule are hidden */ diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 9ad2882c..f048e798 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -265,7 +265,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } //normalize user-specific input - foreach (self::USER_META_MAP as $k => [,$d,]) { + foreach (self::USER_META_MAP as $k => [,$d]) { $t = gettype($d); if (!isset($body[$k])) { $body[$k] = null; @@ -343,7 +343,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function editUser(string $user, array $data): array { // map Miniflux properties to internal metadata properties $in = []; - foreach (self::USER_META_MAP as $i => [$o,,]) { + foreach (self::USER_META_MAP as $i => [$o,]) { if (isset($data[$i])) { if ($i === "entry_sorting_direction") { $in[$o] = $data[$i] === "asc"; diff --git a/lib/Rule/Exception.php b/lib/Rule/Exception.php index e3c66645..1239e378 100644 --- a/lib/Rule/Exception.php +++ b/lib/Rule/Exception.php @@ -7,4 +7,4 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Rule; class Exception extends \JKingWeb\Arsse\AbstractException { -} \ No newline at end of file +} diff --git a/lib/Rule/Rule.php b/lib/Rule/Rule.php index ee3c4a65..c8d41898 100644 --- a/lib/Rule/Rule.php +++ b/lib/Rule/Rule.php @@ -42,7 +42,7 @@ abstract class Rule { } /** applies keep and block rules against the title and categories of an article - * + * * Returns true if the article is to be kept, and false if it is to be suppressed */ public static function apply(string $keepPattern, string $blockPattern, string $title, array $categories = []): bool { diff --git a/lib/User.php b/lib/User.php index accec103..04748962 100644 --- a/lib/User.php +++ b/lib/User.php @@ -44,12 +44,12 @@ class User { public function begin(): Db\Transaction { /* TODO: A proper implementation of this would return a meta-transaction - object which would contain both a user-manager transaction (when + object which would contain both a user-manager transaction (when applicable) and a database transaction, and commit or roll back both - as the situation calls. + as the situation calls. In theory, an external user driver would probably have to implement its - own approximation of atomic transactions and rollback. In practice the + own approximation of atomic transactions and rollback. In practice the only driver is the internal one, which is always backed by an ACID database; the added complexity is thus being deferred until such time as it is actually needed for a concrete implementation. @@ -106,7 +106,7 @@ class User { } public function rename(string $user, string $newName): bool { - // ensure the new user name does not contain any U+003A COLON or + // ensure the new user name does not contain any U+003A COLON or // control characters, as this is incompatible with HTTP Basic authentication if (preg_match("/[\x{00}-\x{1F}\x{7F}:]/", $newName, $m)) { $c = ord($m[0]); diff --git a/lib/User/Driver.php b/lib/User/Driver.php index d4b73706..87668791 100644 --- a/lib/User/Driver.php +++ b/lib/User/Driver.php @@ -28,10 +28,10 @@ interface Driver { public function userAdd(string $user, string $password = null): ?string; /** Renames a user - * - * The implementation must retain all user metadata as well as the + * + * The implementation must retain all user metadata as well as the * user's password - */ + */ public function userRename(string $user, string $newName): bool; /** Removes a user */ diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index 0cd4ffb7..b56a64d8 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -184,8 +184,8 @@ trait SeriesUser { public function testRenameAUser(): void { $this->assertTrue(Arsse::$db->userRename("john.doe@example.com", "juan.doe@example.com")); $state = $this->primeExpectations($this->data, [ - 'arsse_users' => ['id', 'num'], - 'arsse_user_meta' => ["owner", "key", "value"] + 'arsse_users' => ['id', 'num'], + 'arsse_user_meta' => ["owner", "key", "value"], ]); $state['arsse_users']['rows'][2][0] = "juan.doe@example.com"; $state['arsse_user_meta']['rows'][6][0] = "juan.doe@example.com"; diff --git a/tests/cases/Misc/TestRule.php b/tests/cases/Misc/TestRule.php index 0d72f5fd..88503292 100644 --- a/tests/cases/Misc/TestRule.php +++ b/tests/cases/Misc/TestRule.php @@ -7,7 +7,6 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Misc; use JKingWeb\Arsse\Rule\Rule; -use JKingWeb\Arsse\Rule\Exception; /** @covers \JKingWeb\Arsse\Rule\Rule */ class TestRule extends \JKingWeb\Arsse\Test\AbstractTest { @@ -32,12 +31,7 @@ class TestRule extends \JKingWeb\Arsse\Test\AbstractTest { public function testApplyRules(string $keepRule, string $blockRule, string $title, array $categories, $exp): void { $keepRule = Rule::prep($keepRule); $blockRule = Rule::prep($blockRule); - if ($exp instanceof \Exception) { - $this->assertException($exp); - Rule::apply($keepRule, $blockRule, $title, $categories); - } else { - $this->assertSame($exp, Rule::apply($keepRule, $blockRule, $title, $categories)); - } + $this->assertSame($exp, Rule::apply($keepRule, $blockRule, $title, $categories)); } public function provideApplications(): iterable { diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 255d9a55..496df378 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -451,7 +451,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ["", new ErrorResponse(["InvalidCategory", 'title' => ""], 422)], [" ", new ErrorResponse(["InvalidCategory", 'title' => " "], 422)], [null, new ErrorResponse(["MissingInputValue", 'field' => "title"], 422)], - [false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"],422)], + [false, new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 422)], ]; } diff --git a/tests/cases/User/TestInternal.php b/tests/cases/User/TestInternal.php index 858a8765..fa42de17 100644 --- a/tests/cases/User/TestInternal.php +++ b/tests/cases/User/TestInternal.php @@ -9,7 +9,6 @@ namespace JKingWeb\Arsse\TestCase\User; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\User\Driver as DriverInterface; -use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\Internal\Driver; /** @covers \JKingWeb\Arsse\User\Internal\Driver */ diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 7c87e0c3..e9a45c9f 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -200,7 +200,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::when($this->drv)->userRename->thenReturn(true); $u = new User($this->drv); $old = "john.doe@example.com"; - $new = "jane.doe@example.com"; + $new = "jane.doe@example.com"; $this->assertTrue($u->rename($old, $new)); \Phake::inOrder( \Phake::verify($this->drv)->userRename($old, $new), @@ -222,7 +222,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::when($this->drv)->userRename->thenReturn(true); $u = new User($this->drv); $old = "john.doe@example.com"; - $new = "jane.doe@example.com"; + $new = "jane.doe@example.com"; $this->assertTrue($u->rename($old, $new)); \Phake::inOrder( \Phake::verify($this->drv)->userRename($old, $new), From 9e29235d87e57fdc4ca4a529c0319dacc287cb34 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 8 Jan 2021 16:46:21 -0500 Subject: [PATCH 106/366] Don't fetch from example.com during tests --- tests/cases/ImportExport/TestOPML.php | 44 +++++++++---------- tests/docroot/Feed/Caching/200Future.php | 2 +- tests/docroot/Feed/Caching/200Multiple.php | 2 +- tests/docroot/Feed/Caching/200None.php | 2 +- tests/docroot/Feed/Caching/200Past.php | 2 +- tests/docroot/Feed/Caching/200PubDateOnly.php | 2 +- tests/docroot/Feed/Caching/200UpdateDate.php | 2 +- .../Feed/Deduplication/Hashes-Dates1.php | 2 +- .../Feed/Deduplication/Hashes-Dates2.php | 2 +- .../Feed/Deduplication/Hashes-Dates3.php | 2 +- tests/docroot/Feed/Deduplication/Hashes.php | 2 +- tests/docroot/Feed/Deduplication/ID-Dates.php | 2 +- .../Feed/Deduplication/IdenticalHashes.php | 2 +- .../Feed/Deduplication/Permalink-Dates.php | 10 ++--- tests/docroot/Feed/Discovery/Feed.php | 2 +- tests/docroot/Feed/Fetching/TooLarge.php | 2 +- tests/docroot/Feed/Matching/1.php | 2 +- tests/docroot/Feed/Matching/2.php | 2 +- tests/docroot/Feed/Matching/3.php | 2 +- tests/docroot/Feed/Matching/4.php | 2 +- tests/docroot/Feed/Matching/5.php | 2 +- tests/docroot/Feed/NextFetch/1h.php | 2 +- tests/docroot/Feed/NextFetch/3-36h.php | 2 +- tests/docroot/Feed/NextFetch/30m.php | 2 +- tests/docroot/Feed/NextFetch/36h.php | 2 +- tests/docroot/Feed/NextFetch/3h.php | 2 +- tests/docroot/Feed/NextFetch/Fallback.php | 2 +- tests/docroot/Feed/Parsing/Valid.php | 2 +- tests/docroot/Feed/Parsing/XEEAttack.php | 10 ++--- tests/docroot/Feed/Parsing/XXEAttack.php | 10 ++--- tests/docroot/Feed/Scraping/Feed.php | 2 +- tests/docroot/Import/OPML/BrokenOPML.2.opml | 2 +- tests/docroot/Import/OPML/FeedsOnly.opml | 10 ++--- tests/docroot/Import/some-feed.php | 2 +- tests/docroot/index.php | 4 ++ tests/server.php | 3 ++ 36 files changed, 78 insertions(+), 71 deletions(-) create mode 100644 tests/docroot/index.php diff --git a/tests/cases/ImportExport/TestOPML.php b/tests/cases/ImportExport/TestOPML.php index 3c61688e..469c674b 100644 --- a/tests/cases/ImportExport/TestOPML.php +++ b/tests/cases/ImportExport/TestOPML.php @@ -22,12 +22,12 @@ class TestOPML extends \JKingWeb\Arsse\Test\AbstractTest { ['id' => 1, 'parent' => null, 'children' => 1, 'feeds' => 1, 'name' => "Science"], ]; protected $subscriptions = [ - ['id' => 3, 'folder' => 1, 'top_folder' => 1, 'unread' => 2, 'updated' => "2016-05-23 06:40:02", 'err_msg' => 'argh', 'title' => 'Ars Technica', 'url' => "http://example.com/3", 'favicon' => 'http://example.com/3.png'], - ['id' => 4, 'folder' => 6, 'top_folder' => 3, 'unread' => 6, 'updated' => "2017-10-09 15:58:34", 'err_msg' => '', 'title' => 'CBC News', 'url' => "http://example.com/4", 'favicon' => 'http://example.com/4.png'], - ['id' => 6, 'folder' => null, 'top_folder' => null, 'unread' => 0, 'updated' => "2010-02-12 20:08:47", 'err_msg' => '', 'title' => 'Eurogamer', 'url' => "http://example.com/6", 'favicon' => 'http://example.com/6.png'], - ['id' => 1, 'folder' => 2, 'top_folder' => 1, 'unread' => 5, 'updated' => "2017-09-15 22:54:16", 'err_msg' => '', 'title' => 'NASA JPL', 'url' => "http://example.com/1", 'favicon' => null], - ['id' => 5, 'folder' => 6, 'top_folder' => 3, 'unread' => 12, 'updated' => "2017-07-07 17:07:17", 'err_msg' => '', 'title' => 'Ottawa Citizen', 'url' => "http://example.com/5", 'favicon' => ''], - ['id' => 2, 'folder' => 5, 'top_folder' => 3, 'unread' => 10, 'updated' => "2011-11-11 11:11:11", 'err_msg' => 'oops', 'title' => 'Toronto Star', 'url' => "http://example.com/2", 'favicon' => 'http://example.com/2.png'], + ['id' => 3, 'folder' => 1, 'top_folder' => 1, 'unread' => 2, 'updated' => "2016-05-23 06:40:02", 'err_msg' => 'argh', 'title' => 'Ars Technica', 'url' => "http://localhost:8000/3", 'favicon' => 'http://localhost:8000/3.png'], + ['id' => 4, 'folder' => 6, 'top_folder' => 3, 'unread' => 6, 'updated' => "2017-10-09 15:58:34", 'err_msg' => '', 'title' => 'CBC News', 'url' => "http://localhost:8000/4", 'favicon' => 'http://localhost:8000/4.png'], + ['id' => 6, 'folder' => null, 'top_folder' => null, 'unread' => 0, 'updated' => "2010-02-12 20:08:47", 'err_msg' => '', 'title' => 'Eurogamer', 'url' => "http://localhost:8000/6", 'favicon' => 'http://localhost:8000/6.png'], + ['id' => 1, 'folder' => 2, 'top_folder' => 1, 'unread' => 5, 'updated' => "2017-09-15 22:54:16", 'err_msg' => '', 'title' => 'NASA JPL', 'url' => "http://localhost:8000/1", 'favicon' => null], + ['id' => 5, 'folder' => 6, 'top_folder' => 3, 'unread' => 12, 'updated' => "2017-07-07 17:07:17", 'err_msg' => '', 'title' => 'Ottawa Citizen', 'url' => "http://localhost:8000/5", 'favicon' => ''], + ['id' => 2, 'folder' => 5, 'top_folder' => 3, 'unread' => 10, 'updated' => "2011-11-11 11:11:11", 'err_msg' => 'oops', 'title' => 'Toronto Star', 'url' => "http://localhost:8000/2", 'favicon' => 'http://localhost:8000/2.png'], ]; protected $tags = [ ['id' => 1, 'name' => "Canada", 'subscription' => 2], @@ -47,20 +47,20 @@ class TestOPML extends \JKingWeb\Arsse\Test\AbstractTest { - + - - + + - + - + - + OPML_EXPORT_SERIALIZATION; @@ -69,12 +69,12 @@ OPML_EXPORT_SERIALIZATION; - - - - - - + + + + + + OPML_EXPORT_SERIALIZATION; @@ -129,10 +129,10 @@ OPML_EXPORT_SERIALIZATION; ["Empty.2.opml", false, [[], []]], ["Empty.3.opml", false, [[], []]], ["FeedsOnly.opml", false, [[ - ['url' => "http://example.com/1", 'title' => "Feed 1", 'folder' => 0, 'tags' => []], - ['url' => "http://example.com/2", 'title' => "", 'folder' => 0, 'tags' => []], - ['url' => "http://example.com/3", 'title' => "", 'folder' => 0, 'tags' => []], - ['url' => "http://example.com/4", 'title' => "", 'folder' => 0, 'tags' => []], + ['url' => "http://localhost:8000/1", 'title' => "Feed 1", 'folder' => 0, 'tags' => []], + ['url' => "http://localhost:8000/2", 'title' => "", 'folder' => 0, 'tags' => []], + ['url' => "http://localhost:8000/3", 'title' => "", 'folder' => 0, 'tags' => []], + ['url' => "http://localhost:8000/4", 'title' => "", 'folder' => 0, 'tags' => []], ['url' => "", 'title' => "", 'folder' => 0, 'tags' => ["whee"]], ['url' => "", 'title' => "", 'folder' => 0, 'tags' => ["whee", "whoo"]], ], []]], diff --git a/tests/docroot/Feed/Caching/200Future.php b/tests/docroot/Feed/Caching/200Future.php index ef2ae714..ad43e361 100644 --- a/tests/docroot/Feed/Caching/200Future.php +++ b/tests/docroot/Feed/Caching/200Future.php @@ -6,7 +6,7 @@ Test feed - http://example.com/ + http://localhost:8000/ A basic feed for testing diff --git a/tests/docroot/Feed/Caching/200Multiple.php b/tests/docroot/Feed/Caching/200Multiple.php index 583b6633..ebbd8a29 100644 --- a/tests/docroot/Feed/Caching/200Multiple.php +++ b/tests/docroot/Feed/Caching/200Multiple.php @@ -6,7 +6,7 @@ Test feed - http://example.com/ + http://localhost:8000/ A basic feed for testing diff --git a/tests/docroot/Feed/Caching/200None.php b/tests/docroot/Feed/Caching/200None.php index 562554cf..ebe7721f 100644 --- a/tests/docroot/Feed/Caching/200None.php +++ b/tests/docroot/Feed/Caching/200None.php @@ -6,7 +6,7 @@ Test feed - http://example.com/ + http://localhost:8000/ A basic feed for testing diff --git a/tests/docroot/Feed/Caching/200Past.php b/tests/docroot/Feed/Caching/200Past.php index 361d7670..64da54c5 100644 --- a/tests/docroot/Feed/Caching/200Past.php +++ b/tests/docroot/Feed/Caching/200Past.php @@ -6,7 +6,7 @@ Test feed - http://example.com/ + http://localhost:8000/ A basic feed for testing diff --git a/tests/docroot/Feed/Caching/200PubDateOnly.php b/tests/docroot/Feed/Caching/200PubDateOnly.php index 5b8df9b0..93ce6370 100644 --- a/tests/docroot/Feed/Caching/200PubDateOnly.php +++ b/tests/docroot/Feed/Caching/200PubDateOnly.php @@ -6,7 +6,7 @@ Test feed - http://example.com/ + http://localhost:8000/ A basic feed for testing diff --git a/tests/docroot/Feed/Caching/200UpdateDate.php b/tests/docroot/Feed/Caching/200UpdateDate.php index e7f9a20b..3315d289 100644 --- a/tests/docroot/Feed/Caching/200UpdateDate.php +++ b/tests/docroot/Feed/Caching/200UpdateDate.php @@ -6,7 +6,7 @@ Test feed - http://example.com/ + http://localhost:8000/ A basic feed for testing diff --git a/tests/docroot/Feed/Deduplication/Hashes-Dates1.php b/tests/docroot/Feed/Deduplication/Hashes-Dates1.php index 4709e807..8449beed 100644 --- a/tests/docroot/Feed/Deduplication/Hashes-Dates1.php +++ b/tests/docroot/Feed/Deduplication/Hashes-Dates1.php @@ -4,7 +4,7 @@ Test feed - http://example.com/ + http://localhost:8000/ A basic feed for testing diff --git a/tests/docroot/Feed/Deduplication/Hashes-Dates2.php b/tests/docroot/Feed/Deduplication/Hashes-Dates2.php index 321d675d..b460c7d8 100644 --- a/tests/docroot/Feed/Deduplication/Hashes-Dates2.php +++ b/tests/docroot/Feed/Deduplication/Hashes-Dates2.php @@ -4,7 +4,7 @@ Test feed - http://example.com/ + http://localhost:8000/ A basic feed for testing diff --git a/tests/docroot/Feed/Deduplication/Hashes-Dates3.php b/tests/docroot/Feed/Deduplication/Hashes-Dates3.php index 01d0916d..ce950194 100644 --- a/tests/docroot/Feed/Deduplication/Hashes-Dates3.php +++ b/tests/docroot/Feed/Deduplication/Hashes-Dates3.php @@ -4,7 +4,7 @@ Test feed - http://example.com/ + http://localhost:8000/ A basic feed for testing diff --git a/tests/docroot/Feed/Deduplication/Hashes.php b/tests/docroot/Feed/Deduplication/Hashes.php index bc6eaec4..2f2a9677 100644 --- a/tests/docroot/Feed/Deduplication/Hashes.php +++ b/tests/docroot/Feed/Deduplication/Hashes.php @@ -4,7 +4,7 @@ Test feed - http://example.com/ + http://localhost:8000/ A basic feed for testing diff --git a/tests/docroot/Feed/Deduplication/ID-Dates.php b/tests/docroot/Feed/Deduplication/ID-Dates.php index f26cfc50..90f70260 100644 --- a/tests/docroot/Feed/Deduplication/ID-Dates.php +++ b/tests/docroot/Feed/Deduplication/ID-Dates.php @@ -4,7 +4,7 @@ Test feed - http://example.com/ + http://localhost:8000/ A basic feed for testing diff --git a/tests/docroot/Feed/Deduplication/IdenticalHashes.php b/tests/docroot/Feed/Deduplication/IdenticalHashes.php index 138b7b44..b9e64667 100644 --- a/tests/docroot/Feed/Deduplication/IdenticalHashes.php +++ b/tests/docroot/Feed/Deduplication/IdenticalHashes.php @@ -4,7 +4,7 @@ Test feed - http://example.com/ + http://localhost:8000/ A basic feed for testing diff --git a/tests/docroot/Feed/Deduplication/Permalink-Dates.php b/tests/docroot/Feed/Deduplication/Permalink-Dates.php index 304211a8..afb91548 100644 --- a/tests/docroot/Feed/Deduplication/Permalink-Dates.php +++ b/tests/docroot/Feed/Deduplication/Permalink-Dates.php @@ -4,29 +4,29 @@ Test feed - http://example.com/ + http://localhost:8000/ A basic feed for testing - http://example.com/1 + http://localhost:8000/1 Sample article 1 Sun, 18 May 1995 15:21:36 GMT 2002-02-19T15:21:36Z - http://example.com/1 + http://localhost:8000/1 Sample article 2 Sun, 19 May 2002 15:21:36 GMT 2002-04-19T15:21:36Z - http://example.com/1 + http://localhost:8000/1 Sample article 3 Sun, 18 May 2000 15:21:36 GMT 1999-05-19T15:21:36Z - http://example.com/2 + http://localhost:8000/2 Sample article 4 Sun, 18 May 2000 15:21:36 GMT 1999-05-19T15:21:36Z diff --git a/tests/docroot/Feed/Discovery/Feed.php b/tests/docroot/Feed/Discovery/Feed.php index a13398ac..bb413510 100644 --- a/tests/docroot/Feed/Discovery/Feed.php +++ b/tests/docroot/Feed/Discovery/Feed.php @@ -4,7 +4,7 @@ Test feed - http://example.com/ + http://localhost:8000/ Example newsfeed title diff --git a/tests/docroot/Feed/Fetching/TooLarge.php b/tests/docroot/Feed/Fetching/TooLarge.php index 0fef567b..d13f89c6 100644 --- a/tests/docroot/Feed/Fetching/TooLarge.php +++ b/tests/docroot/Feed/Fetching/TooLarge.php @@ -9,7 +9,7 @@ return [ Test feed - http://example.com/ + http://localhost:8000/ Example newsfeed title $item diff --git a/tests/docroot/Feed/Matching/1.php b/tests/docroot/Feed/Matching/1.php index fdc2d676..ef9dfcd0 100644 --- a/tests/docroot/Feed/Matching/1.php +++ b/tests/docroot/Feed/Matching/1.php @@ -4,7 +4,7 @@ Example feed title urn:uuid:0fd8f6d8-43df-11e7-8511-9b59a0324eb8 - + urn:uuid:df329114-43df-11e7-9f23-a938604d62f8 diff --git a/tests/docroot/Feed/Matching/2.php b/tests/docroot/Feed/Matching/2.php index b5e2d51c..0a4ae553 100644 --- a/tests/docroot/Feed/Matching/2.php +++ b/tests/docroot/Feed/Matching/2.php @@ -4,7 +4,7 @@ Example feed title urn:uuid:0fd8f6d8-43df-11e7-8511-9b59a0324eb8 - + urn:uuid:df329114-43df-11e7-9f23-a938604d62f8 diff --git a/tests/docroot/Feed/Matching/3.php b/tests/docroot/Feed/Matching/3.php index d2c8c0d3..665d5d3a 100644 --- a/tests/docroot/Feed/Matching/3.php +++ b/tests/docroot/Feed/Matching/3.php @@ -4,7 +4,7 @@ Example feed title urn:uuid:0fd8f6d8-43df-11e7-8511-9b59a0324eb8 - + urn:uuid:df329114-43df-11e7-9f23-a938604d62f8 diff --git a/tests/docroot/Feed/Matching/4.php b/tests/docroot/Feed/Matching/4.php index a68c0e05..5cd92493 100644 --- a/tests/docroot/Feed/Matching/4.php +++ b/tests/docroot/Feed/Matching/4.php @@ -4,7 +4,7 @@ Example feed title urn:uuid:0fd8f6d8-43df-11e7-8511-9b59a0324eb8 - + urn:uuid:df329114-43df-11e7-9f23-a938604d62f8 diff --git a/tests/docroot/Feed/Matching/5.php b/tests/docroot/Feed/Matching/5.php index efb5a9b3..de61d767 100644 --- a/tests/docroot/Feed/Matching/5.php +++ b/tests/docroot/Feed/Matching/5.php @@ -4,7 +4,7 @@ Example feed title urn:uuid:0fd8f6d8-43df-11e7-8511-9b59a0324eb8 - + urn:uuid:3d5f5154-43e1-11e7-ba11-1dcae392a974 diff --git a/tests/docroot/Feed/NextFetch/1h.php b/tests/docroot/Feed/NextFetch/1h.php index dd016507..ca9cdac6 100644 --- a/tests/docroot/Feed/NextFetch/1h.php +++ b/tests/docroot/Feed/NextFetch/1h.php @@ -4,7 +4,7 @@ Example title - http://example.com + http://localhost:8000/ Example description diff --git a/tests/docroot/Feed/NextFetch/3-36h.php b/tests/docroot/Feed/NextFetch/3-36h.php index 41d799f8..414d2f08 100644 --- a/tests/docroot/Feed/NextFetch/3-36h.php +++ b/tests/docroot/Feed/NextFetch/3-36h.php @@ -4,7 +4,7 @@ Example title - http://example.com + http://localhost:8000/ Example description diff --git a/tests/docroot/Feed/NextFetch/30m.php b/tests/docroot/Feed/NextFetch/30m.php index a7dce241..397871ac 100644 --- a/tests/docroot/Feed/NextFetch/30m.php +++ b/tests/docroot/Feed/NextFetch/30m.php @@ -4,7 +4,7 @@ Example title - http://example.com + http://localhost:8000/ Example description diff --git a/tests/docroot/Feed/NextFetch/36h.php b/tests/docroot/Feed/NextFetch/36h.php index 359ed9e9..251a456a 100644 --- a/tests/docroot/Feed/NextFetch/36h.php +++ b/tests/docroot/Feed/NextFetch/36h.php @@ -4,7 +4,7 @@ Example title - http://example.com + http://localhost:8000/ Example description diff --git a/tests/docroot/Feed/NextFetch/3h.php b/tests/docroot/Feed/NextFetch/3h.php index e2f5758d..dfdd3272 100644 --- a/tests/docroot/Feed/NextFetch/3h.php +++ b/tests/docroot/Feed/NextFetch/3h.php @@ -4,7 +4,7 @@ Example title - http://example.com + http://localhost:8000/ Example description diff --git a/tests/docroot/Feed/NextFetch/Fallback.php b/tests/docroot/Feed/NextFetch/Fallback.php index 04cc8ac0..d127c3a3 100644 --- a/tests/docroot/Feed/NextFetch/Fallback.php +++ b/tests/docroot/Feed/NextFetch/Fallback.php @@ -4,7 +4,7 @@ Example title - http://example.com + http://localhost:8000/ Example description diff --git a/tests/docroot/Feed/Parsing/Valid.php b/tests/docroot/Feed/Parsing/Valid.php index f56bd66b..ab953fc9 100644 --- a/tests/docroot/Feed/Parsing/Valid.php +++ b/tests/docroot/Feed/Parsing/Valid.php @@ -4,7 +4,7 @@ Test feed - http://example.com/ + http://localhost:8000/ Example newsfeed title diff --git a/tests/docroot/Feed/Parsing/XEEAttack.php b/tests/docroot/Feed/Parsing/XEEAttack.php index 12c4cbf7..a4fa7fe7 100644 --- a/tests/docroot/Feed/Parsing/XEEAttack.php +++ b/tests/docroot/Feed/Parsing/XEEAttack.php @@ -16,30 +16,30 @@ Test feed - http://example.com/ + http://localhost:8000/ Example newsfeed title urn:uuid:43fb1908-42ec-11e7-b61b-2b118faca2f2 - http://example.com/1 + http://localhost:8000/1 urn:uuid:4c8dbc84-42eb-11e7-9f61-6f83db96854f urn:uuid:43fb1908-42ec-11e7-b61b-2b118faca2f2 - http://example.com/1 + http://localhost:8000/1 urn:uuid:43fb1908-42ec-11e7-b61b-2b118faca2f2 - http://example.com/2 + http://localhost:8000/2 Example title Example content - + diff --git a/tests/docroot/Feed/Parsing/XXEAttack.php b/tests/docroot/Feed/Parsing/XXEAttack.php index 8a38e142..c1c51487 100644 --- a/tests/docroot/Feed/Parsing/XXEAttack.php +++ b/tests/docroot/Feed/Parsing/XXEAttack.php @@ -7,30 +7,30 @@ Test feed - http://example.com/ + http://localhost:8000/ &xxe; urn:uuid:43fb1908-42ec-11e7-b61b-2b118faca2f2 - http://example.com/1 + http://localhost:8000/1 urn:uuid:4c8dbc84-42eb-11e7-9f61-6f83db96854f urn:uuid:43fb1908-42ec-11e7-b61b-2b118faca2f2 - http://example.com/1 + http://localhost:8000/1 urn:uuid:43fb1908-42ec-11e7-b61b-2b118faca2f2 - http://example.com/2 + http://localhost:8000/2 Example title Example content - + diff --git a/tests/docroot/Feed/Scraping/Feed.php b/tests/docroot/Feed/Scraping/Feed.php index 71bf40ec..514dcfd0 100644 --- a/tests/docroot/Feed/Scraping/Feed.php +++ b/tests/docroot/Feed/Scraping/Feed.php @@ -4,7 +4,7 @@ Test feed - http://example.com/ + http://localhost:8000/ Example newsfeed title diff --git a/tests/docroot/Import/OPML/BrokenOPML.2.opml b/tests/docroot/Import/OPML/BrokenOPML.2.opml index ac70153f..691c2bd7 100644 --- a/tests/docroot/Import/OPML/BrokenOPML.2.opml +++ b/tests/docroot/Import/OPML/BrokenOPML.2.opml @@ -1,2 +1,2 @@ - + diff --git a/tests/docroot/Import/OPML/FeedsOnly.opml b/tests/docroot/Import/OPML/FeedsOnly.opml index 4e682600..88fab76d 100644 --- a/tests/docroot/Import/OPML/FeedsOnly.opml +++ b/tests/docroot/Import/OPML/FeedsOnly.opml @@ -1,10 +1,10 @@ - - - - - + + + + + diff --git a/tests/docroot/Import/some-feed.php b/tests/docroot/Import/some-feed.php index eec58567..7f48836f 100644 --- a/tests/docroot/Import/some-feed.php +++ b/tests/docroot/Import/some-feed.php @@ -4,7 +4,7 @@ Some feed - http://example.com/ + http://localhost:8000/ Just a generic feed diff --git a/tests/docroot/index.php b/tests/docroot/index.php new file mode 100644 index 00000000..4a6611e6 --- /dev/null +++ b/tests/docroot/index.php @@ -0,0 +1,4 @@ + 204, + 'content' => "", +]; diff --git a/tests/server.php b/tests/server.php index 2d738c95..2e7d8d47 100644 --- a/tests/server.php +++ b/tests/server.php @@ -41,6 +41,9 @@ $defaults = [ // default values for response ]; $url = explode("?", $_SERVER['REQUEST_URI'])[0]; +if ($url === "/") { + $url = "/index"; +} $base = BASE."tests".\DIRECTORY_SEPARATOR."docroot"; $test = $base.str_replace("/", \DIRECTORY_SEPARATOR, $url).".php"; if (!file_exists($test)) { From a4146ec129f9337ab7236897137b75ae9a19897e Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 11 Jan 2021 09:53:09 -0500 Subject: [PATCH 107/366] Start on test for filtering during feed parsing --- tests/cases/Feed/TestFeed.php | 14 +++++++ tests/docroot/Feed/Filtering/1.php | 61 ++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 tests/docroot/Feed/Filtering/1.php diff --git a/tests/cases/Feed/TestFeed.php b/tests/cases/Feed/TestFeed.php index cb94c5e7..a10a476a 100644 --- a/tests/cases/Feed/TestFeed.php +++ b/tests/cases/Feed/TestFeed.php @@ -95,6 +95,8 @@ class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { self::clearData(); self::setConf(); Arsse::$db = \Phake::mock(Database::class); + \Phake::when(Arsse::$db)->feedMatchLatest->thenReturn(new Result([])); + \Phake::when(Arsse::$db)->feedMatchIds->thenReturn(new Result([])); \Phake::when(Arsse::$db)->feedRulesGet->thenReturn([]); } @@ -377,4 +379,16 @@ class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame("image/gif", $f->iconType); $this->assertSame($d, $f->iconData); } + + public function testApplyFilterRules(): void { + \Phake::when(Arsse::$db)->feedMatchIds->thenReturn(new Result([ + ['id' => 7, 'guid' => '0f2a218c311e3d8105f1b075142a5d26dabf056ffc61abe77e96c8f071bbf4a7', 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], + ['id' => 47, 'guid' => '1c19e3b9018bc246b7414ae919ddebc88d0c575129e8c4a57b84b826c00f6db5', 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], + ['id' => 2112, 'guid' => '964db0b9292ad0c7a6c225f2e0966f3bda53486fae65db0310c97409974e65b8', 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], + ['id' => 1, 'guid' => '436070cda5713a0d9a8fdc8652c7ab142f0550697acfd5206a16c18aee355039', 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], + ['id' => 42, 'guid' => '1a731433a1904220ef26e731ada7262e1d5bcecae53e7b5df9e1f5713af6e5d3', 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], + ])); + $f = new Feed(null, $this->base."Filtering/1"); + $this->markTestIncomplete(); + } } diff --git a/tests/docroot/Feed/Filtering/1.php b/tests/docroot/Feed/Filtering/1.php new file mode 100644 index 00000000..d7a1d222 --- /dev/null +++ b/tests/docroot/Feed/Filtering/1.php @@ -0,0 +1,61 @@ + "application/atom+xml", + 'content' => << + Example feed title + urn:uuid:0fd8f6d8-43df-11e7-8511-9b59a0324eb8 + + + + urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89790 + A + Z + + + urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89791 + B + Y + + + urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89792 + C + X + + + urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89793 + D + W + + + urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89794 + E + V + + + urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89795 + F + U + + + urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89796 + T + Z + + + urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89797 + S + Z + + + urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89798 + R + Z + + + urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89799 + Q + Z + + +MESSAGE_BODY +]; From 097362881b6f62197f619984a411a5de6783a032 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 11 Jan 2021 23:12:43 -0500 Subject: [PATCH 108/366] Tests for filtering during feed parsing --- lib/Feed.php | 2 +- tests/cases/Feed/TestFeed.php | 23 ++++++++++++++++------- tests/docroot/Feed/Filtering/1.php | 16 ++++++++-------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/lib/Feed.php b/lib/Feed.php index b0e91290..e96d064c 100644 --- a/lib/Feed.php +++ b/lib/Feed.php @@ -286,7 +286,7 @@ class Feed { $articles = Arsse::$db->feedMatchLatest($feedID, sizeof($items))->getAll(); // perform a first pass matching the latest articles against items in the feed [$this->newItems, $this->changedItems] = $this->matchItems($items, $articles); - if (sizeof($this->newItems) && sizeof($items) <= sizeof($articles)) { + if (sizeof($this->newItems)) { // if we need to, perform a second pass on the database looking specifically for IDs and hashes of the new items $ids = $hashesUT = $hashesUC = $hashesTC = []; foreach ($this->newItems as $i) { diff --git a/tests/cases/Feed/TestFeed.php b/tests/cases/Feed/TestFeed.php index a10a476a..51799940 100644 --- a/tests/cases/Feed/TestFeed.php +++ b/tests/cases/Feed/TestFeed.php @@ -382,13 +382,22 @@ class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { public function testApplyFilterRules(): void { \Phake::when(Arsse::$db)->feedMatchIds->thenReturn(new Result([ - ['id' => 7, 'guid' => '0f2a218c311e3d8105f1b075142a5d26dabf056ffc61abe77e96c8f071bbf4a7', 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], - ['id' => 47, 'guid' => '1c19e3b9018bc246b7414ae919ddebc88d0c575129e8c4a57b84b826c00f6db5', 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], - ['id' => 2112, 'guid' => '964db0b9292ad0c7a6c225f2e0966f3bda53486fae65db0310c97409974e65b8', 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], - ['id' => 1, 'guid' => '436070cda5713a0d9a8fdc8652c7ab142f0550697acfd5206a16c18aee355039', 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], - ['id' => 42, 'guid' => '1a731433a1904220ef26e731ada7262e1d5bcecae53e7b5df9e1f5713af6e5d3', 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], + // these are the sixth through tenth entries in the feed; the title hashes have been omitted for brevity + ['id' => 7, 'guid' => '0f2a218c311e3d8105f1b075142a5d26dabf056ffc61abe77e96c8f071bbf4a7', 'edited' => null, 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], + ['id' => 47, 'guid' => '1c19e3b9018bc246b7414ae919ddebc88d0c575129e8c4a57b84b826c00f6db5', 'edited' => null, 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], + ['id' => 2112, 'guid' => '964db0b9292ad0c7a6c225f2e0966f3bda53486fae65db0310c97409974e65b8', 'edited' => null, 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], + ['id' => 1, 'guid' => '436070cda5713a0d9a8fdc8652c7ab142f0550697acfd5206a16c18aee355039', 'edited' => null, 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], + ['id' => 42, 'guid' => '1a731433a1904220ef26e731ada7262e1d5bcecae53e7b5df9e1f5713af6e5d3', 'edited' => null, 'url_title_hash' => "", 'url_content_hash' => '', 'title_content_hash' => ''], ])); - $f = new Feed(null, $this->base."Filtering/1"); - $this->markTestIncomplete(); + \Phake::when(Arsse::$db)->feedRulesGet->thenReturn([ + 'jack' => ['keep' => "", 'block' => '`A|W|J|S`u'], + 'sam' => ['keep' => "`B|T|X`u", 'block' => '`C`u'], + ]); + $f = new Feed(5, $this->base."Filtering/1"); + $exp = [ + 'jack' => ['new' => [false, true, true, false, true], 'changed' => [7 => true, 47 => true, 2112 => false, 1 => true, 42 => false]], + 'sam' => ['new' => [false, true, false, false, false], 'changed' => [7 => false, 47 => true, 2112 => false, 1 => false, 42 => false]], + ]; + $this->assertSame($exp, $f->filteredItems); } } diff --git a/tests/docroot/Feed/Filtering/1.php b/tests/docroot/Feed/Filtering/1.php index d7a1d222..311ac578 100644 --- a/tests/docroot/Feed/Filtering/1.php +++ b/tests/docroot/Feed/Filtering/1.php @@ -38,23 +38,23 @@ urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89796 - T - Z + G + T urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89797 - S - Z + H + S urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89798 - R - Z + I + R urn:uuid:6d4c7964-43e1-11e7-92bd-4fed65d89799 - Q - Z + J + Q MESSAGE_BODY From 7a6186f2d77a4f1751eefac6a0b30ec85546168e Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 13 Jan 2021 14:43:29 -0500 Subject: [PATCH 109/366] Update Miniflux documentation --- docs/en/030_Supported_Protocols/005_Miniflux.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index b29c0825..6a063bfa 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -15,7 +15,7 @@ The Miniflux protocol is a fairly well-designed protocol supporting a wide variety of operations on newsfeeds, folders (termed "categories"), and articles; it also allows for user administration, and native OPML importing and exporting. Architecturally it is similar to the Nextcloud News protocol, but is generally more efficient and has more capabilities. -Miniflux version 2.0.26 is emulated, though not all features are implemented +Miniflux version 2.0.27 is emulated, though not all features are implemented # Missing features @@ -39,7 +39,7 @@ Miniflux version 2.0.26 is emulated, though not all features are implemented The Miniflux documentation gives only a brief example of a pattern for its filtering rules; the allowed syntax is described in full [in Google's documentation for RE2](https://github.com/google/re2/wiki/Syntax). Being a PHP application, The Arsse instead accepts [PCRE syntax](http://www.pcre.org/original/doc/html/pcresyntax.html) (or since PHP 7.3 [PCRE2 syntax](https://www.pcre.org/current/doc/html/pcre2syntax.html)), specifically in UTF-8 mode. Delimiters should not be included, and slashes should not be escaped; anchors may be used if desired. For example `^(?i)RE/MAX$` is a valid pattern. -For convenience the patterns are tested after collapsing whitespace. Unlike Miniflux, The Arsse tests the patterns against an article's author-supplied categories if they do not match its title. +For convenience the patterns are tested after collapsing whitespace. Unlike Miniflux, The Arsse tests the patterns against an article's author-supplied categories if they do not match its title. Also unlike Miniflux, when filter rules are modified they are re-evaluated against all applicable articles immediately. # Special handling of the "All" category From 618fd67f8024333e6abdfcc556f87b81d21ec9b9 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 13 Jan 2021 14:54:22 -0500 Subject: [PATCH 110/366] Set marks for filtered articles on feed refresh --- lib/Database.php | 50 ++++++++++++++++++++++++++--- tests/cases/Database/SeriesFeed.php | 30 +++++++++-------- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 5e44d384..d60a0281 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1118,8 +1118,9 @@ class Database { $icon = $this->db->prepare("INSERT INTO arsse_icons(url, type, data) values(?, ?, ?)", "str", "str", "blob")->run($feed->iconUrl, $feed->iconType, $feed->iconData)->lastId(); } } - // actually perform updates - foreach ($feed->newItems as $article) { + $articleMap = []; + // actually perform updates, starting with inserting new articles + foreach ($feed->newItems as $k => $article) { $articleID = $qInsertArticle->run( $article->url, $article->title, @@ -1133,14 +1134,20 @@ class Database { $article->titleContentHash, $feedID )->lastId(); + // note the new ID for later use + $articleMap[$k] = $articleID; + // insert any enclosures if ($article->enclosureUrl) { $qInsertEnclosure->run($articleID, $article->enclosureUrl, $article->enclosureType); } + // insert any categories foreach ($article->categories as $c) { $qInsertCategory->run($articleID, $c); } + // assign a new edition ID to the article $qInsertEdition->run($articleID); } + // next update existing artricles which have been edited foreach ($feed->changedItems as $articleID => $article) { $qUpdateArticle->run( $article->url, @@ -1155,6 +1162,7 @@ class Database { $article->titleContentHash, $articleID ); + // delete all enclosures and categories and re-insert them $qDeleteEnclosures->run($articleID); $qDeleteCategories->run($articleID); if ($article->enclosureUrl) { @@ -1163,9 +1171,33 @@ class Database { foreach ($article->categories as $c) { $qInsertCategory->run($articleID, $c); } + // assign a new edition ID to this version of the article $qInsertEdition->run($articleID); $qClearReadMarks->run($articleID); } + // hide or unhide any filtered articles + foreach ($feed->filteredItems as $user => $filterData) { + $hide = []; + $unhide = []; + foreach ($filterData['new'] as $index => $keep) { + if (!$keep) { + $hide[] = $articleMap[$index]; + } + } + foreach ($filterData['changed'] as $article => $keep) { + if (!$keep) { + $hide[] = $article; + } else { + $unhide[] = $article; + } + } + if ($hide) { + $this->articleMark($user, ['hidden' => true], (new Context)->articles($hide), false); + } + if ($unhide) { + $this->articleMark($user, ['hidden' => false], (new Context)->articles($unhide), false); + } + } // lastly update the feed database itself with updated information. $this->db->prepareArray( "UPDATE arsse_feeds SET title = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ?, icon = ? WHERE id = ?", @@ -1693,8 +1725,9 @@ class Database { * @param string $user The user who owns the articles to be modified * @param array $data An associative array of properties to modify. Anything not specified will remain unchanged * @param Context $context The query context to match articles against + * @param bool $updateTimestamp Whether to also update the timestamp. This should only be false if a mark is changed as a result of an automated action not taken by the user */ - public function articleMark(string $user, array $data, Context $context = null): int { + public function articleMark(string $user, array $data, Context $context = null, bool $updateTimestamp = true): int { $data = [ 'read' => $data['read'] ?? null, 'starred' => $data['starred'] ?? null, @@ -1743,7 +1776,11 @@ class Database { $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); } // finally set the modification date for all touched marks and return the number of affected marks - $out = $this->db->query("UPDATE arsse_marks set modified = CURRENT_TIMESTAMP, touched = 0 where touched = 1")->changes(); + if ($updateTimestamp) { + $out = $this->db->query("UPDATE arsse_marks set modified = CURRENT_TIMESTAMP, touched = 0 where touched = 1")->changes(); + } else { + $out = $this->db->query("UPDATE arsse_marks set touched = 0 where touched = 1")->changes(); + } } else { if (!isset($data['read']) && ($context->edition() || $context->editions())) { // get the articles associated with the requested editions @@ -1763,7 +1800,10 @@ class Database { return isset($v); }); [$set, $setTypes, $setValues] = $this->generateSet($data, ['read' => "bool", 'starred' => "bool", 'hidden' => "bool", 'note' => "str"]); - $q->setBody("UPDATE arsse_marks set $set, modified = CURRENT_TIMESTAMP where article in(select article from target_articles) and subscription in(select distinct subscription from target_articles)", $setTypes, $setValues); + if ($updateTimestamp) { + $set .= ", modified = CURRENT_TIMESTAMP"; + } + $q->setBody("UPDATE arsse_marks set $set where article in(select article from target_articles) and subscription in(select distinct subscription from target_articles)", $setTypes, $setValues); $out = $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes(); } $tr->commit(); diff --git a/tests/cases/Database/SeriesFeed.php b/tests/cases/Database/SeriesFeed.php index 65a2931f..5cc0d84c 100644 --- a/tests/cases/Database/SeriesFeed.php +++ b/tests/cases/Database/SeriesFeed.php @@ -80,7 +80,7 @@ trait SeriesFeed { [3,'john.doe@example.com',3,'\w+',null], [4,'john.doe@example.com',4,'\w+',"["], // invalid rule leads to both rules being ignored [5,'john.doe@example.com',5,null,'and/or'], - [6,'jane.doe@example.com',1,'^(?i)[a-z]+','bluberry'], + [6,'jane.doe@example.com',1,'^(?i)[a-z]+','3|6'], ], ], 'arsse_articles' => [ @@ -129,19 +129,20 @@ trait SeriesFeed { 'subscription' => "int", 'read' => "bool", 'starred' => "bool", + 'hidden' => "bool", 'modified' => "datetime", ], 'rows' => [ // Jane's marks - [1,6,1,0,$past], - [2,6,1,0,$past], - [3,6,1,1,$past], - [4,6,1,0,$past], - [5,6,1,1,$past], + [1,6,1,0,0,$past], + [2,6,1,0,0,$past], + [3,6,1,1,0,$past], + [4,6,1,0,1,$past], + [5,6,1,1,0,$past], // John's marks - [1,1,1,0,$past], - [3,1,1,0,$past], - [4,1,0,1,$past], + [1,1,1,0,0,$past], + [3,1,1,0,0,$past], + [4,1,0,1,0,$past], ], ], 'arsse_enclosures' => [ @@ -210,7 +211,7 @@ trait SeriesFeed { public function provideFilterRules(): iterable { return [ - [1, ['jane.doe@example.com' => ['keep' => "`^(?i)[a-z]+`u", 'block' => "`bluberry`u"], 'john.doe@example.com' => ['keep' => "", 'block' => "`^Sport$`u"]]], + [1, ['jane.doe@example.com' => ['keep' => "`^(?i)[a-z]+`u", 'block' => "`3|6`u"], 'john.doe@example.com' => ['keep' => "", 'block' => "`^Sport$`u"]]], [2, []], [3, ['john.doe@example.com' => ['keep' => '`\w+`u', 'block' => ""]]], [4, []], @@ -225,7 +226,7 @@ trait SeriesFeed { $state = $this->primeExpectations($this->data, [ 'arsse_articles' => ["id", "feed","url","title","author","published","edited","content","guid","url_title_hash","url_content_hash","title_content_hash","modified"], 'arsse_editions' => ["id","article","modified"], - 'arsse_marks' => ["subscription","article","read","starred","modified"], + 'arsse_marks' => ["subscription","article","read","starred","hidden","modified"], 'arsse_feeds' => ["id","size"], ]); $state['arsse_articles']['rows'][2] = [3,1,'http://example.com/3','Article title 3 (updated)','','2000-01-03 00:00:00','2000-01-03 00:00:00','

Article content 3

','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','6cc99be662ef3486fef35a890123f18d74c29a32d714802d743c5b4ef713315a','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','d5faccc13bf8267850a1e8e61f95950a0f34167df2c8c58011c0aaa6367026ac',$now]; @@ -236,9 +237,10 @@ trait SeriesFeed { [7,3,$now], [8,4,$now], ]); - $state['arsse_marks']['rows'][2] = [6,3,0,1,$now]; - $state['arsse_marks']['rows'][3] = [6,4,0,0,$now]; - $state['arsse_marks']['rows'][6] = [1,3,0,0,$now]; + $state['arsse_marks']['rows'][2] = [6,3,0,1,1,$now]; + $state['arsse_marks']['rows'][3] = [6,4,0,0,0,$now]; + $state['arsse_marks']['rows'][6] = [1,3,0,0,0,$now]; + $state['arsse_marks']['rows'][] = [6,8,0,0,1,null]; $state['arsse_feeds']['rows'][0] = [1,6]; $this->compareExpectations(static::$drv, $state); // update a valid feed which previously had an error From 9f2b8d4f8333ddb32e6a9d0cdfcfb8146e73fdcd Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 14 Jan 2021 12:42:33 -0500 Subject: [PATCH 111/366] Imprement setting of filter rules --- lib/AbstractException.php | 1 + lib/Database.php | 132 +++++++++++++++----- locale/en.php | 1 + tests/cases/Database/SeriesSubscription.php | 52 ++++---- 4 files changed, 131 insertions(+), 55 deletions(-) diff --git a/lib/AbstractException.php b/lib/AbstractException.php index 5a575c34..b6696c92 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -46,6 +46,7 @@ abstract class AbstractException extends \Exception { "Db/Exception.savepointStale" => 10227, "Db/Exception.resultReused" => 10228, "Db/ExceptionRetry.schemaChange" => 10229, + "Db/ExceptionInput.invalidValue" => 10230, "Db/ExceptionInput.missing" => 10231, "Db/ExceptionInput.whitespace" => 10232, "Db/ExceptionInput.tooLong" => 10233, diff --git a/lib/Database.php b/lib/Database.php index d60a0281..255c2acd 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -754,6 +754,28 @@ class Database { } /** Lists a user's subscriptions, returning various data + * + * Each record has the following keys: + * + * - "id": The numeric identifier of the subscription + * - "feed": The numeric identifier of the underlying newsfeed + * - "url": The URL of the newsfeed, after discovery and HTTP redirects + * - "title": The title of the newsfeed + * - "source": The URL of the source of the newsfeed i.e. its parent Web site + * - "favicon": The URL of an icon representing the newsfeed or its source + * - "folder": The numeric identifier (or null) of the subscription's folder + * - "top_folder": The numeric identifier (or null) of the top-level folder for the subscription + * - "pinned": Whether the subscription is pinned + * - "err_count": The count of times attempting to refresh the newsfeed has resulted in an error since the last successful retrieval + * - "err_msg": The error message of the last unsuccessful retrieval + * - "order_type": Whether articles should be sorted in reverse cronological order (2), chronological order (1), or the default (0) + * - "keep_rule": The subscription's "keep" filter rule; articles which do not match this are hidden + * - "block_rule": The subscription's "block" filter rule; articles which match this are hidden + * - "added": The date and time at which the subscription was added + * - "updated": The date and time at which the newsfeed was last updated in the database + * - "edited": The date and time at which the newsfeed was last modified by its authors + * - "modified": The date and time at which the subscription properties were last changed by the user + * - "unread": The number of unread articles associated with the subscription * * @param string $user The user whose subscriptions are to be listed * @param integer|null $folder The identifier of the folder under which to list subscriptions; by default the root folder is used @@ -817,7 +839,11 @@ class Database { return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); } - /** Returns the number of subscriptions in a folder, counting recursively */ + /** Returns the number of subscriptions in a folder, counting recursively + * + * @param string $user The user whose subscriptions are to be counted + * @param integer|null $folder The identifier of the folder under which to count subscriptions; by default the root folder is used + */ public function subscriptionCount(string $user, $folder = null): int { // validate inputs $folder = $this->folderValidateId($user, $folder)['id']; @@ -851,24 +877,7 @@ class Database { return true; } - /** Retrieves data about a particular subscription, as an associative array with the following keys: - * - * - "id": The numeric identifier of the subscription - * - "feed": The numeric identifier of the underlying newsfeed - * - "url": The URL of the newsfeed, after discovery and HTTP redirects - * - "title": The title of the newsfeed - * - "favicon": The URL of an icon representing the newsfeed or its source - * - "source": The URL of the source of the newsfeed i.e. its parent Web site - * - "folder": The numeric identifier (or null) of the subscription's folder - * - "top_folder": The numeric identifier (or null) of the top-level folder for the subscription - * - "pinned": Whether the subscription is pinned - * - "err_count": The count of times attempting to refresh the newsfeed has resulted in an error since the last successful retrieval - * - "err_msg": The error message of the last unsuccessful retrieval - * - "order_type": Whether articles should be sorted in reverse cronological order (2), chronological order (1), or the default (0) - * - "added": The date and time at which the subscription was added - * - "updated": The date and time at which the newsfeed was last updated (not when it was last refreshed) - * - "unread": The number of unread articles associated with the subscription - */ + /** Retrieves data about a particular subscription, as an associative array; see subscriptionList for details */ public function subscriptionPropertiesGet(string $user, $id): array { if (!V::id($id)) { throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'type' => "int > 0"]); @@ -884,10 +893,12 @@ class Database { * * The $data array must contain one or more of the following keys: * - * - "title": The title of the newsfeed + * - "title": The title of the subscription * - "folder": The numeric identifier (or null) of the subscription's folder * - "pinned": Whether the subscription is pinned * - "order_type": Whether articles should be sorted in reverse cronological order (2), chronological order (1), or the default (0) + * - "keep_rule": The subscription's "keep" filter rule; articles which do not match this are hidden + * - "block_rule": The subscription's "block" filter rule; articles which match this are hidden * * @param string $user The user whose subscription is to be modified * @param integer $id the numeric identifier of the subscription to modfify @@ -896,29 +907,45 @@ class Database { public function subscriptionPropertiesSet(string $user, $id, array $data): bool { $tr = $this->db->begin(); // validate the ID - $id = $this->subscriptionValidateId($user, $id, true)['id']; + $id = (int) $this->subscriptionValidateId($user, $id, true)['id']; if (array_key_exists("folder", $data)) { // ensure the target folder exists and belong to the user $data['folder'] = $this->folderValidateId($user, $data['folder'])['id']; } - if (array_key_exists("title", $data)) { + if (isset($data['title'])) { // if the title is null, this signals intended use of the default title; otherwise make sure it's not effectively an empty string - if (!is_null($data['title'])) { - $info = V::str($data['title']); - if ($info & V::EMPTY) { - throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "title"]); - } elseif ($info & V::WHITE) { - throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "title"]); - } elseif (!($info & V::VALID)) { - throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "title", 'type' => "string"]); - } + $info = V::str($data['title']); + if ($info & V::EMPTY) { + throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "title"]); + } elseif ($info & V::WHITE) { + throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "title"]); + } elseif (!($info & V::VALID)) { + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "title", 'type' => "string"]); } } + // validate any filter rules + if (isset($data['keep_rule'])) { + if (!is_string($data['keep_rule'])) { + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "keep_rule", 'type' => "string"]); + } elseif (!Rule::validate($data['keep_rule'])) { + throw new Db\ExceptionInput("invalidValue", ["action" => __FUNCTION__, "field" => "keep_rule"]); + } + } + if (isset($data['block_rule'])) { + if (!is_string($data['block_rule'])) { + throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "block_rule", 'type' => "string"]); + } elseif (!Rule::validate($data['block_rule'])) { + throw new Db\ExceptionInput("invalidValue", ["action" => __FUNCTION__, "field" => "block_rule"]); + } + } + // perform the update $valid = [ 'title' => "str", 'folder' => "int", 'order_type' => "strict int", 'pinned' => "strict bool", + 'keep_rule' => "str", + 'block_rule' => "str", ]; [$setClause, $setTypes, $setValues] = $this->generateSet($data, $valid); if (!$setClause) { @@ -927,6 +954,10 @@ class Database { } $out = (bool) $this->db->prepare("UPDATE arsse_subscriptions set $setClause, modified = CURRENT_TIMESTAMP where owner = ? and id = ?", $setTypes, "str", "int")->run($setValues, $user, $id)->changes(); $tr->commit(); + // if filter rules were changed, apply them + if (array_key_exists("keep_rule", $data) || array_key_exists("block_rule", $data)) { + $this->subscriptionRulesApply($user, $id); + } return $out; } @@ -984,6 +1015,45 @@ class Database { return V::normalize($out, V::T_DATE | V::M_NULL, "sql"); } + /** Evalutes the filter rules specified for a subscription against every article associated with the subscription's feed + * + * @param string $user The user who owns the subscription + * @param integer $id The identifier of the subscription whose rules are to be evaluated + */ + protected function subscriptionRulesApply(string $user, int $id): void { + $sub = $this->db->prepare("SELECT feed, coalesce(keep_rule, '') as keep, coalesce(block_rule, '') as block from arsse_subscriptions where owner = ? and id = ?", "str", "int")->run($user, $id)->getRow(); + try { + $keep = Rule::prep($sub['keep']); + $block = Rule::prep($sub['block']); + $feed = $sub['feed']; + } catch (RuleException $e) { + // invalid rules should not normally appear in the database, but it's possible + // in this case we should halt evaluation and just leave things as they are + return; + } + $articles = $this->db->prepare("SELECT id, title, coalesce(categories, 0) as categories from arsse_articles as a join (select article, count(*) as categories from arsse_categories group by article) as c on a.id = c.article where a.feed = ?", "int")->run($feed)->getAll(); + $hide = []; + $unhide = []; + foreach ($articles as $r) { + // retrieve the list of categories if the article has any + $categories = $r['categories'] ? $this->articleCategoriesGet($user, $r['id']) : []; + // evaluate the rule for the article + if (Rule::apply($keep, $block, $r['title'], $categories)) { + $unhide[] = $r['id']; + } else { + $hide[] = $r['id']; + } + } + // apply any marks + if ($hide) { + $this->articleMark($user, ['hidden' => true], (new Context)->articles($hide), false); + } + if ($unhide) { + $this->articleMark($user, ['hidden' => false], (new Context)->articles($unhide), false); + } + } + + /** Ensures the specified subscription exists and raises an exception otherwise * * Returns an associative array containing the id of the subscription and the id of the underlying newsfeed diff --git a/locale/en.php b/locale/en.php index 1927eaf7..b8098959 100644 --- a/locale/en.php +++ b/locale/en.php @@ -138,6 +138,7 @@ return [ // indicates programming error 'Exception.JKingWeb/Arsse/Db/Exception.resultReused' => 'Result set already iterated', 'Exception.JKingWeb/Arsse/Db/ExceptionRetry.schemaChange' => '{0}', + 'Exception.JKingWeb/Arsse/Db/ExceptionInput.invalidValue' => 'Value of field "{field}" of action "{action}" is invalid', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.missing' => 'Required field "{field}" missing while performing action "{action}"', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.whitespace' => 'Field "{field}" of action "{action}" may not contain only whitespace', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooLong' => 'Field "{field}" of action "{action}" has a maximum length of {max}', diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index 7fe700a8..4ae83439 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -77,12 +77,14 @@ trait SeriesSubscription { 'folder' => "int", 'pinned' => "bool", 'order_type' => "int", + 'keep_rule' => "str", + 'block_rule' => "str", ], 'rows' => [ - [1,"john.doe@example.com",2,null,null,1,2], - [2,"jane.doe@example.com",2,null,null,0,0], - [3,"john.doe@example.com",3,"Ook",2,0,1], - [4,"jill.doe@example.com",2,null,null,0,0], + [1,"john.doe@example.com",2,null,null,1,2,null,null], + [2,"jane.doe@example.com",2,null,null,0,0,null,null], + [3,"john.doe@example.com",3,"Ook",2,0,1,null,null], + [4,"jill.doe@example.com",2,null,null,0,0,null,null], ], ], 'arsse_tags' => [ @@ -369,17 +371,21 @@ trait SeriesSubscription { 'folder' => 3, 'pinned' => false, 'order_type' => 0, + 'keep_rule' => "ook", + 'block_rule' => "eek", ]); $state = $this->primeExpectations($this->data, [ 'arsse_feeds' => ['id','url','username','password','title'], - 'arsse_subscriptions' => ['id','owner','feed','title','folder','pinned','order_type'], + 'arsse_subscriptions' => ['id','owner','feed','title','folder','pinned','order_type','keep_rule','block_rule'], ]); - $state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,"Ook Ook",3,0,0]; + $state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,"Ook Ook",3,0,0,"ook","eek"]; $this->compareExpectations(static::$drv, $state); Arsse::$db->subscriptionPropertiesSet($this->user, 1, [ - 'title' => null, + 'title' => null, + 'keep_rule' => null, + 'block_rule' => null, ]); - $state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,null,3,0,0]; + $state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,null,3,0,0,null,null]; $this->compareExpectations(static::$drv, $state); // making no changes is a valid result Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['unhinged' => true]); @@ -395,30 +401,28 @@ trait SeriesSubscription { $this->assertTrue(Arsse::$db->subscriptionPropertiesSet($this->user, 3, ['folder' => null])); } - public function testRenameASubscriptionToABlankTitle(): void { - $this->assertException("missing", "Db", "ExceptionInput"); - Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => ""]); + /** @dataProvider provideInvalidSubscriptionProperties */ + public function testSetThePropertiesOfASubscriptionToInvalidValues(array $data, string $exp): void { + $this->assertException($exp, "Db", "ExceptionInput"); + Arsse::$db->subscriptionPropertiesSet($this->user, 1, $data); } - public function testRenameASubscriptionToAWhitespaceTitle(): void { - $this->assertException("whitespace", "Db", "ExceptionInput"); - Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => " "]); - } - - public function testRenameASubscriptionToFalse(): void { - $this->assertException("typeViolation", "Db", "ExceptionInput"); - Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => false]); + public function provideInvalidSubscriptionProperties(): iterable { + return [ + 'Empty title' => [['title' => ""], "missing"], + 'Whitespace title' => [['title' => " "], "whitespace"], + 'Non-string title' => [['title' => []], "typeViolation"], + 'Non-string keep rule' => [['keep_rule' => 0], "typeViolation"], + 'Invalid keep rule' => [['keep_rule' => "*"], "invalidValue"], + 'Non-string block rule' => [['block_rule' => 0], "typeViolation"], + 'Invalid block rule' => [['block_rule' => "*"], "invalidValue"], + ]; } public function testRenameASubscriptionToZero(): void { $this->assertTrue(Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => 0])); } - public function testRenameASubscriptionToAnArray(): void { - $this->assertException("typeViolation", "Db", "ExceptionInput"); - Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => []]); - } - public function testSetThePropertiesOfAMissingSubscription(): void { $this->assertException("subjectMissing", "Db", "ExceptionInput"); Arsse::$db->subscriptionPropertiesSet($this->user, 2112, ['folder' => null]); From 2536c9fe03207f6ce93c94e1b4823d15c4a86a08 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 15 Jan 2021 23:02:33 -0500 Subject: [PATCH 112/366] Last tests for article filters --- lib/Database.php | 10 +-- tests/cases/Database/SeriesSubscription.php | 79 ++++++++++++++++----- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 255c2acd..1424bccb 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1026,17 +1026,17 @@ class Database { $keep = Rule::prep($sub['keep']); $block = Rule::prep($sub['block']); $feed = $sub['feed']; - } catch (RuleException $e) { + } catch (RuleException $e) { // @codeCoverageIgnore // invalid rules should not normally appear in the database, but it's possible // in this case we should halt evaluation and just leave things as they are - return; + return; // @codeCoverageIgnore } - $articles = $this->db->prepare("SELECT id, title, coalesce(categories, 0) as categories from arsse_articles as a join (select article, count(*) as categories from arsse_categories group by article) as c on a.id = c.article where a.feed = ?", "int")->run($feed)->getAll(); + $articles = $this->db->prepare("SELECT id, title, coalesce(categories, 0) as categories from arsse_articles as a left join (select article, count(*) as categories from arsse_categories group by article) as c on a.id = c.article where a.feed = ?", "int")->run($feed)->getAll(); $hide = []; $unhide = []; foreach ($articles as $r) { // retrieve the list of categories if the article has any - $categories = $r['categories'] ? $this->articleCategoriesGet($user, $r['id']) : []; + $categories = $r['categories'] ? $this->articleCategoriesGet($user, (int) $r['id']) : []; // evaluate the rule for the article if (Rule::apply($keep, $block, $r['title'], $categories)) { $unhide[] = $r['id']; @@ -2006,7 +2006,7 @@ class Database { FROM arsse_articles join arsse_subscriptions on arsse_subscriptions.feed = arsse_articles.feed WHERE arsse_articles.id = ? and arsse_subscriptions.owner = ? - ) as articles join arsse_editions on arsse_editions.article = articles.article group by articles.article", + ) as articles left join arsse_editions on arsse_editions.article = articles.article group by articles.article", ["int", "str"] )->run($id, $user)->getRow(); if (!$out) { diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index 4ae83439..abbdab39 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -24,6 +24,7 @@ trait SeriesSubscription { ["jane.doe@example.com", "", 1], ["john.doe@example.com", "", 2], ["jill.doe@example.com", "", 3], + ["jack.doe@example.com", "", 4], ], ], 'arsse_folders' => [ @@ -85,6 +86,7 @@ trait SeriesSubscription { [2,"jane.doe@example.com",2,null,null,0,0,null,null], [3,"john.doe@example.com",3,"Ook",2,0,1,null,null], [4,"jill.doe@example.com",2,null,null,0,0,null,null], + [5,"jack.doe@example.com",2,null,null,1,2,"","3|E"], ], ], 'arsse_tags' => [ @@ -121,16 +123,48 @@ trait SeriesSubscription { 'url_title_hash' => "str", 'url_content_hash' => "str", 'title_content_hash' => "str", + 'title' => "str", ], 'rows' => [ - [1,2,"","",""], - [2,2,"","",""], - [3,2,"","",""], - [4,2,"","",""], - [5,2,"","",""], - [6,3,"","",""], - [7,3,"","",""], - [8,3,"","",""], + [1,2,"","","","Title 1"], + [2,2,"","","","Title 2"], + [3,2,"","","","Title 3"], + [4,2,"","","","Title 4"], + [5,2,"","","","Title 5"], + [6,3,"","","","Title 6"], + [7,3,"","","","Title 7"], + [8,3,"","","","Title 8"], + ], + ], + 'arsse_editions' => [ + 'columns' => [ + 'id' => "int", + 'article' => "int", + ], + 'rows' => [ + [1,1], + [2,2], + [3,3], + [4,4], + [5,5], + [6,6], + [7,7], + [8,8], + ], + ], + 'arsse_categories' => [ + 'columns' => [ + 'article' => "int", + 'name' => "str", + ], + 'rows' => [ + [1,"A"], + [2,"B"], + [4,"D"], + [5,"E"], + [6,"F"], + [7,"G"], + [8,"H"], ], ], 'arsse_marks' => [ @@ -139,16 +173,21 @@ trait SeriesSubscription { 'subscription' => "int", 'read' => "bool", 'starred' => "bool", + 'hidden' => "bool", ], 'rows' => [ - [1,2,1,0], - [2,2,1,0], - [3,2,1,0], - [4,2,1,0], - [5,2,1,0], - [1,1,1,0], - [7,3,1,0], - [8,3,0,0], + [1,2,1,0,0], + [2,2,1,0,0], + [3,2,1,0,0], + [4,2,1,0,0], + [5,2,1,0,0], + [1,1,1,0,0], + [7,3,1,0,0], + [8,3,0,0,0], + [1,5,1,0,0], + [3,5,1,0,1], + [4,5,0,0,0], + [5,5,0,0,1], ], ], ]; @@ -483,4 +522,12 @@ trait SeriesSubscription { $this->assertException("subjectMissing", "Db", "ExceptionInput"); $this->assertTime(strtotime("now - 1 hour"), Arsse::$db->subscriptionRefreshed("john.doe@example.com", 2)); } + + public function testSetTheFilterRulesOfASubscriptionCheckingMarks(): void { + Arsse::$db->subscriptionPropertiesSet("jack.doe@example.com", 5, ['keep_rule' => "1|B|3|D", 'block_rule' => "4"]); + $state = $this->primeExpectations($this->data, ['arsse_marks' => ['article', 'subscription', 'hidden']]); + $state['arsse_marks']['rows'][9][2] = 0; + $state['arsse_marks']['rows'][10][2] = 1; + $this->compareExpectations(static::$drv, $state); + } } From e74b44cc397ada29c70b2d647ce4afa511ecbbff Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 15 Jan 2021 23:15:22 -0500 Subject: [PATCH 113/366] Change favicon to icon_url and add icon_id --- lib/Database.php | 6 ++++-- lib/REST/NextcloudNews/V1_2.php | 2 +- lib/REST/TinyTinyRSS/API.php | 6 +++--- tests/cases/ImportExport/TestOPML.php | 12 ++++++------ tests/cases/REST/Fever/TestAPI.php | 6 +++--- tests/cases/REST/NextcloudNews/TestV1_2.php | 6 +++--- tests/cases/REST/TinyTinyRSS/TestAPI.php | 12 ++++++------ 7 files changed, 26 insertions(+), 24 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 1424bccb..6e72cf6f 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -762,7 +762,8 @@ class Database { * - "url": The URL of the newsfeed, after discovery and HTTP redirects * - "title": The title of the newsfeed * - "source": The URL of the source of the newsfeed i.e. its parent Web site - * - "favicon": The URL of an icon representing the newsfeed or its source + * - "icon_id": The numeric identifier of an icon representing the newsfeed or its source + * - "icon_url": The URL of an icon representing the newsfeed or its source * - "folder": The numeric identifier (or null) of the subscription's folder * - "top_folder": The numeric identifier (or null) of the top-level folder for the subscription * - "pinned": Whether the subscription is pinned @@ -795,7 +796,8 @@ class Database { f.updated as updated, f.modified as edited, s.modified as modified, - i.url as favicon, + i.id as icon_id, + i.url as icon_url, t.top as top_folder, coalesce(s.title, f.title) as title, coalesce((articles - hidden - marked), articles) as unread diff --git a/lib/REST/NextcloudNews/V1_2.php b/lib/REST/NextcloudNews/V1_2.php index 32405f87..57d1e732 100644 --- a/lib/REST/NextcloudNews/V1_2.php +++ b/lib/REST/NextcloudNews/V1_2.php @@ -181,7 +181,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { 'added' => "added", 'pinned' => "pinned", 'link' => "source", - 'faviconLink' => "favicon", + 'faviconLink' => "icon_url", 'folderId' => "top_folder", 'unreadCount' => "unread", 'ordering' => "order_type", diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index 9f8ea590..3de48637 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -256,7 +256,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // prepare data for each subscription; we also add unread counts for their host categories foreach (Arsse::$db->subscriptionList($user) as $f) { // add the feed to the list of feeds - $feeds[] = ['id' => (string) $f['id'], 'updated' => Date::transform($f['updated'], "iso8601", "sql"),'counter' => (int) $f['unread'], 'has_img' => (int) (strlen((string) $f['favicon']) > 0)]; // ID is cast to string for consistency with TTRSS + $feeds[] = ['id' => (string) $f['id'], 'updated' => Date::transform($f['updated'], "iso8601", "sql"),'counter' => (int) $f['unread'], 'has_img' => (int) (strlen((string) $f['icon_url']) > 0)]; // ID is cast to string for consistency with TTRSS // add the feed's unread count to the global unread count $countAll += $f['unread']; // add the feed's unread count to its category unread count @@ -441,7 +441,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { 'name' => $s['title'], 'id' => "FEED:".$s['id'], 'bare_id' => (int) $s['id'], - 'icon' => $s['favicon'] ? "feed-icons/".$s['id'].".ico" : false, + 'icon' => $s['icon_url'] ? "feed-icons/".$s['id'].".ico" : false, 'error' => (string) $s['err_msg'], 'param' => Date::transform($s['updated'], "iso8601", "sql"), 'unread' => 0, @@ -794,7 +794,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { 'unread' => (int) $s['unread'], 'cat_id' => (int) $s['folder'], 'feed_url' => $s['url'], - 'has_icon' => (bool) $s['favicon'], + 'has_icon' => (bool) $s['icon_url'], 'last_updated' => (int) Date::transform($s['updated'], "unix", "sql"), 'order_id' => $order, ]; diff --git a/tests/cases/ImportExport/TestOPML.php b/tests/cases/ImportExport/TestOPML.php index 469c674b..e527db0d 100644 --- a/tests/cases/ImportExport/TestOPML.php +++ b/tests/cases/ImportExport/TestOPML.php @@ -22,12 +22,12 @@ class TestOPML extends \JKingWeb\Arsse\Test\AbstractTest { ['id' => 1, 'parent' => null, 'children' => 1, 'feeds' => 1, 'name' => "Science"], ]; protected $subscriptions = [ - ['id' => 3, 'folder' => 1, 'top_folder' => 1, 'unread' => 2, 'updated' => "2016-05-23 06:40:02", 'err_msg' => 'argh', 'title' => 'Ars Technica', 'url' => "http://localhost:8000/3", 'favicon' => 'http://localhost:8000/3.png'], - ['id' => 4, 'folder' => 6, 'top_folder' => 3, 'unread' => 6, 'updated' => "2017-10-09 15:58:34", 'err_msg' => '', 'title' => 'CBC News', 'url' => "http://localhost:8000/4", 'favicon' => 'http://localhost:8000/4.png'], - ['id' => 6, 'folder' => null, 'top_folder' => null, 'unread' => 0, 'updated' => "2010-02-12 20:08:47", 'err_msg' => '', 'title' => 'Eurogamer', 'url' => "http://localhost:8000/6", 'favicon' => 'http://localhost:8000/6.png'], - ['id' => 1, 'folder' => 2, 'top_folder' => 1, 'unread' => 5, 'updated' => "2017-09-15 22:54:16", 'err_msg' => '', 'title' => 'NASA JPL', 'url' => "http://localhost:8000/1", 'favicon' => null], - ['id' => 5, 'folder' => 6, 'top_folder' => 3, 'unread' => 12, 'updated' => "2017-07-07 17:07:17", 'err_msg' => '', 'title' => 'Ottawa Citizen', 'url' => "http://localhost:8000/5", 'favicon' => ''], - ['id' => 2, 'folder' => 5, 'top_folder' => 3, 'unread' => 10, 'updated' => "2011-11-11 11:11:11", 'err_msg' => 'oops', 'title' => 'Toronto Star', 'url' => "http://localhost:8000/2", 'favicon' => 'http://localhost:8000/2.png'], + ['id' => 3, 'folder' => 1, 'top_folder' => 1, 'unread' => 2, 'updated' => "2016-05-23 06:40:02", 'err_msg' => 'argh', 'title' => 'Ars Technica', 'url' => "http://localhost:8000/3", 'icon_url' => 'http://localhost:8000/3.png'], + ['id' => 4, 'folder' => 6, 'top_folder' => 3, 'unread' => 6, 'updated' => "2017-10-09 15:58:34", 'err_msg' => '', 'title' => 'CBC News', 'url' => "http://localhost:8000/4", 'icon_url' => 'http://localhost:8000/4.png'], + ['id' => 6, 'folder' => null, 'top_folder' => null, 'unread' => 0, 'updated' => "2010-02-12 20:08:47", 'err_msg' => '', 'title' => 'Eurogamer', 'url' => "http://localhost:8000/6", 'icon_url' => 'http://localhost:8000/6.png'], + ['id' => 1, 'folder' => 2, 'top_folder' => 1, 'unread' => 5, 'updated' => "2017-09-15 22:54:16", 'err_msg' => '', 'title' => 'NASA JPL', 'url' => "http://localhost:8000/1", 'icon_url' => null], + ['id' => 5, 'folder' => 6, 'top_folder' => 3, 'unread' => 12, 'updated' => "2017-07-07 17:07:17", 'err_msg' => '', 'title' => 'Ottawa Citizen', 'url' => "http://localhost:8000/5", 'icon_url' => ''], + ['id' => 2, 'folder' => 5, 'top_folder' => 3, 'unread' => 10, 'updated' => "2011-11-11 11:11:11", 'err_msg' => 'oops', 'title' => 'Toronto Star', 'url' => "http://localhost:8000/2", 'icon_url' => 'http://localhost:8000/2.png'], ]; protected $tags = [ ['id' => 1, 'name' => "Canada", 'subscription' => 2], diff --git a/tests/cases/REST/Fever/TestAPI.php b/tests/cases/REST/Fever/TestAPI.php index 1aa77ba3..2d41dd15 100644 --- a/tests/cases/REST/Fever/TestAPI.php +++ b/tests/cases/REST/Fever/TestAPI.php @@ -273,9 +273,9 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { public function testListFeeds(): void { \Phake::when(Arsse::$db)->subscriptionList(Arsse::$user->id)->thenReturn(new Result([ - ['id' => 1, 'feed' => 5, 'title' => "Ankh-Morpork News", 'url' => "http://example.com/feed", 'source' => "http://example.com/", 'edited' => "2019-01-01 21:12:00", 'favicon' => "http://example.com/favicon.ico"], - ['id' => 2, 'feed' => 9, 'title' => "Ook, Ook Eek Ook!", 'url' => "http://example.net/feed", 'source' => "http://example.net/", 'edited' => "1988-06-24 12:21:00", 'favicon' => ""], - ['id' => 3, 'feed' => 1, 'title' => "The Last Soul", 'url' => "http://example.org/feed", 'source' => "http://example.org/", 'edited' => "1991-08-12 03:22:00", 'favicon' => "http://example.org/favicon.ico"], + ['id' => 1, 'feed' => 5, 'title' => "Ankh-Morpork News", 'url' => "http://example.com/feed", 'source' => "http://example.com/", 'edited' => "2019-01-01 21:12:00", 'icon_url' => "http://example.com/favicon.ico"], + ['id' => 2, 'feed' => 9, 'title' => "Ook, Ook Eek Ook!", 'url' => "http://example.net/feed", 'source' => "http://example.net/", 'edited' => "1988-06-24 12:21:00", 'icon_url' => ""], + ['id' => 3, 'feed' => 1, 'title' => "The Last Soul", 'url' => "http://example.org/feed", 'source' => "http://example.org/", 'edited' => "1991-08-12 03:22:00", 'icon_url' => "http://example.org/favicon.ico"], ])); \Phake::when(Arsse::$db)->tagSummarize(Arsse::$user->id)->thenReturn(new Result([ ['id' => 1, 'name' => "Fascinating", 'subscription' => 1], diff --git a/tests/cases/REST/NextcloudNews/TestV1_2.php b/tests/cases/REST/NextcloudNews/TestV1_2.php index a88eb81e..eccc0cc1 100644 --- a/tests/cases/REST/NextcloudNews/TestV1_2.php +++ b/tests/cases/REST/NextcloudNews/TestV1_2.php @@ -28,7 +28,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { [ 'id' => 2112, 'url' => 'http://example.com/news.atom', - 'favicon' => 'http://example.com/favicon.png', + 'icon_url' => 'http://example.com/favicon.png', 'source' => 'http://example.com/', 'folder' => null, 'top_folder' => null, @@ -43,7 +43,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { [ 'id' => 42, 'url' => 'http://example.org/news.atom', - 'favicon' => 'http://example.org/favicon.png', + 'icon_url' => 'http://example.org/favicon.png', 'source' => 'http://example.org/', 'folder' => 12, 'top_folder' => 8, @@ -58,7 +58,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { [ 'id' => 47, 'url' => 'http://example.net/news.atom', - 'favicon' => 'http://example.net/favicon.png', + 'icon_url' => 'http://example.net/favicon.png', 'source' => 'http://example.net/', 'folder' => null, 'top_folder' => null, diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index 923380dd..ff6d8954 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -40,12 +40,12 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { ['id' => 1, 'parent' => null, 'children' => 1, 'feeds' => 1, 'name' => "Science"], ]; protected $subscriptions = [ - ['id' => 3, 'folder' => 1, 'top_folder' => 1, 'unread' => 2, 'updated' => "2016-05-23 06:40:02", 'err_msg' => 'argh', 'title' => 'Ars Technica', 'url' => " http://example.com/3", 'favicon' => 'http://example.com/3.png'], - ['id' => 4, 'folder' => 6, 'top_folder' => 3, 'unread' => 6, 'updated' => "2017-10-09 15:58:34", 'err_msg' => '', 'title' => 'CBC News', 'url' => " http://example.com/4", 'favicon' => 'http://example.com/4.png'], - ['id' => 6, 'folder' => null, 'top_folder' => null, 'unread' => 0, 'updated' => "2010-02-12 20:08:47", 'err_msg' => '', 'title' => 'Eurogamer', 'url' => " http://example.com/6", 'favicon' => 'http://example.com/6.png'], - ['id' => 1, 'folder' => 2, 'top_folder' => 1, 'unread' => 5, 'updated' => "2017-09-15 22:54:16", 'err_msg' => '', 'title' => 'NASA JPL', 'url' => " http://example.com/1", 'favicon' => null], - ['id' => 5, 'folder' => 6, 'top_folder' => 3, 'unread' => 12, 'updated' => "2017-07-07 17:07:17", 'err_msg' => '', 'title' => 'Ottawa Citizen', 'url' => " http://example.com/5", 'favicon' => ''], - ['id' => 2, 'folder' => 5, 'top_folder' => 3, 'unread' => 10, 'updated' => "2011-11-11 11:11:11", 'err_msg' => 'oops', 'title' => 'Toronto Star', 'url' => " http://example.com/2", 'favicon' => 'http://example.com/2.png'], + ['id' => 3, 'folder' => 1, 'top_folder' => 1, 'unread' => 2, 'updated' => "2016-05-23 06:40:02", 'err_msg' => 'argh', 'title' => 'Ars Technica', 'url' => " http://example.com/3", 'icon_url' => 'http://example.com/3.png'], + ['id' => 4, 'folder' => 6, 'top_folder' => 3, 'unread' => 6, 'updated' => "2017-10-09 15:58:34", 'err_msg' => '', 'title' => 'CBC News', 'url' => " http://example.com/4", 'icon_url' => 'http://example.com/4.png'], + ['id' => 6, 'folder' => null, 'top_folder' => null, 'unread' => 0, 'updated' => "2010-02-12 20:08:47", 'err_msg' => '', 'title' => 'Eurogamer', 'url' => " http://example.com/6", 'icon_url' => 'http://example.com/6.png'], + ['id' => 1, 'folder' => 2, 'top_folder' => 1, 'unread' => 5, 'updated' => "2017-09-15 22:54:16", 'err_msg' => '', 'title' => 'NASA JPL', 'url' => " http://example.com/1", 'icon_url' => null], + ['id' => 5, 'folder' => 6, 'top_folder' => 3, 'unread' => 12, 'updated' => "2017-07-07 17:07:17", 'err_msg' => '', 'title' => 'Ottawa Citizen', 'url' => " http://example.com/5", 'icon_url' => ''], + ['id' => 2, 'folder' => 5, 'top_folder' => 3, 'unread' => 10, 'updated' => "2011-11-11 11:11:11", 'err_msg' => 'oops', 'title' => 'Toronto Star', 'url' => " http://example.com/2", 'icon_url' => 'http://example.com/2.png'], ]; protected $labels = [ ['id' => 3, 'articles' => 100, 'read' => 94, 'unread' => 6, 'name' => "Fascinating"], From 4cb23dd1980e75a8d78da7f6da9194b343e15992 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 16 Jan 2021 14:24:01 -0500 Subject: [PATCH 114/366] Partial implementation of proper content scraping --- lib/Database.php | 27 ++++++++++++++++++--------- lib/Feed.php | 2 +- sql/MySQL/6.sql | 4 ++++ sql/PostgreSQL/6.sql | 4 ++++ sql/SQLite3/6.sql | 28 ++++++++++++++++++++++++++-- 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 6e72cf6f..a78ba370 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1126,12 +1126,19 @@ class Database { if (!V::id($feedID)) { throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID, 'type' => "int > 0"]); } - $f = $this->db->prepare("SELECT url, username, password, modified, etag, err_count, scrape FROM arsse_feeds where id = ?", "int")->run($feedID)->getRow(); + $f = $this->db->prepareArray( + "SELECT + url, username, password, modified, etag, err_count, scrapers + FROM arsse_feeds as f + left join (select feed, count(*) as scrapers from arsse_subscriptions where scrape = 1 group by feed) as s on f.id = s.feed + where id = ?", + ["int"] + )->run($feedID)->getRow(); if (!$f) { throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID]); } // determine whether the feed's items should be scraped for full content from the source Web site - $scrape = (Arsse::$conf->fetchEnableScraping && $f['scrape']); + $scrape = (Arsse::$conf->fetchEnableScraping && $f['scrapers']); // the Feed object throws an exception when there are problems, but that isn't ideal // here. When an exception is thrown it should update the database with the // error instead of failing; if other exceptions are thrown, we should simply roll back @@ -1161,8 +1168,8 @@ class Database { } if (sizeof($feed->newItems)) { $qInsertArticle = $this->db->prepareArray( - "INSERT INTO arsse_articles(url,title,author,published,edited,guid,content,url_title_hash,url_content_hash,title_content_hash,feed) values(?,?,?,?,?,?,?,?,?,?,?)", - ['str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str', 'int'] + "INSERT INTO arsse_articles(url,title,author,published,edited,guid,content,url_title_hash,url_content_hash,title_content_hash,feed,content_scraped) values(?,?,?,?,?,?,?,?,?,?,?,?)", + ["str", "str", "str", "datetime", "datetime", "str", "str", "str", "str", "str", "int", "str"] ); } if (sizeof($feed->changedItems)) { @@ -1170,8 +1177,8 @@ class Database { $qDeleteCategories = $this->db->prepare("DELETE FROM arsse_categories WHERE article = ?", 'int'); $qClearReadMarks = $this->db->prepare("UPDATE arsse_marks SET \"read\" = 0, modified = CURRENT_TIMESTAMP WHERE article = ? and \"read\" = 1", 'int'); $qUpdateArticle = $this->db->prepareArray( - "UPDATE arsse_articles SET url = ?, title = ?, author = ?, published = ?, edited = ?, modified = CURRENT_TIMESTAMP, guid = ?, content = ?, url_title_hash = ?, url_content_hash = ?, title_content_hash = ? WHERE id = ?", - ['str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str', 'int'] + "UPDATE arsse_articles SET url = ?, title = ?, author = ?, published = ?, edited = ?, modified = CURRENT_TIMESTAMP, guid = ?, content = ?, url_title_hash = ?, url_content_hash = ?, title_content_hash = ?, content_scraped = ? WHERE id = ?", + ["str", "str", "str", "datetime", "datetime", "str", "str", "str", "str", "str", "str", "int"] ); } // determine if the feed icon needs to be updated, and update it if appropriate @@ -1204,7 +1211,8 @@ class Database { $article->urlTitleHash, $article->urlContentHash, $article->titleContentHash, - $feedID + $feedID, + $article->scrapedContent ?? null )->lastId(); // note the new ID for later use $articleMap[$k] = $articleID; @@ -1232,6 +1240,7 @@ class Database { $article->urlTitleHash, $article->urlContentHash, $article->titleContentHash, + $article->scrapedContent ?? null, $articleID ); // delete all enclosures and categories and re-insert them @@ -1273,7 +1282,7 @@ class Database { // lastly update the feed database itself with updated information. $this->db->prepareArray( "UPDATE arsse_feeds SET title = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ?, icon = ? WHERE id = ?", - ['str', 'str', 'datetime', 'strict str', 'datetime', 'int', 'int', 'int'] + ["str", "str", "datetime", "strict str", "datetime", "int", "int", "int"] )->run( $feed->data->title, $feed->data->siteUrl, @@ -1429,7 +1438,7 @@ class Database { 'url' => "arsse_articles.url", 'title' => "arsse_articles.title", 'author' => "arsse_articles.author", - 'content' => "arsse_articles.content", + 'content' => "coalesce(case when arsse_subscriptions.scrape = 1 then arsse_articles.content_scraped end, 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)", diff --git a/lib/Feed.php b/lib/Feed.php index e96d064c..af43f22e 100644 --- a/lib/Feed.php +++ b/lib/Feed.php @@ -448,7 +448,7 @@ class Feed { $scraper->setUrl($item->url); $scraper->execute(); if ($scraper->hasRelevantContent()) { - $item->content = $scraper->getFilteredContent(); + $item->scrapedContent = $scraper->getFilteredContent(); } } } diff --git a/sql/MySQL/6.sql b/sql/MySQL/6.sql index c2f8b532..7d9eb128 100644 --- a/sql/MySQL/6.sql +++ b/sql/MySQL/6.sql @@ -32,6 +32,10 @@ create table arsse_user_meta( primary key(owner,"key") ) character set utf8mb4 collate utf8mb4_unicode_ci; +alter table arsse_subscriptions add column scrape boolean not null default 0; +alter table arsse_feeds drop column scrape; +alter table arsse_articles add column content_scraped longtext; + create table arsse_icons( id serial primary key, url varchar(767) unique not null, diff --git a/sql/PostgreSQL/6.sql b/sql/PostgreSQL/6.sql index a27b87a6..825f67de 100644 --- a/sql/PostgreSQL/6.sql +++ b/sql/PostgreSQL/6.sql @@ -32,6 +32,10 @@ create table arsse_user_meta( primary key(owner,key) ); +alter table arsse_subscriptions add column scrape smallint not null default 0; +alter table arsse_feeds drop column scrape; +alter table arsse_articles add column content_scraped text; + create table arsse_icons( id bigserial primary key, url text unique not null, diff --git a/sql/SQLite3/6.sql b/sql/SQLite3/6.sql index 3c5f3589..e43c4ea3 100644 --- a/sql/SQLite3/6.sql +++ b/sql/SQLite3/6.sql @@ -44,8 +44,11 @@ create table arsse_user_meta( primary key(owner,key) ) without rowid; +-- Add a "scrape" column for subscriptions +alter table arsse_subscriptions add column scrape boolean not null default 0; -- Add a separate table for feed icons and replace their URLs in the feeds table with their IDs +-- Also remove the "scrape" column of the feeds table, which was never an advertised feature create table arsse_icons( -- Icons associated with feeds -- At a minimum the URL of the icon must be known, but its content may be missing @@ -76,16 +79,37 @@ create table arsse_feeds_new( username text not null default '', -- HTTP authentication username password text not null default '', -- HTTP authentication password (this is stored in plain text) size integer not null default 0, -- number of articles in the feed at last fetch - scrape boolean not null default 0, -- whether to use picoFeed's content scraper with this feed icon integer references arsse_icons(id) on delete set null, -- numeric identifier of any associated icon unique(url,username,password) -- a URL with particular credentials should only appear once ); insert into arsse_feeds_new - select f.id, f.url, title, source, updated, f.modified, f.next_fetch, f.orphaned, f.etag, err_count, err_msg, username, password, size, scrape, i.id + select f.id, f.url, title, source, updated, f.modified, f.next_fetch, f.orphaned, f.etag, err_count, err_msg, username, password, size, i.id from arsse_feeds as f left join arsse_icons as i on f.favicon = i.url; drop table arsse_feeds; alter table arsse_feeds_new rename to arsse_feeds; +-- Add a column for scraped article content, and re-order some column +create table arsse_articles_new( +-- entries in newsfeeds + id integer primary key, -- sequence number + feed integer not null references arsse_feeds(id) on delete cascade, -- feed for the subscription + url text, -- URL of article + title text collate nocase, -- article title + author text collate nocase, -- author's name + published text, -- time of original publication + edited text, -- time of last edit by author + modified text not null default CURRENT_TIMESTAMP, -- time when article was last modified in database + guid text, -- GUID + url_title_hash text not null, -- hash of URL + title; used when checking for updates and for identification if there is no guid. + url_content_hash text not null, -- hash of URL + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid. + title_content_hash text not null, -- hash of title + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid. + content_scraped text, -- scraped content, as HTML + content text -- content, as HTML +); +insert into arsse_articles_new select id, feed, url, title, author, published, edited, modified, guid, url_title_hash, url_content_hash, title_content_hash, null, content from arsse_articles; +drop table arsse_articles; +alter table arsse_articles_new rename to arsse_articles; + -- set version marker pragma user_version = 7; update arsse_meta set value = '7' where "key" = 'schema_version'; From 76f70119fde6b903242e7d72fae9291ac5e5b3b5 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 16 Jan 2021 16:48:35 -0500 Subject: [PATCH 115/366] More work on scraping --- tests/cases/Database/SeriesArticle.php | 80 +++++++++++++------------- tests/cases/Feed/TestFeed.php | 2 + 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index c444977f..09342c9a 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -93,22 +93,23 @@ trait SeriesArticle { 'feed' => "int", 'folder' => "int", 'title' => "str", + 'scrape' => "bool", ], 'rows' => [ - [1, "john.doe@example.com",1, null,"Subscription 1"], - [2, "john.doe@example.com",2, null,null], - [3, "john.doe@example.com",3, 1,"Subscription 3"], - [4, "john.doe@example.com",4, 6,null], - [5, "john.doe@example.com",10, 5,"Subscription 5"], - [6, "jane.doe@example.com",1, null,null], - [7, "jane.doe@example.com",10,null,"Subscription 7"], - [8, "john.doe@example.org",11,null,null], - [9, "john.doe@example.org",12,null,"Subscription 9"], - [10,"john.doe@example.org",13,null,null], - [11,"john.doe@example.net",10,null,"Subscription 11"], - [12,"john.doe@example.net",2, 9,null], - [13,"john.doe@example.net",3, 8,"Subscription 13"], - [14,"john.doe@example.net",4, 7,null], + [1, "john.doe@example.com",1, null,"Subscription 1",0], + [2, "john.doe@example.com",2, null,null,0], + [3, "john.doe@example.com",3, 1,"Subscription 3",0], + [4, "john.doe@example.com",4, 6,null,0], + [5, "john.doe@example.com",10, 5,"Subscription 5",0], + [6, "jane.doe@example.com",1, null,null,0], + [7, "jane.doe@example.com",10,null,"Subscription 7",0], + [8, "john.doe@example.org",11,null,null,0], + [9, "john.doe@example.org",12,null,"Subscription 9",0], + [10,"john.doe@example.org",13,null,null,0], + [11,"john.doe@example.net",10,null,"Subscription 11",0], + [12,"john.doe@example.net",2, 9,null,0], + [13,"john.doe@example.net",3, 8,"Subscription 13",0], + [14,"john.doe@example.net",4, 7,null,0], ], ], 'arsse_tag_members' => [ @@ -145,33 +146,34 @@ trait SeriesArticle { 'url_content_hash' => "str", 'title_content_hash' => "str", 'modified' => "datetime", + 'content_scraped' => "str", ], 'rows' => [ - [1,1,null,"Title one", null,null,null,"First article", null,"","","","2000-01-01T00:00:00Z"], - [2,1,null,"Title two", null,null,null,"Second article",null,"","","","2010-01-01T00:00:00Z"], - [3,2,null,"Title three",null,null,null,"Third article", null,"","","","2000-01-01T00:00:00Z"], - [4,2,null,null,"John Doe",null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [5,3,null,null,"John Doe",null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [6,3,null,null,"Jane Doe",null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [7,4,null,null,"Jane Doe",null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [8,4,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [9,5,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [10,5,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [11,6,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [12,6,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [13,7,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [14,7,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [15,8,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [16,8,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [17,9,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [18,9,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [19,10,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [20,10,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [101,11,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:01','

Article content 1

','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00'], - [102,11,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:02','

Article content 2

','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00'], - [103,12,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:03','

Article content 3

','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00'], - [104,12,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:04','

Article content 4

','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00'], - [105,13,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:05','

Article content 5

','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00'], + [1,1,null,"Title one", null,null,null,"First article", null,"","","","2000-01-01T00:00:00Z",null], + [2,1,null,"Title two", null,null,null,"Second article",null,"","","","2010-01-01T00:00:00Z",null], + [3,2,null,"Title three",null,null,null,"Third article", null,"","","","2000-01-01T00:00:00Z",null], + [4,2,null,null,"John Doe",null,null,null,null,"","","","2010-01-01T00:00:00Z",null], + [5,3,null,null,"John Doe",null,null,null,null,"","","","2000-01-01T00:00:00Z",null], + [6,3,null,null,"Jane Doe",null,null,null,null,"","","","2010-01-01T00:00:00Z",null], + [7,4,null,null,"Jane Doe",null,null,null,null,"","","","2000-01-01T00:00:00Z",null], + [8,4,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z",null], + [9,5,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z",null], + [10,5,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z",null], + [11,6,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z",null], + [12,6,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z",null], + [13,7,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z",null], + [14,7,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z",null], + [15,8,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z",null], + [16,8,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z",null], + [17,9,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z",null], + [18,9,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z",null], + [19,10,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z",null], + [20,10,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z",null], + [101,11,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:01','

Article content 1

','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00',"

Scraped content 1

"], + [102,11,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:02','

Article content 2

','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00',null], + [103,12,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:03','

Article content 3

','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00',null], + [104,12,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:04','

Article content 4

','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00',null], + [105,13,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:05','

Article content 5

','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00',null], ], ], 'arsse_enclosures' => [ diff --git a/tests/cases/Feed/TestFeed.php b/tests/cases/Feed/TestFeed.php index 51799940..cdfcec1e 100644 --- a/tests/cases/Feed/TestFeed.php +++ b/tests/cases/Feed/TestFeed.php @@ -369,6 +369,8 @@ class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { // now try to scrape and get different content $f = new Feed(null, $this->base."Scraping/Feed", "", "", "", "", true); $exp = "

Partial content, followed by more content

"; + $this->assertSame($exp, $f->newItems[0]->scrapedContent); + $exp = "

Partial content

"; $this->assertSame($exp, $f->newItems[0]->content); } From 7897585d9887a6f7e58d1630d421b01fd81de4a5 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 16 Jan 2021 17:58:31 -0500 Subject: [PATCH 116/366] Test scraping Text search should also match scraped content when appropriate --- lib/Database.php | 16 +++++++++--- tests/cases/Database/SeriesArticle.php | 35 +++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index a78ba370..ea70d953 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1711,10 +1711,10 @@ class Database { } // handle text-matching context options $options = [ - "titleTerms" => ["arsse_articles.title"], - "searchTerms" => ["arsse_articles.title", "arsse_articles.content"], - "authorTerms" => ["arsse_articles.author"], - "annotationTerms" => ["arsse_marks.note"], + "titleTerms" => ["title"], + "searchTerms" => ["title", "content"], + "authorTerms" => ["author"], + "annotationTerms" => ["note"], ]; foreach ($options as $m => $columns) { if (!$context->$m()) { @@ -1722,6 +1722,10 @@ class Database { } elseif (!$context->$m) { throw new Db\ExceptionInput("tooShort", ['field' => $m, 'action' => $this->caller(), 'min' => 1]); // must have at least one array element } + $columns = array_map(function ($c) use ($colDefs) { + assert(isset($colDefs[$c]), new Exception("constantUnknown", $c)); + return $colDefs[$c]; + }, $columns); $q->setWhere(...$this->generateSearch($context->$m, $columns)); } // further handle exclusionary text-matching context options @@ -1729,6 +1733,10 @@ class Database { if (!$context->not->$m() || !$context->not->$m) { continue; } + $columns = array_map(function ($c) use ($colDefs) { + assert(isset($colDefs[$c]), new Exception("constantUnknown", $c)); + return $colDefs[$c]; + }, $columns); $q->setWhereNot(...$this->generateSearch($context->not->$m, $columns, true)); } // return the query diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 09342c9a..6302f5df 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -22,10 +22,11 @@ trait SeriesArticle { 'num' => 'int', ], 'rows' => [ - ["jane.doe@example.com", "",1], - ["john.doe@example.com", "",2], - ["john.doe@example.org", "",3], - ["john.doe@example.net", "",4], + ["jane.doe@example.com", "", 1], + ["john.doe@example.com", "", 2], + ["john.doe@example.org", "", 3], + ["john.doe@example.net", "", 4], + ["jill.doe@example.com", "", 5], ], ], 'arsse_feeds' => [ @@ -110,6 +111,7 @@ trait SeriesArticle { [12,"john.doe@example.net",2, 9,null,0], [13,"john.doe@example.net",3, 8,"Subscription 13",0], [14,"john.doe@example.net",4, 7,null,0], + [15,"jill.doe@example.com",11,null,null,1], ], ], 'arsse_tag_members' => [ @@ -1149,4 +1151,29 @@ trait SeriesArticle { $state = $this->primeExpectations($this->data, $this->checkTables); $this->compareExpectations(static::$drv, $state); } + + public function testSelectScrapedContent(): void { + $exp = [ + ['id' => 101, 'content' => "

Article content 1

"], + ['id' => 102, 'content' => "

Article content 2

"], + ]; + $this->assertResult($exp, Arsse::$db->articleList("john.doe@example.org", (new Context)->subscription(8), ["id", "content"])); + $exp = [ + ['id' => 101, 'content' => "

Scraped content 1

"], + ['id' => 102, 'content' => "

Article content 2

"], + ]; + $this->assertResult($exp, Arsse::$db->articleList("jill.doe@example.com", (new Context)->subscription(15), ["id", "content"])); + } + + public function testSearchScrapedContent(): void { + $exp = [ + ['id' => 101, 'content' => "

Scraped content 1

"], + ['id' => 102, 'content' => "

Article content 2

"], + ]; + $this->assertResult($exp, Arsse::$db->articleList("jill.doe@example.com", (new Context)->subscription(15)->searchTerms(["article"]), ["id", "content"])); + $exp = [ + ['id' => 101, 'content' => "

Scraped content 1

"], + ]; + $this->assertResult($exp, Arsse::$db->articleList("jill.doe@example.com", (new Context)->subscription(15)->searchTerms(["scraped"]), ["id", "content"])); + } } From 86897af0b3e085f3e3e7dd7895a487e34aa898ab Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 16 Jan 2021 19:06:20 -0500 Subject: [PATCH 117/366] Add ability to enable scraper Also transfer any existing scraper booleans on database upgrade. It was previously possible to enable scraping manually by editing the database, and these settings will be honoured. --- lib/Database.php | 2 + sql/MySQL/6.sql | 1 + sql/PostgreSQL/6.sql | 1 + sql/SQLite3/6.sql | 47 +++++++++++---------- tests/cases/Database/SeriesSubscription.php | 18 ++++---- tests/cases/Db/BaseUpdate.php | 31 ++++++++++---- 6 files changed, 61 insertions(+), 39 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index ea70d953..a69d2465 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -898,6 +898,7 @@ class Database { * - "title": The title of the subscription * - "folder": The numeric identifier (or null) of the subscription's folder * - "pinned": Whether the subscription is pinned + * - "scrape": Whether to scrape full article contents from the HTML article * - "order_type": Whether articles should be sorted in reverse cronological order (2), chronological order (1), or the default (0) * - "keep_rule": The subscription's "keep" filter rule; articles which do not match this are hidden * - "block_rule": The subscription's "block" filter rule; articles which match this are hidden @@ -948,6 +949,7 @@ class Database { 'pinned' => "strict bool", 'keep_rule' => "str", 'block_rule' => "str", + 'scrape' => "bool", ]; [$setClause, $setTypes, $setValues] = $this->generateSet($data, $valid); if (!$setClause) { diff --git a/sql/MySQL/6.sql b/sql/MySQL/6.sql index 7d9eb128..789900ef 100644 --- a/sql/MySQL/6.sql +++ b/sql/MySQL/6.sql @@ -33,6 +33,7 @@ create table arsse_user_meta( ) character set utf8mb4 collate utf8mb4_unicode_ci; alter table arsse_subscriptions add column scrape boolean not null default 0; +update arsse_subscriptions set scrape = 1 where feed in (select id from arsse_feeds where scrape = 1); alter table arsse_feeds drop column scrape; alter table arsse_articles add column content_scraped longtext; diff --git a/sql/PostgreSQL/6.sql b/sql/PostgreSQL/6.sql index 825f67de..0f559a87 100644 --- a/sql/PostgreSQL/6.sql +++ b/sql/PostgreSQL/6.sql @@ -33,6 +33,7 @@ create table arsse_user_meta( ); alter table arsse_subscriptions add column scrape smallint not null default 0; +update arsse_subscriptions set scrape = 1 where feed in (select id from arsse_feeds where scrape = 1); alter table arsse_feeds drop column scrape; alter table arsse_articles add column content_scraped text; diff --git a/sql/SQLite3/6.sql b/sql/SQLite3/6.sql index e43c4ea3..2be4fed5 100644 --- a/sql/SQLite3/6.sql +++ b/sql/SQLite3/6.sql @@ -44,8 +44,31 @@ create table arsse_user_meta( primary key(owner,key) ) without rowid; --- Add a "scrape" column for subscriptions +-- Add a "scrape" column for subscriptions and copy any existing scraping alter table arsse_subscriptions add column scrape boolean not null default 0; +update arsse_subscriptions set scrape = 1 where feed in (select id from arsse_feeds where scrape = 1); + +-- Add a column for scraped article content, and re-order some columns +create table arsse_articles_new( +-- entries in newsfeeds + id integer primary key, -- sequence number + feed integer not null references arsse_feeds(id) on delete cascade, -- feed for the subscription + url text, -- URL of article + title text collate nocase, -- article title + author text collate nocase, -- author's name + published text, -- time of original publication + edited text, -- time of last edit by author + modified text not null default CURRENT_TIMESTAMP, -- time when article was last modified in database + guid text, -- GUID + url_title_hash text not null, -- hash of URL + title; used when checking for updates and for identification if there is no guid. + url_content_hash text not null, -- hash of URL + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid. + title_content_hash text not null, -- hash of title + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid. + content_scraped text, -- scraped content, as HTML + content text -- content, as HTML +); +insert into arsse_articles_new select id, feed, url, title, author, published, edited, modified, guid, url_title_hash, url_content_hash, title_content_hash, null, content from arsse_articles; +drop table arsse_articles; +alter table arsse_articles_new rename to arsse_articles; -- Add a separate table for feed icons and replace their URLs in the feeds table with their IDs -- Also remove the "scrape" column of the feeds table, which was never an advertised feature @@ -88,28 +111,6 @@ insert into arsse_feeds_new drop table arsse_feeds; alter table arsse_feeds_new rename to arsse_feeds; --- Add a column for scraped article content, and re-order some column -create table arsse_articles_new( --- entries in newsfeeds - id integer primary key, -- sequence number - feed integer not null references arsse_feeds(id) on delete cascade, -- feed for the subscription - url text, -- URL of article - title text collate nocase, -- article title - author text collate nocase, -- author's name - published text, -- time of original publication - edited text, -- time of last edit by author - modified text not null default CURRENT_TIMESTAMP, -- time when article was last modified in database - guid text, -- GUID - url_title_hash text not null, -- hash of URL + title; used when checking for updates and for identification if there is no guid. - url_content_hash text not null, -- hash of URL + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid. - title_content_hash text not null, -- hash of title + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid. - content_scraped text, -- scraped content, as HTML - content text -- content, as HTML -); -insert into arsse_articles_new select id, feed, url, title, author, published, edited, modified, guid, url_title_hash, url_content_hash, title_content_hash, null, content from arsse_articles; -drop table arsse_articles; -alter table arsse_articles_new rename to arsse_articles; - -- set version marker pragma user_version = 7; update arsse_meta set value = '7' where "key" = 'schema_version'; diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index abbdab39..389495d3 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -80,13 +80,14 @@ trait SeriesSubscription { 'order_type' => "int", 'keep_rule' => "str", 'block_rule' => "str", + 'scrape' => "bool", ], 'rows' => [ - [1,"john.doe@example.com",2,null,null,1,2,null,null], - [2,"jane.doe@example.com",2,null,null,0,0,null,null], - [3,"john.doe@example.com",3,"Ook",2,0,1,null,null], - [4,"jill.doe@example.com",2,null,null,0,0,null,null], - [5,"jack.doe@example.com",2,null,null,1,2,"","3|E"], + [1,"john.doe@example.com",2,null,null,1,2,null,null,0], + [2,"jane.doe@example.com",2,null,null,0,0,null,null,0], + [3,"john.doe@example.com",3,"Ook",2,0,1,null,null,0], + [4,"jill.doe@example.com",2,null,null,0,0,null,null,0], + [5,"jack.doe@example.com",2,null,null,1,2,"","3|E",0], ], ], 'arsse_tags' => [ @@ -409,22 +410,23 @@ trait SeriesSubscription { 'title' => "Ook Ook", 'folder' => 3, 'pinned' => false, + 'scrape' => true, 'order_type' => 0, 'keep_rule' => "ook", 'block_rule' => "eek", ]); $state = $this->primeExpectations($this->data, [ 'arsse_feeds' => ['id','url','username','password','title'], - 'arsse_subscriptions' => ['id','owner','feed','title','folder','pinned','order_type','keep_rule','block_rule'], + 'arsse_subscriptions' => ['id','owner','feed','title','folder','pinned','order_type','keep_rule','block_rule','scrape'], ]); - $state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,"Ook Ook",3,0,0,"ook","eek"]; + $state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,"Ook Ook",3,0,0,"ook","eek",1]; $this->compareExpectations(static::$drv, $state); Arsse::$db->subscriptionPropertiesSet($this->user, 1, [ 'title' => null, 'keep_rule' => null, 'block_rule' => null, ]); - $state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,null,3,0,0,null,null]; + $state['arsse_subscriptions']['rows'][0] = [1,"john.doe@example.com",2,null,3,0,0,null,null,1]; $this->compareExpectations(static::$drv, $state); // making no changes is a valid result Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['unhinged' => true]); diff --git a/tests/cases/Db/BaseUpdate.php b/tests/cases/Db/BaseUpdate.php index bce4dbcf..4e1ed79b 100644 --- a/tests/cases/Db/BaseUpdate.php +++ b/tests/cases/Db/BaseUpdate.php @@ -139,14 +139,22 @@ class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest { $this->drv->schemaUpdate(6); $this->drv->exec( <<drv->schemaUpdate(7); @@ -168,9 +176,16 @@ QUERY_TEXT ['url' => 'https://example.com/', 'icon' => 1], ['url' => 'http://example.net/', 'icon' => null], ]; + $subs = [ + ['id' => 1, 'scrape' => 1], + ['id' => 2, 'scrape' => 1], + ['id' => 3, 'scrape' => 0], + ['id' => 4, 'scrape' => 0], + ]; $this->assertEquals($users, $this->drv->query("SELECT id, password, num from arsse_users order by id")->getAll()); $this->assertEquals($folders, $this->drv->query("SELECT owner, name from arsse_folders order by owner")->getAll()); $this->assertEquals($icons, $this->drv->query("SELECT id, url from arsse_icons order by id")->getAll()); $this->assertEquals($feeds, $this->drv->query("SELECT url, icon from arsse_feeds order by id")->getAll()); + $this->assertEquals($subs, $this->drv->query("SELECT id, scrape from arsse_subscriptions order by id")->getAll()); } } From 2cf4bf0d4d7c3af1df5adad4672b2a76dfdd07d6 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 16 Jan 2021 22:52:07 -0500 Subject: [PATCH 118/366] Prototype Miniflux feed listing --- lib/Database.php | 5 ++++- lib/REST/Miniflux/V1.php | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/lib/Database.php b/lib/Database.php index a69d2465..77af92ca 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -776,7 +776,9 @@ class Database { * - "updated": The date and time at which the newsfeed was last updated in the database * - "edited": The date and time at which the newsfeed was last modified by its authors * - "modified": The date and time at which the subscription properties were last changed by the user + * - "next_fetch": The date and time and which the feed will next be fetched * - "unread": The number of unread articles associated with the subscription + * - "etag": The ETag header-field in the last fetch response * * @param string $user The user whose subscriptions are to be listed * @param integer|null $folder The identifier of the folder under which to list subscriptions; by default the root folder is used @@ -792,10 +794,11 @@ class Database { "SELECT s.id as id, s.feed as feed, - f.url,source,folder,pinned,err_count,err_msg,order_type,added,keep_rule,block_rule, + f.url,source,folder,pinned,err_count,err_msg,order_type,added,keep_rule,block_rule,f.etag,s.scrape, f.updated as updated, f.modified as edited, s.modified as modified, + f.next_fetch, i.id as icon_id, i.url as icon_url, t.top as top_folder, diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index f048e798..c1bc8e57 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -22,6 +22,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\JsonResponse as Response; +use Laminas\Diactoros\Uri; class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { public const VERSION = "2.0.26"; @@ -590,6 +591,52 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } + protected function getFeeds(): ResponseInterface { + $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); + $tr = Arsse::$db->begin(); + $out = []; + // compile the list of folders; the feed list includes folder names + $folders = [0 => ['id' => 1, 'title' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName"), 'user_id' => $meta['num']]]; + foreach(Arsse::$db->folderList(Arsse::$user->id, null, false) as $r) { + $folders[(int) $r['id']] = [ + 'id' => ((int) $r['id']) + 1, + 'title' => $r['name'], + 'user_id' => $meta['num'], + ]; + } + // next compile the list of feeds + foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $r) { + $url = new Uri($r['url']); + $out = [ + 'id' => (int) $r['id'], + 'user_id' => $meta['num'], + 'feed_url' => $url->withUserInfo(""), + 'site_url' => $r['source'], + 'title' => $r['title'], + 'checked_at' => Date::transform($r['updated'], "iso8601", "sql"), + 'next_check_at' => Date::transform($r['next_fetch'], "iso8601", "sql") ?? "0001-01-01T00:00:00Z", + 'etag_header' => $r['etag'] ?? "", + 'last_modified_header' => (string) Date::transform($r['edited'], "http", "sql"), + 'parsing_error_message' => (string) $r['err_msg'], + 'parsing_error_count' => (int) $r['err_count'], + 'scraper_rules' => "", + 'rewrite_rules' => "", + 'crawler' => (bool) $r['scrape'], + 'blocklist_rules' => (string) $r['block_rule'], + 'keeplist_rules' => (string) $r['keep_rule'], + 'user_agent' => "", + 'username' => explode(":", $url->getUserInfo(), 2)[0] ?? "", + 'password' => explode(":", $url->getUserInfo(), 2)[1] ?? "", + 'disabled' => false, + 'ignore_http_cache' => false, + 'fetch_via_proxy' => false, + 'category' => $folders[$r['top_folder']], + 'icon' => $r['icon_id'] ? ['feed_id' => (int) $r['id'], 'icon_id' => (int) $r['icon_id']] : null, + ]; + return new Response($out); + } + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokens in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); From 14d2d19ae1fa50614bae5b6f2bf243abf6637e74 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 17 Jan 2021 13:02:31 -0500 Subject: [PATCH 119/366] Tests for Miniflux feed listing --- .../030_Supported_Protocols/005_Miniflux.md | 1 + lib/Database.php | 3 +- lib/REST/Miniflux/V1.php | 75 ++++++++++-------- tests/cases/REST/Miniflux/TestV1.php | 76 +++++++++++++++++++ 4 files changed, 121 insertions(+), 34 deletions(-) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index 6a063bfa..a85a61ea 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -34,6 +34,7 @@ Miniflux version 2.0.27 is emulated, though not all features are implemented - The "All" category is treated specially (see below for details) - Category names consisting only of whitespace are rejected along with the empty string - Filtering rules may not function identically (see below for details) +- The `checked_at` field of feeds indicates when the feed was last updated rather than when it was last checked # Behaviour of filtering (block and keep) rules diff --git a/lib/Database.php b/lib/Database.php index 77af92ca..3866ddaf 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -777,8 +777,9 @@ class Database { * - "edited": The date and time at which the newsfeed was last modified by its authors * - "modified": The date and time at which the subscription properties were last changed by the user * - "next_fetch": The date and time and which the feed will next be fetched - * - "unread": The number of unread articles associated with the subscription * - "etag": The ETag header-field in the last fetch response + * - "scrape": Whether the user wants scrape full-article content + * - "unread": The number of unread articles associated with the subscription * * @param string $user The user whose subscriptions are to be listed * @param integer|null $folder The identifier of the folder under which to list subscriptions; by default the root folder is used diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index c1bc8e57..43d9e36c 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -591,50 +591,59 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } - protected function getFeeds(): ResponseInterface { + protected function mapFolders(): array { $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); - $tr = Arsse::$db->begin(); - $out = []; - // compile the list of folders; the feed list includes folder names $folders = [0 => ['id' => 1, 'title' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName"), 'user_id' => $meta['num']]]; - foreach(Arsse::$db->folderList(Arsse::$user->id, null, false) as $r) { + foreach (Arsse::$db->folderList(Arsse::$user->id, null, false) as $r) { $folders[(int) $r['id']] = [ 'id' => ((int) $r['id']) + 1, 'title' => $r['name'], 'user_id' => $meta['num'], ]; } + return $folders; + } + + protected function transformFeed(array $sub, array $folders): array { + $url = new Uri($sub['url']); + return [ + 'id' => (int) $sub['id'], + 'user_id' => $folders[0]['user_id'], + 'feed_url' => (string) $url->withUserInfo(""), + 'site_url' => (string) $sub['source'], + 'title' => (string) $sub['title'], + 'checked_at' => Date::transform($sub['updated'], "iso8601m", "sql"), + 'next_check_at' => Date::transform($sub['next_fetch'], "iso8601m", "sql") ?? "0001-01-01T00:00:00.000000Z", + 'etag_header' => (string) $sub['etag'], + 'last_modified_header' => (string) Date::transform($sub['edited'], "http", "sql"), + 'parsing_error_message' => (string) $sub['err_msg'], + 'parsing_error_count' => (int) $sub['err_count'], + 'scraper_rules' => "", + 'rewrite_rules' => "", + 'crawler' => (bool) $sub['scrape'], + 'blocklist_rules' => (string) $sub['block_rule'], + 'keeplist_rules' => (string) $sub['keep_rule'], + 'user_agent' => "", + 'username' => rawurldecode(explode(":", $url->getUserInfo(), 2)[0] ?? ""), + 'password' => rawurldecode(explode(":", $url->getUserInfo(), 2)[1] ?? ""), + 'disabled' => false, + 'ignore_http_cache' => false, + 'fetch_via_proxy' => false, + 'category' => $folders[(int) $sub['top_folder']], + 'icon' => $sub['icon_id'] ? ['feed_id' => (int) $sub['id'], 'icon_id' => (int) $sub['icon_id']] : null, + ]; + } + + protected function getFeeds(): ResponseInterface { + $tr = Arsse::$db->begin(); + // compile the list of folders; the feed list includes folder names + $folders = $this->mapFolders(); // next compile the list of feeds + $out = []; foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $r) { - $url = new Uri($r['url']); - $out = [ - 'id' => (int) $r['id'], - 'user_id' => $meta['num'], - 'feed_url' => $url->withUserInfo(""), - 'site_url' => $r['source'], - 'title' => $r['title'], - 'checked_at' => Date::transform($r['updated'], "iso8601", "sql"), - 'next_check_at' => Date::transform($r['next_fetch'], "iso8601", "sql") ?? "0001-01-01T00:00:00Z", - 'etag_header' => $r['etag'] ?? "", - 'last_modified_header' => (string) Date::transform($r['edited'], "http", "sql"), - 'parsing_error_message' => (string) $r['err_msg'], - 'parsing_error_count' => (int) $r['err_count'], - 'scraper_rules' => "", - 'rewrite_rules' => "", - 'crawler' => (bool) $r['scrape'], - 'blocklist_rules' => (string) $r['block_rule'], - 'keeplist_rules' => (string) $r['keep_rule'], - 'user_agent' => "", - 'username' => explode(":", $url->getUserInfo(), 2)[0] ?? "", - 'password' => explode(":", $url->getUserInfo(), 2)[1] ?? "", - 'disabled' => false, - 'ignore_http_cache' => false, - 'fetch_via_proxy' => false, - 'category' => $folders[$r['top_folder']], - 'icon' => $r['icon_id'] ? ['feed_id' => (int) $r['id'], 'icon_id' => (int) $r['icon_id']] : null, - ]; - return new Response($out); + $out[] = $this->transformFeed($r, $folders); } + return new Response($out); } public static function tokenGenerate(string $user, string $label): string { diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 496df378..54e03435 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -525,4 +525,80 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->articleMark("john.doe@example.com", ['read' => true], (new Context)->folder(2111)) ); } + + public function testListReeds(): void { + \Phake::when(Arsse::$db)->folderList->thenReturn(new Result([ + ['id' => 5, 'name' => "Cat Ook"], + ])); + \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result([ + ['id' => 1, 'feed' => 12, 'url' => "http://example.com/ook", 'title' => "Ook", 'source' => "http://example.com/", 'icon_id' => 47, 'icon_url' => "http://example.com/icon", 'folder' => 2112, 'top_folder' => 5, 'pinned' => 0, 'err_count' => 1, 'err_msg' => "Oopsie", 'order_type' => 0, 'keep_rule' => "this|that", 'block_rule' => "both", 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => "2021-01-01 00:00:00", 'modified' => "2020-11-30 04:08:52", 'next_fetch' => "2021-01-20 00:00:00", 'etag' => "OOKEEK", 'scrape' => 0, 'unread' => 42], + ['id' => 55, 'feed' => 12, 'url' => "http://j%20k:super%20secret@example.com/eek", 'title' => "Eek", 'source' => "http://example.com/", 'icon_id' => null, 'icon_url' => null, 'folder' => null, 'top_folder' => null, 'pinned' => 0, 'err_count' => 0, 'err_msg' => null, 'order_type' => 0, 'keep_rule' => null, 'block_rule' => null, 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => null, 'modified' => "2020-11-30 04:08:52", 'next_fetch' => null, 'etag' => null, 'scrape' => 1, 'unread' => 0], + ])); + $exp = new Response([ + [ + 'id' => 1, + 'user_id' => 42, + 'feed_url' => "http://example.com/ook", + 'site_url' => "http://example.com/", + 'title' => "Ook", + 'checked_at' => "2021-01-05T13:51:32.000000Z", + 'next_check_at' => "2021-01-20T00:00:00.000000Z", + 'etag_header' => "OOKEEK", + 'last_modified_header' => "Fri, 01 Jan 2021 00:00:00 GMT", + 'parsing_error_message' => "Oopsie", + 'parsing_error_count' => 1, + 'scraper_rules' => "", + 'rewrite_rules' => "", + 'crawler' => false, + 'blocklist_rules' => "both", + 'keeplist_rules' => "this|that", + 'user_agent' => "", + 'username' => "", + 'password' => "", + 'disabled' => false, + 'ignore_http_cache' => false, + 'fetch_via_proxy' => false, + 'category' => [ + 'id' => 6, + 'title' => "Cat Ook", + 'user_id' => 42 + ], + 'icon' => [ + 'feed_id' => 1, + 'icon_id' => 47 + ], + ], + [ + 'id' => 55, + 'user_id' => 42, + 'feed_url' => "http://example.com/eek", + 'site_url' => "http://example.com/", + 'title' => "Eek", + 'checked_at' => "2021-01-05T13:51:32.000000Z", + 'next_check_at' => "0001-01-01T00:00:00.000000Z", + 'etag_header' => "", + 'last_modified_header' => "", + 'parsing_error_message' => "", + 'parsing_error_count' => 0, + 'scraper_rules' => "", + 'rewrite_rules' => "", + 'crawler' => true, + 'blocklist_rules' => "", + 'keeplist_rules' => "", + 'user_agent' => "", + 'username' => "j k", + 'password' => "super secret", + 'disabled' => false, + 'ignore_http_cache' => false, + 'fetch_via_proxy' => false, + 'category' => [ + 'id' => 1, + 'title' => "All", + 'user_id' => 42 + ], + 'icon' => null, + ], + ]); + $this->assertMessage($exp, $this->req("GET", "/feeds")); + } } From e7b2f541839270183e267a2bd280995ee080d4e8 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 19 Jan 2021 23:17:03 -0500 Subject: [PATCH 120/366] Prototype feed creation --- .../030_Supported_Protocols/005_Miniflux.md | 5 +- lib/Database.php | 15 +++-- lib/REST/Miniflux/V1.php | 66 ++++++++++++++++--- locale/en.php | 4 +- tests/cases/Database/SeriesSubscription.php | 6 +- tests/cases/REST/Miniflux/TestV1.php | 22 ++++--- 6 files changed, 90 insertions(+), 28 deletions(-) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index a85a61ea..c3a19211 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -25,16 +25,17 @@ Miniflux version 2.0.27 is emulated, though not all features are implemented - Custom User-Agent strings - The `disabled`, `ignore_http_cache`, and `fetch_via_proxy` flags - Changing the URL, username, or password of a feed +- Titles and types are not available during feed discovery and are filled with generic data # Differences -- Various error messages differ due to significant implementation differences +- Various error codes and messages differ due to significant implementation differences - `PUT` requests which return a body respond with `200 OK` rather than `201 Created` -- Only the URL should be considered reliable in feed discovery results - The "All" category is treated specially (see below for details) - Category names consisting only of whitespace are rejected along with the empty string - Filtering rules may not function identically (see below for details) - The `checked_at` field of feeds indicates when the feed was last updated rather than when it was last checked +- Creating a feed with the `scrape` property set to `true` might not return scraped content for the initial synchronization # Behaviour of filtering (block and keep) rules diff --git a/lib/Database.php b/lib/Database.php index 3866ddaf..de829dba 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -745,10 +745,11 @@ class Database { * @param string $fetchUser The user name required to access the newsfeed, if applicable * @param string $fetchPassword The password required to fetch the newsfeed, if applicable; this will be stored in cleartext * @param boolean $discover Whether to perform newsfeed discovery if $url points to an HTML document + * @param boolean $scrape Whether the initial synchronization should scrape full-article content */ - public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = "", bool $discover = true): int { + public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = "", bool $discover = true, bool $scrape = false): int { // get the ID of the underlying feed, or add it if it's not yet in the database - $feedID = $this->feedAdd($url, $fetchUser, $fetchPassword, $discover); + $feedID = $this->feedAdd($url, $fetchUser, $fetchPassword, $discover, $scrape); // Add the feed to the user's subscriptions and return the new subscription's ID. return $this->db->prepare('INSERT INTO arsse_subscriptions(owner,feed) values(?,?)', 'str', 'int')->run($user, $feedID)->lastId(); } @@ -1089,8 +1090,9 @@ class Database { * @param string $fetchUser The user name required to access the newsfeed, if applicable * @param string $fetchPassword The password required to fetch the newsfeed, if applicable; this will be stored in cleartext * @param boolean $discover Whether to perform newsfeed discovery if $url points to an HTML document + * @param boolean $scrape Whether the initial synchronization should scrape full-article content */ - public function feedAdd(string $url, string $fetchUser = "", string $fetchPassword = "", bool $discover = true): int { + public function feedAdd(string $url, string $fetchUser = "", string $fetchPassword = "", bool $discover = true, bool $scrape = false): int { // normalize the input URL $url = URL::normalize($url); // check to see if the feed already exists @@ -1106,7 +1108,7 @@ class Database { $feedID = $this->db->prepare('INSERT INTO arsse_feeds(url,username,password) values(?,?,?)', 'str', 'str', 'str')->run($url, $fetchUser, $fetchPassword)->lastId(); try { // perform an initial update on the newly added feed - $this->feedUpdate($feedID, true); + $this->feedUpdate($feedID, true, $scrape); } catch (\Throwable $e) { // if the update fails, delete the feed we just added $this->db->prepare('DELETE from arsse_feeds where id = ?', 'int')->run($feedID); @@ -1126,8 +1128,9 @@ class Database { * * @param integer $feedID The numerical identifier of the newsfeed to refresh * @param boolean $throwError Whether to throw an exception on failure in addition to storing error information in the database + * @param boolean|null $scrapeOverride If not null, overrides information in the database signaling whether or not to scrape full-article content. This is intended for when there are no subscriptions for the feed in the database yet */ - public function feedUpdate($feedID, bool $throwError = false): bool { + public function feedUpdate($feedID, bool $throwError = false, ?bool $scrapeOverride = null): bool { // check to make sure the feed exists if (!V::id($feedID)) { throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID, 'type' => "int > 0"]); @@ -1144,7 +1147,7 @@ class Database { throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID]); } // determine whether the feed's items should be scraped for full content from the source Web site - $scrape = (Arsse::$conf->fetchEnableScraping && $f['scrapers']); + $scrape = (Arsse::$conf->fetchEnableScraping && ($scrapeOverride ?? $f['scrapers'])); // the Feed object throws an exception when there are problems, but that isn't ideal // here. When an exception is thrown it should update the database with the // error instead of failing; if other exceptions are thrown, we should simply roll back diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 43d9e36c..7d481da1 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -14,8 +14,10 @@ use JKingWeb\Arsse\Context\Context; use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Misc\HTTP; use JKingWeb\Arsse\Misc\Date; +use JKingWeb\Arsse\Misc\URL; use JKingWeb\Arsse\Misc\ValueInfo as V; use JKingWeb\Arsse\REST\Exception; +use JKingWeb\Arsse\Rule\Rule; use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\Exception as UserException; use Psr\Http\Message\ServerRequestInterface; @@ -31,12 +33,25 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected const ACCEPTED_TYPES_JSON = ["application/json"]; protected const TOKEN_LENGTH = 32; protected const VALID_JSON = [ - // user properties which map directly to Arsse user metadata are listed separately - 'url' => "string", - 'username' => "string", - 'password' => "string", - 'user_agent' => "string", - 'title' => "string", + // user properties which map directly to Arsse user metadata are listed separately; + // not all these properties are used by our implementation, but they are treated + // with the same strictness as in Miniflux to ease cross-compatibility + 'url' => "string", + 'username' => "string", + 'password' => "string", + 'user_agent' => "string", + 'title' => "string", + 'feed_url' => "string", + 'category_id' => "integer", + 'crawler' => "boolean", + 'user_agent' => "string", + 'scraper_rules' => "string", + 'rewrite_rules' => "string", + 'keeplist_rules' => "string", + 'blocklist_rules' => "string", + 'disabled' => "boolean", + 'ignore_http_cache' => "boolean", + 'fetch_via_proxy' => "boolean", ]; protected const USER_META_MAP = [ // Miniflux ID // Arsse ID Default value Extra @@ -81,7 +96,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { ], '/feeds' => [ 'GET' => ["getFeeds", false, false, false, false, []], - 'POST' => ["createFeed", false, false, true, false, []], + 'POST' => ["createFeed", false, false, true, false, ["feed_url", "category_id"]], ], '/feeds/1' => [ 'GET' => ["getFeed", false, true, false, false, []], @@ -263,6 +278,10 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $body[$k] = null; } elseif (gettype($body[$k]) !== $t) { return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422); + } elseif (in_array($k, ["keeplist_rules", "blocklist_rules"]) && !Rule::validate($body[$k])) { + return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422); + } elseif (in_array($k, ["url", "feed_url"]) && !URL::absolute($body[$k])) { + return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422); } } //normalize user-specific input @@ -377,7 +396,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 10506 => "Fetch403", 10507 => "Fetch401", ][$e->getCode()] ?? "FetchOther"; - return new ErrorResponse($msg, 500); + return new ErrorResponse($msg, 502); } $out = []; foreach ($list as $url) { @@ -646,6 +665,37 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($out); } + protected function createFeed(array $data): ResponseInterface { + $props = [ + 'keep_rule' => $data['keeplist_rules'], + 'block_rule' => $data['blocklist_rules'], + 'folder' => $data['category_id'] - 1, + 'scrape' => (bool) $data['crawler'], + ]; + try { + Arsse::$db->feedAdd($data['feed_url'], (string) $data['username'], (string) $data['password'], false, (bool) $data['crawler']); + $tr = Arsse::$db->begin(); + $id = Arsse::$db->subscriptionAdd(Arsse::$user->id, $data['feed_url'], (string) $data['username'], (string) $data['password'], false, (bool) $data['crawler']); + Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, $props); + $tr->commit(); + } catch (FeedException $e) { + $msg = [ + 10502 => "Fetch404", + 10506 => "Fetch403", + 10507 => "Fetch401", + ][$e->getCode()] ?? "FetchOther"; + return new ErrorResponse($msg, 502); + } catch (ExceptionInput $e) { + switch ($e->getCode()) { + case 10235: + return new ErrorResponse("MissingCategory", 422); + case 10236: + return new ErrorResponse("DuplicateFeed", 409); + } + } + return new Response(['feed_id' => $id], 201); + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokens in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); diff --git a/locale/en.php b/locale/en.php index b8098959..1f917c4c 100644 --- a/locale/en.php +++ b/locale/en.php @@ -19,10 +19,12 @@ return [ 'API.Miniflux.Error.Fetch401' => 'You are not authorized to access this resource (invalid username/password)', 'API.Miniflux.Error.Fetch403' => 'Unable to fetch this resource (Status Code = 403)', 'API.Miniflux.Error.FetchOther' => 'Unable to fetch this resource', - 'API.Miniflux.Error.DuplicateCategory' => 'Category "{title}" already exists', + 'API.Miniflux.Error.DuplicateCategory' => 'This category already exists.', 'API.Miniflux.Error.InvalidCategory' => 'Invalid category title "{title}"', + 'API.Miniflux.Error.MissingCategory' => 'This category does not exist or does not belong to this user.', 'API.Miniflux.Error.InvalidElevation' => 'Only administrators can change permissions of standard users', 'API.Miniflux.Error.DuplicateUser' => 'The user name "{user}" already exists', + 'API.Miniflux.Error.DuplicateFeed' => 'This feed already exists.', 'API.TTRSS.Category.Uncategorized' => 'Uncategorized', 'API.TTRSS.Category.Special' => 'Special', diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index 389495d3..3cdeeead 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -221,7 +221,7 @@ trait SeriesSubscription { $subID = $this->nextID("arsse_subscriptions"); \Phake::when(Arsse::$db)->feedUpdate->thenReturn(true); $this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url, "", "", false)); - \Phake::verify(Arsse::$db)->feedUpdate($feedID, true); + \Phake::verify(Arsse::$db)->feedUpdate($feedID, true, false); $state = $this->primeExpectations($this->data, [ 'arsse_feeds' => ['id','url','username','password'], 'arsse_subscriptions' => ['id','owner','feed'], @@ -238,7 +238,7 @@ trait SeriesSubscription { $subID = $this->nextID("arsse_subscriptions"); \Phake::when(Arsse::$db)->feedUpdate->thenReturn(true); $this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url, "", "", true)); - \Phake::verify(Arsse::$db)->feedUpdate($feedID, true); + \Phake::verify(Arsse::$db)->feedUpdate($feedID, true, false); $state = $this->primeExpectations($this->data, [ 'arsse_feeds' => ['id','url','username','password'], 'arsse_subscriptions' => ['id','owner','feed'], @@ -256,7 +256,7 @@ trait SeriesSubscription { try { Arsse::$db->subscriptionAdd($this->user, $url, "", "", false); } finally { - \Phake::verify(Arsse::$db)->feedUpdate($feedID, true); + \Phake::verify(Arsse::$db)->feedUpdate($feedID, true, false); $state = $this->primeExpectations($this->data, [ 'arsse_feeds' => ['id','url','username','password'], 'arsse_subscriptions' => ['id','owner','feed'], diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 54e03435..fbcf82c9 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -172,16 +172,22 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => 2112])); } - public function testDiscoverFeeds(): void { - $exp = new Response([ + /** @dataProvider provideDiscoveries */ + public function testDiscoverFeeds($in, ResponseInterface $exp): void { + $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => $in])); + } + + public function provideDiscoveries(): iterable { + self::clearData(); + $discovered = [ ['title' => "Feed", 'type' => "rss", 'url' => "http://localhost:8000/Feed/Discovery/Feed"], ['title' => "Feed", 'type' => "rss", 'url' => "http://localhost:8000/Feed/Discovery/Missing"], - ]); - $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Valid"])); - $exp = new Response([]); - $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Invalid"])); - $exp = new ErrorResponse("Fetch404", 500); - $this->assertMessage($exp, $this->req("POST", "/discover", ['url' => "http://localhost:8000/Feed/Discovery/Missing"])); + ]; + return [ + ["http://localhost:8000/Feed/Discovery/Valid", new Response($discovered)], + ["http://localhost:8000/Feed/Discovery/Invalid", new Response([])], + ["http://localhost:8000/Feed/Discovery/Missing", new ErrorResponse("Fetch404", 502)], + ]; } /** @dataProvider provideUserQueries */ From fd25be5c27346ef3f2a67b3f829376edd1243dfd Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 20 Jan 2021 18:28:51 -0500 Subject: [PATCH 121/366] Basic tests for feed creation --- lib/REST/Miniflux/V1.php | 8 ++-- tests/cases/REST/Miniflux/TestV1.php | 62 +++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 7d481da1..44c8c3b8 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -278,9 +278,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $body[$k] = null; } elseif (gettype($body[$k]) !== $t) { return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422); - } elseif (in_array($k, ["keeplist_rules", "blocklist_rules"]) && !Rule::validate($body[$k])) { - return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422); - } elseif (in_array($k, ["url", "feed_url"]) && !URL::absolute($body[$k])) { + } elseif ( + (in_array($k, ["keeplist_rules", "blocklist_rules"]) && !Rule::validate($body[$k])) || + (in_array($k, ["url", "feed_url"]) && !URL::absolute($body[$k])) || + ($k === "category_id" && $body[$k] < 1) + ) { return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422); } } diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index fbcf82c9..84965ad0 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -184,9 +184,12 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ['title' => "Feed", 'type' => "rss", 'url' => "http://localhost:8000/Feed/Discovery/Missing"], ]; return [ - ["http://localhost:8000/Feed/Discovery/Valid", new Response($discovered)], + ["http://localhost:8000/Feed/Discovery/Valid", new Response($discovered)], ["http://localhost:8000/Feed/Discovery/Invalid", new Response([])], ["http://localhost:8000/Feed/Discovery/Missing", new ErrorResponse("Fetch404", 502)], + [1, new ErrorResponse(["InvalidInputType", 'field' => "url", 'expected' => "string", 'actual' => "integer"], 422)], + ["Not a URL", new ErrorResponse(["InvalidInputValue", 'field' => "url"], 422)], + [null, new ErrorResponse(["MissingInputValue", 'field' => "url"], 422)], ]; } @@ -607,4 +610,61 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ]); $this->assertMessage($exp, $this->req("GET", "/feeds")); } + + /** @dataProvider provideFeedCreations */ + public function testCreateAFeed(array $in, $out1, $out2, $out3, ResponseInterface $exp): void { + if ($out1 instanceof \Exception) { + \Phake::when(Arsse::$db)->feedAdd->thenThrow($out1); + } else { + \Phake::when(Arsse::$db)->feedAdd->thenReturn($out1); + } + if ($out2 instanceof \Exception) { + \Phake::when(Arsse::$db)->subscriptionAdd->thenThrow($out2); + } else { + \Phake::when(Arsse::$db)->subscriptionAdd->thenReturn($out2); + } + if ($out3 instanceof \Exception) { + \Phake::when(Arsse::$db)->subscriptionPropertiesSet->thenThrow($out3); + } else { + \Phake::when(Arsse::$db)->subscriptionPropertiesSet->thenReturn($out3); + } + $this->assertMessage($exp, $this->req("POST", "/feeds", $in)); + $in1 = $out1 !== null; + $in2 = $out2 !== null; + $in3 = $out3 !== null; + if ($in1) { + \Phake::verify(Arsse::$db)->feedAdd($in['feed_url'], $in['username'] ?? "", $in['password'] ?? "", true, $in['crawler'] ?? false); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->feedAdd; + } + if ($in2) { + \Phake::verify(Arsse::$db)->subscriptionAdd("john.doe@example.com", $in['feed_url'], $in['username'] ?? "", $in['password'] ?? "", true, $in['crawler'] ?? false); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->subscriptionAdd; + } + if ($in3) { + $props = [ + 'keep_rule' => $in['keeplist_rules'], + 'block_rule' => $in['blocklist_rules'], + 'folder' => $in['category_id'] - 1, + 'scrape' => $in['crawler'] ?? false, + ]; + \Phake::verify(Arsse::$db)->subscriptionPropertiesSet("john.doe@example.com", $out2, $props); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->subscriptionPropertiesSet; + } + } + + public function provideFeedCreations(): iterable { + self::clearData(); + return [ + [['category_id' => 1], null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "feed_url"], 422)], + [['feed_url' => "http://example.com/"], null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "category_id"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => "1"], null, null, null, new ErrorResponse(["InvalidInputType", 'field' => "category_id", 'expected' => "integer", 'actual' => "string"], 422)], + [['feed_url' => "Not a URL", 'category_id' => 1], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "feed_url"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 0], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "["], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "keeplist_rules"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "["], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "blocklist_rules"], 422)], + ]; + } } From 6936f365e4b10d565cd8ea81e264a54e4a7b8e5a Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 21 Jan 2021 11:11:25 -0500 Subject: [PATCH 122/366] Add calls coming in next version of Miniflux --- lib/REST/Miniflux/V1.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 44c8c3b8..fdcac8a6 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -75,6 +75,15 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'PUT' => ["updateCategory", false, true, true, false, ["title"]], // title is effectively required since no other field can be changed 'DELETE' => ["deleteCategory", false, true, false, false, []], ], + '/categories/1/entries' => [ + 'GET' => ["getCategoryEntries", false, false, false, true, []], + ], + '/categories/1/entries/1' => [ + 'GET' => ["getCategoryEntry", false, false, false, true, []], + ], + '/categories/1/feeds' => [ + 'GET' => ["getCategoryFeeds", false, false, false, true, []], + ], '/categories/1/mark-all-as-read' => [ 'PUT' => ["markCategory", false, true, false, false, []], ], From 4972c79e321c4c56e9ecad489b1d1863598538a7 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 21 Jan 2021 22:44:22 -0500 Subject: [PATCH 123/366] Allow simpler feed exception creation --- lib/Feed.php | 10 ++--- lib/Feed/Exception.php | 45 +++++++++++---------- tests/cases/CLI/TestCLI.php | 2 +- tests/cases/Database/SeriesSubscription.php | 2 +- tests/cases/Feed/TestException.php | 12 +++--- tests/cases/REST/NextcloudNews/TestV1_2.php | 2 +- tests/cases/REST/TinyTinyRSS/TestAPI.php | 14 +++---- tests/lib/FeedException.php | 15 +++++++ 8 files changed, 60 insertions(+), 42 deletions(-) create mode 100644 tests/lib/FeedException.php diff --git a/lib/Feed.php b/lib/Feed.php index af43f22e..ce4ab4c1 100644 --- a/lib/Feed.php +++ b/lib/Feed.php @@ -39,7 +39,7 @@ class Feed { if (!$links) { // work around a PicoFeed memory leak libxml_use_internal_errors(false); - throw new Feed\Exception($url, new \PicoFeed\Reader\SubscriptionNotFoundException('Unable to find a subscription')); + throw new Feed\Exception("", ['url' => $url], new \PicoFeed\Reader\SubscriptionNotFoundException('Unable to find a subscription')); } else { $out = $links[0]; } @@ -119,9 +119,9 @@ class Feed { $client->reader = $reader; return $client; } catch (PicoFeedException $e) { - throw new Feed\Exception($url, $e); // @codeCoverageIgnore + throw new Feed\Exception("", ['url' => $url], $e); // @codeCoverageIgnore } catch (\GuzzleHttp\Exception\GuzzleException $e) { - throw new Feed\Exception($url, $e); + throw new Feed\Exception("", ['url' => $url], $e); } } @@ -133,9 +133,9 @@ class Feed { $this->resource->getEncoding() )->execute(); } catch (PicoFeedException $e) { - throw new Feed\Exception($this->resource->getUrl(), $e); + throw new Feed\Exception("", ['url' => $this->resource->getUrl()], $e); } catch (\GuzzleHttp\Exception\GuzzleException $e) { // @codeCoverageIgnore - throw new Feed\Exception($this->resource->getUrl(), $e); // @codeCoverageIgnore + throw new Feed\Exception("", ['url' => $this->resource->getUrl()], $e); // @codeCoverageIgnore } // Grab the favicon for the feed, or null if no valid icon is found diff --git a/lib/Feed/Exception.php b/lib/Feed/Exception.php index 2bf181e6..1a8e68fc 100644 --- a/lib/Feed/Exception.php +++ b/lib/Feed/Exception.php @@ -15,30 +15,33 @@ class Exception extends \JKingWeb\Arsse\AbstractException { protected const CURL_ERROR_MAP = [1 => "invalidUrl",3 => "invalidUrl",5 => "transmissionError","connectionFailed","connectionFailed","transmissionError","forbidden","unauthorized","transmissionError","transmissionError","transmissionError","transmissionError","connectionFailed","connectionFailed","transmissionError","transmissionError","transmissionError","transmissionError","transmissionError","invalidUrl","transmissionError","transmissionError","transmissionError","transmissionError",28 => "timeout","transmissionError","transmissionError","transmissionError","transmissionError","transmissionError",35 => "invalidCertificate","transmissionError","transmissionError","transmissionError","transmissionError",45 => "transmissionError","unauthorized","maxRedirect",52 => "transmissionError","invalidCertificate","invalidCertificate","transmissionError","transmissionError",58 => "invalidCertificate","invalidCertificate","invalidCertificate","transmissionError","invalidUrl","transmissionError","invalidCertificate","transmissionError","invalidCertificate","forbidden","invalidUrl","forbidden","transmissionError",73 => "transmissionError","transmissionError",77 => "invalidCertificate","invalidUrl",90 => "invalidCertificate","invalidCertificate","transmissionError",94 => "unauthorized","transmissionError","connectionFailed"]; protected const HTTP_ERROR_MAP = [401 => "unauthorized",403 => "forbidden",404 => "invalidUrl",408 => "timeout",410 => "invalidUrl",414 => "invalidUrl",451 => "invalidUrl"]; - public function __construct($url, \Throwable $e) { - if ($e instanceof BadResponseException) { - $msgID = self::HTTP_ERROR_MAP[$e->getCode()] ?? "transmissionError"; - } elseif ($e instanceof TooManyRedirectsException) { - $msgID = "maxRedirect"; - } elseif ($e instanceof GuzzleException) { - $msg = $e->getMessage(); - if (preg_match("/^Error creating resource:/", $msg)) { - // PHP stream error; the class of error is ambiguous - $msgID = "transmissionError"; - } elseif (preg_match("/^cURL error (\d+):/", $msg, $match)) { - $msgID = self::CURL_ERROR_MAP[(int) $match[1]] ?? "internalError"; + public function __construct(string $msgID = "", $vars, \Throwable $e) { + if ($msgID === "") { + assert($e !== null, new \Exception("Expecting Picofeed or Guzzle exception when no message specified.")); + if ($e instanceof BadResponseException) { + $msgID = self::HTTP_ERROR_MAP[$e->getCode()] ?? "transmissionError"; + } elseif ($e instanceof TooManyRedirectsException) { + $msgID = "maxRedirect"; + } elseif ($e instanceof GuzzleException) { + $msg = $e->getMessage(); + if (preg_match("/^Error creating resource:/", $msg)) { + // PHP stream error; the class of error is ambiguous + $msgID = "transmissionError"; + } elseif (preg_match("/^cURL error (\d+):/", $msg, $match)) { + $msgID = self::CURL_ERROR_MAP[(int) $match[1]] ?? "internalError"; + } else { + $msgID = "internalError"; + } + } elseif ($e instanceof PicoFeedException) { + $className = get_class($e); + // Convert the exception thrown by PicoFeed to the one to be thrown here. + $msgID = preg_replace('/^PicoFeed\\\(?:Client|Parser|Reader)\\\([A-Za-z]+)Exception$/', '$1', $className); + // If the message ID doesn't change then it's unknown. + $msgID = ($msgID !== $className) ? lcfirst($msgID) : "internalError"; } else { $msgID = "internalError"; } - } elseif ($e instanceof PicoFeedException) { - $className = get_class($e); - // Convert the exception thrown by PicoFeed to the one to be thrown here. - $msgID = preg_replace('/^PicoFeed\\\(?:Client|Parser|Reader)\\\([A-Za-z]+)Exception$/', '$1', $className); - // If the message ID doesn't change then it's unknown. - $msgID = ($msgID !== $className) ? lcfirst($msgID) : "internalError"; - } else { - $msgID = "internalError"; } - parent::__construct($msgID, ['url' => $url], $e); + parent::__construct($msgID, $vars, $e); } } diff --git a/tests/cases/CLI/TestCLI.php b/tests/cases/CLI/TestCLI.php index 671fbb91..3d0d6f14 100644 --- a/tests/cases/CLI/TestCLI.php +++ b/tests/cases/CLI/TestCLI.php @@ -82,7 +82,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { public function testRefreshAFeed(string $cmd, int $exitStatus, string $output): void { Arsse::$db = \Phake::mock(Database::class); \Phake::when(Arsse::$db)->feedUpdate(1, true)->thenReturn(true); - \Phake::when(Arsse::$db)->feedUpdate(2, true)->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/", $this->mockGuzzleException(ClientException::class, "", 404))); + \Phake::when(Arsse::$db)->feedUpdate(2, true)->thenThrow(new \JKingWeb\Arsse\Feed\Exception("", ['url' => "http://example.com/"], $this->mockGuzzleException(ClientException::class, "", 404))); $this->assertConsole($this->cli, $cmd, $exitStatus, $output); \Phake::verify($this->cli)->loadConf; \Phake::verify(Arsse::$db)->feedUpdate; diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index 3cdeeead..c009b600 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -251,7 +251,7 @@ trait SeriesSubscription { public function testAddASubscriptionToAnInvalidFeed(): void { $url = "http://example.org/feed1"; $feedID = $this->nextID("arsse_feeds"); - \Phake::when(Arsse::$db)->feedUpdate->thenThrow(new FeedException($url, $this->mockGuzzleException(ClientException::class, "", 404))); + \Phake::when(Arsse::$db)->feedUpdate->thenThrow(new FeedException("", ['url' => $url], $this->mockGuzzleException(ClientException::class, "", 404))); $this->assertException("invalidUrl", "Feed"); try { Arsse::$db->subscriptionAdd($this->user, $url, "", "", false); diff --git a/tests/cases/Feed/TestException.php b/tests/cases/Feed/TestException.php index 95adde1d..b28d0d1d 100644 --- a/tests/cases/Feed/TestException.php +++ b/tests/cases/Feed/TestException.php @@ -20,7 +20,7 @@ class TestException extends \JKingWeb\Arsse\Test\AbstractTest { public function testHandleCurlErrors(int $code, string $message): void { $e = $this->mockGuzzleException(TransferException::class, "cURL error $code: Some message", 0); $this->assertException($message, "Feed"); - throw new FeedException("https://example.com/", $e); + throw new FeedException("", ['url' => "https://example.com/"], $e); } public function provideCurlErrors() { @@ -119,7 +119,7 @@ class TestException extends \JKingWeb\Arsse\Test\AbstractTest { public function testHandleHttpErrors(int $code, string $message): void { $e = $this->mockGuzzleException(BadResponseException::class, "Irrelevant message", $code); $this->assertException($message, "Feed"); - throw new FeedException("https://example.com/", $e); + throw new FeedException("", ['url' => "https://example.com/"], $e); } public function provideHTTPErrors() { @@ -145,7 +145,7 @@ class TestException extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider providePicoFeedException */ public function testHandlePicofeedException(PicoFeedException $e, string $message) { $this->assertException($message, "Feed"); - throw new FeedException("https://example.com/", $e); + throw new FeedException("", ['url' => "https://example.com/"], $e); } public function providePicoFeedException() { @@ -160,18 +160,18 @@ class TestException extends \JKingWeb\Arsse\Test\AbstractTest { public function testHandleExcessRedirections() { $e = $this->mockGuzzleException(TooManyRedirectsException::class, "Irrelevant message", 404); $this->assertException("maxRedirect", "Feed"); - throw new FeedException("https://example.com/", $e); + throw new FeedException("", ['url' => "https://example.com/"], $e); } public function testHandleGenericStreamErrors() { $e = $this->mockGuzzleException(TransferException::class, "Error creating resource: Irrelevant message", 403); $this->assertException("transmissionError", "Feed"); - throw new FeedException("https://example.com/", $e); + throw new FeedException("", ['url' => "https://example.com/"], $e); } public function testHandleUnexpectedError() { $e = new \Exception; $this->assertException("internalError", "Feed"); - throw new FeedException("https://example.com/", $e); + throw new FeedException("", ['url' => "https://example.com/"], $e); } } diff --git a/tests/cases/REST/NextcloudNews/TestV1_2.php b/tests/cases/REST/NextcloudNews/TestV1_2.php index eccc0cc1..7b4ce328 100644 --- a/tests/cases/REST/NextcloudNews/TestV1_2.php +++ b/tests/cases/REST/NextcloudNews/TestV1_2.php @@ -534,7 +534,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { } public function provideNewSubscriptions(): array { - $feedException = new \JKingWeb\Arsse\Feed\Exception("", new \PicoFeed\Reader\SubscriptionNotFoundException); + $feedException = new \JKingWeb\Arsse\Feed\Exception("", [], new \PicoFeed\Reader\SubscriptionNotFoundException); return [ [['url' => "http://example.com/news.atom", 'folderId' => 3], 2112, 0, $this->feeds['db'][0], new ExceptionInput("idMissing"), new Response(['feeds' => [$this->feeds['rest'][0]]])], [['url' => "http://example.org/news.atom", 'folderId' => 8], 42, 4758915, $this->feeds['db'][1], true, new Response(['feeds' => [$this->feeds['rest'][1]], 'newestItemId' => 4758915])], diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index ff6d8954..6191d84b 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -787,13 +787,13 @@ LONG_STRING; ]; $out = [ ['code' => 1, 'feed_id' => 2], - ['code' => 5, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://example.com/1", $this->mockGuzzleException(ClientException::class, "", 401)))->getMessage()], + ['code' => 5, 'message' => (new \JKingWeb\Arsse\Feed\Exception("", ['url' => "http://example.com/1"], $this->mockGuzzleException(ClientException::class, "", 401)))->getMessage()], ['code' => 1, 'feed_id' => 0], ['code' => 0, 'feed_id' => 3], ['code' => 0, 'feed_id' => 1], - ['code' => 3, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://localhost:8000/Feed/Discovery/Invalid", new \PicoFeed\Reader\SubscriptionNotFoundException()))->getMessage()], - ['code' => 2, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://example.com/6", $this->mockGuzzleException(ClientException::class, "", 404)))->getMessage()], - ['code' => 6, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://example.com/7", new \PicoFeed\Parser\MalformedXmlException()))->getMessage()], + ['code' => 3, 'message' => (new \JKingWeb\Arsse\Feed\Exception("", ['url' => "http://localhost:8000/Feed/Discovery/Invalid"], new \PicoFeed\Reader\SubscriptionNotFoundException()))->getMessage()], + ['code' => 2, 'message' => (new \JKingWeb\Arsse\Feed\Exception("", ['url' => "http://example.com/6"], $this->mockGuzzleException(ClientException::class, "", 404)))->getMessage()], + ['code' => 6, 'message' => (new \JKingWeb\Arsse\Feed\Exception("", ['url' => "http://example.com/7"], new \PicoFeed\Parser\MalformedXmlException()))->getMessage()], ['code' => 1, 'feed_id' => 4], ['code' => 0, 'feed_id' => 4], ]; @@ -804,13 +804,13 @@ LONG_STRING; ['id' => 4, 'url' => "http://example.com/9"], ]; \Phake::when(Arsse::$db)->subscriptionAdd(...$db[0])->thenReturn(2); - \Phake::when(Arsse::$db)->subscriptionAdd(...$db[1])->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/1", $this->mockGuzzleException(ClientException::class, "", 401))); + \Phake::when(Arsse::$db)->subscriptionAdd(...$db[1])->thenThrow(new \JKingWeb\Arsse\Feed\Exception("", ['url' => "http://example.com/1"], $this->mockGuzzleException(ClientException::class, "", 401))); \Phake::when(Arsse::$db)->subscriptionAdd(...$db[2])->thenReturn(2); \Phake::when(Arsse::$db)->subscriptionAdd(...$db[3])->thenThrow(new ExceptionInput("constraintViolation")); \Phake::when(Arsse::$db)->subscriptionAdd(...$db[4])->thenThrow(new ExceptionInput("constraintViolation")); \Phake::when(Arsse::$db)->subscriptionAdd(...$db[5])->thenThrow(new ExceptionInput("constraintViolation")); - \Phake::when(Arsse::$db)->subscriptionAdd(...$db[6])->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/6", $this->mockGuzzleException(ClientException::class, "", 404))); - \Phake::when(Arsse::$db)->subscriptionAdd(...$db[7])->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/7", new \PicoFeed\Parser\MalformedXmlException())); + \Phake::when(Arsse::$db)->subscriptionAdd(...$db[6])->thenThrow(new \JKingWeb\Arsse\Feed\Exception("", ['url' => "http://example.com/6"], $this->mockGuzzleException(ClientException::class, "", 404))); + \Phake::when(Arsse::$db)->subscriptionAdd(...$db[7])->thenThrow(new \JKingWeb\Arsse\Feed\Exception("", ['url' => "http://example.com/7"], new \PicoFeed\Parser\MalformedXmlException())); \Phake::when(Arsse::$db)->subscriptionAdd(...$db[8])->thenReturn(4); \Phake::when(Arsse::$db)->subscriptionAdd(...$db[9])->thenThrow(new ExceptionInput("constraintViolation")); \Phake::when(Arsse::$db)->folderPropertiesGet(Arsse::$user->id, 42)->thenReturn($this->v(['id' => 42])); diff --git a/tests/lib/FeedException.php b/tests/lib/FeedException.php new file mode 100644 index 00000000..414dbe43 --- /dev/null +++ b/tests/lib/FeedException.php @@ -0,0 +1,15 @@ + Date: Fri, 22 Jan 2021 18:24:33 -0500 Subject: [PATCH 124/366] Implement feed listing by category Also modify user list to reflect changes in Miniflux 2.0.27. --- lib/REST/Miniflux/V1.php | 85 +++++++++++------- tests/cases/REST/Miniflux/TestV1.php | 125 ++++++++++----------------- tests/lib/FeedException.php | 15 ---- 3 files changed, 102 insertions(+), 123 deletions(-) delete mode 100644 tests/lib/FeedException.php diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index fdcac8a6..c9a4fdde 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -54,17 +54,17 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'fetch_via_proxy' => "boolean", ]; protected const USER_META_MAP = [ - // Miniflux ID // Arsse ID Default value Extra - 'is_admin' => ["admin", false, false], - 'theme' => ["theme", "light_serif", false], - 'language' => ["lang", "en_US", false], - 'timezone' => ["tz", "UTC", false], - 'entry_sorting_direction' => ["sort_asc", false, false], - 'entries_per_page' => ["page_size", 100, false], - 'keyboard_shortcuts' => ["shortcuts", true, false], - 'show_reading_time' => ["reading_time", true, false], - 'entry_swipe' => ["swipe", true, false], - 'custom_css' => ["stylesheet", "", true], + // Miniflux ID // Arsse ID Default value + 'is_admin' => ["admin", false], + 'theme' => ["theme", "light_serif"], + 'language' => ["lang", "en_US"], + 'timezone' => ["tz", "UTC"], + 'entry_sorting_direction' => ["sort_asc", false], + 'entries_per_page' => ["page_size", 100], + 'keyboard_shortcuts' => ["shortcuts", true], + 'show_reading_time' => ["reading_time", true], + 'entry_swipe' => ["swipe", true], + 'stylesheet' => ["stylesheet", ""], ]; protected const CALLS = [ // handler method Admin Path Body Query Required fields '/categories' => [ @@ -76,13 +76,13 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'DELETE' => ["deleteCategory", false, true, false, false, []], ], '/categories/1/entries' => [ - 'GET' => ["getCategoryEntries", false, false, false, true, []], + 'GET' => ["getCategoryEntries", false, true, false, false, []], ], '/categories/1/entries/1' => [ - 'GET' => ["getCategoryEntry", false, false, false, true, []], + 'GET' => ["getCategoryEntry", false, true, false, false, []], ], '/categories/1/feeds' => [ - 'GET' => ["getCategoryFeeds", false, false, false, true, []], + 'GET' => ["getCategoryFeeds", false, true, false, false, []], ], '/categories/1/mark-all-as-read' => [ 'PUT' => ["markCategory", false, true, false, false, []], @@ -354,16 +354,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'id' => $info['num'], 'username' => $u, 'last_login_at' => $now, + 'google_id' => "", + 'openid_connect_id' => "", ]; - foreach (self::USER_META_MAP as $ext => [$int, $default, $extra]) { - if (!$extra) { - $entry[$ext] = $info[$int] ?? $default; - } else { - if (!isset($entry['extra'])) { - $entry['extra'] = []; - } - $entry['extra'][$ext] = $info[$int] ?? $default; - } + foreach (self::USER_META_MAP as $ext => [$int, $default]) { + $entry[$ext] = $info[$int] ?? $default; } $entry['entry_sorting_direction'] = ($entry['entry_sorting_direction']) ? "asc" : "desc"; $out[] = $entry; @@ -530,15 +525,21 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } - protected function getCategories(): ResponseInterface { - $out = []; + protected function baseCategory(): array { + // the root folder is always a category and is always ID 1 + // the specific formulation is verbose, so a function makes sense $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); + return ['id' => 1, 'title' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName"), 'user_id' => $meta['num']]; + } + + protected function getCategories(): ResponseInterface { // add the root folder as a category - $out[] = ['id' => 1, 'title' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName"), 'user_id' => $meta['num']]; + $out = [$this->baseCategory()]; + $num = $out[0]['user_id']; // add other top folders as categories foreach (Arsse::$db->folderList(Arsse::$user->id, null, false) as $f) { // always add 1 to the ID since the root folder will always be 1 instead of 0. - $out[] = ['id' => $f['id'] + 1, 'title' => $f['name'], 'user_id' => $meta['num']]; + $out[] = ['id' => $f['id'] + 1, 'title' => $f['name'], 'user_id' => $num]; } return new Response($out); } @@ -622,13 +623,13 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function mapFolders(): array { - $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); - $folders = [0 => ['id' => 1, 'title' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName"), 'user_id' => $meta['num']]]; + $folders = [0 => $this->baseCategory()]; + $num = $folders[0]['user_id']; foreach (Arsse::$db->folderList(Arsse::$user->id, null, false) as $r) { $folders[(int) $r['id']] = [ 'id' => ((int) $r['id']) + 1, 'title' => $r['name'], - 'user_id' => $meta['num'], + 'user_id' => $num, ]; } return $folders; @@ -676,6 +677,30 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($out); } + protected function getCategoryFeeds(array $path): ResponseInterface { + // transform the category number into a folder number by subtracting one + $folder = ((int) $path[1]) - 1; + // unless the folder is root, list recursive + $recursive = $folder > 0; + $tr = Arsse::$db->begin(); + // get the list of subscriptions, or bail\ + try { + $subs = Arsse::$db->subscriptionList(Arsse::$user->id, $folder, $recursive)->getAll(); + } catch (ExceptionInput $e) { + // the folder does not exist + return new EmptyResponse(404); + } + // compile the list of folders; the feed list includes folder names + // NOTE: We compile the full list of folders in case someone has manually selected a non-top folder + $folders = $this->mapFolders(); + // next compile the list of feeds + $out = []; + foreach ($subs as $r) { + $out[] = $this->transformFeed($r, $folders); + } + return new Response($out); + } + protected function createFeed(array $data): ResponseInterface { $props = [ 'keep_rule' => $data['keeplist_rules'], diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 84965ad0..0bc1d50b 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -15,6 +15,7 @@ use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\REST\Miniflux\V1; use JKingWeb\Arsse\REST\Miniflux\ErrorResponse; +use JKingWeb\Arsse\Test\FeedException; use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\ExceptionInput as UserExceptionInput; use Psr\Http\Message\ResponseInterface; @@ -34,6 +35,8 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { 'id' => 1, 'username' => "john.doe@example.com", 'last_login_at' => self::NOW, + 'google_id' => "", + 'openid_connect_id' => "", 'is_admin' => true, 'theme' => "custom", 'language' => "fr_CA", @@ -43,14 +46,14 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { 'keyboard_shortcuts' => false, 'show_reading_time' => false, 'entry_swipe' => false, - 'extra' => [ - 'custom_css' => "p {}", - ], + 'stylesheet' => "p {}", ], [ 'id' => 2, 'username' => "jane.doe@example.com", 'last_login_at' => self::NOW, + 'google_id' => "", + 'openid_connect_id' => "", 'is_admin' => false, 'theme' => "light_serif", 'language' => "en_US", @@ -60,11 +63,17 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { 'keyboard_shortcuts' => true, 'show_reading_time' => true, 'entry_swipe' => true, - 'extra' => [ - 'custom_css' => "", - ], + 'stylesheet' => "", ], ]; + protected $feeds = [ + ['id' => 1, 'feed' => 12, 'url' => "http://example.com/ook", 'title' => "Ook", 'source' => "http://example.com/", 'icon_id' => 47, 'icon_url' => "http://example.com/icon", 'folder' => 2112, 'top_folder' => 5, 'pinned' => 0, 'err_count' => 1, 'err_msg' => "Oopsie", 'order_type' => 0, 'keep_rule' => "this|that", 'block_rule' => "both", 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => "2021-01-01 00:00:00", 'modified' => "2020-11-30 04:08:52", 'next_fetch' => "2021-01-20 00:00:00", 'etag' => "OOKEEK", 'scrape' => 0, 'unread' => 42], + ['id' => 55, 'feed' => 12, 'url' => "http://j%20k:super%20secret@example.com/eek", 'title' => "Eek", 'source' => "http://example.com/", 'icon_id' => null, 'icon_url' => null, 'folder' => null, 'top_folder' => null, 'pinned' => 0, 'err_count' => 0, 'err_msg' => null, 'order_type' => 0, 'keep_rule' => null, 'block_rule' => null, 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => null, 'modified' => "2020-11-30 04:08:52", 'next_fetch' => null, 'etag' => null, 'scrape' => 1, 'unread' => 0], + ]; + protected $feedsOut = [ + ['id' => 1, 'user_id' => 42, 'feed_url' => "http://example.com/ook", 'site_url' => "http://example.com/", 'title' => "Ook", 'checked_at' => "2021-01-05T13:51:32.000000Z", 'next_check_at' => "2021-01-20T00:00:00.000000Z", 'etag_header' => "OOKEEK", 'last_modified_header' => "Fri, 01 Jan 2021 00:00:00 GMT", 'parsing_error_message' => "Oopsie", 'parsing_error_count' => 1, 'scraper_rules' => "", 'rewrite_rules' => "", 'crawler' => false, 'blocklist_rules' => "both", 'keeplist_rules' => "this|that", 'user_agent' => "", 'username' => "", 'password' => "", 'disabled' => false, 'ignore_http_cache' => false, 'fetch_via_proxy' => false, 'category' => ['id' => 6, 'title' => "Cat Ook", 'user_id' => 42], 'icon' => ['feed_id' => 1,'icon_id' => 47]], + ['id' => 55, 'user_id' => 42, 'feed_url' => "http://example.com/eek", 'site_url' => "http://example.com/", 'title' => "Eek", 'checked_at' => "2021-01-05T13:51:32.000000Z", 'next_check_at' => "0001-01-01T00:00:00.000000Z", 'etag_header' => "", 'last_modified_header' => "", 'parsing_error_message' => "", 'parsing_error_count' => 0, 'scraper_rules' => "", 'rewrite_rules' => "", 'crawler' => true, 'blocklist_rules' => "", 'keeplist_rules' => "", 'user_agent' => "", 'username' => "j k", 'password' => "super secret", 'disabled' => false, 'ignore_http_cache' => false, 'fetch_via_proxy' => false, 'category' => ['id' => 1,'title' => "All", 'user_id' => 42], 'icon' => null], + ]; protected function req(string $method, string $target, $data = "", array $headers = [], ?string $user = "john.doe@example.com", bool $body = true): ResponseInterface { $prefix = "/v1"; @@ -535,82 +544,42 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ); } - public function testListReeds(): void { - \Phake::when(Arsse::$db)->folderList->thenReturn(new Result([ + public function testListFeeds(): void { + \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ ['id' => 5, 'name' => "Cat Ook"], - ])); - \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result([ - ['id' => 1, 'feed' => 12, 'url' => "http://example.com/ook", 'title' => "Ook", 'source' => "http://example.com/", 'icon_id' => 47, 'icon_url' => "http://example.com/icon", 'folder' => 2112, 'top_folder' => 5, 'pinned' => 0, 'err_count' => 1, 'err_msg' => "Oopsie", 'order_type' => 0, 'keep_rule' => "this|that", 'block_rule' => "both", 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => "2021-01-01 00:00:00", 'modified' => "2020-11-30 04:08:52", 'next_fetch' => "2021-01-20 00:00:00", 'etag' => "OOKEEK", 'scrape' => 0, 'unread' => 42], - ['id' => 55, 'feed' => 12, 'url' => "http://j%20k:super%20secret@example.com/eek", 'title' => "Eek", 'source' => "http://example.com/", 'icon_id' => null, 'icon_url' => null, 'folder' => null, 'top_folder' => null, 'pinned' => 0, 'err_count' => 0, 'err_msg' => null, 'order_type' => 0, 'keep_rule' => null, 'block_rule' => null, 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => null, 'modified' => "2020-11-30 04:08:52", 'next_fetch' => null, 'etag' => null, 'scrape' => 1, 'unread' => 0], - ])); - $exp = new Response([ - [ - 'id' => 1, - 'user_id' => 42, - 'feed_url' => "http://example.com/ook", - 'site_url' => "http://example.com/", - 'title' => "Ook", - 'checked_at' => "2021-01-05T13:51:32.000000Z", - 'next_check_at' => "2021-01-20T00:00:00.000000Z", - 'etag_header' => "OOKEEK", - 'last_modified_header' => "Fri, 01 Jan 2021 00:00:00 GMT", - 'parsing_error_message' => "Oopsie", - 'parsing_error_count' => 1, - 'scraper_rules' => "", - 'rewrite_rules' => "", - 'crawler' => false, - 'blocklist_rules' => "both", - 'keeplist_rules' => "this|that", - 'user_agent' => "", - 'username' => "", - 'password' => "", - 'disabled' => false, - 'ignore_http_cache' => false, - 'fetch_via_proxy' => false, - 'category' => [ - 'id' => 6, - 'title' => "Cat Ook", - 'user_id' => 42 - ], - 'icon' => [ - 'feed_id' => 1, - 'icon_id' => 47 - ], - ], - [ - 'id' => 55, - 'user_id' => 42, - 'feed_url' => "http://example.com/eek", - 'site_url' => "http://example.com/", - 'title' => "Eek", - 'checked_at' => "2021-01-05T13:51:32.000000Z", - 'next_check_at' => "0001-01-01T00:00:00.000000Z", - 'etag_header' => "", - 'last_modified_header' => "", - 'parsing_error_message' => "", - 'parsing_error_count' => 0, - 'scraper_rules' => "", - 'rewrite_rules' => "", - 'crawler' => true, - 'blocklist_rules' => "", - 'keeplist_rules' => "", - 'user_agent' => "", - 'username' => "j k", - 'password' => "super secret", - 'disabled' => false, - 'ignore_http_cache' => false, - 'fetch_via_proxy' => false, - 'category' => [ - 'id' => 1, - 'title' => "All", - 'user_id' => 42 - ], - 'icon' => null, - ], - ]); + ]))); + \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v($this->feeds))); + $exp = new Response($this->feedsOut); $this->assertMessage($exp, $this->req("GET", "/feeds")); } + public function testListFeedsOfACategory(): void { + \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ + ['id' => 5, 'name' => "Cat Ook"], + ]))); + \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v($this->feeds))); + $exp = new Response($this->feedsOut); + $this->assertMessage($exp, $this->req("GET", "/categories/2112/feeds")); + \Phake::verify(Arsse::$db)->subscriptionList(Arsse::$user->id, 2111, true); + } + + public function testListFeedsOfTheRootCategory(): void { + \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ + ['id' => 5, 'name' => "Cat Ook"], + ]))); + \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v($this->feeds))); + $exp = new Response($this->feedsOut); + $this->assertMessage($exp, $this->req("GET", "/categories/1/feeds")); + \Phake::verify(Arsse::$db)->subscriptionList(Arsse::$user->id, 0, false); + } + + public function testListFeedsOfAMissingCategory(): void { + \Phake::when(Arsse::$db)->subscriptionList->thenThrow(new ExceptionInput("idMissing")); + $exp = new EmptyResponse(404); + $this->assertMessage($exp, $this->req("GET", "/categories/2112/feeds")); + \Phake::verify(Arsse::$db)->subscriptionList(Arsse::$user->id, 2111, true); + } + /** @dataProvider provideFeedCreations */ public function testCreateAFeed(array $in, $out1, $out2, $out3, ResponseInterface $exp): void { if ($out1 instanceof \Exception) { diff --git a/tests/lib/FeedException.php b/tests/lib/FeedException.php deleted file mode 100644 index 414dbe43..00000000 --- a/tests/lib/FeedException.php +++ /dev/null @@ -1,15 +0,0 @@ - Date: Sat, 23 Jan 2021 12:00:11 -0500 Subject: [PATCH 125/366] Test feed fetching errors for Miniflux --- lib/Feed/Exception.php | 2 +- lib/REST/Miniflux/V1.php | 3 +++ locale/en.php | 1 + tests/cases/REST/Miniflux/TestV1.php | 34 ++++++++++++++++++++-------- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/Feed/Exception.php b/lib/Feed/Exception.php index 1a8e68fc..113d405e 100644 --- a/lib/Feed/Exception.php +++ b/lib/Feed/Exception.php @@ -15,7 +15,7 @@ class Exception extends \JKingWeb\Arsse\AbstractException { protected const CURL_ERROR_MAP = [1 => "invalidUrl",3 => "invalidUrl",5 => "transmissionError","connectionFailed","connectionFailed","transmissionError","forbidden","unauthorized","transmissionError","transmissionError","transmissionError","transmissionError","connectionFailed","connectionFailed","transmissionError","transmissionError","transmissionError","transmissionError","transmissionError","invalidUrl","transmissionError","transmissionError","transmissionError","transmissionError",28 => "timeout","transmissionError","transmissionError","transmissionError","transmissionError","transmissionError",35 => "invalidCertificate","transmissionError","transmissionError","transmissionError","transmissionError",45 => "transmissionError","unauthorized","maxRedirect",52 => "transmissionError","invalidCertificate","invalidCertificate","transmissionError","transmissionError",58 => "invalidCertificate","invalidCertificate","invalidCertificate","transmissionError","invalidUrl","transmissionError","invalidCertificate","transmissionError","invalidCertificate","forbidden","invalidUrl","forbidden","transmissionError",73 => "transmissionError","transmissionError",77 => "invalidCertificate","invalidUrl",90 => "invalidCertificate","invalidCertificate","transmissionError",94 => "unauthorized","transmissionError","connectionFailed"]; protected const HTTP_ERROR_MAP = [401 => "unauthorized",403 => "forbidden",404 => "invalidUrl",408 => "timeout",410 => "invalidUrl",414 => "invalidUrl",451 => "invalidUrl"]; - public function __construct(string $msgID = "", $vars, \Throwable $e) { + public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) { if ($msgID === "") { assert($e !== null, new \Exception("Expecting Picofeed or Guzzle exception when no message specified.")); if ($e instanceof BadResponseException) { diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index c9a4fdde..303dac16 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -401,6 +401,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 10502 => "Fetch404", 10506 => "Fetch403", 10507 => "Fetch401", + 10521 => "Fetch404", ][$e->getCode()] ?? "FetchOther"; return new ErrorResponse($msg, 502); } @@ -719,6 +720,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 10502 => "Fetch404", 10506 => "Fetch403", 10507 => "Fetch401", + 10521 => "Fetch404", + 10522 => "FetchFormat", ][$e->getCode()] ?? "FetchOther"; return new ErrorResponse($msg, 502); } catch (ExceptionInput $e) { diff --git a/locale/en.php b/locale/en.php index 1f917c4c..812a50c9 100644 --- a/locale/en.php +++ b/locale/en.php @@ -19,6 +19,7 @@ return [ 'API.Miniflux.Error.Fetch401' => 'You are not authorized to access this resource (invalid username/password)', 'API.Miniflux.Error.Fetch403' => 'Unable to fetch this resource (Status Code = 403)', 'API.Miniflux.Error.FetchOther' => 'Unable to fetch this resource', + 'API.Miniflux.Error.FetchFormat' => 'Unsupported feed format', 'API.Miniflux.Error.DuplicateCategory' => 'This category already exists.', 'API.Miniflux.Error.InvalidCategory' => 'Invalid category title "{title}"', 'API.Miniflux.Error.MissingCategory' => 'This category does not exist or does not belong to this user.', diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 0bc1d50b..a18a3025 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -15,7 +15,7 @@ use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\REST\Miniflux\V1; use JKingWeb\Arsse\REST\Miniflux\ErrorResponse; -use JKingWeb\Arsse\Test\FeedException; +use JKingWeb\Arsse\Feed\Exception as FeedException; use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\ExceptionInput as UserExceptionInput; use Psr\Http\Message\ResponseInterface; @@ -602,12 +602,12 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { $in2 = $out2 !== null; $in3 = $out3 !== null; if ($in1) { - \Phake::verify(Arsse::$db)->feedAdd($in['feed_url'], $in['username'] ?? "", $in['password'] ?? "", true, $in['crawler'] ?? false); + \Phake::verify(Arsse::$db)->feedAdd($in['feed_url'], $in['username'] ?? "", $in['password'] ?? "", false, $in['crawler'] ?? false); } else { \Phake::verify(Arsse::$db, \Phake::times(0))->feedAdd; } if ($in2) { - \Phake::verify(Arsse::$db)->subscriptionAdd("john.doe@example.com", $in['feed_url'], $in['username'] ?? "", $in['password'] ?? "", true, $in['crawler'] ?? false); + \Phake::verify(Arsse::$db)->subscriptionAdd("john.doe@example.com", $in['feed_url'], $in['username'] ?? "", $in['password'] ?? "", false, $in['crawler'] ?? false); } else { \Phake::verify(Arsse::$db, \Phake::times(0))->subscriptionAdd; } @@ -627,13 +627,27 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function provideFeedCreations(): iterable { self::clearData(); return [ - [['category_id' => 1], null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "feed_url"], 422)], - [['feed_url' => "http://example.com/"], null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "category_id"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => "1"], null, null, null, new ErrorResponse(["InvalidInputType", 'field' => "category_id", 'expected' => "integer", 'actual' => "string"], 422)], - [['feed_url' => "Not a URL", 'category_id' => 1], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "feed_url"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => 0], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "["], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "keeplist_rules"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "["], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "blocklist_rules"], 422)], + [['category_id' => 1], null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "feed_url"], 422)], + [['feed_url' => "http://example.com/"], null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "category_id"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => "1"], null, null, null, new ErrorResponse(["InvalidInputType", 'field' => "category_id", 'expected' => "integer", 'actual' => "string"], 422)], + [['feed_url' => "Not a URL", 'category_id' => 1], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "feed_url"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 0], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "["], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "keeplist_rules"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "["], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "blocklist_rules"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("internalError"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidCertificate"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidUrl"), null, null, new ErrorResponse("Fetch404", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxRedirect"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxSize"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("timeout"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("forbidden"), null, null, new ErrorResponse("Fetch403", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unauthorized"), null, null, new ErrorResponse("Fetch401", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("transmissionError"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("connectionFailed"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("malformedXml"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("xmlEntity"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("subscriptionNotFound"), null, null, new ErrorResponse("Fetch404", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unsupportedFeedFormat"), null, null, new ErrorResponse("FetchFormat", 502)], ]; } } From 7893b5f59d6dedf23df931d38a6f6212c6841e3a Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 23 Jan 2021 18:01:23 -0500 Subject: [PATCH 126/366] More feed adding tests --- lib/REST/Miniflux/V1.php | 14 ++++---- tests/cases/REST/Miniflux/TestV1.php | 49 +++++++++++++++------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 303dac16..6bbbeea8 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -232,7 +232,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } try { return $this->$func(...$args); - // @codeCoverageIgnoreStart + // @codeCoverageIgnoreStart } catch (Exception $e) { // if there was a REST exception return 400 return new EmptyResponse(400); @@ -703,18 +703,16 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function createFeed(array $data): ResponseInterface { - $props = [ - 'keep_rule' => $data['keeplist_rules'], - 'block_rule' => $data['blocklist_rules'], - 'folder' => $data['category_id'] - 1, - 'scrape' => (bool) $data['crawler'], - ]; try { Arsse::$db->feedAdd($data['feed_url'], (string) $data['username'], (string) $data['password'], false, (bool) $data['crawler']); $tr = Arsse::$db->begin(); $id = Arsse::$db->subscriptionAdd(Arsse::$user->id, $data['feed_url'], (string) $data['username'], (string) $data['password'], false, (bool) $data['crawler']); - Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, $props); + Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, ['folder' => $data['category_id'] - 1, 'scrape' => (bool) $data['crawler']]); $tr->commit(); + if (strlen($data['keeplist_rules'] ?? "") || strlen($data['blocklist_rules'] ?? "")) { + // we do rules separately so as not to tie up the database + Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, ['keep_rule' => $data['keeplist_rules'], 'block_rule' => $data['blocklist_rules']]); + } } catch (FeedException $e) { $msg = [ 10502 => "Fetch404", diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index a18a3025..3bcfbfc9 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -613,11 +613,13 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } if ($in3) { $props = [ - 'keep_rule' => $in['keeplist_rules'], - 'block_rule' => $in['blocklist_rules'], 'folder' => $in['category_id'] - 1, 'scrape' => $in['crawler'] ?? false, ]; + $rules = (strlen($in['keeplist_rules'] ?? "") || strlen($in['blocklist_rules'] ?? "")) ? [ + 'keep_rule' => $in['keeplist_rules'], + 'block_rule' => $in['blocklist_rules'], + ] : []; \Phake::verify(Arsse::$db)->subscriptionPropertiesSet("john.doe@example.com", $out2, $props); } else { \Phake::verify(Arsse::$db, \Phake::times(0))->subscriptionPropertiesSet; @@ -627,27 +629,28 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function provideFeedCreations(): iterable { self::clearData(); return [ - [['category_id' => 1], null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "feed_url"], 422)], - [['feed_url' => "http://example.com/"], null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "category_id"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => "1"], null, null, null, new ErrorResponse(["InvalidInputType", 'field' => "category_id", 'expected' => "integer", 'actual' => "string"], 422)], - [['feed_url' => "Not a URL", 'category_id' => 1], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "feed_url"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => 0], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "["], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "keeplist_rules"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "["], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "blocklist_rules"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("internalError"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidCertificate"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidUrl"), null, null, new ErrorResponse("Fetch404", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxRedirect"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxSize"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("timeout"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("forbidden"), null, null, new ErrorResponse("Fetch403", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unauthorized"), null, null, new ErrorResponse("Fetch401", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("transmissionError"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("connectionFailed"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("malformedXml"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("xmlEntity"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("subscriptionNotFound"), null, null, new ErrorResponse("Fetch404", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unsupportedFeedFormat"), null, null, new ErrorResponse("FetchFormat", 502)], + [['category_id' => 1], null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "feed_url"], 422)], + [['feed_url' => "http://example.com/"], null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "category_id"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => "1"], null, null, null, new ErrorResponse(["InvalidInputType", 'field' => "category_id", 'expected' => "integer", 'actual' => "string"], 422)], + [['feed_url' => "Not a URL", 'category_id' => 1], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "feed_url"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 0], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "["], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "keeplist_rules"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "["], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "blocklist_rules"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("internalError"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidCertificate"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidUrl"), null, null, new ErrorResponse("Fetch404", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxRedirect"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxSize"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("timeout"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("forbidden"), null, null, new ErrorResponse("Fetch403", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unauthorized"), null, null, new ErrorResponse("Fetch401", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("transmissionError"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("connectionFailed"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("malformedXml"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("xmlEntity"), null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("subscriptionNotFound"), null, null, new ErrorResponse("Fetch404", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unsupportedFeedFormat"), null, null, new ErrorResponse("FetchFormat", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], 2112, new ExceptionInput("constraintViolation"), null, new ErrorResponse("DuplicateFeed", 409)], ]; } } From a34edcb0d1d4d3524c20b43d1a7e9c8d1deb0430 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 24 Jan 2021 11:25:38 -0500 Subject: [PATCH 127/366] Last tests for feed creation --- tests/cases/REST/Miniflux/TestV1.php | 73 +++++++++++++++++----------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 3bcfbfc9..98184c20 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -581,7 +581,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideFeedCreations */ - public function testCreateAFeed(array $in, $out1, $out2, $out3, ResponseInterface $exp): void { + public function testCreateAFeed(array $in, $out1, $out2, $out3, $out4, ResponseInterface $exp): void { if ($out1 instanceof \Exception) { \Phake::when(Arsse::$db)->feedAdd->thenThrow($out1); } else { @@ -594,21 +594,26 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } if ($out3 instanceof \Exception) { \Phake::when(Arsse::$db)->subscriptionPropertiesSet->thenThrow($out3); + } elseif ($out4 instanceof \Exception) { + \Phake::when(Arsse::$db)->subscriptionPropertiesSet->thenReturn($out3)->thenThrow($out4); } else { - \Phake::when(Arsse::$db)->subscriptionPropertiesSet->thenReturn($out3); + \Phake::when(Arsse::$db)->subscriptionPropertiesSet->thenReturn($out3)->thenReturn($out4); } $this->assertMessage($exp, $this->req("POST", "/feeds", $in)); $in1 = $out1 !== null; $in2 = $out2 !== null; $in3 = $out3 !== null; + $in4 = $out4 !== null; if ($in1) { \Phake::verify(Arsse::$db)->feedAdd($in['feed_url'], $in['username'] ?? "", $in['password'] ?? "", false, $in['crawler'] ?? false); } else { \Phake::verify(Arsse::$db, \Phake::times(0))->feedAdd; } if ($in2) { + \Phake::verify(Arsse::$db)->begin(); \Phake::verify(Arsse::$db)->subscriptionAdd("john.doe@example.com", $in['feed_url'], $in['username'] ?? "", $in['password'] ?? "", false, $in['crawler'] ?? false); } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->begin; \Phake::verify(Arsse::$db, \Phake::times(0))->subscriptionAdd; } if ($in3) { @@ -616,41 +621,53 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { 'folder' => $in['category_id'] - 1, 'scrape' => $in['crawler'] ?? false, ]; - $rules = (strlen($in['keeplist_rules'] ?? "") || strlen($in['blocklist_rules'] ?? "")) ? [ - 'keep_rule' => $in['keeplist_rules'], - 'block_rule' => $in['blocklist_rules'], - ] : []; \Phake::verify(Arsse::$db)->subscriptionPropertiesSet("john.doe@example.com", $out2, $props); + if (!$out3 instanceof \Exception) { + \Phake::verify($this->transaction)->commit(); + } } else { \Phake::verify(Arsse::$db, \Phake::times(0))->subscriptionPropertiesSet; } + if ($in4) { + $rules = [ + 'keep_rule' => $in['keeplist_rules'] ?? null, + 'block_rule' => $in['blocklist_rules'] ?? null, + ]; + \Phake::verify(Arsse::$db)->subscriptionPropertiesSet("john.doe@example.com", $out2, $rules); + } else { + \Phake::verify(Arsse::$db, \Phake::atMost(1))->subscriptionPropertiesSet; + } } public function provideFeedCreations(): iterable { self::clearData(); return [ - [['category_id' => 1], null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "feed_url"], 422)], - [['feed_url' => "http://example.com/"], null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "category_id"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => "1"], null, null, null, new ErrorResponse(["InvalidInputType", 'field' => "category_id", 'expected' => "integer", 'actual' => "string"], 422)], - [['feed_url' => "Not a URL", 'category_id' => 1], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "feed_url"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => 0], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "["], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "keeplist_rules"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "["], null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "blocklist_rules"], 422)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("internalError"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidCertificate"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidUrl"), null, null, new ErrorResponse("Fetch404", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxRedirect"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxSize"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("timeout"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("forbidden"), null, null, new ErrorResponse("Fetch403", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unauthorized"), null, null, new ErrorResponse("Fetch401", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("transmissionError"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("connectionFailed"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("malformedXml"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("xmlEntity"), null, null, new ErrorResponse("FetchOther", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("subscriptionNotFound"), null, null, new ErrorResponse("Fetch404", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unsupportedFeedFormat"), null, null, new ErrorResponse("FetchFormat", 502)], - [['feed_url' => "http://example.com/", 'category_id' => 1], 2112, new ExceptionInput("constraintViolation"), null, new ErrorResponse("DuplicateFeed", 409)], + [['category_id' => 1], null, null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "feed_url"], 422)], + [['feed_url' => "http://example.com/"], null, null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "category_id"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => "1"], null, null, null, null, new ErrorResponse(["InvalidInputType", 'field' => "category_id", 'expected' => "integer", 'actual' => "string"], 422)], + [['feed_url' => "Not a URL", 'category_id' => 1], null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "feed_url"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 0], null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "["], null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "keeplist_rules"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "["], null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "blocklist_rules"], 422)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("internalError"), null, null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidCertificate"), null, null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("invalidUrl"), null, null, null, new ErrorResponse("Fetch404", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxRedirect"), null, null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("maxSize"), null, null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("timeout"), null, null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("forbidden"), null, null, null, new ErrorResponse("Fetch403", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unauthorized"), null, null, null, new ErrorResponse("Fetch401", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("transmissionError"), null, null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("connectionFailed"), null, null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("malformedXml"), null, null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("xmlEntity"), null, null, null, new ErrorResponse("FetchOther", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("subscriptionNotFound"), null, null, null, new ErrorResponse("Fetch404", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], new FeedException("unsupportedFeedFormat"), null, null, null, new ErrorResponse("FetchFormat", 502)], + [['feed_url' => "http://example.com/", 'category_id' => 1], 2112, new ExceptionInput("constraintViolation"), null, null, new ErrorResponse("DuplicateFeed", 409)], + [['feed_url' => "http://example.com/", 'category_id' => 1], 2112, 44, new ExceptionInput("idMissing"), null, new ErrorResponse("MissingCategory", 422)], + [['feed_url' => "http://example.com/", 'category_id' => 1], 2112, 44, true, null, new Response(['feed_id' => 44], 201)], + [['feed_url' => "http://example.com/", 'category_id' => 1, 'keeplist_rules' => "^A"], 2112, 44, true, true, new Response(['feed_id' => 44], 201)], + [['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "A"], 2112, 44, true, true, new Response(['feed_id' => 44], 201)], ]; } } From cca4b205e4347678ee9af6621a00f9ccd667cca6 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 24 Jan 2021 11:33:00 -0500 Subject: [PATCH 128/366] Correct error output of getCategoryFeeds --- lib/REST/Miniflux/V1.php | 2 +- tests/cases/REST/Miniflux/TestV1.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 6bbbeea8..2ad0f66f 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -689,7 +689,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $subs = Arsse::$db->subscriptionList(Arsse::$user->id, $folder, $recursive)->getAll(); } catch (ExceptionInput $e) { // the folder does not exist - return new EmptyResponse(404); + return new ErrorResponse("404", 404); } // compile the list of folders; the feed list includes folder names // NOTE: We compile the full list of folders in case someone has manually selected a non-top folder diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 98184c20..dae4e415 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -575,7 +575,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function testListFeedsOfAMissingCategory(): void { \Phake::when(Arsse::$db)->subscriptionList->thenThrow(new ExceptionInput("idMissing")); - $exp = new EmptyResponse(404); + $exp = new ErrorResponse("404", 404); $this->assertMessage($exp, $this->req("GET", "/categories/2112/feeds")); \Phake::verify(Arsse::$db)->subscriptionList(Arsse::$user->id, 2111, true); } From a646ad77b7b51cb0e72cb4f7b47205e49c207842 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 24 Jan 2021 11:45:08 -0500 Subject: [PATCH 129/366] Use a read transaction when computing filter rules --- lib/Database.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/Database.php b/lib/Database.php index de829dba..bdb36b64 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -963,7 +963,7 @@ class Database { } $out = (bool) $this->db->prepare("UPDATE arsse_subscriptions set $setClause, modified = CURRENT_TIMESTAMP where owner = ? and id = ?", $setTypes, "str", "int")->run($setValues, $user, $id)->changes(); $tr->commit(); - // if filter rules were changed, apply them + // if filter rules were changed, apply them; this is done outside the transaction because it may take some time if (array_key_exists("keep_rule", $data) || array_key_exists("block_rule", $data)) { $this->subscriptionRulesApply($user, $id); } @@ -1030,6 +1030,8 @@ class Database { * @param integer $id The identifier of the subscription whose rules are to be evaluated */ protected function subscriptionRulesApply(string $user, int $id): void { + // start a transaction for read isolation + $tr = $this->begin(); $sub = $this->db->prepare("SELECT feed, coalesce(keep_rule, '') as keep, coalesce(block_rule, '') as block from arsse_subscriptions where owner = ? and id = ?", "str", "int")->run($user, $id)->getRow(); try { $keep = Rule::prep($sub['keep']); @@ -1053,6 +1055,8 @@ class Database { $hide[] = $r['id']; } } + // roll back the read transation + $tr->rollback(); // apply any marks if ($hide) { $this->articleMark($user, ['hidden' => true], (new Context)->articles($hide), false); From 5a8a044a92422f61e3c55b1f7c28831e79326bdf Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 24 Jan 2021 13:54:54 -0500 Subject: [PATCH 130/366] Implement single-feed querying --- lib/REST/Miniflux/V1.php | 12 ++++++++++++ tests/cases/REST/Miniflux/TestV1.php | 19 ++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 2ad0f66f..2438494e 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -702,6 +702,18 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response($out); } + protected function getFeed(array $path): ResponseInterface { + $tr = Arsse::$db->begin(); + try { + $sub = Arsse::$db->subscriptionPropertiesGet(Arsse::$user->id, (int) $path[1]); + } catch (ExceptionInput $e) { + return new ErrorResponse("404", 404); + } + // compile the list of folders; the feed list includes folder names + $folders = $this->mapFolders(); + return new Response($this->transformFeed($sub, $folders)); + } + protected function createFeed(array $data): ResponseInterface { try { Arsse::$db->feedAdd($data['feed_url'], (string) $data['username'], (string) $data['password'], false, (bool) $data['crawler']); diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index dae4e415..9b5877d7 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -564,9 +564,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } public function testListFeedsOfTheRootCategory(): void { - \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ - ['id' => 5, 'name' => "Cat Ook"], - ]))); + \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([['id' => 5, 'name' => "Cat Ook"],]))); \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v($this->feeds))); $exp = new Response($this->feedsOut); $this->assertMessage($exp, $this->req("GET", "/categories/1/feeds")); @@ -580,6 +578,21 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->subscriptionList(Arsse::$user->id, 2111, true); } + public function testGetAFeed(): void { + \Phake::when(Arsse::$db)->subscriptionPropertiesGet->thenReturn($this->v($this->feeds[0]))->thenReturn($this->v($this->feeds[1])); + \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([['id' => 5, 'name' => "Cat Ook"],]))); + $this->assertMessage(new Response($this->feedsOut[0]), $this->req("GET", "/feeds/1")); + \Phake::verify(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 1); + $this->assertMessage(new Response($this->feedsOut[1]), $this->req("GET", "/feeds/55")); + \Phake::verify(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 55); + } + + public function testGetAMissingFeed(): void { + \Phake::when(Arsse::$db)->subscriptionPropertiesGet->thenThrow(new ExceptionInput("subjectMissing")); + $this->assertMessage(new ErrorResponse("404", 404), $this->req("GET", "/feeds/1")); + \Phake::verify(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 1); + } + /** @dataProvider provideFeedCreations */ public function testCreateAFeed(array $in, $out1, $out2, $out3, $out4, ResponseInterface $exp): void { if ($out1 instanceof \Exception) { From 8eebb75b1809cc9bc5f444ddda264318a5065523 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 24 Jan 2021 20:28:00 -0500 Subject: [PATCH 131/366] Implement feed editing --- .../030_Supported_Protocols/005_Miniflux.md | 2 +- lib/REST/Miniflux/V1.php | 54 +++++++++++++++++++ locale/en.php | 3 +- tests/cases/REST/Miniflux/TestV1.php | 29 ++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index c3a19211..c098aa1e 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -32,7 +32,7 @@ Miniflux version 2.0.27 is emulated, though not all features are implemented - Various error codes and messages differ due to significant implementation differences - `PUT` requests which return a body respond with `200 OK` rather than `201 Created` - The "All" category is treated specially (see below for details) -- Category names consisting only of whitespace are rejected along with the empty string +- Feed and category titles consisting only of whitespace are rejected along with the empty string - Filtering rules may not function identically (see below for details) - The `checked_at` field of feeds indicates when the feed was last updated rather than when it was last checked - Creating a feed with the `scrape` property set to `true` might not return scraped content for the initial synchronization diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 2438494e..e987a25e 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -66,6 +66,34 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'entry_swipe' => ["swipe", true], 'stylesheet' => ["stylesheet", ""], ]; + /** A map between Miniflux's input properties and our input properties when modifiying feeds + * + * Miniflux also allows changing the following properties: + * + * - feed_url + * - username + * - password + * - user_agent + * - scraper_rules + * - rewrite_rules + * - disabled + * - ignore_http_cache + * - fetch_via_proxy + * + * These either do not apply because we have no cache or proxy, + * or cannot be changed because feeds are deduplicated and changing + * how they are fetched is not practical with our implementation. + * The properties are still checked for type and syntactic validity + * where practical, on the assumption Miniflux would also reject + * invalid values. + */ + protected const FEED_META_MAP = [ + 'title' => "title", + 'category_id' => "folder", + 'crawler' => "scrape", + 'keeplist_rules' => "keep_rule", + 'blocklist_rules' => "block_rule", + ]; protected const CALLS = [ // handler method Admin Path Body Query Required fields '/categories' => [ 'GET' => ["getCategories", false, false, false, false, []], @@ -745,6 +773,32 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(['feed_id' => $id], 201); } + protected function updateFeed(array $path, array $data): ResponseInterface { + $in = []; + foreach (self::FEED_META_MAP as $from => $to) { + if (isset($data[$from])) { + $in[$to] = $data[$from]; + } + } + if (isset($in['folder'])) { + $in['folder'] -= 1; + } + try { + Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $path[1], $in); + return $this->getFeed($path); + } catch (ExceptionInput $e) { + switch ($e->getCode()) { + case 10231: + case 10232: + return new ErrorResponse("InvalidTitle", 422); + case 10235: + return new ErrorResponse("MissingCategory", 422); + case 10239: + return new ErrorResponse("404", 404); + } + } + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokens in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); diff --git a/locale/en.php b/locale/en.php index 812a50c9..b44d7490 100644 --- a/locale/en.php +++ b/locale/en.php @@ -21,11 +21,12 @@ return [ 'API.Miniflux.Error.FetchOther' => 'Unable to fetch this resource', 'API.Miniflux.Error.FetchFormat' => 'Unsupported feed format', 'API.Miniflux.Error.DuplicateCategory' => 'This category already exists.', - 'API.Miniflux.Error.InvalidCategory' => 'Invalid category title "{title}"', + 'API.Miniflux.Error.InvalidCategory' => 'Invalid category title', 'API.Miniflux.Error.MissingCategory' => 'This category does not exist or does not belong to this user.', 'API.Miniflux.Error.InvalidElevation' => 'Only administrators can change permissions of standard users', 'API.Miniflux.Error.DuplicateUser' => 'The user name "{user}" already exists', 'API.Miniflux.Error.DuplicateFeed' => 'This feed already exists.', + 'API.Miniflux.Error.InvalidTitle' => 'Invalid feed title', 'API.TTRSS.Category.Uncategorized' => 'Uncategorized', 'API.TTRSS.Category.Special' => 'Special', diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 9b5877d7..dbb0eff7 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -683,4 +683,33 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { [['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "A"], 2112, 44, true, true, new Response(['feed_id' => 44], 201)], ]; } + + /** @dataProvider provideFeedModifications */ + public function testModifyAFeed(array $in, array $data, $out, ResponseInterface $exp): void { + $this->h = \Phake::partialMock(V1::class); + \Phake::when($this->h)->getFeed->thenReturn(new Response($this->feedsOut[0])); + if ($out instanceof \Exception) { + \Phake::when(Arsse::$db)->subscriptionPropertiesSet->thenThrow($out); + } else { + \Phake::when(Arsse::$db)->subscriptionPropertiesSet->thenReturn($out); + } + $this->assertMessage($exp, $this->req("PUT", "/feeds/2112")); + } + + public function provideFeedModifications(): iterable { + self::clearData(); + $success = new Response($this->feedsOut[0]); + return [ + [[], [], true, $success], + [[], [], new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)], + [['title' => ""], ['title' => ""], new ExceptionInput("missing"), new ErrorResponse("InvalidTitle", 422)], + [['title' => " "], ['title' => " "], new ExceptionInput("whitespace"), new ErrorResponse("InvalidTitle", 422)], + [['title' => " "], ['title' => " "], new ExceptionInput("whitespace"), new ErrorResponse("InvalidTitle", 422)], + [['category_id' => 47], ['folder' => 46], new ExceptionInput("idMissing"), new ErrorResponse("MissingCategory", 422)], + [['crawler' => false], ['scrape' => false], true, $success], + [['keeplist_rules' => ""], ['keep_rule' => ""], true, $success], + [['blocklist_rules' => "ook"], ['block_rule' => "ook"], true, $success], + [['title' => "Ook!", 'crawler' => true], ['title' => "Ook!", 'scrape' => true], true, $success] + ]; + } } From 9197a8d08b74c57766916cf880d71f89818c64c5 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 24 Jan 2021 21:12:32 -0500 Subject: [PATCH 132/366] Implement feed deletion --- lib/REST/Miniflux/V1.php | 11 ++++++++++- tests/cases/REST/Miniflux/TestV1.php | 12 ++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index e987a25e..705a77fa 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -785,7 +785,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } try { Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $path[1], $in); - return $this->getFeed($path); } catch (ExceptionInput $e) { switch ($e->getCode()) { case 10231: @@ -797,6 +796,16 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("404", 404); } } + return $this->getFeed($path); + } + + protected function deleteFeed(array $path): ResponseInterface { + try { + Arsse::$db->subscriptionRemove(Arsse::$user->id, (int) $path[1]); + return new EmptyResponse(204); + } catch (ExceptionInput $e) { + return new ErrorResponse("404", 404); + } } public static function tokenGenerate(string $user, string $label): string { diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index dbb0eff7..c866aa1e 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -712,4 +712,16 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { [['title' => "Ook!", 'crawler' => true], ['title' => "Ook!", 'scrape' => true], true, $success] ]; } + + public function testDeleteAFeed(): void { + \Phake::when(Arsse::$db)->subscriptionRemove->thenReturn(true); + $this->assertMessage(new EmptyResponse(204), $this->req("DELETE", "/feeds/2112")); + \Phake::verify(Arsse::$db)->subscriptionRemove(Arsse::$user->id, 2112); + } + + public function testDeleteAMissingFeed(): void { + \Phake::when(Arsse::$db)->subscriptionRemove->thenThrow(new ExceptionInput("subjectMissing")); + $this->assertMessage(new ErrorResponse("404", 404), $this->req("DELETE", "/feeds/2112")); + \Phake::verify(Arsse::$db)->subscriptionRemove(Arsse::$user->id, 2112); + } } From bdf9c0e9d2e0f58df52d6fb084e4dee2bbcb3b8e Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 24 Jan 2021 21:53:45 -0500 Subject: [PATCH 133/366] Prototype feed icon querying --- lib/REST/Miniflux/V1.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 705a77fa..c577ce0c 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -808,6 +808,22 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } + protected function getFeedIcon(array $path): ResponseInterface { + try { + $icon = Arsse::$db->subscriptionIcon(Arsse::$user->id, (int) $path[1]); + } catch (ExceptionInput $e) { + return new ErrorResponse("404", 404); + } + if (!$icon['id']) { + return new ErrorResponse("404", 404); + } + return new Response([ + 'id' => $icon['id'], + 'data' => ($icon['type'] ?? "application/octet-stream").";base64,".base64_encode($icon['data']), + 'mime_type' => $icon['type'], + ]); + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokens in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); From 8e749bb73c3fe0665dfb41f5b85a425d611a61eb Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 25 Jan 2021 09:02:52 -0500 Subject: [PATCH 134/366] Report 404 on icons for absence of data This is significant as upgraded databases have icon IDs, but no data --- lib/REST/Miniflux/V1.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index c577ce0c..6832b902 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -814,7 +814,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } catch (ExceptionInput $e) { return new ErrorResponse("404", 404); } - if (!$icon['id']) { + if (!$icon['data']) { return new ErrorResponse("404", 404); } return new Response([ From 1eea3b3a4c6cb9af8b6bda0c1fced62ccfd28452 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 26 Jan 2021 10:32:27 -0500 Subject: [PATCH 135/366] Fix feed update test --- tests/cases/REST/Miniflux/TestV1.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index c866aa1e..7154dd98 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -693,7 +693,8 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } else { \Phake::when(Arsse::$db)->subscriptionPropertiesSet->thenReturn($out); } - $this->assertMessage($exp, $this->req("PUT", "/feeds/2112")); + $this->assertMessage($exp, $this->req("PUT", "/feeds/2112", $in)); + \Phake::verify(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 2112, $data); } public function provideFeedModifications(): iterable { From cc2672fb0ab8d4a545388de59c12919adeb10a24 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 26 Jan 2021 12:03:26 -0500 Subject: [PATCH 136/366] Improve icon fetching interface --- lib/Database.php | 6 +++++- tests/cases/Database/SeriesSubscription.php | 7 +++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index bdb36b64..f8053ae9 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -991,12 +991,14 @@ class Database { * - "url": The URL of the icon * - "type": The Content-Type of the icon e.g. "image/png" * - "data": The icon itself, as a binary sring; if $withData is false this will be null + * + * If the subscription has no icon null is returned instead of an array * * @param string|null $user The user who owns the subscription being queried; using null here is supported for TT-RSS and SHOULD NOT be used elsewhere as it leaks information * @param int $subscription The numeric identifier of the subscription * @param bool $includeData Whether to include the binary data of the icon itself in the result */ - public function subscriptionIcon(?string $user, int $id, bool $includeData = true): array { + public function subscriptionIcon(?string $user, int $id, bool $includeData = true): ?array { $data = $includeData ? "i.data" : "null as data"; $q = new Query("SELECT i.id, i.url, i.type, $data from arsse_subscriptions as s join arsse_feeds as f on s.feed = f.id left join arsse_icons as i on f.icon = i.id"); $q->setWhere("s.id = ?", "int", $id); @@ -1006,6 +1008,8 @@ class Database { $out = $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getRow(); if (!$out) { throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "subscription", 'id' => $id]); + } elseif (!$out['id']) { + return null; } return $out; } diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index c009b600..e4a2b099 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -478,7 +478,7 @@ trait SeriesSubscription { $exp = "http://example.com/favicon.ico"; $this->assertSame($exp, Arsse::$db->subscriptionIcon(null, 1)['url']); $this->assertSame($exp, Arsse::$db->subscriptionIcon(null, 2)['url']); - $this->assertSame(null, Arsse::$db->subscriptionIcon(null, 3)['url']); + $this->assertSame(null, Arsse::$db->subscriptionIcon(null, 3)); } public function testRetrieveTheFaviconOfAMissingSubscription(): void { @@ -490,16 +490,15 @@ trait SeriesSubscription { $exp = "http://example.com/favicon.ico"; $user = "john.doe@example.com"; $this->assertSame($exp, Arsse::$db->subscriptionIcon($user, 1)['url']); - $this->assertSame(null, Arsse::$db->subscriptionIcon($user, 3)['url']); + $this->assertSame(null, Arsse::$db->subscriptionIcon($user, 3)); $user = "jane.doe@example.com"; $this->assertSame($exp, Arsse::$db->subscriptionIcon($user, 2)['url']); } public function testRetrieveTheFaviconOfASubscriptionOfTheWrongUser(): void { - $exp = "http://example.com/favicon.ico"; $user = "john.doe@example.com"; $this->assertException("subjectMissing", "Db", "ExceptionInput"); - $this->assertSame(null, Arsse::$db->subscriptionIcon($user, 2)['url']); + Arsse::$db->subscriptionIcon($user, 2); } public function testListTheTagsOfASubscription(): void { From 76f1cc8e9156ff0f5325a2aba019e81a16d623f9 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 26 Jan 2021 13:44:44 -0500 Subject: [PATCH 137/366] Adjust users of subscriptionIcon --- lib/REST/Miniflux/V1.php | 4 ++-- lib/REST/TinyTinyRSS/Icon.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 6832b902..1ca0e79c 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -814,12 +814,12 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } catch (ExceptionInput $e) { return new ErrorResponse("404", 404); } - if (!$icon['data']) { + if (!$icon || !$icon['data']) { return new ErrorResponse("404", 404); } return new Response([ 'id' => $icon['id'], - 'data' => ($icon['type'] ?? "application/octet-stream").";base64,".base64_encode($icon['data']), + 'data' => ($icon['type'] ?: "application/octet-stream").";base64,".base64_encode($icon['data']), 'mime_type' => $icon['type'], ]); } diff --git a/lib/REST/TinyTinyRSS/Icon.php b/lib/REST/TinyTinyRSS/Icon.php index b49ae4e4..9e7c7ec0 100644 --- a/lib/REST/TinyTinyRSS/Icon.php +++ b/lib/REST/TinyTinyRSS/Icon.php @@ -31,7 +31,7 @@ class Icon extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(404); } try { - $url = Arsse::$db->subscriptionIcon(Arsse::$user->id ?? null, (int) $match[1], false)['url']; + $url = Arsse::$db->subscriptionIcon(Arsse::$user->id ?? null, (int) $match[1], false)['url'] ?? null; if (!$url) { return new Response(404); } From cd5f13f4b9fcda677472b7c606b92ed584cca071 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 27 Jan 2021 11:53:07 -0500 Subject: [PATCH 138/366] Tests for icon querying --- lib/REST/Miniflux/V1.php | 2 +- tests/cases/REST/Miniflux/TestV1.php | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 1ca0e79c..95198650 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -820,7 +820,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response([ 'id' => $icon['id'], 'data' => ($icon['type'] ?: "application/octet-stream").";base64,".base64_encode($icon['data']), - 'mime_type' => $icon['type'], + 'mime_type' => ($icon['type'] ?: "application/octet-stream"), ]); } diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 7154dd98..8e6d13d4 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -725,4 +725,27 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertMessage(new ErrorResponse("404", 404), $this->req("DELETE", "/feeds/2112")); \Phake::verify(Arsse::$db)->subscriptionRemove(Arsse::$user->id, 2112); } + + /** @dataProvider provideIcons */ + public function testGetTheIconOfASubscription($out, ResponseInterface $exp): void { + if ($out instanceof \Exception) { + \Phake::when(Arsse::$db)->subscriptionIcon->thenThrow($out); + } else { + \Phake::when(Arsse::$db)->subscriptionIcon->thenReturn($this->v($out)); + } + $this->assertMessage($exp, $this->req("GET", "/feeds/2112/icon")); + \Phake::verify(Arsse::$db)->subscriptionIcon(Arsse::$user->id, 2112); + } + + public function provideIcons(): iterable { + self::clearData(); + return [ + [['id' => 44, 'type' => "image/svg+xml", 'data' => ""], new Response(['id' => 44, 'data' => "image/svg+xml;base64,PHN2Zy8+", 'mime_type' => "image/svg+xml"])], + [['id' => 47, 'type' => "", 'data' => ""], new Response(['id' => 47, 'data' => "application/octet-stream;base64,PHN2Zy8+", 'mime_type' => "application/octet-stream"])], + [['id' => 47, 'type' => null, 'data' => ""], new Response(['id' => 47, 'data' => "application/octet-stream;base64,PHN2Zy8+", 'mime_type' => "application/octet-stream"])], + [['id' => 47, 'type' => null, 'data' => null], new ErrorResponse("404", 404)], + [null, new ErrorResponse("404", 404)], + [new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)], + ]; + } } From ad094f5217d9f346ade0f1ece2871dfcb8c69a1a Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 27 Jan 2021 13:41:10 -0500 Subject: [PATCH 139/366] Don't return icons without types at all --- lib/REST/Miniflux/V1.php | 6 +++--- tests/cases/REST/Miniflux/TestV1.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 95198650..efd04695 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -814,13 +814,13 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } catch (ExceptionInput $e) { return new ErrorResponse("404", 404); } - if (!$icon || !$icon['data']) { + if (!$icon || !$icon['type'] || !$icon['data']) { return new ErrorResponse("404", 404); } return new Response([ 'id' => $icon['id'], - 'data' => ($icon['type'] ?: "application/octet-stream").";base64,".base64_encode($icon['data']), - 'mime_type' => ($icon['type'] ?: "application/octet-stream"), + 'data' => $icon['type'].";base64,".base64_encode($icon['data']), + 'mime_type' => $icon['type'], ]); } diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 8e6d13d4..d04f45a3 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -741,8 +741,8 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { self::clearData(); return [ [['id' => 44, 'type' => "image/svg+xml", 'data' => ""], new Response(['id' => 44, 'data' => "image/svg+xml;base64,PHN2Zy8+", 'mime_type' => "image/svg+xml"])], - [['id' => 47, 'type' => "", 'data' => ""], new Response(['id' => 47, 'data' => "application/octet-stream;base64,PHN2Zy8+", 'mime_type' => "application/octet-stream"])], - [['id' => 47, 'type' => null, 'data' => ""], new Response(['id' => 47, 'data' => "application/octet-stream;base64,PHN2Zy8+", 'mime_type' => "application/octet-stream"])], + [['id' => 47, 'type' => "", 'data' => ""], new ErrorResponse("404", 404)], + [['id' => 47, 'type' => null, 'data' => ""], new ErrorResponse("404", 404)], [['id' => 47, 'type' => null, 'data' => null], new ErrorResponse("404", 404)], [null, new ErrorResponse("404", 404)], [new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)], From 3b2190ca105afb200f8b7a86c87101f718e7f0c7 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 28 Jan 2021 14:55:18 -0500 Subject: [PATCH 140/366] Include folder names directly in subscription list --- lib/Database.php | 8 ++- lib/REST/Miniflux/V1.php | 75 ++++++++++----------- tests/cases/Database/SeriesSubscription.php | 32 +++++---- tests/cases/REST/Miniflux/TestV1.php | 12 +--- 4 files changed, 59 insertions(+), 68 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index f8053ae9..4c7237ad 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -796,19 +796,21 @@ class Database { "SELECT s.id as id, s.feed as feed, - f.url,source,folder,pinned,err_count,err_msg,order_type,added,keep_rule,block_rule,f.etag,s.scrape, + f.url,source,pinned,err_count,err_msg,order_type,added,keep_rule,block_rule,f.etag,s.scrape, f.updated as updated, f.modified as edited, s.modified as modified, f.next_fetch, i.id as icon_id, i.url as icon_url, - t.top as top_folder, + folder, t.top as top_folder, d.name as folder_name, dt.name as top_folder_name, coalesce(s.title, f.title) as title, coalesce((articles - hidden - marked), articles) as unread FROM arsse_subscriptions as s - left join topmost as t on t.f_id = s.folder join arsse_feeds as f on f.id = s.feed + left join topmost as t on t.f_id = s.folder + left join arsse_folders as d on s.folder = d.id + left join arsse_folders as dt on t.top = dt.id left join arsse_icons as i on i.id = f.icon left join ( select diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index efd04695..acecc027 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -554,21 +554,30 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } - protected function baseCategory(): array { - // the root folder is always a category and is always ID 1 - // the specific formulation is verbose, so a function makes sense + /** Returns a useful subset of user metadata + * + * The following keys are included: + * + * - "num": The user's numeric ID, + * - "root": The effective name of the root folder + */ + protected function userMeta(string $user): array { $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); - return ['id' => 1, 'title' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName"), 'user_id' => $meta['num']]; + return [ + 'num' => $meta['num'], + 'root' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName") + ]; } protected function getCategories(): ResponseInterface { + $out = []; // add the root folder as a category - $out = [$this->baseCategory()]; - $num = $out[0]['user_id']; + $meta = $this->userMeta(Arsse::$user->id); + $out[] = ['id' => 1, 'title' => $meta['root'], 'user_id' => $meta['num']]; // add other top folders as categories foreach (Arsse::$db->folderList(Arsse::$user->id, null, false) as $f) { // always add 1 to the ID since the root folder will always be 1 instead of 0. - $out[] = ['id' => $f['id'] + 1, 'title' => $f['name'], 'user_id' => $num]; + $out[] = ['id' => $f['id'] + 1, 'title' => $f['name'], 'user_id' => $meta['num']]; } return new Response($out); } @@ -651,24 +660,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } - protected function mapFolders(): array { - $folders = [0 => $this->baseCategory()]; - $num = $folders[0]['user_id']; - foreach (Arsse::$db->folderList(Arsse::$user->id, null, false) as $r) { - $folders[(int) $r['id']] = [ - 'id' => ((int) $r['id']) + 1, - 'title' => $r['name'], - 'user_id' => $num, - ]; - } - return $folders; - } - - protected function transformFeed(array $sub, array $folders): array { + protected function transformFeed(array $sub, int $uid, string $rootName): array { $url = new Uri($sub['url']); return [ 'id' => (int) $sub['id'], - 'user_id' => $folders[0]['user_id'], + 'user_id' => $uid, 'feed_url' => (string) $url->withUserInfo(""), 'site_url' => (string) $sub['source'], 'title' => (string) $sub['title'], @@ -689,19 +685,21 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'disabled' => false, 'ignore_http_cache' => false, 'fetch_via_proxy' => false, - 'category' => $folders[(int) $sub['top_folder']], + 'category' => [ + 'id' => (int) $sub['top_folder'] + 1, + 'title' => $sub['top_folder_name'] ?? $rootName, + 'user_id' => $uid, + ], 'icon' => $sub['icon_id'] ? ['feed_id' => (int) $sub['id'], 'icon_id' => (int) $sub['icon_id']] : null, ]; } protected function getFeeds(): ResponseInterface { - $tr = Arsse::$db->begin(); - // compile the list of folders; the feed list includes folder names - $folders = $this->mapFolders(); - // next compile the list of feeds $out = []; + $tr = Arsse::$db->begin(); + $meta = $this->userMeta(Arsse::$user->id); foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $r) { - $out[] = $this->transformFeed($r, $folders); + $out[] = $this->transformFeed($r, $meta['num'], $meta['root']); } return new Response($out); } @@ -711,35 +709,30 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $folder = ((int) $path[1]) - 1; // unless the folder is root, list recursive $recursive = $folder > 0; + $out = []; $tr = Arsse::$db->begin(); - // get the list of subscriptions, or bail\ + // get the list of subscriptions, or bail try { - $subs = Arsse::$db->subscriptionList(Arsse::$user->id, $folder, $recursive)->getAll(); + $meta = $this->userMeta(Arsse::$user->id); + foreach (Arsse::$db->subscriptionList(Arsse::$user->id, $folder, $recursive) as $r) { + $out[] = $this->transformFeed($r, $meta['num'], $meta['root']); + } } catch (ExceptionInput $e) { // the folder does not exist return new ErrorResponse("404", 404); } - // compile the list of folders; the feed list includes folder names - // NOTE: We compile the full list of folders in case someone has manually selected a non-top folder - $folders = $this->mapFolders(); - // next compile the list of feeds - $out = []; - foreach ($subs as $r) { - $out[] = $this->transformFeed($r, $folders); - } return new Response($out); } protected function getFeed(array $path): ResponseInterface { $tr = Arsse::$db->begin(); + $meta = $this->userMeta(Arsse::$user->id); try { $sub = Arsse::$db->subscriptionPropertiesGet(Arsse::$user->id, (int) $path[1]); + return new Response($this->transformFeed($sub, $meta['num'], $meta['root'])); } catch (ExceptionInput $e) { return new ErrorResponse("404", 404); } - // compile the list of folders; the feed list includes folder names - $folders = $this->mapFolders(); - return new Response($this->transformFeed($sub, $folders)); } protected function createFeed(array $data): ResponseInterface { diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index e4a2b099..f235a916 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -314,22 +314,26 @@ trait SeriesSubscription { public function testListSubscriptions(): void { $exp = [ [ - 'url' => "http://example.com/feed2", - 'title' => "eek", - 'folder' => null, - 'top_folder' => null, - 'unread' => 4, - 'pinned' => 1, - 'order_type' => 2, + 'url' => "http://example.com/feed2", + 'title' => "eek", + 'folder' => null, + 'top_folder' => null, + 'folder_name' => null, + 'top_folder_name' => null, + 'unread' => 4, + 'pinned' => 1, + 'order_type' => 2, ], [ - 'url' => "http://example.com/feed3", - 'title' => "Ook", - 'folder' => 2, - 'top_folder' => 1, - 'unread' => 2, - 'pinned' => 0, - 'order_type' => 1, + 'url' => "http://example.com/feed3", + 'title' => "Ook", + 'folder' => 2, + 'top_folder' => 1, + 'folder_name' => "Software", + 'top_folder_name' => "Technology", + 'unread' => 2, + 'pinned' => 0, + 'order_type' => 1, ], ]; $this->assertResult($exp, Arsse::$db->subscriptionList($this->user)); diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index d04f45a3..818bffc1 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -67,8 +67,8 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ], ]; protected $feeds = [ - ['id' => 1, 'feed' => 12, 'url' => "http://example.com/ook", 'title' => "Ook", 'source' => "http://example.com/", 'icon_id' => 47, 'icon_url' => "http://example.com/icon", 'folder' => 2112, 'top_folder' => 5, 'pinned' => 0, 'err_count' => 1, 'err_msg' => "Oopsie", 'order_type' => 0, 'keep_rule' => "this|that", 'block_rule' => "both", 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => "2021-01-01 00:00:00", 'modified' => "2020-11-30 04:08:52", 'next_fetch' => "2021-01-20 00:00:00", 'etag' => "OOKEEK", 'scrape' => 0, 'unread' => 42], - ['id' => 55, 'feed' => 12, 'url' => "http://j%20k:super%20secret@example.com/eek", 'title' => "Eek", 'source' => "http://example.com/", 'icon_id' => null, 'icon_url' => null, 'folder' => null, 'top_folder' => null, 'pinned' => 0, 'err_count' => 0, 'err_msg' => null, 'order_type' => 0, 'keep_rule' => null, 'block_rule' => null, 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => null, 'modified' => "2020-11-30 04:08:52", 'next_fetch' => null, 'etag' => null, 'scrape' => 1, 'unread' => 0], + ['id' => 1, 'feed' => 12, 'url' => "http://example.com/ook", 'title' => "Ook", 'source' => "http://example.com/", 'icon_id' => 47, 'icon_url' => "http://example.com/icon", 'folder' => 2112, 'top_folder' => 5, 'folder_name' => "Cat Eek", 'top_folder_name' => "Cat Ook", 'pinned' => 0, 'err_count' => 1, 'err_msg' => "Oopsie", 'order_type' => 0, 'keep_rule' => "this|that", 'block_rule' => "both", 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => "2021-01-01 00:00:00", 'modified' => "2020-11-30 04:08:52", 'next_fetch' => "2021-01-20 00:00:00", 'etag' => "OOKEEK", 'scrape' => 0, 'unread' => 42], + ['id' => 55, 'feed' => 12, 'url' => "http://j%20k:super%20secret@example.com/eek", 'title' => "Eek", 'source' => "http://example.com/", 'icon_id' => null, 'icon_url' => null, 'folder' => null, 'top_folder' => null, 'folder_name' => null, 'top_folder_name' => null, 'pinned' => 0, 'err_count' => 0, 'err_msg' => null, 'order_type' => 0, 'keep_rule' => null, 'block_rule' => null, 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => null, 'modified' => "2020-11-30 04:08:52", 'next_fetch' => null, 'etag' => null, 'scrape' => 1, 'unread' => 0], ]; protected $feedsOut = [ ['id' => 1, 'user_id' => 42, 'feed_url' => "http://example.com/ook", 'site_url' => "http://example.com/", 'title' => "Ook", 'checked_at' => "2021-01-05T13:51:32.000000Z", 'next_check_at' => "2021-01-20T00:00:00.000000Z", 'etag_header' => "OOKEEK", 'last_modified_header' => "Fri, 01 Jan 2021 00:00:00 GMT", 'parsing_error_message' => "Oopsie", 'parsing_error_count' => 1, 'scraper_rules' => "", 'rewrite_rules' => "", 'crawler' => false, 'blocklist_rules' => "both", 'keeplist_rules' => "this|that", 'user_agent' => "", 'username' => "", 'password' => "", 'disabled' => false, 'ignore_http_cache' => false, 'fetch_via_proxy' => false, 'category' => ['id' => 6, 'title' => "Cat Ook", 'user_id' => 42], 'icon' => ['feed_id' => 1,'icon_id' => 47]], @@ -545,18 +545,12 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } public function testListFeeds(): void { - \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ - ['id' => 5, 'name' => "Cat Ook"], - ]))); \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v($this->feeds))); $exp = new Response($this->feedsOut); $this->assertMessage($exp, $this->req("GET", "/feeds")); } public function testListFeedsOfACategory(): void { - \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ - ['id' => 5, 'name' => "Cat Ook"], - ]))); \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v($this->feeds))); $exp = new Response($this->feedsOut); $this->assertMessage($exp, $this->req("GET", "/categories/2112/feeds")); @@ -564,7 +558,6 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } public function testListFeedsOfTheRootCategory(): void { - \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([['id' => 5, 'name' => "Cat Ook"],]))); \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v($this->feeds))); $exp = new Response($this->feedsOut); $this->assertMessage($exp, $this->req("GET", "/categories/1/feeds")); @@ -580,7 +573,6 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function testGetAFeed(): void { \Phake::when(Arsse::$db)->subscriptionPropertiesGet->thenReturn($this->v($this->feeds[0]))->thenReturn($this->v($this->feeds[1])); - \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([['id' => 5, 'name' => "Cat Ook"],]))); $this->assertMessage(new Response($this->feedsOut[0]), $this->req("GET", "/feeds/1")); \Phake::verify(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 1); $this->assertMessage(new Response($this->feedsOut[1]), $this->req("GET", "/feeds/55")); From 1e924bed83d9900a254248274404ec9d8447fba1 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 30 Jan 2021 13:38:02 -0500 Subject: [PATCH 141/366] Partial query string normalization --- lib/REST/Miniflux/V1.php | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index acecc027..17864bbc 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -32,6 +32,20 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected const ACCEPTED_TYPES_OPML = ["application/xml", "text/xml", "text/x-opml"]; protected const ACCEPTED_TYPES_JSON = ["application/json"]; protected const TOKEN_LENGTH = 32; + protected const VALID_QUERY = [ + 'status' => V::T_STRING + V::M_ARRAY, + 'offset' => V::T_INT, + 'limit' => V::T_INT, + 'order' => V::T_STRING, + 'direction' => V::T_STRING, + 'before' => V::T_DATE, // Unix timestamp + 'after' => V::T_DATE, // Unix timestamp + 'before_entry_id' => V::T_INT, + 'after_entry_id' => V::T_INT, + 'starred' => V::T_BOOL, + 'search' => V::T_STRING, + 'category_id' => V::T_INT, + ]; protected const VALID_JSON = [ // user properties which map directly to Arsse user metadata are listed separately; // not all these properties are used by our implementation, but they are treated @@ -345,6 +359,32 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return $body; } + protected function normalizeQuery(string $query): array { + // fill an array with all valid keys + $out = []; + foreach (self::VALID_QUERY as $k => $t) { + $out[$k] = ($t >= V::M_ARRAY) ? [] : null; + } + // split the query string and normalize the values to their correct types + foreach (explode("&", $query) as $parts) { + $parts = explode("=", $parts, 2); + $k = rawurldecode($parts[0]); + $v = (isset($parts[1])) ? rawurldecode($parts[1]) : null; + if (!isset(self::VALID_QUERY[$k]) || !isset($v)) { + // ignore unknown keys and missing values + continue; + } + $t = self::VALID_QUERY[$k] & ~V::M_ARRAY; + $a = self::VALID_QUERY[$k] >= V::M_ARRAY; + if ($a) { + $out[$k][] = V::normalize($v, $t + V::M_DROP, "unix"); + } elseif (!isset($out[$k])) { + $out[$k] = V::normalize($v, $t + V::M_DROP, "unix"); + } + } + return $out; + } + protected function handleHTTPOptions(string $url): ResponseInterface { // normalize the URL path: change any IDs to 1 for easier comparison $url = $this->normalizePathIDs($url); From bb890834444e815a5710a27b0eba01c3fcec6f4f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 30 Jan 2021 21:37:19 -0500 Subject: [PATCH 142/366] Perform strict validation of query parameters This is in fact stricter than Miniflux, which ignores duplicate values and does not validate anything other than the string enumerations --- lib/Conf.php | 10 +--------- lib/Misc/ValueInfo.php | 12 ++++++++++++ lib/REST/Miniflux/V1.php | 41 ++++++++++++++++++++++++++++------------ locale/en.php | 1 + 4 files changed, 43 insertions(+), 21 deletions(-) diff --git a/lib/Conf.php b/lib/Conf.php index c6fd33c1..dfe35e29 100644 --- a/lib/Conf.php +++ b/lib/Conf.php @@ -113,14 +113,6 @@ class Conf { /** @var \DateInterval|null (OBSOLETE) Number of seconds for SQLite to wait before returning a timeout error when trying to acquire a write lock on the database (zero does not wait) */ public $dbSQLite3Timeout = null; // previously 60.0 - - protected const TYPE_NAMES = [ - Value::T_BOOL => "boolean", - Value::T_STRING => "string", - Value::T_FLOAT => "float", - VALUE::T_INT => "integer", - Value::T_INTERVAL => "interval", - ]; protected const EXPECTED_TYPES = [ 'dbTimeoutExec' => "double", 'dbTimeoutLock' => "double", @@ -318,7 +310,7 @@ class Conf { return $value; } catch (ExceptionType $e) { $type = $this->types[$key]['const'] & ~(Value::M_STRICT | Value::M_DROP | Value::M_NULL | Value::M_ARRAY); - throw new Conf\Exception("typeMismatch", ['param' => $key, 'type' => self::TYPE_NAMES[$type], 'file' => $file, 'nullable' => $nullable]); + throw new Conf\Exception("typeMismatch", ['param' => $key, 'type' => Value::TYPE_NAMES[$type], 'file' => $file, 'nullable' => $nullable]); } } diff --git a/lib/Misc/ValueInfo.php b/lib/Misc/ValueInfo.php index e9af5272..9977e786 100644 --- a/lib/Misc/ValueInfo.php +++ b/lib/Misc/ValueInfo.php @@ -35,6 +35,17 @@ class ValueInfo { public const M_DROP = 1 << 29; // drop the value (return null) if the type doesn't match public const M_STRICT = 1 << 30; // throw an exception if the type doesn't match public const M_ARRAY = 1 << 31; // the value should be a flat array of values of the specified type; indexed and associative are both acceptable + public const TYPE_NAMES = [ + self::T_MIXED => "mixed", + self::T_NULL => "null", + self::T_BOOL => "boolean", + self::T_INT => "integer", + self::T_FLOAT => "float", + self::T_DATE => "date", + self::T_STRING => "string", + self::T_ARRAY => "array", + self::T_INTERVAL => "interval", + ]; // symbolic date and time formats protected const DATE_FORMATS = [ // in out 'iso8601' => ["!Y-m-d\TH:i:s", "Y-m-d\TH:i:s\Z" ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets @@ -48,6 +59,7 @@ class ValueInfo { 'float' => ["U.u", "U.u" ], ]; + public static function normalize($value, int $type, string $dateInFormat = null, $dateOutFormat = null) { $allowNull = ($type & self::M_NULL); $strict = ($type & (self::M_STRICT | self::M_DROP)); diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 17864bbc..c6761213 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -8,6 +8,7 @@ namespace JKingWeb\Arsse\REST\Miniflux; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Feed; +use JKingWeb\Arsse\ExceptionType; use JKingWeb\Arsse\Feed\Exception as FeedException; use JKingWeb\Arsse\AbstractException; use JKingWeb\Arsse\Context\Context; @@ -118,7 +119,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'DELETE' => ["deleteCategory", false, true, false, false, []], ], '/categories/1/entries' => [ - 'GET' => ["getCategoryEntries", false, true, false, false, []], + 'GET' => ["getCategoryEntries", false, true, false, true, []], ], '/categories/1/entries/1' => [ 'GET' => ["getCategoryEntry", false, true, false, false, []], @@ -155,7 +156,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'DELETE' => ["deleteFeed", false, true, false, false, []], ], '/feeds/1/entries' => [ - 'GET' => ["getFeedEntries", false, true, false, false, []], + 'GET' => ["getFeedEntries", false, true, false, true, []], ], '/feeds/1/entries/1' => [ 'GET' => ["getFeedEntry", false, true, false, false, []], @@ -226,7 +227,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("401", 401); } // get the request path only; this is assumed to already be normalized - $target = parse_url($req->getRequestTarget())['path'] ?? ""; + $target = parse_url($req->getRequestTarget(), \PHP_URL_PATH) ?? ""; $method = $req->getMethod(); // handle HTTP OPTIONS requests if ($method === "OPTIONS") { @@ -270,7 +271,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $args[] = $data; } if ($reqQuery) { - $args[] = $req->getQueryParams(); + $args[] = $this->normalizeQuery(parse_url($req->getRequestTarget(), \PHP_URL_QUERY) ?? ""); } try { return $this->$func(...$args); @@ -330,9 +331,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } elseif (gettype($body[$k]) !== $t) { return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422); } elseif ( - (in_array($k, ["keeplist_rules", "blocklist_rules"]) && !Rule::validate($body[$k])) || - (in_array($k, ["url", "feed_url"]) && !URL::absolute($body[$k])) || - ($k === "category_id" && $body[$k] < 1) + (in_array($k, ["keeplist_rules", "blocklist_rules"]) && !Rule::validate($body[$k])) + || (in_array($k, ["url", "feed_url"]) && !URL::absolute($body[$k])) + || ($k === "category_id" && $body[$k] < 1) ) { return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422); } @@ -359,7 +360,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return $body; } - protected function normalizeQuery(string $query): array { + protected function normalizeQuery(string $query) { // fill an array with all valid keys $out = []; foreach (self::VALID_QUERY as $k => $t) { @@ -376,10 +377,26 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } $t = self::VALID_QUERY[$k] & ~V::M_ARRAY; $a = self::VALID_QUERY[$k] >= V::M_ARRAY; - if ($a) { - $out[$k][] = V::normalize($v, $t + V::M_DROP, "unix"); - } elseif (!isset($out[$k])) { - $out[$k] = V::normalize($v, $t + V::M_DROP, "unix"); + try { + if ($a) { + $out[$k][] = V::normalize($v, $t + V::M_STRICT, "unix"); + } elseif (!isset($out[$k])) { + $out[$k] = V::normalize($v, $t + V::M_STRICT, "unix"); + } else { + return new ErrorResponse(["DuplicateInputValue", 'field' => $k], 400); + } + } catch (ExceptionType $e) { + return new ErrorResponse(["InvalidInputValue", 'field' => $k], 400); + } + if ( + // TODO: does the "starred" param accept 0/1, or just true/false? + (in_array($k, ["category_id", "before_entry_id", "after_entry_id"]) && $v < 1) + || (in_array($k, ["limit", "offset"]) && $v < 0) + || ($k === "direction" && !in_array($v, ["asc", "desc"])) + || ($k === "order" && !in_array($v, ["id", "status", "published_at", "category_title", "category_id"])) + || ($k === "status" && !in_array($v, ["read", "unread", "removed"])) + ) { + return new ErrorResponse(["InvalidInputValue", 'field' => $k], 400); } } return $out; diff --git a/locale/en.php b/locale/en.php index b44d7490..03b0579f 100644 --- a/locale/en.php +++ b/locale/en.php @@ -12,6 +12,7 @@ return [ 'API.Miniflux.Error.403' => 'Access Forbidden', 'API.Miniflux.Error.404' => 'Resource Not Found', 'API.Miniflux.Error.MissingInputValue' => 'Required key "{field}" was not present in input', + 'API.Miniflux.Error.DuplicateInputValue' => 'Key "{field}" accepts only one value', 'API.Miniflux.Error.InvalidBodyJSON' => 'Invalid JSON payload: {0}', 'API.Miniflux.Error.InvalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}', 'API.Miniflux.Error.InvalidInputValue' => 'Supplied value is not valid for input key "{field}"', From ddbcb598e8885234aa4c7f63c8ff739059216b0b Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 31 Jan 2021 10:44:27 -0500 Subject: [PATCH 143/366] Match more closely Miniflux query string behaviour - The starred key is a simople boolean whose value is immaterial - Blank values are honoured for keys other than starred and status --- lib/REST/Miniflux/V1.php | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index c6761213..23d9e021 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -43,7 +43,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'after' => V::T_DATE, // Unix timestamp 'before_entry_id' => V::T_INT, 'after_entry_id' => V::T_INT, - 'starred' => V::T_BOOL, + 'starred' => V::T_MIXED, // the presence of the starred key is the only thing considered by Miniflux 'search' => V::T_STRING, 'category_id' => V::T_INT, ]; @@ -271,7 +271,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $args[] = $data; } if ($reqQuery) { - $args[] = $this->normalizeQuery(parse_url($req->getRequestTarget(), \PHP_URL_QUERY) ?? ""); + $query = $this->normalizeQuery(parse_url($req->getRequestTarget(), \PHP_URL_QUERY) ?? ""); + if ($query instanceof ResponseInterface) { + return $query; + } + $args[] = $query; } try { return $this->$func(...$args); @@ -363,33 +367,46 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function normalizeQuery(string $query) { // fill an array with all valid keys $out = []; + $seen = []; foreach (self::VALID_QUERY as $k => $t) { $out[$k] = ($t >= V::M_ARRAY) ? [] : null; + $seen[$k] = false; } // split the query string and normalize the values to their correct types foreach (explode("&", $query) as $parts) { $parts = explode("=", $parts, 2); $k = rawurldecode($parts[0]); - $v = (isset($parts[1])) ? rawurldecode($parts[1]) : null; - if (!isset(self::VALID_QUERY[$k]) || !isset($v)) { - // ignore unknown keys and missing values + $v = (isset($parts[1])) ? rawurldecode($parts[1]) : ""; + if (!isset(self::VALID_QUERY[$k])) { + // ignore unknown keys continue; } $t = self::VALID_QUERY[$k] & ~V::M_ARRAY; $a = self::VALID_QUERY[$k] >= V::M_ARRAY; try { - if ($a) { - $out[$k][] = V::normalize($v, $t + V::M_STRICT, "unix"); - } elseif (!isset($out[$k])) { - $out[$k] = V::normalize($v, $t + V::M_STRICT, "unix"); - } else { + if ($seen[$k] && !$a) { + // if the key has already been seen and it's not an array field, bail + // NOTE: Miniflux itself simply ignores duplicates entirely return new ErrorResponse(["DuplicateInputValue", 'field' => $k], 400); } + $seen[$k] = true; + if ($k === "starred") { + // the starred key is a special case in that Miniflux only considers the presence of the key + $out[$k] = true; + continue; + } elseif ($v === "") { + // if the value is empty we can discard the value, but subsequent values for the same non-array key are still considered duplicates + continue; + } elseif ($a) { + $out[$k][] = V::normalize($v, $t + V::M_STRICT, "unix"); + } else { + $out[$k] = V::normalize($v, $t + V::M_STRICT, "unix"); + } } catch (ExceptionType $e) { return new ErrorResponse(["InvalidInputValue", 'field' => $k], 400); } + // perform additional validation if ( - // TODO: does the "starred" param accept 0/1, or just true/false? (in_array($k, ["category_id", "before_entry_id", "after_entry_id"]) && $v < 1) || (in_array($k, ["limit", "offset"]) && $v < 0) || ($k === "direction" && !in_array($v, ["asc", "desc"])) From 197cbba77dd1caff18368d6129d70b8741f47d4d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 1 Feb 2021 15:48:44 -0500 Subject: [PATCH 144/366] Document article column definitions --- lib/Database.php | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 4c7237ad..62740d4e 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1451,29 +1451,29 @@ class Database { protected function articleColumns(): array { $greatest = $this->db->sqlToken("greatest"); return [ - 'id' => "arsse_articles.id", - 'edition' => "latest_editions.edition", - 'latest_edition' => "max(latest_editions.edition)", - 'url' => "arsse_articles.url", - 'title' => "arsse_articles.title", - 'author' => "arsse_articles.author", - 'content' => "coalesce(case when arsse_subscriptions.scrape = 1 then arsse_articles.content_scraped end, 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", - 'hidden' => "coalesce(arsse_marks.hidden,0)", - '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", + 'id' => "arsse_articles.id", // The article's unchanging numeric ID + 'edition' => "latest_editions.edition", // The article's numeric ID which increases each time it is modified in the feed + 'latest_edition' => "max(latest_editions.edition)", // The most recent of all editions + 'url' => "arsse_articles.url", // The URL of the article's full content + 'title' => "arsse_articles.title", // The title + 'author' => "arsse_articles.author", // The name of the author + 'content' => "coalesce(case when arsse_subscriptions.scrape = 1 then arsse_articles.content_scraped end, arsse_articles.content)", // The article content + 'guid' => "arsse_articles.guid", // The GUID of the article, as presented in the feed (NOTE: Picofeed actually provides a hash of the ID) + 'fingerprint' => "arsse_articles.url_title_hash || ':' || arsse_articles.url_content_hash || ':' || arsse_articles.title_content_hash", // A combination of three hashes + 'folder' => "coalesce(arsse_subscriptions.folder,0)", // The folder of the article's feed. This is mainly for use in WHERE clauses + 'subscription' => "arsse_subscriptions.id", // The article's parent subscription + 'feed' => "arsse_subscriptions.feed", // The article's parent feed + 'hidden' => "coalesce(arsse_marks.hidden,0)", // Whether the article is hidden + 'starred' => "coalesce(arsse_marks.starred,0)", // Whether the article is starred + 'unread' => "abs(coalesce(arsse_marks.read,0) - 1)", // Whether the article is unread + 'note' => "coalesce(arsse_marks.note,'')", // The article's note, if any + 'published_date' => "arsse_articles.published", // The date at which the article was first published i.e. its creation date + 'edited_date' => "arsse_articles.edited", // The date at which the article was last edited according to the feed + 'modified_date' => "arsse_articles.modified", // The date at which the article was last updated in our database + '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'))", // The date at which the article metadata was last modified by the user + 'subscription_title' => "coalesce(arsse_subscriptions.title, arsse_feeds.title)", // The parent subscription's title + 'media_url' => "arsse_enclosures.url", // The URL of the article's enclosure, if any (NOTE: Picofeed only exposes one enclosure) + 'media_type' => "arsse_enclosures.type", // The Content-Type of the article's enclosure, if any ]; } From 007183450a8a9f868cf914dbca68a0eccfe72287 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 1 Feb 2021 21:02:46 -0500 Subject: [PATCH 145/366] Context and column list for article queries Sorting and transformation still need to be figured out --- .../030_Supported_Protocols/005_Miniflux.md | 2 ++ lib/REST/Miniflux/V1.php | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index c098aa1e..da52cb49 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -36,6 +36,8 @@ Miniflux version 2.0.27 is emulated, though not all features are implemented - Filtering rules may not function identically (see below for details) - The `checked_at` field of feeds indicates when the feed was last updated rather than when it was last checked - Creating a feed with the `scrape` property set to `true` might not return scraped content for the initial synchronization +- Querying articles for both read/unread and removed statuses will not return all removed articles +- Search strings will match partial words # Behaviour of filtering (block and keep) rules diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 23d9e021..57398f88 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -109,6 +109,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'keeplist_rules' => "keep_rule", 'blocklist_rules' => "block_rule", ]; + protected const ARTICLE_COLUMNS = ["id", "url", "title", "author", "fingerprint", "subscription", "published_date", "modified_date", "starred", "unread", "content", "media_url", "media_type"]; protected const CALLS = [ // handler method Admin Path Body Query Required fields '/categories' => [ 'GET' => ["getCategories", false, false, false, false, []], @@ -891,6 +892,41 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { ]); } + protected function getEntries(array $query): ResponseInterface { + $c = (new Context) + ->limit($query['limit']) + ->offset($query['offset']) + ->starred($query['starred']) + ->modifiedSince($query['after']) // FIXME: This may not be the correct date field + ->notModifiedSince($query['before']) + ->oldestArticle($query['after_entry_id'] ? $query['after_entry_id'] + 1 : null) // FIXME: This might be edition + ->latestArticle($query['before_entry_id'] ? $query['before_entry_id'] - 1 : null) + ->searchTerms(strlen($query['search'] ?? "") ? preg_split("/\s+/", $query['search']) : null); // NOTE: Miniflux matches only whole words; we match simple substrings + if ($query['category_id']) { + if ($query['category_id'] === 1) { + $c->folderShallow(0); + } else { + $c->folder($query['category_id'] - 1); + } + } + // FIXME: specifying e.g. ?status=read&status=removed should yield all hidden articles and all read articles, but the best we can do is all read articles which are or are not hidden + sort($status = array_unique($query['status'])); + if ($status === ["read", "removed"]) { + $c->unread(false); + } elseif ($status === ["read", "unread"]) { + $c->hidden(false); + } elseif ($status === ["read"]) { + $c->hidden(false)->unread(false); + } elseif ($status === ["removed", "unread"]) { + $c->unread(true); + } elseif ($status === ["removed"]) { + $c->hidden(true); + } elseif ($status === ["unread"]) { + $c->hidden(false)->unread(true); + } + $articles = Arsse::$db->articleList(Arsse::$user->id, $c, self::ARTICLE_COLUMNS); + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokens in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); From 9d7ada7f5969f5bd36ccb2766b88a9e81c1fa313 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 1 Feb 2021 22:11:15 -0500 Subject: [PATCH 146/366] Partial implementation of article sorting --- lib/REST/Miniflux/V1.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 57398f88..5510bd2f 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -924,7 +924,25 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } elseif ($status === ["unread"]) { $c->hidden(false)->unread(true); } - $articles = Arsse::$db->articleList(Arsse::$user->id, $c, self::ARTICLE_COLUMNS); + $desc = $query['direction'] === "desc" ? " desc" : ""; + if ($query['order'] === "id") { + $order = ["id".$desc]; + } elseif ($query['order'] === "status") { + if (!$desc) { + $order = ["hidden", "unread desc"]; + } else { + $order = ["hidden desc", "unread"]; + } + } elseif ($query['order'] === "published_at") { + $order = ["modified_date".$desc]; + } elseif ($query['order'] === "category_title") { + $order = []; // TODO + } elseif ($query['order'] === "catgory_id") { + $order = []; //TODO + } else { + $order = []; + } + $articles = Arsse::$db->articleList(Arsse::$user->id, $c, self::ARTICLE_COLUMNS, $order); } public static function tokenGenerate(string $user, string $label): string { From ed27e0aaaa5e9165ee305eab10bca536d9be6f92 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 2 Feb 2021 10:00:08 -0500 Subject: [PATCH 147/366] Sort nulls consistently PostgreSQL normally sorts nulls after everything else in ascending order and vice versa; we reverse this, to match SQLIte and MySQL --- lib/Db/Driver.php | 2 ++ lib/Db/MySQL/Driver.php | 2 ++ lib/Db/PostgreSQL/Driver.php | 4 ++++ lib/Db/SQLite3/Driver.php | 2 ++ tests/cases/Db/BaseDriver.php | 4 ++++ 5 files changed, 14 insertions(+) diff --git a/lib/Db/Driver.php b/lib/Db/Driver.php index d533b926..cc522dc7 100644 --- a/lib/Db/Driver.php +++ b/lib/Db/Driver.php @@ -75,6 +75,8 @@ interface Driver { * - "nocase": the name of a general-purpose case-insensitive collation sequence * - "like": the case-insensitive LIKE operator * - "integer": the integer type to use for explicit casts + * - "asc": ascending sort order + * - "desc": descending sort order */ public function sqlToken(string $token): string; diff --git a/lib/Db/MySQL/Driver.php b/lib/Db/MySQL/Driver.php index 8a82be44..1c0da1e1 100644 --- a/lib/Db/MySQL/Driver.php +++ b/lib/Db/MySQL/Driver.php @@ -83,6 +83,8 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { return '"utf8mb4_unicode_ci"'; case "integer": return "signed integer"; + case "asc": + return ""; default: return $token; } diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php index fccc0710..c22f0963 100644 --- a/lib/Db/PostgreSQL/Driver.php +++ b/lib/Db/PostgreSQL/Driver.php @@ -119,6 +119,10 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { return '"und-x-icu"'; case "like": return "ilike"; + case "asc": + return "nulls first"; + case "desc": + return "desc nulls last"; default: return $token; } diff --git a/lib/Db/SQLite3/Driver.php b/lib/Db/SQLite3/Driver.php index bef5ec65..3445b892 100644 --- a/lib/Db/SQLite3/Driver.php +++ b/lib/Db/SQLite3/Driver.php @@ -114,6 +114,8 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { switch (strtolower($token)) { case "greatest": return "max"; + case "asc": + return ""; default: return $token; } diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index 665443d3..f47bcf06 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -385,6 +385,8 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { $nocase = $this->drv->sqlToken("noCASE"); $like = $this->drv->sqlToken("liKe"); $integer = $this->drv->sqlToken("InTEGer"); + $asc = $this->drv->sqlToken("asc"); + $desc = $this->drv->sqlToken("desc"); $this->assertSame("NOT_A_TOKEN", $this->drv->sqlToken("NOT_A_TOKEN")); @@ -392,5 +394,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame("Z", $this->drv->query("SELECT 'Z' collate $nocase")->getValue()); $this->assertSame("Z", $this->drv->query("SELECT 'Z' where 'Z' $like 'z'")->getValue()); $this->assertEquals(1, $this->drv->query("SELECT CAST((1=1) as $integer)")->getValue()); + $this->assertEquals([null, 1], array_column($this->drv->query("SELECT 1 as t union select null as t order by t $asc")->getAll(), "t")); + $this->assertEquals([1, null], array_column($this->drv->query("SELECT 1 as t union select null as t order by t $desc")->getAll(), "t")); } } From a43f8797c520bd5d3811540f6a1c1d04bc598aa8 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 2 Feb 2021 11:51:19 -0500 Subject: [PATCH 148/366] Add ability to sort by folder ID or name --- docs/en/030_Supported_Protocols/005_Miniflux.md | 2 ++ lib/Database.php | 10 ++++++++-- lib/REST/Miniflux/V1.php | 4 ++-- tests/cases/Database/SeriesArticle.php | 3 ++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index da52cb49..38ede1c7 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -49,6 +49,8 @@ For convenience the patterns are tested after collapsing whitespace. Unlike Mini Nextcloud News' root folder and Tiny Tiny RSS' "Uncategorized" catgory are mapped to Miniflux's initial "All" category. This Miniflux category can be renamed, but it cannot be deleted. Attempting to do so will delete the child feeds it contains, but not the category itself. +Because the root folder does not existing in the database as a separate entity, it will always sort first when ordering by `category_id` or `category_title`. + # Interaction with nested categories Tiny Tiny RSS is unique in allowing newsfeeds to be grouped into categories nested to arbitrary depth. When newsfeeds are placed into nested categories, they simply appear in the top-level category when accessed via the Miniflux protocol. This does not affect OPML exports, where full nesting is preserved. diff --git a/lib/Database.php b/lib/Database.php index 62740d4e..db6f087d 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1461,6 +1461,9 @@ class Database { 'guid' => "arsse_articles.guid", // The GUID of the article, as presented in the feed (NOTE: Picofeed actually provides a hash of the ID) 'fingerprint' => "arsse_articles.url_title_hash || ':' || arsse_articles.url_content_hash || ':' || arsse_articles.title_content_hash", // A combination of three hashes 'folder' => "coalesce(arsse_subscriptions.folder,0)", // The folder of the article's feed. This is mainly for use in WHERE clauses + 'top_folder' => "coalesce(folder_data.top,0)", // The top-most folder of the article's feed. This is mainly for use in WHERE clauses + 'folder_name' => "folder_data.name", // The name of the folder of the article's feed. This is mainly for use in WHERE clauses + 'top_folder_name' => "folder_data.top_name", // The name of the top-most folder of the article's feed. This is mainly for use in WHERE clauses 'subscription' => "arsse_subscriptions.id", // The article's parent subscription 'feed' => "arsse_subscriptions.feed", // The article's parent feed 'hidden' => "coalesce(arsse_marks.hidden,0)", // Whether the article is hidden @@ -1537,6 +1540,7 @@ class Database { 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 + left join folder_data on arsse_subscriptions.folder = folder_data.id left join arsse_marks on arsse_marks.subscription = arsse_subscriptions.id and arsse_marks.article = arsse_articles.id left join arsse_enclosures on arsse_enclosures.article = arsse_articles.id join ( @@ -1548,6 +1552,8 @@ class Database { ["str", "str"], [$user, $user] ); + $q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders where owner = ? and parent is null union all select id,top from arsse_folders join topmost on parent=f_id", ["str"], [$user]); + $q->setCTE("folder_data(id,name,top,top_name)", "SELECT f1.id, f1.name, top, f2.name from arsse_folders as f1 join topmost on f1.id = f_id join arsse_folders as f2 on f2.id = top"); $q->setLimit($context->limit, $context->offset); // handle the simple context options $options = [ @@ -1788,9 +1794,9 @@ class Database { $order = $col[1] ?? ""; $col = $col[0]; if ($order === "desc") { - $order = " desc"; + $order = " ".$this->db->sqlToken("desc"); } elseif ($order === "asc" || $order === "") { - $order = ""; + $order = " ".$this->db->sqlToken("asc"); } else { // column direction spec is bogus continue; diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 5510bd2f..ea725106 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -936,9 +936,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } elseif ($query['order'] === "published_at") { $order = ["modified_date".$desc]; } elseif ($query['order'] === "category_title") { - $order = []; // TODO + $order = ["top_folder_name".$desc]; } elseif ($query['order'] === "catgory_id") { - $order = []; //TODO + $order = ["top_folder".$desc]; } else { $order = []; } diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 6302f5df..eace73af 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -408,8 +408,9 @@ trait SeriesArticle { ], ]; $this->fields = [ - "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", + "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "hidden", "edition", "edited_date", "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", + "folder", "top_folder", "folder_name", "top_folder_name", "content", "media_url", "media_type", "note", ]; From 0e7abfa8f9fc8c8330f10778224f857db7ddd012 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 2 Feb 2021 16:05:16 -0500 Subject: [PATCH 149/366] Largely complete article querying Tests to come --- lib/REST/Miniflux/V1.php | 110 ++++++++++++++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 12 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index ea725106..ffd6c363 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -109,7 +109,12 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'keeplist_rules' => "keep_rule", 'blocklist_rules' => "block_rule", ]; - protected const ARTICLE_COLUMNS = ["id", "url", "title", "author", "fingerprint", "subscription", "published_date", "modified_date", "starred", "unread", "content", "media_url", "media_type"]; + protected const ARTICLE_COLUMNS = [ + "id", "url", "title", "author", "fingerprint", "subscription", + "published_date", "modified_date", + "starred", "unread", + "content", "media_url", "media_type" + ]; protected const CALLS = [ // handler method Admin Path Body Query Required fields '/categories' => [ 'GET' => ["getCategories", false, false, false, false, []], @@ -640,7 +645,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); return [ 'num' => $meta['num'], - 'root' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName") + 'root' => $meta['root_folder_name'] ?? Arsse::$lang->msg("API.Miniflux.DefaultCategoryName"), + 'tz' => new \DateTimeZone($meta['tz'] ?? "UTC"), ]; } @@ -892,8 +898,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { ]); } - protected function getEntries(array $query): ResponseInterface { - $c = (new Context) + protected function computeContext(array $query, Context $c = null): Context { + $c = ($c ?? new Context) ->limit($query['limit']) ->offset($query['offset']) ->starred($query['starred']) @@ -924,25 +930,105 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } elseif ($status === ["unread"]) { $c->hidden(false)->unread(true); } + return $c; + } + + protected function computeOrder(array $query): array { $desc = $query['direction'] === "desc" ? " desc" : ""; if ($query['order'] === "id") { - $order = ["id".$desc]; + return ["id".$desc]; } elseif ($query['order'] === "status") { if (!$desc) { - $order = ["hidden", "unread desc"]; + return ["hidden", "unread desc"]; } else { - $order = ["hidden desc", "unread"]; + return ["hidden desc", "unread"]; } } elseif ($query['order'] === "published_at") { - $order = ["modified_date".$desc]; + return ["modified_date".$desc]; } elseif ($query['order'] === "category_title") { - $order = ["top_folder_name".$desc]; + return ["top_folder_name".$desc]; } elseif ($query['order'] === "catgory_id") { - $order = ["top_folder".$desc]; + return ["top_folder".$desc]; } else { - $order = []; + return []; } - $articles = Arsse::$db->articleList(Arsse::$user->id, $c, self::ARTICLE_COLUMNS, $order); + } + + protected function transformEntry(array $entry, int $uid, \DateTimeZone $tz): array { + if ($entry['hidden']) { + $status = "removed"; + } elseif ($entry['unread']) { + $status = "unread"; + } else { + $status = "read"; + } + if ($entry['media_url']) { + $enclosures = [ + [ + 'id' => $entry['id'], // NOTE: We don't have IDs for enclosures, but we also only have one enclosure per entry, so we can just re-use the same ID + 'user_id' => $uid, + 'entry_id' => $entry['id'], + 'url' => $entry['media_url'], + 'mime_type' => $entry['media_type'] ?: "application/octet-stream", + 'size' => 0, + ] + ]; + } else { + $enclosures = null; + } + return [ + 'id' => (int) $entry['id'], + 'user_id' => $uid, + 'feed_id' => (int) $entry['subscription'], + 'status' => $status, + 'hash' => $entry['fingerprint'], + 'title' => $entry['title'], + 'url' => $entry['url'], + 'comments_url' => "", + 'published_at' => Date::transform(Date::normalize($entry['published_date'], "sql")->setTimezone($tz), "iso8601"), + 'created_at' => Date::transform(Date::normalize($entry['modified_date'], "sql")->setTimezone($tz), "iso8601m"), + 'content' => $entry['content'], + 'author' => (string) $entry['author'], + 'share_code' => "", + 'starred' => (bool) $entry['starred'], + 'reading_time' => 0, + 'enclosures' => $enclosures, + 'feed' => null, + ]; + } + + protected function getEntries(array $query): ResponseInterface { + $c = $this->computeContext($query); + $order = $this->computeOrder($query); + $tr = Arsse::$db->begin(); + $meta = $this->userMeta(Arsse::$user->id); + // compile the list of entries + try { + $entries = Arsse::$db->articleList(Arsse::$user->id, $c, self::ARTICLE_COLUMNS, $order); + } catch (ExceptionInput $e) { + return new ErrorResponse("MissingCategory", 400); + } + $out = []; + foreach ($entries as $entry) { + $out[] = $this->transformEntry($entry, $meta['num'], $meta['tz']); + } + // next compile a map of feeds to add to the entries + $feeds = []; + foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $r) { + $feeds[(int) $r['id']] = $this->transformFeed($r, $meta['num'], $meta['root']); + } + // add the feed objects to each entry + // NOTE: If ever we implement multiple enclosure, this would be the right place to add them + for ($a = 0; $a < sizeof($out); $a++) { + $out[$a]['feed'] = $feeds[$out[$a]['feed_id']]; + } + // finally compute the total number of entries match the query, if the query hs a limit or offset + if ($c->limit || $c->offset) { + $count = Arsse::$db->articleCount(Arsse::$user->id, $c); + } else { + $count = sizeof($out); + } + return new Response(['total' => $count, 'entries' => $out]); } public static function tokenGenerate(string $user, string $label): string { From 23ca6bb77b7aeb87c871e112dade1d2539226535 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 2 Feb 2021 16:14:04 -0500 Subject: [PATCH 150/366] Count articles without offset or limit --- lib/REST/Miniflux/V1.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index ffd6c363..e7dad341 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -1024,7 +1024,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } // finally compute the total number of entries match the query, if the query hs a limit or offset if ($c->limit || $c->offset) { - $count = Arsse::$db->articleCount(Arsse::$user->id, $c); + $count = Arsse::$db->articleCount(Arsse::$user->id, (clone $c)->limit(0)->offset(0)); } else { $count = sizeof($out); } @@ -1032,7 +1032,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } public static function tokenGenerate(string $user, string $label): string { - // Miniflux produces tokens in base64url alphabet + // Miniflux produces tokenss in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); return Arsse::$db->tokenCreate($user, "miniflux.login", $t, null, $label); } From af51377fe9b43d6c1956cea9d7de0fe9121edb12 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 3 Feb 2021 13:06:36 -0500 Subject: [PATCH 151/366] First set of article query tests --- .../030_Supported_Protocols/005_Miniflux.md | 3 + lib/REST/Miniflux/V1.php | 26 ++- tests/cases/REST/Miniflux/TestV1.php | 188 +++++++++++------- 3 files changed, 132 insertions(+), 85 deletions(-) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index 38ede1c7..62a27bd3 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -26,6 +26,9 @@ Miniflux version 2.0.27 is emulated, though not all features are implemented - The `disabled`, `ignore_http_cache`, and `fetch_via_proxy` flags - Changing the URL, username, or password of a feed - Titles and types are not available during feed discovery and are filled with generic data +- Reading time is not calculated and will always be zero +- Only the first enclosure of an article is retained +- Comment URLs of articles are not exposed # Differences diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index e7dad341..b1c16c09 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -110,9 +110,10 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'blocklist_rules' => "block_rule", ]; protected const ARTICLE_COLUMNS = [ - "id", "url", "title", "author", "fingerprint", "subscription", + "id", "url", "title", "subscription", + "author", "fingerprint", "published_date", "modified_date", - "starred", "unread", + "starred", "unread", "hidden", "content", "media_url", "media_type" ]; protected const CALLS = [ // handler method Admin Path Body Query Required fields @@ -916,7 +917,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } // FIXME: specifying e.g. ?status=read&status=removed should yield all hidden articles and all read articles, but the best we can do is all read articles which are or are not hidden - sort($status = array_unique($query['status'])); + $status = array_unique($query['status']); + sort($status); if ($status === ["read", "removed"]) { $c->unread(false); } elseif ($status === ["read", "unread"]) { @@ -1013,14 +1015,16 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $out[] = $this->transformEntry($entry, $meta['num'], $meta['tz']); } // next compile a map of feeds to add to the entries - $feeds = []; - foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $r) { - $feeds[(int) $r['id']] = $this->transformFeed($r, $meta['num'], $meta['root']); - } - // add the feed objects to each entry - // NOTE: If ever we implement multiple enclosure, this would be the right place to add them - for ($a = 0; $a < sizeof($out); $a++) { - $out[$a]['feed'] = $feeds[$out[$a]['feed_id']]; + if ($out) { + $feeds = []; + foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $r) { + $feeds[(int) $r['id']] = $this->transformFeed($r, $meta['num'], $meta['root']); + } + // add the feed objects to each entry + // NOTE: If ever we implement multiple enclosure, this would be the right place to add them + for ($a = 0; $a < sizeof($out); $a++) { + $out[$a]['feed'] = $feeds[$out[$a]['feed_id']]; + } } // finally compute the total number of entries match the query, if the query hs a limit or offset if ($c->limit || $c->offset) { diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 818bffc1..899ab9b6 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -26,54 +26,61 @@ use JKingWeb\Arsse\Test\Result; /** @covers \JKingWeb\Arsse\REST\Miniflux\V1 */ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { protected const NOW = "2020-12-09T22:35:10.023419Z"; - - protected $h; - protected $transaction; - protected $token = "Tk2o9YubmZIL2fm2w8Z4KlDEQJz532fNSOcTG0s2_xc="; - protected $users = [ - [ - 'id' => 1, - 'username' => "john.doe@example.com", - 'last_login_at' => self::NOW, - 'google_id' => "", - 'openid_connect_id' => "", - 'is_admin' => true, - 'theme' => "custom", - 'language' => "fr_CA", - 'timezone' => "Asia/Gaza", - 'entry_sorting_direction' => "asc", - 'entries_per_page' => 200, - 'keyboard_shortcuts' => false, - 'show_reading_time' => false, - 'entry_swipe' => false, - 'stylesheet' => "p {}", - ], - [ - 'id' => 2, - 'username' => "jane.doe@example.com", - 'last_login_at' => self::NOW, - 'google_id' => "", - 'openid_connect_id' => "", - 'is_admin' => false, - 'theme' => "light_serif", - 'language' => "en_US", - 'timezone' => "UTC", - 'entry_sorting_direction' => "desc", - 'entries_per_page' => 100, - 'keyboard_shortcuts' => true, - 'show_reading_time' => true, - 'entry_swipe' => true, - 'stylesheet' => "", - ], + protected const TOKEN = "Tk2o9YubmZIL2fm2w8Z4KlDEQJz532fNSOcTG0s2_xc="; + protected const USERS = [ + ['id' => 1, 'username' => "john.doe@example.com", 'last_login_at' => self::NOW, 'google_id' => "", 'openid_connect_id' => "", 'is_admin' => true, 'theme' => "custom", 'language' => "fr_CA", 'timezone' => "Asia/Gaza", 'entry_sorting_direction' => "asc", 'entries_per_page' => 200, 'keyboard_shortcuts' => false, 'show_reading_time' => false, 'entry_swipe' => false, 'stylesheet' => "p {}"], + ['id' => 2, 'username' => "jane.doe@example.com", 'last_login_at' => self::NOW, 'google_id' => "", 'openid_connect_id' => "", 'is_admin' => false, 'theme' => "light_serif", 'language' => "en_US", 'timezone' => "UTC", 'entry_sorting_direction' => "desc", 'entries_per_page' => 100, 'keyboard_shortcuts' => true, 'show_reading_time' => true, 'entry_swipe' => true, 'stylesheet' => ""], ]; - protected $feeds = [ + protected const FEEDS = [ ['id' => 1, 'feed' => 12, 'url' => "http://example.com/ook", 'title' => "Ook", 'source' => "http://example.com/", 'icon_id' => 47, 'icon_url' => "http://example.com/icon", 'folder' => 2112, 'top_folder' => 5, 'folder_name' => "Cat Eek", 'top_folder_name' => "Cat Ook", 'pinned' => 0, 'err_count' => 1, 'err_msg' => "Oopsie", 'order_type' => 0, 'keep_rule' => "this|that", 'block_rule' => "both", 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => "2021-01-01 00:00:00", 'modified' => "2020-11-30 04:08:52", 'next_fetch' => "2021-01-20 00:00:00", 'etag' => "OOKEEK", 'scrape' => 0, 'unread' => 42], ['id' => 55, 'feed' => 12, 'url' => "http://j%20k:super%20secret@example.com/eek", 'title' => "Eek", 'source' => "http://example.com/", 'icon_id' => null, 'icon_url' => null, 'folder' => null, 'top_folder' => null, 'folder_name' => null, 'top_folder_name' => null, 'pinned' => 0, 'err_count' => 0, 'err_msg' => null, 'order_type' => 0, 'keep_rule' => null, 'block_rule' => null, 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => null, 'modified' => "2020-11-30 04:08:52", 'next_fetch' => null, 'etag' => null, 'scrape' => 1, 'unread' => 0], ]; - protected $feedsOut = [ + protected const FEEDS_OUT = [ ['id' => 1, 'user_id' => 42, 'feed_url' => "http://example.com/ook", 'site_url' => "http://example.com/", 'title' => "Ook", 'checked_at' => "2021-01-05T13:51:32.000000Z", 'next_check_at' => "2021-01-20T00:00:00.000000Z", 'etag_header' => "OOKEEK", 'last_modified_header' => "Fri, 01 Jan 2021 00:00:00 GMT", 'parsing_error_message' => "Oopsie", 'parsing_error_count' => 1, 'scraper_rules' => "", 'rewrite_rules' => "", 'crawler' => false, 'blocklist_rules' => "both", 'keeplist_rules' => "this|that", 'user_agent' => "", 'username' => "", 'password' => "", 'disabled' => false, 'ignore_http_cache' => false, 'fetch_via_proxy' => false, 'category' => ['id' => 6, 'title' => "Cat Ook", 'user_id' => 42], 'icon' => ['feed_id' => 1,'icon_id' => 47]], ['id' => 55, 'user_id' => 42, 'feed_url' => "http://example.com/eek", 'site_url' => "http://example.com/", 'title' => "Eek", 'checked_at' => "2021-01-05T13:51:32.000000Z", 'next_check_at' => "0001-01-01T00:00:00.000000Z", 'etag_header' => "", 'last_modified_header' => "", 'parsing_error_message' => "", 'parsing_error_count' => 0, 'scraper_rules' => "", 'rewrite_rules' => "", 'crawler' => true, 'blocklist_rules' => "", 'keeplist_rules' => "", 'user_agent' => "", 'username' => "j k", 'password' => "super secret", 'disabled' => false, 'ignore_http_cache' => false, 'fetch_via_proxy' => false, 'category' => ['id' => 1,'title' => "All", 'user_id' => 42], 'icon' => null], ]; + protected const ENTRIES = [ + [ + 'id' => 42, + 'url' => "http://example.com/42", + 'title' => "Title 42", + 'subscription' => 2112, + 'author' => "Thomas Costain", + 'fingerprint' => "FINGERPRINT", + 'published_date' => "2021-01-22 02:21:12", + 'modified_date' => "2021-01-22 13:44:47", + 'starred' => 0, + 'unread' => 0, + 'hidden' => 0, + 'content' => "Content 42", + 'media_url' => null, + 'media_type' => null, + ], + ]; + protected const ENTRIES_OUT = [ + [ + 'id' => 42, + 'user_id' => 42, + 'feed_id' => 55, + 'status' => "read", + 'hash' => "FINGERPRINT", + 'title' => "Title 42", + 'url' => "http://example.com/42", + 'comments_url' => "", + 'published_at' => "2021-01-22T02:21:12+00:00", + 'created_at' => "2021-01-22T13:44:47.000000+00:00", + 'content' => "Content 42", + 'author' => "Thomas Costain", + 'share_code' => "", + 'starred' => false, + 'reading_time' => 0, + 'enclosures' => null, + 'feed' => self::FEEDS_OUT[1], + ], + ]; + + protected $h; + protected $transaction; protected function req(string $method, string $target, $data = "", array $headers = [], ?string $user = "john.doe@example.com", bool $body = true): ResponseInterface { $prefix = "/v1"; @@ -122,23 +129,23 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } Arsse::$user->id = null; \Phake::when(Arsse::$db)->tokenLookup->thenThrow(new ExceptionInput("subjectMissing")); - \Phake::when(Arsse::$db)->tokenLookup("miniflux.login", $this->token)->thenReturn(['user' => $user]); + \Phake::when(Arsse::$db)->tokenLookup("miniflux.login", self::TOKEN)->thenReturn(['user' => $user]); $this->assertMessage($exp, $this->req("GET", "/", "", $headers, $auth ? "john.doe@example.com" : null)); $this->assertSame($success ? $user : null, Arsse::$user->id); } public function provideAuthResponses(): iterable { return [ - [null, false, false], - [null, true, true], - [$this->token, false, true], - [[$this->token, "BOGUS"], false, true], - ["", true, true], - [["", "BOGUS"], true, true], - ["NOT A TOKEN", false, false], - ["NOT A TOKEN", true, false], - [["BOGUS", $this->token], false, false], - [["", $this->token], false, false], + [null, false, false], + [null, true, true], + [self::TOKEN, false, true], + [[self::TOKEN, "BOGUS"], false, true], + ["", true, true], + [["", "BOGUS"], true, true], + ["NOT A TOKEN", false, false], + ["NOT A TOKEN", true, false], + [["BOGUS", self::TOKEN], false, false], + [["", self::TOKEN], false, false], ]; } @@ -239,16 +246,16 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function provideUserQueries(): iterable { self::clearData(); return [ - [true, "/users", new Response($this->users)], - [true, "/me", new Response($this->users[0])], - [true, "/users/john.doe@example.com", new Response($this->users[0])], - [true, "/users/1", new Response($this->users[0])], - [true, "/users/jane.doe@example.com", new Response($this->users[1])], - [true, "/users/2", new Response($this->users[1])], + [true, "/users", new Response(self::USERS)], + [true, "/me", new Response(self::USERS[0])], + [true, "/users/john.doe@example.com", new Response(self::USERS[0])], + [true, "/users/1", new Response(self::USERS[0])], + [true, "/users/jane.doe@example.com", new Response(self::USERS[1])], + [true, "/users/2", new Response(self::USERS[1])], [true, "/users/jack.doe@example.com", new ErrorResponse("404", 404)], [true, "/users/47", new ErrorResponse("404", 404)], [false, "/users", new ErrorResponse("403", 403)], - [false, "/me", new Response($this->users[1])], + [false, "/me", new Response(self::USERS[1])], [false, "/users/john.doe@example.com", new ErrorResponse("403", 403)], [false, "/users/1", new ErrorResponse("403", 403)], [false, "/users/jane.doe@example.com", new ErrorResponse("403", 403)], @@ -318,8 +325,8 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function provideUserModifications(): iterable { $out1 = ['num' => 2, 'admin' => false]; $out2 = ['num' => 1, 'admin' => false]; - $resp1 = array_merge($this->users[1], ['username' => "john.doe@example.com"]); - $resp2 = array_merge($this->users[1], ['id' => 1, 'is_admin' => true]); + $resp1 = array_merge(self::USERS[1], ['username' => "john.doe@example.com"]); + $resp2 = array_merge(self::USERS[1], ['id' => 1, 'is_admin' => true]); return [ [false, "/users/1", ['is_admin' => 0], null, null, null, null, null, null, new ErrorResponse(["InvalidInputType", 'field' => "is_admin", 'expected' => "boolean", 'actual' => "integer"], 422)], [false, "/users/1", ['entry_sorting_direction' => "bad"], null, null, null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "entry_sorting_direction"], 422)], @@ -376,7 +383,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } public function provideUserAdditions(): iterable { - $resp1 = array_merge($this->users[1], ['username' => "ook", 'password' => "eek"]); + $resp1 = array_merge(self::USERS[1], ['username' => "ook", 'password' => "eek"]); return [ [[], null, null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "username"], 422)], [['username' => "ook"], null, null, null, null, new ErrorResponse(["MissingInputValue", 'field' => "password"], 422)], @@ -545,21 +552,21 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } public function testListFeeds(): void { - \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v($this->feeds))); - $exp = new Response($this->feedsOut); + \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v(self::FEEDS))); + $exp = new Response(self::FEEDS_OUT); $this->assertMessage($exp, $this->req("GET", "/feeds")); } public function testListFeedsOfACategory(): void { - \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v($this->feeds))); - $exp = new Response($this->feedsOut); + \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v(self::FEEDS))); + $exp = new Response(self::FEEDS_OUT); $this->assertMessage($exp, $this->req("GET", "/categories/2112/feeds")); \Phake::verify(Arsse::$db)->subscriptionList(Arsse::$user->id, 2111, true); } public function testListFeedsOfTheRootCategory(): void { - \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v($this->feeds))); - $exp = new Response($this->feedsOut); + \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v(self::FEEDS))); + $exp = new Response(self::FEEDS_OUT); $this->assertMessage($exp, $this->req("GET", "/categories/1/feeds")); \Phake::verify(Arsse::$db)->subscriptionList(Arsse::$user->id, 0, false); } @@ -572,10 +579,10 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } public function testGetAFeed(): void { - \Phake::when(Arsse::$db)->subscriptionPropertiesGet->thenReturn($this->v($this->feeds[0]))->thenReturn($this->v($this->feeds[1])); - $this->assertMessage(new Response($this->feedsOut[0]), $this->req("GET", "/feeds/1")); + \Phake::when(Arsse::$db)->subscriptionPropertiesGet->thenReturn($this->v(self::FEEDS[0]))->thenReturn($this->v(self::FEEDS[1])); + $this->assertMessage(new Response(self::FEEDS_OUT[0]), $this->req("GET", "/feeds/1")); \Phake::verify(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 1); - $this->assertMessage(new Response($this->feedsOut[1]), $this->req("GET", "/feeds/55")); + $this->assertMessage(new Response(self::FEEDS_OUT[1]), $this->req("GET", "/feeds/55")); \Phake::verify(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 55); } @@ -679,7 +686,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideFeedModifications */ public function testModifyAFeed(array $in, array $data, $out, ResponseInterface $exp): void { $this->h = \Phake::partialMock(V1::class); - \Phake::when($this->h)->getFeed->thenReturn(new Response($this->feedsOut[0])); + \Phake::when($this->h)->getFeed->thenReturn(new Response(self::FEEDS_OUT[0])); if ($out instanceof \Exception) { \Phake::when(Arsse::$db)->subscriptionPropertiesSet->thenThrow($out); } else { @@ -691,7 +698,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function provideFeedModifications(): iterable { self::clearData(); - $success = new Response($this->feedsOut[0]); + $success = new Response(self::FEEDS_OUT[0]); return [ [[], [], true, $success], [[], [], new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)], @@ -730,7 +737,6 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } public function provideIcons(): iterable { - self::clearData(); return [ [['id' => 44, 'type' => "image/svg+xml", 'data' => ""], new Response(['id' => 44, 'data' => "image/svg+xml;base64,PHN2Zy8+", 'mime_type' => "image/svg+xml"])], [['id' => 47, 'type' => "", 'data' => ""], new ErrorResponse("404", 404)], @@ -740,4 +746,38 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { [new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)], ]; } + + /** @dataProvider provideEntryQueries */ + public function testGetEntries(string $url, ?Context $c, ?array $order, $out, bool $getFeeds, ResponseInterface $exp) { + if ($out instanceof \Exception) { + \Phake::when(Arsse::$db)->articleList->thenThrow($out); + } else { + \Phake::when(Arsse::$db)->articleList->thenReturn(new Result($this->v($out))); + } + $this->assertMessage($exp, $this->req("GET", $url)); + if ($c) { + \Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, $c, array_keys(self::ENTRIES[0]), $order); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->articleList; + } + } + + public function provideEntryQueries(): iterable { + self::clearData(); + $c = new Context; + return [ + ["/entries?after=A", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "after"], 400)], + ["/entries?before=B", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "before"], 400)], + ["/entries?category_id=0", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 400)], + ["/entries?after_entry_id=0", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "after_entry_id"], 400)], + ["/entries?before_entry_id=0", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "before_entry_id"], 400)], + ["/entries?limit=-1", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "limit"], 400)], + ["/entries?offset=-1", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "offset"], 400)], + ["/entries?direction=sideways", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "direction"], 400)], + ["/entries?order=false", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "order"], 400)], + ["/entries?starred&starred", null, null, [], false, new ErrorResponse(["DuplicateInputValue", 'field' => "starred"], 400)], + ["/entries?after&after=0", null, null, [], false, new ErrorResponse(["DuplicateInputValue", 'field' => "after"], 400)], + ["/entries", $c, [], [], false, new Response(['total' => 0, 'entries' => []])], + ]; + } } From f7b3a473a995c5f5412fff4658f234ea5e8c9bd7 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 3 Feb 2021 14:20:34 -0500 Subject: [PATCH 152/366] Clarify ordering syntax rationale --- lib/Db/Driver.php | 4 ++-- tests/cases/Db/BaseDriver.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Db/Driver.php b/lib/Db/Driver.php index cc522dc7..09f16e78 100644 --- a/lib/Db/Driver.php +++ b/lib/Db/Driver.php @@ -75,8 +75,8 @@ interface Driver { * - "nocase": the name of a general-purpose case-insensitive collation sequence * - "like": the case-insensitive LIKE operator * - "integer": the integer type to use for explicit casts - * - "asc": ascending sort order - * - "desc": descending sort order + * - "asc": ascending sort order when dealing with nulls + * - "desc": descending sort order when dealing with nulls */ public function sqlToken(string $token): string; diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index f47bcf06..e34cf65f 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -394,7 +394,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame("Z", $this->drv->query("SELECT 'Z' collate $nocase")->getValue()); $this->assertSame("Z", $this->drv->query("SELECT 'Z' where 'Z' $like 'z'")->getValue()); $this->assertEquals(1, $this->drv->query("SELECT CAST((1=1) as $integer)")->getValue()); - $this->assertEquals([null, 1], array_column($this->drv->query("SELECT 1 as t union select null as t order by t $asc")->getAll(), "t")); - $this->assertEquals([1, null], array_column($this->drv->query("SELECT 1 as t union select null as t order by t $desc")->getAll(), "t")); + $this->assertEquals([null, 1, 2], array_column($this->drv->query("SELECT 1 as t union select null as t union select 2 as t order by t $asc")->getAll(), "t")); + $this->assertEquals([2, 1, null], array_column($this->drv->query("SELECT 1 as t union select null as t union select 2 as t order by t $desc")->getAll(), "t")); } } From e42e25d333b5eff16187260e5a325da518de046d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 3 Feb 2021 16:27:55 -0500 Subject: [PATCH 153/366] More article query tests --- lib/REST/Miniflux/V1.php | 20 +++--- tests/cases/REST/Miniflux/TestV1.php | 96 +++++++++++++--------------- 2 files changed, 56 insertions(+), 60 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index b1c16c09..3e8674f3 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -33,6 +33,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected const ACCEPTED_TYPES_OPML = ["application/xml", "text/xml", "text/x-opml"]; protected const ACCEPTED_TYPES_JSON = ["application/json"]; protected const TOKEN_LENGTH = 32; + protected const DATE_FORMAT_SEC = "Y-m-d\TH:i:sP"; + protected const DATE_FORMAT_MICRO = "Y-m-d\TH:i:s.uP"; protected const VALID_QUERY = [ 'status' => V::T_STRING + V::M_ARRAY, 'offset' => V::T_INT, @@ -742,7 +744,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } - protected function transformFeed(array $sub, int $uid, string $rootName): array { + protected function transformFeed(array $sub, int $uid, string $rootName, \DateTimeZone $tz): array { $url = new Uri($sub['url']); return [ 'id' => (int) $sub['id'], @@ -750,8 +752,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'feed_url' => (string) $url->withUserInfo(""), 'site_url' => (string) $sub['source'], 'title' => (string) $sub['title'], - 'checked_at' => Date::transform($sub['updated'], "iso8601m", "sql"), - 'next_check_at' => Date::transform($sub['next_fetch'], "iso8601m", "sql") ?? "0001-01-01T00:00:00.000000Z", + 'checked_at' => Date::normalize($sub['updated'], "sql")->setTimezone($tz)->format(self::DATE_FORMAT_MICRO), + 'next_check_at' => $sub['next_fetch'] ? Date::normalize($sub['next_fetch'], "sql")->setTimezone($tz)->format(self::DATE_FORMAT_MICRO) : "0001-01-01T00:00:00Z", 'etag_header' => (string) $sub['etag'], 'last_modified_header' => (string) Date::transform($sub['edited'], "http", "sql"), 'parsing_error_message' => (string) $sub['err_msg'], @@ -781,7 +783,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $tr = Arsse::$db->begin(); $meta = $this->userMeta(Arsse::$user->id); foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $r) { - $out[] = $this->transformFeed($r, $meta['num'], $meta['root']); + $out[] = $this->transformFeed($r, $meta['num'], $meta['root'], $meta['tz']); } return new Response($out); } @@ -797,7 +799,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { try { $meta = $this->userMeta(Arsse::$user->id); foreach (Arsse::$db->subscriptionList(Arsse::$user->id, $folder, $recursive) as $r) { - $out[] = $this->transformFeed($r, $meta['num'], $meta['root']); + $out[] = $this->transformFeed($r, $meta['num'], $meta['root'], $meta['tz']); } } catch (ExceptionInput $e) { // the folder does not exist @@ -811,7 +813,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $meta = $this->userMeta(Arsse::$user->id); try { $sub = Arsse::$db->subscriptionPropertiesGet(Arsse::$user->id, (int) $path[1]); - return new Response($this->transformFeed($sub, $meta['num'], $meta['root'])); + return new Response($this->transformFeed($sub, $meta['num'], $meta['root'], $meta['tz'])); } catch (ExceptionInput $e) { return new ErrorResponse("404", 404); } @@ -987,8 +989,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'title' => $entry['title'], 'url' => $entry['url'], 'comments_url' => "", - 'published_at' => Date::transform(Date::normalize($entry['published_date'], "sql")->setTimezone($tz), "iso8601"), - 'created_at' => Date::transform(Date::normalize($entry['modified_date'], "sql")->setTimezone($tz), "iso8601m"), + 'published_at' => Date::normalize($entry['published_date'], "sql")->setTimezone($tz)->format(self::DATE_FORMAT_SEC), + 'created_at' => Date::normalize($entry['modified_date'], "sql")->setTimezone($tz)->format(self::DATE_FORMAT_MICRO), 'content' => $entry['content'], 'author' => (string) $entry['author'], 'share_code' => "", @@ -1018,7 +1020,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if ($out) { $feeds = []; foreach (Arsse::$db->subscriptionList(Arsse::$user->id) as $r) { - $feeds[(int) $r['id']] = $this->transformFeed($r, $meta['num'], $meta['root']); + $feeds[(int) $r['id']] = $this->transformFeed($r, $meta['num'], $meta['root'], $meta['tz']); } // add the feed objects to each entry // NOTE: If ever we implement multiple enclosure, this would be the right place to add them diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 899ab9b6..395e080c 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -36,47 +36,20 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ['id' => 55, 'feed' => 12, 'url' => "http://j%20k:super%20secret@example.com/eek", 'title' => "Eek", 'source' => "http://example.com/", 'icon_id' => null, 'icon_url' => null, 'folder' => null, 'top_folder' => null, 'folder_name' => null, 'top_folder_name' => null, 'pinned' => 0, 'err_count' => 0, 'err_msg' => null, 'order_type' => 0, 'keep_rule' => null, 'block_rule' => null, 'added' => "2020-12-21 21:12:00", 'updated' => "2021-01-05 13:51:32", 'edited' => null, 'modified' => "2020-11-30 04:08:52", 'next_fetch' => null, 'etag' => null, 'scrape' => 1, 'unread' => 0], ]; protected const FEEDS_OUT = [ - ['id' => 1, 'user_id' => 42, 'feed_url' => "http://example.com/ook", 'site_url' => "http://example.com/", 'title' => "Ook", 'checked_at' => "2021-01-05T13:51:32.000000Z", 'next_check_at' => "2021-01-20T00:00:00.000000Z", 'etag_header' => "OOKEEK", 'last_modified_header' => "Fri, 01 Jan 2021 00:00:00 GMT", 'parsing_error_message' => "Oopsie", 'parsing_error_count' => 1, 'scraper_rules' => "", 'rewrite_rules' => "", 'crawler' => false, 'blocklist_rules' => "both", 'keeplist_rules' => "this|that", 'user_agent' => "", 'username' => "", 'password' => "", 'disabled' => false, 'ignore_http_cache' => false, 'fetch_via_proxy' => false, 'category' => ['id' => 6, 'title' => "Cat Ook", 'user_id' => 42], 'icon' => ['feed_id' => 1,'icon_id' => 47]], - ['id' => 55, 'user_id' => 42, 'feed_url' => "http://example.com/eek", 'site_url' => "http://example.com/", 'title' => "Eek", 'checked_at' => "2021-01-05T13:51:32.000000Z", 'next_check_at' => "0001-01-01T00:00:00.000000Z", 'etag_header' => "", 'last_modified_header' => "", 'parsing_error_message' => "", 'parsing_error_count' => 0, 'scraper_rules' => "", 'rewrite_rules' => "", 'crawler' => true, 'blocklist_rules' => "", 'keeplist_rules' => "", 'user_agent' => "", 'username' => "j k", 'password' => "super secret", 'disabled' => false, 'ignore_http_cache' => false, 'fetch_via_proxy' => false, 'category' => ['id' => 1,'title' => "All", 'user_id' => 42], 'icon' => null], + ['id' => 1, 'user_id' => 42, 'feed_url' => "http://example.com/ook", 'site_url' => "http://example.com/", 'title' => "Ook", 'checked_at' => "2021-01-05T15:51:32.000000+02:00", 'next_check_at' => "2021-01-20T02:00:00.000000+02:00", 'etag_header' => "OOKEEK", 'last_modified_header' => "Fri, 01 Jan 2021 00:00:00 GMT", 'parsing_error_message' => "Oopsie", 'parsing_error_count' => 1, 'scraper_rules' => "", 'rewrite_rules' => "", 'crawler' => false, 'blocklist_rules' => "both", 'keeplist_rules' => "this|that", 'user_agent' => "", 'username' => "", 'password' => "", 'disabled' => false, 'ignore_http_cache' => false, 'fetch_via_proxy' => false, 'category' => ['id' => 6, 'title' => "Cat Ook", 'user_id' => 42], 'icon' => ['feed_id' => 1,'icon_id' => 47]], + ['id' => 55, 'user_id' => 42, 'feed_url' => "http://example.com/eek", 'site_url' => "http://example.com/", 'title' => "Eek", 'checked_at' => "2021-01-05T15:51:32.000000+02:00", 'next_check_at' => "0001-01-01T00:00:00Z", 'etag_header' => "", 'last_modified_header' => "", 'parsing_error_message' => "", 'parsing_error_count' => 0, 'scraper_rules' => "", 'rewrite_rules' => "", 'crawler' => true, 'blocklist_rules' => "", 'keeplist_rules' => "", 'user_agent' => "", 'username' => "j k", 'password' => "super secret", 'disabled' => false, 'ignore_http_cache' => false, 'fetch_via_proxy' => false, 'category' => ['id' => 1,'title' => "All", 'user_id' => 42], 'icon' => null], ]; protected const ENTRIES = [ - [ - 'id' => 42, - 'url' => "http://example.com/42", - 'title' => "Title 42", - 'subscription' => 2112, - 'author' => "Thomas Costain", - 'fingerprint' => "FINGERPRINT", - 'published_date' => "2021-01-22 02:21:12", - 'modified_date' => "2021-01-22 13:44:47", - 'starred' => 0, - 'unread' => 0, - 'hidden' => 0, - 'content' => "Content 42", - 'media_url' => null, - 'media_type' => null, - ], + ['id' => 42, 'url' => "http://example.com/42", 'title' => "Title 42", 'subscription' => 55, 'author' => "Thomas Costain", 'fingerprint' => "FINGERPRINT", 'published_date' => "2021-01-22 02:21:12", 'modified_date' => "2021-01-22 13:44:47", 'starred' => 0, 'unread' => 0, 'hidden' => 0, 'content' => "Content 42", 'media_url' => null, 'media_type' => null], + ['id' => 44, 'url' => "http://example.com/44", 'title' => "Title 44", 'subscription' => 55, 'author' => null, 'fingerprint' => "FINGERPRINT", 'published_date' => "2021-01-22 02:21:12", 'modified_date' => "2021-01-22 13:44:47", 'starred' => 1, 'unread' => 1, 'hidden' => 0, 'content' => "Content 44", 'media_url' => "http://example.com/44/enclosure", 'media_type' => null], + ['id' => 47, 'url' => "http://example.com/47", 'title' => "Title 47", 'subscription' => 55, 'author' => null, 'fingerprint' => "FINGERPRINT", 'published_date' => "2021-01-22 02:21:12", 'modified_date' => "2021-01-22 13:44:47", 'starred' => 0, 'unread' => 1, 'hidden' => 1, 'content' => "Content 47", 'media_url' => "http://example.com/47/enclosure", 'media_type' => ""], + ['id' => 2112, 'url' => "http://example.com/2112", 'title' => "Title 2112", 'subscription' => 55, 'author' => null, 'fingerprint' => "FINGERPRINT", 'published_date' => "2021-01-22 02:21:12", 'modified_date' => "2021-01-22 13:44:47", 'starred' => 0, 'unread' => 0, 'hidden' => 1, 'content' => "Content 2112", 'media_url' => "http://example.com/2112/enclosure", 'media_type' => "image/png"] ]; protected const ENTRIES_OUT = [ - [ - 'id' => 42, - 'user_id' => 42, - 'feed_id' => 55, - 'status' => "read", - 'hash' => "FINGERPRINT", - 'title' => "Title 42", - 'url' => "http://example.com/42", - 'comments_url' => "", - 'published_at' => "2021-01-22T02:21:12+00:00", - 'created_at' => "2021-01-22T13:44:47.000000+00:00", - 'content' => "Content 42", - 'author' => "Thomas Costain", - 'share_code' => "", - 'starred' => false, - 'reading_time' => 0, - 'enclosures' => null, - 'feed' => self::FEEDS_OUT[1], - ], + ['id' => 42, 'user_id' => 42, 'feed_id' => 55, 'status' => "read", 'hash' => "FINGERPRINT", 'title' => "Title 42", 'url' => "http://example.com/42", 'comments_url' => "", 'published_at' => "2021-01-22T04:21:12+02:00", 'created_at' => "2021-01-22T15:44:47.000000+02:00", 'content' => "Content 42", 'author' => "Thomas Costain", 'share_code' => "", 'starred' => false, 'reading_time' => 0, 'enclosures' => null, 'feed' => self::FEEDS_OUT[1]], + ['id' => 44, 'user_id' => 42, 'feed_id' => 55, 'status' => "unread", 'hash' => "FINGERPRINT", 'title' => "Title 44", 'url' => "http://example.com/44", 'comments_url' => "", 'published_at' => "2021-01-22T04:21:12+02:00", 'created_at' => "2021-01-22T15:44:47.000000+02:00", 'content' => "Content 44", 'author' => "", 'share_code' => "", 'starred' => true, 'reading_time' => 0, 'enclosures' => [['id' => 44, 'user_id' => 42, 'entry_id' => 44, 'url' => "http://example.com/44/enclosure", 'mime_type' => "application/octet-stream", 'size' => 0]], 'feed' => self::FEEDS_OUT[1]], + ['id' => 47, 'user_id' => 42, 'feed_id' => 55, 'status' => "removed", 'hash' => "FINGERPRINT", 'title' => "Title 47", 'url' => "http://example.com/47", 'comments_url' => "", 'published_at' => "2021-01-22T04:21:12+02:00", 'created_at' => "2021-01-22T15:44:47.000000+02:00", 'content' => "Content 47", 'author' => "", 'share_code' => "", 'starred' => false, 'reading_time' => 0, 'enclosures' => [['id' => 47, 'user_id' => 42, 'entry_id' => 47, 'url' => "http://example.com/47/enclosure", 'mime_type' => "application/octet-stream", 'size' => 0]], 'feed' => self::FEEDS_OUT[1]], + ['id' => 2112, 'user_id' => 42, 'feed_id' => 55, 'status' => "removed", 'hash' => "FINGERPRINT", 'title' => "Title 2112", 'url' => "http://example.com/2112", 'comments_url' => "", 'published_at' => "2021-01-22T04:21:12+02:00", 'created_at' => "2021-01-22T15:44:47.000000+02:00", 'content' => "Content 2112", 'author' => "", 'share_code' => "", 'starred' => false, 'reading_time' => 0, 'enclosures' => [['id' => 2112, 'user_id' => 42, 'entry_id' => 2112, 'url' => "http://example.com/2112/enclosure", 'mime_type' => "image/png", 'size' => 0]], 'feed' => self::FEEDS_OUT[1]], ]; protected $h; @@ -104,7 +77,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::when(Arsse::$db)->begin->thenReturn($this->transaction); // create a mock user manager; we use a PHPUnitmock because Phake for reasons unknown is unable to mock the User class correctly, sometimes Arsse::$user = $this->createMock(User::class); - Arsse::$user->method("propertiesGet")->willReturn(['num' => 42, 'admin' => false, 'root_folder_name' => null]); + Arsse::$user->method("propertiesGet")->willReturn(['num' => 42, 'admin' => false, 'root_folder_name' => null, 'tz' => "Asia/Gaza"]); Arsse::$user->method("begin")->willReturn($this->transaction); //initialize a handler $this->h = new V1(); @@ -748,7 +721,8 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideEntryQueries */ - public function testGetEntries(string $url, ?Context $c, ?array $order, $out, bool $getFeeds, ResponseInterface $exp) { + public function testGetEntries(string $url, ?Context $c, ?array $order, $out, ResponseInterface $exp) { + \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v(self::FEEDS))); if ($out instanceof \Exception) { \Phake::when(Arsse::$db)->articleList->thenThrow($out); } else { @@ -760,24 +734,44 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } else { \Phake::verify(Arsse::$db, \Phake::times(0))->articleList; } + if ($out) { + \Phake::verify(Arsse::$db)->subscriptionList(Arsse::$user->id); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->subscriptionList; + } } public function provideEntryQueries(): iterable { self::clearData(); $c = new Context; return [ - ["/entries?after=A", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "after"], 400)], - ["/entries?before=B", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "before"], 400)], - ["/entries?category_id=0", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 400)], - ["/entries?after_entry_id=0", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "after_entry_id"], 400)], - ["/entries?before_entry_id=0", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "before_entry_id"], 400)], - ["/entries?limit=-1", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "limit"], 400)], - ["/entries?offset=-1", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "offset"], 400)], - ["/entries?direction=sideways", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "direction"], 400)], - ["/entries?order=false", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "order"], 400)], - ["/entries?starred&starred", null, null, [], false, new ErrorResponse(["DuplicateInputValue", 'field' => "starred"], 400)], - ["/entries?after&after=0", null, null, [], false, new ErrorResponse(["DuplicateInputValue", 'field' => "after"], 400)], - ["/entries", $c, [], [], false, new Response(['total' => 0, 'entries' => []])], + ["/entries?after=A", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "after"], 400)], + ["/entries?before=B", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "before"], 400)], + ["/entries?category_id=0", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 400)], + ["/entries?after_entry_id=0", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "after_entry_id"], 400)], + ["/entries?before_entry_id=0", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "before_entry_id"], 400)], + ["/entries?limit=-1", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "limit"], 400)], + ["/entries?offset=-1", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "offset"], 400)], + ["/entries?direction=sideways", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "direction"], 400)], + ["/entries?order=false", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "order"], 400)], + ["/entries?starred&starred", null, null, [], new ErrorResponse(["DuplicateInputValue", 'field' => "starred"], 400)], + ["/entries?after&after=0", null, null, [], new ErrorResponse(["DuplicateInputValue", 'field' => "after"], 400)], + ["/entries", $c, [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?category_id=47", (clone $c)->folder(46), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?category_id=1", (clone $c)->folderShallow(0), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=unread", (clone $c)->unread(true)->hidden(false), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=read", (clone $c)->unread(false)->hidden(false), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=removed", (clone $c)->hidden(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=unread&status=read", (clone $c)->hidden(false), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=unread&status=removed", (clone $c)->unread(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=removed&status=read", (clone $c)->unread(false), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=removed&status=read&status=removed", (clone $c)->unread(false), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=removed&status=read&status=unread", $c, [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?starred", (clone $c)->starred(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?starred=", (clone $c)->starred(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?starred=true", (clone $c)->starred(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?starred=false", (clone $c)->starred(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ]; } } From d4a6909cf6773c9f67c982ad3ad12d8ef8d6deb7 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 3 Feb 2021 23:00:14 -0500 Subject: [PATCH 154/366] Positional article queries tests --- tests/cases/REST/Miniflux/TestV1.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 395e080c..d390bad5 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -771,7 +771,10 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ["/entries?starred=", (clone $c)->starred(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], ["/entries?starred=true", (clone $c)->starred(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], ["/entries?starred=false", (clone $c)->starred(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - + ["/entries?after=0", (clone $c)->modifiedSince(0), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?before=0", (clone $c)->notModifiedSince(0), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?after_entry_id=42", (clone $c)->oldestArticle(43), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?before_entry_id=47", (clone $c)->latestArticle(46), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], ]; } } From 00ad1cc5b942dc6191ead7c3df2283df1e571ec5 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 4 Feb 2021 17:07:22 -0500 Subject: [PATCH 155/366] Last tests for article querying --- lib/REST/Miniflux/V1.php | 15 ++--- tests/cases/REST/Miniflux/TestV1.php | 89 +++++++++++++++++----------- 2 files changed, 64 insertions(+), 40 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 3e8674f3..2d427680 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -33,6 +33,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected const ACCEPTED_TYPES_OPML = ["application/xml", "text/xml", "text/x-opml"]; protected const ACCEPTED_TYPES_JSON = ["application/json"]; protected const TOKEN_LENGTH = 32; + protected const DEFAULT_ENTRY_LIMIT = 100; + protected const DEFAULT_ORDER_COL = "modified_date"; protected const DATE_FORMAT_SEC = "Y-m-d\TH:i:sP"; protected const DATE_FORMAT_MICRO = "Y-m-d\TH:i:s.uP"; protected const VALID_QUERY = [ @@ -903,7 +905,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function computeContext(array $query, Context $c = null): Context { $c = ($c ?? new Context) - ->limit($query['limit']) + ->limit($query['limit'] ?? self::DEFAULT_ENTRY_LIMIT) // NOTE: This does not honour user preferences ->offset($query['offset']) ->starred($query['starred']) ->modifiedSince($query['after']) // FIXME: This may not be the correct date field @@ -951,10 +953,10 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return ["modified_date".$desc]; } elseif ($query['order'] === "category_title") { return ["top_folder_name".$desc]; - } elseif ($query['order'] === "catgory_id") { + } elseif ($query['order'] === "category_id") { return ["top_folder".$desc]; } else { - return []; + return [self::DEFAULT_ORDER_COL.$desc]; } } @@ -1028,11 +1030,10 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $out[$a]['feed'] = $feeds[$out[$a]['feed_id']]; } } - // finally compute the total number of entries match the query, if the query hs a limit or offset - if ($c->limit || $c->offset) { + // finally compute the total number of entries match the query, where necessary + $count = sizeof($out); + if ($c->offset || ($c->limit && $count >= $c->limit)) { $count = Arsse::$db->articleCount(Arsse::$user->id, (clone $c)->limit(0)->offset(0)); - } else { - $count = sizeof($out); } return new Response(['total' => $count, 'entries' => $out]); } diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index d390bad5..392a2781 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -721,8 +721,9 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideEntryQueries */ - public function testGetEntries(string $url, ?Context $c, ?array $order, $out, ResponseInterface $exp) { + public function testGetEntries(string $url, ?Context $c, ?array $order, $out, bool $count, ResponseInterface $exp) { \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v(self::FEEDS))); + \Phake::when(Arsse::$db)->articleCount->thenReturn(2112); if ($out instanceof \Exception) { \Phake::when(Arsse::$db)->articleList->thenThrow($out); } else { @@ -734,47 +735,69 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } else { \Phake::verify(Arsse::$db, \Phake::times(0))->articleList; } - if ($out) { + if ($out && !$out instanceof \Exception) { \Phake::verify(Arsse::$db)->subscriptionList(Arsse::$user->id); } else { \Phake::verify(Arsse::$db, \Phake::times(0))->subscriptionList; } + if ($count) { + \Phake::verify(Arsse::$db)->articleCount(Arsse::$user->id, (clone $c)->limit(0)->offset(0)); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->articleCount; + } } public function provideEntryQueries(): iterable { self::clearData(); - $c = new Context; + $c = (new Context)->limit(100); + $o = ["modified_date"]; // the default sort order return [ - ["/entries?after=A", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "after"], 400)], - ["/entries?before=B", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "before"], 400)], - ["/entries?category_id=0", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 400)], - ["/entries?after_entry_id=0", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "after_entry_id"], 400)], - ["/entries?before_entry_id=0", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "before_entry_id"], 400)], - ["/entries?limit=-1", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "limit"], 400)], - ["/entries?offset=-1", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "offset"], 400)], - ["/entries?direction=sideways", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "direction"], 400)], - ["/entries?order=false", null, null, [], new ErrorResponse(["InvalidInputValue", 'field' => "order"], 400)], - ["/entries?starred&starred", null, null, [], new ErrorResponse(["DuplicateInputValue", 'field' => "starred"], 400)], - ["/entries?after&after=0", null, null, [], new ErrorResponse(["DuplicateInputValue", 'field' => "after"], 400)], - ["/entries", $c, [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?category_id=47", (clone $c)->folder(46), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?category_id=1", (clone $c)->folderShallow(0), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?status=unread", (clone $c)->unread(true)->hidden(false), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?status=read", (clone $c)->unread(false)->hidden(false), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?status=removed", (clone $c)->hidden(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?status=unread&status=read", (clone $c)->hidden(false), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?status=unread&status=removed", (clone $c)->unread(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?status=removed&status=read", (clone $c)->unread(false), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?status=removed&status=read&status=removed", (clone $c)->unread(false), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?status=removed&status=read&status=unread", $c, [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?starred", (clone $c)->starred(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?starred=", (clone $c)->starred(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?starred=true", (clone $c)->starred(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?starred=false", (clone $c)->starred(true), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?after=0", (clone $c)->modifiedSince(0), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?before=0", (clone $c)->notModifiedSince(0), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?after_entry_id=42", (clone $c)->oldestArticle(43), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?before_entry_id=47", (clone $c)->latestArticle(46), [], self::ENTRIES, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?after=A", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "after"], 400)], + ["/entries?before=B", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "before"], 400)], + ["/entries?category_id=0", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "category_id"], 400)], + ["/entries?after_entry_id=0", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "after_entry_id"], 400)], + ["/entries?before_entry_id=0", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "before_entry_id"], 400)], + ["/entries?limit=-1", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "limit"], 400)], + ["/entries?offset=-1", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "offset"], 400)], + ["/entries?direction=sideways", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "direction"], 400)], + ["/entries?order=false", null, null, [], false, new ErrorResponse(["InvalidInputValue", 'field' => "order"], 400)], + ["/entries?starred&starred", null, null, [], false, new ErrorResponse(["DuplicateInputValue", 'field' => "starred"], 400)], + ["/entries?after&after=0", null, null, [], false, new ErrorResponse(["DuplicateInputValue", 'field' => "after"], 400)], + ["/entries", $c, $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?category_id=47", (clone $c)->folder(46), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?category_id=1", (clone $c)->folderShallow(0), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=unread", (clone $c)->unread(true)->hidden(false), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=read", (clone $c)->unread(false)->hidden(false), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=removed", (clone $c)->hidden(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=unread&status=read", (clone $c)->hidden(false), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=unread&status=removed", (clone $c)->unread(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=removed&status=read", (clone $c)->unread(false), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=removed&status=read&status=removed", (clone $c)->unread(false), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?status=removed&status=read&status=unread", $c, $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?starred", (clone $c)->starred(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?starred=", (clone $c)->starred(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?starred=true", (clone $c)->starred(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?starred=false", (clone $c)->starred(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?after=0", (clone $c)->modifiedSince(0), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?before=0", (clone $c)->notModifiedSince(0), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?after_entry_id=42", (clone $c)->oldestArticle(43), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?before_entry_id=47", (clone $c)->latestArticle(46), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?search=alpha%20beta", (clone $c)->searchTerms(["alpha", "beta"]), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?limit=4", (clone $c)->limit(4), $o, self::ENTRIES, true, new Response(['total' => 2112, 'entries' => self::ENTRIES_OUT])], + ["/entries?offset=20", (clone $c)->offset(20), $o, [], true, new Response(['total' => 2112, 'entries' => []])], + ["/entries?direction=asc", $c, $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?order=id", $c, ["id"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?order=published_at", $c, ["modified_date"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?order=category_id", $c, ["top_folder"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?order=category_title", $c, ["top_folder_name"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?order=status", $c, ["hidden", "unread desc"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?direction=desc", $c, ["modified_date desc"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?order=id&direction=desc", $c, ["id desc"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?order=published_at&direction=desc", $c, ["modified_date desc"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?order=category_id&direction=desc", $c, ["top_folder desc"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?order=category_title&direction=desc", $c, ["top_folder_name desc"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?order=status&direction=desc", $c, ["hidden desc", "unread"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?category_id=2112", (clone $c)->folder(2111), $o, new ExceptionInput("idMissing"), false, new ErrorResponse("MissingCategory")], ]; } } From a7d05a77173c87a65c8ff28e5f005ab5d960037f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 4 Feb 2021 17:52:40 -0500 Subject: [PATCH 156/366] Feed- and category-specific entry list routes --- lib/REST/Miniflux/V1.php | 40 +++++++++++++++++++++------- tests/cases/REST/Miniflux/TestV1.php | 8 ++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 2d427680..be093d48 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -1003,19 +1003,14 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { ]; } - protected function getEntries(array $query): ResponseInterface { - $c = $this->computeContext($query); + protected function listEntries(array $query, Context $c): array { + $c = $this->computeContext($query, $c); $order = $this->computeOrder($query); $tr = Arsse::$db->begin(); $meta = $this->userMeta(Arsse::$user->id); // compile the list of entries - try { - $entries = Arsse::$db->articleList(Arsse::$user->id, $c, self::ARTICLE_COLUMNS, $order); - } catch (ExceptionInput $e) { - return new ErrorResponse("MissingCategory", 400); - } $out = []; - foreach ($entries as $entry) { + foreach (Arsse::$db->articleList(Arsse::$user->id, $c, self::ARTICLE_COLUMNS, $order) as $entry) { $out[] = $this->transformEntry($entry, $meta['num'], $meta['tz']); } // next compile a map of feeds to add to the entries @@ -1035,7 +1030,34 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if ($c->offset || ($c->limit && $count >= $c->limit)) { $count = Arsse::$db->articleCount(Arsse::$user->id, (clone $c)->limit(0)->offset(0)); } - return new Response(['total' => $count, 'entries' => $out]); + return ['total' => $count, 'entries' => $out]; + } + + protected function getEntries(array $query): ResponseInterface { + try { + return new Response($this->listEntries($query, new Context)); + } catch (ExceptionInput $e) { + return new ErrorResponse("MissingCategory", 400); + } + } + + protected function getFeedEntries(array $path, array $query): ResponseInterface { + $c = (new Context)->subscription((int) $path[1]); + try { + return new Response($this->listEntries($query, $c)); + } catch (ExceptionInput $e) { + // FIXME: this should differentiate between a missing feed and a missing category, but doesn't + return new ErrorResponse("404", 404); + } + } + + protected function getCategoryEntries(array $path, array $query): ResponseInterface { + $query['category_id'] = (int) $path[1]; + try { + return new Response($this->listEntries($query, new Context)); + } catch (ExceptionInput $e) { + return new ErrorResponse("404", 404); + } } public static function tokenGenerate(string $user, string $label): string { diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 392a2781..02387dc1 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -798,6 +798,14 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ["/entries?order=category_title&direction=desc", $c, ["top_folder_name desc"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], ["/entries?order=status&direction=desc", $c, ["hidden desc", "unread"], self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], ["/entries?category_id=2112", (clone $c)->folder(2111), $o, new ExceptionInput("idMissing"), false, new ErrorResponse("MissingCategory")], + ["/feeds/42/entries", (clone $c)->subscription(42), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/feeds/42/entries?category_id=47", (clone $c)->subscription(42)->folder(46), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/feeds/2112/entries", (clone $c)->subscription(2112), $o, new ExceptionInput("idMissing"), false, new ErrorResponse("404", 404)], + ["/categories/42/entries", (clone $c)->folder(41), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/categories/42/entries?category_id=47", (clone $c)->folder(41), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/categories/42/entries?starred", (clone $c)->folder(41)->starred(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/categories/1/entries", (clone $c)->folderShallow(0), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/categories/2112/entries", (clone $c)->folder(2111), $o, new ExceptionInput("idMissing"), false, new ErrorResponse("404", 404)], ]; } } From 334a585cb89d79462ad166a510562bcf615b2de9 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 4 Feb 2021 20:19:35 -0500 Subject: [PATCH 157/366] Implement single-entry querying --- lib/REST/Miniflux/V1.php | 46 ++++++++++++++++++++++++++++ tests/cases/REST/Miniflux/TestV1.php | 40 +++++++++++++++++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index be093d48..47decbfa 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -1032,6 +1032,21 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } return ['total' => $count, 'entries' => $out]; } + + protected function findEntry(int $id, Context $c = null): array { + $c = ($c ?? new Context)->article($id); + $tr = Arsse::$db->begin(); + $meta = $this->userMeta(Arsse::$user->id); + // find the entry we want + $entry = Arsse::$db->articleList(Arsse::$user->id, $c, self::ARTICLE_COLUMNS)->getRow(); + if (!$entry) { + throw new ExceptionInput("idMissing"); + } + $out = $this->transformEntry($entry, $meta['num'], $meta['tz']); + // next transform the parent feed of the entry + $out['feed'] = $this->transformFeed(Arsse::$db->subscriptionPropertiesGet(Arsse::$user->id, $out['feed_id']), $meta['num'], $meta['root'], $meta['tz']); + return $out; + } protected function getEntries(array $query): ResponseInterface { try { @@ -1059,6 +1074,37 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("404", 404); } } + + protected function getEntry(array $path): ResponseInterface { + try { + return new Response($this->findEntry((int) $path[1])); + } catch (ExceptionInput $e) { + return new ErrorResponse("404", 404); + } + } + + protected function getFeedEntry(array $path): ResponseInterface { + $c = (new Context)->subscription((int) $path[1]); + try { + return new Response($this->findEntry((int) $path[3], $c)); + } catch (ExceptionInput $e) { + return new ErrorResponse("404", 404); + } + } + + protected function getCategoryEntry(array $path): ResponseInterface { + $c = new Context; + if ($path[1] === "1") { + $c->folderShallow(0); + } else { + $c->folder((int) $path[1] - 1); + } + try { + return new Response($this->findEntry((int) $path[3], $c)); + } catch (ExceptionInput $e) { + return new ErrorResponse("404", 404); + } + } public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokenss in base64url alphabet diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 02387dc1..a2c6aa82 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -721,7 +721,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideEntryQueries */ - public function testGetEntries(string $url, ?Context $c, ?array $order, $out, bool $count, ResponseInterface $exp) { + public function testGetEntries(string $url, ?Context $c, ?array $order, $out, bool $count, ResponseInterface $exp): void { \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v(self::FEEDS))); \Phake::when(Arsse::$db)->articleCount->thenReturn(2112); if ($out instanceof \Exception) { @@ -808,4 +808,42 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ["/categories/2112/entries", (clone $c)->folder(2111), $o, new ExceptionInput("idMissing"), false, new ErrorResponse("404", 404)], ]; } + + /** @dataProvider provideSingleEntryQueries */ + public function testGetASingleEntry(string $url, Context $c, $out, ResponseInterface $exp): void { + \Phake::when(Arsse::$db)->subscriptionPropertiesGet->thenReturn($this->v(self::FEEDS[1])); + if ($out instanceof \Exception) { + \Phake::when(Arsse::$db)->articleList->thenThrow($out); + } else { + \Phake::when(Arsse::$db)->articleList->thenReturn(new Result($this->v($out))); + } + $this->assertMessage($exp, $this->req("GET", $url)); + if ($c) { + \Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, $c, array_keys(self::ENTRIES[0])); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->articleList; + } + if ($out && is_array($out)) { + \Phake::verify(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 55); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->subscriptionList; + } + } + + public function provideSingleEntryQueries(): iterable { + $c = new Context; + return [ + ["/entries/42", (clone $c)->article(42), [self::ENTRIES[1]], new Response(self::ENTRIES_OUT[1])], + ["/entries/2112", (clone $c)->article(2112), new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)], + ["/feeds/47/entries/42", (clone $c)->subscription(47)->article(42), [self::ENTRIES[1]], new Response(self::ENTRIES_OUT[1])], + ["/feeds/47/entries/44", (clone $c)->subscription(47)->article(44), [], new ErrorResponse("404", 404)], + ["/feeds/47/entries/2112", (clone $c)->subscription(47)->article(2112), new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)], + ["/feeds/2112/entries/47", (clone $c)->subscription(2112)->article(47), new ExceptionInput("idMissing"), new ErrorResponse("404", 404)], + ["/categories/47/entries/42", (clone $c)->folder(46)->article(42), [self::ENTRIES[1]], new Response(self::ENTRIES_OUT[1])], + ["/categories/47/entries/44", (clone $c)->folder(46)->article(44), [], new ErrorResponse("404", 404)], + ["/categories/47/entries/2112", (clone $c)->folder(46)->article(2112), new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)], + ["/categories/2112/entries/47", (clone $c)->folder(2111)->article(47), new ExceptionInput("idMissing"), new ErrorResponse("404", 404)], + ["/categories/1/entries/42", (clone $c)->folderShallow(0)->article(42), [self::ENTRIES[1]], new Response(self::ENTRIES_OUT[1])], + ]; + } } From ab1cf7447bbfed3404e8f85bfec521c608673754 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 5 Feb 2021 08:48:14 -0500 Subject: [PATCH 158/366] Implement article marking --- lib/REST/Miniflux/V1.php | 113 ++++++++++++++++++++------- tests/cases/REST/Miniflux/TestV1.php | 113 ++++++++++++++++++++++----- 2 files changed, 178 insertions(+), 48 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 47decbfa..51884ea4 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -71,6 +71,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'disabled' => "boolean", 'ignore_http_cache' => "boolean", 'fetch_via_proxy' => "boolean", + 'entry_ids' => "array", // this is a special case: it is an array of integers + 'status' => "string", ]; protected const USER_META_MAP = [ // Miniflux ID // Arsse ID Default value @@ -146,7 +148,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { ], '/entries' => [ 'GET' => ["getEntries", false, false, false, true, []], - 'PUT' => ["updateEntries", false, false, true, false, []], + 'PUT' => ["updateEntries", false, false, true, false, ["entry_ids", "status"]], ], '/entries/1' => [ 'GET' => ["getEntry", false, true, false, false, []], @@ -349,8 +351,17 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { (in_array($k, ["keeplist_rules", "blocklist_rules"]) && !Rule::validate($body[$k])) || (in_array($k, ["url", "feed_url"]) && !URL::absolute($body[$k])) || ($k === "category_id" && $body[$k] < 1) + || ($k === "status" && !in_array($body[$k], ["read", "unread", "removed"])) ) { return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422); + } elseif ($k === "entry_ids") { + foreach ($body[$k] as $v) { + if (gettype($v) !== "integer") { + return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => "integer", 'actual' => gettype($v)], 422); + } elseif ($v < 1) { + return new ErrorResponse(["InvalidInputValue", 'field' => $k], 422); + } + } } } //normalize user-specific input @@ -368,7 +379,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } // check for any missing required values foreach ($req as $k) { - if (!isset($body[$k])) { + if (!isset($body[$k]) || (is_array($body[$k]) && !$body[$k])) { return new ErrorResponse(["MissingInputValue", 'field' => $k], 422); } } @@ -629,16 +640,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } - protected function markUserByNum(array $path): ResponseInterface { - // this function is restricted to the logged-in user - $user = Arsse::$user->propertiesGet(Arsse::$user->id, false); - if (((int) $path[1]) !== $user['num']) { - return new ErrorResponse("403", 403); - } - Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], (new Context)->hidden(false)); - return new EmptyResponse(204); - } - /** Returns a useful subset of user metadata * * The following keys are included: @@ -729,23 +730,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } - protected function markCategory(array $path): ResponseInterface { - $folder = $path[1] - 1; - $c = new Context; - if ($folder === 0) { - // if we're marking the root folder don't also mark its child folders, since Miniflux organizes it as a peer of other folders - $c = $c->folderShallow($folder); - } else { - $c = $c->folder($folder); - } - try { - Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c); - } catch (ExceptionInput $e) { - return new ErrorResponse("404", 404); - } - return new EmptyResponse(204); - } - protected function transformFeed(array $sub, int $uid, string $rootName, \DateTimeZone $tz): array { $url = new Uri($sub['url']); return [ @@ -1106,6 +1090,77 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } + protected function updateEntries(array $data): ResponseInterface { + if ($data['status'] === "read") { + $in = ['read' => true, 'hidden' => false]; + } elseif ($data['status'] === "unread") { + $in = ['read' => false, 'hidden' => false]; + } elseif ($data['status'] === "removed") { + $in = ['read' => true, 'hidden' => true]; + } + assert(isset($in), new \Exception("Unknown status specified")); + Arsse::$db->articleMark(Arsse::$user->id, $in, (new Context)->articles($data['entry_ids'])); + return new EmptyResponse(204); + } + + protected function massRead(Context $c): void { + Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c->hidden(false)); + } + + protected function markUserByNum(array $path): ResponseInterface { + // this function is restricted to the logged-in user + $user = Arsse::$user->propertiesGet(Arsse::$user->id, false); + if (((int) $path[1]) !== $user['num']) { + return new ErrorResponse("403", 403); + } + $this->massRead(new Context); + return new EmptyResponse(204); + } + + protected function markFeed(array $path): ResponseInterface { + try { + $this->massRead((new Context)->subscription((int) $path[1])); + } catch (ExceptionInput $e) { + return new ErrorResponse("404", 404); + } + return new EmptyResponse(204); + } + + protected function markCategory(array $path): ResponseInterface { + $folder = $path[1] - 1; + $c = new Context; + if ($folder === 0) { + // if we're marking the root folder don't also mark its child folders, since Miniflux organizes it as a peer of other folders + $c->folderShallow($folder); + } else { + $c->folder($folder); + } + try { + $this->massRead($c); + } catch (ExceptionInput $e) { + return new ErrorResponse("404", 404); + } + return new EmptyResponse(204); + } + + protected function toggleEntryBookmark(array $path): ResponseInterface { + // NOTE: A toggle is bad design, but we have no choice but to implement what Miniflux does + $id = (int) $path[1]; + $c = (new Context)->article($id); + try { + $tr = Arsse::$db->begin(); + if (Arsse::$db->articleCount(Arsse::$user->id, (clone $c)->starred(false))) { + Arsse::$db->articleMark(Arsse::$user->id, ['starred' => true], $c); + } else { + Arsse::$db->articleMark(Arsse::$user->id, ['starred' => false], $c); + } + $tr->commit(); + } catch (ExceptionInput $e) { + return new ErrorResponse("404", 404); + } + return new EmptyResponse(204); + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokenss in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index a2c6aa82..18982f34 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -398,13 +398,6 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertMessage(new ErrorResponse("403", 403), $this->req("DELETE", "/users/2112")); } - public function testMarkAllArticlesAsRead(): void { - \Phake::when(Arsse::$db)->articleMark->thenReturn(true); - $this->assertMessage(new ErrorResponse("403", 403), $this->req("PUT", "/users/1/mark-all-as-read")); - $this->assertMessage(new EmptyResponse(204), $this->req("PUT", "/users/42/mark-all-as-read")); - \Phake::verify(Arsse::$db)->articleMark("john.doe@example.com", ['read' => true], (new Context)->hidden(false)); - } - public function testListCategories(): void { \Phake::when(Arsse::$db)->folderList->thenReturn(new Result($this->v([ ['id' => 1, 'name' => "Science"], @@ -512,18 +505,6 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ); } - public function testMarkACategoryAsRead(): void { - \Phake::when(Arsse::$db)->articleMark->thenReturn(1)->thenReturn(1)->thenThrow(new ExceptionInput("idMissing")); - $this->assertMessage(new EmptyResponse(204), $this->req("PUT", "/categories/2/mark-all-as-read")); - $this->assertMessage(new EmptyResponse(204), $this->req("PUT", "/categories/1/mark-all-as-read")); - $this->assertMessage(new ErrorResponse("404", 404), $this->req("PUT", "/categories/2112/mark-all-as-read")); - \Phake::inOrder( - \Phake::verify(Arsse::$db)->articleMark("john.doe@example.com", ['read' => true], (new Context)->folder(1)), - \Phake::verify(Arsse::$db)->articleMark("john.doe@example.com", ['read' => true], (new Context)->folderShallow(0)), - \Phake::verify(Arsse::$db)->articleMark("john.doe@example.com", ['read' => true], (new Context)->folder(2111)) - ); - } - public function testListFeeds(): void { \Phake::when(Arsse::$db)->subscriptionList->thenReturn(new Result($this->v(self::FEEDS))); $exp = new Response(self::FEEDS_OUT); @@ -831,6 +812,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { } public function provideSingleEntryQueries(): iterable { + self::clearData(); $c = new Context; return [ ["/entries/42", (clone $c)->article(42), [self::ENTRIES[1]], new Response(self::ENTRIES_OUT[1])], @@ -846,4 +828,97 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ["/categories/1/entries/42", (clone $c)->folderShallow(0)->article(42), [self::ENTRIES[1]], new Response(self::ENTRIES_OUT[1])], ]; } + + /** @dataProvider provideEntryMarkings */ + public function testMarkEntries(array $in, ?array $data, ResponseInterface $exp): void { + \Phake::when(Arsse::$db)->articleMark->thenReturn(0); + $this->assertMessage($exp, $this->req("PUT", "/entries", $in)); + if ($data) { + \Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, $data, (new Context)->articles($in['entry_ids'])); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->articleMark; + } + } + + public function provideEntryMarkings(): iterable { + self::clearData(); + return [ + [['status' => "read"], null, new ErrorResponse(["MissingInputValue", 'field' => "entry_ids"], 422)], + [['entry_ids' => [1]], null, new ErrorResponse(["MissingInputValue", 'field' => "status"], 422)], + [['entry_ids' => [], 'status' => "read"], null, new ErrorResponse(["MissingInputValue", 'field' => "entry_ids"], 422)], + [['entry_ids' => 1, 'status' => "read"], null, new ErrorResponse(["InvalidInputType", 'field' => "entry_ids", 'expected' => "array", 'actual' => "integer"], 422)], + [['entry_ids' => ["1"], 'status' => "read"], null, new ErrorResponse(["InvalidInputType", 'field' => "entry_ids", 'expected' => "integer", 'actual' => "string"], 422)], + [['entry_ids' => [1], 'status' => 1], null, new ErrorResponse(["InvalidInputType", 'field' => "status", 'expected' => "string", 'actual' => "integer"], 422)], + [['entry_ids' => [0], 'status' => "read"], null, new ErrorResponse(["InvalidInputValue", 'field' => "entry_ids",], 422)], + [['entry_ids' => [1], 'status' => "reread"], null, new ErrorResponse(["InvalidInputValue", 'field' => "status",], 422)], + [['entry_ids' => [1, 2], 'status' => "read"], ['read' => true, 'hidden' => false], new EmptyResponse(204)], + [['entry_ids' => [1, 2], 'status' => "unread"], ['read' => false, 'hidden' => false], new EmptyResponse(204)], + [['entry_ids' => [1, 2], 'status' => "removed"], ['read' => true, 'hidden' => true], new EmptyResponse(204)], + ]; + } + + /** @dataProvider provideMassMarkings */ + public function testMassMarkEntries(string $url, Context $c, $out, ResponseInterface $exp): void { + if ($out instanceof \Exception) { + \Phake::when(Arsse::$db)->articleMark->thenThrow($out); + } else { + \Phake::when(Arsse::$db)->articleMark->thenReturn($out); + } + $this->assertMessage($exp, $this->req("PUT", $url)); + if ($out !== null) { + \Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, ['read' => true], $c); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->articleMark; + } + } + + public function provideMassMarkings(): iterable { + self::clearData(); + $c = (new Context)->hidden(false); + return [ + ["/users/42/mark-all-as-read", $c, 1123, new EmptyResponse(204)], + ["/users/2112/mark-all-as-read", $c, null, new ErrorResponse("403", 403)], + ["/feeds/47/mark-all-as-read", (clone $c)->subscription(47), 2112, new EmptyResponse(204)], + ["/feeds/2112/mark-all-as-read", (clone $c)->subscription(2112), new ExceptionInput("idMissing"), new ErrorResponse("404", 404)], + ["/categories/47/mark-all-as-read", (clone $c)->folder(46), 1337, new EmptyResponse(204)], + ["/categories/2112/mark-all-as-read", (clone $c)->folder(2111), new ExceptionInput("idMissing"), new ErrorResponse("404", 404)], + ["/categories/1/mark-all-as-read", (clone $c)->folderShallow(0), 6666, new EmptyResponse(204)], + ]; + } + + /** @dataProvider provideBookmarkTogglings */ + public function testToggleABookmark($before, ?bool $after, ResponseInterface $exp): void { + $c = (new Context)->article(2112); + \Phake::when(Arsse::$db)->articleMark->thenReturn(1); + if ($before instanceof \Exception) { + \Phake::when(Arsse::$db)->articleCount->thenThrow($before); + } else { + \Phake::when(Arsse::$db)->articleCount->thenReturn($before); + } + $this->assertMessage($exp, $this->req("PUT", "/entries/2112/bookmark")); + if ($after !== null) { + \Phake::inOrder( + \Phake::verify(Arsse::$db)->begin(), + \Phake::verify(Arsse::$db)->articleCount(Arsse::$user->id, (clone $c)->starred(false)), + \Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, ['starred' => $after], $c), + \Phake::verify($this->transaction)->commit() + ); + } else { + \Phake::inOrder( + \Phake::verify(Arsse::$db)->begin(), + \Phake::verify(Arsse::$db)->articleCount(Arsse::$user->id, (clone $c)->starred(false)) + ); + \Phake::verify(Arsse::$db, \Phake::times(0))->articleMark; + \Phake::verifyNoInteraction($this->transaction); + } + } + + public function provideBookmarkTogglings(): iterable { + self::clearData(); + return [ + [1, true, new EmptyResponse(204)], + [0, false, new EmptyResponse(204)], + [new ExceptionInput("subjectMissing"), null, new ErrorResponse("404", 404)], + ]; + } } From dd29ef6c1bd9e026e4e7018b838b1574f6b54e96 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 5 Feb 2021 09:04:00 -0500 Subject: [PATCH 159/366] Add feed refreshing stubs --- lib/REST/Miniflux/V1.php | 16 ++++++++++++++++ tests/cases/REST/Miniflux/TestV1.php | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 51884ea4..2198ebb6 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -1161,6 +1161,22 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } + protected function refreshFeed(array $path): ResponseInterface { + // NOTE: This is a no-op; we simply check that the feed exists + try { + Arsse::$db->subscriptionPropertiesGet(Arsse::$user->id, (int) $path[1]); + } catch (ExceptionInput $e) { + return new ErrorResponse("404", 404); + } + return new EmptyResponse(204); + } + + protected function refreshAllFeeds(): ResponseInterface { + // NOTE: This is a no-op + // It could be implemented, but the need is considered low since we use a dynamic schedule always + return new EmptyResponse(204); + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokenss in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 18982f34..22a53db6 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -921,4 +921,20 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { [new ExceptionInput("subjectMissing"), null, new ErrorResponse("404", 404)], ]; } + + public function testRefreshAFeed(): void { + \Phake::when(Arsse::$db)->subscriptionPropertiesGet->thenReturn([]); + $this->assertMessage(new EmptyResponse(204), $this->req("PUT", "/feeds/47/refresh")); + \Phake::verify(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 47); + } + + public function testRefreshAMissingFeed(): void { + \Phake::when(Arsse::$db)->subscriptionPropertiesGet->thenThrow(new ExceptionInput("subjectMissing")); + $this->assertMessage(new ErrorResponse("404", 404), $this->req("PUT", "/feeds/2112/refresh")); + \Phake::verify(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 2112); + } + + public function testRefreshAllFeeds(): void { + $this->assertMessage(new EmptyResponse(204), $this->req("PUT", "/feeds/refresh")); + } } From 681654f24938b29460ce5d72ddac98751d9141cc Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 5 Feb 2021 09:22:10 -0500 Subject: [PATCH 160/366] Documentation update --- docs/en/030_Supported_Protocols/005_Miniflux.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index 62a27bd3..1bc47dde 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -13,9 +13,9 @@
API Reference, Filtering Rules
-The Miniflux protocol is a fairly well-designed protocol supporting a wide variety of operations on newsfeeds, folders (termed "categories"), and articles; it also allows for user administration, and native OPML importing and exporting. Architecturally it is similar to the Nextcloud News protocol, but is generally more efficient and has more capabilities. +The Miniflux protocol is a fairly well-designed protocol supporting a wide variety of operations on newsfeeds, folders (termed "categories"), and articles; it also allows for user administration, and native OPML importing and exporting. Architecturally it is similar to the Nextcloud News protocol, but has more capabilities. -Miniflux version 2.0.27 is emulated, though not all features are implemented +Miniflux version 2.0.28 is emulated, though not all features are implemented # Missing features @@ -25,6 +25,7 @@ Miniflux version 2.0.27 is emulated, though not all features are implemented - Custom User-Agent strings - The `disabled`, `ignore_http_cache`, and `fetch_via_proxy` flags - Changing the URL, username, or password of a feed + - Manually refreshing feeds - Titles and types are not available during feed discovery and are filled with generic data - Reading time is not calculated and will always be zero - Only the first enclosure of an article is retained From b4ae988b790513672dba1200e7bff7faed2e62d1 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 5 Feb 2021 20:29:41 -0500 Subject: [PATCH 161/366] Prototype OPML handling --- .../030_Supported_Protocols/005_Miniflux.md | 1 + lib/AbstractException.php | 13 ++++++ lib/REST/Miniflux/V1.php | 40 ++++++++++++++++--- locale/en.php | 7 ++++ 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index 1bc47dde..2e6c23b1 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -42,6 +42,7 @@ Miniflux version 2.0.28 is emulated, though not all features are implemented - Creating a feed with the `scrape` property set to `true` might not return scraped content for the initial synchronization - Querying articles for both read/unread and removed statuses will not return all removed articles - Search strings will match partial words +- OPML import either succeeds or fails atomically: if one feed fails, no feeds are imported # Behaviour of filtering (block and keep) rules diff --git a/lib/AbstractException.php b/lib/AbstractException.php index b6696c92..922b9cd8 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -104,7 +104,12 @@ abstract class AbstractException extends \Exception { "Rule/Exception.invalidPattern" => 10701, ]; + protected $symbol; + protected $params; + public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) { + $this->symbol = $msgID; + $this->params = $vars ?? []; if ($msgID === "") { $msg = "Exception.unknown"; $code = 10000; @@ -121,4 +126,12 @@ abstract class AbstractException extends \Exception { } parent::__construct($msg, $code, $e); } + + public function getSymbol(): string { + return $this->symbol; + } + + public function getParams(): array { + return $this->aparams; + } } diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 2198ebb6..00c58f02 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -13,7 +13,8 @@ use JKingWeb\Arsse\Feed\Exception as FeedException; use JKingWeb\Arsse\AbstractException; use JKingWeb\Arsse\Context\Context; use JKingWeb\Arsse\Db\ExceptionInput; -use JKingWeb\Arsse\Misc\HTTP; +use JKingWeb\Arsse\ImportExport\OPML; +use JKingWeb\Arsse\ImportExport\Exception as ImportException; use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\URL; use JKingWeb\Arsse\Misc\ValueInfo as V; @@ -25,6 +26,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\JsonResponse as Response; +use Laminas\Diactoros\Response\TextResponse as GenericResponse; use Laminas\Diactoros\Uri; class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { @@ -141,8 +143,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'GET' => ["getCategoryFeeds", false, true, false, false, []], ], '/categories/1/mark-all-as-read' => [ - 'PUT' => ["markCategory", false, true, false, false, []], ], + 'PUT' => ["markCategory", false, true, false, false, []], '/discover' => [ 'POST' => ["discoverSubscriptions", false, false, true, false, ["url"]], ], @@ -212,6 +214,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { public function __construct() { } + /** @codeCoverageIgnore */ + protected function getInstance(string $class) { + return new $class; + } + protected function authenticate(ServerRequestInterface $req): bool { // first check any tokens; this is what Miniflux does if ($req->hasHeader("X-Auth-Token")) { @@ -261,9 +268,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } if ($reqBody) { if ($func === "opmlImport") { - if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) { - return new ErrorResponse("", 415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]); - } $args[] = (string) $req->getBody(); } else { $data = (string) $req->getBody(); @@ -1177,6 +1181,32 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new EmptyResponse(204); } + protected function opmlImport(string $data): ResponseInterface { + try { + $this->getInstance(OPML::class)->import(Arsse::$user->id, $data); + } catch (ImportException $e) { + switch ($e->getCode()) { + case 10611: + return new ErrorResponse("InvalidBodyXML", 400); + case 10612: + return new ErrorResponse("InvalidBodyOPML", 422); + case 10613: + return new ErrorResponse("InvalidImportCategory", 422); + case 10614: + return new ErrorResponse("DuplicateImportCatgory", 422); + case 10615: + return new ErrorResponse("InvalidImportLabel", 422); + } + } catch (FeedException $e) { + return new ErrorResponse(["FailedImportFeed", 'url' => $e->getParams()['url'], 'code' => $e->getCode()], 502); + } + return new Response(['message' => Arsse::$lang->msg("ImportSuccess")]); + } + + protected function opmlExport(): ResponseInterface { + return new GenericResponse($this->getInstance(OPML::class)->export(Arsse::$user->id), 200, ['Content-Type' => "application/xml"]); + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokenss in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); diff --git a/locale/en.php b/locale/en.php index 03b0579f..470d09ea 100644 --- a/locale/en.php +++ b/locale/en.php @@ -8,12 +8,15 @@ return [ 'CLI.Auth.Failure' => 'Authentication failed', 'API.Miniflux.DefaultCategoryName' => "All", + 'API.Miniflux.ImportSuccess' => 'Feeds imported successfully', 'API.Miniflux.Error.401' => 'Access Unauthorized', 'API.Miniflux.Error.403' => 'Access Forbidden', 'API.Miniflux.Error.404' => 'Resource Not Found', 'API.Miniflux.Error.MissingInputValue' => 'Required key "{field}" was not present in input', 'API.Miniflux.Error.DuplicateInputValue' => 'Key "{field}" accepts only one value', 'API.Miniflux.Error.InvalidBodyJSON' => 'Invalid JSON payload: {0}', + 'API.Miniflux.Error.InvalidBodyXML' => 'Invalid XML payload', + 'API.Miniflux.Error.InvalidBodyOPML' => 'Payload is not a valid OPML document', 'API.Miniflux.Error.InvalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}', 'API.Miniflux.Error.InvalidInputValue' => 'Supplied value is not valid for input key "{field}"', 'API.Miniflux.Error.Fetch404' => 'Resource not found (404), this feed doesn\'t exists anymore, check the feed URL', @@ -28,6 +31,10 @@ return [ 'API.Miniflux.Error.DuplicateUser' => 'The user name "{user}" already exists', 'API.Miniflux.Error.DuplicateFeed' => 'This feed already exists.', 'API.Miniflux.Error.InvalidTitle' => 'Invalid feed title', + 'API.Miniflux.Error.InvalidImportCategory' => 'Payload contains an invalid category name', + 'API.Miniflux.Error.DuplicateImportCategory' => 'Payload contains the same category name twice', + 'API.Miniflux.Error.FailedImportFeed' => 'Unable to import feed at URL "{url}" (code {code}', + 'API.Miniflux.Error.InvalidImportLabel' => 'Payload contains an invalid label name', 'API.TTRSS.Category.Uncategorized' => 'Uncategorized', 'API.TTRSS.Category.Special' => 'Special', From a0d563e468f9781d818a65de7fb956390422223d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 6 Feb 2021 21:48:27 -0500 Subject: [PATCH 162/366] Update dependencies --- composer.lock | 36 ++--- vendor-bin/csfixer/composer.lock | 195 ++++++++++++------------- vendor-bin/daux/composer.lock | 243 ++++++++++++------------------- vendor-bin/phpunit/composer.lock | 41 +++--- vendor-bin/robo/composer.lock | 122 ++++++++-------- 5 files changed, 285 insertions(+), 352 deletions(-) diff --git a/composer.lock b/composer.lock index b3e19e88..adc76eb0 100644 --- a/composer.lock +++ b/composer.lock @@ -949,16 +949,16 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117" + "reference": "0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/3b75acd829741c768bc8b1f84eb33265e7cc5117", - "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44", + "reference": "0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44", "shasum": "" }, "require": { @@ -972,7 +972,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1015,20 +1015,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "727d1096295d807c309fb01a851577302394c897" + "reference": "6e971c891537eb617a00bb07a43d182a6915faba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897", - "reference": "727d1096295d807c309fb01a851577302394c897", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/6e971c891537eb617a00bb07a43d182a6915faba", + "reference": "6e971c891537eb617a00bb07a43d182a6915faba", "shasum": "" }, "require": { @@ -1040,7 +1040,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1082,20 +1082,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T17:09:11+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930" + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cede45fcdfabdd6043b3592e83678e42ec69e930", - "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", "shasum": "" }, "require": { @@ -1104,7 +1104,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1141,7 +1141,7 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" } ], "packages-dev": [ diff --git a/vendor-bin/csfixer/composer.lock b/vendor-bin/csfixer/composer.lock index 17bfa2ef..70c3175d 100644 --- a/vendor-bin/csfixer/composer.lock +++ b/vendor-bin/csfixer/composer.lock @@ -262,16 +262,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.17.3", + "version": "v2.18.2", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "bd32f5dd72cdfc7b53f54077f980e144bfa2f595" + "reference": "18f8c9d184ba777380794a389fabc179896ba913" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/bd32f5dd72cdfc7b53f54077f980e144bfa2f595", - "reference": "bd32f5dd72cdfc7b53f54077f980e144bfa2f595", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/18f8c9d184ba777380794a389fabc179896ba913", + "reference": "18f8c9d184ba777380794a389fabc179896ba913", "shasum": "" }, "require": { @@ -293,7 +293,6 @@ "symfony/stopwatch": "^3.0 || ^4.0 || ^5.0" }, "require-dev": { - "johnkary/phpunit-speedtrap": "^1.1 || ^2.0 || ^3.0", "justinrainbow/json-schema": "^5.0", "keradus/cli-executor": "^1.4", "mikey179/vfsstream": "^1.6", @@ -302,11 +301,11 @@ "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", "phpspec/prophecy-phpunit": "^1.1 || ^2.0", - "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.13 || ^9.4.4 <9.5", + "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.13 || ^9.5", "phpunitgoodpractices/polyfill": "^1.5", "phpunitgoodpractices/traits": "^1.9.1", "sanmai/phpunit-legacy-adapter": "^6.4 || ^8.2.1", - "symfony/phpunit-bridge": "^5.1", + "symfony/phpunit-bridge": "^5.2.1", "symfony/yaml": "^3.0 || ^4.0 || ^5.0" }, "suggest": { @@ -352,7 +351,7 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2020-12-24T11:14:44+00:00" + "time": "2021-01-26T00:22:21+00:00" }, { "name": "php-cs-fixer/diff", @@ -549,16 +548,16 @@ }, { "name": "symfony/console", - "version": "v5.2.1", + "version": "v5.2.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "47c02526c532fb381374dab26df05e7313978976" + "reference": "89d4b176d12a2946a1ae4e34906a025b7b6b135a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/47c02526c532fb381374dab26df05e7313978976", - "reference": "47c02526c532fb381374dab26df05e7313978976", + "url": "https://api.github.com/repos/symfony/console/zipball/89d4b176d12a2946a1ae4e34906a025b7b6b135a", + "reference": "89d4b176d12a2946a1ae4e34906a025b7b6b135a", "shasum": "" }, "require": { @@ -617,7 +616,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Console Component", + "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", "keywords": [ "cli", @@ -625,7 +624,7 @@ "console", "terminal" ], - "time": "2020-12-18T08:03:05+00:00" + "time": "2021-01-28T22:06:19+00:00" }, { "name": "symfony/deprecation-contracts", @@ -679,16 +678,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v5.2.1", + "version": "v5.2.3", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "1c93f7a1dff592c252574c79a8635a8a80856042" + "reference": "4f9760f8074978ad82e2ce854dff79a71fe45367" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/1c93f7a1dff592c252574c79a8635a8a80856042", - "reference": "1c93f7a1dff592c252574c79a8635a8a80856042", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4f9760f8074978ad82e2ce854dff79a71fe45367", + "reference": "4f9760f8074978ad82e2ce854dff79a71fe45367", "shasum": "" }, "require": { @@ -741,9 +740,9 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony EventDispatcher Component", + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", - "time": "2020-12-18T08:03:05+00:00" + "time": "2021-01-27T10:36:42+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -809,16 +808,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.2.1", + "version": "v5.2.3", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "fa8f8cab6b65e2d99a118e082935344c5ba8c60d" + "reference": "262d033b57c73e8b59cd6e68a45c528318b15038" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/fa8f8cab6b65e2d99a118e082935344c5ba8c60d", - "reference": "fa8f8cab6b65e2d99a118e082935344c5ba8c60d", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/262d033b57c73e8b59cd6e68a45c528318b15038", + "reference": "262d033b57c73e8b59cd6e68a45c528318b15038", "shasum": "" }, "require": { @@ -848,22 +847,22 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Filesystem Component", + "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", - "time": "2020-11-30T17:05:38+00:00" + "time": "2021-01-27T10:01:46+00:00" }, { "name": "symfony/finder", - "version": "v5.2.1", + "version": "v5.2.3", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "0b9231a5922fd7287ba5b411893c0ecd2733e5ba" + "reference": "4adc8d172d602008c204c2e16956f99257248e03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/0b9231a5922fd7287ba5b411893c0ecd2733e5ba", - "reference": "0b9231a5922fd7287ba5b411893c0ecd2733e5ba", + "url": "https://api.github.com/repos/symfony/finder/zipball/4adc8d172d602008c204c2e16956f99257248e03", + "reference": "4adc8d172d602008c204c2e16956f99257248e03", "shasum": "" }, "require": { @@ -892,22 +891,22 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Finder Component", + "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "time": "2020-12-08T17:02:38+00:00" + "time": "2021-01-28T22:06:19+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.2.1", + "version": "v5.2.3", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "87a2a4a766244e796dd9cb9d6f58c123358cd986" + "reference": "5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/87a2a4a766244e796dd9cb9d6f58c123358cd986", - "reference": "87a2a4a766244e796dd9cb9d6f58c123358cd986", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce", + "reference": "5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce", "shasum": "" }, "require": { @@ -939,27 +938,27 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony OptionsResolver Component", + "description": "Provides an improved replacement for the array_replace PHP function", "homepage": "https://symfony.com", "keywords": [ "config", "configuration", "options" ], - "time": "2020-10-24T12:08:07+00:00" + "time": "2021-01-27T12:56:27+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", - "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", "shasum": "" }, "require": { @@ -971,7 +970,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1008,20 +1007,20 @@ "polyfill", "portable" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c" + "reference": "267a9adeb8ecb8071040a740930e077cdfb987af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c", - "reference": "c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/267a9adeb8ecb8071040a740930e077cdfb987af", + "reference": "267a9adeb8ecb8071040a740930e077cdfb987af", "shasum": "" }, "require": { @@ -1033,7 +1032,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1072,20 +1071,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "727d1096295d807c309fb01a851577302394c897" + "reference": "6e971c891537eb617a00bb07a43d182a6915faba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897", - "reference": "727d1096295d807c309fb01a851577302394c897", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/6e971c891537eb617a00bb07a43d182a6915faba", + "reference": "6e971c891537eb617a00bb07a43d182a6915faba", "shasum": "" }, "require": { @@ -1097,7 +1096,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1139,20 +1138,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T17:09:11+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "39d483bdf39be819deabf04ec872eb0b2410b531" + "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/39d483bdf39be819deabf04ec872eb0b2410b531", - "reference": "39d483bdf39be819deabf04ec872eb0b2410b531", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", + "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", "shasum": "" }, "require": { @@ -1164,7 +1163,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1202,7 +1201,7 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-php70", @@ -1257,16 +1256,16 @@ }, { "name": "symfony/polyfill-php72", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930" + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cede45fcdfabdd6043b3592e83678e42ec69e930", - "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", "shasum": "" }, "require": { @@ -1275,7 +1274,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1312,20 +1311,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed" + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/8ff431c517be11c78c48a39a66d37431e26a6bed", - "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", "shasum": "" }, "require": { @@ -1334,7 +1333,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1374,20 +1373,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de" + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/e70aa8b064c5b72d3df2abd5ab1e90464ad009de", - "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", "shasum": "" }, "require": { @@ -1396,7 +1395,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1440,20 +1439,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/process", - "version": "v5.2.1", + "version": "v5.2.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "bd8815b8b6705298beaa384f04fabd459c10bedd" + "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/bd8815b8b6705298beaa384f04fabd459c10bedd", - "reference": "bd8815b8b6705298beaa384f04fabd459c10bedd", + "url": "https://api.github.com/repos/symfony/process/zipball/313a38f09c77fbcdc1d223e57d368cea76a2fd2f", + "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f", "shasum": "" }, "require": { @@ -1483,9 +1482,9 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Process Component", + "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", - "time": "2020-12-08T17:03:37+00:00" + "time": "2021-01-27T10:15:41+00:00" }, { "name": "symfony/service-contracts", @@ -1551,16 +1550,16 @@ }, { "name": "symfony/stopwatch", - "version": "v5.2.1", + "version": "v5.2.3", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "2b105c0354f39a63038a1d8bf776ee92852813af" + "reference": "b12274acfab9d9850c52583d136a24398cdf1a0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/2b105c0354f39a63038a1d8bf776ee92852813af", - "reference": "2b105c0354f39a63038a1d8bf776ee92852813af", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b12274acfab9d9850c52583d136a24398cdf1a0c", + "reference": "b12274acfab9d9850c52583d136a24398cdf1a0c", "shasum": "" }, "require": { @@ -1590,22 +1589,22 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Stopwatch Component", + "description": "Provides a way to profile code", "homepage": "https://symfony.com", - "time": "2020-11-01T16:14:45+00:00" + "time": "2021-01-27T10:15:41+00:00" }, { "name": "symfony/string", - "version": "v5.2.1", + "version": "v5.2.3", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "5bd67751d2e3f7d6f770c9154b8fbcb2aa05f7ed" + "reference": "c95468897f408dd0aca2ff582074423dd0455122" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/5bd67751d2e3f7d6f770c9154b8fbcb2aa05f7ed", - "reference": "5bd67751d2e3f7d6f770c9154b8fbcb2aa05f7ed", + "url": "https://api.github.com/repos/symfony/string/zipball/c95468897f408dd0aca2ff582074423dd0455122", + "reference": "c95468897f408dd0aca2ff582074423dd0455122", "shasum": "" }, "require": { @@ -1648,7 +1647,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony String component", + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", "homepage": "https://symfony.com", "keywords": [ "grapheme", @@ -1658,7 +1657,7 @@ "utf-8", "utf8" ], - "time": "2020-12-05T07:33:16+00:00" + "time": "2021-01-25T15:14:59+00:00" } ], "aliases": [], diff --git a/vendor-bin/daux/composer.lock b/vendor-bin/daux/composer.lock index 35bc4c60..2ed3ed66 100644 --- a/vendor-bin/daux/composer.lock +++ b/vendor-bin/daux/composer.lock @@ -655,16 +655,16 @@ }, { "name": "symfony/console", - "version": "v4.4.18", + "version": "v4.4.19", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "12e071278e396cc3e1c149857337e9e192deca0b" + "reference": "24026c44fc37099fa145707fecd43672831b837a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/12e071278e396cc3e1c149857337e9e192deca0b", - "reference": "12e071278e396cc3e1c149857337e9e192deca0b", + "url": "https://api.github.com/repos/symfony/console/zipball/24026c44fc37099fa145707fecd43672831b837a", + "reference": "24026c44fc37099fa145707fecd43672831b837a", "shasum": "" }, "require": { @@ -721,9 +721,9 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Console Component", + "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", - "time": "2020-12-18T07:41:31+00:00" + "time": "2021-01-27T09:09:26+00:00" }, { "name": "symfony/deprecation-contracts", @@ -780,16 +780,16 @@ }, { "name": "symfony/http-foundation", - "version": "v4.4.18", + "version": "v4.4.19", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "5ebda66b51612516bf338d5f87da2f37ff74cf34" + "reference": "8888741b633f6c3d1e572b7735ad2cae3e03f9c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5ebda66b51612516bf338d5f87da2f37ff74cf34", - "reference": "5ebda66b51612516bf338d5f87da2f37ff74cf34", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/8888741b633f6c3d1e572b7735ad2cae3e03f9c5", + "reference": "8888741b633f6c3d1e572b7735ad2cae3e03f9c5", "shasum": "" }, "require": { @@ -825,93 +825,22 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony HttpFoundation Component", + "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", - "time": "2020-12-18T07:41:31+00:00" - }, - { - "name": "symfony/intl", - "version": "v5.2.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/intl.git", - "reference": "53927f98c9201fe5db3cfc4d574b1f4039020297" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/53927f98c9201fe5db3cfc4d574b1f4039020297", - "reference": "53927f98c9201fe5db3cfc4d574b1f4039020297", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-intl-icu": "~1.0", - "symfony/polyfill-php80": "^1.15" - }, - "require-dev": { - "symfony/filesystem": "^4.4|^5.0" - }, - "suggest": { - "ext-intl": "to use the component with locales other than \"en\"" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Intl\\": "" - }, - "classmap": [ - "Resources/stubs" - ], - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - }, - { - "name": "Eriksen Costa", - "email": "eriksen.costa@infranology.com.br" - }, - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A PHP replacement layer for the C intl extension that includes additional data from the ICU library.", - "homepage": "https://symfony.com", - "keywords": [ - "i18n", - "icu", - "internationalization", - "intl", - "l10n", - "localization" - ], - "time": "2020-12-14T10:10:03+00:00" + "time": "2021-01-27T09:09:26+00:00" }, { "name": "symfony/mime", - "version": "v5.2.1", + "version": "v5.2.3", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "de97005aef7426ba008c46ba840fc301df577ada" + "reference": "7dee6a43493f39b51ff6c5bb2bd576fe40a76c86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/de97005aef7426ba008c46ba840fc301df577ada", - "reference": "de97005aef7426ba008c46ba840fc301df577ada", + "url": "https://api.github.com/repos/symfony/mime/zipball/7dee6a43493f39b51ff6c5bb2bd576fe40a76c86", + "reference": "7dee6a43493f39b51ff6c5bb2bd576fe40a76c86", "shasum": "" }, "require": { @@ -922,6 +851,8 @@ "symfony/polyfill-php80": "^1.15" }, "conflict": { + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", "symfony/mailer": "<4.4" }, "require-dev": { @@ -955,26 +886,26 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "A library to manipulate MIME messages", + "description": "Allows manipulating MIME messages", "homepage": "https://symfony.com", "keywords": [ "mime", "mime-type" ], - "time": "2020-12-09T18:54:12+00:00" + "time": "2021-02-02T06:10:15+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", - "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", "shasum": "" }, "require": { @@ -986,7 +917,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1023,33 +954,32 @@ "polyfill", "portable" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-intl-icu", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-icu.git", - "reference": "c44d5bf6a75eed79555c6bf37505c6d39559353e" + "reference": "b2b1e732a6c039f1a3ea3414b3379a2433e183d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/c44d5bf6a75eed79555c6bf37505c6d39559353e", - "reference": "c44d5bf6a75eed79555c6bf37505c6d39559353e", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/b2b1e732a6c039f1a3ea3414b3379a2433e183d6", + "reference": "b2b1e732a6c039f1a3ea3414b3379a2433e183d6", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/intl": "~2.3|~3.0|~4.0|~5.0" + "php": ">=7.1" }, "suggest": { - "ext-intl": "For best performance" + "ext-intl": "For best performance and support of other locales than \"en\"" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1059,6 +989,15 @@ "autoload": { "files": [ "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1085,20 +1024,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117" + "reference": "0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/3b75acd829741c768bc8b1f84eb33265e7cc5117", - "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44", + "reference": "0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44", "shasum": "" }, "require": { @@ -1112,7 +1051,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1155,20 +1094,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "727d1096295d807c309fb01a851577302394c897" + "reference": "6e971c891537eb617a00bb07a43d182a6915faba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897", - "reference": "727d1096295d807c309fb01a851577302394c897", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/6e971c891537eb617a00bb07a43d182a6915faba", + "reference": "6e971c891537eb617a00bb07a43d182a6915faba", "shasum": "" }, "require": { @@ -1180,7 +1119,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1222,20 +1161,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T17:09:11+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "39d483bdf39be819deabf04ec872eb0b2410b531" + "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/39d483bdf39be819deabf04ec872eb0b2410b531", - "reference": "39d483bdf39be819deabf04ec872eb0b2410b531", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", + "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", "shasum": "" }, "require": { @@ -1247,7 +1186,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1285,20 +1224,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930" + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cede45fcdfabdd6043b3592e83678e42ec69e930", - "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", "shasum": "" }, "require": { @@ -1307,7 +1246,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1344,20 +1283,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed" + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/8ff431c517be11c78c48a39a66d37431e26a6bed", - "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", "shasum": "" }, "require": { @@ -1366,7 +1305,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1406,20 +1345,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de" + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/e70aa8b064c5b72d3df2abd5ab1e90464ad009de", - "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", "shasum": "" }, "require": { @@ -1428,7 +1367,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1472,20 +1411,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/process", - "version": "v4.4.18", + "version": "v4.4.19", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "075316ff72233ce3d04a9743414292e834f2cb4a" + "reference": "7e950b6366d4da90292c2e7fa820b3c1842b965a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/075316ff72233ce3d04a9743414292e834f2cb4a", - "reference": "075316ff72233ce3d04a9743414292e834f2cb4a", + "url": "https://api.github.com/repos/symfony/process/zipball/7e950b6366d4da90292c2e7fa820b3c1842b965a", + "reference": "7e950b6366d4da90292c2e7fa820b3c1842b965a", "shasum": "" }, "require": { @@ -1514,9 +1453,9 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Process Component", + "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", - "time": "2020-12-08T16:59:59+00:00" + "time": "2021-01-27T09:09:26+00:00" }, { "name": "symfony/service-contracts", @@ -1582,16 +1521,16 @@ }, { "name": "symfony/yaml", - "version": "v5.2.1", + "version": "v5.2.3", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "290ea5e03b8cf9b42c783163123f54441fb06939" + "reference": "338cddc6d74929f6adf19ca5682ac4b8e109cdb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/290ea5e03b8cf9b42c783163123f54441fb06939", - "reference": "290ea5e03b8cf9b42c783163123f54441fb06939", + "url": "https://api.github.com/repos/symfony/yaml/zipball/338cddc6d74929f6adf19ca5682ac4b8e109cdb0", + "reference": "338cddc6d74929f6adf19ca5682ac4b8e109cdb0", "shasum": "" }, "require": { @@ -1634,9 +1573,9 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Yaml Component", + "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", - "time": "2020-12-08T17:02:38+00:00" + "time": "2021-02-03T04:42:09+00:00" }, { "name": "webuni/commonmark-table-extension", diff --git a/vendor-bin/phpunit/composer.lock b/vendor-bin/phpunit/composer.lock index 45e101b1..2dd58522 100644 --- a/vendor-bin/phpunit/composer.lock +++ b/vendor-bin/phpunit/composer.lock @@ -879,16 +879,16 @@ }, { "name": "phpunit/phpunit", - "version": "8.5.13", + "version": "8.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "8e86be391a58104ef86037ba8a846524528d784e" + "reference": "c25f79895d27b6ecd5abfa63de1606b786a461a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8e86be391a58104ef86037ba8a846524528d784e", - "reference": "8e86be391a58104ef86037ba8a846524528d784e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c25f79895d27b6ecd5abfa63de1606b786a461a3", + "reference": "c25f79895d27b6ecd5abfa63de1606b786a461a3", "shasum": "" }, "require": { @@ -958,7 +958,7 @@ "testing", "xunit" ], - "time": "2020-12-01T04:53:52+00:00" + "time": "2021-01-17T07:37:30+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -1577,16 +1577,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", - "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", "shasum": "" }, "require": { @@ -1598,7 +1598,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1635,7 +1635,7 @@ "polyfill", "portable" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "theseer/tokenizer", @@ -1728,26 +1728,25 @@ }, { "name": "webmozart/glob", - "version": "4.1.0", + "version": "4.3.0", "source": { "type": "git", - "url": "https://github.com/webmozart/glob.git", - "reference": "3cbf63d4973cf9d780b93d2da8eec7e4a9e63bbe" + "url": "https://github.com/webmozarts/glob.git", + "reference": "06358fafde0f32edb4513f4fd88fe113a40c90ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/glob/zipball/3cbf63d4973cf9d780b93d2da8eec7e4a9e63bbe", - "reference": "3cbf63d4973cf9d780b93d2da8eec7e4a9e63bbe", + "url": "https://api.github.com/repos/webmozarts/glob/zipball/06358fafde0f32edb4513f4fd88fe113a40c90ee", + "reference": "06358fafde0f32edb4513f4fd88fe113a40c90ee", "shasum": "" }, "require": { - "php": "^5.3.3|^7.0", + "php": "^7.3 || ^8.0.0", "webmozart/path-util": "^2.2" }, "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1", - "symfony/filesystem": "^2.5" + "phpunit/phpunit": "^8.0", + "symfony/filesystem": "^5.1" }, "type": "library", "extra": { @@ -1771,7 +1770,7 @@ } ], "description": "A PHP implementation of Ant's glob.", - "time": "2015-12-29T11:14:33+00:00" + "time": "2021-01-21T06:17:15+00:00" }, { "name": "webmozart/path-util", diff --git a/vendor-bin/robo/composer.lock b/vendor-bin/robo/composer.lock index d4395718..f5ef2e37 100644 --- a/vendor-bin/robo/composer.lock +++ b/vendor-bin/robo/composer.lock @@ -660,16 +660,16 @@ }, { "name": "pear/archive_tar", - "version": "1.4.11", + "version": "1.4.12", "source": { "type": "git", "url": "https://github.com/pear/Archive_Tar.git", - "reference": "17d355cb7d3c4ff08e5729f29cd7660145208d9d" + "reference": "19bb8e95490d3e3ad92fcac95500ca80bdcc7495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pear/Archive_Tar/zipball/17d355cb7d3c4ff08e5729f29cd7660145208d9d", - "reference": "17d355cb7d3c4ff08e5729f29cd7660145208d9d", + "url": "https://api.github.com/repos/pear/Archive_Tar/zipball/19bb8e95490d3e3ad92fcac95500ca80bdcc7495", + "reference": "19bb8e95490d3e3ad92fcac95500ca80bdcc7495", "shasum": "" }, "require": { @@ -722,11 +722,7 @@ "archive", "tar" ], - "support": { - "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Archive_Tar", - "source": "https://github.com/pear/Archive_Tar" - }, - "time": "2020-11-19T22:10:24+00:00" + "time": "2021-01-18T19:32:54+00:00" }, { "name": "pear/console_getopt", @@ -972,16 +968,16 @@ }, { "name": "symfony/console", - "version": "v4.4.18", + "version": "v4.4.19", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "12e071278e396cc3e1c149857337e9e192deca0b" + "reference": "24026c44fc37099fa145707fecd43672831b837a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/12e071278e396cc3e1c149857337e9e192deca0b", - "reference": "12e071278e396cc3e1c149857337e9e192deca0b", + "url": "https://api.github.com/repos/symfony/console/zipball/24026c44fc37099fa145707fecd43672831b837a", + "reference": "24026c44fc37099fa145707fecd43672831b837a", "shasum": "" }, "require": { @@ -1038,22 +1034,22 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Console Component", + "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", - "time": "2020-12-18T07:41:31+00:00" + "time": "2021-01-27T09:09:26+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.4.18", + "version": "v4.4.19", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "5d4c874b0eb1c32d40328a09dbc37307a5a910b0" + "reference": "c352647244bd376bf7d31efbd5401f13f50dad0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/5d4c874b0eb1c32d40328a09dbc37307a5a910b0", - "reference": "5d4c874b0eb1c32d40328a09dbc37307a5a910b0", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c352647244bd376bf7d31efbd5401f13f50dad0c", + "reference": "c352647244bd376bf7d31efbd5401f13f50dad0c", "shasum": "" }, "require": { @@ -1104,9 +1100,9 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony EventDispatcher Component", + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", - "time": "2020-12-18T07:41:31+00:00" + "time": "2021-01-27T09:09:26+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -1172,16 +1168,16 @@ }, { "name": "symfony/filesystem", - "version": "v4.4.18", + "version": "v4.4.19", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d99fbef7e0f69bf162ae6131b31132fa3cc4bcbe" + "reference": "83a6feed14846d2d9f3916adbaf838819e4e3380" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d99fbef7e0f69bf162ae6131b31132fa3cc4bcbe", - "reference": "d99fbef7e0f69bf162ae6131b31132fa3cc4bcbe", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/83a6feed14846d2d9f3916adbaf838819e4e3380", + "reference": "83a6feed14846d2d9f3916adbaf838819e4e3380", "shasum": "" }, "require": { @@ -1211,22 +1207,22 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Filesystem Component", + "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", - "time": "2020-11-30T13:04:35+00:00" + "time": "2021-01-27T09:09:26+00:00" }, { "name": "symfony/finder", - "version": "v5.2.1", + "version": "v5.2.3", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "0b9231a5922fd7287ba5b411893c0ecd2733e5ba" + "reference": "4adc8d172d602008c204c2e16956f99257248e03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/0b9231a5922fd7287ba5b411893c0ecd2733e5ba", - "reference": "0b9231a5922fd7287ba5b411893c0ecd2733e5ba", + "url": "https://api.github.com/repos/symfony/finder/zipball/4adc8d172d602008c204c2e16956f99257248e03", + "reference": "4adc8d172d602008c204c2e16956f99257248e03", "shasum": "" }, "require": { @@ -1255,22 +1251,22 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Finder Component", + "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "time": "2020-12-08T17:02:38+00:00" + "time": "2021-01-28T22:06:19+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", - "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", "shasum": "" }, "require": { @@ -1282,7 +1278,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1319,20 +1315,20 @@ "polyfill", "portable" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "39d483bdf39be819deabf04ec872eb0b2410b531" + "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/39d483bdf39be819deabf04ec872eb0b2410b531", - "reference": "39d483bdf39be819deabf04ec872eb0b2410b531", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", + "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", "shasum": "" }, "require": { @@ -1344,7 +1340,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1382,20 +1378,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed" + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/8ff431c517be11c78c48a39a66d37431e26a6bed", - "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", "shasum": "" }, "require": { @@ -1404,7 +1400,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1444,20 +1440,20 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.20.0", + "version": "v1.22.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de" + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/e70aa8b064c5b72d3df2abd5ab1e90464ad009de", - "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", "shasum": "" }, "require": { @@ -1466,7 +1462,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "1.22-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1510,7 +1506,7 @@ "portable", "shim" ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2021-01-07T16:49:33+00:00" }, { "name": "symfony/process", @@ -1623,16 +1619,16 @@ }, { "name": "symfony/yaml", - "version": "v4.4.18", + "version": "v4.4.19", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "bbce94f14d73732340740366fcbe63363663a403" + "reference": "17ed9f14c1aa05b1a5cf2e2c5ef2d0be28058ef9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/bbce94f14d73732340740366fcbe63363663a403", - "reference": "bbce94f14d73732340740366fcbe63363663a403", + "url": "https://api.github.com/repos/symfony/yaml/zipball/17ed9f14c1aa05b1a5cf2e2c5ef2d0be28058ef9", + "reference": "17ed9f14c1aa05b1a5cf2e2c5ef2d0be28058ef9", "shasum": "" }, "require": { @@ -1671,9 +1667,9 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Yaml Component", + "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", - "time": "2020-12-08T16:59:59+00:00" + "time": "2021-01-27T09:09:26+00:00" } ], "aliases": [], From 54a6fcc0d63e9a5a6abcc82599447d7da8b1730c Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 6 Feb 2021 23:51:23 -0500 Subject: [PATCH 163/366] Consolidate object factoriesinto one place --- lib/Arsse.php | 5 ++++- lib/CLI.php | 21 ++++++++------------- lib/Factory.php | 13 +++++++++++++ lib/REST.php | 7 +------ lib/REST/AbstractHandler.php | 3 +-- lib/REST/Miniflux/V1.php | 9 ++------- tests/cases/CLI/TestCLI.php | 18 ++++++++---------- tests/cases/Misc/TestFactory.php | 17 +++++++++++++++++ tests/cases/REST/Miniflux/TestV1.php | 9 +++------ tests/cases/REST/TestREST.php | 10 +--------- tests/cases/REST/TinyTinyRSS/TestAPI.php | 3 +-- tests/lib/AbstractTest.php | 6 ++++++ tests/phpunit.dist.xml | 1 + 13 files changed, 66 insertions(+), 56 deletions(-) create mode 100644 lib/Factory.php create mode 100644 tests/cases/Misc/TestFactory.php diff --git a/lib/Arsse.php b/lib/Arsse.php index 7d53a427..0cd7d6c5 100644 --- a/lib/Arsse.php +++ b/lib/Arsse.php @@ -7,8 +7,10 @@ declare(strict_types=1); namespace JKingWeb\Arsse; class Arsse { - public const VERSION = "0.8.5"; + public const VERSION = "0.9.0"; + /** @var Factory */ + public static $obj; /** @var Lang */ public static $lang; /** @var Conf */ @@ -19,6 +21,7 @@ class Arsse { public static $user; public static function load(Conf $conf): void { + static::$obj = static::$obj ?? new Factory; static::$lang = static::$lang ?? new Lang; static::$conf = $conf; static::$lang->set($conf->lang); diff --git a/lib/CLI.php b/lib/CLI.php index c9a59673..bc96f468 100644 --- a/lib/CLI.php +++ b/lib/CLI.php @@ -182,26 +182,26 @@ USAGE_TEXT; echo Arsse::VERSION.\PHP_EOL; return 0; case "daemon": - $this->getInstance(Service::class)->watch(true); + Arsse::$obj->get(Service::class)->watch(true); return 0; case "feed refresh": return (int) !Arsse::$db->feedUpdate((int) $args[''], true); case "feed refresh-all": - $this->getInstance(Service::class)->watch(false); + Arsse::$obj->get(Service::class)->watch(false); return 0; case "conf save-defaults": $file = $this->resolveFile($args[''], "w"); - return (int) !$this->getInstance(Conf::class)->exportFile($file, true); + return (int) !Arsse::$obj->get(Conf::class)->exportFile($file, true); case "user": return $this->userManage($args); case "export": $u = $args['']; $file = $this->resolveFile($args[''], "w"); - return (int) !$this->getInstance(OPML::class)->exportFile($file, $u, ($args['--flat'] || $args['-f'])); + return (int) !Arsse::$obj->get(OPML::class)->exportFile($file, $u, ($args['--flat'] || $args['-f'])); case "import": $u = $args['']; $file = $this->resolveFile($args[''], "r"); - return (int) !$this->getInstance(OPML::class)->importFile($file, $u, ($args['--flat'] || $args['-f']), ($args['--replace'] || $args['-r'])); + return (int) !Arsse::$obj->get(OPML::class)->importFile($file, $u, ($args['--flat'] || $args['-f']), ($args['--replace'] || $args['-r'])); } } catch (AbstractException $e) { $this->logError($e->getMessage()); @@ -214,11 +214,6 @@ USAGE_TEXT; fwrite(STDERR, $msg.\PHP_EOL); } - /** @codeCoverageIgnore */ - protected function getInstance(string $class) { - return new $class; - } - protected function userManage($args): int { $cmd = $this->command(["add", "remove", "set-pass", "unset-pass", "list", "auth"], $args); switch ($cmd) { @@ -226,7 +221,7 @@ USAGE_TEXT; return $this->userAddOrSetPassword("add", $args[""], $args[""]); case "set-pass": if ($args['--fever']) { - $passwd = $this->getInstance(Fever::class)->register($args[""], $args[""]); + $passwd = Arsse::$obj->get(Fever::class)->register($args[""], $args[""]); if (is_null($args[""])) { echo $passwd.\PHP_EOL; } @@ -237,7 +232,7 @@ USAGE_TEXT; // no break case "unset-pass": if ($args['--fever']) { - $this->getInstance(Fever::class)->unregister($args[""]); + Arsse::$obj->get(Fever::class)->unregister($args[""]); } else { Arsse::$user->passwordUnset($args[""], $args["--oldpass"]); } @@ -271,7 +266,7 @@ USAGE_TEXT; } protected function userAuthenticate(string $user, string $password, bool $fever = false): int { - $result = $fever ? $this->getInstance(Fever::class)->authenticate($user, $password) : Arsse::$user->auth($user, $password); + $result = $fever ? Arsse::$obj->get(Fever::class)->authenticate($user, $password) : Arsse::$user->auth($user, $password); if ($result) { echo Arsse::$lang->msg("CLI.Auth.Success").\PHP_EOL; return 0; diff --git a/lib/Factory.php b/lib/Factory.php new file mode 100644 index 00000000..96989025 --- /dev/null +++ b/lib/Factory.php @@ -0,0 +1,13 @@ +withMethod(strtoupper($req->getMethod()))->withRequestTarget($target); // fetch the correct handler - $drv = $this->getHandler($class); + $drv = Arsse::$obj->get($class); // generate a response if ($req->getMethod() === "HEAD") { // if the request is a HEAD request, we act exactly as if it were a GET request, and simply remove the response body later @@ -105,11 +105,6 @@ class REST { return $this->normalizeResponse($res, $req); } - public function getHandler(string $className): REST\Handler { - // instantiate the API handler - return new $className(); - } - public function apiMatch(string $url): array { $map = $this->apis; // sort the API list so the longest URL prefixes come first diff --git a/lib/REST/AbstractHandler.php b/lib/REST/AbstractHandler.php index f0e39e79..7103c34a 100644 --- a/lib/REST/AbstractHandler.php +++ b/lib/REST/AbstractHandler.php @@ -16,9 +16,8 @@ abstract class AbstractHandler implements Handler { abstract public function __construct(); abstract public function dispatch(ServerRequestInterface $req): ResponseInterface; - /** @codeCoverageIgnore */ protected function now(): \DateTimeImmutable { - return Date::normalize("now"); + return Arsse::$obj->get(\DateTimeImmutable::class)->setTimezone(new \DateTimeZone("UTC")); } protected function isAdmin(): bool { diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 00c58f02..e82d5906 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -214,11 +214,6 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { public function __construct() { } - /** @codeCoverageIgnore */ - protected function getInstance(string $class) { - return new $class; - } - protected function authenticate(ServerRequestInterface $req): bool { // first check any tokens; this is what Miniflux does if ($req->hasHeader("X-Auth-Token")) { @@ -1183,7 +1178,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function opmlImport(string $data): ResponseInterface { try { - $this->getInstance(OPML::class)->import(Arsse::$user->id, $data); + Arsse::$obj->get(OPML::class)->import(Arsse::$user->id, $data); } catch (ImportException $e) { switch ($e->getCode()) { case 10611: @@ -1204,7 +1199,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function opmlExport(): ResponseInterface { - return new GenericResponse($this->getInstance(OPML::class)->export(Arsse::$user->id), 200, ['Content-Type' => "application/xml"]); + return new GenericResponse(Arsse::$obj->get(OPML::class)->export(Arsse::$user->id), 200, ['Content-Type' => "application/xml"]); } public static function tokenGenerate(string $user, string $label): string { diff --git a/tests/cases/CLI/TestCLI.php b/tests/cases/CLI/TestCLI.php index 3d0d6f14..30237757 100644 --- a/tests/cases/CLI/TestCLI.php +++ b/tests/cases/CLI/TestCLI.php @@ -60,22 +60,20 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { public function testStartTheDaemon(): void { $srv = \Phake::mock(Service::class); + \Phake::when(Arsse::$obj)->get(Service::class)->thenReturn($srv); \Phake::when($srv)->watch->thenReturn(new \DateTimeImmutable); - \Phake::when($this->cli)->getInstance(Service::class)->thenReturn($srv); $this->assertConsole($this->cli, "arsse.php daemon", 0); \Phake::verify($this->cli)->loadConf; \Phake::verify($srv)->watch(true); - \Phake::verify($this->cli)->getInstance(Service::class); } public function testRefreshAllFeeds(): void { $srv = \Phake::mock(Service::class); + \Phake::when(Arsse::$obj)->get(Service::class)->thenReturn($srv); \Phake::when($srv)->watch->thenReturn(new \DateTimeImmutable); - \Phake::when($this->cli)->getInstance(Service::class)->thenReturn($srv); $this->assertConsole($this->cli, "arsse.php feed refresh-all", 0); \Phake::verify($this->cli)->loadConf; \Phake::verify($srv)->watch(false); - \Phake::verify($this->cli)->getInstance(Service::class); } /** @dataProvider provideFeedUpdates */ @@ -98,10 +96,10 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideDefaultConfigurationSaves */ public function testSaveTheDefaultConfiguration(string $cmd, int $exitStatus, string $file): void { $conf = \Phake::mock(Conf::class); + \Phake::when(Arsse::$obj)->get(Conf::class)->thenReturn($conf); \Phake::when($conf)->exportFile("php://output", true)->thenReturn(true); \Phake::when($conf)->exportFile("good.conf", true)->thenReturn(true); \Phake::when($conf)->exportFile("bad.conf", true)->thenThrow(new \JKingWeb\Arsse\Conf\Exception("fileUnwritable")); - \Phake::when($this->cli)->getInstance(Conf::class)->thenReturn($conf); $this->assertConsole($this->cli, $cmd, $exitStatus); \Phake::verify($this->cli, \Phake::times(0))->loadConf; \Phake::verify($conf)->exportFile($file, true); @@ -169,10 +167,10 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { ; })); $fever = \Phake::mock(FeverUser::class); + \Phake::when(Arsse::$obj)->get(FeverUser::class)->thenReturn($fever); \Phake::when($fever)->authenticate->thenReturn(false); \Phake::when($fever)->authenticate("john.doe@example.com", "ashalla")->thenReturn(true); \Phake::when($fever)->authenticate("jane.doe@example.com", "thx1138")->thenReturn(true); - \Phake::when($this->cli)->getInstance(FeverUser::class)->thenReturn($fever); $this->assertConsole($this->cli, $cmd, $exitStatus, $output); } @@ -226,8 +224,8 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { Arsse::$user = $this->createMock(User::class); Arsse::$user->method("passwordSet")->will($this->returnCallback($passwordChange)); $fever = \Phake::mock(FeverUser::class); + \Phake::when(Arsse::$obj)->get(FeverUser::class)->thenReturn($fever); \Phake::when($fever)->register->thenReturnCallback($passwordChange); - \Phake::when($this->cli)->getInstance(FeverUser::class)->thenReturn($fever); $this->assertConsole($this->cli, $cmd, $exitStatus, $output); } @@ -256,8 +254,8 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { Arsse::$user = $this->createMock(User::class); Arsse::$user->method("passwordUnset")->will($this->returnCallback($passwordClear)); $fever = \Phake::mock(FeverUser::class); + \Phake::when(Arsse::$obj)->get(FeverUser::class)->thenReturn($fever); \Phake::when($fever)->unregister->thenReturnCallback($passwordClear); - \Phake::when($this->cli)->getInstance(FeverUser::class)->thenReturn($fever); $this->assertConsole($this->cli, $cmd, $exitStatus, $output); } @@ -273,10 +271,10 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideOpmlExports */ public function testExportToOpml(string $cmd, int $exitStatus, string $file, string $user, bool $flat): void { $opml = \Phake::mock(OPML::class); + \Phake::when(Arsse::$obj)->get(OPML::class)->thenReturn($opml); \Phake::when($opml)->exportFile("php://output", $user, $flat)->thenReturn(true); \Phake::when($opml)->exportFile("good.opml", $user, $flat)->thenReturn(true); \Phake::when($opml)->exportFile("bad.opml", $user, $flat)->thenThrow(new \JKingWeb\Arsse\ImportExport\Exception("fileUnwritable")); - \Phake::when($this->cli)->getInstance(OPML::class)->thenReturn($opml); $this->assertConsole($this->cli, $cmd, $exitStatus); \Phake::verify($this->cli)->loadConf; \Phake::verify($opml)->exportFile($file, $user, $flat); @@ -314,10 +312,10 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideOpmlImports */ public function testImportFromOpml(string $cmd, int $exitStatus, string $file, string $user, bool $flat, bool $replace): void { $opml = \Phake::mock(OPML::class); + \Phake::when(Arsse::$obj)->get(OPML::class)->thenReturn($opml); \Phake::when($opml)->importFile("php://input", $user, $flat, $replace)->thenReturn(true); \Phake::when($opml)->importFile("good.opml", $user, $flat, $replace)->thenReturn(true); \Phake::when($opml)->importFile("bad.opml", $user, $flat, $replace)->thenThrow(new \JKingWeb\Arsse\ImportExport\Exception("fileUnreadable")); - \Phake::when($this->cli)->getInstance(OPML::class)->thenReturn($opml); $this->assertConsole($this->cli, $cmd, $exitStatus); \Phake::verify($this->cli)->loadConf; \Phake::verify($opml)->importFile($file, $user, $flat, $replace); diff --git a/tests/cases/Misc/TestFactory.php b/tests/cases/Misc/TestFactory.php new file mode 100644 index 00000000..e400c2f7 --- /dev/null +++ b/tests/cases/Misc/TestFactory.php @@ -0,0 +1,17 @@ +assertInstanceOf(\stdClass::class, $f->get(\stdClass::class)); + } +} \ No newline at end of file diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 22a53db6..fc08dea4 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -211,8 +211,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { throw $u[2]; } }); - $this->h = $this->createPartialMock(V1::class, ["now"]); - $this->h->method("now")->willReturn(Date::normalize(self::NOW)); + \Phake::when(Arsse::$obj)->get(\DateTimeImmutable::class)->thenReturn(new \DateTimeImmutable(self::NOW)); $this->assertMessage($exp, $this->req("GET", $route, "", [], $user)); } @@ -240,8 +239,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideUserModifications */ public function testModifyAUser(bool $admin, string $url, array $body, $in1, $out1, $in2, $out2, $in3, $out3, ResponseInterface $exp): void { - $this->h = $this->createPartialMock(V1::class, ["now"]); - $this->h->method("now")->willReturn(Date::normalize(self::NOW)); + \Phake::when(Arsse::$obj)->get(\DateTimeImmutable::class)->thenReturn(new \DateTimeImmutable(self::NOW)); Arsse::$user = $this->createMock(User::class); Arsse::$user->method("begin")->willReturn($this->transaction); Arsse::$user->method("propertiesGet")->willReturnCallback(function(string $u, bool $includeLarge) use ($admin) { @@ -321,8 +319,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideUserAdditions */ public function testAddAUser(array $body, $in1, $out1, $in2, $out2, ResponseInterface $exp): void { - $this->h = $this->createPartialMock(V1::class, ["now"]); - $this->h->method("now")->willReturn(Date::normalize(self::NOW)); + \Phake::when(Arsse::$obj)->get(\DateTimeImmutable::class)->thenReturn(new \DateTimeImmutable(self::NOW)); Arsse::$user = $this->createMock(User::class); Arsse::$user->method("begin")->willReturn($this->transaction); Arsse::$user->method("propertiesGet")->willReturnCallback(function(string $u, bool $includeLarge) { diff --git a/tests/cases/REST/TestREST.php b/tests/cases/REST/TestREST.php index da458930..18642150 100644 --- a/tests/cases/REST/TestREST.php +++ b/tests/cases/REST/TestREST.php @@ -286,14 +286,6 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest { ]; } - public function testCreateHandlers(): void { - $r = new REST(); - foreach (REST::API_LIST as $api) { - $class = $api['class']; - $this->assertInstanceOf(Handler::class, $r->getHandler($class)); - } - } - /** @dataProvider provideMockRequests */ public function testDispatchRequests(ServerRequest $req, string $method, bool $called, string $class = "", string $target = ""): void { $r = \Phake::partialMock(REST::class); @@ -305,7 +297,7 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest { }); if ($called) { $h = \Phake::mock($class); - \Phake::when($r)->getHandler($class)->thenReturn($h); + \Phake::when(Arsse::$obj)->get($class)->thenReturn($h); \Phake::when($h)->dispatch->thenReturn(new EmptyResponse(204)); } $out = $r->dispatch($req); diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index 6191d84b..874a103c 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -1700,8 +1700,7 @@ LONG_STRING; public function testRetrieveHeadlines(bool $full, array $in, $out, Context $c, array $fields, array $order, ResponseInterface $exp): void { $base = ['op' => $full ? "getHeadlines" : "getCompactHeadlines", 'sid' => "PriestsOfSyrinx"]; $in = array_merge($base, $in); - $this->h = \Phake::partialMock(API::class); - \Phake::when($this->h)->now->thenReturn(Date::normalize(self::NOW)); + \Phake::when(Arsse::$obj)->get(\DateTimeImmutable::class)->thenReturn(new \DateTimeImmutable(self::NOW)); \Phake::when(Arsse::$db)->labelList->thenReturn(new Result($this->v($this->labels))); \Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result($this->v($this->usedLabels))); \Phake::when(Arsse::$db)->articleLabelsGet->thenReturn([]); diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index 54cde189..e096ca1e 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -13,6 +13,7 @@ use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Conf; use JKingWeb\Arsse\Db\Driver; use JKingWeb\Arsse\Db\Result; +use JKingWeb\Arsse\Factory; use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\ValueInfo; use JKingWeb\Arsse\Misc\URL; @@ -45,6 +46,11 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { } if ($loadLang) { Arsse::$lang = new \JKingWeb\Arsse\Lang(); + // also create the object factory as a mock + Arsse::$obj = \Phake::mock(Factory::class); + \Phake::when(Arsse::$obj)->get->thenReturnCallback(function(string $class) { + return new $class; + }); } } diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml index 0875bf54..3d576064 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -45,6 +45,7 @@ cases/Conf/TestConf.php
+ cases/Misc/TestFactory.php cases/Misc/TestValueInfo.php cases/Misc/TestDate.php cases/Misc/TestQuery.php From 6c2de89f3e1693aa15e8f452502872a88ff4d6a7 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 6 Feb 2021 23:55:40 -0500 Subject: [PATCH 164/366] Revert copy-paste corruption --- lib/REST/Miniflux/V1.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index e82d5906..db2b40d9 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -143,8 +143,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'GET' => ["getCategoryFeeds", false, true, false, false, []], ], '/categories/1/mark-all-as-read' => [ + 'PUT' => ["markCategory", false, true, false, false, []], ], - 'PUT' => ["markCategory", false, true, false, false, []], '/discover' => [ 'POST' => ["discoverSubscriptions", false, false, true, false, ["url"]], ], From 37fd2ad4e915e35d946fa2a4296cf350470ee006 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 7 Feb 2021 09:07:53 -0500 Subject: [PATCH 165/366] Tests for new exception features --- lib/AbstractException.php | 2 +- tests/cases/Exception/TestException.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/AbstractException.php b/lib/AbstractException.php index 922b9cd8..d2cb0d5a 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -132,6 +132,6 @@ abstract class AbstractException extends \Exception { } public function getParams(): array { - return $this->aparams; + return $this->params; } } diff --git a/tests/cases/Exception/TestException.php b/tests/cases/Exception/TestException.php index 66a1f3a8..563a4f58 100644 --- a/tests/cases/Exception/TestException.php +++ b/tests/cases/Exception/TestException.php @@ -78,4 +78,14 @@ class TestException extends \JKingWeb\Arsse\Test\AbstractTest { $this->expectException('JKingWeb\Arsse\ExceptionFatal'); throw new \JKingWeb\Arsse\ExceptionFatal(""); } + + public function testGetExceptionSymbol(): void { + $e = new LangException("stringMissing", ['msgID' => "OOK"]); + $this->assertSame("stringMissing", $e->getSymbol()); + } + + public function testGetExceptionParams(): void { + $e = new LangException("stringMissing", ['msgID' => "OOK"]); + $this->assertSame(['msgID' => "OOK"], $e->getParams()); + } } From 9cc779a717fad1389b7b2923ae407b9996735053 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 7 Feb 2021 13:04:44 -0500 Subject: [PATCH 166/366] Import/export tests --- lib/REST/Miniflux/V1.php | 6 ++--- tests/cases/REST/Miniflux/TestV1.php | 37 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index db2b40d9..ee9c3329 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -263,7 +263,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } if ($reqBody) { if ($func === "opmlImport") { - $args[] = (string) $req->getBody(); + $data = (string) $req->getBody(); } else { $data = (string) $req->getBody(); if (strlen($data)) { @@ -1188,14 +1188,14 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { case 10613: return new ErrorResponse("InvalidImportCategory", 422); case 10614: - return new ErrorResponse("DuplicateImportCatgory", 422); + return new ErrorResponse("DuplicateImportCategory", 422); case 10615: return new ErrorResponse("InvalidImportLabel", 422); } } catch (FeedException $e) { return new ErrorResponse(["FailedImportFeed", 'url' => $e->getParams()['url'], 'code' => $e->getCode()], 502); } - return new Response(['message' => Arsse::$lang->msg("ImportSuccess")]); + return new Response(['message' => Arsse::$lang->msg("API.Miniflux.ImportSuccess")]); } protected function opmlExport(): ResponseInterface { diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index fc08dea4..6466fba3 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -16,12 +16,15 @@ use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\REST\Miniflux\V1; use JKingWeb\Arsse\REST\Miniflux\ErrorResponse; use JKingWeb\Arsse\Feed\Exception as FeedException; +use JKingWeb\Arsse\ImportExport\Exception as ImportException; +use JKingWeb\Arsse\ImportExport\OPML; use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\ExceptionInput as UserExceptionInput; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\EmptyResponse; use JKingWeb\Arsse\Test\Result; +use Laminas\Diactoros\Response\TextResponse; /** @covers \JKingWeb\Arsse\REST\Miniflux\V1 */ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { @@ -934,4 +937,38 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function testRefreshAllFeeds(): void { $this->assertMessage(new EmptyResponse(204), $this->req("PUT", "/feeds/refresh")); } + + /** @dataProvider provideImports */ + public function testImport($out, ResponseInterface $exp): void { + $opml = \Phake::mock(OPML::class); + \Phake::when(Arsse::$obj)->get(OPML::class)->thenReturn($opml); + if ($out instanceof \Exception) { + \Phake::when($opml)->import->thenThrow($out); + } else { + \Phake::when($opml)->import->thenReturn($out); + } + $this->assertMessage($exp, $this->req("POST", "/import", "IMPORT DATA")); + \Phake::verify($opml)->import(Arsse::$user->id, "IMPORT DATA"); + } + + public function provideImports(): iterable { + self::clearData(); + return [ + [new ImportException("invalidSyntax"), new ErrorResponse("InvalidBodyXML", 400)], + [new ImportException("invalidSemantics"), new ErrorResponse("InvalidBodyOPML", 422)], + [new ImportException("invalidFolderName"), new ErrorResponse("InvalidImportCategory", 422)], + [new ImportException("invalidFolderCopy"), new ErrorResponse("DuplicateImportCategory", 422)], + [new ImportException("invalidTagName"), new ErrorResponse("InvalidImportLabel", 422)], + [new FeedException("invalidUrl", ['url' => "http://example.com/"]), new ErrorResponse(["FailedImportFeed", 'url' => "http://example.com/", 'code' => 10502], 502)], + [true, new Response(['message' => Arsse::$lang->msg("API.Miniflux.ImportSuccess")])], + ]; + } + + public function testExport(): void { + $opml = \Phake::mock(OPML::class); + \Phake::when(Arsse::$obj)->get(OPML::class)->thenReturn($opml); + \Phake::when($opml)->export->thenReturn("EXPORT DATA"); + $this->assertMessage(new TextResponse("EXPORT DATA", 200, ['Content-Type' => "application/xml"]), $this->req("GET", "/export")); + \Phake::verify($opml)->export(Arsse::$user->id); + } } From eae0ba4b68c70e82a31405b9b5372ee999fe5cd4 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 7 Feb 2021 19:20:10 -0500 Subject: [PATCH 167/366] Tests fortoken operations --- lib/REST/Miniflux/V1.php | 2 +- tests/cases/REST/Miniflux/TestV1.php | 40 +++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index ee9c3329..097a9c86 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -1203,7 +1203,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } public static function tokenGenerate(string $user, string $label): string { - // Miniflux produces tokenss in base64url alphabet + // Miniflux produces tokens in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); return Arsse::$db->tokenCreate($user, "miniflux.login", $t, null, $label); } diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 6466fba3..73b9b4a8 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -667,6 +667,14 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ]; } + public function testModifyAFeedWithNoBody(): void { + $this->h = \Phake::partialMock(V1::class); + \Phake::when($this->h)->getFeed->thenReturn(new Response(self::FEEDS_OUT[0])); + \Phake::when(Arsse::$db)->subscriptionPropertiesSet->thenReturn(true); + $this->assertMessage(new Response(self::FEEDS_OUT[0]), $this->req("PUT", "/feeds/2112", "")); + \Phake::verify(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 2112, []); + } + public function testDeleteAFeed(): void { \Phake::when(Arsse::$db)->subscriptionRemove->thenReturn(true); $this->assertMessage(new EmptyResponse(204), $this->req("DELETE", "/feeds/2112")); @@ -971,4 +979,34 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertMessage(new TextResponse("EXPORT DATA", 200, ['Content-Type' => "application/xml"]), $this->req("GET", "/export")); \Phake::verify($opml)->export(Arsse::$user->id); } -} + + public function testGenerateTokens(): void { + \Phake::when(Arsse::$db)->tokenCreate->thenReturn("RANDOM TOKEN"); + $this->assertSame("RANDOM TOKEN", V1::tokenGenerate("ook", "Eek")); + \Phake::verify(Arsse::$db)->tokenCreate("ook", "miniflux.login", \Phake::capture($token), null, "Eek"); + $this->assertRegExp("/^[A-Za-z0-9_\-]{43}=$/", $token); + } + + public function testListTheTokensOfAUser(): void { + $out = [ + ['id' => "TOKEN 1", 'data' => "Ook"], + ['id' => "TOKEN 2", 'data' => "Eek"], + ['id' => "TOKEN 3", 'data' => "Ack"], + ]; + $exp = [ + ['label' => "Ook", 'id' => "TOKEN 1"], + ['label' => "Eek", 'id' => "TOKEN 2"], + ['label' => "Ack", 'id' => "TOKEN 3"], + ]; + \Phake::when(Arsse::$db)->tokenList->thenReturn(new Result($this->v($out))); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertSame($exp, V1::tokenList("john.doe@example.com")); + \Phake::verify(Arsse::$db)->tokenList("john.doe@example.com", "miniflux.login"); + } + + public function testListTheTokensOfAMissingUser(): void { + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); + V1::tokenList("john.doe@example.com"); + } +} \ No newline at end of file From f2e5d567ecb06b31dc86a2c18a0bdd3145551e25 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 7 Feb 2021 21:38:16 -0500 Subject: [PATCH 168/366] Update sample Web server configuration --- dist/apache.conf | 8 ++++---- dist/nginx.conf | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/dist/apache.conf b/dist/apache.conf index 3c27b5a2..c0122967 100644 --- a/dist/apache.conf +++ b/dist/apache.conf @@ -10,13 +10,13 @@ ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/usr/share/arsse/arsse.php" ProxyPreserveHost On - # Nextcloud News v1.2, Tiny Tiny RSS API, TT-RSS newsfeed icons - + # Nextcloud News v1.2, Tiny Tiny RSS API, TT-RSS newsfeed icons, Miniflux API + ProxyPass "unix:/var/run/php/php7.2-fpm.sock|fcgi://localhost/usr/share/arsse" - # Nextcloud News API detection, Fever API - + # Nextcloud News API detection, Fever API, Miniflux miscellanies + # these locations should not be behind HTTP authentication ProxyPass "unix:/var/run/php/php7.2-fpm.sock|fcgi://localhost/usr/share/arsse" diff --git a/dist/nginx.conf b/dist/nginx.conf index c9c7845b..c12ff214 100644 --- a/dist/nginx.conf +++ b/dist/nginx.conf @@ -55,4 +55,21 @@ server { # this path should not be behind HTTP authentication try_files $uri @arsse; } + + # Miniflux protocol + location /v1/ { + try_files $uri @arsse; + } + + # Miniflux version number + location /version { + # this path should not be behind HTTP authentication + try_files $uri @arsse; + } + + # Miniflux "health check" + location /healthcheck { + # this path should not be behind HTTP authentication + try_files $uri @arsse; + } } From 211cea648e18c5ecb5b4d73b2fd0269b49ce2264 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 8 Feb 2021 19:07:49 -0500 Subject: [PATCH 169/366] Implement TT-RSS API level 15 --- lib/REST/TinyTinyRSS/API.php | 16 ++++- tests/cases/REST/TinyTinyRSS/TestAPI.php | 87 +++++++++++------------- 2 files changed, 53 insertions(+), 50 deletions(-) diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index 3de48637..0d4d12ae 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -24,7 +24,7 @@ use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\EmptyResponse; class API extends \JKingWeb\Arsse\REST\AbstractHandler { - public const LEVEL = 14; // emulated API level + public const LEVEL = 15; // emulated API level public const VERSION = "17.4"; // emulated TT-RSS version protected const LABEL_OFFSET = 1024; // offset below zero at which labels begin, counting down @@ -79,7 +79,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { 'include_header' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to attach a header to the results of `getHeadlines` 'search' => ValueInfo::T_STRING, // search string for `getHeadlines` 'field' => ValueInfo::T_INT, // which state to change in `updateArticle` - 'mode' => ValueInfo::T_INT, // whether to set, clear, or toggle the selected state in `updateArticle` + 'mode' => ValueInfo::T_MIXED, // whether to set, clear, or toggle the selected state in `updateArticle` (integer), or whether to ignore a certain recent timeframe in `catchupFeed` (string) 'data' => ValueInfo::T_STRING, // note text in `updateArticle` if setting a note ]; protected const VIEW_MODES = ["all_articles", "adaptive", "unread", "marked", "has_note", "published"]; @@ -1037,6 +1037,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { public function opCatchUpFeed(array $data): array { $id = $data['feed_id'] ?? self::FEED_ARCHIVED; $cat = $data['is_cat'] ?? false; + $mode = $data['mode'] ?? "all"; $out = ['status' => "OK"]; // first prepare the context; unsupported contexts simply return early $c = (new Context)->hidden(false); @@ -1089,6 +1090,16 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } } } + switch ($mode) { + case "2week": + $c->notModifiedSince(Date::sub("P2W", $this->now())); + break; + case "1week": + $c->notModifiedSince(Date::sub("P1W", $this->now())); + break; + case "1day": + $c->notModifiedSince(Date::sub("PT24H", $this->now())); + } // perform the marking try { Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c); @@ -1102,6 +1113,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { public function opUpdateArticle(array $data): array { // normalize input $articles = array_filter(ValueInfo::normalize(explode(",", (string) $data['article_ids']), ValueInfo::T_INT | ValueInfo::M_ARRAY), [ValueInfo::class, "id"]); + $data['mode'] = ValueInfo::normalize($data['mode'], ValueInfo::T_INT); if (!$articles) { // if there are no valid articles this is an error throw new Exception("INCORRECT_USAGE"); diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index 874a103c..e79a2f65 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -19,8 +19,8 @@ use JKingWeb\Arsse\REST\TinyTinyRSS\API; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\EmptyResponse; - /** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\API + * @covers \JKingWeb\Arsse\REST\TinyTinyRSS\Exception */ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { protected const NOW = "2020-12-21T23:09:17.189065Z"; @@ -1309,55 +1309,46 @@ LONG_STRING; $this->assertMessage($this->respGood($exp), $this->req($in[1])); } - public function testMarkFeedsAsRead(): void { - $in1 = [ - // no-ops - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx"], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -6], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1, 'is_cat' => true], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3, 'is_cat' => true], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4, 'is_cat' => true], - ]; - $in2 = [ - // simple contexts - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -1], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -4], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2112], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 2112], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 42, 'is_cat' => true], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => 0, 'is_cat' => true], - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -2, 'is_cat' => true], - ]; - $in3 = [ - // this one has a tricky time-based context - ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx", 'feed_id' => -3], - ]; + /** @dataProvider provideMassMarkings */ + public function testMarkFeedsAsRead(array $in, ?Context $c): void { + $base = ['op' => "catchupFeed", 'sid' => "PriestsOfSyrinx"]; + $in = array_merge($base, $in); \Phake::when(Arsse::$db)->articleMark->thenThrow(new ExceptionInput("typeViolation")); - $exp = $this->respGood(['status' => "OK"]); - // verify the above are in fact no-ops - for ($a = 0; $a < sizeof($in1); $a++) { - $this->assertMessage($exp, $this->req($in1[$a]), "Test $a failed"); + // create a mock-current time + \Phake::when(Arsse::$obj)->get(\DateTimeImmutable::class)->thenReturn(new \DateTimeImmutable(self::NOW)); + // TT-RSS always responds the same regardless of success or failure + $this->assertMessage($this->respGood(['status' => "OK"]), $this->req($in)); + if (isset($c)) { + \Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, ['read' => true], $c); + } else { + \Phake::verify(Arsse::$db, \Phake::times(0))->articleMark; } - \Phake::verify(Arsse::$db, \Phake::times(0))->articleMark; - // verify the simple contexts - for ($a = 0; $a < sizeof($in2); $a++) { - $this->assertMessage($exp, $this->req($in2[$a]), "Test $a failed"); - } - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->hidden(false)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->starred(true)->hidden(false)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->label(1088)->hidden(false)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->subscription(2112)->hidden(false)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->folder(42)->hidden(false)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->folderShallow(0)->hidden(false)); - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->labelled(true)->hidden(false)); - // verify the time-based mock - $t = Date::sub("PT24H"); - for ($a = 0; $a < sizeof($in3); $a++) { - $this->assertMessage($exp, $this->req($in3[$a]), "Test $a failed"); - } - \Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], $this->equalTo((new Context)->hidden(false)->modifiedSince($t), 2)); // within two seconds + } + + public function provideMassMarkings(): iterable { + $c = (new Context)->hidden(false); + return [ + [[], null], + [['feed_id' => 0], null], + [['feed_id' => 0, 'is_cat' => true], (clone $c)->folderShallow(0)], + [['feed_id' => 0, 'is_cat' => true, 'mode' => "bogus"], (clone $c)->folderShallow(0)], + [['feed_id' => -1], (clone $c)->starred(true)], + [['feed_id' => -1, 'is_cat' => true], null], + [['feed_id' => -3], (clone $c)->modifiedSince(Date::sub("PT24H", self::NOW))], + [['feed_id' => -3, 'mode' => "1day"], (clone $c)->modifiedSince(Date::sub("PT24H", self::NOW))->notModifiedSince(Date::sub("PT24H", self::NOW))], // this is a nonsense query, but it's what TT-RSS appearsto do + [['feed_id' => -3, 'is_cat' => true], null], + [['feed_id' => -2], null], + [['feed_id' => -2, 'is_cat' => true], (clone $c)->labelled(true)], + [['feed_id' => -2, 'is_cat' => true, 'mode' => "all"], (clone $c)->labelled(true)], + [['feed_id' => -4], $c], + [['feed_id' => -4, 'is_cat' => true], null], + [['feed_id' => -6], null], + [['feed_id' => -2112], (clone $c)->label(1088)], + [['feed_id' => 42, 'is_cat' => true], (clone $c)->folder(42)], + [['feed_id' => 42, 'is_cat' => true, 'mode' => "1week"], (clone $c)->folder(42)->notModifiedSince(Date::sub("P1W", self::NOW))], + [['feed_id' => 2112], (clone $c)->subscription(2112)], + [['feed_id' => 2112, 'mode' => "2week"], (clone $c)->subscription(2112)->notModifiedSince(Date::sub("P2W", self::NOW))], + ]; } public function testRetrieveFeedList(): void { From 90034ac1f8a3a046538bf7131ffb7a69e7a21bf0 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 8 Feb 2021 19:14:11 -0500 Subject: [PATCH 170/366] Style fixes --- lib/Database.php | 11 +++---- lib/Factory.php | 2 +- lib/Misc/ValueInfo.php | 1 - lib/REST/Miniflux/V1.php | 40 ++++++++++++------------ tests/cases/Misc/TestFactory.php | 2 +- tests/cases/REST/Miniflux/TestV1.php | 11 +++---- tests/cases/REST/TestREST.php | 1 - tests/cases/REST/TinyTinyRSS/TestAPI.php | 4 +-- 8 files changed, 34 insertions(+), 38 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index db6f087d..b3b2b22a 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -755,7 +755,7 @@ class Database { } /** Lists a user's subscriptions, returning various data - * + * * Each record has the following keys: * * - "id": The numeric identifier of the subscription @@ -993,7 +993,7 @@ class Database { * - "url": The URL of the icon * - "type": The Content-Type of the icon e.g. "image/png" * - "data": The icon itself, as a binary sring; if $withData is false this will be null - * + * * If the subscription has no icon null is returned instead of an array * * @param string|null $user The user who owns the subscription being queried; using null here is supported for TT-RSS and SHOULD NOT be used elsewhere as it leaks information @@ -1031,7 +1031,7 @@ class Database { } /** Evalutes the filter rules specified for a subscription against every article associated with the subscription's feed - * + * * @param string $user The user who owns the subscription * @param integer $id The identifier of the subscription whose rules are to be evaluated */ @@ -1072,7 +1072,6 @@ class Database { } } - /** Ensures the specified subscription exists and raises an exception otherwise * * Returns an associative array containing the id of the subscription and the id of the underlying newsfeed @@ -1747,7 +1746,7 @@ class Database { } elseif (!$context->$m) { throw new Db\ExceptionInput("tooShort", ['field' => $m, 'action' => $this->caller(), 'min' => 1]); // must have at least one array element } - $columns = array_map(function ($c) use ($colDefs) { + $columns = array_map(function($c) use ($colDefs) { assert(isset($colDefs[$c]), new Exception("constantUnknown", $c)); return $colDefs[$c]; }, $columns); @@ -1758,7 +1757,7 @@ class Database { if (!$context->not->$m() || !$context->not->$m) { continue; } - $columns = array_map(function ($c) use ($colDefs) { + $columns = array_map(function($c) use ($colDefs) { assert(isset($colDefs[$c]), new Exception("constantUnknown", $c)); return $colDefs[$c]; }, $columns); diff --git a/lib/Factory.php b/lib/Factory.php index 96989025..0dfcea85 100644 --- a/lib/Factory.php +++ b/lib/Factory.php @@ -10,4 +10,4 @@ class Factory { public function get(string $class) { return new $class; } -} \ No newline at end of file +} diff --git a/lib/Misc/ValueInfo.php b/lib/Misc/ValueInfo.php index 9977e786..688a394b 100644 --- a/lib/Misc/ValueInfo.php +++ b/lib/Misc/ValueInfo.php @@ -59,7 +59,6 @@ class ValueInfo { 'float' => ["U.u", "U.u" ], ]; - public static function normalize($value, int $type, string $dateInFormat = null, $dateOutFormat = null) { $allowNull = ($type & self::M_NULL); $strict = ($type & (self::M_STRICT | self::M_DROP)); diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 097a9c86..3b192f90 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -55,7 +55,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { ]; protected const VALID_JSON = [ // user properties which map directly to Arsse user metadata are listed separately; - // not all these properties are used by our implementation, but they are treated + // not all these properties are used by our implementation, but they are treated // with the same strictness as in Miniflux to ease cross-compatibility 'url' => "string", 'username' => "string", @@ -90,7 +90,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'stylesheet' => ["stylesheet", ""], ]; /** A map between Miniflux's input properties and our input properties when modifiying feeds - * + * * Miniflux also allows changing the following properties: * * - feed_url @@ -107,7 +107,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { * or cannot be changed because feeds are deduplicated and changing * how they are fetched is not practical with our implementation. * The properties are still checked for type and syntactic validity - * where practical, on the assumption Miniflux would also reject + * where practical, on the assumption Miniflux would also reject * invalid values. */ protected const FEED_META_MAP = [ @@ -118,11 +118,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'blocklist_rules' => "block_rule", ]; protected const ARTICLE_COLUMNS = [ - "id", "url", "title", "subscription", + "id", "url", "title", "subscription", "author", "fingerprint", - "published_date", "modified_date", + "published_date", "modified_date", "starred", "unread", "hidden", - "content", "media_url", "media_type" + "content", "media_url", "media_type", ]; protected const CALLS = [ // handler method Admin Path Body Query Required fields '/categories' => [ @@ -291,7 +291,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } try { return $this->$func(...$args); - // @codeCoverageIgnoreStart + // @codeCoverageIgnoreStart } catch (Exception $e) { // if there was a REST exception return 400 return new EmptyResponse(400); @@ -348,7 +348,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse(["InvalidInputType", 'field' => $k, 'expected' => $t, 'actual' => gettype($body[$k])], 422); } elseif ( (in_array($k, ["keeplist_rules", "blocklist_rules"]) && !Rule::validate($body[$k])) - || (in_array($k, ["url", "feed_url"]) && !URL::absolute($body[$k])) + || (in_array($k, ["url", "feed_url"]) && !URL::absolute($body[$k])) || ($k === "category_id" && $body[$k] < 1) || ($k === "status" && !in_array($body[$k], ["read", "unread", "removed"])) ) { @@ -492,7 +492,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function editUser(string $user, array $data): array { // map Miniflux properties to internal metadata properties $in = []; - foreach (self::USER_META_MAP as $i => [$o,]) { + foreach (self::USER_META_MAP as $i => [$o]) { if (isset($data[$i])) { if ($i === "entry_sorting_direction") { $in[$o] = $data[$i] === "asc"; @@ -640,9 +640,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } /** Returns a useful subset of user metadata - * + * * The following keys are included: - * + * * - "num": The user's numeric ID, * - "root": The effective name of the root folder */ @@ -880,8 +880,8 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("404", 404); } return new Response([ - 'id' => $icon['id'], - 'data' => $icon['type'].";base64,".base64_encode($icon['data']), + 'id' => $icon['id'], + 'data' => $icon['type'].";base64,".base64_encode($icon['data']), 'mime_type' => $icon['type'], ]); } @@ -960,7 +960,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'url' => $entry['media_url'], 'mime_type' => $entry['media_type'] ?: "application/octet-stream", 'size' => 0, - ] + ], ]; } else { $enclosures = null; @@ -1030,7 +1030,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $out['feed'] = $this->transformFeed(Arsse::$db->subscriptionPropertiesGet(Arsse::$user->id, $out['feed_id']), $meta['num'], $meta['root'], $meta['tz']); return $out; } - + protected function getEntries(array $query): ResponseInterface { try { return new Response($this->listEntries($query, new Context)); @@ -1038,7 +1038,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("MissingCategory", 400); } } - + protected function getFeedEntries(array $path, array $query): ResponseInterface { $c = (new Context)->subscription((int) $path[1]); try { @@ -1048,7 +1048,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("404", 404); } } - + protected function getCategoryEntries(array $path, array $query): ResponseInterface { $query['category_id'] = (int) $path[1]; try { @@ -1057,7 +1057,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("404", 404); } } - + protected function getEntry(array $path): ResponseInterface { try { return new Response($this->findEntry((int) $path[1])); @@ -1065,7 +1065,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("404", 404); } } - + protected function getFeedEntry(array $path): ResponseInterface { $c = (new Context)->subscription((int) $path[1]); try { @@ -1074,7 +1074,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("404", 404); } } - + protected function getCategoryEntry(array $path): ResponseInterface { $c = new Context; if ($path[1] === "1") { diff --git a/tests/cases/Misc/TestFactory.php b/tests/cases/Misc/TestFactory.php index e400c2f7..c6940193 100644 --- a/tests/cases/Misc/TestFactory.php +++ b/tests/cases/Misc/TestFactory.php @@ -14,4 +14,4 @@ class TestFactory extends \JKingWeb\Arsse\Test\AbstractTest { $f = new Factory; $this->assertInstanceOf(\stdClass::class, $f->get(\stdClass::class)); } -} \ No newline at end of file +} diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 73b9b4a8..ad6b9886 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -12,7 +12,6 @@ use JKingWeb\Arsse\User; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Db\Transaction; use JKingWeb\Arsse\Db\ExceptionInput; -use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\REST\Miniflux\V1; use JKingWeb\Arsse\REST\Miniflux\ErrorResponse; use JKingWeb\Arsse\Feed\Exception as FeedException; @@ -46,7 +45,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ['id' => 42, 'url' => "http://example.com/42", 'title' => "Title 42", 'subscription' => 55, 'author' => "Thomas Costain", 'fingerprint' => "FINGERPRINT", 'published_date' => "2021-01-22 02:21:12", 'modified_date' => "2021-01-22 13:44:47", 'starred' => 0, 'unread' => 0, 'hidden' => 0, 'content' => "Content 42", 'media_url' => null, 'media_type' => null], ['id' => 44, 'url' => "http://example.com/44", 'title' => "Title 44", 'subscription' => 55, 'author' => null, 'fingerprint' => "FINGERPRINT", 'published_date' => "2021-01-22 02:21:12", 'modified_date' => "2021-01-22 13:44:47", 'starred' => 1, 'unread' => 1, 'hidden' => 0, 'content' => "Content 44", 'media_url' => "http://example.com/44/enclosure", 'media_type' => null], ['id' => 47, 'url' => "http://example.com/47", 'title' => "Title 47", 'subscription' => 55, 'author' => null, 'fingerprint' => "FINGERPRINT", 'published_date' => "2021-01-22 02:21:12", 'modified_date' => "2021-01-22 13:44:47", 'starred' => 0, 'unread' => 1, 'hidden' => 1, 'content' => "Content 47", 'media_url' => "http://example.com/47/enclosure", 'media_type' => ""], - ['id' => 2112, 'url' => "http://example.com/2112", 'title' => "Title 2112", 'subscription' => 55, 'author' => null, 'fingerprint' => "FINGERPRINT", 'published_date' => "2021-01-22 02:21:12", 'modified_date' => "2021-01-22 13:44:47", 'starred' => 0, 'unread' => 0, 'hidden' => 1, 'content' => "Content 2112", 'media_url' => "http://example.com/2112/enclosure", 'media_type' => "image/png"] + ['id' => 2112, 'url' => "http://example.com/2112", 'title' => "Title 2112", 'subscription' => 55, 'author' => null, 'fingerprint' => "FINGERPRINT", 'published_date' => "2021-01-22 02:21:12", 'modified_date' => "2021-01-22 13:44:47", 'starred' => 0, 'unread' => 0, 'hidden' => 1, 'content' => "Content 2112", 'media_url' => "http://example.com/2112/enclosure", 'media_type' => "image/png"], ]; protected const ENTRIES_OUT = [ ['id' => 42, 'user_id' => 42, 'feed_id' => 55, 'status' => "read", 'hash' => "FINGERPRINT", 'title' => "Title 42", 'url' => "http://example.com/42", 'comments_url' => "", 'published_at' => "2021-01-22T04:21:12+02:00", 'created_at' => "2021-01-22T15:44:47.000000+02:00", 'content' => "Content 42", 'author' => "Thomas Costain", 'share_code' => "", 'starred' => false, 'reading_time' => 0, 'enclosures' => null, 'feed' => self::FEEDS_OUT[1]], @@ -663,7 +662,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { [['crawler' => false], ['scrape' => false], true, $success], [['keeplist_rules' => ""], ['keep_rule' => ""], true, $success], [['blocklist_rules' => "ook"], ['block_rule' => "ook"], true, $success], - [['title' => "Ook!", 'crawler' => true], ['title' => "Ook!", 'scrape' => true], true, $success] + [['title' => "Ook!", 'crawler' => true], ['title' => "Ook!", 'scrape' => true], true, $success], ]; } @@ -857,8 +856,8 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { [['entry_ids' => 1, 'status' => "read"], null, new ErrorResponse(["InvalidInputType", 'field' => "entry_ids", 'expected' => "array", 'actual' => "integer"], 422)], [['entry_ids' => ["1"], 'status' => "read"], null, new ErrorResponse(["InvalidInputType", 'field' => "entry_ids", 'expected' => "integer", 'actual' => "string"], 422)], [['entry_ids' => [1], 'status' => 1], null, new ErrorResponse(["InvalidInputType", 'field' => "status", 'expected' => "string", 'actual' => "integer"], 422)], - [['entry_ids' => [0], 'status' => "read"], null, new ErrorResponse(["InvalidInputValue", 'field' => "entry_ids",], 422)], - [['entry_ids' => [1], 'status' => "reread"], null, new ErrorResponse(["InvalidInputValue", 'field' => "status",], 422)], + [['entry_ids' => [0], 'status' => "read"], null, new ErrorResponse(["InvalidInputValue", 'field' => "entry_ids"], 422)], + [['entry_ids' => [1], 'status' => "reread"], null, new ErrorResponse(["InvalidInputValue", 'field' => "status"], 422)], [['entry_ids' => [1, 2], 'status' => "read"], ['read' => true, 'hidden' => false], new EmptyResponse(204)], [['entry_ids' => [1, 2], 'status' => "unread"], ['read' => false, 'hidden' => false], new EmptyResponse(204)], [['entry_ids' => [1, 2], 'status' => "removed"], ['read' => true, 'hidden' => true], new EmptyResponse(204)], @@ -1009,4 +1008,4 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertException("doesNotExist", "User", "ExceptionConflict"); V1::tokenList("john.doe@example.com"); } -} \ No newline at end of file +} diff --git a/tests/cases/REST/TestREST.php b/tests/cases/REST/TestREST.php index 18642150..a7eb83e9 100644 --- a/tests/cases/REST/TestREST.php +++ b/tests/cases/REST/TestREST.php @@ -9,7 +9,6 @@ namespace JKingWeb\Arsse\TestCase\REST; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\User; use JKingWeb\Arsse\REST; -use JKingWeb\Arsse\REST\Handler; use JKingWeb\Arsse\REST\Exception501; use JKingWeb\Arsse\REST\NextcloudNews\V1_2 as NCN; use JKingWeb\Arsse\REST\TinyTinyRSS\API as TTRSS; diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index e79a2f65..139f7dcf 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -19,8 +19,8 @@ use JKingWeb\Arsse\REST\TinyTinyRSS\API; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\EmptyResponse; -/** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\API +/** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\API * @covers \JKingWeb\Arsse\REST\TinyTinyRSS\Exception */ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { protected const NOW = "2020-12-21T23:09:17.189065Z"; @@ -1317,7 +1317,7 @@ LONG_STRING; // create a mock-current time \Phake::when(Arsse::$obj)->get(\DateTimeImmutable::class)->thenReturn(new \DateTimeImmutable(self::NOW)); // TT-RSS always responds the same regardless of success or failure - $this->assertMessage($this->respGood(['status' => "OK"]), $this->req($in)); + $this->assertMessage($this->respGood(['status' => "OK"]), $this->req($in)); if (isset($c)) { \Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, ['read' => true], $c); } else { From dad74c2616dd71874ec07c04d61e0e7770aa314d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 8 Feb 2021 23:51:40 -0500 Subject: [PATCH 171/366] Implement Fever icons --- lib/REST/Fever/API.php | 29 ++++++++++++++++++++--------- tests/cases/REST/Fever/TestAPI.php | 23 ++++++++++++++++------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/lib/REST/Fever/API.php b/lib/REST/Fever/API.php index 8c94a8dd..8f43d450 100644 --- a/lib/REST/Fever/API.php +++ b/lib/REST/Fever/API.php @@ -150,14 +150,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { $out['feeds_groups'] = $this->getRelationships(); } 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, - ], - ]; + $out['favicons'] = $this->getIcons(); } if ($G['items']) { $out['items'] = $this->getItems($G); @@ -333,7 +326,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { foreach (arsse::$db->subscriptionList(Arsse::$user->id) as $sub) { $out[] = [ 'id' => (int) $sub['id'], - 'favicon_id' => 0, // TODO: implement favicons + 'favicon_id' => (int) $sub['icon_id'], 'title' => (string) $sub['title'], 'url' => $sub['url'], 'site_url' => $sub['source'], @@ -344,6 +337,24 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { return $out; } + protected function getIcons(): array { + $out = [ + [ + 'id' => 0, + 'data' => self::GENERIC_ICON_TYPE.",".self::GENERIC_ICON_DATA, + ], + ]; + foreach (Arsse::$db->iconList(Arsse::$user->id) as $icon) { + if ($icon['data']) { + $out[] = [ + 'id' => (int) $icon['id'], + 'data' => ($icon['type'] ?: "application/octet-stream").";base64,".base64_encode($icon['data']), + ]; + } + } + return $out; + } + protected function getGroups(): array { $out = []; foreach (Arsse::$db->tagList(Arsse::$user->id) as $member) { diff --git a/tests/cases/REST/Fever/TestAPI.php b/tests/cases/REST/Fever/TestAPI.php index 2d41dd15..eb44d660 100644 --- a/tests/cases/REST/Fever/TestAPI.php +++ b/tests/cases/REST/Fever/TestAPI.php @@ -273,9 +273,9 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { public function testListFeeds(): void { \Phake::when(Arsse::$db)->subscriptionList(Arsse::$user->id)->thenReturn(new Result([ - ['id' => 1, 'feed' => 5, 'title' => "Ankh-Morpork News", 'url' => "http://example.com/feed", 'source' => "http://example.com/", 'edited' => "2019-01-01 21:12:00", 'icon_url' => "http://example.com/favicon.ico"], - ['id' => 2, 'feed' => 9, 'title' => "Ook, Ook Eek Ook!", 'url' => "http://example.net/feed", 'source' => "http://example.net/", 'edited' => "1988-06-24 12:21:00", 'icon_url' => ""], - ['id' => 3, 'feed' => 1, 'title' => "The Last Soul", 'url' => "http://example.org/feed", 'source' => "http://example.org/", 'edited' => "1991-08-12 03:22:00", 'icon_url' => "http://example.org/favicon.ico"], + ['id' => 1, 'feed' => 5, 'title' => "Ankh-Morpork News", 'url' => "http://example.com/feed", 'source' => "http://example.com/", 'edited' => "2019-01-01 21:12:00", 'icon_url' => "http://example.com/favicon.ico", 'icon_id' => 42], + ['id' => 2, 'feed' => 9, 'title' => "Ook, Ook Eek Ook!", 'url' => "http://example.net/feed", 'source' => "http://example.net/", 'edited' => "1988-06-24 12:21:00", 'icon_url' => "", 'icon_id' => null], + ['id' => 3, 'feed' => 1, 'title' => "The Last Soul", 'url' => "http://example.org/feed", 'source' => "http://example.org/", 'edited' => "1991-08-12 03:22:00", 'icon_url' => "http://example.org/favicon.ico", 'icon_id' => 42], ])); \Phake::when(Arsse::$db)->tagSummarize(Arsse::$user->id)->thenReturn(new Result([ ['id' => 1, 'name' => "Fascinating", 'subscription' => 1], @@ -285,9 +285,9 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { ])); $exp = new JsonResponse([ 'feeds' => [ - ['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' => 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")], + ['id' => 1, 'favicon_id' => 42, 'title' => "Ankh-Morpork News", 'url' => "http://example.com/feed", 'site_url' => "http://example.com/", 'is_spark' => 0, 'last_updated_on_time' => strtotime("2019-01-01T21:12:00Z")], + ['id' => 2, 'favicon_id' => 0, 'title' => "Ook, Ook Eek Ook!", 'url' => "http://example.net/feed", 'site_url' => "http://example.net/", 'is_spark' => 0, 'last_updated_on_time' => strtotime("1988-06-24T12:21:00Z")], + ['id' => 3, 'favicon_id' => 42, '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"], @@ -492,8 +492,17 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { public function testListFeedIcons(): void { $iconType = (new \ReflectionClassConstant(API::class, "GENERIC_ICON_TYPE"))->getValue(); $iconData = (new \ReflectionClassConstant(API::class, "GENERIC_ICON_DATA"))->getValue(); + \Phake::when(Arsse::$db)->iconList->thenReturn(new Result($this->v([ + ['id' => 42, 'type' => "image/svg+xml", 'data' => ""], + ['id' => 44, 'type' => null, 'data' => "IMAGE DATA"], + ['id' => 47, 'type' => null, 'data' => null], + ]))); $act = $this->h->dispatch($this->req("api&favicons")); - $exp = new JsonResponse(['favicons' => [['id' => 0, 'data' => $iconType.",".$iconData]]]); + $exp = new JsonResponse(['favicons' => [ + ['id' => 0, 'data' => $iconType.",".$iconData], + ['id' => 42, 'data' => "image/svg+xml;base64,PHN2Zy8+"], + ['id' => 44, 'data' => "application/octet-stream;base64,SU1BR0UgREFUQQ=="], + ]]); $this->assertMessage($exp, $act); } From 29761d767a50d08f3e8c2fc8a416df4cab8400b3 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 8 Feb 2021 23:52:13 -0500 Subject: [PATCH 172/366] Update documentation --- CHANGELOG | 2 ++ docs/en/030_Supported_Protocols/030_Fever.md | 1 - lib/REST/Miniflux/V1.php | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8580d40b..f41bf0ff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,8 @@ Version 0.9.0 (????-??-??) New features: - Support for the Miniflux protocol (see manual for details) +- Support for API level 15 of Tiny Tiny RSS +- Support for feed icons in Fever Bug fixes: - Use icons specified in Atom feeds when available diff --git a/docs/en/030_Supported_Protocols/030_Fever.md b/docs/en/030_Supported_Protocols/030_Fever.md index 094a909f..846d7e1f 100644 --- a/docs/en/030_Supported_Protocols/030_Fever.md +++ b/docs/en/030_Supported_Protocols/030_Fever.md @@ -23,7 +23,6 @@ The Fever protocol is incomplete, unusual, _and_ a product of proprietary softwa - All feeds are considered "Kindling" - The "Hot Links" feature is not implemented; when requested, an empty array will be returned. As there is no way to classify a feed as a "Spark" in the protocol itself and no documentation exists on how link temperature was calculated, an implementation is unlikely to appear in the future -- Favicons are not currently supported; all feeds have a simple blank image as their favicon unless the client finds the icons itself # Special considerations diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 3b192f90..4e4c959d 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -30,7 +30,7 @@ use Laminas\Diactoros\Response\TextResponse as GenericResponse; use Laminas\Diactoros\Uri; class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { - public const VERSION = "2.0.26"; + public const VERSION = "2.0.28"; protected const ACCEPTED_TYPES_OPML = ["application/xml", "text/xml", "text/x-opml"]; protected const ACCEPTED_TYPES_JSON = ["application/json"]; From 687995c4972e79b67a758373224af017eebb0f19 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 9 Feb 2021 00:33:41 -0500 Subject: [PATCH 173/366] More potential Miniflux Web clints --- docs/en/040_Compatible_Clients.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/en/040_Compatible_Clients.md b/docs/en/040_Compatible_Clients.md index f4585c9b..bc089e24 100644 --- a/docs/en/040_Compatible_Clients.md +++ b/docs/en/040_Compatible_Clients.md @@ -19,6 +19,24 @@ The Arsse does not at this time have any first party clients. However, because T Web + + maxiflux + + ✔ + ✘ + ✘ + ✘ + + + + Miniflux Reader + + ✔ + ✘ + ✘ + ✘ + + reminiflux From 9ad4a37ddfdc84e93139d7e6448d5f46e47597cd Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 9 Feb 2021 09:26:12 -0500 Subject: [PATCH 174/366] Tests and fixes for Miniflux with PDO --- lib/REST/Miniflux/V1.php | 6 +++--- tests/cases/REST/Miniflux/PDO/TestV1.php | 13 +++++++++++++ tests/phpunit.dist.xml | 1 + 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 tests/cases/REST/Miniflux/PDO/TestV1.php diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 4e4c959d..1a529547 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -880,7 +880,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("404", 404); } return new Response([ - 'id' => $icon['id'], + 'id' => (int) $icon['id'], 'data' => $icon['type'].";base64,".base64_encode($icon['data']), 'mime_type' => $icon['type'], ]); @@ -954,9 +954,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if ($entry['media_url']) { $enclosures = [ [ - 'id' => $entry['id'], // NOTE: We don't have IDs for enclosures, but we also only have one enclosure per entry, so we can just re-use the same ID + 'id' => (int) $entry['id'], // NOTE: We don't have IDs for enclosures, but we also only have one enclosure per entry, so we can just re-use the same ID 'user_id' => $uid, - 'entry_id' => $entry['id'], + 'entry_id' => (int) $entry['id'], 'url' => $entry['media_url'], 'mime_type' => $entry['media_type'] ?: "application/octet-stream", 'size' => 0, diff --git a/tests/cases/REST/Miniflux/PDO/TestV1.php b/tests/cases/REST/Miniflux/PDO/TestV1.php new file mode 100644 index 00000000..977ffa4e --- /dev/null +++ b/tests/cases/REST/Miniflux/PDO/TestV1.php @@ -0,0 +1,13 @@ + + * @group optional */ +class TestV1 extends \JKingWeb\Arsse\TestCase\REST\Miniflux\TestV1 { + use \JKingWeb\Arsse\Test\PDOTest; +} diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml index 3d576064..99831a45 100644 --- a/tests/phpunit.dist.xml +++ b/tests/phpunit.dist.xml @@ -118,6 +118,7 @@ cases/REST/Miniflux/TestErrorResponse.php cases/REST/Miniflux/TestStatus.php cases/REST/Miniflux/TestV1.php + cases/REST/Miniflux/PDO/TestV1.php cases/REST/NextcloudNews/TestVersions.php From a760bf2ded3eba8c671a2d48f6487aaaaf177cfe Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 9 Feb 2021 09:37:31 -0500 Subject: [PATCH 175/366] Implement "t" and "f" booleans in TT-RSS --- CHANGELOG | 1 + lib/REST/AbstractHandler.php | 13 --- lib/REST/NextcloudNews/V1_2.php | 12 +++ lib/REST/TinyTinyRSS/API.php | 122 +++++++++++++---------- tests/cases/REST/TinyTinyRSS/TestAPI.php | 4 +- 5 files changed, 86 insertions(+), 66 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f41bf0ff..ba7040e8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ Bug fixes: - Do not return null as subscription unread count - Explicitly forbid U+003A COLON and control characters in usernames, for compatibility with RFC 7617 +- Accept "t" and "f" as booleans in Tiny Tiny RSS Version 0.8.5 (2020-10-27) ========================== diff --git a/lib/REST/AbstractHandler.php b/lib/REST/AbstractHandler.php index 7103c34a..2dadfa91 100644 --- a/lib/REST/AbstractHandler.php +++ b/lib/REST/AbstractHandler.php @@ -8,7 +8,6 @@ namespace JKingWeb\Arsse\REST; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Misc\Date; -use JKingWeb\Arsse\Misc\ValueInfo; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; @@ -46,16 +45,4 @@ abstract class AbstractHandler implements Handler { } return $data; } - - protected function normalizeInput(array $data, array $types, string $dateFormat = null, int $mode = 0): array { - $out = []; - foreach ($types as $key => $type) { - if (isset($data[$key])) { - $out[$key] = ValueInfo::normalize($data[$key], $type | $mode, $dateFormat); - } else { - $out[$key] = null; - } - } - return $out; - } } diff --git a/lib/REST/NextcloudNews/V1_2.php b/lib/REST/NextcloudNews/V1_2.php index 57d1e732..2b14cbd5 100644 --- a/lib/REST/NextcloudNews/V1_2.php +++ b/lib/REST/NextcloudNews/V1_2.php @@ -136,6 +136,18 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return implode("/", $path); } + protected function normalizeInput(array $data, array $types, string $dateFormat = null, int $mode = 0): array { + $out = []; + foreach ($types as $key => $type) { + if (isset($data[$key])) { + $out[$key] = ValueInfo::normalize($data[$key], $type | $mode, $dateFormat); + } else { + $out[$key] = null; + } + } + return $out; + } + protected function chooseCall(string $url, string $method) { // // normalize the URL path: change any IDs to 1 for easier comparison $url = $this->normalizePathIds($url); diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index 0d4d12ae..11b983d3 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -12,7 +12,7 @@ use JKingWeb\Arsse\Service; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Context\Context; use JKingWeb\Arsse\Misc\Date; -use JKingWeb\Arsse\Misc\ValueInfo; +use JKingWeb\Arsse\Misc\ValueInfo as V; use JKingWeb\Arsse\AbstractException; use JKingWeb\Arsse\ExceptionType; use JKingWeb\Arsse\Db\ExceptionInput; @@ -46,41 +46,41 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // valid input protected const ACCEPTED_TYPES = ["application/json", "text/json"]; protected const VALID_INPUT = [ - 'op' => ValueInfo::T_STRING, // the function ("operation") to perform - 'sid' => ValueInfo::T_STRING, // session ID - 'seq' => ValueInfo::T_INT, // request number from client - 'user' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // user name for `login` - 'password' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // password for `login` or remote password for `subscribeToFeed` - 'include_empty' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include empty items in `getFeedTree` and `getCategories` - 'unread_only' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to exclude items without unread articles in `getCategories` and `getFeeds` - 'enable_nested' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to NOT show subcategories in `getCategories - 'include_nested' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include subcategories in `getFeeds` and the articles thereof in `getHeadlines` - 'caption' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // name for categories, feed, and labels - 'parent_id' => ValueInfo::T_INT, // parent category for `addCategory` and `moveCategory` - 'category_id' => ValueInfo::T_INT, // parent category for `subscribeToFeed` and `moveFeed`, and subject for category-modification functions - 'cat_id' => ValueInfo::T_INT, // parent category for `getFeeds` - 'label_id' => ValueInfo::T_INT, // label ID in label-related functions - 'feed_url' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // URL of feed in `subscribeToFeed` - 'login' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // remote user name in `subscribeToFeed` - 'feed_id' => ValueInfo::T_INT, // feed, label, or category ID for various functions - 'is_cat' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether 'feed_id' refers to a category - 'article_id' => ValueInfo::T_MIXED, // single article ID in `getLabels`; one or more (comma-separated) article IDs in `getArticle` - 'article_ids' => ValueInfo::T_STRING, // one or more (comma-separated) article IDs in `updateArticle` and `setArticleLabel` - 'assign' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to assign or clear (false) a label in `setArticleLabel` - 'limit' => ValueInfo::T_INT, // maximum number of records returned in `getFeeds`, `getHeadlines`, and `getCompactHeadlines` - 'offset' => ValueInfo::T_INT, // number of records to skip in `getFeeds`, for pagination - 'skip' => ValueInfo::T_INT, // number of records to skip in `getHeadlines` and `getCompactHeadlines`, for pagination - 'show_excerpt' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include article excerpts in `getHeadlines` - 'show_content' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include article content in `getHeadlines` - 'include_attachments' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include article enclosures in `getHeadlines` - 'view_mode' => ValueInfo::T_STRING, // various filters for `getHeadlines` - 'since_id' => ValueInfo::T_INT, // cut-off article ID for `getHeadlines` and `getCompactHeadlines; returns only higher article IDs when specified - 'order_by' => ValueInfo::T_STRING, // sort order for `getHeadlines` - 'include_header' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to attach a header to the results of `getHeadlines` - 'search' => ValueInfo::T_STRING, // search string for `getHeadlines` - 'field' => ValueInfo::T_INT, // which state to change in `updateArticle` - 'mode' => ValueInfo::T_MIXED, // whether to set, clear, or toggle the selected state in `updateArticle` (integer), or whether to ignore a certain recent timeframe in `catchupFeed` (string) - 'data' => ValueInfo::T_STRING, // note text in `updateArticle` if setting a note + 'op' => V::T_STRING, // the function ("operation") to perform + 'sid' => V::T_STRING, // session ID + 'seq' => V::T_INT, // request number from client + 'user' => V::T_STRING | V::M_STRICT, // user name for `login` + 'password' => V::T_STRING | V::M_STRICT, // password for `login` or remote password for `subscribeToFeed` + 'include_empty' => V::T_BOOL | V::M_DROP, // whether to include empty items in `getFeedTree` and `getCategories` + 'unread_only' => V::T_BOOL | V::M_DROP, // whether to exclude items without unread articles in `getCategories` and `getFeeds` + 'enable_nested' => V::T_BOOL | V::M_DROP, // whether to NOT show subcategories in `getCategories + 'include_nested' => V::T_BOOL | V::M_DROP, // whether to include subcategories in `getFeeds` and the articles thereof in `getHeadlines` + 'caption' => V::T_STRING | V::M_STRICT, // name for categories, feed, and labels + 'parent_id' => V::T_INT, // parent category for `addCategory` and `moveCategory` + 'category_id' => V::T_INT, // parent category for `subscribeToFeed` and `moveFeed`, and subject for category-modification functions + 'cat_id' => V::T_INT, // parent category for `getFeeds` + 'label_id' => V::T_INT, // label ID in label-related functions + 'feed_url' => V::T_STRING | V::M_STRICT, // URL of feed in `subscribeToFeed` + 'login' => V::T_STRING | V::M_STRICT, // remote user name in `subscribeToFeed` + 'feed_id' => V::T_INT, // feed, label, or category ID for various functions + 'is_cat' => V::T_BOOL | V::M_DROP, // whether 'feed_id' refers to a category + 'article_id' => V::T_MIXED, // single article ID in `getLabels`; one or more (comma-separated) article IDs in `getArticle` + 'article_ids' => V::T_STRING, // one or more (comma-separated) article IDs in `updateArticle` and `setArticleLabel` + 'assign' => V::T_BOOL | V::M_DROP, // whether to assign or clear (false) a label in `setArticleLabel` + 'limit' => V::T_INT, // maximum number of records returned in `getFeeds`, `getHeadlines`, and `getCompactHeadlines` + 'offset' => V::T_INT, // number of records to skip in `getFeeds`, for pagination + 'skip' => V::T_INT, // number of records to skip in `getHeadlines` and `getCompactHeadlines`, for pagination + 'show_excerpt' => V::T_BOOL | V::M_DROP, // whether to include article excerpts in `getHeadlines` + 'show_content' => V::T_BOOL | V::M_DROP, // whether to include article content in `getHeadlines` + 'include_attachments' => V::T_BOOL | V::M_DROP, // whether to include article enclosures in `getHeadlines` + 'view_mode' => V::T_STRING, // various filters for `getHeadlines` + 'since_id' => V::T_INT, // cut-off article ID for `getHeadlines` and `getCompactHeadlines; returns only higher article IDs when specified + 'order_by' => V::T_STRING, // sort order for `getHeadlines` + 'include_header' => V::T_BOOL | V::M_DROP, // whether to attach a header to the results of `getHeadlines` + 'search' => V::T_STRING, // search string for `getHeadlines` + 'field' => V::T_INT, // which state to change in `updateArticle` + 'mode' => V::T_MIXED, // whether to set, clear, or toggle the selected state in `updateArticle` (integer), or whether to ignore a certain recent timeframe in `catchupFeed` (string) + 'data' => V::T_STRING, // note text in `updateArticle` if setting a note ]; protected const VIEW_MODES = ["all_articles", "adaptive", "unread", "marked", "has_note", "published"]; // generic error construct @@ -156,6 +156,26 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } } + protected function normalizeInput(array $data): array { + $out = []; + foreach (self::VALID_INPUT as $key => $type) { + if (isset($data[$key])) { + // TT-RSS accepts "t" and "f" as booleans + if ($type === V::T_BOOL | V::M_DROP) { + if ($data[$key] === "t") { + $data[$key] = true; + } elseif ($data[$key] === "f") { + $data[$key] = false; + } + } + $out[$key] = V::normalize($data[$key], $type, "unix"); + } else { + $out[$key] = null; + } + } + return $out; + } + protected function resumeSession(string $id): bool { // if HTTP authentication was successful and sessions are not enforced, proceed unconditionally if (isset(Arsse::$user->id) && !Arsse::$conf->userSessionEnforced) { @@ -589,7 +609,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opRemoveCategory(array $data) { - if (!ValueInfo::id($data['category_id'])) { + if (!V::id($data['category_id'])) { // if the folder is invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -603,7 +623,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opMoveCategory(array $data) { - if (!ValueInfo::id($data['category_id']) || !ValueInfo::id($data['parent_id'], true)) { + if (!V::id($data['category_id']) || !V::id($data['parent_id'], true)) { // if the folder or parent is invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -620,8 +640,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opRenameCategory(array $data) { - $info = ValueInfo::str($data['caption']); - if (!ValueInfo::id($data['category_id']) || !($info & ValueInfo::VALID) || ($info & ValueInfo::EMPTY) || ($info & ValueInfo::WHITE)) { + $info = V::str($data['caption']); + if (!V::id($data['category_id']) || !($info & V::VALID) || ($info & V::EMPTY) || ($info & V::WHITE)) { // if the folder or its new name are invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -646,7 +666,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { $offset = $data['offset'] ?? 0; $nested = $data['include_nested'] ?? false; // if a special category was selected, nesting does not apply - if (!ValueInfo::id($cat)) { + if (!V::id($cat)) { $nested = false; // if the All, Special, or Labels category was selected, pagination also does not apply if (in_array($cat, [self::CAT_ALL, self::CAT_SPECIAL, self::CAT_LABELS])) { @@ -820,7 +840,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opSubscribeToFeed(array $data): array { - if (!$data['feed_url'] || !ValueInfo::id($data['category_id'], true)) { + if (!$data['feed_url'] || !V::id($data['category_id'], true)) { // if the feed URL or the category ID is invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -887,7 +907,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opMoveFeed(array $data) { - if (!ValueInfo::id($data['feed_id']) || !isset($data['category_id']) || !ValueInfo::id($data['category_id'], true)) { + if (!V::id($data['feed_id']) || !isset($data['category_id']) || !V::id($data['category_id'], true)) { // if the feed or folder is invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -904,8 +924,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opRenameFeed(array $data) { - $info = ValueInfo::str($data['caption']); - if (!ValueInfo::id($data['feed_id']) || !($info & ValueInfo::VALID) || ($info & ValueInfo::EMPTY) || ($info & ValueInfo::WHITE)) { + $info = V::str($data['caption']); + if (!V::id($data['feed_id']) || !($info & V::VALID) || ($info & V::EMPTY) || ($info & V::WHITE)) { // if the feed ID or name is invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -922,7 +942,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opUpdateFeed(array $data): array { - if (!isset($data['feed_id']) || !ValueInfo::id($data['feed_id'])) { + if (!isset($data['feed_id']) || !V::id($data['feed_id'])) { // if the feed is invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -935,7 +955,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function labelIn($id, bool $throw = true): int { - if (!(ValueInfo::int($id) & ValueInfo::NEG) || $id > (-1 - self::LABEL_OFFSET)) { + if (!(V::int($id) & V::NEG) || $id > (-1 - self::LABEL_OFFSET)) { if ($throw) { throw new Exception("INCORRECT_USAGE"); } else { @@ -951,7 +971,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { public function opGetLabels(array $data): array { // this function doesn't complain about invalid article IDs - $article = ValueInfo::id($data['article_id']) ? $data['article_id'] : 0; + $article = V::id($data['article_id']) ? $data['article_id'] : 0; try { $list = $article ? Arsse::$db->articleLabelsGet(Arsse::$user->id, $article) : []; } catch (ExceptionInput $e) { @@ -1112,8 +1132,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { public function opUpdateArticle(array $data): array { // normalize input - $articles = array_filter(ValueInfo::normalize(explode(",", (string) $data['article_ids']), ValueInfo::T_INT | ValueInfo::M_ARRAY), [ValueInfo::class, "id"]); - $data['mode'] = ValueInfo::normalize($data['mode'], ValueInfo::T_INT); + $articles = array_filter(V::normalize(explode(",", (string) $data['article_ids']), V::T_INT | V::M_ARRAY), [V::class, "id"]); + $data['mode'] = V::normalize($data['mode'], V::T_INT); if (!$articles) { // if there are no valid articles this is an error throw new Exception("INCORRECT_USAGE"); @@ -1185,7 +1205,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { public function opGetArticle(array $data): array { // normalize input - $articles = array_filter(ValueInfo::normalize(explode(",", (string) $data['article_id']), ValueInfo::T_INT | ValueInfo::M_ARRAY), [ValueInfo::class, "id"]); + $articles = array_filter(V::normalize(explode(",", (string) $data['article_id']), V::T_INT | V::M_ARRAY), [V::class, "id"]); if (!$articles) { // if there are no valid articles this is an error throw new Exception("INCORRECT_USAGE"); diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index 139f7dcf..cac71ddf 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -1333,7 +1333,7 @@ LONG_STRING; [['feed_id' => 0, 'is_cat' => true], (clone $c)->folderShallow(0)], [['feed_id' => 0, 'is_cat' => true, 'mode' => "bogus"], (clone $c)->folderShallow(0)], [['feed_id' => -1], (clone $c)->starred(true)], - [['feed_id' => -1, 'is_cat' => true], null], + [['feed_id' => -1, 'is_cat' => "t"], null], [['feed_id' => -3], (clone $c)->modifiedSince(Date::sub("PT24H", self::NOW))], [['feed_id' => -3, 'mode' => "1day"], (clone $c)->modifiedSince(Date::sub("PT24H", self::NOW))->notModifiedSince(Date::sub("PT24H", self::NOW))], // this is a nonsense query, but it's what TT-RSS appearsto do [['feed_id' => -3, 'is_cat' => true], null], @@ -1342,7 +1342,7 @@ LONG_STRING; [['feed_id' => -2, 'is_cat' => true, 'mode' => "all"], (clone $c)->labelled(true)], [['feed_id' => -4], $c], [['feed_id' => -4, 'is_cat' => true], null], - [['feed_id' => -6], null], + [['feed_id' => -6, 'is_cat' => "f"], null], [['feed_id' => -2112], (clone $c)->label(1088)], [['feed_id' => 42, 'is_cat' => true], (clone $c)->folder(42)], [['feed_id' => 42, 'is_cat' => true, 'mode' => "1week"], (clone $c)->folder(42)->notModifiedSince(Date::sub("P1W", self::NOW))], From b7c7915a653a43f18ffd634cf2bf498408359733 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 9 Feb 2021 10:05:44 -0500 Subject: [PATCH 176/366] Enforce admin rquirements in NCNv1 --- CHANGELOG | 4 +++ .../010_Nextcloud_News.md | 1 - lib/REST/NextcloudNews/V1_2.php | 12 ++++++++ tests/cases/REST/NextcloudNews/TestV1_2.php | 29 +++++++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index ba7040e8..0e7cdc67 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,10 @@ Bug fixes: compatibility with RFC 7617 - Accept "t" and "f" as booleans in Tiny Tiny RSS +Changes: +- Administrator account requirements for Nextcloud News functionality are + now enforced + Version 0.8.5 (2020-10-27) ========================== diff --git a/docs/en/030_Supported_Protocols/010_Nextcloud_News.md b/docs/en/030_Supported_Protocols/010_Nextcloud_News.md index a2c34d04..17f5d2df 100644 --- a/docs/en/030_Supported_Protocols/010_Nextcloud_News.md +++ b/docs/en/030_Supported_Protocols/010_Nextcloud_News.md @@ -24,7 +24,6 @@ It allows organizing newsfeeds into single-level folders, and supports a wide ra - When marking articles as starred the feed ID is ignored, as they are not needed to establish uniqueness - The feed updater ignores the `userId` parameter: feeds in The Arsse are deduplicated, and have no owner - The `/feeds/all` route lists only feeds which should be checked for updates, and it also returns all `userId` attributes as empty strings: feeds in The Arsse are deduplicated, and have no owner -- The API's "updater" routes do not require administrator priviledges as The Arsse has no concept of user classes - The "updater" console commands mentioned in the protocol specification are not implemented, as The Arsse does not implement the required Nextcloud subsystems - The `lastLoginTimestamp` attribute of the user metadata is always the current time: The Arsse's implementation of the protocol is fully stateless - Syntactically invalid JSON input will yield a `400 Bad Request` response instead of falling back to GET parameters diff --git a/lib/REST/NextcloudNews/V1_2.php b/lib/REST/NextcloudNews/V1_2.php index 2b14cbd5..984491ab 100644 --- a/lib/REST/NextcloudNews/V1_2.php +++ b/lib/REST/NextcloudNews/V1_2.php @@ -360,6 +360,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // return list of feeds which should be refreshed protected function feedListStale(array $url, array $data): ResponseInterface { + if (!$this->isAdmin()) { + return new EmptyResponse(403); + } // list stale feeds which should be checked for updates $feeds = Arsse::$db->feedListStale(); $out = []; @@ -372,6 +375,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // refresh a feed protected function feedUpdate(array $url, array $data): ResponseInterface { + if (!$this->isAdmin()) { + return new EmptyResponse(403); + } try { Arsse::$db->feedUpdate($data['feedId']); } catch (ExceptionInput $e) { @@ -667,11 +673,17 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function cleanupBefore(array $url, array $data): ResponseInterface { + if (!$this->isAdmin()) { + return new EmptyResponse(403); + } Service::cleanupPre(); return new EmptyResponse(204); } protected function cleanupAfter(array $url, array $data): ResponseInterface { + if (!$this->isAdmin()) { + return new EmptyResponse(403); + } Service::cleanupPost(); return new EmptyResponse(204); } diff --git a/tests/cases/REST/NextcloudNews/TestV1_2.php b/tests/cases/REST/NextcloudNews/TestV1_2.php index 7b4ce328..3d5a342a 100644 --- a/tests/cases/REST/NextcloudNews/TestV1_2.php +++ b/tests/cases/REST/NextcloudNews/TestV1_2.php @@ -317,6 +317,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { // create a mock user manager Arsse::$user = \Phake::mock(User::class); Arsse::$user->id = "john.doe@example.com"; + \Phake::when(Arsse::$user)->propertiesGet->thenReturn(['admin' => true]); // create a mock database interface Arsse::$db = \Phake::mock(Database::class); $this->transaction = \Phake::mock(Transaction::class); @@ -629,6 +630,13 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertMessage($exp, $this->req("GET", "/feeds/all")); } + public function testListStaleFeedsWithoutAuthority(): void { + \Phake::when(Arsse::$user)->propertiesGet->thenReturn(['admin' => false]); + $exp = new EmptyResponse(403); + $this->assertMessage($exp, $this->req("GET", "/feeds/all")); + \Phake::verify(Arsse::$db, \Phake::times(0))->feedListStale; + } + public function testUpdateAFeed(): void { $in = [ ['feedId' => 42], // valid @@ -650,6 +658,13 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertMessage($exp, $this->req("GET", "/feeds/update", json_encode($in[4]))); } + public function testUpdateAFeedWithoutAuthority(): void { + \Phake::when(Arsse::$user)->propertiesGet->thenReturn(['admin' => false]); + $exp = new EmptyResponse(403); + $this->assertMessage($exp, $this->req("GET", "/feeds/update", ['feedId' => 42])); + \Phake::verify(Arsse::$db, \Phake::times(0))->feedUpdate; + } + /** @dataProvider provideArticleQueries */ public function testListArticles(string $url, array $in, Context $c, $out, ResponseInterface $exp): void { if ($out instanceof \Exception) { @@ -849,6 +864,13 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->feedCleanup(); } + public function testCleanUpBeforeUpdateWithoutAuthority(): void { + \Phake::when(Arsse::$user)->propertiesGet->thenReturn(['admin' => false]); + $exp = new EmptyResponse(403); + $this->assertMessage($exp, $this->req("GET", "/cleanup/before-update")); + \Phake::verify(Arsse::$db, \Phake::times(0))->feedCleanup; + } + public function testCleanUpAfterUpdate(): void { \Phake::when(Arsse::$db)->articleCleanup()->thenReturn(true); $exp = new EmptyResponse(204); @@ -856,6 +878,13 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->articleCleanup(); } + public function testCleanUpAfterUpdateWithoutAuthority(): void { + \Phake::when(Arsse::$user)->propertiesGet->thenReturn(['admin' => false]); + $exp = new EmptyResponse(403); + $this->assertMessage($exp, $this->req("GET", "/cleanup/after-update")); + \Phake::verify(Arsse::$db, \Phake::times(0))->feedCleanup; + } + public function testQueryTheUserStatus(): void { $act = $this->req("GET", "/user"); $exp = new Response([ From 68422390dae37a7b4968ff8c7771b849e5c4ed2f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 10 Feb 2021 11:24:01 -0500 Subject: [PATCH 177/366] Implement CLI for user metadata --- CHANGELOG | 1 + lib/CLI.php | 112 ++++++++++++++++++++++++++++++++++-- lib/User.php | 4 +- tests/cases/CLI/TestCLI.php | 61 ++++++++++++++++++++ tests/lib/AbstractTest.php | 1 + 5 files changed, 172 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0e7cdc67..781625de 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ New features: - Support for the Miniflux protocol (see manual for details) - Support for API level 15 of Tiny Tiny RSS - Support for feed icons in Fever +- Command-line functionality for managing user metadata Bug fixes: - Use icons specified in Atom feeds when available diff --git a/lib/CLI.php b/lib/CLI.php index bc96f468..d40f683e 100644 --- a/lib/CLI.php +++ b/lib/CLI.php @@ -17,8 +17,11 @@ Usage: arsse.php feed refresh arsse.php conf save-defaults [] arsse.php user [list] - arsse.php user add [] + arsse.php user add [] [--admin] arsse.php user remove + arsse.php user show + arsse.php user set + arsse.php user unset arsse.php user set-pass [] [--oldpass=] [--fever] arsse.php user unset-pass @@ -63,11 +66,13 @@ Commands: Prints a list of all existing users, one per line. - user add [] + user add [] [--admin] Adds the user specified by , with the provided password . If no password is specified, a random password will be - generated and printed to standard output. + generated and printed to standard output. The --admin option will make + the user an administrator, which allows them to manage users via the + Miniflux protocol, among other things. user remove @@ -76,6 +81,22 @@ Commands: which the user was subscribed will be retained and refreshed until the configured retention time elapses. + user show + + Displays the metadata of a user in a basic tabular format. See below for + details on the various properties displayed. + + user set + + Sets a user's metadata proprty to the supplied value. See below for + details on the various properties available. + + user unset + + Sets a user's metadata proprty to its default value. See below for + details on the various properties available. What the default value + for a property evaluates to depends on which protocol is used. + user set-pass [] Changes 's password to . If no password is specified, @@ -128,6 +149,65 @@ Commands: The --flat option can be used to omit folders from the export. Some OPML implementations may not support folders, or arbitrary nesting; this option may be used when planning to import into such software. + +User metadata: + + User metadata is primary used by the Miniflux protocol, and most + properties have identical or similar names to those used by Miniflux. + Properties may also affect other protocols, or conversely may have no + effect even when using the Miniflux protocol; this is noted below when + appropriate. + + Booleans accept any of the values true/false, 1/0, yes/no, on/off. + + The following metadata properties exist for each user: + + num + Integer. The numeric identifier of the user. This is assigned at user + creation and is read-only. + admin + Boolean. Whether the user is an administrator. Administrators may + manage other users via the Miniflux protocol, and also may trigger + feed updates manually via the Nextcloud News protocol. + lang + String. The preferred language of the user, as a BCP 47 language tag + e.g. "en-ca". Note that since The Arsse currently only includes + English text it is not used by The Arsse itself, but clients may + use this metadata in protocols which expose it. + tz + String. The time zone of the user, as a tzdata identifier e.g. + "America/Los_Angeles". + root_folder_name + String. The name of the root folder, in protocols which allow it to + be renamed. + sort_asc + Boolean. Whether the user prefers ascending sort order for articles. + Descending order is usually the default, but explicitly setting this + property false will also make a preference for descending order + explicit. + theme + String. The user's preferred theme. This is not used by The Arsse + itself, but clients may use this metadata in protocols which expose + it. + page_size + Integer. The user's preferred pge size when listing articles. This is + not used by The Arsse itself, but clients may use this metadata in + protocols which expose it. + shortcuts + Boolean. Whether to enable keyboard shortcuts. This is not used by + The Arsse itself, but clients may use this metadata in protocols which + expose it. + gestures + Boolean. Whether to enable touch gestures. This is not used by + The Arsse itself, but clients may use this metadata in protocols which + expose it. + reading_time + Boolean. Whether to calculate and display the estimated reading time + for articles. Currently The Arsse does not calculate reading time, so + changing this will likely have no effect. + stylesheet + String. A user CSS stylesheet. This is not used by The Arsse itself, + but clients may use this metadata in protocols which expose it. USAGE_TEXT; protected function usage($prog): string { @@ -215,10 +295,14 @@ USAGE_TEXT; } protected function userManage($args): int { - $cmd = $this->command(["add", "remove", "set-pass", "unset-pass", "list", "auth"], $args); + $cmd = $this->command(["add", "remove", "show", "set", "unset", "set-pass", "unset-pass", "list", "auth"], $args); switch ($cmd) { case "add": - return $this->userAddOrSetPassword("add", $args[""], $args[""]); + $out = $this->userAddOrSetPassword("add", $args[""], $args[""]); + if ($args['--admin']) { + Arsse::$user->propertiesSet($args[""], ['admin' => true]); + } + return $out; case "set-pass": if ($args['--fever']) { $passwd = Arsse::$obj->get(Fever::class)->register($args[""], $args[""]); @@ -239,6 +323,12 @@ USAGE_TEXT; return 0; case "remove": return (int) !Arsse::$user->remove($args[""]); + case "show": + return $this->userShowProperties($args[""]); + case "set": + return (int) !Arsse::$user->propertiesSet($args[""], [$args[""] => $args[""]]); + case "unset": + return (int) !Arsse::$user->propertiesSet($args[""], [$args[""] => null]); case "auth": return $this->userAuthenticate($args[""], $args[""], $args["--fever"]); case "list": @@ -275,4 +365,16 @@ USAGE_TEXT; return 1; } } + + protected function userShowProperties(string $user): int { + $data = Arsse::$user->propertiesGet($user); + $len = array_reduce(array_keys($data), function($carry, $item) { + return max($carry, strlen($item)); + }, 0) + 2; + foreach ($data as $k => $v) { + echo str_pad($k, $len, " "); + echo var_export($v, true).\PHP_EOL; + } + return 0; + } } diff --git a/lib/User.php b/lib/User.php index 04748962..4bf8e36b 100644 --- a/lib/User.php +++ b/lib/User.php @@ -18,14 +18,14 @@ class User { 'admin' => V::T_BOOL, 'lang' => V::T_STRING, 'tz' => V::T_STRING, + 'root_folder_name' => V::T_STRING, 'sort_asc' => V::T_BOOL, 'theme' => V::T_STRING, 'page_size' => V::T_INT, // greater than zero 'shortcuts' => V::T_BOOL, 'gestures' => V::T_BOOL, - 'stylesheet' => V::T_STRING, 'reading_time' => V::T_BOOL, - 'root_folder_name' => V::T_STRING, + 'stylesheet' => V::T_STRING, ]; public const PROPERTIES_LARGE = ["stylesheet"]; diff --git a/tests/cases/CLI/TestCLI.php b/tests/cases/CLI/TestCLI.php index 30237757..dfb4d12e 100644 --- a/tests/cases/CLI/TestCLI.php +++ b/tests/cases/CLI/TestCLI.php @@ -156,6 +156,15 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { ]; } + public function testAddAUserAsAdministrator(): void { + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("add")->willReturn("random password"); + Arsse::$user->method("propertiesSet")->willReturn([]); + Arsse::$user->expects($this->exactly(1))->method("add")->with("jane.doe@example.com", null); + Arsse::$user->expects($this->exactly(1))->method("propertiesSet")->with("jane.doe@example.com", ['admin' => true]); + $this->assertConsole($this->cli, "arsse.php user add jane.doe@example.com --admin", 0, "random password"); + } + /** @dataProvider provideUserAuthentication */ public function testAuthenticateAUser(string $cmd, int $exitStatus, string $output): void { // FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead @@ -357,4 +366,56 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { ["arsse.php import jane.doe@example.com bad.opml --replace --flat", 10603, "bad.opml", "jane.doe@example.com", true, true], ]; } + + public function testShowMetadataOfAUser(): void { + $data = [ + 'num' => 42, + 'admin' => false, + 'lang' => "en-ca", + 'tz' => "America/Toronto", + 'root_folder_name' => null, + 'sort_asc' => true, + 'theme' => null, + 'page_size' => 50, + 'shortcuts' => true, + 'gestures' => null, + 'reading_time' => false, + 'stylesheet' => "body {color:gray}", + ]; + $exp = implode(\PHP_EOL, [ + "num 42", + "admin false", + "lang 'en-ca'", + "tz 'America/Toronto'", + "root_folder_name NULL", + "sort_asc true", + "theme NULL", + "page_size 50", + "shortcuts true", + "gestures NULL", + "reading_time false", + "stylesheet 'body {color:gray}'", + ]); + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("propertiesGet")->willReturn($data); + Arsse::$user->expects($this->once())->method("propertiesGet")->with("john.doe@example.com", true); + $this->assertConsole($this->cli, "arsse.php user show john.doe@example.com", 0, $exp); + } + + /** @dataProvider provideMetadataChanges */ + public function testSetMetadataOfAUser(string $cmd, string $user, array $in, array $out, int $exp): void { + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("propertiesSet")->willReturn($out); + Arsse::$user->expects($this->once())->method("propertiesSet")->with($user, $in); + $this->assertConsole($this->cli, $cmd, $exp, ""); + } + + public function provideMetadataChanges(): iterable { + return [ + ["arsse.php user set john admin true", "john", ['admin' => "true"], ['admin' => "true"], 0], + ["arsse.php user set john bogus 1", "john", ['bogus' => "1"], [], 1], + ["arsse.php user unset john admin", "john", ['admin' => null], ['admin' => null], 0], + ["arsse.php user unset john bogus", "john", ['bogus' => null], [], 1], + ]; + } } diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index e096ca1e..f807e6bb 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -88,6 +88,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { }, $params, array_keys($params))); } $url = URL::queryAppend($url, (string) $params); + $params = null; } $q = parse_url($url, \PHP_URL_QUERY); if (strlen($q ?? "")) { From 97d1de46f8c68d79c3f254b88bdce12333b15d9b Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 10 Feb 2021 11:24:16 -0500 Subject: [PATCH 178/366] Fill in upgrade notes --- UPGRADING | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/UPGRADING b/UPGRADING index f18bf760..3c05ec4d 100644 --- a/UPGRADING +++ b/UPGRADING @@ -11,6 +11,23 @@ usually prudent: `composer install -o --no-dev` +Upgrading from 0.8.5 to 0.9.0 +============================= + +- The database schema has changed from rev6 to rev7; if upgrading the database + manually, apply the 6.sql file +- Web server configuration has changed to accommodate Miniflux; the following + URL paths are affected: + - /v1/ + - /version + - /healthcheck +- Icons for existing feeds in Miniflux and Fever will only appear once the + feeds in question have been fetched after upgrade. This may take up to + twenty-four hours to occur +- An administrator account is now required to refresh feeds via the + Nextcloud News protocol + + Upgrading from 0.8.4 to 0.8.5 ============================= From e8ed716ae6034aa3174338ea55b21b00d3bb1d81 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 10 Feb 2021 12:11:28 -0500 Subject: [PATCH 179/366] Fix errors in CLI documentation --- lib/CLI.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/CLI.php b/lib/CLI.php index d40f683e..8e8c1e67 100644 --- a/lib/CLI.php +++ b/lib/CLI.php @@ -152,7 +152,7 @@ Commands: User metadata: - User metadata is primary used by the Miniflux protocol, and most + User metadata are primarily used by the Miniflux protocol, and most properties have identical or similar names to those used by Miniflux. Properties may also affect other protocols, or conversely may have no effect even when using the Miniflux protocol; this is noted below when @@ -173,7 +173,7 @@ User metadata: String. The preferred language of the user, as a BCP 47 language tag e.g. "en-ca". Note that since The Arsse currently only includes English text it is not used by The Arsse itself, but clients may - use this metadata in protocols which expose it. + use this metadatum in protocols which expose it. tz String. The time zone of the user, as a tzdata identifier e.g. "America/Los_Angeles". @@ -187,19 +187,19 @@ User metadata: explicit. theme String. The user's preferred theme. This is not used by The Arsse - itself, but clients may use this metadata in protocols which expose + itself, but clients may use this metadatum in protocols which expose it. page_size - Integer. The user's preferred pge size when listing articles. This is - not used by The Arsse itself, but clients may use this metadata in + Integer. The user's preferred page size when listing articles. This is + not used by The Arsse itself, but clients may use this metadatum in protocols which expose it. shortcuts Boolean. Whether to enable keyboard shortcuts. This is not used by - The Arsse itself, but clients may use this metadata in protocols which + The Arsse itself, but clients may use this metadatum in protocols which expose it. gestures Boolean. Whether to enable touch gestures. This is not used by - The Arsse itself, but clients may use this metadata in protocols which + The Arsse itself, but clients may use this metadatum in protocols which expose it. reading_time Boolean. Whether to calculate and display the estimated reading time @@ -207,7 +207,7 @@ User metadata: changing this will likely have no effect. stylesheet String. A user CSS stylesheet. This is not used by The Arsse itself, - but clients may use this metadata in protocols which expose it. + but clients may use this metadatum in protocols which expose it. USAGE_TEXT; protected function usage($prog): string { From 3795b1ccd860a832f658a6ef4c90e3295ef36e42 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 10 Feb 2021 12:46:28 -0500 Subject: [PATCH 180/366] Simplify CLI command processing --- lib/CLI.php | 118 +++++++++++++++++++++++----------------------------- 1 file changed, 53 insertions(+), 65 deletions(-) diff --git a/lib/CLI.php b/lib/CLI.php index 8e8c1e67..53c50755 100644 --- a/lib/CLI.php +++ b/lib/CLI.php @@ -88,12 +88,12 @@ Commands: user set - Sets a user's metadata proprty to the supplied value. See below for + Sets a user's metadata property to the supplied value. See below for details on the various properties available. user unset - Sets a user's metadata proprty to its default value. See below for + Sets a user's metadata property to its default value. See below for details on the various properties available. What the default value for a property evaluates to depends on which protocol is used. @@ -215,16 +215,14 @@ USAGE_TEXT; return str_replace("arsse.php", $prog, self::USAGE); } - protected function command(array $options, $args): string { - foreach ($options as $cmd) { - foreach (explode(" ", $cmd) as $part) { - if (!$args[$part]) { - continue 2; - } + protected function command($args): string { + $out = []; + foreach ($args as $k => $v) { + if (preg_match("/^[a-z]/", $k) && $v === true) { + $out[] = $k; } - return $cmd; } - return ""; + return implode(" ", $out); } /** @codeCoverageIgnore */ @@ -248,18 +246,18 @@ USAGE_TEXT; 'help' => false, ]); try { - $cmd = $this->command(["-h", "--help", "--version", "daemon", "feed refresh", "feed refresh-all", "conf save-defaults", "user", "export", "import"], $args); - if ($cmd && !in_array($cmd, ["-h", "--help", "--version", "conf save-defaults"])) { + $cmd = $this->command($args); + if ($cmd && !in_array($cmd, ["", "conf save-defaults"])) { // only certain commands don't require configuration to be loaded $this->loadConf(); } switch ($cmd) { - case "-h": - case "--help": - echo $this->usage($argv0).\PHP_EOL; - return 0; - case "--version": - echo Arsse::VERSION.\PHP_EOL; + case "": + if ($args['--version']) { + echo Arsse::VERSION.\PHP_EOL; + } elseif ($args['--help'] || $args['-h']) { + echo $this->usage($argv0).\PHP_EOL; + } return 0; case "daemon": Arsse::$obj->get(Service::class)->watch(true); @@ -272,8 +270,6 @@ USAGE_TEXT; case "conf save-defaults": $file = $this->resolveFile($args[''], "w"); return (int) !Arsse::$obj->get(Conf::class)->exportFile($file, true); - case "user": - return $this->userManage($args); case "export": $u = $args['']; $file = $this->resolveFile($args[''], "w"); @@ -282,6 +278,43 @@ USAGE_TEXT; $u = $args['']; $file = $this->resolveFile($args[''], "r"); return (int) !Arsse::$obj->get(OPML::class)->importFile($file, $u, ($args['--flat'] || $args['-f']), ($args['--replace'] || $args['-r'])); + case "user add": + $out = $this->userAddOrSetPassword("add", $args[""], $args[""]); + if ($args['--admin']) { + Arsse::$user->propertiesSet($args[""], ['admin' => true]); + } + return $out; + case "user set-pass": + if ($args['--fever']) { + $passwd = Arsse::$obj->get(Fever::class)->register($args[""], $args[""]); + if (is_null($args[""])) { + echo $passwd.\PHP_EOL; + } + return 0; + } else { + return $this->userAddOrSetPassword("passwordSet", $args[""], $args[""], $args["--oldpass"]); + } + // no break + case "user unset-pass": + if ($args['--fever']) { + Arsse::$obj->get(Fever::class)->unregister($args[""]); + } else { + Arsse::$user->passwordUnset($args[""], $args["--oldpass"]); + } + return 0; + case "user remove": + return (int) !Arsse::$user->remove($args[""]); + case "user show": + return $this->userShowProperties($args[""]); + case "user set": + return (int) !Arsse::$user->propertiesSet($args[""], [$args[""] => $args[""]]); + case "user unset": + return (int) !Arsse::$user->propertiesSet($args[""], [$args[""] => null]); + case "user auth": + return $this->userAuthenticate($args[""], $args[""], $args["--fever"]); + case "user list": + case "user": + return $this->userList(); } } catch (AbstractException $e) { $this->logError($e->getMessage()); @@ -294,51 +327,6 @@ USAGE_TEXT; fwrite(STDERR, $msg.\PHP_EOL); } - protected function userManage($args): int { - $cmd = $this->command(["add", "remove", "show", "set", "unset", "set-pass", "unset-pass", "list", "auth"], $args); - switch ($cmd) { - case "add": - $out = $this->userAddOrSetPassword("add", $args[""], $args[""]); - if ($args['--admin']) { - Arsse::$user->propertiesSet($args[""], ['admin' => true]); - } - return $out; - case "set-pass": - if ($args['--fever']) { - $passwd = Arsse::$obj->get(Fever::class)->register($args[""], $args[""]); - if (is_null($args[""])) { - echo $passwd.\PHP_EOL; - } - return 0; - } else { - return $this->userAddOrSetPassword("passwordSet", $args[""], $args[""], $args["--oldpass"]); - } - // no break - case "unset-pass": - if ($args['--fever']) { - Arsse::$obj->get(Fever::class)->unregister($args[""]); - } else { - Arsse::$user->passwordUnset($args[""], $args["--oldpass"]); - } - return 0; - case "remove": - return (int) !Arsse::$user->remove($args[""]); - case "show": - return $this->userShowProperties($args[""]); - case "set": - return (int) !Arsse::$user->propertiesSet($args[""], [$args[""] => $args[""]]); - case "unset": - return (int) !Arsse::$user->propertiesSet($args[""], [$args[""] => null]); - case "auth": - return $this->userAuthenticate($args[""], $args[""], $args["--fever"]); - case "list": - case "": - return $this->userList(); - default: - throw new Exception("constantUnknown", $cmd); // @codeCoverageIgnore - } - } - protected function userAddOrSetPassword(string $method, string $user, string $password = null, string $oldpass = null): int { $passwd = Arsse::$user->$method(...array_slice(func_get_args(), 1)); if (is_null($password)) { From fa6d641634324d59f4b689df83f395ee08fb16c8 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 10 Feb 2021 21:40:51 -0500 Subject: [PATCH 181/366] Implement CLI for tokens --- CHANGELOG | 1 + lib/CLI.php | 44 +++++++++++++ lib/REST/Miniflux/Token.php | 31 +++++++++ lib/REST/Miniflux/V1.php | 18 ------ tests/cases/CLI/TestCLI.php | 51 +++++++++++++++ tests/cases/REST/Miniflux/PDO/TestToken.php | 13 ++++ tests/cases/REST/Miniflux/TestToken.php | 70 +++++++++++++++++++++ tests/cases/REST/Miniflux/TestV1.php | 30 --------- tests/phpunit.dist.xml | 2 + 9 files changed, 212 insertions(+), 48 deletions(-) create mode 100644 lib/REST/Miniflux/Token.php create mode 100644 tests/cases/REST/Miniflux/PDO/TestToken.php create mode 100644 tests/cases/REST/Miniflux/TestToken.php diff --git a/CHANGELOG b/CHANGELOG index 781625de..683f89d0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ New features: - Support for API level 15 of Tiny Tiny RSS - Support for feed icons in Fever - Command-line functionality for managing user metadata +- Command-line functionality for managing Miniflux login tokens Bug fixes: - Use icons specified in Atom feeds when available diff --git a/lib/CLI.php b/lib/CLI.php index 53c50755..f892bddd 100644 --- a/lib/CLI.php +++ b/lib/CLI.php @@ -8,6 +8,7 @@ namespace JKingWeb\Arsse; use JKingWeb\Arsse\REST\Fever\User as Fever; use JKingWeb\Arsse\ImportExport\OPML; +use JKingWeb\Arsse\REST\Miniflux\Token as Miniflux; class CLI { public const USAGE = << [--oldpass=] [--fever] arsse.php user auth [--fever] + arsse.php token list + arsse.php token create [ cases/REST/NextcloudNews/TestVersions.php From 3ba82b7c6da01d961990dc5ba872e1449ff9eb47 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 10 Feb 2021 22:12:00 -0500 Subject: [PATCH 182/366] Fix CLI bootstrap problem --- arsse.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arsse.php b/arsse.php index 546723b9..32db15ae 100644 --- a/arsse.php +++ b/arsse.php @@ -15,7 +15,8 @@ ini_set("memory_limit", "-1"); ini_set("max_execution_time", "0"); if (\PHP_SAPI === "cli") { - // initialize the CLI; this automatically handles --help and --version + // initialize the CLI; this automatically handles --help and --version else { + Arsse::$obj = new Factory; $cli = new CLI; // handle other CLI requests; some do not require configuration $exitStatus = $cli->dispatch(); From 9c0a3b7a57fe77118c6175c413360011576362e2 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 10 Feb 2021 22:30:16 -0500 Subject: [PATCH 183/366] Fix typo --- arsse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arsse.php b/arsse.php index 32db15ae..b0f856cf 100644 --- a/arsse.php +++ b/arsse.php @@ -15,7 +15,7 @@ ini_set("memory_limit", "-1"); ini_set("max_execution_time", "0"); if (\PHP_SAPI === "cli") { - // initialize the CLI; this automatically handles --help and --version else { + // initialize the CLI; this automatically handles --help and --version else Arsse::$obj = new Factory; $cli = new CLI; // handle other CLI requests; some do not require configuration From 50b2ca4500161d0acc0124abf6a4b7d586974dd6 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 11 Feb 2021 11:01:04 -0500 Subject: [PATCH 184/366] Document tokens and metadata in the manual --- .../025_Using_The_Arsse/010_Managing_Users.md | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/en/025_Using_The_Arsse/010_Managing_Users.md b/docs/en/025_Using_The_Arsse/010_Managing_Users.md index 1562768c..53f2da09 100644 --- a/docs/en/025_Using_The_Arsse/010_Managing_Users.md +++ b/docs/en/025_Using_The_Arsse/010_Managing_Users.md @@ -21,7 +21,7 @@ Ji0ivMYqi6gKxQK1MHuE # Setting and changing passwords -Setting's a user's password is practically identical to adding a password: +Setting a user's password is nearly identical to adding a user: ```sh sudo -u www-data php arsse.php user set-pass "user@example.com" "new password" @@ -49,7 +49,40 @@ $ sudo -u www-data php arsse.php user set-pass --fever "jane.doe" YfZJHq4fNTRUKDYhzQdR ``` +## Managing login tokens for Miniflux +[Miniflux](/en/Supported_Protocols/Miniflux) clients may optionally log in using tokens: randomly-generated strings which act as persistent passwords. For now these must be generated using the command-line interface: - +```console +$ sudo -u www-data php arsse.php token create "jane.doe" +xRK0huUE9KHNHf_x_H8JG0oRDo4t_WV44whBtr8Ckf0= +``` +Multiple tokens may be generated for use with different clients, and descriptive labels can be assigned for later identification: + +```console +$ sudo -u www-data php arsse.php token create "jane.doe" Newsflash +xRK0huUE9KHNHf_x_H8JG0oRDo4t_WV44whBtr8Ckf0= +$ sudo -u www-data php arsse.php token create "jane.doe" Reminiflux +L7asI2X_d-krinGJd1GsiRdFm2o06ZUlgD22H913hK4= +``` + +There are also commands for listing and revoking tokens. Please consult the integrated help for more details. + +# Setting and changing user metadata + +Users may also have various metadata properties set. These largely exist for compatibility with [the Miniflux protocol](/en/Supported_Protocols/Miniflux) and have no significant effect. One exception to this, however, is the `admin` flag, which signals whether the user may perform privileged operations where they exist in the supported protocols. + +The flag may be changed using the following command: + +```sh +sudo -u www-data php arsse.php user set "jane.doe" admin true +``` + +As a shortcut it is also possible to create administrators directly: + +```sh +sudo -u www-data php arsse.php user add "user@example.com" "example password" --admin +``` + +Please consult the integrated help for more details on metadata and their effects. From d836d6a24342d2e2ddf92c4d473d9f72852ece5f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 11 Feb 2021 13:22:04 -0500 Subject: [PATCH 185/366] Add more clients to the untested list --- docs/en/040_Compatible_Clients.md | 65 +++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/docs/en/040_Compatible_Clients.md b/docs/en/040_Compatible_Clients.md index bc089e24..03ab4317 100644 --- a/docs/en/040_Compatible_Clients.md +++ b/docs/en/040_Compatible_Clients.md @@ -347,6 +347,39 @@ The Arsse does not at this time have any first party clients. However, because T

+ + Fuoten + Sailfish + ✘ + ✔ + ✘ + ✘ + +

+ + + + Geekttrss + Android + ✘ + ✘ + ✔ + ✘ + +

+ + + + Kaktus + Sailfish, BlackBerry + ✘ + ✘ + ✔ + ✘ + +

+ + + + maxiflux + Web + ✔ + ✘ + ✘ + ✘ + + Newsie Ubuntu Touch From dcb81ea043bcecc6bd4456cd7d21ec24684670f9 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 5 Mar 2021 19:31:11 -0500 Subject: [PATCH 207/366] Only provide icon ID when there is data --- CHANGELOG | 2 ++ lib/Database.php | 4 +-- tests/cases/Database/SeriesSubscription.php | 33 +++++++++++++++++---- tests/cases/REST/Fever/TestAPI.php | 4 +-- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 93cea5eb..dedc22d0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,10 +9,12 @@ New features: - Command-line functionality for managing Miniflux login tokens Bug fixes: +- Further relax Fever HTTP correctness, to fix more clients - Use icons specified in Atom feeds when available - Do not return null as subscription unread count - Explicitly forbid U+003A COLON and control characters in usernames, for compatibility with RFC 7617 +- Never return 401 in response to an OPTIONS request - Accept "t" and "f" as booleans in Tiny Tiny RSS Changes: diff --git a/lib/Database.php b/lib/Database.php index b3b2b22a..776b46d2 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -801,11 +801,11 @@ class Database { f.modified as edited, s.modified as modified, f.next_fetch, - i.id as icon_id, + case when i.data is not null then i.id end as icon_id, i.url as icon_url, folder, t.top as top_folder, d.name as folder_name, dt.name as top_folder_name, coalesce(s.title, f.title) as title, - coalesce((articles - hidden - marked), articles) as unread + coalesce((articles - hidden - marked), coalesce(articles,0)) as unread FROM arsse_subscriptions as s join arsse_feeds as f on f.id = s.feed left join topmost as t on t.f_id = s.folder diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index b029846b..625ac399 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -47,9 +47,11 @@ trait SeriesSubscription { 'columns' => [ 'id' => "int", 'url' => "str", + 'data' => "blob", ], 'rows' => [ - [1,"http://example.com/favicon.ico"], + [1,"http://example.com/favicon.ico", "ICON DATA"], + [2,"http://example.net/favicon.ico", null], ], ], 'arsse_feeds' => [ @@ -66,7 +68,8 @@ trait SeriesSubscription { 'rows' => [ [1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),strtotime("now"),null], [2,"http://example.com/feed2", "eek", "", "",strtotime("now - 1 hour"),strtotime("now - 1 hour"),1], - [3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),strtotime("now + 1 hour"),null], + [3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),strtotime("now + 1 hour"),2], + [4,"http://example.com/feed4", "Foo", "", "",strtotime("now + 1 hour"),strtotime("now + 1 hour"),null], ], ], 'arsse_subscriptions' => [ @@ -88,6 +91,7 @@ trait SeriesSubscription { [3,"john.doe@example.com",3,"Ook",2,0,1,null,null,0], [4,"jill.doe@example.com",2,null,null,0,0,null,null,0], [5,"jack.doe@example.com",2,null,null,1,2,"","3|E",0], + [6,"john.doe@example.com",4,"Bar",3,0,0,null,null,0], ], ], 'arsse_tags' => [ @@ -329,6 +333,8 @@ trait SeriesSubscription { 'unread' => 4, 'pinned' => 1, 'order_type' => 2, + 'icon_url' => "http://example.com/favicon.ico", + 'icon_id' => 1, ], [ 'url' => "http://example.com/feed3", @@ -340,6 +346,21 @@ trait SeriesSubscription { 'unread' => 2, 'pinned' => 0, 'order_type' => 1, + 'icon_url' => "http://example.net/favicon.ico", + 'icon_id' => null, + ], + [ + 'url' => "http://example.com/feed4", + 'title' => "Bar", + 'folder' => 3, + 'top_folder' => 1, + 'folder_name' => "Rocketry", + 'top_folder_name' => "Technology", + 'unread' => 0, + 'pinned' => 0, + 'order_type' => 0, + 'icon_url' => null, + 'icon_id' => null, ], ]; $this->assertResult($exp, Arsse::$db->subscriptionList($this->user)); @@ -375,7 +396,7 @@ trait SeriesSubscription { $this->assertResult($exp, Arsse::$db->subscriptionList($this->user, null, false)); } - public function testListSubscriptionsWithoutRecursion(): void { + public function testListSubscriptionsWithRecursion(): void { $exp = [ [ 'url' => "http://example.com/feed3", @@ -396,7 +417,7 @@ trait SeriesSubscription { } public function testCountSubscriptions(): void { - $this->assertSame(2, Arsse::$db->subscriptionCount($this->user)); + $this->assertSame(3, Arsse::$db->subscriptionCount($this->user)); $this->assertSame(1, Arsse::$db->subscriptionCount($this->user, 2)); } @@ -488,7 +509,7 @@ trait SeriesSubscription { $exp = "http://example.com/favicon.ico"; $this->assertSame($exp, Arsse::$db->subscriptionIcon(null, 1)['url']); $this->assertSame($exp, Arsse::$db->subscriptionIcon(null, 2)['url']); - $this->assertSame(null, Arsse::$db->subscriptionIcon(null, 3)); + $this->assertSame(null, Arsse::$db->subscriptionIcon(null, 6)); } public function testRetrieveTheFaviconOfAMissingSubscription(): void { @@ -500,7 +521,7 @@ trait SeriesSubscription { $exp = "http://example.com/favicon.ico"; $user = "john.doe@example.com"; $this->assertSame($exp, Arsse::$db->subscriptionIcon($user, 1)['url']); - $this->assertSame(null, Arsse::$db->subscriptionIcon($user, 3)); + $this->assertSame(null, Arsse::$db->subscriptionIcon($user, 6)); $user = "jane.doe@example.com"; $this->assertSame($exp, Arsse::$db->subscriptionIcon($user, 2)['url']); } diff --git a/tests/cases/REST/Fever/TestAPI.php b/tests/cases/REST/Fever/TestAPI.php index 3128c446..0868d13f 100644 --- a/tests/cases/REST/Fever/TestAPI.php +++ b/tests/cases/REST/Fever/TestAPI.php @@ -425,8 +425,8 @@ class TestAPI extends \JKingWeb\Arsse\Test\AbstractTest { 'Not an API request' => ["", "", "POST", null, new EmptyResponse(404)], 'Wrong method' => ["api", "", "PUT", null, new EmptyResponse(405, ['Allow' => "OPTIONS,POST"])], 'Non-standard method' => ["api", "", "GET", null, new JsonResponse([])], - 'Wrong content type' => ["api", '{"api_key":"validToken"}', "POST", "application/json", new EmptyResponse(415, ['Accept' => "application/x-www-form-urlencoded, multipart/form-data"])], - 'Non-standard content type' => ["api", '{"api_key":"validToken"}', "POST", "multipart/form-data; boundary=33b68964f0de4c1f-5144aa6caaa6e4a8-18bfaf416a1786c8-5c5053a45f221bc1", new JsonResponse([])], + 'Wrong content type' => ["api", '{"api_key":"validToken"}', "POST", "application/json", new JsonResponse([])], // some clients send nonsensical content types; Fever seems to have allowed this + 'Non-standard content type' => ["api", '{"api_key":"validToken"}', "POST", "multipart/form-data; boundary=33b68964f0de4c1f-5144aa6caaa6e4a8-18bfaf416a1786c8-5c5053a45f221bc1", new JsonResponse([])], // some clients send nonsensical content types; Fever seems to have allowed this ]; } From 2e4c57b75b4d87f4759c1dd660900b13fa97ecf1 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 6 Mar 2021 11:26:14 -0500 Subject: [PATCH 208/366] Work around Microflux for Miniflux --- lib/REST/Miniflux/V1.php | 3 +++ tests/cases/REST/Miniflux/TestV1.php | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 96eb8c40..43943993 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -886,6 +886,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function computeContext(array $query, Context $c = null): Context { + if ($query['before'] && $query['before']->getTimestamp() === 0) { + $query['before'] = null; // NOTE: This workaround is needed for compatibility with "Microflux for Miniflux", an Android Client + } $c = ($c ?? new Context) ->limit($query['limit'] ?? self::DEFAULT_ENTRY_LIMIT) // NOTE: This does not honour user preferences ->offset($query['offset']) diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index d6b43c74..4e40398f 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -769,7 +769,8 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { ["/entries?starred=true", (clone $c)->starred(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], ["/entries?starred=false", (clone $c)->starred(true), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], ["/entries?after=0", (clone $c)->modifiedSince(0), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], - ["/entries?before=0", (clone $c)->notModifiedSince(0), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?before=0", $c, $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], + ["/entries?before=1", (clone $c)->notModifiedSince(1), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], ["/entries?after_entry_id=42", (clone $c)->oldestArticle(43), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], ["/entries?before_entry_id=47", (clone $c)->latestArticle(46), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], ["/entries?search=alpha%20beta", (clone $c)->searchTerms(["alpha", "beta"]), $o, self::ENTRIES, false, new Response(['total' => sizeof(self::ENTRIES_OUT), 'entries' => self::ENTRIES_OUT])], From 4b0571299a425a041923fa1104f4937342c31e90 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 6 Mar 2021 11:39:45 -0500 Subject: [PATCH 209/366] Add results of client testing --- docs/en/040_Compatible_Clients.md | 71 +++++++++++++++---------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/docs/en/040_Compatible_Clients.md b/docs/en/040_Compatible_Clients.md index 1b25ee53..3034a107 100644 --- a/docs/en/040_Compatible_Clients.md +++ b/docs/en/040_Compatible_Clients.md @@ -1,4 +1,4 @@ -The Arsse does not at this time have any first party clients. However, because The Arsse [supports existing protocols](/en/Supported_Protocols), most clients built for these protocols are compatible with The Arsse. Below are those that we personally know of and have tested with The Arsse. +The Arsse does not at this time have any first party clients. However, because The Arsse [supports existing protocols](/en/Supported_Protocols), most clients built for these protocols are compatible with The Arsse. Below are those that we personally know of and have tested with The Arsse, presented in alphabetical order. @@ -26,7 +26,16 @@ The Arsse does not at this time have any first party clients. However, because T - + + + + + + + + + + @@ -36,7 +45,18 @@ The Arsse does not at this time have any first party clients. However, because T + + + + + + + + + @@ -47,7 +67,7 @@ The Arsse does not at this time have any first party clients. However, because T @@ -193,15 +213,6 @@ The Arsse does not at this time have any first party clients. However, because T - - - - - - - - - @@ -383,7 +394,6 @@ The Arsse does not at this time have any first party clients. However, because T

Level of functionality unclear.

- --> @@ -391,7 +401,17 @@ The Arsse does not at this time have any first party clients. However, because T - + + + --> + + + + + + + + @@ -403,16 +423,6 @@ The Arsse does not at this time have any first party clients. However, because T - - - - - - - - - @@ -446,17 +456,6 @@ The Arsse does not at this time have any first party clients. However, because T

Does not support HTTP authentication.

- - - - - - - - - From 764b604edda8f163d880e268428175bacf4d6efa Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 6 Mar 2021 12:00:36 -0500 Subject: [PATCH 210/366] Note Fiery Feeds' support for HTTP auth with Fever --- docs/en/030_Supported_Protocols/030_Fever.md | 2 +- docs/en/040_Compatible_Clients.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/en/030_Supported_Protocols/030_Fever.md b/docs/en/030_Supported_Protocols/030_Fever.md index 846d7e1f..438fe399 100644 --- a/docs/en/030_Supported_Protocols/030_Fever.md +++ b/docs/en/030_Supported_Protocols/030_Fever.md @@ -37,7 +37,7 @@ The Fever protocol is incomplete, unusual, _and_ a product of proprietary softwa # Interaction with HTTP Authentication -We are not aware of any Fever clients which respond to HTTP authentication challenges. If the Web server or The Arsse is configured to require successful HTTP authentication, Fever clients are not likely to be able to connect properly. +Fever was not designed with HTTP authentication in mind, and few clients respond to challenges. If the Web server or The Arsse is configured to require successful HTTP authentication, most Fever clients are not likely to be able to connect properly. # Interaction with Folders diff --git a/docs/en/040_Compatible_Clients.md b/docs/en/040_Compatible_Clients.md index 3034a107..dcdc33c7 100644 --- a/docs/en/040_Compatible_Clients.md +++ b/docs/en/040_Compatible_Clients.md @@ -30,7 +30,7 @@ The Arsse does not at this time have any first party clients. However, because T - + @@ -190,6 +190,7 @@ The Arsse does not at this time have any first party clients. However, because T @@ -411,7 +412,7 @@ The Arsse does not at this time have any first party clients. However, because T - + From bff3e21cd23a5b2017d24afca627774a6917354a Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 6 Mar 2021 16:41:07 -0500 Subject: [PATCH 211/366] Date release --- CHANGELOG | 2 +- UPGRADING | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dedc22d0..c3476d8d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -Version 0.9.0 (????-??-??) +Version 0.9.0 (2021-03-06) ========================== New features: diff --git a/UPGRADING b/UPGRADING index 3c05ec4d..f6dcfff6 100644 --- a/UPGRADING +++ b/UPGRADING @@ -22,8 +22,8 @@ Upgrading from 0.8.5 to 0.9.0 - /version - /healthcheck - Icons for existing feeds in Miniflux and Fever will only appear once the - feeds in question have been fetched after upgrade. This may take up to - twenty-four hours to occur + feeds in question have been fetched and parsed after upgrade. This may take + some time to occur depending on how often the feed is updated - An administrator account is now required to refresh feeds via the Nextcloud News protocol From abc291460ce85816f8263638db2da241fe70ad7f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 18 Mar 2021 09:54:03 -0400 Subject: [PATCH 212/366] Update Web server configuration in manual --- .../030_Web_Server_Configuration.md | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/en/020_Getting_Started/030_Web_Server_Configuration.md b/docs/en/020_Getting_Started/030_Web_Server_Configuration.md index 679b0395..8f0371e9 100644 --- a/docs/en/020_Getting_Started/030_Web_Server_Configuration.md +++ b/docs/en/020_Getting_Started/030_Web_Server_Configuration.md @@ -66,6 +66,23 @@ server { # this path should not be behind HTTP authentication try_files $uri @arsse; } + + # Miniflux protocol + location /v1/ { + try_files $uri @arsse; + } + + # Miniflux version number + location /version { + # this path should not be behind HTTP authentication + try_files $uri @arsse; + } + + # Miniflux "health check" + location /healthcheck { + # this path should not be behind HTTP authentication + try_files $uri @arsse; + } } ``` @@ -93,13 +110,13 @@ Afterward the follow virtual host configuration should work, after modifying pat ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/usr/share/arsse/arsse.php" ProxyPreserveHost On - # Nextcloud News v1.2, Tiny Tiny RSS API, TT-RSS newsfeed icons - + # Nextcloud News v1.2, Tiny Tiny RSS API, TT-RSS newsfeed icons, Miniflux API + ProxyPass "unix:/var/run/php/php7.2-fpm.sock|fcgi://localhost/usr/share/arsse" - # Nextcloud News API detection, Fever API - + # Nextcloud News API detection, Fever API, Miniflux miscellanies + # these locations should not be behind HTTP authentication ProxyPass "unix:/var/run/php/php7.2-fpm.sock|fcgi://localhost/usr/share/arsse" From c4260323bcaad4a85d7daa63e4a2949a69edb3fc Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 18 Mar 2021 10:38:20 -0400 Subject: [PATCH 213/366] Answer 201 to PUTs like Miniflux This does not apply to PUTs to /v1/entries, which were always 204 --- .../030_Supported_Protocols/005_Miniflux.md | 1 - lib/REST/Miniflux/V1.php | 6 ++--- tests/cases/REST/Miniflux/TestV1.php | 22 +++++++++---------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index 2e6c23b1..ebbb4423 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -34,7 +34,6 @@ Miniflux version 2.0.28 is emulated, though not all features are implemented # Differences - Various error codes and messages differ due to significant implementation differences -- `PUT` requests which return a body respond with `200 OK` rather than `201 Created` - The "All" category is treated specially (see below for details) - Feed and category titles consisting only of whitespace are rejected along with the empty string - Filtering rules may not function identically (see below for details) diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 43943993..4c55fbc6 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -626,7 +626,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } throw $e; // @codeCoverageIgnore } - return new Response($out); + return new Response($out, 201); } protected function deleteUserByNum(array $path): ResponseInterface { @@ -705,7 +705,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } } $meta = Arsse::$user->propertiesGet(Arsse::$user->id, false); - return new Response(['id' => (int) $path[1], 'title' => $title, 'user_id' => $meta['num']]); + return new Response(['id' => (int) $path[1], 'title' => $title, 'user_id' => $meta['num']], 201); } protected function deleteCategory(array $path): ResponseInterface { @@ -857,7 +857,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new ErrorResponse("404", 404); } } - return $this->getFeed($path); + return $this->getFeed($path)->withStatus(201); } protected function deleteFeed(array $path): ResponseInterface { diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 4e40398f..f1dd8d33 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -308,16 +308,16 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { [false, "/users/1", ['entry_sorting_direction' => "bad"], null, null, null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "entry_sorting_direction"], 422)], [false, "/users/1", ['theme' => "stark"], null, null, null, null, null, null, new ErrorResponse("403", 403)], [false, "/users/2", ['is_admin' => true], null, null, null, null, null, null, new ErrorResponse("InvalidElevation", 403)], - [false, "/users/2", ['language' => "fr_CA"], null, null, null, null, ['lang' => "fr_CA"], $out1, new Response($resp1)], - [false, "/users/2", ['entry_sorting_direction' => "asc"], null, null, null, null, ['sort_asc' => true], $out1, new Response($resp1)], - [false, "/users/2", ['entry_sorting_direction' => "desc"], null, null, null, null, ['sort_asc' => false], $out1, new Response($resp1)], + [false, "/users/2", ['language' => "fr_CA"], null, null, null, null, ['lang' => "fr_CA"], $out1, new Response($resp1, 201)], + [false, "/users/2", ['entry_sorting_direction' => "asc"], null, null, null, null, ['sort_asc' => true], $out1, new Response($resp1, 201)], + [false, "/users/2", ['entry_sorting_direction' => "desc"], null, null, null, null, ['sort_asc' => false], $out1, new Response($resp1, 201)], [false, "/users/2", ['entries_per_page' => -1], null, null, null, null, ['page_size' => -1], new UserExceptionInput("invalidNonZeroInteger"), new ErrorResponse(["InvalidInputValue", 'field' => "entries_per_page"], 422)], [false, "/users/2", ['timezone' => "Ook"], null, null, null, null, ['tz' => "Ook"], new UserExceptionInput("invalidTimezone"), new ErrorResponse(["InvalidInputValue", 'field' => "timezone"], 422)], [false, "/users/2", ['username' => "j:k"], "j:k", new UserExceptionInput("invalidUsername"), null, null, null, null, new ErrorResponse(["InvalidInputValue", 'field' => "username"], 422)], [false, "/users/2", ['username' => "ook"], "ook", new ExceptionConflict("alreadyExists"), null, null, null, null, new ErrorResponse(["DuplicateUser", 'user' => "ook"], 409)], - [false, "/users/2", ['password' => "ook"], null, null, "ook", "ook", null, null, new Response(array_merge($resp1, ['password' => "ook"]))], - [false, "/users/2", ['username' => "ook", 'password' => "ook"], "ook", true, "ook", "ook", null, null, new Response(array_merge($resp1, ['username' => "ook", 'password' => "ook"]))], - [true, "/users/1", ['theme' => "stark"], null, null, null, null, ['theme' => "stark"], $out2, new Response($resp2)], + [false, "/users/2", ['password' => "ook"], null, null, "ook", "ook", null, null, new Response(array_merge($resp1, ['password' => "ook"]), 201)], + [false, "/users/2", ['username' => "ook", 'password' => "ook"], "ook", true, "ook", "ook", null, null, new Response(array_merge($resp1, ['username' => "ook", 'password' => "ook"]), 201)], + [true, "/users/1", ['theme' => "stark"], null, null, null, null, ['theme' => "stark"], $out2, new Response($resp2, 201)], [true, "/users/3", ['theme' => "stark"], null, null, null, null, null, null, new ErrorResponse("404", 404)], ]; } @@ -465,14 +465,14 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function provideCategoryUpdates(): iterable { return [ [3, "New", "subjectMissing", new ErrorResponse("404", 404)], - [2, "New", true, new Response(['id' => 2, 'title' => "New", 'user_id' => 42])], + [2, "New", true, new Response(['id' => 2, 'title' => "New", 'user_id' => 42], 201)], [2, "Duplicate", "constraintViolation", new ErrorResponse(["DuplicateCategory", 'title' => "Duplicate"], 409)], [2, "", "missing", new ErrorResponse(["InvalidCategory", 'title' => ""], 422)], [2, " ", "whitespace", new ErrorResponse(["InvalidCategory", 'title' => " "], 422)], [2, null, "missing", new ErrorResponse(["MissingInputValue", 'field' => "title"], 422)], [2, false, "subjectMissing", new ErrorResponse(["InvalidInputType", 'field' => "title", 'actual' => "boolean", 'expected' => "string"], 422)], - [1, "New", true, new Response(['id' => 1, 'title' => "New", 'user_id' => 42])], - [1, "Duplicate", "constraintViolation", new Response(['id' => 1, 'title' => "Duplicate", 'user_id' => 42])], // This is allowed because the name of the root folder is only a duplicate in circumstances where it is used + [1, "New", true, new Response(['id' => 1, 'title' => "New", 'user_id' => 42], 201)], + [1, "Duplicate", "constraintViolation", new Response(['id' => 1, 'title' => "Duplicate", 'user_id' => 42], 201)], // This is allowed because the name of the root folder is only a duplicate in circumstances where it is used [1, "", "missing", new ErrorResponse(["InvalidCategory", 'title' => ""], 422)], [1, " ", "whitespace", new ErrorResponse(["InvalidCategory", 'title' => " "], 422)], [1, null, "missing", new ErrorResponse(["MissingInputValue", 'field' => "title"], 422)], @@ -653,7 +653,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { public function provideFeedModifications(): iterable { self::clearData(); - $success = new Response(self::FEEDS_OUT[0]); + $success = new Response(self::FEEDS_OUT[0], 201); return [ [[], [], true, $success], [[], [], new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)], @@ -672,7 +672,7 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { $this->h = $this->partialMock(V1::class); $this->h->getFeed->returns(new Response(self::FEEDS_OUT[0])); $this->dbMock->subscriptionPropertiesSet->returns(true); - $this->assertMessage(new Response(self::FEEDS_OUT[0]), $this->req("PUT", "/feeds/2112", "")); + $this->assertMessage(new Response(self::FEEDS_OUT[0], 201), $this->req("PUT", "/feeds/2112", "")); $this->dbMock->subscriptionPropertiesSet->calledWith(Arsse::$user->id, 2112, []); } From fa4ab3218a6a0f48df9b1da83c668ed0d2eb4574 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 18 Mar 2021 10:45:28 -0400 Subject: [PATCH 214/366] Version bump --- CHANGELOG | 9 +++++++++ lib/Arsse.php | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index c3476d8d..999c3a70 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +Version 0.9.1 (2021-03-18) +========================== + +Bug fixes: +- Respond to PUT requests with 201 rather than 200 in Miniflux + +Changes: +- Corrected Web server configuration in manual + Version 0.9.0 (2021-03-06) ========================== diff --git a/lib/Arsse.php b/lib/Arsse.php index 0cd7d6c5..84223cb3 100644 --- a/lib/Arsse.php +++ b/lib/Arsse.php @@ -7,7 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse; class Arsse { - public const VERSION = "0.9.0"; + public const VERSION = "0.9.1"; /** @var Factory */ public static $obj; From 4a9e66d872389328c08eebfbbcee8d68b99d1d00 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 18 Mar 2021 10:50:45 -0400 Subject: [PATCH 215/366] Fix inconsistent grammar --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 999c3a70..07ab0288 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,7 +5,7 @@ Bug fixes: - Respond to PUT requests with 201 rather than 200 in Miniflux Changes: -- Corrected Web server configuration in manual +- Correct Web server configuration in manual Version 0.9.0 (2021-03-06) ========================== From 8e063bea2f3c2f45ba13c2305fc298bdd331e89f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 8 Apr 2021 09:27:15 -0400 Subject: [PATCH 216/366] Appease GitHub again --- yarn.lock | 264 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 136 insertions(+), 128 deletions(-) diff --git a/yarn.lock b/yarn.lock index 635cd097..24fe8bd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,25 +2,25 @@ # yarn lockfile v1 -"@nodelib/fs.scandir@2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" - integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== +"@nodelib/fs.scandir@2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" + integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== dependencies: - "@nodelib/fs.stat" "2.0.3" + "@nodelib/fs.stat" "2.0.4" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" - integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== +"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" + integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== "@nodelib/fs.walk@^1.2.3": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" - integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + version "1.2.6" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" + integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== dependencies: - "@nodelib/fs.scandir" "2.1.3" + "@nodelib/fs.scandir" "2.1.4" fastq "^1.6.0" ansi-regex@^2.0.0: @@ -53,9 +53,9 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: color-convert "^2.0.1" anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -96,14 +96,14 @@ balanced-match@0.1.0: integrity sha1-tQS9BYabOSWd0MXvw12EMXbczEo= balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== binary-extensions@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" - integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== brace-expansion@^1.1.7: version "1.1.11" @@ -121,14 +121,15 @@ braces@^3.0.1, braces@~3.0.2: fill-range "^7.0.1" browserslist@^4.12.0: - version "4.14.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" - integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA== + version "4.16.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" + integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== dependencies: - caniuse-lite "^1.0.30001135" - electron-to-chromium "^1.3.571" - escalade "^3.1.0" - node-releases "^1.1.61" + caniuse-lite "^1.0.30001181" + colorette "^1.2.1" + electron-to-chromium "^1.3.649" + escalade "^3.1.1" + node-releases "^1.1.70" caller-callsite@^2.0.0: version "2.0.0" @@ -154,10 +155,10 @@ camelcase@^5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135: - version "1.0.30001151" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001151.tgz#1ddfde5e6fff02aad7940b4edb7d3ac76b0cb00b" - integrity sha512-Zh3sHqskX6mHNrqUerh+fkf0N72cMxrmflzje/JyVImfpknscMnkeJrlFGJcqTmaa0iszdYptGpWMJCRQDkBVw== +caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001181: + version "1.0.30001208" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz#a999014a35cebd4f98c405930a057a0d75352eb9" + integrity sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA== chalk@^1.1.3: version "1.1.3" @@ -188,9 +189,9 @@ chalk@^4.0.0: supports-color "^7.1.0" chokidar@^3.3.0: - version "3.4.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" - integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== + version "3.5.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" + integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -200,7 +201,7 @@ chokidar@^3.3.0: normalize-path "~3.0.0" readdirp "~3.5.0" optionalDependencies: - fsevents "~2.1.2" + fsevents "~2.3.1" cliui@^6.0.0: version "6.0.0" @@ -257,9 +258,9 @@ color@^0.11.0: color-string "^0.3.0" colorette@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" - integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== concat-map@0.0.1: version "0.0.1" @@ -286,12 +287,12 @@ css-color-function@~1.3.3: debug "^3.1.0" rgb "~0.1.0" -css-tree@1.0.0-alpha.39: - version "1.0.0-alpha.39" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb" - integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA== +css-tree@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== dependencies: - mdn-data "2.0.6" + mdn-data "2.0.14" source-map "^0.6.1" cssesc@^3.0.0: @@ -300,16 +301,16 @@ cssesc@^3.0.0: integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== csso@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.3.tgz#0d9985dc852c7cc2b2cacfbbe1079014d1a8e903" - integrity sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ== + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== dependencies: - css-tree "1.0.0-alpha.39" + css-tree "^1.1.2" debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" @@ -330,10 +331,10 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -electron-to-chromium@^1.3.571: - version "1.3.583" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.583.tgz#47a9fde74740b1205dba96db2e433132964ba3ee" - integrity sha512-L9BwLwJohjZW9mQESI79HRzhicPk1DFgM+8hOCfGgGCFEcA3Otpv7QK6SGtYoZvfQfE3wKLh0Hd5ptqUFv3gvQ== +electron-to-chromium@^1.3.649: + version "1.3.710" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.710.tgz#b33d316e5d6de92b916e766d8a478d19796ffe11" + integrity sha512-b3r0E2o4yc7mNmBeJviejF1rEx49PUBi+2NPa7jHEX3arkAXnVgLhR0YmV8oi6/Qf3HH2a8xzQmCjHNH0IpXWQ== emoji-regex@^8.0.0: version "8.0.0" @@ -347,7 +348,7 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -escalade@^3.1.0: +escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== @@ -363,9 +364,9 @@ esprima@^4.0.0: integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== fast-glob@^3.1.1: - version "3.2.4" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" - integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" + integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -375,9 +376,9 @@ fast-glob@^3.1.1: picomatch "^2.2.1" fastq@^1.6.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" - integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== + version "1.11.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" + integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== dependencies: reusify "^1.0.4" @@ -397,19 +398,19 @@ find-up@^4.1.0: path-exists "^4.0.0" fs-extra@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" - integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== dependencies: at-least-node "^1.0.0" graceful-fs "^4.2.0" jsonfile "^6.0.1" - universalify "^1.0.0" + universalify "^2.0.0" -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +fsevents@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== function-bind@^1.1.1: version "1.1.1" @@ -427,9 +428,9 @@ get-stdin@^8.0.0: integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== glob-parent@^5.1.0, glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" @@ -445,9 +446,9 @@ glob@^6.0.4: path-is-absolute "^1.0.0" globby@^11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" - integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + version "11.0.3" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" + integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" @@ -457,9 +458,9 @@ globby@^11.0.0: slash "^3.0.0" graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== has-ansi@^2.0.0: version "2.0.0" @@ -536,9 +537,9 @@ inherits@2: integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== ip-regex@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.2.0.tgz#a03f5eb661d9a154e3973a03de8b23dd0ad6892e" - integrity sha512-n5cDDeTWWRwK1EBoWwRti+8nP4NbytBBY0pldmnIkq6Z55KNFmWofh4rl9dPZpj+U/nVq7gweR3ylrvMt4YZ5A== + version "4.3.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" + integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== is-arrayish@^0.2.1: version "0.2.1" @@ -552,10 +553,10 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-core-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.0.0.tgz#58531b70aed1db7c0e8d4eb1a0a2d1ddd64bd12d" - integrity sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw== +is-core-module@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" + integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== dependencies: has "^1.0.3" @@ -599,9 +600,9 @@ js-base64@^2.1.9: integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== js-yaml@^3.13.1: - version "3.14.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" - integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -612,11 +613,11 @@ json-parse-better-errors@^1.0.1: integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== jsonfile@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" - integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== dependencies: - universalify "^1.0.0" + universalify "^2.0.0" optionalDependencies: graceful-fs "^4.1.6" @@ -628,9 +629,9 @@ locate-path@^5.0.0: p-locate "^4.1.0" lodash@^4.17.11: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-symbols@^2.2.0: version "2.2.0" @@ -639,10 +640,10 @@ log-symbols@^2.2.0: dependencies: chalk "^2.0.1" -mdn-data@2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" - integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== merge2@^1.3.0: version "1.4.1" @@ -665,14 +666,14 @@ micromatch@^4.0.2: brace-expansion "^1.1.7" ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -node-releases@^1.1.61: - version "1.1.64" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.64.tgz#71b4ae988e9b1dd7c1ffce58dd9e561752dfebc5" - integrity sha512-Iec8O9166/x2HRMJyLLLWkd0sFFLrFNy+Xf+JQfSQsdBJzPcHpNl3JQ9gD4j+aJxmCa25jNsIbM4bmACtSbkSg== +node-releases@^1.1.70: + version "1.1.71" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" + integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -944,6 +945,11 @@ pretty-hrtime@^1.0.3: resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + read-cache@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" @@ -974,11 +980,11 @@ resolve-from@^3.0.0: integrity sha1-six699nWiBvItuZTM17rywoYh0g= resolve@^1.1.7: - version "1.18.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130" - integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA== + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== dependencies: - is-core-module "^2.0.0" + is-core-module "^2.2.0" path-parse "^1.0.6" reusify@^1.0.4: @@ -992,9 +998,11 @@ rgb@~0.1.0: integrity sha1-vieykej+/+rBvZlylyG/pA/AN7U= run-parallel@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" - integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" set-blocking@^2.0.0: version "2.0.0" @@ -1022,9 +1030,9 @@ sprintf-js@~1.0.2: integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" @@ -1078,9 +1086,9 @@ supports-color@^7.1.0: has-flag "^4.0.0" tlds@^1.203.0: - version "1.212.0" - resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.212.0.tgz#f3f29bd5d10d0fd9a6f171a5f9d57d58b71eccf7" - integrity sha512-03rYYO1rGhOYpdYB+wlLY2d0xza6hdN/S67ol2ZpaH+CtFedMVAVhj8ft0rwxEkr90zatou8opBv7Xp6X4cK6g== + version "1.219.0" + resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.219.0.tgz#7e636062386a1f3c9184356de93d40842ffe91d9" + integrity sha512-o4g9c8kXCmTDwUnK/9HpTT9o/GNH85KCvs+S5SgUw5yILdECvMmTGzK7ngoWMp97P5tfYr8fZeF16YhgV/l90A== to-regex-range@^5.0.1: version "5.0.1" @@ -1094,10 +1102,10 @@ uniq@^1.0.1: resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= -universalify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" - integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== url-regex@^5.0.0: version "5.0.0" @@ -1132,9 +1140,9 @@ wrappy@1: integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== yargs-parser@^18.1.2: version "18.1.3" From 035feae0ce8faee6aef27d2ecdb0e7ca47d654af Mon Sep 17 00:00:00 2001 From: Dustin Wilson Date: Fri, 9 Apr 2021 16:31:04 -0500 Subject: [PATCH 217/366] Removed postcss in favor of sass for building manual theme --- README.md | 2 +- RoboFile.php | 7 +- docs/theme/arsse/arsse.css | 3 +- docs/theme/arsse/daux.min.js | 2 +- docs/theme/src/arsse.scss | 8 +- package.json | 14 +- postcss.config.js | 17 - yarn.lock | 1076 +--------------------------------- 8 files changed, 23 insertions(+), 1106 deletions(-) delete mode 100644 postcss.config.js diff --git a/README.md b/README.md index 74831aa4..8a7e76d1 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ The `/vendor-bin/` directory houses the files needed for the tools used in The A | `/robo` | Simple wrapper for executing Robo on POSIX systems | | `/robo.bat` | Simple wrapper for executing Robo on Windows | -In addition the files `/package.json`, `/yarn.lock`, and `/postcss.config.js` as well as the `/node_modules/` directory are used by [Yarn](https://yarnpkg.com/) and [PostCSS](https://postcss.org/) when modifying the stylesheet for The Arsse's manual. +In addition the files `/package.json`, `/yarn.lock`, and the `/node_modules/` directory are used by [Yarn](https://yarnpkg.com/) and [Sass](https://sass-lang.com/) when modifying the stylesheet for The Arsse's manual. # Common tasks diff --git a/RoboFile.php b/RoboFile.php index ec6457cf..f9ba27e4 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -190,8 +190,7 @@ class RoboFile extends \Robo\Tasks { $dir."robo", $dir."robo.bat", $dir."package.json", - $dir."yarn.lock", - $dir."postcss.config.js", + $dir."yarn.lock" ]); // generate a sample configuration file $t->taskExec(escapeshellarg(\PHP_BINARY)." arsse.php conf save-defaults config.defaults.php")->dir($dir); @@ -230,7 +229,7 @@ class RoboFile extends \Robo\Tasks { * Daux's theme changes */ public function manualTheme(array $args): Result { - $postcss = escapeshellarg(norm(BASE."node_modules/.bin/postcss")); + $sass = escapeshellarg(norm(BASE."node_modules/.bin/sass")); $themesrc = norm(BASE."docs/theme/src/").\DIRECTORY_SEPARATOR; $themeout = norm(BASE."docs/theme/arsse/").\DIRECTORY_SEPARATOR; $dauxjs = norm(BASE."vendor-bin/daux/vendor/daux/daux.io/themes/daux/js/").\DIRECTORY_SEPARATOR; @@ -239,7 +238,7 @@ class RoboFile extends \Robo\Tasks { // install dependencies via Yarn $t->taskExec("yarn install"); // compile the stylesheet - $t->taskExec($postcss)->arg($themesrc."arsse.scss")->option("-o", $themeout."arsse.css"); + $t->taskExec($sass)->arg('--no-source-map')->option('--style', 'compressed')->arg("{$themesrc}arsse.scss")->arg("{$themeout}arsse.css"); // copy JavaScript files from the Daux theme foreach (glob($dauxjs."daux*.js") as $file) { $t->taskFilesystemStack()->copy($file, $themeout.basename($file), true); diff --git a/docs/theme/arsse/arsse.css b/docs/theme/arsse/arsse.css index 9971fbaa..b8def289 100644 --- a/docs/theme/arsse/arsse.css +++ b/docs/theme/arsse/arsse.css @@ -1,2 +1 @@ -/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ -html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;font-size:14px}body{margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress,sub,sup{vertical-align:baseline}.s-content pre code:after,.s-content pre code:before,[hidden],template{display:none}a{background-color:transparent;-webkit-text-decoration-skip:objects;text-decoration:none;color:#e63c2f}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}.s-content blockquote cite,dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;clear:both;margin:1em 0;border:0;border-top:1px solid #ddd}button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:700}button,hr,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}*,:after,:before{box-sizing:border-box}@media (min-width:850px){html{font-size:16px}}body,html{height:100%;background-color:#fff;color:#15284b}.Columns__left{background-color:#e8d5d3}.Columns__right__content{padding:10px;background-color:#fff}@media (max-width:768px){html:not(.no-js) .Collapsible__content{height:0;overflow:hidden;transition:height 400ms ease-in-out}}.Collapsible__trigger{margin:12px;padding:7px 10px;background-color:transparent;border:0;float:right;background-image:none;filter:none;box-shadow:none}.Collapsible__trigger__bar{display:block;width:18px;height:2px;margin-top:2px;margin-bottom:3px;background-color:#e8d5d3}.Collapsible__trigger:hover{background-color:#93b7bb;box-shadow:none}.Collapsible__trigger:hover .Collapsible__trigger__bar{background-color:#15284b}@media screen and (min-width:769px){body{background-color:#15284b}.Navbar{position:fixed;z-index:1030;width:100%}.Collapsible__trigger{display:none!important}.Collapsible__content{display:block!important}.Columns{height:100%}.Columns:after,.Columns:before{content:" ";display:table}.Columns:after{clear:both}.Columns__left,.Columns__right{position:relative;min-height:1px;float:left;overflow:auto;height:100%}.Columns__left{width:25%;border-right:1px solid #e7e7e9;overflow-x:hidden}.Columns__right{width:75%}.Columns__right__content{padding:0 20px 20px;min-height:100%}}.Page{max-width:860px}body{font-family:"Cabin","Trebuchet MS",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-feature-settings:"kern" 1;-webkit-font-kerning:normal;font-kerning:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;line-height:1.618}h1,h2,h3,h4,h5,h6{font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.s-content h1,.s-content h2,.s-content h3,.s-content h4,.s-content h5,.s-content h6{cursor:text;line-height:1.4em;margin:2em 0 .5em}.s-content h1 code,.s-content h1 tt,.s-content h2 code,.s-content h2 tt,.s-content h3 code,.s-content h3 tt,.s-content h4 code,.s-content h4 tt,.s-content h5 code,.s-content h5 tt,.s-content h6 code,.s-content h6 tt{font-size:inherit}.s-content h1 i,.s-content h2 i,.s-content h3 i,.s-content h4 i,.s-content h5 i,.s-content h6 i{font-size:.7em}.s-content h1 p,.s-content h2 p,.s-content h3 p,.s-content h4 p,.s-content h5 p,.s-content h6 p{margin-top:0}.s-content h1{margin-top:0;font-size:2.618rem}.s-content h2{font-size:2rem}.s-content h3{font-size:1.618rem}.s-content h4,.s-content h5,.s-content h6,.s-content small{font-size:1.309rem}.s-content a{text-decoration:underline}.s-content p{margin-bottom:1.3em}.s-content ol,.s-content ul{padding-left:2em}.s-content ul p,.s-content ul ul{margin:0}.s-content dl{padding:0}.s-content dl dt{font-weight:700;font-style:italic;padding:0;margin:15px 0 5px}.s-content dl dt:first-child{padding:0}.s-content dl dd{margin:0 0 15px;padding:0 15px}.s-content blockquote{margin:.75em 2em;padding:.5em 1em;font-style:italic;border-left:.25em solid #15284b}.s-content blockquote cite:before{content:"\2014";padding-right:.5em}.s-content table{width:100%;padding:0;margin-bottom:1em;border-collapse:separate;border-spacing:2px;border:2px solid #b3aab1}.s-content table+table{margin-top:1em}.s-content table tr{background-color:#fff;margin:0;padding:0;border-top:0}.s-content table tr:nth-child(2n){background-color:transparent}.s-content table th{font-weight:700;background:#e8d5d3}.s-content table td,.s-content table th{margin:0;padding:.5em}.s-content blockquote>:first-child,.s-content dl dd>:first-child,.s-content dl dt>:first-child,.s-content ol>:first-child,.s-content table td>:first-child,.s-content table th>:first-child,.s-content ul>:first-child{margin-top:0}.s-content blockquote>:last-child,.s-content dl dd>:last-child,.s-content dl dt>:last-child,.s-content ol>:last-child,.s-content table td>:last-child,.s-content table th>:last-child,.s-content ul>:last-child{margin-bottom:0}.s-content img{max-width:100%;display:block;margin:0 auto}.s-content code{font-family:Monaco,Menlo,Consolas,"Lucida Console","Courier New",monospace;padding-top:.1rem;padding-bottom:.1rem;background:#f9f5f4;border-radius:0;box-shadow:none;display:inline-block;padding:.5ch;border:0}.s-content code:after,.s-content code:before{letter-spacing:-.2em;content:"\00a0"}.s-content pre{background:#f5f2f0;line-height:1.5em;overflow:auto;border:0;border-radius:0;padding:.75em 20px;margin:0 -20px 20px}.s-content pre code{margin:0;padding:0;white-space:pre;box-shadow:none}.s-content pre code,.s-content pre tt{background-color:transparent;border:0}.s-content ins,.s-content u{text-decoration:none;border-bottom:1px solid #15284b}.s-content del a,.s-content ins a,.s-content u a{color:inherit}a.Link--external:after{content:" " url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAVklEQVR4Xn3PgQkAMQhDUXfqTu7kTtkpd5RA8AInfArtQ2iRXFWT2QedAfttj2FsPIOE1eCOlEuoWWjgzYaB/IkeGOrxXhqB+uA9Bfcm0lAZuh+YIeAD+cAqSz4kCMUAAAAASUVORK5CYII=)}a.Link--broken{color:red}p{margin:0 0 1em}.Button{display:inline-block;text-align:center;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;margin-bottom:0}.Button--small{font-size:12px;line-height:1.5;border-radius:3px}.Button--default{color:#333;background-color:#fff;border-color:#ccc}.Button--default.Button--active{color:#333;background-color:#e6e6e6;border-color:#adadad}.Brand,.Nav__item a:hover{color:#15284b;text-shadow:none}.Brand,.Navbar{background-color:#e63c2f}.Brand{display:block;padding:.75em .6em;font-size:2rem;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.Navbar{box-shadow:0 1px 5px rgba(0,0,0,.25);margin-bottom:0}.CodeToggler{padding:0 20px}.CodeToggler__text{font-size:12px;line-height:1.5;padding:6px 10px 6px 0;display:inline-block;vertical-align:middle}.no-js .CodeToggler{display:none}.Nav{margin:0;padding:0}.Nav__arrow{display:inline-block;position:relative;width:16px;margin-left:-16px}.Nav__arrow:before{position:absolute;display:block;content:"";margin:-.25em 0 0 -.4em;left:50%;top:50%;border-right:.15em solid #15284b;border-top:.15em solid #15284b;transform:rotate(45deg);transition-duration:.3s}.Nav__item,.Nav__item a{display:block}.Nav__item a{margin:0;padding:6px 15px 6px 20px;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-weight:400;color:#15284b;text-shadow:none}.Nav__item a:hover{background-color:#93b7bb}.Nav .Nav{margin-left:15px}html:not(.no-js) .Nav .Nav{height:0;transition:height 400ms ease-in-out;overflow:hidden}.Nav .Nav .Nav__item a{margin:0 0 0 -15px;padding:3px 30px;font-family:"Cabin","Trebuchet MS",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;color:#15284b;opacity:.7}.HomepageButtons .Button--hero:hover,.Nav .Nav .Nav__item a:hover{opacity:1}.Nav .Nav .Nav__item--active a{color:#15284b}.Nav__item--active>a,.Nav__item--open>a{background-color:#93b7bb}.Nav__item--open>a>.Nav__arrow:before{margin-left:-.25em;transform:rotate(135deg)}.Page__header{margin:0 0 10px;padding:0}.Page__header:after,.Page__header:before{content:" ";display:table}.Page__header:after{clear:both}.Page__header h1{margin:0;padding:0;line-height:57px}.Page__header--separator{height:.6em}.Page__header a{text-decoration:none}.Page__header .EditOn,.Page__header .ModifiedDate{float:left;font-size:10px;color:gray}.Page__header .EditOn{float:right}.Links,.Twitter{padding:0 20px}.Links a{font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-weight:400;color:#15284b;line-height:2em}.Twitter{font:11px/18px "Helvetica Neue",Arial,sans-serif}.Twitter__button{text-decoration:none;display:inline-block;vertical-align:top;zoom:1;position:relative;height:20px;box-sizing:border-box;padding:1px 8px 1px 6px;background-color:#1b95e0;color:#fff;border-radius:3px;font-weight:500;cursor:pointer}.Twitter__button .Twitter__button__label{display:inline-block;vertical-align:top;zoom:1;margin-left:3px;white-space:nowrap}.Twitter__button svg{position:relative;top:2px;display:inline-block;width:14px;height:14px}.PoweredBy{padding:0 20px 1rem;font-size:1.309rem}.Search{position:relative}.Search__field{display:block;width:100%;height:34px;padding:6px 30px 6px 20px;color:#555;border-width:0 0 1px;border-bottom:1px solid #ccc;background:#fff;transition:border-color ease-in-out .15s}.Search__field:focus{border-color:#93b7bb;outline:0}.Search__icon{position:absolute;right:9px;top:9px;width:16px;height:16px}.Navbar .Search{float:right;margin:8px 20px}.Navbar .Search__field{box-shadow:inset 0 1px 1px rgba(0,0,0,.075);border-width:0;border-radius:4px;padding-left:10px}.TableOfContentsContainer{float:right;min-width:300px;max-width:25%;padding-left:1em}.TableOfContentsContainer__title{margin-bottom:0!important}.TableOfContentsContainer__content{border:1px solid #efefef;border-width:4px 2px 2px 6px}.TableOfContentsContainer__content>.TableOfContents>li+li{border-top:1px solid #ddd}ul.TableOfContents{font-size:1rem;padding-left:0;margin:0;list-style-type:none;border-left:6px solid #e8d5d3}ul.TableOfContents p{margin-bottom:0}ul.TableOfContents a{text-decoration:none;display:block;padding:.2em 0 .2em .75em}ul.TableOfContents .TableOfContents{padding-left:.75em}.Pager{padding-left:0;margin:1em 0;list-style:none;text-align:center}.Pager:after,.Pager:before{content:" ";display:table}.Pager,.Pager:after{clear:both}.Pager li{display:inline}.Pager li>a{display:inline-block;padding:5px 14px;background-color:#fff}.Pager li>a:focus,.Pager li>a:hover{text-decoration:none}.Pager--next>a{float:right}.Pager--prev>a{float:left}.Checkbox{position:relative;display:block;padding-left:30px;cursor:pointer}.Checkbox input{position:absolute;z-index:-1;opacity:0}.Checkbox__indicator{position:absolute;top:50%;left:0;width:20px;height:20px;margin-top:-10px;background:#e6e6e6}.Checkbox__indicator:after{position:absolute;display:none;content:""}.Checkbox input:focus~.Checkbox__indicator,.Checkbox:hover input~.Checkbox__indicator{background:#ccc}.Checkbox input:checked~.Checkbox__indicator{background:#15284b}.Checkbox input:checked~.Checkbox__indicator:after{display:block}.Checkbox input:checked:focus~.Checkbox__indicator,.Checkbox:hover input:not([disabled]):checked~.Checkbox__indicator{background:#93b7bb}.Checkbox input:disabled~.Checkbox__indicator{pointer-events:none;opacity:.6;background:#e6e6e6}.Checkbox .Checkbox__indicator:after{top:4px;left:8px;width:5px;height:10px;transform:rotate(45deg);border:solid #fff;border-width:0 2px 2px 0}.Checkbox input:disabled~.Checkbox__indicator:after{border-color:#7b7b7b}.Hidden{display:none}.Container{margin-right:auto;margin-left:auto}.Container--inner{width:80%;margin:0 auto}@media (min-width:1200px){.Container{width:1170px}}@media (min-width:992px){.Container{width:970px}}@media (min-width:769px){.Container{width:750px}}.Homepage{background-color:#fff;border-radius:0;border:0;color:#15284b;overflow:hidden;padding-bottom:0;margin-bottom:0;box-shadow:none}.HomepageTitle h2{width:80%;font-size:30px;margin:20px auto;text-align:center}.HomepageImage img{display:block;max-width:80%;margin:0 auto;height:auto}.HomepageButtons{padding:20px 0;background-color:#e8d5d3;text-align:center}.HomepageButtons:after,.HomepageButtons:before{content:" ";display:table}.HomepageButtons:after{clear:both}.HomepageButtons .Button--hero{padding:20px 30px;border-radius:0;text-shadow:none;opacity:.8;margin:0 10px;text-transform:uppercase;border:5px solid #15284b;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;background-image:none;filter:none;box-shadow:none}@media (max-width:768px){.HomepageButtons .Button--hero{display:block;margin-bottom:10px}}.HomepageButtons .Button--hero.Button--secondary{background-color:#93b7bb;color:#15284b}.HomepageButtons .Button--hero.Button--primary{background-color:#15284b;color:#e8d5d3}.HomepageContent{background-color:#fff;padding:40px 0}.HomepageContent ol li,.HomepageContent ul li{list-style:none;margin-bottom:.5em;position:relative}.HomepageContent ol li:before,.HomepageContent ul li:before{position:absolute;top:50%;left:-1.5em;content:"";width:0;height:0;border:.5em solid transparent;border-left:.5em solid #93b7bb;float:left;display:block;margin-top:-.5em}.HomepageContent .HeroText,.HomepageFooter__links li a{font-size:16px;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.HomepageContent .HeroText{font-weight:300;margin-bottom:20px;line-height:1.4}@media (min-width:769px){.HomepageContent{padding:40px 20px}.HomepageContent .HeroText{font-size:21px}.HomepageContent .Row{margin:0 -15px}.HomepageContent .Row__half,.HomepageContent .Row__quarter,.HomepageContent .Row__third{float:left;position:relative;min-height:1px;padding-left:15px;padding-right:15px}.HomepageContent .Row__third{width:33.333333%}.HomepageContent .Row__half{width:50%}.HomepageContent .Row__quarter{width:25%}}.HomepageFooter{background-color:#15284b;color:#93b7bb;border:0;box-shadow:none}.HomepageFooter:after,.HomepageFooter:before{content:" ";display:table}.HomepageFooter:after{clear:both}@media (max-width:768px){.HomepageFooter{padding:0 20px;text-align:center}.HomepageFooter .HomepageFooter__links{padding-left:0;list-style-type:none}}@media (min-width:769px){.HomepageFooter .HomepageFooter__links{float:left}.HomepageFooter .HomepageFooter__twitter{float:right}}.HomepageFooter__links,.HomepageFooter__twitter{margin:40px 0}.HomepageFooter__links li a{line-height:32px;font-weight:700}.HomepageFooter__links li a:hover{text-decoration:underline}.HomepageFooter .Twitter__button{margin-bottom:20px}@media print{*{text-shadow:none!important;color:#000!important;background:0 0!important;box-shadow:none!important}h1,h2,h3,h4,h5,h6{page-break-after:avoid;page-break-before:auto}blockquote,img,pre{page-break-inside:avoid}blockquote,pre{border:1px solid #999;font-style:italic}img{border:0}a,a:visited{text-decoration:underline}abbr[title]:after{content:" (" attr(title) ")"}q{quotes:none}.s-content a[href^="#"]:after,q:before{content:""}q:after{content:" (" attr(cite) ")"}.PageBreak{display:block;page-break-before:always}.NoPrint,.Pager,aside{display:none}.Columns__right{width:100%!important}.s-content a:after{content:" (" attr(href) ")";font-size:80%;word-wrap:break-word}h1 a[href]:after{font-size:50%}}@font-face{font-family:'League Gothic';src:url(fonts/leaguegothic.woff2) format('woff2'),url(fonts/leaguegothic.woff) format('woff');font-style:normal;font-display:swap}@font-face{font-family:'Cabin';src:url(fonts/cabin-regular.woff2) format('woff2'),url(fonts/cabin-regular.woff) format('woff');font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:'Cabin';src:url(fonts/cabin-italic.woff2) format('woff2'),url(fonts/cabin-italic.woff) format('woff');font-style:italic;font-display:swap}@font-face{font-family:'Cabin';src:url(fonts/cabin-bold.woff2) format('woff2'),url(fonts/cabin-bold.woff) format('woff');font-weight:700;font-style:normal;font-display:swap}.s-content code::after,.s-content code::before,a.Link--external::after{content:''}pre .s-content code{display:inline}.s-content table tbody,.s-content table thead{background-color:#fff}.s-content table tr:nth-child(2n) td{background-color:#f9f5f4}.s-content table td,.s-content table th{border:0}.Nav__item .Nav__item,.s-content table{font-size:1rem}.Brand,h1,h2,h3,h4,h5,h6{font-weight:400}.Button,.Pager li>a{border-radius:0}.HomepageButtons .Button--hero{font-weight:400;font-size:1.309rem}.Page__header{border-bottom:2px solid #e8d5d3}.Pager li>a{border:2px solid #e8d5d3}.Pager li>a:focus,.Pager li>a:hover{background-color:#e8d5d3}.Pager--prev a::before{content:"\2190\00a0"}.Pager--next a::after{content:"\00a0\2192"}.Navbar{height:auto;box-shadow:none}.Navbar .Brand{float:none;line-height:inherit;height:auto}.Homepage{padding-top:10px!important}.Nav__item{font-size:1.309rem}.Nav .Nav .Nav__item a .Nav__arrow:before,.Nav__arrow:before{font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;width:1ch;height:1ch}.TableOfContentsContainer__title{border-bottom:4px solid #e8d5d3}.Columns__right--full .TableOfContentsContainer .TableOfContentsContainer__content>.TableOfContents{border-right:2px solid #e8d5d3}.Columns__right--full .TableOfContentsContainer a{border-bottom:1px solid #e8d5d3}.clients thead tr:first-child th{text-align:left}.clients thead tr:first-child th:first-child{width:15%}.clients thead tr:first-child th:nth-child(3){width:50%;text-align:center}.clients thead tr+tr th{width:12%;text-align:center}.clients tbody td:nth-child(3),.clients tbody td:nth-child(4),.clients tbody td:nth-child(5),.clients tbody td:nth-child(6){text-align:center}.clients tbody td.Y{color:#2c9a42}.clients tbody td.N{color:#e63c2f}.hljs,.s-content pre{background:#15284b;color:#e8d5d3}.hljs{display:block;overflow-x:auto;padding:.5em}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-comment,.hljs-quote{color:#978e9c}.hljs-addition,.hljs-keyword,.hljs-selector-tag{color:#acb39a}.hljs-doctag,.hljs-literal,.hljs-meta .hljs-meta-string,.hljs-number,.hljs-regexp,.hljs-string{color:#93b7bb}.hljs-name,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-title{color:#82b7e5}.hljs-attr,.hljs-attribute,.hljs-class .hljs-title,.hljs-template-variable,.hljs-type,.hljs-variable{color:#c5b031}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-meta .hljs-keyword,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-subst,.hljs-symbol{color:#ea8031}.hljs-built_in,.hljs-deletion{color:#e63c2f}.hljs-formula{background:#686986}@media (min-width:850px){.Columns__left{border:0}} \ No newline at end of file +/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress{vertical-align:baseline}template,[hidden]{display:none}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:bold}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}:root{--gray: #7a8288;--dark-gray: color(var(--gray) blend(#000 40%));--light-gray: color(var(--gray) blend(#fff 50%));--lighter-gray: color(var(--gray) blend(#fff 70%));--lightest-gray: color(var(--gray) blend(#fff 90%));--type-size-1: 2.441rem;--type-size-2: 1.953rem;--type-size-3: 1.563rem;--type-size-4: 1.25rem;--type-size-5: 1rem;--type-size-6: 0.75rem;--dark: #3f4657;--light: #82becd;--text: #222;--link-color: var(--light);--brand-color: var(--light);--brand-background: var(--dark);--sidebar-border: #e7e7e9;--sidebar-background: #f7f7f7;--sidebar-link-color: var(--dark);--sidebar-link-active-background: #c5c5cb;--sidebar-link-hover-background: var(--sidebar-link-active-background);--sidebar-link-arrow-color: var(--dark);--sidebar-link-secondary-color: var(--text);--checkbox-background: #e6e6e6;--checkbox-hover-background: #ccc;--checkbox-checked-background: var(--dark);--checkbox-checked-hover-background: var(--light);--checkbox-tick-color: #fff;--checkbox-disabled-background: #e6e6e6;--checkbox-disabled-tick-color: #7b7b7b;--search-field-color: #555;--search-field-border-color: #ccc;--search-field-background: #fff;--search-field-hover-border-color: var(--light);--sidebar-collapsible--hamburger-color: var(--light);--sidebar-collapsible--hamburger-hover-color: var(--dark);--sidebar-collapsible--hamburger-hover-background: var(--light);--homepage-navbar-background: var(--dark);--homepage-hero-background: var(--light);--homepage-hero-color: var(--dark);--homepage-bullet-color: var(--light);--homepage-footer-color: var(--light);--homepage-footer-background: var(--dark);--hero-button-block-background: var(--sidebar-link-active-background);--hero-button-border-color: var(--dark);--hero-button-primary-color: var(--sidebar-background);--hero-buttom-primary-background: var(--dark);--hero-button-secondary-color: var(--dark);--hero-button-secondary-background: var(--sidebar-link-active-background);--content-floating-blocks-background: var(--light);--code-tag-color: var(--dark);--code-tag-background-color: #fafafa;--code-tag-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.125);--code-tag-border-radius: 4px;--blockquote-border-color: var(--dark)}@custom-media --viewport-small (width < 850px);@custom-media --viewport-large (width >= 850px);*,*:after,*:before{box-sizing:border-box}body{margin:0;padding:0}html{font-size:14px}@media(--viewport-large){html{font-size:16px}}html,body{height:100%;background-color:#fff;color:var(--text)}.Columns__left{background-color:var(--sidebar-background)}.Columns__right__content{padding:10px;background-color:#fff}@media(max-width: 768px){html:not(.no-js) .Collapsible__content{height:0;overflow:hidden;transition:height 400ms ease-in-out}}.Collapsible__trigger{margin:12px;padding:7px 10px;background-color:transparent;border:none;float:right;background-image:none;filter:none;box-shadow:none}.Collapsible__trigger__bar{display:block;width:18px;height:2px;margin-top:2px;margin-bottom:3px;background-color:var(--sidebar-collapsible--hamburger-color)}.Collapsible__trigger:hover{background-color:var(--sidebar-collapsible--hamburger-hover-background);box-shadow:none}.Collapsible__trigger:hover .Collapsible__trigger__bar{background-color:var(--sidebar-collapsible--hamburger-hover-color)}@media screen and (min-width: 769px){body{background-color:var(--content-floating-blocks-background)}.Navbar{position:fixed;z-index:1030;width:100%}.Collapsible__trigger{display:none !important}.Collapsible__content{display:block !important}.Columns{height:100%}.Columns:after,.Columns:before{content:" ";display:table}.Columns:after{clear:both}.Columns__left,.Columns__right{position:relative;min-height:1px;float:left;overflow:auto;height:100%}.Columns__left{width:25%;border-right:1px solid var(--sidebar-border);overflow-x:hidden}.Columns__right{width:75%}.Columns__right__content{padding:0 20px 20px;min-height:100%}}.Page{max-width:860px}.u-visuallyHidden{position:absolute !important;height:1px;width:1px;overflow:hidden;clip:rect(1px 1px 1px 1px);clip:rect(1px, 1px, 1px, 1px);white-space:nowrap}body{line-height:1.5;font-family:var(--font-family-text);font-feature-settings:"kern" 1;font-kerning:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}h1,h2,h3,h4,h5,h6{font-family:var(--font-family-heading);font-weight:300}.s-content h1,.s-content h2,.s-content h3,.s-content h4,.s-content h5,.s-content h6{cursor:text;line-height:1.4em;margin:2em 0 .5em}.s-content h1 tt,.s-content h1 code,.s-content h2 tt,.s-content h2 code,.s-content h3 tt,.s-content h3 code,.s-content h4 tt,.s-content h4 code,.s-content h5 tt,.s-content h5 code,.s-content h6 tt,.s-content h6 code{font-size:inherit}.s-content h1 i,.s-content h2 i,.s-content h3 i,.s-content h4 i,.s-content h5 i,.s-content h6 i{font-size:.7em}.s-content h1 p,.s-content h2 p,.s-content h3 p,.s-content h4 p,.s-content h5 p,.s-content h6 p{margin-top:0}.s-content h1{margin-top:0;font-size:var(--type-size-3)}.s-content h2{font-size:var(--type-size-4)}.s-content h3{font-size:var(--type-size-5)}.s-content h4{font-size:var(--type-size-6)}.s-content h5{font-size:var(--type-size-6)}.s-content h6{font-size:var(--type-size-6)}.s-content a{text-decoration:underline}.s-content small{font-size:var(--type-size-6)}.s-content p{margin-bottom:1.3em}.s-content ul,.s-content ol{padding-left:2em}.s-content ul p{margin:0}.s-content ul ul{margin:0}.s-content dl{padding:0}.s-content dl dt{font-weight:bold;font-style:italic;padding:0;margin:15px 0 5px}.s-content dl dt:first-child{padding:0}.s-content dl dd{margin:0 0 15px;padding:0 15px}.s-content blockquote{margin:.75em 2em;padding:.5em 1em;font-style:italic;border-left:.25em solid var(--blockquote-border-color)}.s-content blockquote cite{font-style:italic}.s-content blockquote cite:before{content:"—";padding-right:.5em}.s-content table{width:100%;padding:0;margin-bottom:1em;border-collapse:collapse;font-size:var(--type-size-6)}.s-content table+table{margin-top:1em}.s-content table tr{border-top:1px solid #eee;background-color:#fff;margin:0;padding:0}.s-content table tr:nth-child(2n){background-color:var(--lightest-gray)}.s-content table th{font-weight:bold;border:1px solid var(--light-gray);background:var(--lighter-gray);margin:0;padding:.5em}.s-content table td{border:1px solid var(--lighter-gray);margin:0;padding:.5em}.s-content ul>:first-child,.s-content ol>:first-child,.s-content blockquote>:first-child,.s-content dl dt>:first-child,.s-content dl dd>:first-child,.s-content table th>:first-child,.s-content table td>:first-child{margin-top:0}.s-content ul>:last-child,.s-content ol>:last-child,.s-content blockquote>:last-child,.s-content dl dt>:last-child,.s-content dl dd>:last-child,.s-content table th>:last-child,.s-content table td>:last-child{margin-bottom:0}.s-content img{max-width:100%;display:inline-block}.s-content code{font-family:var(--font-family-monospace);padding-top:.1rem;padding-bottom:.1rem;background:var(--code-tag-background-color);border:1px solid var(--light-gray);border-radius:var(--code-tag-border-radius);box-shadow:var(--code-tag-box-shadow)}.s-content code:before,.s-content code:after{letter-spacing:-0.2em;content:" "}.s-content pre{background:#f5f2f0;color:#333;line-height:1.5em;overflow:auto;border:none;border-radius:0;padding:.75em 20px;margin:0 -20px 20px -20px}.s-content pre code{margin:0;padding:0;white-space:pre;box-shadow:none}.s-content pre code:before,.s-content pre code:after{display:none}.s-content pre code,.s-content pre tt{background-color:transparent;border:none}.s-content u,.s-content ins{text-decoration:none;border-bottom:1px solid var(--text)}.s-content u a,.s-content ins a{color:inherit}.s-content del a{color:inherit}a{text-decoration:none;color:var(--link-color)}a.Link--external:after{content:" " url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAVklEQVR4Xn3PgQkAMQhDUXfqTu7kTtkpd5RA8AInfArtQ2iRXFWT2QedAfttj2FsPIOE1eCOlEuoWWjgzYaB/IkeGOrxXhqB+uA9Bfcm0lAZuh+YIeAD+cAqSz4kCMUAAAAASUVORK5CYII=)}a.Link--broken{color:red}p{margin:0 0 1em}hr{clear:both;margin:1em 0;border:0;border-top:1px solid #ddd}.Button{display:inline-block;text-align:center;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;border-radius:4px;margin-bottom:0}.Button--small{font-size:12px;line-height:1.5;border-radius:3px}.Button--default{color:#333;background-color:#fff;border-color:#ccc}.Button--default.Button--active{color:#333;background-color:#e6e6e6;border-color:#adadad}.Brand{display:block;background-color:var(--brand-background);padding:.75em .6em;font-size:var(--type-size-4);text-shadow:none;font-family:var(--font-family-heading);font-weight:700;color:var(--brand-color)}.Navbar{height:50px;box-shadow:0 1px 5px rgba(0,0,0,.25);background-color:var(--homepage-navbar-background);margin-bottom:0}.Navbar .Brand{float:left;line-height:20px;height:50px}.CodeToggler{padding:0 20px}.CodeToggler__text{font-size:12px;line-height:1.5;padding:6px 10px 6px 0;display:inline-block;vertical-align:middle}.no-js .CodeToggler,.CodeToggler--hidden{display:none}.Nav{margin:0;padding:0}.Nav__arrow{display:inline-block;position:relative;width:16px;margin-left:-16px}.Nav__arrow:before{position:absolute;display:block;content:"";margin:-0.25em 0 0 -0.4em;left:50%;top:50%;width:.5em;height:.5em;border-right:.15em solid var(--sidebar-link-arrow-color);border-top:.15em solid var(--sidebar-link-arrow-color);transform:rotate(45deg);transition-duration:.3s}.Nav__item{display:block}.Nav__item a{display:block;margin:0;padding:6px 15px 6px 20px;font-family:var(--font-family-heading);font-weight:400;color:var(--sidebar-link-color);text-shadow:none}.Nav__item a:hover{color:var(--sidebar-link-color);text-shadow:none;background-color:var(--sidebar-link-hover-background)}.Nav .Nav{margin-left:15px}html:not(.no-js) .Nav .Nav{height:0;transition:height 400ms ease-in-out;overflow:hidden}.Nav .Nav .Nav__item a{margin:0;margin-left:-15px;padding:3px 30px;font-family:var(--font-family-text);color:var(--sidebar-link-secondary-color);opacity:.7}.Nav .Nav .Nav__item a:hover{opacity:1}.Nav .Nav .Nav__item--active a{color:var(--sidebar-link-color)}.Nav__item--open>a,.Nav__item--active>a{background-color:var(--sidebar-link-active-background)}.Nav__item--open>a>.Nav__arrow:before{margin-left:-0.25em;transform:rotate(135deg)}.Page__header{margin:0 0 10px;padding:0;border-bottom:1px solid #eee}.Page__header:before,.Page__header:after{content:" ";display:table}.Page__header:after{clear:both}.Page__header h1{margin:0;padding:0;line-height:57px}.Page__header--separator{height:.6em}.Page__header a{text-decoration:none}.Page__header .ModifiedDate{float:left;font-size:10px;color:gray}.Page__header .EditOn{float:right;font-size:10px;color:gray}.Links{padding:0 20px}.Links a{font-family:var(--font-family-heading);font-weight:400;color:var(--sidebar-link-color);line-height:2em}.Twitter{padding:0 20px;font:normal normal normal 11px/18px "Helvetica Neue",Arial,sans-serif}.Twitter__button{text-decoration:none;display:inline-block;vertical-align:top;position:relative;height:20px;box-sizing:border-box;padding:1px 8px 1px 6px;background-color:#1b95e0;color:#fff;border-radius:3px;font-weight:500;cursor:pointer}.Twitter__button .Twitter__button__label{display:inline-block;vertical-align:top;margin-left:3px;white-space:nowrap}.Twitter__button svg{position:relative;top:2px;display:inline-block;width:14px;height:14px}.PoweredBy{padding:0 20px 1rem 20px;font-size:var(--type-size-6)}.Search{position:relative}.Search__field{display:block;width:100%;height:34px;padding:6px 30px 6px 20px;color:var(--search-field-color);border-width:0 0 1px;border-bottom:1px solid var(--search-field-border-color);background:var(--search-field-background);transition:border-color ease-in-out .15s}.Search__field:focus{border-color:var(--search-field-hover-border-color);outline:0}.Search__icon{position:absolute;right:9px;top:9px;width:16px;height:16px;cursor:pointer}.Navbar .Search{float:right;margin:8px 20px}.Navbar .Search__field{box-shadow:inset 0 1px 1px rgba(0,0,0,.075);border-width:0;border-radius:4px;padding-left:10px}.TableOfContentsContainer{float:right;min-width:300px;max-width:25%;padding-left:1em}.TableOfContentsContainer__title{margin-bottom:0 !important}.TableOfContentsContainer__content{border:1px solid #efefef;border-width:4px 2px 2px 6px}.TableOfContentsContainer__content>.TableOfContents>li+li{border-top:1px solid #ddd}ul.TableOfContents{font-size:1rem;padding-left:0;margin:0;list-style-type:none}ul.TableOfContents p{margin-bottom:0}ul.TableOfContents a{text-decoration:none;display:block;padding:.2em 0 .2em .75em}ul.TableOfContents .TableOfContents{padding-left:.75em}.Pager{padding-left:0;margin:1em 0;list-style:none;text-align:center;clear:both}.Pager:before,.Pager:after{content:" ";display:table}.Pager:after{clear:both}.Pager li{display:inline}.Pager li>a{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.Pager li>a:hover,.Pager li>a:focus{text-decoration:none;background-color:#eee}.Pager--next>a{float:right}.Pager--prev>a{float:left}.Checkbox{position:relative;display:block;padding-left:30px;cursor:pointer}.Checkbox input{position:absolute;z-index:-1;opacity:0}.Checkbox__indicator{position:absolute;top:50%;left:0;width:20px;height:20px;margin-top:-10px;background:var(--checkbox-background)}.Checkbox__indicator:after{position:absolute;display:none;content:""}.Checkbox:hover input~.Checkbox__indicator,.Checkbox input:focus~.Checkbox__indicator{background:var(--checkbox-hover-background)}.Checkbox input:checked~.Checkbox__indicator{background:var(--checkbox-checked-background)}.Checkbox input:checked~.Checkbox__indicator:after{display:block}.Checkbox:hover input:not([disabled]):checked~.Checkbox__indicator,.Checkbox input:checked:focus~.Checkbox__indicator{background:var(--checkbox-checked-hover-background)}.Checkbox input:disabled~.Checkbox__indicator{pointer-events:none;opacity:.6;background:var(--checkbox-disabled-background)}.Checkbox .Checkbox__indicator:after{top:4px;left:8px;width:5px;height:10px;transform:rotate(45deg);border:solid var(--checkbox-tick-color);border-width:0 2px 2px 0}.Checkbox input:disabled~.Checkbox__indicator:after{border-color:var(--checkbox-disabled-tick-color)}.Container{margin-right:auto;margin-left:auto}.Container--inner{width:80%;margin:0 auto}@media(min-width: 1200px){.Container{width:1170px}}@media(min-width: 992px){.Container{width:970px}}@media(min-width: 769px){.Container{width:750px}}.Homepage{padding-top:60px !important;background-color:var(--homepage-hero-background);border-radius:0;border:none;color:var(--homepage-hero-color);overflow:hidden;padding-bottom:0;margin-bottom:0;box-shadow:none}.HomepageTitle h2{width:80%;font-size:30px;margin:20px auto;text-align:center}.HomepageImage img{display:block;max-width:80%;margin:0 auto;height:auto}.HomepageButtons{padding:20px 0;background-color:var(--hero-button-block-background);text-align:center}.HomepageButtons:before,.HomepageButtons:after{content:" ";display:table}.HomepageButtons:after{clear:both}.HomepageButtons .Button--hero{padding:20px 30px;border-radius:0;text-shadow:none;opacity:.8;margin:0 10px;text-transform:uppercase;border:5px solid var(--hero-button-border-color);font-family:var(--font-family-heading);font-weight:700;background-image:none;filter:none;box-shadow:none}@media(max-width: 768px){.HomepageButtons .Button--hero{display:block;margin-bottom:10px}}.HomepageButtons .Button--hero:hover{opacity:1}.HomepageButtons .Button--hero.Button--secondary{background-color:var(--hero-button-secondary-background);color:var(--hero-button-secondary-color)}.HomepageButtons .Button--hero.Button--primary{background-color:var(--hero-buttom-primary-background);color:var(--hero-button-primary-color)}.HomepageContent{background-color:#fff;padding:40px 0}.HomepageContent ul li,.HomepageContent ol li{list-style:none;margin-bottom:.5em;position:relative}.HomepageContent ul li:before,.HomepageContent ol li:before{position:absolute;top:50%;left:-1.5em;content:"";width:0;height:0;border:.5em solid transparent;border-left:.5em solid var(--homepage-bullet-color);float:left;display:block;margin-top:-0.5em}.HomepageContent .HeroText{font-family:var(--font-family-heading);font-weight:300;font-size:16px;margin-bottom:20px;line-height:1.4}@media(min-width: 769px){.HomepageContent{padding:40px 20px}.HomepageContent .HeroText{font-size:21px}.HomepageContent .Row{margin:0 -15px}.HomepageContent .Row__third,.HomepageContent .Row__half,.HomepageContent .Row__quarter{float:left;position:relative;min-height:1px;padding-left:15px;padding-right:15px}.HomepageContent .Row__third{width:33.333333%}.HomepageContent .Row__half{width:50%}.HomepageContent .Row__quarter{width:25%}}.HomepageFooter{background-color:var(--homepage-footer-background);color:var(--homepage-footer-color);border:none;box-shadow:none}.HomepageFooter:before,.HomepageFooter:after{content:" ";display:table}.HomepageFooter:after{clear:both}@media(max-width: 768px){.HomepageFooter{padding:0 20px;text-align:center}.HomepageFooter .HomepageFooter__links{padding-left:0;list-style-type:none}}@media(min-width: 769px){.HomepageFooter .HomepageFooter__links{float:left}.HomepageFooter .HomepageFooter__twitter{float:right}}.HomepageFooter__links{margin:40px 0}.HomepageFooter__links li a{line-height:32px;font-size:16px;font-family:var(--font-family-heading);font-weight:700}.HomepageFooter__links li a:hover{text-decoration:underline}.HomepageFooter__twitter{margin:40px 0}.HomepageFooter .Twitter__button{margin-bottom:20px}@media print{*{text-shadow:none !important;color:#000 !important;background:transparent !important;box-shadow:none !important}h1,h2,h3,h4,h5,h6{break-after:avoid;break-before:auto}pre,blockquote{border:1px solid #999;font-style:italic;break-inside:avoid}img{break-inside:avoid;border:0}a,a:visited{text-decoration:underline}abbr[title]:after{content:" (" attr(title) ")"}q{quotes:none}q:before{content:""}q:after{content:" (" attr(cite) ")"}.PageBreak{display:block;break-before:always}.NoPrint{display:none}aside{display:none}.Pager{display:none}.Columns__right{width:100% !important}.s-content a:after{content:" (" attr(href) ")";font-size:80%;word-wrap:break-word}.s-content a[href^="#"]:after{content:""}h1 a[href]:after{font-size:50%}}@font-face{font-family:"League Gothic";src:url("fonts/leaguegothic.woff2") format("woff2"),url("fonts/leaguegothic.woff") format("woff");font-style:normal;font-display:swap}@font-face{font-family:"Cabin";src:url("fonts/cabin-regular.woff2") format("woff2"),url("fonts/cabin-regular.woff") format("woff");font-weight:normal;font-style:normal;font-display:swap}@font-face{font-family:"Cabin";src:url("fonts/cabin-italic.woff2") format("woff2"),url("fonts/cabin-italic.woff") format("woff");font-style:italic;font-display:swap}@font-face{font-family:"Cabin";src:url("fonts/cabin-bold.woff2") format("woff2"),url("fonts/cabin-bold.woff") format("woff");font-weight:bold;font-style:normal;font-display:swap}:root{--font-family-text: "Cabin", "Trebuchet MS", -apple-system, ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", Arial, sans-serif;--font-family-monospace: Monaco, Menlo, Consolas, "Lucida Console", "Courier New", monospace;--font-family-heading: "League Gothic", -apple-system, ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", Arial, sans-serif;--type-size-1: 4rem;--type-size-2: 3.236rem;--type-size-3: 2.618rem;--type-size-4: 2rem;--type-size-5: 1.618rem;--type-size-6: 1.309rem;--red: #e63c2f;--blue: #15284b;--light-blue: #93b7bb;--beige: #e8d5d3;--green: #2c9a42;--dark-gray: color(var(--beige) blend(var(--blue) 50%));--gray: color(var(--beige) blend(var(--blue) 25%));--light-gray: color(var(--beige) blend(var(--blue) 12.5%));--lighter-gray: var(--beige);--lightest-gray: color(var(--beige) blend(#fff 75%));--dark: var(--blue);--light: var(--light-blue);--sidebar-background: var(--beige);--sidebar-link-active-background: var(--light-blue);--sidebar-collapsible--hamburger-color: var(--beige);--text: var(--blue);--link-color: var(--red);--brand-color: var(--blue);--brand-background: var(--red);--code-tag-background-color: var(--lightest-gray);--code-tag-border-radius: 0;--code-tag-box-shadow: none;--homepage-navbar-background: var(--red);--hero-button-block-background: var(--beige);--homepage-hero-background: #fff;--content-floating-blocks-background: var(--blue)}html,body{font-size:16px}body{line-height:1.618}a.Link--external::after{content:""}.s-content code{display:inline-block;padding-top:0;padding-bottom:0;padding:.5ch;border:0}.s-content code::before,.s-content code::after{content:""}pre .s-content code{display:inline}.s-content table{border-collapse:separate;border-spacing:2px;border:2px solid var(--gray)}.s-content table thead,.s-content table tbody{background-color:#fff}.s-content table tr{border-top:0}.s-content table tr:nth-child(2n){background-color:transparent}.s-content table tr:nth-child(2n) td{background-color:var(--lightest-gray)}.s-content table th,.s-content table td{border:0}.s-content table,.Nav__item .Nav__item{font-size:1rem}.Brand,h1,h2,h3,h4,h5,h6{font-weight:normal}.Button{border-radius:0}.HomepageButtons .Button--hero{font-weight:normal;font-size:var(--type-size-6)}.Page__header{border-bottom:2px solid var(--lighter-gray)}.Pager li>a{border:2px solid var(--lighter-gray);border-radius:0}.Pager li>a:hover,.Pager li>a:focus{background-color:var(--lighter-gray)}.Pager--prev a::before{content:"← "}.Pager--next a::after{content:" →"}.Navbar{height:auto;box-shadow:none}.Navbar .Brand{float:none;line-height:inherit;height:auto}.Homepage{padding-top:10px !important}.Nav__item{font-size:var(--type-size-6)}.Nav__arrow:before,.Nav .Nav .Nav__item a .Nav__arrow:before{font-family:var(--font-family-heading);width:1ch;height:1ch}.TableOfContentsContainer__title{border-bottom:4px solid var(--lighter-gray)}ul.TableOfContents{border-left:6px solid var(--lighter-gray)}.Columns__right--full .TableOfContentsContainer .TableOfContentsContainer__content>.TableOfContents{border-right:2px solid var(--lighter-gray)}.Columns__right--full .TableOfContentsContainer a{border-bottom:1px solid var(--lighter-gray)}.clients thead tr:first-child th{text-align:left}.clients thead tr:first-child th:first-child{width:15%}.clients thead tr:first-child th:nth-child(3){width:50%;text-align:center}.clients thead tr+tr th{width:12%;text-align:center}.clients tbody td:nth-child(3),.clients tbody td:nth-child(4),.clients tbody td:nth-child(5),.clients tbody td:nth-child(6){text-align:center}.clients tbody td.Y{color:var(--green)}.clients tbody td.N{color:var(--red)}.hljs,.s-content pre{background:var(--blue);color:var(--beige)}.hljs{display:block;overflow-x:auto;padding:.5em}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold}.hljs-comment,.hljs-quote{color:#978e9c}.hljs-keyword,.hljs-selector-tag,.hljs-addition{color:#acb39a}.hljs-number,.hljs-string,.hljs-meta .hljs-meta-string,.hljs-literal,.hljs-doctag,.hljs-regexp{color:var(--light-blue)}.hljs-title,.hljs-section,.hljs-name,.hljs-selector-id,.hljs-selector-class{color:#82b7e5}.hljs-attribute,.hljs-attr,.hljs-variable,.hljs-template-variable,.hljs-class .hljs-title,.hljs-type{color:#c5b031}.hljs-symbol,.hljs-bullet,.hljs-subst,.hljs-meta,.hljs-meta .hljs-keyword,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-link{color:#ea8031}.hljs-built_in,.hljs-deletion{color:var(--red)}.hljs-formula{background:#686986}@media(--viewport-large){.Columns__left{border:0}} diff --git a/docs/theme/arsse/daux.min.js b/docs/theme/arsse/daux.min.js index fd875881..d9e2c54c 100644 --- a/docs/theme/arsse/daux.min.js +++ b/docs/theme/arsse/daux.min.js @@ -1,2 +1,2 @@ -var e=document.querySelectorAll(".s-content pre"),t=document.querySelector(".CodeToggler"),n="daux_code_blocks_hidden";function a(t){for(var a=0;a code:not(.hljs)");if(l.length){var i=document.getElementsByTagName("head")[0],c=document.createElement("script");c.type="text/javascript",c.async=!0,c.src="".concat(window.base_url,"daux_libraries/highlight.pack.js"),c.onload=function(e){[].forEach.call(l,window.hljs.highlightBlock)},i.appendChild(c)}function s(e){var t=void 0!==e.preventDefault;t&&e.preventDefault();var n=function(e){for(var t=e;(t=t.parentNode)&&9!==t.nodeType;)if(1===t.nodeType&&t.classList.contains("Nav__item"))return t;throw new Error("Could not find a NavItem...")}(e.target),a=n.querySelector("ul.Nav");t&&n.classList.contains("Nav__item--open")?(a.style.height="".concat(a.scrollHeight,"px"),a.style.transitionDuration="150ms",a.style.height="0px",n.classList.remove("Nav__item--open")):t?(a.style.transitionDuration="150ms",a.addEventListener("transitionend",(function e(t){"0px"!==t.target.style.height&&(t.target.style.height="auto"),t.target.removeEventListener("transitionend",e)})),a.style.height="".concat(a.scrollHeight,"px"),n.classList.add("Nav__item--open")):a.style.height="auto"}for(var d,u=document.querySelectorAll(".Nav__item.has-children i.Nav__arrow"),h=u.length-1;h>=0;h--)(d=u[h]).addEventListener("click",s),d.parentNode.parentNode.classList.contains("Nav__item--open")&&s({target:d});var g=document.querySelectorAll(".Nav__item__link--nopage"),v=!0,p=!1,_=void 0;try{for(var y,m=g[Symbol.iterator]();!(v=(y=m.next()).done);v=!0){y.value.addEventListener("click",s)}}catch(e){p=!0,_=e}finally{try{v||null==m.return||m.return()}finally{if(p)throw _}} +!function(){"use strict";function e(e){"loading"===document.readyState?document.addEventListener("DOMContentLoaded",e):e()}function t(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[o++]}},e:function(e){throw e},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,l=!0,c=!1;return{s:function(){r=e[Symbol.iterator]()},n:function(){var e=r.next();return l=e.done,e},e:function(e){c=!0,i=e},f:function(){try{l||null==r.return||r.return()}finally{if(c)throw i}}}}function o(e){var t=void 0!==e.preventDefault;t&&e.preventDefault();var n=function(e){for(var t=e;(t=t.parentNode)&&9!==t.nodeType;)if(1===t.nodeType&&t.classList.contains("Nav__item"))return t;throw new Error("Could not find a NavItem...")}(e.target),r=n.querySelector("ul.Nav");t&&n.classList.contains("Nav__item--open")?(r.style.height="".concat(r.scrollHeight,"px"),r.style.transitionDuration="150ms",r.style.height="0px",n.classList.remove("Nav__item--open")):t?(r.style.transitionDuration="150ms",r.addEventListener("transitionend",(function e(t){"0px"!==t.target.style.height&&(t.target.style.height="auto"),t.target.removeEventListener("transitionend",e)})),r.style.height="".concat(r.scrollHeight,"px"),n.classList.add("Nav__item--open")):r.style.height="auto"}e((function(){var e=document.querySelectorAll(".s-content pre"),n=document.querySelector(".CodeToggler");n&&(e.length?function(e,n){var r=e.querySelector(".CodeToggler__button--main");r.addEventListener("change",(function(e){t(n,!e.target.checked)}),!1);var o=!1;try{"false"===(o=localStorage.getItem("daux_code_blocks_hidden"))?o=!1:"true"===o&&(o=!0),o&&(t(n,!!o),r.checked=!o)}catch(e){}}(n,e):n.classList.add("CodeToggler--hidden"))})),e((function(){var e=document.querySelector(".Collapsible__trigger");if(e){var t=document.querySelector(".Collapsible__content");e.addEventListener("click",(function(n){t.classList.contains("Collapsible__content--open")?(t.style.height=0,t.classList.remove("Collapsible__content--open"),e.setAttribute("aria-expanded","false")):(e.setAttribute("aria-expanded","true"),t.style.transitionDuration="150ms",t.style.height="".concat(t.scrollHeight,"px"),t.classList.add("Collapsible__content--open"))}))}})),e((function(){var e=document.querySelectorAll("pre > code:not(.hljs)");if(e.length){var t=document.getElementsByTagName("head")[0],n=document.createElement("script");n.type="text/javascript",n.async=!0,n.src="".concat(window.base_url,"daux_libraries/highlight.pack.js"),n.onload=function(t){[].forEach.call(e,window.hljs.highlightBlock)},t.appendChild(n)}})),e((function(){for(var e,t=document.querySelectorAll(".Nav__item.has-children i.Nav__arrow"),n=t.length-1;n>=0;n--)(e=t[n]).addEventListener("click",o),e.parentNode.parentNode.classList.contains("Nav__item--open")&&o({target:e});var a,i=r(document.querySelectorAll(".Nav__item__link--nopage"));try{for(i.s();!(a=i.n()).done;){a.value.addEventListener("click",o)}}catch(e){i.e(e)}finally{i.f()}}))}(); //# sourceMappingURL=daux.min.js.map diff --git a/docs/theme/src/arsse.scss b/docs/theme/src/arsse.scss index 6f5d9d83..1df6dde0 100644 --- a/docs/theme/src/arsse.scss +++ b/docs/theme/src/arsse.scss @@ -6,8 +6,10 @@ @import "../../../vendor-bin/daux/vendor/daux/daux.io/src/css/theme_daux/_typography.scss"; @import "../../../vendor-bin/daux/vendor/daux/daux.io/src/css/theme_daux/_components.scss"; @import "../../../vendor-bin/daux/vendor/daux/daux.io/src/css/theme_daux/_homepage.scss"; -@import "../../../vendor-bin/daux/vendor/daux/daux.io/src/css/theme_daux/_print.scss" print; +@media print { + @import "../../../vendor-bin/daux/vendor/daux/daux.io/src/css/theme_daux/_print.scss"; +} /* The Arsse overrides */ @@ -98,6 +100,10 @@ --content-floating-blocks-background: var(--blue); } +html, body { + font-size: 16px; +} + body { line-height: 1.618; } diff --git a/package.json b/package.json index e7a3b351..e0267e35 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,5 @@ { "devDependencies": { - "autoprefixer": "^9.6.1", - "postcss": "^7.0.0", - "postcss-cli": "^7.1.1", - "postcss-color-function": "^4.1.0", - "postcss-csso": "^4.0.0", - "postcss-custom-media": "^7.0.8", - "postcss-custom-properties": "^9.0.2", - "postcss-discard-comments": "^4.0.2", - "postcss-import": "^12.0.1", - "postcss-media-minmax": "^4.0.0", - "postcss-nested": "^4.1.2", - "postcss-sassy-mixins": "^2.1.0", - "postcss-scss": "^2.0.0" + "sass": "^1.32.8" } } diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index 4eecda3c..00000000 --- a/postcss.config.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = ctx => ({ - //map: ctx.options.map, - parser: 'postcss-scss', - //syntax: 'postcss-scss', - plugins: { - 'postcss-import': { root: ctx.file.dirname }, - 'postcss-discard-comments': {}, - 'postcss-sassy-mixins': {}, - 'postcss-custom-media': {preserve: false}, - 'postcss-media-minmax': {}, - 'postcss-custom-properties': {preserve: false}, - 'postcss-color-function': {}, - 'postcss-nested': {}, - 'autoprefixer': {}, - 'postcss-csso': {}, - } -}) diff --git a/yarn.lock b/yarn.lock index 24fe8bd7..850a7cd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,56 +2,6 @@ # yarn lockfile v1 -"@nodelib/fs.scandir@2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" - integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== - dependencies: - "@nodelib/fs.stat" "2.0.4" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" - integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" - integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== - dependencies: - "@nodelib/fs.scandir" "2.1.4" - fastq "^1.6.0" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - anymatch@~3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" @@ -60,135 +10,19 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -autoprefixer@^9.6.1: - version "9.8.6" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" - integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== - dependencies: - browserslist "^4.12.0" - caniuse-lite "^1.0.30001109" - colorette "^1.2.1" - normalize-range "^0.1.2" - num2fraction "^1.2.2" - postcss "^7.0.32" - postcss-value-parser "^4.1.0" - -balanced-match@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" - integrity sha1-tQS9BYabOSWd0MXvw12EMXbczEo= - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^3.0.1, braces@~3.0.2: +braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: fill-range "^7.0.1" -browserslist@^4.12.0: - version "4.16.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" - integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== - dependencies: - caniuse-lite "^1.0.30001181" - colorette "^1.2.1" - electron-to-chromium "^1.3.649" - escalade "^3.1.1" - node-releases "^1.1.70" - -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001181: - version "1.0.30001208" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz#a999014a35cebd4f98c405930a057a0d75352eb9" - integrity sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA== - -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.1, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chokidar@^3.3.0: +"chokidar@>=2.0.0 <4.0.0": version "3.5.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== @@ -203,185 +37,6 @@ chokidar@^3.3.0: optionalDependencies: fsevents "~2.3.1" -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= - -color-convert@^1.3.0, color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" - integrity sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE= - dependencies: - color-name "^1.0.0" - -color@^0.11.0: - version "0.11.4" - resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" - integrity sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q= - dependencies: - clone "^1.0.2" - color-convert "^1.3.0" - color-string "^0.3.0" - -colorette@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -cosmiconfig@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - -css-color-function@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.3.tgz#8ed24c2c0205073339fafa004bc8c141fccb282e" - integrity sha1-jtJMLAIFBzM5+voAS8jBQfzLKC4= - dependencies: - balanced-match "0.1.0" - color "^0.11.0" - debug "^3.1.0" - rgb "~0.1.0" - -css-tree@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -csso@^4.0.2: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== - dependencies: - css-tree "^1.1.2" - -debug@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -dependency-graph@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.9.0.tgz#11aed7e203bc8b00f48356d92db27b265c445318" - integrity sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -electron-to-chromium@^1.3.649: - version "1.3.710" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.710.tgz#b33d316e5d6de92b916e766d8a478d19796ffe11" - integrity sha512-b3r0E2o4yc7mNmBeJviejF1rEx49PUBi+2NPa7jHEX3arkAXnVgLhR0YmV8oi6/Qf3HH2a8xzQmCjHNH0IpXWQ== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -fast-glob@^3.1.1: - version "3.2.5" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" - integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" - merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" - -fastq@^1.6.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" - integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== - dependencies: - reusify "^1.0.4" - fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -389,163 +44,18 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -fs-extra@^9.0.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fsevents@~2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-stdin@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" - integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== - -glob-parent@^5.1.0, glob-parent@~5.1.0: +glob-parent@~5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" - integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globby@^11.0.0: - version "11.0.3" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" - integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - -graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -ignore@^5.1.4: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== - -import-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" - integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= - dependencies: - import-from "^2.1.0" - -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" - -import-from@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" - integrity sha1-M1238qev/VOqpHHUuAId7ja387E= - dependencies: - resolve-from "^3.0.0" - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ip-regex@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" - integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -553,28 +63,11 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-core-module@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" - integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== - dependencies: - has "^1.0.3" - -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" @@ -587,376 +80,16 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-url-superb@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-url-superb/-/is-url-superb-3.0.0.tgz#b9a1da878a1ac73659047d1e6f4ef22c209d3e25" - integrity sha512-3faQP+wHCGDQT1qReM5zCPx2mxoal6DzbzquFlCYJLWyy4WPTved33ea2xFbX37z4NoriEwZGIYhFtx8RUB5wQ== - dependencies: - url-regex "^5.0.0" - -js-base64@^2.1.9: - version "2.6.4" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" - integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== - -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash@^4.17.11: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log-symbols@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" - integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== - dependencies: - chalk "^2.0.1" - -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== - -merge2@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== - dependencies: - braces "^3.0.1" - picomatch "^2.0.5" - -"minimatch@2 || 3": - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -node-releases@^1.1.70: - version "1.1.71" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" - integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== - normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= - -num2fraction@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" - integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: +picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== -pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -postcss-cli@^7.1.1: - version "7.1.2" - resolved "https://registry.yarnpkg.com/postcss-cli/-/postcss-cli-7.1.2.tgz#ba8d5d918b644bd18e80ad2c698064d4c0da51cd" - integrity sha512-3mlEmN1v2NVuosMWZM2tP8bgZn7rO5PYxRRrXtdSyL5KipcgBDjJ9ct8/LKxImMCJJi3x5nYhCGFJOkGyEqXBQ== - dependencies: - chalk "^4.0.0" - chokidar "^3.3.0" - dependency-graph "^0.9.0" - fs-extra "^9.0.0" - get-stdin "^8.0.0" - globby "^11.0.0" - postcss "^7.0.0" - postcss-load-config "^2.0.0" - postcss-reporter "^6.0.0" - pretty-hrtime "^1.0.3" - read-cache "^1.0.0" - yargs "^15.0.2" - -postcss-color-function@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.1.0.tgz#b6f9355e07b12fcc5c34dab957834769b03d8f57" - integrity sha512-2/fuv6mP5Lt03XbRpVfMdGC8lRP1sykme+H1bR4ARyOmSMB8LPSjcL6EAI1iX6dqUF+jNEvKIVVXhan1w/oFDQ== - dependencies: - css-color-function "~1.3.3" - postcss "^6.0.23" - postcss-message-helpers "^2.0.0" - postcss-value-parser "^3.3.1" - -postcss-csso@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-csso/-/postcss-csso-4.0.0.tgz#30fef9303ecbeb0424dab1228275416fc7186a50" - integrity sha512-Yh9Ug0w3+T/LZIh1vGJQY8+hE13yFRHpINoAmgOhvu9lBmG1jyHkAprGHEHlGjWODJzB4DCNBVBb6Cs0QEoglQ== - dependencies: - csso "^4.0.2" - -postcss-custom-media@^7.0.8: - version "7.0.8" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz#fffd13ffeffad73621be5f387076a28b00294e0c" - integrity sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg== - dependencies: - postcss "^7.0.14" - -postcss-custom-properties@^9.0.2: - version "9.2.0" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-9.2.0.tgz#80bae0d6e0c510245ace7ede95ac527712ea24e7" - integrity sha512-IFRV7LwapFkNa3MtvFpw+MEhgyUpaVZ62VlR5EM0AbmnGbNhU9qIE8u02vgUbl1gLkHK6sterEavamVPOwdE8g== - dependencies: - postcss "^7.0.17" - postcss-values-parser "^3.0.5" - -postcss-discard-comments@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" - integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== - dependencies: - postcss "^7.0.0" - -postcss-import@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153" - integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw== - dependencies: - postcss "^7.0.1" - postcss-value-parser "^3.2.3" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-load-config@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a" - integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw== - dependencies: - cosmiconfig "^5.0.0" - import-cwd "^2.0.0" - -postcss-media-minmax@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" - integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== - dependencies: - postcss "^7.0.2" - -postcss-message-helpers@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" - integrity sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4= - -postcss-nested@^4.1.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-4.2.3.tgz#c6f255b0a720549776d220d00c4b70cd244136f6" - integrity sha512-rOv0W1HquRCamWy2kFl3QazJMMe1ku6rCFoAAH+9AcxdbpDeBr6k968MLWuLjvjMcGEip01ak09hKOEgpK9hvw== - dependencies: - postcss "^7.0.32" - postcss-selector-parser "^6.0.2" - -postcss-reporter@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-6.0.1.tgz#7c055120060a97c8837b4e48215661aafb74245f" - integrity sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw== - dependencies: - chalk "^2.4.1" - lodash "^4.17.11" - log-symbols "^2.2.0" - postcss "^7.0.7" - -postcss-sassy-mixins@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-sassy-mixins/-/postcss-sassy-mixins-2.1.0.tgz#368f200946bfdef6a8b12d68c0f6379b9a222f26" - integrity sha1-No8gCUa/3vaosS1owPY3m5oiLyY= - dependencies: - glob "^6.0.4" - postcss "^5.0.14" - postcss-simple-vars "^1.2.0" - -postcss-scss@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.1.1.tgz#ec3a75fa29a55e016b90bf3269026c53c1d2b383" - integrity sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA== - dependencies: - postcss "^7.0.6" - -postcss-selector-parser@^6.0.2: - version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" - integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== - dependencies: - cssesc "^3.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - util-deprecate "^1.0.2" - -postcss-simple-vars@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-1.2.0.tgz#2e6689921144b74114e765353275a3c32143f150" - integrity sha1-LmaJkhFEt0EU52U1MnWjwyFD8VA= - dependencies: - postcss "^5.0.13" - -postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" - integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== - -postcss-value-parser@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== - -postcss-values-parser@^3.0.5: - version "3.2.1" - resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-3.2.1.tgz#55114607de6631338ba8728d3e9c15785adcc027" - integrity sha512-SQ7/88VE9LhJh9gc27/hqnSU/aZaREVJcRVccXBmajgP2RkjdJzNyH/a9GCVMI5nsRhT0jC5HpUMwfkz81DVVg== - dependencies: - color-name "^1.1.4" - is-url-superb "^3.0.0" - postcss "^7.0.5" - url-regex "^5.0.0" - -postcss@^5.0.13, postcss@^5.0.14: - version "5.2.18" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" - integrity sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg== - dependencies: - chalk "^1.1.3" - js-base64 "^2.1.9" - source-map "^0.5.6" - supports-color "^3.2.3" - -postcss@^6.0.23: - version "6.0.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" - integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== - dependencies: - chalk "^2.4.1" - source-map "^0.6.1" - supports-color "^5.4.0" - -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6, postcss@^7.0.7: - version "7.0.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -pretty-hrtime@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" - integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -read-cache@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" - integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q= - dependencies: - pify "^2.3.0" - readdirp@~3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" @@ -964,131 +97,12 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -resolve@^1.1.7: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== +sass@^1.32.8: + version "1.32.8" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.8.tgz#f16a9abd8dc530add8834e506878a2808c037bdc" + integrity sha512-Sl6mIeGpzjIUZqvKnKETfMf0iDAswD9TNlv13A7aAF3XZlRPMq4VvJWBC2N2DXbp94MQVdNSFG6LfF/iOXrPHQ== dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rgb@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" - integrity sha1-vieykej+/+rBvZlylyG/pA/AN7U= - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" - integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= - dependencies: - has-flag "^1.0.0" - -supports-color@^5.3.0, supports-color@^5.4.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -tlds@^1.203.0: - version "1.219.0" - resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.219.0.tgz#7e636062386a1f3c9184356de93d40842ffe91d9" - integrity sha512-o4g9c8kXCmTDwUnK/9HpTT9o/GNH85KCvs+S5SgUw5yILdECvMmTGzK7ngoWMp97P5tfYr8fZeF16YhgV/l90A== + chokidar ">=2.0.0 <4.0.0" to-regex-range@^5.0.1: version "5.0.1" @@ -1096,75 +110,3 @@ to-regex-range@^5.0.1: integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" - -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - -url-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/url-regex/-/url-regex-5.0.0.tgz#8f5456ab83d898d18b2f91753a702649b873273a" - integrity sha512-O08GjTiAFNsSlrUWfqF1jH0H1W3m35ZyadHrGv5krdnmPPoxP27oDTqux/579PtaroiSGm5yma6KT1mHFH6Y/g== - dependencies: - ip-regex "^4.1.0" - tlds "^1.203.0" - -util-deprecate@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -y18n@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== - -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@^15.0.2: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" From 1331b14a04d1a3b64d02df5126e03e495cd9463c Mon Sep 17 00:00:00 2001 From: Dustin Wilson Date: Fri, 9 Apr 2021 18:18:42 -0500 Subject: [PATCH 218/366] Reverting for now --- README.md | 2 +- RoboFile.php | 7 +- docs/theme/arsse/arsse.css | 3 +- docs/theme/arsse/daux.min.js | 2 +- docs/theme/src/arsse.scss | 8 +- package.json | 14 +- postcss.config.js | 17 + yarn.lock | 1076 +++++++++++++++++++++++++++++++++- 8 files changed, 1106 insertions(+), 23 deletions(-) create mode 100644 postcss.config.js diff --git a/README.md b/README.md index 8a7e76d1..74831aa4 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ The `/vendor-bin/` directory houses the files needed for the tools used in The A | `/robo` | Simple wrapper for executing Robo on POSIX systems | | `/robo.bat` | Simple wrapper for executing Robo on Windows | -In addition the files `/package.json`, `/yarn.lock`, and the `/node_modules/` directory are used by [Yarn](https://yarnpkg.com/) and [Sass](https://sass-lang.com/) when modifying the stylesheet for The Arsse's manual. +In addition the files `/package.json`, `/yarn.lock`, and `/postcss.config.js` as well as the `/node_modules/` directory are used by [Yarn](https://yarnpkg.com/) and [PostCSS](https://postcss.org/) when modifying the stylesheet for The Arsse's manual. # Common tasks diff --git a/RoboFile.php b/RoboFile.php index f9ba27e4..ec6457cf 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -190,7 +190,8 @@ class RoboFile extends \Robo\Tasks { $dir."robo", $dir."robo.bat", $dir."package.json", - $dir."yarn.lock" + $dir."yarn.lock", + $dir."postcss.config.js", ]); // generate a sample configuration file $t->taskExec(escapeshellarg(\PHP_BINARY)." arsse.php conf save-defaults config.defaults.php")->dir($dir); @@ -229,7 +230,7 @@ class RoboFile extends \Robo\Tasks { * Daux's theme changes */ public function manualTheme(array $args): Result { - $sass = escapeshellarg(norm(BASE."node_modules/.bin/sass")); + $postcss = escapeshellarg(norm(BASE."node_modules/.bin/postcss")); $themesrc = norm(BASE."docs/theme/src/").\DIRECTORY_SEPARATOR; $themeout = norm(BASE."docs/theme/arsse/").\DIRECTORY_SEPARATOR; $dauxjs = norm(BASE."vendor-bin/daux/vendor/daux/daux.io/themes/daux/js/").\DIRECTORY_SEPARATOR; @@ -238,7 +239,7 @@ class RoboFile extends \Robo\Tasks { // install dependencies via Yarn $t->taskExec("yarn install"); // compile the stylesheet - $t->taskExec($sass)->arg('--no-source-map')->option('--style', 'compressed')->arg("{$themesrc}arsse.scss")->arg("{$themeout}arsse.css"); + $t->taskExec($postcss)->arg($themesrc."arsse.scss")->option("-o", $themeout."arsse.css"); // copy JavaScript files from the Daux theme foreach (glob($dauxjs."daux*.js") as $file) { $t->taskFilesystemStack()->copy($file, $themeout.basename($file), true); diff --git a/docs/theme/arsse/arsse.css b/docs/theme/arsse/arsse.css index b8def289..9971fbaa 100644 --- a/docs/theme/arsse/arsse.css +++ b/docs/theme/arsse/arsse.css @@ -1 +1,2 @@ -/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress{vertical-align:baseline}template,[hidden]{display:none}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:bold}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}:root{--gray: #7a8288;--dark-gray: color(var(--gray) blend(#000 40%));--light-gray: color(var(--gray) blend(#fff 50%));--lighter-gray: color(var(--gray) blend(#fff 70%));--lightest-gray: color(var(--gray) blend(#fff 90%));--type-size-1: 2.441rem;--type-size-2: 1.953rem;--type-size-3: 1.563rem;--type-size-4: 1.25rem;--type-size-5: 1rem;--type-size-6: 0.75rem;--dark: #3f4657;--light: #82becd;--text: #222;--link-color: var(--light);--brand-color: var(--light);--brand-background: var(--dark);--sidebar-border: #e7e7e9;--sidebar-background: #f7f7f7;--sidebar-link-color: var(--dark);--sidebar-link-active-background: #c5c5cb;--sidebar-link-hover-background: var(--sidebar-link-active-background);--sidebar-link-arrow-color: var(--dark);--sidebar-link-secondary-color: var(--text);--checkbox-background: #e6e6e6;--checkbox-hover-background: #ccc;--checkbox-checked-background: var(--dark);--checkbox-checked-hover-background: var(--light);--checkbox-tick-color: #fff;--checkbox-disabled-background: #e6e6e6;--checkbox-disabled-tick-color: #7b7b7b;--search-field-color: #555;--search-field-border-color: #ccc;--search-field-background: #fff;--search-field-hover-border-color: var(--light);--sidebar-collapsible--hamburger-color: var(--light);--sidebar-collapsible--hamburger-hover-color: var(--dark);--sidebar-collapsible--hamburger-hover-background: var(--light);--homepage-navbar-background: var(--dark);--homepage-hero-background: var(--light);--homepage-hero-color: var(--dark);--homepage-bullet-color: var(--light);--homepage-footer-color: var(--light);--homepage-footer-background: var(--dark);--hero-button-block-background: var(--sidebar-link-active-background);--hero-button-border-color: var(--dark);--hero-button-primary-color: var(--sidebar-background);--hero-buttom-primary-background: var(--dark);--hero-button-secondary-color: var(--dark);--hero-button-secondary-background: var(--sidebar-link-active-background);--content-floating-blocks-background: var(--light);--code-tag-color: var(--dark);--code-tag-background-color: #fafafa;--code-tag-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.125);--code-tag-border-radius: 4px;--blockquote-border-color: var(--dark)}@custom-media --viewport-small (width < 850px);@custom-media --viewport-large (width >= 850px);*,*:after,*:before{box-sizing:border-box}body{margin:0;padding:0}html{font-size:14px}@media(--viewport-large){html{font-size:16px}}html,body{height:100%;background-color:#fff;color:var(--text)}.Columns__left{background-color:var(--sidebar-background)}.Columns__right__content{padding:10px;background-color:#fff}@media(max-width: 768px){html:not(.no-js) .Collapsible__content{height:0;overflow:hidden;transition:height 400ms ease-in-out}}.Collapsible__trigger{margin:12px;padding:7px 10px;background-color:transparent;border:none;float:right;background-image:none;filter:none;box-shadow:none}.Collapsible__trigger__bar{display:block;width:18px;height:2px;margin-top:2px;margin-bottom:3px;background-color:var(--sidebar-collapsible--hamburger-color)}.Collapsible__trigger:hover{background-color:var(--sidebar-collapsible--hamburger-hover-background);box-shadow:none}.Collapsible__trigger:hover .Collapsible__trigger__bar{background-color:var(--sidebar-collapsible--hamburger-hover-color)}@media screen and (min-width: 769px){body{background-color:var(--content-floating-blocks-background)}.Navbar{position:fixed;z-index:1030;width:100%}.Collapsible__trigger{display:none !important}.Collapsible__content{display:block !important}.Columns{height:100%}.Columns:after,.Columns:before{content:" ";display:table}.Columns:after{clear:both}.Columns__left,.Columns__right{position:relative;min-height:1px;float:left;overflow:auto;height:100%}.Columns__left{width:25%;border-right:1px solid var(--sidebar-border);overflow-x:hidden}.Columns__right{width:75%}.Columns__right__content{padding:0 20px 20px;min-height:100%}}.Page{max-width:860px}.u-visuallyHidden{position:absolute !important;height:1px;width:1px;overflow:hidden;clip:rect(1px 1px 1px 1px);clip:rect(1px, 1px, 1px, 1px);white-space:nowrap}body{line-height:1.5;font-family:var(--font-family-text);font-feature-settings:"kern" 1;font-kerning:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}h1,h2,h3,h4,h5,h6{font-family:var(--font-family-heading);font-weight:300}.s-content h1,.s-content h2,.s-content h3,.s-content h4,.s-content h5,.s-content h6{cursor:text;line-height:1.4em;margin:2em 0 .5em}.s-content h1 tt,.s-content h1 code,.s-content h2 tt,.s-content h2 code,.s-content h3 tt,.s-content h3 code,.s-content h4 tt,.s-content h4 code,.s-content h5 tt,.s-content h5 code,.s-content h6 tt,.s-content h6 code{font-size:inherit}.s-content h1 i,.s-content h2 i,.s-content h3 i,.s-content h4 i,.s-content h5 i,.s-content h6 i{font-size:.7em}.s-content h1 p,.s-content h2 p,.s-content h3 p,.s-content h4 p,.s-content h5 p,.s-content h6 p{margin-top:0}.s-content h1{margin-top:0;font-size:var(--type-size-3)}.s-content h2{font-size:var(--type-size-4)}.s-content h3{font-size:var(--type-size-5)}.s-content h4{font-size:var(--type-size-6)}.s-content h5{font-size:var(--type-size-6)}.s-content h6{font-size:var(--type-size-6)}.s-content a{text-decoration:underline}.s-content small{font-size:var(--type-size-6)}.s-content p{margin-bottom:1.3em}.s-content ul,.s-content ol{padding-left:2em}.s-content ul p{margin:0}.s-content ul ul{margin:0}.s-content dl{padding:0}.s-content dl dt{font-weight:bold;font-style:italic;padding:0;margin:15px 0 5px}.s-content dl dt:first-child{padding:0}.s-content dl dd{margin:0 0 15px;padding:0 15px}.s-content blockquote{margin:.75em 2em;padding:.5em 1em;font-style:italic;border-left:.25em solid var(--blockquote-border-color)}.s-content blockquote cite{font-style:italic}.s-content blockquote cite:before{content:"—";padding-right:.5em}.s-content table{width:100%;padding:0;margin-bottom:1em;border-collapse:collapse;font-size:var(--type-size-6)}.s-content table+table{margin-top:1em}.s-content table tr{border-top:1px solid #eee;background-color:#fff;margin:0;padding:0}.s-content table tr:nth-child(2n){background-color:var(--lightest-gray)}.s-content table th{font-weight:bold;border:1px solid var(--light-gray);background:var(--lighter-gray);margin:0;padding:.5em}.s-content table td{border:1px solid var(--lighter-gray);margin:0;padding:.5em}.s-content ul>:first-child,.s-content ol>:first-child,.s-content blockquote>:first-child,.s-content dl dt>:first-child,.s-content dl dd>:first-child,.s-content table th>:first-child,.s-content table td>:first-child{margin-top:0}.s-content ul>:last-child,.s-content ol>:last-child,.s-content blockquote>:last-child,.s-content dl dt>:last-child,.s-content dl dd>:last-child,.s-content table th>:last-child,.s-content table td>:last-child{margin-bottom:0}.s-content img{max-width:100%;display:inline-block}.s-content code{font-family:var(--font-family-monospace);padding-top:.1rem;padding-bottom:.1rem;background:var(--code-tag-background-color);border:1px solid var(--light-gray);border-radius:var(--code-tag-border-radius);box-shadow:var(--code-tag-box-shadow)}.s-content code:before,.s-content code:after{letter-spacing:-0.2em;content:" "}.s-content pre{background:#f5f2f0;color:#333;line-height:1.5em;overflow:auto;border:none;border-radius:0;padding:.75em 20px;margin:0 -20px 20px -20px}.s-content pre code{margin:0;padding:0;white-space:pre;box-shadow:none}.s-content pre code:before,.s-content pre code:after{display:none}.s-content pre code,.s-content pre tt{background-color:transparent;border:none}.s-content u,.s-content ins{text-decoration:none;border-bottom:1px solid var(--text)}.s-content u a,.s-content ins a{color:inherit}.s-content del a{color:inherit}a{text-decoration:none;color:var(--link-color)}a.Link--external:after{content:" " url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAVklEQVR4Xn3PgQkAMQhDUXfqTu7kTtkpd5RA8AInfArtQ2iRXFWT2QedAfttj2FsPIOE1eCOlEuoWWjgzYaB/IkeGOrxXhqB+uA9Bfcm0lAZuh+YIeAD+cAqSz4kCMUAAAAASUVORK5CYII=)}a.Link--broken{color:red}p{margin:0 0 1em}hr{clear:both;margin:1em 0;border:0;border-top:1px solid #ddd}.Button{display:inline-block;text-align:center;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;border-radius:4px;margin-bottom:0}.Button--small{font-size:12px;line-height:1.5;border-radius:3px}.Button--default{color:#333;background-color:#fff;border-color:#ccc}.Button--default.Button--active{color:#333;background-color:#e6e6e6;border-color:#adadad}.Brand{display:block;background-color:var(--brand-background);padding:.75em .6em;font-size:var(--type-size-4);text-shadow:none;font-family:var(--font-family-heading);font-weight:700;color:var(--brand-color)}.Navbar{height:50px;box-shadow:0 1px 5px rgba(0,0,0,.25);background-color:var(--homepage-navbar-background);margin-bottom:0}.Navbar .Brand{float:left;line-height:20px;height:50px}.CodeToggler{padding:0 20px}.CodeToggler__text{font-size:12px;line-height:1.5;padding:6px 10px 6px 0;display:inline-block;vertical-align:middle}.no-js .CodeToggler,.CodeToggler--hidden{display:none}.Nav{margin:0;padding:0}.Nav__arrow{display:inline-block;position:relative;width:16px;margin-left:-16px}.Nav__arrow:before{position:absolute;display:block;content:"";margin:-0.25em 0 0 -0.4em;left:50%;top:50%;width:.5em;height:.5em;border-right:.15em solid var(--sidebar-link-arrow-color);border-top:.15em solid var(--sidebar-link-arrow-color);transform:rotate(45deg);transition-duration:.3s}.Nav__item{display:block}.Nav__item a{display:block;margin:0;padding:6px 15px 6px 20px;font-family:var(--font-family-heading);font-weight:400;color:var(--sidebar-link-color);text-shadow:none}.Nav__item a:hover{color:var(--sidebar-link-color);text-shadow:none;background-color:var(--sidebar-link-hover-background)}.Nav .Nav{margin-left:15px}html:not(.no-js) .Nav .Nav{height:0;transition:height 400ms ease-in-out;overflow:hidden}.Nav .Nav .Nav__item a{margin:0;margin-left:-15px;padding:3px 30px;font-family:var(--font-family-text);color:var(--sidebar-link-secondary-color);opacity:.7}.Nav .Nav .Nav__item a:hover{opacity:1}.Nav .Nav .Nav__item--active a{color:var(--sidebar-link-color)}.Nav__item--open>a,.Nav__item--active>a{background-color:var(--sidebar-link-active-background)}.Nav__item--open>a>.Nav__arrow:before{margin-left:-0.25em;transform:rotate(135deg)}.Page__header{margin:0 0 10px;padding:0;border-bottom:1px solid #eee}.Page__header:before,.Page__header:after{content:" ";display:table}.Page__header:after{clear:both}.Page__header h1{margin:0;padding:0;line-height:57px}.Page__header--separator{height:.6em}.Page__header a{text-decoration:none}.Page__header .ModifiedDate{float:left;font-size:10px;color:gray}.Page__header .EditOn{float:right;font-size:10px;color:gray}.Links{padding:0 20px}.Links a{font-family:var(--font-family-heading);font-weight:400;color:var(--sidebar-link-color);line-height:2em}.Twitter{padding:0 20px;font:normal normal normal 11px/18px "Helvetica Neue",Arial,sans-serif}.Twitter__button{text-decoration:none;display:inline-block;vertical-align:top;position:relative;height:20px;box-sizing:border-box;padding:1px 8px 1px 6px;background-color:#1b95e0;color:#fff;border-radius:3px;font-weight:500;cursor:pointer}.Twitter__button .Twitter__button__label{display:inline-block;vertical-align:top;margin-left:3px;white-space:nowrap}.Twitter__button svg{position:relative;top:2px;display:inline-block;width:14px;height:14px}.PoweredBy{padding:0 20px 1rem 20px;font-size:var(--type-size-6)}.Search{position:relative}.Search__field{display:block;width:100%;height:34px;padding:6px 30px 6px 20px;color:var(--search-field-color);border-width:0 0 1px;border-bottom:1px solid var(--search-field-border-color);background:var(--search-field-background);transition:border-color ease-in-out .15s}.Search__field:focus{border-color:var(--search-field-hover-border-color);outline:0}.Search__icon{position:absolute;right:9px;top:9px;width:16px;height:16px;cursor:pointer}.Navbar .Search{float:right;margin:8px 20px}.Navbar .Search__field{box-shadow:inset 0 1px 1px rgba(0,0,0,.075);border-width:0;border-radius:4px;padding-left:10px}.TableOfContentsContainer{float:right;min-width:300px;max-width:25%;padding-left:1em}.TableOfContentsContainer__title{margin-bottom:0 !important}.TableOfContentsContainer__content{border:1px solid #efefef;border-width:4px 2px 2px 6px}.TableOfContentsContainer__content>.TableOfContents>li+li{border-top:1px solid #ddd}ul.TableOfContents{font-size:1rem;padding-left:0;margin:0;list-style-type:none}ul.TableOfContents p{margin-bottom:0}ul.TableOfContents a{text-decoration:none;display:block;padding:.2em 0 .2em .75em}ul.TableOfContents .TableOfContents{padding-left:.75em}.Pager{padding-left:0;margin:1em 0;list-style:none;text-align:center;clear:both}.Pager:before,.Pager:after{content:" ";display:table}.Pager:after{clear:both}.Pager li{display:inline}.Pager li>a{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.Pager li>a:hover,.Pager li>a:focus{text-decoration:none;background-color:#eee}.Pager--next>a{float:right}.Pager--prev>a{float:left}.Checkbox{position:relative;display:block;padding-left:30px;cursor:pointer}.Checkbox input{position:absolute;z-index:-1;opacity:0}.Checkbox__indicator{position:absolute;top:50%;left:0;width:20px;height:20px;margin-top:-10px;background:var(--checkbox-background)}.Checkbox__indicator:after{position:absolute;display:none;content:""}.Checkbox:hover input~.Checkbox__indicator,.Checkbox input:focus~.Checkbox__indicator{background:var(--checkbox-hover-background)}.Checkbox input:checked~.Checkbox__indicator{background:var(--checkbox-checked-background)}.Checkbox input:checked~.Checkbox__indicator:after{display:block}.Checkbox:hover input:not([disabled]):checked~.Checkbox__indicator,.Checkbox input:checked:focus~.Checkbox__indicator{background:var(--checkbox-checked-hover-background)}.Checkbox input:disabled~.Checkbox__indicator{pointer-events:none;opacity:.6;background:var(--checkbox-disabled-background)}.Checkbox .Checkbox__indicator:after{top:4px;left:8px;width:5px;height:10px;transform:rotate(45deg);border:solid var(--checkbox-tick-color);border-width:0 2px 2px 0}.Checkbox input:disabled~.Checkbox__indicator:after{border-color:var(--checkbox-disabled-tick-color)}.Container{margin-right:auto;margin-left:auto}.Container--inner{width:80%;margin:0 auto}@media(min-width: 1200px){.Container{width:1170px}}@media(min-width: 992px){.Container{width:970px}}@media(min-width: 769px){.Container{width:750px}}.Homepage{padding-top:60px !important;background-color:var(--homepage-hero-background);border-radius:0;border:none;color:var(--homepage-hero-color);overflow:hidden;padding-bottom:0;margin-bottom:0;box-shadow:none}.HomepageTitle h2{width:80%;font-size:30px;margin:20px auto;text-align:center}.HomepageImage img{display:block;max-width:80%;margin:0 auto;height:auto}.HomepageButtons{padding:20px 0;background-color:var(--hero-button-block-background);text-align:center}.HomepageButtons:before,.HomepageButtons:after{content:" ";display:table}.HomepageButtons:after{clear:both}.HomepageButtons .Button--hero{padding:20px 30px;border-radius:0;text-shadow:none;opacity:.8;margin:0 10px;text-transform:uppercase;border:5px solid var(--hero-button-border-color);font-family:var(--font-family-heading);font-weight:700;background-image:none;filter:none;box-shadow:none}@media(max-width: 768px){.HomepageButtons .Button--hero{display:block;margin-bottom:10px}}.HomepageButtons .Button--hero:hover{opacity:1}.HomepageButtons .Button--hero.Button--secondary{background-color:var(--hero-button-secondary-background);color:var(--hero-button-secondary-color)}.HomepageButtons .Button--hero.Button--primary{background-color:var(--hero-buttom-primary-background);color:var(--hero-button-primary-color)}.HomepageContent{background-color:#fff;padding:40px 0}.HomepageContent ul li,.HomepageContent ol li{list-style:none;margin-bottom:.5em;position:relative}.HomepageContent ul li:before,.HomepageContent ol li:before{position:absolute;top:50%;left:-1.5em;content:"";width:0;height:0;border:.5em solid transparent;border-left:.5em solid var(--homepage-bullet-color);float:left;display:block;margin-top:-0.5em}.HomepageContent .HeroText{font-family:var(--font-family-heading);font-weight:300;font-size:16px;margin-bottom:20px;line-height:1.4}@media(min-width: 769px){.HomepageContent{padding:40px 20px}.HomepageContent .HeroText{font-size:21px}.HomepageContent .Row{margin:0 -15px}.HomepageContent .Row__third,.HomepageContent .Row__half,.HomepageContent .Row__quarter{float:left;position:relative;min-height:1px;padding-left:15px;padding-right:15px}.HomepageContent .Row__third{width:33.333333%}.HomepageContent .Row__half{width:50%}.HomepageContent .Row__quarter{width:25%}}.HomepageFooter{background-color:var(--homepage-footer-background);color:var(--homepage-footer-color);border:none;box-shadow:none}.HomepageFooter:before,.HomepageFooter:after{content:" ";display:table}.HomepageFooter:after{clear:both}@media(max-width: 768px){.HomepageFooter{padding:0 20px;text-align:center}.HomepageFooter .HomepageFooter__links{padding-left:0;list-style-type:none}}@media(min-width: 769px){.HomepageFooter .HomepageFooter__links{float:left}.HomepageFooter .HomepageFooter__twitter{float:right}}.HomepageFooter__links{margin:40px 0}.HomepageFooter__links li a{line-height:32px;font-size:16px;font-family:var(--font-family-heading);font-weight:700}.HomepageFooter__links li a:hover{text-decoration:underline}.HomepageFooter__twitter{margin:40px 0}.HomepageFooter .Twitter__button{margin-bottom:20px}@media print{*{text-shadow:none !important;color:#000 !important;background:transparent !important;box-shadow:none !important}h1,h2,h3,h4,h5,h6{break-after:avoid;break-before:auto}pre,blockquote{border:1px solid #999;font-style:italic;break-inside:avoid}img{break-inside:avoid;border:0}a,a:visited{text-decoration:underline}abbr[title]:after{content:" (" attr(title) ")"}q{quotes:none}q:before{content:""}q:after{content:" (" attr(cite) ")"}.PageBreak{display:block;break-before:always}.NoPrint{display:none}aside{display:none}.Pager{display:none}.Columns__right{width:100% !important}.s-content a:after{content:" (" attr(href) ")";font-size:80%;word-wrap:break-word}.s-content a[href^="#"]:after{content:""}h1 a[href]:after{font-size:50%}}@font-face{font-family:"League Gothic";src:url("fonts/leaguegothic.woff2") format("woff2"),url("fonts/leaguegothic.woff") format("woff");font-style:normal;font-display:swap}@font-face{font-family:"Cabin";src:url("fonts/cabin-regular.woff2") format("woff2"),url("fonts/cabin-regular.woff") format("woff");font-weight:normal;font-style:normal;font-display:swap}@font-face{font-family:"Cabin";src:url("fonts/cabin-italic.woff2") format("woff2"),url("fonts/cabin-italic.woff") format("woff");font-style:italic;font-display:swap}@font-face{font-family:"Cabin";src:url("fonts/cabin-bold.woff2") format("woff2"),url("fonts/cabin-bold.woff") format("woff");font-weight:bold;font-style:normal;font-display:swap}:root{--font-family-text: "Cabin", "Trebuchet MS", -apple-system, ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", Arial, sans-serif;--font-family-monospace: Monaco, Menlo, Consolas, "Lucida Console", "Courier New", monospace;--font-family-heading: "League Gothic", -apple-system, ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", Arial, sans-serif;--type-size-1: 4rem;--type-size-2: 3.236rem;--type-size-3: 2.618rem;--type-size-4: 2rem;--type-size-5: 1.618rem;--type-size-6: 1.309rem;--red: #e63c2f;--blue: #15284b;--light-blue: #93b7bb;--beige: #e8d5d3;--green: #2c9a42;--dark-gray: color(var(--beige) blend(var(--blue) 50%));--gray: color(var(--beige) blend(var(--blue) 25%));--light-gray: color(var(--beige) blend(var(--blue) 12.5%));--lighter-gray: var(--beige);--lightest-gray: color(var(--beige) blend(#fff 75%));--dark: var(--blue);--light: var(--light-blue);--sidebar-background: var(--beige);--sidebar-link-active-background: var(--light-blue);--sidebar-collapsible--hamburger-color: var(--beige);--text: var(--blue);--link-color: var(--red);--brand-color: var(--blue);--brand-background: var(--red);--code-tag-background-color: var(--lightest-gray);--code-tag-border-radius: 0;--code-tag-box-shadow: none;--homepage-navbar-background: var(--red);--hero-button-block-background: var(--beige);--homepage-hero-background: #fff;--content-floating-blocks-background: var(--blue)}html,body{font-size:16px}body{line-height:1.618}a.Link--external::after{content:""}.s-content code{display:inline-block;padding-top:0;padding-bottom:0;padding:.5ch;border:0}.s-content code::before,.s-content code::after{content:""}pre .s-content code{display:inline}.s-content table{border-collapse:separate;border-spacing:2px;border:2px solid var(--gray)}.s-content table thead,.s-content table tbody{background-color:#fff}.s-content table tr{border-top:0}.s-content table tr:nth-child(2n){background-color:transparent}.s-content table tr:nth-child(2n) td{background-color:var(--lightest-gray)}.s-content table th,.s-content table td{border:0}.s-content table,.Nav__item .Nav__item{font-size:1rem}.Brand,h1,h2,h3,h4,h5,h6{font-weight:normal}.Button{border-radius:0}.HomepageButtons .Button--hero{font-weight:normal;font-size:var(--type-size-6)}.Page__header{border-bottom:2px solid var(--lighter-gray)}.Pager li>a{border:2px solid var(--lighter-gray);border-radius:0}.Pager li>a:hover,.Pager li>a:focus{background-color:var(--lighter-gray)}.Pager--prev a::before{content:"← "}.Pager--next a::after{content:" →"}.Navbar{height:auto;box-shadow:none}.Navbar .Brand{float:none;line-height:inherit;height:auto}.Homepage{padding-top:10px !important}.Nav__item{font-size:var(--type-size-6)}.Nav__arrow:before,.Nav .Nav .Nav__item a .Nav__arrow:before{font-family:var(--font-family-heading);width:1ch;height:1ch}.TableOfContentsContainer__title{border-bottom:4px solid var(--lighter-gray)}ul.TableOfContents{border-left:6px solid var(--lighter-gray)}.Columns__right--full .TableOfContentsContainer .TableOfContentsContainer__content>.TableOfContents{border-right:2px solid var(--lighter-gray)}.Columns__right--full .TableOfContentsContainer a{border-bottom:1px solid var(--lighter-gray)}.clients thead tr:first-child th{text-align:left}.clients thead tr:first-child th:first-child{width:15%}.clients thead tr:first-child th:nth-child(3){width:50%;text-align:center}.clients thead tr+tr th{width:12%;text-align:center}.clients tbody td:nth-child(3),.clients tbody td:nth-child(4),.clients tbody td:nth-child(5),.clients tbody td:nth-child(6){text-align:center}.clients tbody td.Y{color:var(--green)}.clients tbody td.N{color:var(--red)}.hljs,.s-content pre{background:var(--blue);color:var(--beige)}.hljs{display:block;overflow-x:auto;padding:.5em}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold}.hljs-comment,.hljs-quote{color:#978e9c}.hljs-keyword,.hljs-selector-tag,.hljs-addition{color:#acb39a}.hljs-number,.hljs-string,.hljs-meta .hljs-meta-string,.hljs-literal,.hljs-doctag,.hljs-regexp{color:var(--light-blue)}.hljs-title,.hljs-section,.hljs-name,.hljs-selector-id,.hljs-selector-class{color:#82b7e5}.hljs-attribute,.hljs-attr,.hljs-variable,.hljs-template-variable,.hljs-class .hljs-title,.hljs-type{color:#c5b031}.hljs-symbol,.hljs-bullet,.hljs-subst,.hljs-meta,.hljs-meta .hljs-keyword,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-link{color:#ea8031}.hljs-built_in,.hljs-deletion{color:var(--red)}.hljs-formula{background:#686986}@media(--viewport-large){.Columns__left{border:0}} +/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ +html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;font-size:14px}body{margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress,sub,sup{vertical-align:baseline}.s-content pre code:after,.s-content pre code:before,[hidden],template{display:none}a{background-color:transparent;-webkit-text-decoration-skip:objects;text-decoration:none;color:#e63c2f}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}.s-content blockquote cite,dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;clear:both;margin:1em 0;border:0;border-top:1px solid #ddd}button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:700}button,hr,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}*,:after,:before{box-sizing:border-box}@media (min-width:850px){html{font-size:16px}}body,html{height:100%;background-color:#fff;color:#15284b}.Columns__left{background-color:#e8d5d3}.Columns__right__content{padding:10px;background-color:#fff}@media (max-width:768px){html:not(.no-js) .Collapsible__content{height:0;overflow:hidden;transition:height 400ms ease-in-out}}.Collapsible__trigger{margin:12px;padding:7px 10px;background-color:transparent;border:0;float:right;background-image:none;filter:none;box-shadow:none}.Collapsible__trigger__bar{display:block;width:18px;height:2px;margin-top:2px;margin-bottom:3px;background-color:#e8d5d3}.Collapsible__trigger:hover{background-color:#93b7bb;box-shadow:none}.Collapsible__trigger:hover .Collapsible__trigger__bar{background-color:#15284b}@media screen and (min-width:769px){body{background-color:#15284b}.Navbar{position:fixed;z-index:1030;width:100%}.Collapsible__trigger{display:none!important}.Collapsible__content{display:block!important}.Columns{height:100%}.Columns:after,.Columns:before{content:" ";display:table}.Columns:after{clear:both}.Columns__left,.Columns__right{position:relative;min-height:1px;float:left;overflow:auto;height:100%}.Columns__left{width:25%;border-right:1px solid #e7e7e9;overflow-x:hidden}.Columns__right{width:75%}.Columns__right__content{padding:0 20px 20px;min-height:100%}}.Page{max-width:860px}body{font-family:"Cabin","Trebuchet MS",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-feature-settings:"kern" 1;-webkit-font-kerning:normal;font-kerning:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;line-height:1.618}h1,h2,h3,h4,h5,h6{font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.s-content h1,.s-content h2,.s-content h3,.s-content h4,.s-content h5,.s-content h6{cursor:text;line-height:1.4em;margin:2em 0 .5em}.s-content h1 code,.s-content h1 tt,.s-content h2 code,.s-content h2 tt,.s-content h3 code,.s-content h3 tt,.s-content h4 code,.s-content h4 tt,.s-content h5 code,.s-content h5 tt,.s-content h6 code,.s-content h6 tt{font-size:inherit}.s-content h1 i,.s-content h2 i,.s-content h3 i,.s-content h4 i,.s-content h5 i,.s-content h6 i{font-size:.7em}.s-content h1 p,.s-content h2 p,.s-content h3 p,.s-content h4 p,.s-content h5 p,.s-content h6 p{margin-top:0}.s-content h1{margin-top:0;font-size:2.618rem}.s-content h2{font-size:2rem}.s-content h3{font-size:1.618rem}.s-content h4,.s-content h5,.s-content h6,.s-content small{font-size:1.309rem}.s-content a{text-decoration:underline}.s-content p{margin-bottom:1.3em}.s-content ol,.s-content ul{padding-left:2em}.s-content ul p,.s-content ul ul{margin:0}.s-content dl{padding:0}.s-content dl dt{font-weight:700;font-style:italic;padding:0;margin:15px 0 5px}.s-content dl dt:first-child{padding:0}.s-content dl dd{margin:0 0 15px;padding:0 15px}.s-content blockquote{margin:.75em 2em;padding:.5em 1em;font-style:italic;border-left:.25em solid #15284b}.s-content blockquote cite:before{content:"\2014";padding-right:.5em}.s-content table{width:100%;padding:0;margin-bottom:1em;border-collapse:separate;border-spacing:2px;border:2px solid #b3aab1}.s-content table+table{margin-top:1em}.s-content table tr{background-color:#fff;margin:0;padding:0;border-top:0}.s-content table tr:nth-child(2n){background-color:transparent}.s-content table th{font-weight:700;background:#e8d5d3}.s-content table td,.s-content table th{margin:0;padding:.5em}.s-content blockquote>:first-child,.s-content dl dd>:first-child,.s-content dl dt>:first-child,.s-content ol>:first-child,.s-content table td>:first-child,.s-content table th>:first-child,.s-content ul>:first-child{margin-top:0}.s-content blockquote>:last-child,.s-content dl dd>:last-child,.s-content dl dt>:last-child,.s-content ol>:last-child,.s-content table td>:last-child,.s-content table th>:last-child,.s-content ul>:last-child{margin-bottom:0}.s-content img{max-width:100%;display:block;margin:0 auto}.s-content code{font-family:Monaco,Menlo,Consolas,"Lucida Console","Courier New",monospace;padding-top:.1rem;padding-bottom:.1rem;background:#f9f5f4;border-radius:0;box-shadow:none;display:inline-block;padding:.5ch;border:0}.s-content code:after,.s-content code:before{letter-spacing:-.2em;content:"\00a0"}.s-content pre{background:#f5f2f0;line-height:1.5em;overflow:auto;border:0;border-radius:0;padding:.75em 20px;margin:0 -20px 20px}.s-content pre code{margin:0;padding:0;white-space:pre;box-shadow:none}.s-content pre code,.s-content pre tt{background-color:transparent;border:0}.s-content ins,.s-content u{text-decoration:none;border-bottom:1px solid #15284b}.s-content del a,.s-content ins a,.s-content u a{color:inherit}a.Link--external:after{content:" " url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAVklEQVR4Xn3PgQkAMQhDUXfqTu7kTtkpd5RA8AInfArtQ2iRXFWT2QedAfttj2FsPIOE1eCOlEuoWWjgzYaB/IkeGOrxXhqB+uA9Bfcm0lAZuh+YIeAD+cAqSz4kCMUAAAAASUVORK5CYII=)}a.Link--broken{color:red}p{margin:0 0 1em}.Button{display:inline-block;text-align:center;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;margin-bottom:0}.Button--small{font-size:12px;line-height:1.5;border-radius:3px}.Button--default{color:#333;background-color:#fff;border-color:#ccc}.Button--default.Button--active{color:#333;background-color:#e6e6e6;border-color:#adadad}.Brand,.Nav__item a:hover{color:#15284b;text-shadow:none}.Brand,.Navbar{background-color:#e63c2f}.Brand{display:block;padding:.75em .6em;font-size:2rem;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.Navbar{box-shadow:0 1px 5px rgba(0,0,0,.25);margin-bottom:0}.CodeToggler{padding:0 20px}.CodeToggler__text{font-size:12px;line-height:1.5;padding:6px 10px 6px 0;display:inline-block;vertical-align:middle}.no-js .CodeToggler{display:none}.Nav{margin:0;padding:0}.Nav__arrow{display:inline-block;position:relative;width:16px;margin-left:-16px}.Nav__arrow:before{position:absolute;display:block;content:"";margin:-.25em 0 0 -.4em;left:50%;top:50%;border-right:.15em solid #15284b;border-top:.15em solid #15284b;transform:rotate(45deg);transition-duration:.3s}.Nav__item,.Nav__item a{display:block}.Nav__item a{margin:0;padding:6px 15px 6px 20px;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-weight:400;color:#15284b;text-shadow:none}.Nav__item a:hover{background-color:#93b7bb}.Nav .Nav{margin-left:15px}html:not(.no-js) .Nav .Nav{height:0;transition:height 400ms ease-in-out;overflow:hidden}.Nav .Nav .Nav__item a{margin:0 0 0 -15px;padding:3px 30px;font-family:"Cabin","Trebuchet MS",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;color:#15284b;opacity:.7}.HomepageButtons .Button--hero:hover,.Nav .Nav .Nav__item a:hover{opacity:1}.Nav .Nav .Nav__item--active a{color:#15284b}.Nav__item--active>a,.Nav__item--open>a{background-color:#93b7bb}.Nav__item--open>a>.Nav__arrow:before{margin-left:-.25em;transform:rotate(135deg)}.Page__header{margin:0 0 10px;padding:0}.Page__header:after,.Page__header:before{content:" ";display:table}.Page__header:after{clear:both}.Page__header h1{margin:0;padding:0;line-height:57px}.Page__header--separator{height:.6em}.Page__header a{text-decoration:none}.Page__header .EditOn,.Page__header .ModifiedDate{float:left;font-size:10px;color:gray}.Page__header .EditOn{float:right}.Links,.Twitter{padding:0 20px}.Links a{font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-weight:400;color:#15284b;line-height:2em}.Twitter{font:11px/18px "Helvetica Neue",Arial,sans-serif}.Twitter__button{text-decoration:none;display:inline-block;vertical-align:top;zoom:1;position:relative;height:20px;box-sizing:border-box;padding:1px 8px 1px 6px;background-color:#1b95e0;color:#fff;border-radius:3px;font-weight:500;cursor:pointer}.Twitter__button .Twitter__button__label{display:inline-block;vertical-align:top;zoom:1;margin-left:3px;white-space:nowrap}.Twitter__button svg{position:relative;top:2px;display:inline-block;width:14px;height:14px}.PoweredBy{padding:0 20px 1rem;font-size:1.309rem}.Search{position:relative}.Search__field{display:block;width:100%;height:34px;padding:6px 30px 6px 20px;color:#555;border-width:0 0 1px;border-bottom:1px solid #ccc;background:#fff;transition:border-color ease-in-out .15s}.Search__field:focus{border-color:#93b7bb;outline:0}.Search__icon{position:absolute;right:9px;top:9px;width:16px;height:16px}.Navbar .Search{float:right;margin:8px 20px}.Navbar .Search__field{box-shadow:inset 0 1px 1px rgba(0,0,0,.075);border-width:0;border-radius:4px;padding-left:10px}.TableOfContentsContainer{float:right;min-width:300px;max-width:25%;padding-left:1em}.TableOfContentsContainer__title{margin-bottom:0!important}.TableOfContentsContainer__content{border:1px solid #efefef;border-width:4px 2px 2px 6px}.TableOfContentsContainer__content>.TableOfContents>li+li{border-top:1px solid #ddd}ul.TableOfContents{font-size:1rem;padding-left:0;margin:0;list-style-type:none;border-left:6px solid #e8d5d3}ul.TableOfContents p{margin-bottom:0}ul.TableOfContents a{text-decoration:none;display:block;padding:.2em 0 .2em .75em}ul.TableOfContents .TableOfContents{padding-left:.75em}.Pager{padding-left:0;margin:1em 0;list-style:none;text-align:center}.Pager:after,.Pager:before{content:" ";display:table}.Pager,.Pager:after{clear:both}.Pager li{display:inline}.Pager li>a{display:inline-block;padding:5px 14px;background-color:#fff}.Pager li>a:focus,.Pager li>a:hover{text-decoration:none}.Pager--next>a{float:right}.Pager--prev>a{float:left}.Checkbox{position:relative;display:block;padding-left:30px;cursor:pointer}.Checkbox input{position:absolute;z-index:-1;opacity:0}.Checkbox__indicator{position:absolute;top:50%;left:0;width:20px;height:20px;margin-top:-10px;background:#e6e6e6}.Checkbox__indicator:after{position:absolute;display:none;content:""}.Checkbox input:focus~.Checkbox__indicator,.Checkbox:hover input~.Checkbox__indicator{background:#ccc}.Checkbox input:checked~.Checkbox__indicator{background:#15284b}.Checkbox input:checked~.Checkbox__indicator:after{display:block}.Checkbox input:checked:focus~.Checkbox__indicator,.Checkbox:hover input:not([disabled]):checked~.Checkbox__indicator{background:#93b7bb}.Checkbox input:disabled~.Checkbox__indicator{pointer-events:none;opacity:.6;background:#e6e6e6}.Checkbox .Checkbox__indicator:after{top:4px;left:8px;width:5px;height:10px;transform:rotate(45deg);border:solid #fff;border-width:0 2px 2px 0}.Checkbox input:disabled~.Checkbox__indicator:after{border-color:#7b7b7b}.Hidden{display:none}.Container{margin-right:auto;margin-left:auto}.Container--inner{width:80%;margin:0 auto}@media (min-width:1200px){.Container{width:1170px}}@media (min-width:992px){.Container{width:970px}}@media (min-width:769px){.Container{width:750px}}.Homepage{background-color:#fff;border-radius:0;border:0;color:#15284b;overflow:hidden;padding-bottom:0;margin-bottom:0;box-shadow:none}.HomepageTitle h2{width:80%;font-size:30px;margin:20px auto;text-align:center}.HomepageImage img{display:block;max-width:80%;margin:0 auto;height:auto}.HomepageButtons{padding:20px 0;background-color:#e8d5d3;text-align:center}.HomepageButtons:after,.HomepageButtons:before{content:" ";display:table}.HomepageButtons:after{clear:both}.HomepageButtons .Button--hero{padding:20px 30px;border-radius:0;text-shadow:none;opacity:.8;margin:0 10px;text-transform:uppercase;border:5px solid #15284b;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;background-image:none;filter:none;box-shadow:none}@media (max-width:768px){.HomepageButtons .Button--hero{display:block;margin-bottom:10px}}.HomepageButtons .Button--hero.Button--secondary{background-color:#93b7bb;color:#15284b}.HomepageButtons .Button--hero.Button--primary{background-color:#15284b;color:#e8d5d3}.HomepageContent{background-color:#fff;padding:40px 0}.HomepageContent ol li,.HomepageContent ul li{list-style:none;margin-bottom:.5em;position:relative}.HomepageContent ol li:before,.HomepageContent ul li:before{position:absolute;top:50%;left:-1.5em;content:"";width:0;height:0;border:.5em solid transparent;border-left:.5em solid #93b7bb;float:left;display:block;margin-top:-.5em}.HomepageContent .HeroText,.HomepageFooter__links li a{font-size:16px;font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.HomepageContent .HeroText{font-weight:300;margin-bottom:20px;line-height:1.4}@media (min-width:769px){.HomepageContent{padding:40px 20px}.HomepageContent .HeroText{font-size:21px}.HomepageContent .Row{margin:0 -15px}.HomepageContent .Row__half,.HomepageContent .Row__quarter,.HomepageContent .Row__third{float:left;position:relative;min-height:1px;padding-left:15px;padding-right:15px}.HomepageContent .Row__third{width:33.333333%}.HomepageContent .Row__half{width:50%}.HomepageContent .Row__quarter{width:25%}}.HomepageFooter{background-color:#15284b;color:#93b7bb;border:0;box-shadow:none}.HomepageFooter:after,.HomepageFooter:before{content:" ";display:table}.HomepageFooter:after{clear:both}@media (max-width:768px){.HomepageFooter{padding:0 20px;text-align:center}.HomepageFooter .HomepageFooter__links{padding-left:0;list-style-type:none}}@media (min-width:769px){.HomepageFooter .HomepageFooter__links{float:left}.HomepageFooter .HomepageFooter__twitter{float:right}}.HomepageFooter__links,.HomepageFooter__twitter{margin:40px 0}.HomepageFooter__links li a{line-height:32px;font-weight:700}.HomepageFooter__links li a:hover{text-decoration:underline}.HomepageFooter .Twitter__button{margin-bottom:20px}@media print{*{text-shadow:none!important;color:#000!important;background:0 0!important;box-shadow:none!important}h1,h2,h3,h4,h5,h6{page-break-after:avoid;page-break-before:auto}blockquote,img,pre{page-break-inside:avoid}blockquote,pre{border:1px solid #999;font-style:italic}img{border:0}a,a:visited{text-decoration:underline}abbr[title]:after{content:" (" attr(title) ")"}q{quotes:none}.s-content a[href^="#"]:after,q:before{content:""}q:after{content:" (" attr(cite) ")"}.PageBreak{display:block;page-break-before:always}.NoPrint,.Pager,aside{display:none}.Columns__right{width:100%!important}.s-content a:after{content:" (" attr(href) ")";font-size:80%;word-wrap:break-word}h1 a[href]:after{font-size:50%}}@font-face{font-family:'League Gothic';src:url(fonts/leaguegothic.woff2) format('woff2'),url(fonts/leaguegothic.woff) format('woff');font-style:normal;font-display:swap}@font-face{font-family:'Cabin';src:url(fonts/cabin-regular.woff2) format('woff2'),url(fonts/cabin-regular.woff) format('woff');font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:'Cabin';src:url(fonts/cabin-italic.woff2) format('woff2'),url(fonts/cabin-italic.woff) format('woff');font-style:italic;font-display:swap}@font-face{font-family:'Cabin';src:url(fonts/cabin-bold.woff2) format('woff2'),url(fonts/cabin-bold.woff) format('woff');font-weight:700;font-style:normal;font-display:swap}.s-content code::after,.s-content code::before,a.Link--external::after{content:''}pre .s-content code{display:inline}.s-content table tbody,.s-content table thead{background-color:#fff}.s-content table tr:nth-child(2n) td{background-color:#f9f5f4}.s-content table td,.s-content table th{border:0}.Nav__item .Nav__item,.s-content table{font-size:1rem}.Brand,h1,h2,h3,h4,h5,h6{font-weight:400}.Button,.Pager li>a{border-radius:0}.HomepageButtons .Button--hero{font-weight:400;font-size:1.309rem}.Page__header{border-bottom:2px solid #e8d5d3}.Pager li>a{border:2px solid #e8d5d3}.Pager li>a:focus,.Pager li>a:hover{background-color:#e8d5d3}.Pager--prev a::before{content:"\2190\00a0"}.Pager--next a::after{content:"\00a0\2192"}.Navbar{height:auto;box-shadow:none}.Navbar .Brand{float:none;line-height:inherit;height:auto}.Homepage{padding-top:10px!important}.Nav__item{font-size:1.309rem}.Nav .Nav .Nav__item a .Nav__arrow:before,.Nav__arrow:before{font-family:"League Gothic",-apple-system,".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;width:1ch;height:1ch}.TableOfContentsContainer__title{border-bottom:4px solid #e8d5d3}.Columns__right--full .TableOfContentsContainer .TableOfContentsContainer__content>.TableOfContents{border-right:2px solid #e8d5d3}.Columns__right--full .TableOfContentsContainer a{border-bottom:1px solid #e8d5d3}.clients thead tr:first-child th{text-align:left}.clients thead tr:first-child th:first-child{width:15%}.clients thead tr:first-child th:nth-child(3){width:50%;text-align:center}.clients thead tr+tr th{width:12%;text-align:center}.clients tbody td:nth-child(3),.clients tbody td:nth-child(4),.clients tbody td:nth-child(5),.clients tbody td:nth-child(6){text-align:center}.clients tbody td.Y{color:#2c9a42}.clients tbody td.N{color:#e63c2f}.hljs,.s-content pre{background:#15284b;color:#e8d5d3}.hljs{display:block;overflow-x:auto;padding:.5em}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-comment,.hljs-quote{color:#978e9c}.hljs-addition,.hljs-keyword,.hljs-selector-tag{color:#acb39a}.hljs-doctag,.hljs-literal,.hljs-meta .hljs-meta-string,.hljs-number,.hljs-regexp,.hljs-string{color:#93b7bb}.hljs-name,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-title{color:#82b7e5}.hljs-attr,.hljs-attribute,.hljs-class .hljs-title,.hljs-template-variable,.hljs-type,.hljs-variable{color:#c5b031}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-meta .hljs-keyword,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-subst,.hljs-symbol{color:#ea8031}.hljs-built_in,.hljs-deletion{color:#e63c2f}.hljs-formula{background:#686986}@media (min-width:850px){.Columns__left{border:0}} \ No newline at end of file diff --git a/docs/theme/arsse/daux.min.js b/docs/theme/arsse/daux.min.js index d9e2c54c..fd875881 100644 --- a/docs/theme/arsse/daux.min.js +++ b/docs/theme/arsse/daux.min.js @@ -1,2 +1,2 @@ -!function(){"use strict";function e(e){"loading"===document.readyState?document.addEventListener("DOMContentLoaded",e):e()}function t(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[o++]}},e:function(e){throw e},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,l=!0,c=!1;return{s:function(){r=e[Symbol.iterator]()},n:function(){var e=r.next();return l=e.done,e},e:function(e){c=!0,i=e},f:function(){try{l||null==r.return||r.return()}finally{if(c)throw i}}}}function o(e){var t=void 0!==e.preventDefault;t&&e.preventDefault();var n=function(e){for(var t=e;(t=t.parentNode)&&9!==t.nodeType;)if(1===t.nodeType&&t.classList.contains("Nav__item"))return t;throw new Error("Could not find a NavItem...")}(e.target),r=n.querySelector("ul.Nav");t&&n.classList.contains("Nav__item--open")?(r.style.height="".concat(r.scrollHeight,"px"),r.style.transitionDuration="150ms",r.style.height="0px",n.classList.remove("Nav__item--open")):t?(r.style.transitionDuration="150ms",r.addEventListener("transitionend",(function e(t){"0px"!==t.target.style.height&&(t.target.style.height="auto"),t.target.removeEventListener("transitionend",e)})),r.style.height="".concat(r.scrollHeight,"px"),n.classList.add("Nav__item--open")):r.style.height="auto"}e((function(){var e=document.querySelectorAll(".s-content pre"),n=document.querySelector(".CodeToggler");n&&(e.length?function(e,n){var r=e.querySelector(".CodeToggler__button--main");r.addEventListener("change",(function(e){t(n,!e.target.checked)}),!1);var o=!1;try{"false"===(o=localStorage.getItem("daux_code_blocks_hidden"))?o=!1:"true"===o&&(o=!0),o&&(t(n,!!o),r.checked=!o)}catch(e){}}(n,e):n.classList.add("CodeToggler--hidden"))})),e((function(){var e=document.querySelector(".Collapsible__trigger");if(e){var t=document.querySelector(".Collapsible__content");e.addEventListener("click",(function(n){t.classList.contains("Collapsible__content--open")?(t.style.height=0,t.classList.remove("Collapsible__content--open"),e.setAttribute("aria-expanded","false")):(e.setAttribute("aria-expanded","true"),t.style.transitionDuration="150ms",t.style.height="".concat(t.scrollHeight,"px"),t.classList.add("Collapsible__content--open"))}))}})),e((function(){var e=document.querySelectorAll("pre > code:not(.hljs)");if(e.length){var t=document.getElementsByTagName("head")[0],n=document.createElement("script");n.type="text/javascript",n.async=!0,n.src="".concat(window.base_url,"daux_libraries/highlight.pack.js"),n.onload=function(t){[].forEach.call(e,window.hljs.highlightBlock)},t.appendChild(n)}})),e((function(){for(var e,t=document.querySelectorAll(".Nav__item.has-children i.Nav__arrow"),n=t.length-1;n>=0;n--)(e=t[n]).addEventListener("click",o),e.parentNode.parentNode.classList.contains("Nav__item--open")&&o({target:e});var a,i=r(document.querySelectorAll(".Nav__item__link--nopage"));try{for(i.s();!(a=i.n()).done;){a.value.addEventListener("click",o)}}catch(e){i.e(e)}finally{i.f()}}))}(); +var e=document.querySelectorAll(".s-content pre"),t=document.querySelector(".CodeToggler"),n="daux_code_blocks_hidden";function a(t){for(var a=0;a code:not(.hljs)");if(l.length){var i=document.getElementsByTagName("head")[0],c=document.createElement("script");c.type="text/javascript",c.async=!0,c.src="".concat(window.base_url,"daux_libraries/highlight.pack.js"),c.onload=function(e){[].forEach.call(l,window.hljs.highlightBlock)},i.appendChild(c)}function s(e){var t=void 0!==e.preventDefault;t&&e.preventDefault();var n=function(e){for(var t=e;(t=t.parentNode)&&9!==t.nodeType;)if(1===t.nodeType&&t.classList.contains("Nav__item"))return t;throw new Error("Could not find a NavItem...")}(e.target),a=n.querySelector("ul.Nav");t&&n.classList.contains("Nav__item--open")?(a.style.height="".concat(a.scrollHeight,"px"),a.style.transitionDuration="150ms",a.style.height="0px",n.classList.remove("Nav__item--open")):t?(a.style.transitionDuration="150ms",a.addEventListener("transitionend",(function e(t){"0px"!==t.target.style.height&&(t.target.style.height="auto"),t.target.removeEventListener("transitionend",e)})),a.style.height="".concat(a.scrollHeight,"px"),n.classList.add("Nav__item--open")):a.style.height="auto"}for(var d,u=document.querySelectorAll(".Nav__item.has-children i.Nav__arrow"),h=u.length-1;h>=0;h--)(d=u[h]).addEventListener("click",s),d.parentNode.parentNode.classList.contains("Nav__item--open")&&s({target:d});var g=document.querySelectorAll(".Nav__item__link--nopage"),v=!0,p=!1,_=void 0;try{for(var y,m=g[Symbol.iterator]();!(v=(y=m.next()).done);v=!0){y.value.addEventListener("click",s)}}catch(e){p=!0,_=e}finally{try{v||null==m.return||m.return()}finally{if(p)throw _}} //# sourceMappingURL=daux.min.js.map diff --git a/docs/theme/src/arsse.scss b/docs/theme/src/arsse.scss index 1df6dde0..6f5d9d83 100644 --- a/docs/theme/src/arsse.scss +++ b/docs/theme/src/arsse.scss @@ -6,10 +6,8 @@ @import "../../../vendor-bin/daux/vendor/daux/daux.io/src/css/theme_daux/_typography.scss"; @import "../../../vendor-bin/daux/vendor/daux/daux.io/src/css/theme_daux/_components.scss"; @import "../../../vendor-bin/daux/vendor/daux/daux.io/src/css/theme_daux/_homepage.scss"; +@import "../../../vendor-bin/daux/vendor/daux/daux.io/src/css/theme_daux/_print.scss" print; -@media print { - @import "../../../vendor-bin/daux/vendor/daux/daux.io/src/css/theme_daux/_print.scss"; -} /* The Arsse overrides */ @@ -100,10 +98,6 @@ --content-floating-blocks-background: var(--blue); } -html, body { - font-size: 16px; -} - body { line-height: 1.618; } diff --git a/package.json b/package.json index e0267e35..e7a3b351 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,17 @@ { "devDependencies": { - "sass": "^1.32.8" + "autoprefixer": "^9.6.1", + "postcss": "^7.0.0", + "postcss-cli": "^7.1.1", + "postcss-color-function": "^4.1.0", + "postcss-csso": "^4.0.0", + "postcss-custom-media": "^7.0.8", + "postcss-custom-properties": "^9.0.2", + "postcss-discard-comments": "^4.0.2", + "postcss-import": "^12.0.1", + "postcss-media-minmax": "^4.0.0", + "postcss-nested": "^4.1.2", + "postcss-sassy-mixins": "^2.1.0", + "postcss-scss": "^2.0.0" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..4eecda3c --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,17 @@ +module.exports = ctx => ({ + //map: ctx.options.map, + parser: 'postcss-scss', + //syntax: 'postcss-scss', + plugins: { + 'postcss-import': { root: ctx.file.dirname }, + 'postcss-discard-comments': {}, + 'postcss-sassy-mixins': {}, + 'postcss-custom-media': {preserve: false}, + 'postcss-media-minmax': {}, + 'postcss-custom-properties': {preserve: false}, + 'postcss-color-function': {}, + 'postcss-nested': {}, + 'autoprefixer': {}, + 'postcss-csso': {}, + } +}) diff --git a/yarn.lock b/yarn.lock index 850a7cd8..24fe8bd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,56 @@ # yarn lockfile v1 +"@nodelib/fs.scandir@2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" + integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== + dependencies: + "@nodelib/fs.stat" "2.0.4" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" + integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" + integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== + dependencies: + "@nodelib/fs.scandir" "2.1.4" + fastq "^1.6.0" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + anymatch@~3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" @@ -10,19 +60,135 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +autoprefixer@^9.6.1: + version "9.8.6" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" + integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== + dependencies: + browserslist "^4.12.0" + caniuse-lite "^1.0.30001109" + colorette "^1.2.1" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.32" + postcss-value-parser "^4.1.0" + +balanced-match@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" + integrity sha1-tQS9BYabOSWd0MXvw12EMXbczEo= + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -braces@~3.0.2: +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== dependencies: fill-range "^7.0.1" -"chokidar@>=2.0.0 <4.0.0": +browserslist@^4.12.0: + version "4.16.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" + integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== + dependencies: + caniuse-lite "^1.0.30001181" + colorette "^1.2.1" + electron-to-chromium "^1.3.649" + escalade "^3.1.1" + node-releases "^1.1.70" + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= + dependencies: + caller-callsite "^2.0.0" + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001181: + version "1.0.30001208" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz#a999014a35cebd4f98c405930a057a0d75352eb9" + integrity sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA== + +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.1, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.3.0: version "3.5.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== @@ -37,6 +203,185 @@ braces@~3.0.2: optionalDependencies: fsevents "~2.3.1" +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + +color-convert@^1.3.0, color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" + integrity sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE= + dependencies: + color-name "^1.0.0" + +color@^0.11.0: + version "0.11.4" + resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" + integrity sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q= + dependencies: + clone "^1.0.2" + color-convert "^1.3.0" + color-string "^0.3.0" + +colorette@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +cosmiconfig@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.13.1" + parse-json "^4.0.0" + +css-color-function@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.3.tgz#8ed24c2c0205073339fafa004bc8c141fccb282e" + integrity sha1-jtJMLAIFBzM5+voAS8jBQfzLKC4= + dependencies: + balanced-match "0.1.0" + color "^0.11.0" + debug "^3.1.0" + rgb "~0.1.0" + +css-tree@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +csso@^4.0.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +dependency-graph@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.9.0.tgz#11aed7e203bc8b00f48356d92db27b265c445318" + integrity sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +electron-to-chromium@^1.3.649: + version "1.3.710" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.710.tgz#b33d316e5d6de92b916e766d8a478d19796ffe11" + integrity sha512-b3r0E2o4yc7mNmBeJviejF1rEx49PUBi+2NPa7jHEX3arkAXnVgLhR0YmV8oi6/Qf3HH2a8xzQmCjHNH0IpXWQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +fast-glob@^3.1.1: + version "3.2.5" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" + integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + +fastq@^1.6.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" + integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== + dependencies: + reusify "^1.0.4" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -44,18 +389,163 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +fs-extra@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fsevents@~2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -glob-parent@~5.1.0: +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-stdin@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" + integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== + +glob-parent@^5.1.0, glob-parent@~5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" +glob@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globby@^11.0.0: + version "11.0.3" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" + integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + +import-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" + integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= + dependencies: + import-from "^2.1.0" + +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + +import-from@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" + integrity sha1-M1238qev/VOqpHHUuAId7ja387E= + dependencies: + resolve-from "^3.0.0" + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ip-regex@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" + integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -63,11 +553,28 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-core-module@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" + integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== + dependencies: + has "^1.0.3" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" @@ -80,16 +587,376 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-url-superb@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-url-superb/-/is-url-superb-3.0.0.tgz#b9a1da878a1ac73659047d1e6f4ef22c209d3e25" + integrity sha512-3faQP+wHCGDQT1qReM5zCPx2mxoal6DzbzquFlCYJLWyy4WPTved33ea2xFbX37z4NoriEwZGIYhFtx8RUB5wQ== + dependencies: + url-regex "^5.0.0" + +js-base64@^2.1.9: + version "2.6.4" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" + integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash@^4.17.11: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" + integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== + dependencies: + chalk "^2.0.1" + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +"minimatch@2 || 3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +node-releases@^1.1.70: + version "1.1.71" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" + integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -picomatch@^2.0.4, picomatch@^2.2.1: +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +postcss-cli@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/postcss-cli/-/postcss-cli-7.1.2.tgz#ba8d5d918b644bd18e80ad2c698064d4c0da51cd" + integrity sha512-3mlEmN1v2NVuosMWZM2tP8bgZn7rO5PYxRRrXtdSyL5KipcgBDjJ9ct8/LKxImMCJJi3x5nYhCGFJOkGyEqXBQ== + dependencies: + chalk "^4.0.0" + chokidar "^3.3.0" + dependency-graph "^0.9.0" + fs-extra "^9.0.0" + get-stdin "^8.0.0" + globby "^11.0.0" + postcss "^7.0.0" + postcss-load-config "^2.0.0" + postcss-reporter "^6.0.0" + pretty-hrtime "^1.0.3" + read-cache "^1.0.0" + yargs "^15.0.2" + +postcss-color-function@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.1.0.tgz#b6f9355e07b12fcc5c34dab957834769b03d8f57" + integrity sha512-2/fuv6mP5Lt03XbRpVfMdGC8lRP1sykme+H1bR4ARyOmSMB8LPSjcL6EAI1iX6dqUF+jNEvKIVVXhan1w/oFDQ== + dependencies: + css-color-function "~1.3.3" + postcss "^6.0.23" + postcss-message-helpers "^2.0.0" + postcss-value-parser "^3.3.1" + +postcss-csso@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-csso/-/postcss-csso-4.0.0.tgz#30fef9303ecbeb0424dab1228275416fc7186a50" + integrity sha512-Yh9Ug0w3+T/LZIh1vGJQY8+hE13yFRHpINoAmgOhvu9lBmG1jyHkAprGHEHlGjWODJzB4DCNBVBb6Cs0QEoglQ== + dependencies: + csso "^4.0.2" + +postcss-custom-media@^7.0.8: + version "7.0.8" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz#fffd13ffeffad73621be5f387076a28b00294e0c" + integrity sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg== + dependencies: + postcss "^7.0.14" + +postcss-custom-properties@^9.0.2: + version "9.2.0" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-9.2.0.tgz#80bae0d6e0c510245ace7ede95ac527712ea24e7" + integrity sha512-IFRV7LwapFkNa3MtvFpw+MEhgyUpaVZ62VlR5EM0AbmnGbNhU9qIE8u02vgUbl1gLkHK6sterEavamVPOwdE8g== + dependencies: + postcss "^7.0.17" + postcss-values-parser "^3.0.5" + +postcss-discard-comments@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" + integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== + dependencies: + postcss "^7.0.0" + +postcss-import@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153" + integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw== + dependencies: + postcss "^7.0.1" + postcss-value-parser "^3.2.3" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-load-config@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a" + integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw== + dependencies: + cosmiconfig "^5.0.0" + import-cwd "^2.0.0" + +postcss-media-minmax@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" + integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== + dependencies: + postcss "^7.0.2" + +postcss-message-helpers@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" + integrity sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4= + +postcss-nested@^4.1.2: + version "4.2.3" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-4.2.3.tgz#c6f255b0a720549776d220d00c4b70cd244136f6" + integrity sha512-rOv0W1HquRCamWy2kFl3QazJMMe1ku6rCFoAAH+9AcxdbpDeBr6k968MLWuLjvjMcGEip01ak09hKOEgpK9hvw== + dependencies: + postcss "^7.0.32" + postcss-selector-parser "^6.0.2" + +postcss-reporter@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-6.0.1.tgz#7c055120060a97c8837b4e48215661aafb74245f" + integrity sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw== + dependencies: + chalk "^2.4.1" + lodash "^4.17.11" + log-symbols "^2.2.0" + postcss "^7.0.7" + +postcss-sassy-mixins@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-sassy-mixins/-/postcss-sassy-mixins-2.1.0.tgz#368f200946bfdef6a8b12d68c0f6379b9a222f26" + integrity sha1-No8gCUa/3vaosS1owPY3m5oiLyY= + dependencies: + glob "^6.0.4" + postcss "^5.0.14" + postcss-simple-vars "^1.2.0" + +postcss-scss@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.1.1.tgz#ec3a75fa29a55e016b90bf3269026c53c1d2b383" + integrity sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA== + dependencies: + postcss "^7.0.6" + +postcss-selector-parser@^6.0.2: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" + integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + util-deprecate "^1.0.2" + +postcss-simple-vars@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-1.2.0.tgz#2e6689921144b74114e765353275a3c32143f150" + integrity sha1-LmaJkhFEt0EU52U1MnWjwyFD8VA= + dependencies: + postcss "^5.0.13" + +postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + +postcss-value-parser@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" + integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== + +postcss-values-parser@^3.0.5: + version "3.2.1" + resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-3.2.1.tgz#55114607de6631338ba8728d3e9c15785adcc027" + integrity sha512-SQ7/88VE9LhJh9gc27/hqnSU/aZaREVJcRVccXBmajgP2RkjdJzNyH/a9GCVMI5nsRhT0jC5HpUMwfkz81DVVg== + dependencies: + color-name "^1.1.4" + is-url-superb "^3.0.0" + postcss "^7.0.5" + url-regex "^5.0.0" + +postcss@^5.0.13, postcss@^5.0.14: + version "5.2.18" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" + integrity sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg== + dependencies: + chalk "^1.1.3" + js-base64 "^2.1.9" + source-map "^0.5.6" + supports-color "^3.2.3" + +postcss@^6.0.23: + version "6.0.23" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" + integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== + dependencies: + chalk "^2.4.1" + source-map "^0.6.1" + supports-color "^5.4.0" + +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6, postcss@^7.0.7: + version "7.0.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" + integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +pretty-hrtime@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" + integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q= + dependencies: + pify "^2.3.0" + readdirp@~3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" @@ -97,12 +964,131 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" -sass@^1.32.8: - version "1.32.8" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.8.tgz#f16a9abd8dc530add8834e506878a2808c037bdc" - integrity sha512-Sl6mIeGpzjIUZqvKnKETfMf0iDAswD9TNlv13A7aAF3XZlRPMq4VvJWBC2N2DXbp94MQVdNSFG6LfF/iOXrPHQ== +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve@^1.1.7: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== dependencies: - chokidar ">=2.0.0 <4.0.0" + is-core-module "^2.2.0" + path-parse "^1.0.6" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rgb@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" + integrity sha1-vieykej+/+rBvZlylyG/pA/AN7U= + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +supports-color@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= + dependencies: + has-flag "^1.0.0" + +supports-color@^5.3.0, supports-color@^5.4.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +tlds@^1.203.0: + version "1.219.0" + resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.219.0.tgz#7e636062386a1f3c9184356de93d40842ffe91d9" + integrity sha512-o4g9c8kXCmTDwUnK/9HpTT9o/GNH85KCvs+S5SgUw5yILdECvMmTGzK7ngoWMp97P5tfYr8fZeF16YhgV/l90A== to-regex-range@^5.0.1: version "5.0.1" @@ -110,3 +1096,75 @@ to-regex-range@^5.0.1: integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +url-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/url-regex/-/url-regex-5.0.0.tgz#8f5456ab83d898d18b2f91753a702649b873273a" + integrity sha512-O08GjTiAFNsSlrUWfqF1jH0H1W3m35ZyadHrGv5krdnmPPoxP27oDTqux/579PtaroiSGm5yma6KT1mHFH6Y/g== + dependencies: + ip-regex "^4.1.0" + tlds "^1.203.0" + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^15.0.2: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" From 114dcc568f05f2cded870935d9a316b3b9e5afcb Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 14 Apr 2021 09:50:25 -0400 Subject: [PATCH 219/366] Update dependencies --- composer.lock | 24 +++--- vendor-bin/csfixer/composer.lock | 121 +++++++++++++-------------- vendor-bin/daux/composer.lock | 139 +++++++++++++++---------------- vendor-bin/phpunit/composer.lock | 59 +++++++------ vendor-bin/robo/composer.lock | 113 ++++++++++++------------- 5 files changed, 225 insertions(+), 231 deletions(-) diff --git a/composer.lock b/composer.lock index 04f0935f..1a90a287 100644 --- a/composer.lock +++ b/composer.lock @@ -129,16 +129,16 @@ }, { "name": "guzzlehttp/promises", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "60d379c243457e073cff02bc323a2a86cb355631" + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631", - "reference": "60d379c243457e073cff02bc323a2a86cb355631", + "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", "shasum": "" }, "require": { @@ -178,22 +178,22 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.4.0" + "source": "https://github.com/guzzle/promises/tree/1.4.1" }, - "time": "2020-09-30T07:37:28+00:00" + "time": "2021-03-07T09:25:29+00:00" }, { "name": "guzzlehttp/psr7", - "version": "1.7.0", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3" + "reference": "35ea11d335fd638b5882ff1725228b3d35496ab1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/53330f47520498c0ae1f61f7e2c90f55690c06a3", - "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/35ea11d335fd638b5882ff1725228b3d35496ab1", + "reference": "35ea11d335fd638b5882ff1725228b3d35496ab1", "shasum": "" }, "require": { @@ -253,9 +253,9 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.7.0" + "source": "https://github.com/guzzle/psr7/tree/1.8.1" }, - "time": "2020-09-30T07:37:11+00:00" + "time": "2021-03-21T16:25:00+00:00" }, { "name": "hosteurope/password-generator", diff --git a/vendor-bin/csfixer/composer.lock b/vendor-bin/csfixer/composer.lock index 5d5b4de4..17edd012 100644 --- a/vendor-bin/csfixer/composer.lock +++ b/vendor-bin/csfixer/composer.lock @@ -90,16 +90,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.4.5", + "version": "1.4.6", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "f28d44c286812c714741478d968104c5e604a1d4" + "reference": "f27e06cd9675801df441b3656569b328e04aa37c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f28d44c286812c714741478d968104c5e604a1d4", - "reference": "f28d44c286812c714741478d968104c5e604a1d4", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f27e06cd9675801df441b3656569b328e04aa37c", + "reference": "f27e06cd9675801df441b3656569b328e04aa37c", "shasum": "" }, "require": { @@ -107,7 +107,8 @@ "psr/log": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8" + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", "autoload": { @@ -133,7 +134,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/1.4.5" + "source": "https://github.com/composer/xdebug-handler/tree/1.4.6" }, "funding": [ { @@ -149,7 +150,7 @@ "type": "tidelift" } ], - "time": "2020-11-13T08:04:11+00:00" + "time": "2021-03-25T17:01:18+00:00" }, { "name": "doctrine/annotations", @@ -303,16 +304,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.18.2", + "version": "v2.18.5", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "18f8c9d184ba777380794a389fabc179896ba913" + "reference": "e0f6d05c8b157f50029ca6c65c19ed2694f475bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/18f8c9d184ba777380794a389fabc179896ba913", - "reference": "18f8c9d184ba777380794a389fabc179896ba913", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/e0f6d05c8b157f50029ca6c65c19ed2694f475bf", + "reference": "e0f6d05c8b157f50029ca6c65c19ed2694f475bf", "shasum": "" }, "require": { @@ -374,6 +375,7 @@ "tests/Test/IntegrationCaseFactoryInterface.php", "tests/Test/InternalIntegrationCaseFactory.php", "tests/Test/IsIdenticalConstraint.php", + "tests/Test/TokensWithObservedTransformers.php", "tests/TestCase.php" ] }, @@ -394,7 +396,7 @@ "description": "A tool to automatically fix PHP code style", "support": { "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.18.2" + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.18.5" }, "funding": [ { @@ -402,7 +404,7 @@ "type": "github" } ], - "time": "2021-01-26T00:22:21+00:00" + "time": "2021-04-06T18:37:33+00:00" }, { "name": "php-cs-fixer/diff", @@ -461,27 +463,22 @@ }, { "name": "psr/container", - "version": "1.0.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.2.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -494,7 +491,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common Container Interface (PHP FIG PSR-11)", @@ -508,9 +505,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/master" + "source": "https://github.com/php-fig/container/tree/1.1.1" }, - "time": "2017-02-14T16:28:37+00:00" + "time": "2021-03-05T17:36:06+00:00" }, { "name": "psr/event-dispatcher", @@ -614,16 +611,16 @@ }, { "name": "symfony/console", - "version": "v5.2.3", + "version": "v5.2.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "89d4b176d12a2946a1ae4e34906a025b7b6b135a" + "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/89d4b176d12a2946a1ae4e34906a025b7b6b135a", - "reference": "89d4b176d12a2946a1ae4e34906a025b7b6b135a", + "url": "https://api.github.com/repos/symfony/console/zipball/35f039df40a3b335ebf310f244cb242b3a83ac8d", + "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d", "shasum": "" }, "require": { @@ -691,7 +688,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.2.3" + "source": "https://github.com/symfony/console/tree/v5.2.6" }, "funding": [ { @@ -707,7 +704,7 @@ "type": "tidelift" } ], - "time": "2021-01-28T22:06:19+00:00" + "time": "2021-03-28T09:42:18+00:00" }, { "name": "symfony/deprecation-contracts", @@ -778,16 +775,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "4f9760f8074978ad82e2ce854dff79a71fe45367" + "reference": "d08d6ec121a425897951900ab692b612a61d6240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4f9760f8074978ad82e2ce854dff79a71fe45367", - "reference": "4f9760f8074978ad82e2ce854dff79a71fe45367", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d08d6ec121a425897951900ab692b612a61d6240", + "reference": "d08d6ec121a425897951900ab692b612a61d6240", "shasum": "" }, "require": { @@ -843,7 +840,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.2.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v5.2.4" }, "funding": [ { @@ -859,7 +856,7 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:36:42+00:00" + "time": "2021-02-18T17:12:37+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -942,16 +939,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.2.3", + "version": "v5.2.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "262d033b57c73e8b59cd6e68a45c528318b15038" + "reference": "8c86a82f51658188119e62cff0a050a12d09836f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/262d033b57c73e8b59cd6e68a45c528318b15038", - "reference": "262d033b57c73e8b59cd6e68a45c528318b15038", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/8c86a82f51658188119e62cff0a050a12d09836f", + "reference": "8c86a82f51658188119e62cff0a050a12d09836f", "shasum": "" }, "require": { @@ -984,7 +981,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.2.3" + "source": "https://github.com/symfony/filesystem/tree/v5.2.6" }, "funding": [ { @@ -1000,20 +997,20 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:01:46+00:00" + "time": "2021-03-28T14:30:26+00:00" }, { "name": "symfony/finder", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "4adc8d172d602008c204c2e16956f99257248e03" + "reference": "0d639a0943822626290d169965804f79400e6a04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/4adc8d172d602008c204c2e16956f99257248e03", - "reference": "4adc8d172d602008c204c2e16956f99257248e03", + "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", + "reference": "0d639a0943822626290d169965804f79400e6a04", "shasum": "" }, "require": { @@ -1045,7 +1042,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.2.3" + "source": "https://github.com/symfony/finder/tree/v5.2.4" }, "funding": [ { @@ -1061,11 +1058,11 @@ "type": "tidelift" } ], - "time": "2021-01-28T22:06:19+00:00" + "time": "2021-02-15T18:55:04+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", @@ -1114,7 +1111,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v5.2.3" + "source": "https://github.com/symfony/options-resolver/tree/v5.2.4" }, "funding": [ { @@ -1764,7 +1761,7 @@ }, { "name": "symfony/process", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -1806,7 +1803,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.2.3" + "source": "https://github.com/symfony/process/tree/v5.2.4" }, "funding": [ { @@ -1905,7 +1902,7 @@ }, { "name": "symfony/stopwatch", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -1947,7 +1944,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.2.3" + "source": "https://github.com/symfony/stopwatch/tree/v5.2.4" }, "funding": [ { @@ -1967,16 +1964,16 @@ }, { "name": "symfony/string", - "version": "v5.2.3", + "version": "v5.2.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "c95468897f408dd0aca2ff582074423dd0455122" + "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/c95468897f408dd0aca2ff582074423dd0455122", - "reference": "c95468897f408dd0aca2ff582074423dd0455122", + "url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", + "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", "shasum": "" }, "require": { @@ -2030,7 +2027,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.2.3" + "source": "https://github.com/symfony/string/tree/v5.2.6" }, "funding": [ { @@ -2046,7 +2043,7 @@ "type": "tidelift" } ], - "time": "2021-01-25T15:14:59+00:00" + "time": "2021-03-17T17:12:15+00:00" } ], "aliases": [], diff --git a/vendor-bin/daux/composer.lock b/vendor-bin/daux/composer.lock index e3712851..6debf534 100644 --- a/vendor-bin/daux/composer.lock +++ b/vendor-bin/daux/composer.lock @@ -83,22 +83,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.2.0", + "version": "7.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79" + "reference": "7008573787b430c1c1f650e3722d9bba59967628" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/0aa74dfb41ae110835923ef10a9d803a22d50e79", - "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7008573787b430c1c1f650e3722d9bba59967628", + "reference": "7008573787b430c1c1f650e3722d9bba59967628", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/promises": "^1.4", - "guzzlehttp/psr7": "^1.7", + "guzzlehttp/psr7": "^1.7 || ^2.0", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0" }, @@ -106,6 +106,7 @@ "psr/http-client-implementation": "1.0" }, "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", "ext-curl": "*", "php-http/client-integration-tests": "^3.0", "phpunit/phpunit": "^8.5.5 || ^9.3.5", @@ -119,7 +120,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "7.1-dev" + "dev-master": "7.3-dev" } }, "autoload": { @@ -161,7 +162,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.2.0" + "source": "https://github.com/guzzle/guzzle/tree/7.3.0" }, "funding": [ { @@ -181,20 +182,20 @@ "type": "github" } ], - "time": "2020-10-10T11:47:56+00:00" + "time": "2021-03-23T11:33:13+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "60d379c243457e073cff02bc323a2a86cb355631" + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631", - "reference": "60d379c243457e073cff02bc323a2a86cb355631", + "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", "shasum": "" }, "require": { @@ -234,22 +235,22 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.4.0" + "source": "https://github.com/guzzle/promises/tree/1.4.1" }, - "time": "2020-09-30T07:37:28+00:00" + "time": "2021-03-07T09:25:29+00:00" }, { "name": "guzzlehttp/psr7", - "version": "1.7.0", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3" + "reference": "35ea11d335fd638b5882ff1725228b3d35496ab1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/53330f47520498c0ae1f61f7e2c90f55690c06a3", - "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/35ea11d335fd638b5882ff1725228b3d35496ab1", + "reference": "35ea11d335fd638b5882ff1725228b3d35496ab1", "shasum": "" }, "require": { @@ -309,22 +310,22 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.7.0" + "source": "https://github.com/guzzle/psr7/tree/1.8.1" }, - "time": "2020-09-30T07:37:11+00:00" + "time": "2021-03-21T16:25:00+00:00" }, { "name": "league/commonmark", - "version": "1.5.7", + "version": "1.5.8", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "11df9b36fd4f1d2b727a73bf14931d81373b9a54" + "reference": "08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/11df9b36fd4f1d2b727a73bf14931d81373b9a54", - "reference": "11df9b36fd4f1d2b727a73bf14931d81373b9a54", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf", + "reference": "08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf", "shasum": "" }, "require": { @@ -412,7 +413,7 @@ "type": "tidelift" } ], - "time": "2020-10-31T13:49:32+00:00" + "time": "2021-03-28T18:51:39+00:00" }, { "name": "league/plates", @@ -480,27 +481,22 @@ }, { "name": "psr/container", - "version": "1.0.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.2.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -513,7 +509,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common Container Interface (PHP FIG PSR-11)", @@ -527,9 +523,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/master" + "source": "https://github.com/php-fig/container/tree/1.1.1" }, - "time": "2017-02-14T16:28:37+00:00" + "time": "2021-03-05T17:36:06+00:00" }, { "name": "psr/http-client", @@ -758,16 +754,16 @@ }, { "name": "symfony/console", - "version": "v5.2.3", + "version": "v5.2.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "89d4b176d12a2946a1ae4e34906a025b7b6b135a" + "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/89d4b176d12a2946a1ae4e34906a025b7b6b135a", - "reference": "89d4b176d12a2946a1ae4e34906a025b7b6b135a", + "url": "https://api.github.com/repos/symfony/console/zipball/35f039df40a3b335ebf310f244cb242b3a83ac8d", + "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d", "shasum": "" }, "require": { @@ -835,7 +831,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.2.3" + "source": "https://github.com/symfony/console/tree/v5.2.6" }, "funding": [ { @@ -851,7 +847,7 @@ "type": "tidelift" } ], - "time": "2021-01-28T22:06:19+00:00" + "time": "2021-03-28T09:42:18+00:00" }, { "name": "symfony/deprecation-contracts", @@ -922,16 +918,16 @@ }, { "name": "symfony/http-foundation", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "20c554c0f03f7cde5ce230ed248470cccbc34c36" + "reference": "54499baea7f7418bce7b5ec92770fd0799e8e9bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/20c554c0f03f7cde5ce230ed248470cccbc34c36", - "reference": "20c554c0f03f7cde5ce230ed248470cccbc34c36", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/54499baea7f7418bce7b5ec92770fd0799e8e9bf", + "reference": "54499baea7f7418bce7b5ec92770fd0799e8e9bf", "shasum": "" }, "require": { @@ -975,7 +971,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.2.3" + "source": "https://github.com/symfony/http-foundation/tree/v5.2.4" }, "funding": [ { @@ -991,20 +987,20 @@ "type": "tidelift" } ], - "time": "2021-02-03T04:42:09+00:00" + "time": "2021-02-25T17:16:57+00:00" }, { "name": "symfony/mime", - "version": "v5.2.3", + "version": "v5.2.6", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "7dee6a43493f39b51ff6c5bb2bd576fe40a76c86" + "reference": "1b2092244374cbe48ae733673f2ca0818b37197b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/7dee6a43493f39b51ff6c5bb2bd576fe40a76c86", - "reference": "7dee6a43493f39b51ff6c5bb2bd576fe40a76c86", + "url": "https://api.github.com/repos/symfony/mime/zipball/1b2092244374cbe48ae733673f2ca0818b37197b", + "reference": "1b2092244374cbe48ae733673f2ca0818b37197b", "shasum": "" }, "require": { @@ -1015,12 +1011,13 @@ "symfony/polyfill-php80": "^1.15" }, "conflict": { + "egulias/email-validator": "~3.0.0", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", "symfony/mailer": "<4.4" }, "require-dev": { - "egulias/email-validator": "^2.1.10", + "egulias/email-validator": "^2.1.10|^3.1", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "symfony/dependency-injection": "^4.4|^5.0", "symfony/property-access": "^4.4|^5.1", @@ -1057,7 +1054,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.2.3" + "source": "https://github.com/symfony/mime/tree/v5.2.6" }, "funding": [ { @@ -1073,7 +1070,7 @@ "type": "tidelift" } ], - "time": "2021-02-02T06:10:15+00:00" + "time": "2021-03-12T13:18:39+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1813,7 +1810,7 @@ }, { "name": "symfony/process", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -1855,7 +1852,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.2.3" + "source": "https://github.com/symfony/process/tree/v5.2.4" }, "funding": [ { @@ -1954,16 +1951,16 @@ }, { "name": "symfony/string", - "version": "v5.2.3", + "version": "v5.2.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "c95468897f408dd0aca2ff582074423dd0455122" + "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/c95468897f408dd0aca2ff582074423dd0455122", - "reference": "c95468897f408dd0aca2ff582074423dd0455122", + "url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", + "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", "shasum": "" }, "require": { @@ -2017,7 +2014,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.2.3" + "source": "https://github.com/symfony/string/tree/v5.2.6" }, "funding": [ { @@ -2033,20 +2030,20 @@ "type": "tidelift" } ], - "time": "2021-01-25T15:14:59+00:00" + "time": "2021-03-17T17:12:15+00:00" }, { "name": "symfony/yaml", - "version": "v5.2.3", + "version": "v5.2.5", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "338cddc6d74929f6adf19ca5682ac4b8e109cdb0" + "reference": "298a08ddda623485208506fcee08817807a251dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/338cddc6d74929f6adf19ca5682ac4b8e109cdb0", - "reference": "338cddc6d74929f6adf19ca5682ac4b8e109cdb0", + "url": "https://api.github.com/repos/symfony/yaml/zipball/298a08ddda623485208506fcee08817807a251dd", + "reference": "298a08ddda623485208506fcee08817807a251dd", "shasum": "" }, "require": { @@ -2092,7 +2089,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.2.3" + "source": "https://github.com/symfony/yaml/tree/v5.2.5" }, "funding": [ { @@ -2108,7 +2105,7 @@ "type": "tidelift" } ], - "time": "2021-02-03T04:42:09+00:00" + "time": "2021-03-06T07:59:01+00:00" }, { "name": "webuni/front-matter", diff --git a/vendor-bin/phpunit/composer.lock b/vendor-bin/phpunit/composer.lock index d37f37db..d440548c 100644 --- a/vendor-bin/phpunit/composer.lock +++ b/vendor-bin/phpunit/composer.lock @@ -771,16 +771,16 @@ }, { "name": "phpspec/prophecy", - "version": "1.12.2", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "245710e971a030f42e08f4912863805570f23d39" + "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/245710e971a030f42e08f4912863805570f23d39", - "reference": "245710e971a030f42e08f4912863805570f23d39", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", + "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", "shasum": "" }, "require": { @@ -832,22 +832,22 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/1.12.2" + "source": "https://github.com/phpspec/prophecy/tree/1.13.0" }, - "time": "2020-12-19T10:15:11+00:00" + "time": "2021-03-17T13:42:18+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.5", + "version": "9.2.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1" + "reference": "f6293e1b30a2354e8428e004689671b83871edde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f3e026641cc91909d421802dd3ac7827ebfd97e1", - "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde", + "reference": "f6293e1b30a2354e8428e004689671b83871edde", "shasum": "" }, "require": { @@ -903,7 +903,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.5" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6" }, "funding": [ { @@ -911,7 +911,7 @@ "type": "github" } ], - "time": "2020-11-28T06:44:49+00:00" + "time": "2021-03-28T07:26:59+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1156,16 +1156,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.2", + "version": "9.5.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f661659747f2f87f9e72095bb207bceb0f151cb4" + "reference": "c73c6737305e779771147af66c96ca6a7ed8a741" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f661659747f2f87f9e72095bb207bceb0f151cb4", - "reference": "f661659747f2f87f9e72095bb207bceb0f151cb4", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c73c6737305e779771147af66c96ca6a7ed8a741", + "reference": "c73c6737305e779771147af66c96ca6a7ed8a741", "shasum": "" }, "require": { @@ -1243,7 +1243,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.2" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.4" }, "funding": [ { @@ -1255,7 +1255,7 @@ "type": "github" } ], - "time": "2021-02-02T14:45:58+00:00" + "time": "2021-03-23T07:16:29+00:00" }, { "name": "sebastian/cli-parser", @@ -2352,30 +2352,35 @@ }, { "name": "webmozart/assert", - "version": "1.9.1", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0 || ^8.0", + "php": "^7.2 || ^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<3.9.1" + "vimeo/psalm": "<4.6.1 || 4.6.2" }, "require-dev": { - "phpunit/phpunit": "^4.8.36 || ^7.5.13" + "phpunit/phpunit": "^8.5.13" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, "autoload": { "psr-4": { "Webmozart\\Assert\\": "src/" @@ -2399,9 +2404,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.9.1" + "source": "https://github.com/webmozarts/assert/tree/1.10.0" }, - "time": "2020-07-08T17:02:28+00:00" + "time": "2021-03-09T10:59:23+00:00" }, { "name": "webmozart/glob", diff --git a/vendor-bin/robo/composer.lock b/vendor-bin/robo/composer.lock index 2fc89c6d..24c4530d 100644 --- a/vendor-bin/robo/composer.lock +++ b/vendor-bin/robo/composer.lock @@ -502,21 +502,21 @@ }, { "name": "league/container", - "version": "3.3.4", + "version": "3.3.5", "source": { "type": "git", "url": "https://github.com/thephpleague/container.git", - "reference": "40aed0f11d16bc23f9d04a27acc3549cd1bb42ab" + "reference": "048ab87810f508dbedbcb7ae941b606eb8ee353b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/container/zipball/40aed0f11d16bc23f9d04a27acc3549cd1bb42ab", - "reference": "40aed0f11d16bc23f9d04a27acc3549cd1bb42ab", + "url": "https://api.github.com/repos/thephpleague/container/zipball/048ab87810f508dbedbcb7ae941b606eb8ee353b", + "reference": "048ab87810f508dbedbcb7ae941b606eb8ee353b", "shasum": "" }, "require": { "php": "^7.0 || ^8.0", - "psr/container": "^1.0" + "psr/container": "^1.0.0 || ^2.0.0" }, "provide": { "psr/container-implementation": "^1.0" @@ -569,7 +569,7 @@ ], "support": { "issues": "https://github.com/thephpleague/container/issues", - "source": "https://github.com/thephpleague/container/tree/3.3.4" + "source": "https://github.com/thephpleague/container/tree/3.3.5" }, "funding": [ { @@ -577,7 +577,7 @@ "type": "github" } ], - "time": "2021-02-22T10:35:05+00:00" + "time": "2021-03-16T09:42:56+00:00" }, { "name": "pear/archive_tar", @@ -760,23 +760,23 @@ }, { "name": "pear/pear_exception", - "version": "v1.0.1", + "version": "v1.0.2", "source": { "type": "git", "url": "https://github.com/pear/PEAR_Exception.git", - "reference": "dbb42a5a0e45f3adcf99babfb2a1ba77b8ac36a7" + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/dbb42a5a0e45f3adcf99babfb2a1ba77b8ac36a7", - "reference": "dbb42a5a0e45f3adcf99babfb2a1ba77b8ac36a7", + "url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", "shasum": "" }, "require": { - "php": ">=4.4.0" + "php": ">=5.2.0" }, "require-dev": { - "phpunit/phpunit": "*" + "phpunit/phpunit": "<9" }, "type": "class", "extra": { @@ -815,31 +815,26 @@ "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", "source": "https://github.com/pear/PEAR_Exception" }, - "time": "2019-12-10T10:24:42+00:00" + "time": "2021-03-21T15:43:46+00:00" }, { "name": "psr/container", - "version": "1.0.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.2.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -852,7 +847,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common Container Interface (PHP FIG PSR-11)", @@ -866,9 +861,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/master" + "source": "https://github.com/php-fig/container/tree/1.1.1" }, - "time": "2017-02-14T16:28:37+00:00" + "time": "2021-03-05T17:36:06+00:00" }, { "name": "psr/event-dispatcher", @@ -1130,16 +1125,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "4f9760f8074978ad82e2ce854dff79a71fe45367" + "reference": "d08d6ec121a425897951900ab692b612a61d6240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4f9760f8074978ad82e2ce854dff79a71fe45367", - "reference": "4f9760f8074978ad82e2ce854dff79a71fe45367", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d08d6ec121a425897951900ab692b612a61d6240", + "reference": "d08d6ec121a425897951900ab692b612a61d6240", "shasum": "" }, "require": { @@ -1195,7 +1190,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.2.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v5.2.4" }, "funding": [ { @@ -1211,7 +1206,7 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:36:42+00:00" + "time": "2021-02-18T17:12:37+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -1294,16 +1289,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.2.3", + "version": "v5.2.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "262d033b57c73e8b59cd6e68a45c528318b15038" + "reference": "8c86a82f51658188119e62cff0a050a12d09836f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/262d033b57c73e8b59cd6e68a45c528318b15038", - "reference": "262d033b57c73e8b59cd6e68a45c528318b15038", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/8c86a82f51658188119e62cff0a050a12d09836f", + "reference": "8c86a82f51658188119e62cff0a050a12d09836f", "shasum": "" }, "require": { @@ -1336,7 +1331,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.2.3" + "source": "https://github.com/symfony/filesystem/tree/v5.2.6" }, "funding": [ { @@ -1352,20 +1347,20 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:01:46+00:00" + "time": "2021-03-28T14:30:26+00:00" }, { "name": "symfony/finder", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "4adc8d172d602008c204c2e16956f99257248e03" + "reference": "0d639a0943822626290d169965804f79400e6a04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/4adc8d172d602008c204c2e16956f99257248e03", - "reference": "4adc8d172d602008c204c2e16956f99257248e03", + "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", + "reference": "0d639a0943822626290d169965804f79400e6a04", "shasum": "" }, "require": { @@ -1397,7 +1392,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.2.3" + "source": "https://github.com/symfony/finder/tree/v5.2.4" }, "funding": [ { @@ -1413,7 +1408,7 @@ "type": "tidelift" } ], - "time": "2021-01-28T22:06:19+00:00" + "time": "2021-02-15T18:55:04+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1903,7 +1898,7 @@ }, { "name": "symfony/process", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -1945,7 +1940,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.2.3" + "source": "https://github.com/symfony/process/tree/v5.2.4" }, "funding": [ { @@ -2044,16 +2039,16 @@ }, { "name": "symfony/string", - "version": "v5.2.3", + "version": "v5.2.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "c95468897f408dd0aca2ff582074423dd0455122" + "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/c95468897f408dd0aca2ff582074423dd0455122", - "reference": "c95468897f408dd0aca2ff582074423dd0455122", + "url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", + "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", "shasum": "" }, "require": { @@ -2107,7 +2102,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.2.3" + "source": "https://github.com/symfony/string/tree/v5.2.6" }, "funding": [ { @@ -2123,20 +2118,20 @@ "type": "tidelift" } ], - "time": "2021-01-25T15:14:59+00:00" + "time": "2021-03-17T17:12:15+00:00" }, { "name": "symfony/yaml", - "version": "v5.2.3", + "version": "v5.2.5", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "338cddc6d74929f6adf19ca5682ac4b8e109cdb0" + "reference": "298a08ddda623485208506fcee08817807a251dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/338cddc6d74929f6adf19ca5682ac4b8e109cdb0", - "reference": "338cddc6d74929f6adf19ca5682ac4b8e109cdb0", + "url": "https://api.github.com/repos/symfony/yaml/zipball/298a08ddda623485208506fcee08817807a251dd", + "reference": "298a08ddda623485208506fcee08817807a251dd", "shasum": "" }, "require": { @@ -2182,7 +2177,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.2.3" + "source": "https://github.com/symfony/yaml/tree/v5.2.5" }, "funding": [ { @@ -2198,7 +2193,7 @@ "type": "tidelift" } ], - "time": "2021-02-03T04:42:09+00:00" + "time": "2021-03-06T07:59:01+00:00" } ], "aliases": [], From 7ba4cabdde95f36025676f68bc3022c9cf216d6d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 15 May 2021 22:26:06 -0400 Subject: [PATCH 220/366] Prototype Arch PKGBUILD and supporting files The package will be created, but the result itself has yet to be tested. --- .gitignore | 3 ++ dist/arch/PKGBUILD | 56 +++++++++++++++++++++++++++++++++++ dist/arch/arsse-fetch.service | 32 ++++++++++++++++++++ dist/arch/arsse-web.service | 33 +++++++++++++++++++++ dist/arch/arsse.service | 12 ++++++++ dist/arch/arsse.sh | 10 +++++++ dist/arch/config.php | 8 +++++ dist/arch/sysuser.conf | 1 + dist/arch/uwsgi.ini | 15 ++++++++++ 9 files changed, 170 insertions(+) create mode 100644 dist/arch/PKGBUILD create mode 100644 dist/arch/arsse-fetch.service create mode 100644 dist/arch/arsse-web.service create mode 100644 dist/arch/arsse.service create mode 100644 dist/arch/arsse.sh create mode 100644 dist/arch/config.php create mode 100644 dist/arch/sysuser.conf create mode 100644 dist/arch/uwsgi.ini diff --git a/.gitignore b/.gitignore index 16e9c935..10bac85b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ /documentation/ /manual/ /tests/coverage/ +/dist/arch/src +/dist/arch/pkg /arsse.db* /config.php /.php_cs.cache @@ -36,6 +38,7 @@ $RECYCLE.BIN/ *.zip *.7z *.tar.gz +*.tar.xz *.tgz *.deb *.rpm diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD new file mode 100644 index 00000000..056ee6af --- /dev/null +++ b/dist/arch/PKGBUILD @@ -0,0 +1,56 @@ +pkgname="arsse" +pkgver="0.9.1" +pkgrel=1 +epoch= +pkgdesc="RSS/Atom newsfeed synchronization server" +arch=("any") +url="https://thearsse.com/" +license=("MIT") +groups=() +depends=("php>=7.1" "php-intl" "php-sqlite" "uwsgi" "uwsgi-plugin-php") +makedepends=("composer") +checkdepends=() +optdepends=("php-pgsql: PostgreSQL database support") +provides=() +conflicts=() +replaces=() +backup=("etc/webapps/arsse/config.php" "etc/webapps/arsse/uwsgi.ini") +options=() +install= +changelog= +source=("https://code.mensbeam.com/attachments/229880aa-3fcc-499f-b747-6932a661dc0e" + "arsse.service" + "arsse-web.service" + "arsse-fetch.service" + "sysuser.conf" + "config.php" + "uwsgi.ini" + "arsse.sh") +noextract=() + +package() { + cd "$pkgdir" + mkdir -p "usr/bin" "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "etc/webapps/arsse" + cd "$srcdir/arsse" + cp ../arsse.sh "$pkgdir/usr/bin/arsse" + cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" + cp -r manual/* "$pkgdir/usr/share/doc/arsse" + cp LICENSE AUTHORS "$pkgdir/usr/share/licenses/arsse" + cp ../*.service "$pkgdir/usr/lib/systemd/system" + cp ../sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" + cp ../config.php config.defaults.php ../uwsgi.ini "$pkgdir/etc/webapps/arsse" + cd "$pkgdir" + chmod -R a=rX * + chmod a=rx usr/bin/arsse + chmod u=r etc/webapps/arsse/* + ln -sT "/etc/webapps/arsse/config.php" "usr/share/webapps/arsse/config.php" +} + +md5sums=('c7c9526f02fe34bf6f8399eff95c819d' + '53f150081dc9097790166ac22575fb1d' + '9ed9119aff93e0099c15cd12a3f71655' + '71a5975aed6b2da581262441f14bc929' + 'b6ef9ab7e9062df1d5ba060066b6d734' + '33e7a5b290ef20339952f1d904b33f8f' + 'ff8fc77353d8e06f5c74ad577880a19d' + '4fb46ec290e497279c3dd7c8c528abf6') diff --git a/dist/arch/arsse-fetch.service b/dist/arch/arsse-fetch.service new file mode 100644 index 00000000..81a31fc7 --- /dev/null +++ b/dist/arch/arsse-fetch.service @@ -0,0 +1,32 @@ +[Unit] +Description=The Arsse newsfeed fetching service +Documentation=https://thearsse.com/manual/ + +[Service] +User=arsse +Group=arsse +Type=simple +WorkingDirectory=/usr/share/webapps/arsse +ExecStart=/usr/bin/env php /usr/share/webapps/arsse/arsse.php daemon + +ProtectProc=invisible +NoNewPrivileges=true +ProtectSystem=full +ProtectHome=true +StateDirectory=arsse +ConfigurationDirectory=webapps/arsse +ReadOnlyPaths=/ +ReadWriePaths=/usr/lib/arsse +NoExecPaths=/ +ExecPaths=/usr/bin/php /usr/bin/php7 +PrivateTmp=true +PrivateDevices=true +RestrictSUIDSGID=true +StandardOutput=journal +StandardError=journal +SyslogIdentifier=arsse +Restart=on-failure +RestartPreventStatus= + +[Install] +WantedBy=multi-user.target diff --git a/dist/arch/arsse-web.service b/dist/arch/arsse-web.service new file mode 100644 index 00000000..8e280d17 --- /dev/null +++ b/dist/arch/arsse-web.service @@ -0,0 +1,33 @@ +[Unit] +Description=The Arsse newsfeed client service +Documentation=https://thearsse.com/manual/ + +[Service] +User=arsse +Group=arsse +Type=simple +WorkingDirectory=/usr/share/webapps/arsse +ExecStart=/usr/bin/uwsgi /etc/webapps/arsse/uwsgi.ini + +ProtectProc=invisible +NoNewPrivileges=true +ProtectSystem=full +ProtectHome=true +RuntimeDirectory=arsse +StateDirectory=arsse +ConfigurationDirectory=webapps/arsse +ReadOnlyPaths=/ +ReadWriePaths=/usr/lib/arsse +NoExecPaths=/ +ExecPaths=/usr/bin/uwsgi +PrivateTmp=true +PrivateDevices=true +RestrictSUIDSGID=true +StandardOutput=journal +StandardError=journal +SyslogIdentifier=arsse +Restart=on-failure +RestartPreventStatus= + +[Install] +WantedBy=multi-user.target diff --git a/dist/arch/arsse.service b/dist/arch/arsse.service new file mode 100644 index 00000000..f04d5846 --- /dev/null +++ b/dist/arch/arsse.service @@ -0,0 +1,12 @@ +[Unit] +Description=The Arsse newsfeed management service +Documentation=https://thearsse.com/manual/ +Requires=arsse-fetch.service +BindsTo=arsse-web.service + +[Service] +Type=oneshot +RemainAfterExit=true + +[Install] +WantedBy=multi-user.target diff --git a/dist/arch/arsse.sh b/dist/arch/arsse.sh new file mode 100644 index 00000000..adc286eb --- /dev/null +++ b/dist/arch/arsse.sh @@ -0,0 +1,10 @@ +#! /usr/bin/bash + +if [ `id -u` -eq 0 ]; then + setpriv --clear-groups --inh-caps -all --egid=arsse --euid=arsse php /usr/share/webapps/arsse/arsse.php $@ +elif [ `id -un` == "arsse" ]; then + php /usr/share/webapps/arsse/arsse.php $@ +else + echo "Not authorized." >&2 + exit 1 +fi diff --git a/dist/arch/config.php b/dist/arch/config.php new file mode 100644 index 00000000..1df16355 --- /dev/null +++ b/dist/arch/config.php @@ -0,0 +1,8 @@ + "/usr/lib/arsse/arsse.db", +]; \ No newline at end of file diff --git a/dist/arch/sysuser.conf b/dist/arch/sysuser.conf new file mode 100644 index 00000000..9f936e4c --- /dev/null +++ b/dist/arch/sysuser.conf @@ -0,0 +1 @@ +u arsse - "The Arsse" /usr/lib/arsse - diff --git a/dist/arch/uwsgi.ini b/dist/arch/uwsgi.ini new file mode 100644 index 00000000..9766b4ee --- /dev/null +++ b/dist/arch/uwsgi.ini @@ -0,0 +1,15 @@ +[uwsgi] + +strict=true +uwsgi-socket=/run/arsse/uwsgi.socket +master=true +processes=4 +workers=2 +vacuum=true +plugin=php +php-sapi-name=apache +php-set=extension=curl +php-set=extension=iconv +php-set=extension=intl +php-set=extension=sqlite3 +php-app=/usr/share/webapps/arsse/arsse.php From edb146b826600c0e90a9b0b92caac6afc061e336 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 16 May 2021 15:59:52 -0400 Subject: [PATCH 221/366] Use PHP-FPM instead of uWSGI --- dist/arch/PKGBUILD | 27 +++++++++++++-------------- dist/arch/arsse-fetch.service | 1 + dist/arch/arsse-web.service | 33 --------------------------------- dist/arch/arsse.service | 3 ++- dist/arch/php-fpm.conf | 16 ++++++++++++++++ dist/arch/uwsgi.ini | 15 --------------- 6 files changed, 32 insertions(+), 63 deletions(-) delete mode 100644 dist/arch/arsse-web.service create mode 100644 dist/arch/php-fpm.conf delete mode 100644 dist/arch/uwsgi.ini diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index 056ee6af..66a5ff16 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -7,30 +7,30 @@ arch=("any") url="https://thearsse.com/" license=("MIT") groups=() -depends=("php>=7.1" "php-intl" "php-sqlite" "uwsgi" "uwsgi-plugin-php") -makedepends=("composer") +depends=() +makedepends=() checkdepends=() optdepends=("php-pgsql: PostgreSQL database support") provides=() conflicts=() replaces=() -backup=("etc/webapps/arsse/config.php" "etc/webapps/arsse/uwsgi.ini") +backup=("etc/webapps/arsse/config.php" "etc/php/php-fpm.d/arsse.conf") options=() install= changelog= -source=("https://code.mensbeam.com/attachments/229880aa-3fcc-499f-b747-6932a661dc0e" +source=("https://thearsse.com/releases/0.9.0.tar.gz" "arsse.service" - "arsse-web.service" "arsse-fetch.service" "sysuser.conf" "config.php" - "uwsgi.ini" + "php-fpm.conf" "arsse.sh") noextract=() package() { + depends=("php" "php-intl" "php-sqlite" "php-fpm") cd "$pkgdir" - mkdir -p "usr/bin" "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "etc/webapps/arsse" + mkdir -p "usr/bin" "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "etc/webapps/arsse" "etc/php/php-fpm.d/" cd "$srcdir/arsse" cp ../arsse.sh "$pkgdir/usr/bin/arsse" cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" @@ -38,19 +38,18 @@ package() { cp LICENSE AUTHORS "$pkgdir/usr/share/licenses/arsse" cp ../*.service "$pkgdir/usr/lib/systemd/system" cp ../sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" - cp ../config.php config.defaults.php ../uwsgi.ini "$pkgdir/etc/webapps/arsse" + cp ../config.php config.defaults.php "$pkgdir/etc/webapps/arsse" + cp ../php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" cd "$pkgdir" chmod -R a=rX * chmod a=rx usr/bin/arsse chmod u=r etc/webapps/arsse/* ln -sT "/etc/webapps/arsse/config.php" "usr/share/webapps/arsse/config.php" } - -md5sums=('c7c9526f02fe34bf6f8399eff95c819d' - '53f150081dc9097790166ac22575fb1d' - '9ed9119aff93e0099c15cd12a3f71655' - '71a5975aed6b2da581262441f14bc929' +md5sums=('93327083c316daf879c70921189ed7b6' + '91871736d9594b2c92d1fa6b6e4f2803' + '0ca05e2965247d4651a986aad81d80e1' 'b6ef9ab7e9062df1d5ba060066b6d734' '33e7a5b290ef20339952f1d904b33f8f' - 'ff8fc77353d8e06f5c74ad577880a19d' + '943d35272b0aa7af2bf3818a0c9bd5fc' '4fb46ec290e497279c3dd7c8c528abf6') diff --git a/dist/arch/arsse-fetch.service b/dist/arch/arsse-fetch.service index 81a31fc7..be2d7105 100644 --- a/dist/arch/arsse-fetch.service +++ b/dist/arch/arsse-fetch.service @@ -1,6 +1,7 @@ [Unit] Description=The Arsse newsfeed fetching service Documentation=https://thearsse.com/manual/ +PartOf=arsse.service [Service] User=arsse diff --git a/dist/arch/arsse-web.service b/dist/arch/arsse-web.service deleted file mode 100644 index 8e280d17..00000000 --- a/dist/arch/arsse-web.service +++ /dev/null @@ -1,33 +0,0 @@ -[Unit] -Description=The Arsse newsfeed client service -Documentation=https://thearsse.com/manual/ - -[Service] -User=arsse -Group=arsse -Type=simple -WorkingDirectory=/usr/share/webapps/arsse -ExecStart=/usr/bin/uwsgi /etc/webapps/arsse/uwsgi.ini - -ProtectProc=invisible -NoNewPrivileges=true -ProtectSystem=full -ProtectHome=true -RuntimeDirectory=arsse -StateDirectory=arsse -ConfigurationDirectory=webapps/arsse -ReadOnlyPaths=/ -ReadWriePaths=/usr/lib/arsse -NoExecPaths=/ -ExecPaths=/usr/bin/uwsgi -PrivateTmp=true -PrivateDevices=true -RestrictSUIDSGID=true -StandardOutput=journal -StandardError=journal -SyslogIdentifier=arsse -Restart=on-failure -RestartPreventStatus= - -[Install] -WantedBy=multi-user.target diff --git a/dist/arch/arsse.service b/dist/arch/arsse.service index f04d5846..62ee4358 100644 --- a/dist/arch/arsse.service +++ b/dist/arch/arsse.service @@ -2,7 +2,8 @@ Description=The Arsse newsfeed management service Documentation=https://thearsse.com/manual/ Requires=arsse-fetch.service -BindsTo=arsse-web.service +BindsTo=php-fpm.service +After=php-fpm.service [Service] Type=oneshot diff --git a/dist/arch/php-fpm.conf b/dist/arch/php-fpm.conf new file mode 100644 index 00000000..ca7f3af4 --- /dev/null +++ b/dist/arch/php-fpm.conf @@ -0,0 +1,16 @@ +[arsse] +user = arsse +group = arsse +listen = /run/php-fpm/arsse.sock +listen.owner = arsse +listen.group = http +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 + +php_value[extension] = intl +php_value[extension] = dom +php_value[extension] = iconv +php_value[extension] = sqlite3 diff --git a/dist/arch/uwsgi.ini b/dist/arch/uwsgi.ini deleted file mode 100644 index 9766b4ee..00000000 --- a/dist/arch/uwsgi.ini +++ /dev/null @@ -1,15 +0,0 @@ -[uwsgi] - -strict=true -uwsgi-socket=/run/arsse/uwsgi.socket -master=true -processes=4 -workers=2 -vacuum=true -plugin=php -php-sapi-name=apache -php-set=extension=curl -php-set=extension=iconv -php-set=extension=intl -php-set=extension=sqlite3 -php-app=/usr/share/webapps/arsse/arsse.php From febc7c7ca4707816030fe5a9e7952fa34d57f5fa Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 16 May 2021 18:44:42 -0400 Subject: [PATCH 222/366] Add configuration for Nginx --- dist/arch/arsse-fetch.service | 2 +- dist/arch/nginx/arsse-fcgi.conf | 12 ++++++++ dist/arch/nginx/arsse-loc.conf | 49 +++++++++++++++++++++++++++++++++ dist/arch/nginx/arsse.conf | 17 ++++++++++++ dist/arch/nginx/sample.conf | 13 +++++++++ 5 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 dist/arch/nginx/arsse-fcgi.conf create mode 100644 dist/arch/nginx/arsse-loc.conf create mode 100644 dist/arch/nginx/arsse.conf create mode 100644 dist/arch/nginx/sample.conf diff --git a/dist/arch/arsse-fetch.service b/dist/arch/arsse-fetch.service index be2d7105..7ee6eb7e 100644 --- a/dist/arch/arsse-fetch.service +++ b/dist/arch/arsse-fetch.service @@ -8,7 +8,7 @@ User=arsse Group=arsse Type=simple WorkingDirectory=/usr/share/webapps/arsse -ExecStart=/usr/bin/env php /usr/share/webapps/arsse/arsse.php daemon +ExecStart=/usr/bin/arsse daemon ProtectProc=invisible NoNewPrivileges=true diff --git a/dist/arch/nginx/arsse-fcgi.conf b/dist/arch/nginx/arsse-fcgi.conf new file mode 100644 index 00000000..eb83097f --- /dev/null +++ b/dist/arch/nginx/arsse-fcgi.conf @@ -0,0 +1,12 @@ +fastcgi_pass_header Authorization; # required if the Arsse is to perform its own HTTP authentication +fastcgi_pass_request_body on; +fastcgi_pass_request_headers on; +fastcgi_intercept_errors off; +fastcgi_buffering off; +fastcgi_param REQUEST_METHOD $request_method; +fastcgi_param CONTENT_TYPE $content_type; +fastcgi_param CONTENT_LENGTH $content_length; +fastcgi_param REQUEST_URI $uri; +fastcgi_param QUERY_STRING $query_string; +fastcgi_param HTTPS $https if_not_empty; +fastcgi_param REMOTE_USER $remote_user; diff --git a/dist/arch/nginx/arsse-loc.conf b/dist/arch/nginx/arsse-loc.conf new file mode 100644 index 00000000..d7e3ec78 --- /dev/null +++ b/dist/arch/nginx/arsse-loc.conf @@ -0,0 +1,49 @@ +# Any provided static files +location / { + try_files $uri $uri/ =404; +} + +# Nextcloud News protocol +location /index.php/apps/news/api { + try_files $uri @arsse; + + location ~ ^/index\.php/apps/news/api/?$ { + try_files $uri @arsse_public; + } +} + +# Tiny Tiny RSS protocol +location /tt-rss/api { + try_files $uri @arsse; +} + +# Tiny Tiny RSS feed icons +location /tt-rss/feed-icons/ { + try_files $uri @arsse; +} + +# Tiny Tiny RSS special-feed icons; these are static files +location /tt-rss/images/ { + try_files $uri =404; +} + +# Fever protocol +location /fever/ { + try_files $uri @arsse; +} + +# Miniflux protocol +location /v1/ { + # If put behind HTTP authentication token login will not be possible + try_files $uri @arsse; +} + +# Miniflux version number +location /version { + try_files $uri @arsse_public; +} + +# Miniflux "health check" +location /healthcheck { + try_files $uri @arsse_public; +} diff --git a/dist/arch/nginx/arsse.conf b/dist/arch/nginx/arsse.conf new file mode 100644 index 00000000..5d2234b8 --- /dev/null +++ b/dist/arch/nginx/arsse.conf @@ -0,0 +1,17 @@ +root /usr/share/webapps/arsse/www; # adjust according to your installation path + +location @arsse { + # HTTP authentication may be enabled for this location, though this may impact some features + fastcgi_pass unix:/run/php-fpm/arsse.sock; + fastcgi_param SCRIPT_FILENAME /usr/share/webapps/arsse/arsse.php; + include /etc/webapps/arsse/nginx/arsse-fcgi.conf; +} + +location @arsse_public { + # HTTP authentication should not be enabled for this location + fastcgi_pass unix:/run/php-fpm/arsse.sock; + fastcgi_param SCRIPT_FILENAME /usr/share/webapps/arsse/arsse.php; + include /etc/webapps/arsse/nginx/arsse-fcgi.conf; +} + +include /etc/webapps/arsse/nginx/arsse-loc.conf; diff --git a/dist/arch/nginx/sample.conf b/dist/arch/nginx/sample.conf new file mode 100644 index 00000000..efaecd62 --- /dev/null +++ b/dist/arch/nginx/sample.conf @@ -0,0 +1,13 @@ +server { + server_name news.example.com; + listen 80; + listen [::]:80; + listen 443 ssl http2; + listen [::]:443 ssl http2; + + ssl_certificate /etc/letsencrypt/live/news.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/news.example.com/privkey.pem; + ssl_trusted_certificate /etc/letsencrypt/live/news.example.com/chain.pem; + + include /etc/webapps/arsse/nginx/arsse.conf; +} From 971c12ff9f890f8f3145973c3aa8ccdb46a59d86 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 16 May 2021 20:21:41 -0400 Subject: [PATCH 223/366] Rename sample to example --- dist/arch/nginx/{sample.conf => example.conf} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dist/arch/nginx/{sample.conf => example.conf} (100%) diff --git a/dist/arch/nginx/sample.conf b/dist/arch/nginx/example.conf similarity index 100% rename from dist/arch/nginx/sample.conf rename to dist/arch/nginx/example.conf From 7abdf05b7fbcd24178104e8e1d72b47b10a49b0f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 16 May 2021 23:06:49 -0400 Subject: [PATCH 224/366] Make package from local files for now --- .gitignore | 1 + dist/arch/PKGBUILD | 44 +++++++++++++++++++++++--------------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 10bac85b..b204061c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /documentation/ /manual/ /tests/coverage/ +/dist/arch/arsse /dist/arch/src /dist/arch/pkg /arsse.db* diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index 66a5ff16..be98802d 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -1,3 +1,4 @@ +_repopath=`dirname $(dirname $(pwd))` pkgname="arsse" pkgver="0.9.1" pkgrel=1 @@ -8,7 +9,7 @@ url="https://thearsse.com/" license=("MIT") groups=() depends=() -makedepends=() +makedepends=("git" "php" "php-intl" "composer") checkdepends=() optdepends=("php-pgsql: PostgreSQL database support") provides=() @@ -18,38 +19,39 @@ backup=("etc/webapps/arsse/config.php" "etc/php/php-fpm.d/arsse.conf") options=() install= changelog= -source=("https://thearsse.com/releases/0.9.0.tar.gz" - "arsse.service" - "arsse-fetch.service" - "sysuser.conf" - "config.php" - "php-fpm.conf" - "arsse.sh") +source=("git+file://$_repopath") noextract=() +md5sums=("SKIP") + +pkgver() { + git describe --tags | sed 's/\([^-]*-\)g/r\1/;s/-/./g' +} + +build() { + cd "$srcdir/arsse" + composer install + ./robo manual + composer install --no-dev -o -n --no-scripts + php arsse.php conf save-defaults config.defaults.php +} package() { depends=("php" "php-intl" "php-sqlite" "php-fpm") cd "$pkgdir" - mkdir -p "usr/bin" "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "etc/webapps/arsse" "etc/php/php-fpm.d/" + mkdir -p "usr/bin" "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "etc/php/php-fpm.d/" "etc/webapps/arsse" "etc/webapps/arsse/nginx" cd "$srcdir/arsse" - cp ../arsse.sh "$pkgdir/usr/bin/arsse" + cp dist/arch/arsse.sh "$pkgdir/usr/bin/arsse" cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" cp -r manual/* "$pkgdir/usr/share/doc/arsse" cp LICENSE AUTHORS "$pkgdir/usr/share/licenses/arsse" - cp ../*.service "$pkgdir/usr/lib/systemd/system" - cp ../sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" - cp ../config.php config.defaults.php "$pkgdir/etc/webapps/arsse" - cp ../php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" + cp dist/arch/*.service "$pkgdir/usr/lib/systemd/system" + cp dist/arch/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" + cp dist/arch/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" + cp dist/arch/config.php config.defaults.php "$pkgdir/etc/webapps/arsse" + cp dist/arch/nginx/* "$pkgdir/etc/webapps/arsse/nginx" cd "$pkgdir" chmod -R a=rX * chmod a=rx usr/bin/arsse chmod u=r etc/webapps/arsse/* ln -sT "/etc/webapps/arsse/config.php" "usr/share/webapps/arsse/config.php" } -md5sums=('93327083c316daf879c70921189ed7b6' - '91871736d9594b2c92d1fa6b6e4f2803' - '0ca05e2965247d4651a986aad81d80e1' - 'b6ef9ab7e9062df1d5ba060066b6d734' - '33e7a5b290ef20339952f1d904b33f8f' - '943d35272b0aa7af2bf3818a0c9bd5fc' - '4fb46ec290e497279c3dd7c8c528abf6') From 3ebc23ab13ccf163bc7b3eae6fc6074e5d712112 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 17 May 2021 07:27:22 -0400 Subject: [PATCH 225/366] Tweaks --- dist/arch/PKGBUILD | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index be98802d..22667736 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -1,6 +1,6 @@ _repopath=`dirname $(dirname $(pwd))` pkgname="arsse" -pkgver="0.9.1" +pkgver=0.9.1.r10.7abdf05 pkgrel=1 epoch= pkgdesc="RSS/Atom newsfeed synchronization server" @@ -32,7 +32,8 @@ build() { composer install ./robo manual composer install --no-dev -o -n --no-scripts - php arsse.php conf save-defaults config.defaults.php + php arsse.php conf save-defaults config.defaults.php + rm -r vendor/bin } package() { From 805a508ea670bb949333f2db042c56ecd088a30e Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 17 May 2021 09:30:10 -0400 Subject: [PATCH 226/366] Use correct state path --- dist/arch/arsse-fetch.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/arch/arsse-fetch.service b/dist/arch/arsse-fetch.service index 7ee6eb7e..78688f58 100644 --- a/dist/arch/arsse-fetch.service +++ b/dist/arch/arsse-fetch.service @@ -17,7 +17,7 @@ ProtectHome=true StateDirectory=arsse ConfigurationDirectory=webapps/arsse ReadOnlyPaths=/ -ReadWriePaths=/usr/lib/arsse +ReadWriePaths=/var/lib/arsse NoExecPaths=/ ExecPaths=/usr/bin/php /usr/bin/php7 PrivateTmp=true From 3eab5aad5d3a443375e08ca8301aaf2ef9bc04a0 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 17 May 2021 15:46:46 -0400 Subject: [PATCH 227/366] Fix adding users to a blank database --- lib/Database.php | 2 +- tests/cases/Database/SeriesUser.php | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/Database.php b/lib/Database.php index 776b46d2..f3320ce2 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -271,7 +271,7 @@ class Database { } $hash = (strlen($password) > 0) ? password_hash($password, \PASSWORD_DEFAULT) : ""; // NOTE: This roundabout construction (with 'select' rather than 'values') is required by MySQL, because MySQL is riddled with pitfalls and exceptions - $this->db->prepare("INSERT INTO arsse_users(id,password,num) select ?, ?, ((select max(num) from arsse_users) + 1)", "str", "str")->runArray([$user,$hash]); + $this->db->prepare("INSERT INTO arsse_users(id,password,num) select ?, ?, (coalesce((select max(num) from arsse_users), 0) + 1)", "str", "str")->runArray([$user,$hash]); return true; } diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index b56a64d8..031e5161 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -205,4 +205,11 @@ trait SeriesUser { $this->assertException("alreadyExists", "User", "ExceptionConflict"); Arsse::$db->userRename("john.doe@example.com", "jane.doe@example.com"); } + + public function testAddFirstUser(): void { + // first truncate the users table + static::$drv->exec("DELETE FROM arsse_users"); + // add a user; if the max of the num column is not properly coalesced, this will result in a constraint violation + $this->assertTrue(Arsse::$db->userAdd("john.doe@example.com", "")); + } } From e2b182ebe6414e99ff6afdd4c7ff896ae21e3626 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 17 May 2021 15:47:26 -0400 Subject: [PATCH 228/366] Fix errors in Arch config file --- dist/arch/PKGBUILD | 7 ++++--- dist/arch/config.php | 4 ++-- dist/arch/nginx/arsse.conf | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index 22667736..17672b1f 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -1,4 +1,3 @@ -_repopath=`dirname $(dirname $(pwd))` pkgname="arsse" pkgver=0.9.1.r10.7abdf05 pkgrel=1 @@ -11,7 +10,9 @@ groups=() depends=() makedepends=("git" "php" "php-intl" "composer") checkdepends=() -optdepends=("php-pgsql: PostgreSQL database support") +optdepends=("php-pgsql: PostgreSQL database support" + "nginx: HTTP server" + "apache: HTTP server") provides=() conflicts=() replaces=() @@ -19,7 +20,7 @@ backup=("etc/webapps/arsse/config.php" "etc/php/php-fpm.d/arsse.conf") options=() install= changelog= -source=("git+file://$_repopath") +source=("git+file://$(dirname $(dirname $(pwd)))") noextract=() md5sums=("SKIP") diff --git a/dist/arch/config.php b/dist/arch/config.php index 1df16355..a6ac33e5 100644 --- a/dist/arch/config.php +++ b/dist/arch/config.php @@ -4,5 +4,5 @@ # for possible configuration parameters return [ - 'dbSQLiteFile' => "/usr/lib/arsse/arsse.db", -]; \ No newline at end of file + 'dbSQLite3File' => "/var/lib/arsse/arsse.db", +]; diff --git a/dist/arch/nginx/arsse.conf b/dist/arch/nginx/arsse.conf index 5d2234b8..dd45d5a5 100644 --- a/dist/arch/nginx/arsse.conf +++ b/dist/arch/nginx/arsse.conf @@ -1,4 +1,4 @@ -root /usr/share/webapps/arsse/www; # adjust according to your installation path +root /usr/share/webapps/arsse/www; location @arsse { # HTTP authentication may be enabled for this location, though this may impact some features From a97ca2363114362c7e19502cc01dfc7792898784 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 17 May 2021 18:03:47 -0400 Subject: [PATCH 229/366] Don't try to enable extensions --- dist/arch/PKGBUILD | 27 +++++++++++---------------- dist/arch/arsse.sh | 13 +++---------- dist/arch/php-fpm.conf | 5 ----- 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index 17672b1f..5a780408 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -1,5 +1,5 @@ pkgname="arsse" -pkgver=0.9.1.r10.7abdf05 +pkgver=0.9.1.r14.e2b182e pkgrel=1 epoch= pkgdesc="RSS/Atom newsfeed synchronization server" @@ -39,21 +39,16 @@ build() { package() { depends=("php" "php-intl" "php-sqlite" "php-fpm") - cd "$pkgdir" - mkdir -p "usr/bin" "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "etc/php/php-fpm.d/" "etc/webapps/arsse" "etc/webapps/arsse/nginx" cd "$srcdir/arsse" - cp dist/arch/arsse.sh "$pkgdir/usr/bin/arsse" - cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" - cp -r manual/* "$pkgdir/usr/share/doc/arsse" - cp LICENSE AUTHORS "$pkgdir/usr/share/licenses/arsse" - cp dist/arch/*.service "$pkgdir/usr/lib/systemd/system" - cp dist/arch/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" - cp dist/arch/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" - cp dist/arch/config.php config.defaults.php "$pkgdir/etc/webapps/arsse" - cp dist/arch/nginx/* "$pkgdir/etc/webapps/arsse/nginx" - cd "$pkgdir" - chmod -R a=rX * - chmod a=rx usr/bin/arsse - chmod u=r etc/webapps/arsse/* + install -DTm755 dist/arch/arsse.sh "$pkgdir/usr/bin/arsse" + install -D lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" + install -D manual/* "$pkgdir/usr/share/doc/arsse" + install -D LICENSE AUTHORS "$pkgdir/usr/share/licenses/arsse" + install -D dist/arch/*.service "$pkgdir/usr/lib/systemd/system" + install -DT dist/arch/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" + install -DT dist/arch/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" + install -Dm640 dist/arch/config.php "$pkgdir/etc/webapps/arsse" + install -D config.defaults.php "$pkgdir/etc/webapps/arsse" + install -D dist/arch/nginx/* "$pkgdir/etc/webapps/arsse/nginx" ln -sT "/etc/webapps/arsse/config.php" "usr/share/webapps/arsse/config.php" } diff --git a/dist/arch/arsse.sh b/dist/arch/arsse.sh index adc286eb..c065c29d 100644 --- a/dist/arch/arsse.sh +++ b/dist/arch/arsse.sh @@ -1,10 +1,3 @@ -#! /usr/bin/bash - -if [ `id -u` -eq 0 ]; then - setpriv --clear-groups --inh-caps -all --egid=arsse --euid=arsse php /usr/share/webapps/arsse/arsse.php $@ -elif [ `id -un` == "arsse" ]; then - php /usr/share/webapps/arsse/arsse.php $@ -else - echo "Not authorized." >&2 - exit 1 -fi +#! /usr/bin/php + Date: Mon, 17 May 2021 20:08:32 -0400 Subject: [PATCH 230/366] Correct permissions A tmpfiles.d configuration is still required --- dist/arch/PKGBUILD | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index 5a780408..ecdee465 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -1,5 +1,5 @@ pkgname="arsse" -pkgver=0.9.1.r14.e2b182e +pkgver=0.9.1.r15.a97ca23 pkgrel=1 epoch= pkgdesc="RSS/Atom newsfeed synchronization server" @@ -39,16 +39,21 @@ build() { package() { depends=("php" "php-intl" "php-sqlite" "php-fpm") + cd "$pkgdir" + mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "etc/php/php-fpm.d/" "etc/webapps/arsse" "etc/webapps/arsse/nginx" + cd "$srcdir/arsse" + cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" + cp -r manual/* "$pkgdir/usr/share/doc/arsse" + cp LICENSE AUTHORS "$pkgdir/usr/share/licenses/arsse" + cp dist/arch/*.service "$pkgdir/usr/lib/systemd/system" + cp dist/arch/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" + cp dist/arch/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" + cp -r dist/arch/nginx config.defaults.php "$pkgdir/etc/webapps/arsse" + cd "$pkgdir" + chmod -R u=rwX,g=rX,o=rX * + chmod u=r etc/webapps/arsse/ + ln -sT "/etc/webapps/arsse/config.php" "usr/share/webapps/arsse/config.php" cd "$srcdir/arsse" install -DTm755 dist/arch/arsse.sh "$pkgdir/usr/bin/arsse" - install -D lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" - install -D manual/* "$pkgdir/usr/share/doc/arsse" - install -D LICENSE AUTHORS "$pkgdir/usr/share/licenses/arsse" - install -D dist/arch/*.service "$pkgdir/usr/lib/systemd/system" - install -DT dist/arch/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" - install -DT dist/arch/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" install -Dm640 dist/arch/config.php "$pkgdir/etc/webapps/arsse" - install -D config.defaults.php "$pkgdir/etc/webapps/arsse" - install -D dist/arch/nginx/* "$pkgdir/etc/webapps/arsse/nginx" - ln -sT "/etc/webapps/arsse/config.php" "usr/share/webapps/arsse/config.php" } From 44612cfe8fc2a4c125203641e39186863591aa41 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 17 May 2021 21:43:52 -0400 Subject: [PATCH 231/366] Add tmpfiles --- dist/arch/PKGBUILD | 5 +++-- dist/arch/tmpfiles.conf | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 dist/arch/tmpfiles.conf diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index ecdee465..d6651408 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -1,5 +1,5 @@ pkgname="arsse" -pkgver=0.9.1.r15.a97ca23 +pkgver=0.9.1.r16.d1fd6e9 pkgrel=1 epoch= pkgdesc="RSS/Atom newsfeed synchronization server" @@ -40,13 +40,14 @@ build() { package() { depends=("php" "php-intl" "php-sqlite" "php-fpm") cd "$pkgdir" - mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "etc/php/php-fpm.d/" "etc/webapps/arsse" "etc/webapps/arsse/nginx" + mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "etc/php/php-fpm.d/" "etc/webapps/arsse" "etc/webapps/arsse/nginx" cd "$srcdir/arsse" cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" cp -r manual/* "$pkgdir/usr/share/doc/arsse" cp LICENSE AUTHORS "$pkgdir/usr/share/licenses/arsse" cp dist/arch/*.service "$pkgdir/usr/lib/systemd/system" cp dist/arch/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" + cp dist/arch/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" cp dist/arch/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" cp -r dist/arch/nginx config.defaults.php "$pkgdir/etc/webapps/arsse" cd "$pkgdir" diff --git a/dist/arch/tmpfiles.conf b/dist/arch/tmpfiles.conf new file mode 100644 index 00000000..8c1e510e --- /dev/null +++ b/dist/arch/tmpfiles.conf @@ -0,0 +1 @@ +z /etc/webapps/arsse/config.php - root arsse - - From 488af80a85204f04f592a306325c968b2a385c06 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 18 May 2021 09:44:52 -0400 Subject: [PATCH 232/366] Update changelog --- CHANGELOG | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 07ab0288..9dfdf8e9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +Version 0.9.2 (2021-??-??) +========================== + +Bug fixes: +- Do not fail adding users to an empty database (regression since 0.9.0) + +Changes: +- Packages for Arch Linux are now available (see manual for details) + Version 0.9.1 (2021-03-18) ========================== From 568b12600b22a680c29d4a81776a2d242066f031 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 18 May 2021 09:46:42 -0400 Subject: [PATCH 233/366] Drop privileges when executing CLI --- dist/arch/arsse.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dist/arch/arsse.sh b/dist/arch/arsse.sh index c065c29d..e34ffd87 100644 --- a/dist/arch/arsse.sh +++ b/dist/arch/arsse.sh @@ -1,3 +1,10 @@ #! /usr/bin/php Date: Tue, 18 May 2021 18:42:42 -0400 Subject: [PATCH 234/366] Fix more bugs --- CHANGELOG | 2 ++ lib/Conf.php | 2 +- lib/Db/SQLite3/Driver.php | 6 ++++++ tests/cases/Conf/TestConf.php | 8 ++++++++ tests/cases/Db/SQLite3/TestCreation.php | 13 +++++++++++++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 9dfdf8e9..b0f89fc2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,8 @@ Version 0.9.2 (2021-??-??) Bug fixes: - Do not fail adding users to an empty database (regression since 0.9.0) +- Cleanly ignore unknown configuration properties +- Set access mode to rw-r---- when creating SQLite databases Changes: - Packages for Arch Linux are now available (see manual for details) diff --git a/lib/Conf.php b/lib/Conf.php index dfe35e29..428e87a4 100644 --- a/lib/Conf.php +++ b/lib/Conf.php @@ -265,7 +265,7 @@ class Conf { protected function propertyImport(string $key, $value, string $file = "") { $typeName = $this->types[$key]['name'] ?? "mixed"; $typeConst = $this->types[$key]['const'] ?? Value::T_MIXED; - $nullable = (int) (bool) ($this->types[$key]['const'] & Value::M_NULL); + $nullable = (int) (bool) ($typeConst & Value::M_NULL); try { if ($typeName === "\\DateInterval") { // date intervals have special handling: if the existing value (ultimately, the default value) diff --git a/lib/Db/SQLite3/Driver.php b/lib/Db/SQLite3/Driver.php index 3445b892..7c5a1109 100644 --- a/lib/Db/SQLite3/Driver.php +++ b/lib/Db/SQLite3/Driver.php @@ -31,6 +31,12 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { $dbKey = Arsse::$conf->dbSQLite3Key; $timeout = Arsse::$conf->dbSQLite3Timeout * 1000; try { + // check whether the file exists; if it doesn't create the file and set its mode to rw-r----- + if ($dbFile !== ":memory:" && !file_exists($dbFile)) { + if (@touch($dbFile)) { + chmod($dbFile, 0640); + } + } $this->makeConnection($dbFile, $dbKey); } catch (\Throwable $e) { // if opening the database doesn't work, check various pre-conditions to find out what the problem might be diff --git a/tests/cases/Conf/TestConf.php b/tests/cases/Conf/TestConf.php index 088746bb..0e827d4b 100644 --- a/tests/cases/Conf/TestConf.php +++ b/tests/cases/Conf/TestConf.php @@ -108,6 +108,14 @@ class TestConf extends \JKingWeb\Arsse\Test\AbstractTest { $conf->import($arr); } + public function testImportCustomProperty(): void { + $arr = [ + 'customProperty' => "I'm special!", + ]; + $conf = new Conf; + $this->assertSame($conf, $conf->import($arr)); + } + public function testImportBogusDriver(): void { $arr = [ 'dbDriver' => "this driver does not exist", diff --git a/tests/cases/Db/SQLite3/TestCreation.php b/tests/cases/Db/SQLite3/TestCreation.php index 3e3b2f29..348e2ae7 100644 --- a/tests/cases/Db/SQLite3/TestCreation.php +++ b/tests/cases/Db/SQLite3/TestCreation.php @@ -185,4 +185,17 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertException("fileCorrupt", "Db"); new Driver; } + + public function testSetFileMode(): void { + $f = tempnam(sys_get_temp_dir(), "arsse"); + Arsse::$conf->dbSQLite3File = $f; + // delete the file PHP just created + unlink($f); + // recreate the file + new Driver; + // check the mode + clearstatcache(); + $mode = base_convert((string) stat($f)['mode'], 10, 8); + $this->assertMatchesRegularExpression("/640$/", $mode); + } } From 79391446cdbc773aaade3859a44d00b9c44071f4 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 19 May 2021 08:51:17 -0400 Subject: [PATCH 235/366] Start moving Arch build responsibility to Robo Also clean up the generic packaging task --- RoboFile.php | 37 +++++++++++++++++++++++++++++++------ dist/arch/PKGBUILD | 23 ++--------------------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/RoboFile.php b/RoboFile.php index ec6457cf..7e78ac45 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -149,7 +149,7 @@ class RoboFile extends \Robo\Tasks { * * The version to package may be any Git tree-ish identifier: a tag, a branch, * or any commit hash. If none is provided on the command line, Robo will prompt - * for a commit to package; the default is "head". + * for a commit to package; the default is "HEAD". * * Note that while it is possible to re-package old versions, the resultant tarball * may not be equivalent due to subsequent changes in the exclude list, or because @@ -164,7 +164,9 @@ class RoboFile extends \Robo\Tasks { // create a temporary directory $dir = $t->tmpDir().\DIRECTORY_SEPARATOR; // create a Git worktree for the selected commit in the temp location - $t->taskExec("git worktree add ".escapeshellarg($dir)." ".escapeshellarg($version)); + $t->taskExec("git worktree add ".escapeshellarg($dir)." ".escapeshellarg($version)) + ->completion($this->taskFilesystemStack()->remove($dir)) + ->completion($this->taskExec("git worktree prune")); // perform Composer installation in the temp location with dev dependencies $t->taskComposerInstall()->dir($dir); // generate the manual @@ -195,13 +197,36 @@ class RoboFile extends \Robo\Tasks { ]); // generate a sample configuration file $t->taskExec(escapeshellarg(\PHP_BINARY)." arsse.php conf save-defaults config.defaults.php")->dir($dir); + // remove any existing archive + $t->taskFilesystemStack()->remove($archive); // package it all up $t->taskPack($archive)->addDir("arsse", $dir); // execute the collection - $out = $t->run(); - // clean the Git worktree list - $this->_exec("git worktree prune"); - return $out; + return $t->run(); + } + + /** Packages a given commit of the software into an Arch package + * + * The version to package may be any Git tree-ish identifier: a tag, a branch, + * or any commit hash. If none is provided on the command line, Robo will prompt + * for a commit to package; the default is "HEAD". + * + * Note that while it is possible to re-package old versions, the resultant tarball + * may not be equivalent due to subsequent changes in the exclude list, or because + * of new tooling. + */ + public function packageArch(string $version = null): Result { + // establish which commit to package + $version = $version ?? $this->askDefault("Commit to package:", "HEAD"); + $archive = BASE."arsse-$version.tar.gz"; + // start a collection + $t = $this->collectionBuilder(); + // create a tarball + $t->addCode(function() use ($version) { + return $this->package($version); + }); + // extract PKGBUILD and run it; todo + return $t->run(); } /** Generates static manual pages in the "manual" directory diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index d6651408..b44991c9 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -1,42 +1,23 @@ pkgname="arsse" -pkgver=0.9.1.r16.d1fd6e9 +pkgver=0.9.1 pkgrel=1 epoch= pkgdesc="RSS/Atom newsfeed synchronization server" arch=("any") url="https://thearsse.com/" license=("MIT") -groups=() depends=() makedepends=("git" "php" "php-intl" "composer") checkdepends=() optdepends=("php-pgsql: PostgreSQL database support" "nginx: HTTP server" "apache: HTTP server") -provides=() -conflicts=() -replaces=() backup=("etc/webapps/arsse/config.php" "etc/php/php-fpm.d/arsse.conf") -options=() install= changelog= -source=("git+file://$(dirname $(dirname $(pwd)))") -noextract=() +source=("arsse-0.9.1.tar.gz") md5sums=("SKIP") -pkgver() { - git describe --tags | sed 's/\([^-]*-\)g/r\1/;s/-/./g' -} - -build() { - cd "$srcdir/arsse" - composer install - ./robo manual - composer install --no-dev -o -n --no-scripts - php arsse.php conf save-defaults config.defaults.php - rm -r vendor/bin -} - package() { depends=("php" "php-intl" "php-sqlite" "php-fpm") cd "$pkgdir" From e75f8cebfb5acc78e4d64b06394840c1015c9f92 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 19 May 2021 11:27:21 -0400 Subject: [PATCH 236/366] Add Arch packaging to Robo file --- RoboFile.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/RoboFile.php b/RoboFile.php index 7e78ac45..1b90d396 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -167,6 +167,16 @@ class RoboFile extends \Robo\Tasks { $t->taskExec("git worktree add ".escapeshellarg($dir)." ".escapeshellarg($version)) ->completion($this->taskFilesystemStack()->remove($dir)) ->completion($this->taskExec("git worktree prune")); + // patch the Arch PKGBUILD file with the correct version string + $t->addCode(function () use ($dir) { + $ver = trim(preg_replace('/^([^-]+)-(\d+)-(\w+)$/', "$1.r$2.$3", `git -C "$dir" describe --tags`)); + return $this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^pkgver=.*$/m')->to("pkgver=$ver")->run(); + }); + // patch the Arch PKGBUILD file with the correct source file + $t->addCode(function () use ($dir, $archive) { + $tar = basename($archive); + return $this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^source=\("arsse-[^"]+"\)$/m')->to("source=(\"$tar\")")->run(); + }); // perform Composer installation in the temp location with dev dependencies $t->taskComposerInstall()->dir($dir); // generate the manual @@ -225,7 +235,13 @@ class RoboFile extends \Robo\Tasks { $t->addCode(function() use ($version) { return $this->package($version); }); - // extract PKGBUILD and run it; todo + // extract the PKGBUILD from the just-created archive and build it + $t->addCode(function() use ($archive) { + // because Robo doesn't support extracting a single file we have to do it ourselves + (new \Archive_Tar($archive))->extractList("arsse/dist/arch/PKGBUILD", BASE, "arsse/dist/arch/", false); + return $this->taskFilesystemStack()->touch(BASE."PKGBUILD")->run(); + })->completion($this->taskFilesystemStack()->remove(BASE."PKGBUILD")); + $t->taskExec("makepkg -Ccf")->dir(BASE); return $t->run(); } From fbe03a25344840117e70003e77f9fed10466e1b7 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 19 May 2021 11:34:54 -0400 Subject: [PATCH 237/366] Use chmod instead of touch --- RoboFile.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/RoboFile.php b/RoboFile.php index 1b90d396..8bb5991e 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -220,10 +220,6 @@ class RoboFile extends \Robo\Tasks { * The version to package may be any Git tree-ish identifier: a tag, a branch, * or any commit hash. If none is provided on the command line, Robo will prompt * for a commit to package; the default is "HEAD". - * - * Note that while it is possible to re-package old versions, the resultant tarball - * may not be equivalent due to subsequent changes in the exclude list, or because - * of new tooling. */ public function packageArch(string $version = null): Result { // establish which commit to package @@ -239,7 +235,8 @@ class RoboFile extends \Robo\Tasks { $t->addCode(function() use ($archive) { // because Robo doesn't support extracting a single file we have to do it ourselves (new \Archive_Tar($archive))->extractList("arsse/dist/arch/PKGBUILD", BASE, "arsse/dist/arch/", false); - return $this->taskFilesystemStack()->touch(BASE."PKGBUILD")->run(); + // perform a do-nothing filesystem operation since we need a Robo task result + return $this->taskFilesystemStack()->chmod(BASE."PKGBUILD", 0644)->run(); })->completion($this->taskFilesystemStack()->remove(BASE."PKGBUILD")); $t->taskExec("makepkg -Ccf")->dir(BASE); return $t->run(); From 3a3b9231df4aaee6ed28d5eb6b6f7eb48b3addff Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 19 May 2021 15:06:37 -0400 Subject: [PATCH 238/366] Use generic configuration where possible --- dist/arch/PKGBUILD | 33 ++++++---- dist/arch/nginx/arsse.conf | 17 ----- dist/arch/sysuser.conf | 1 - dist/arch/tmpfiles.conf | 1 - dist/{arch/arsse.sh => arsse} | 2 +- dist/arsse.service | 15 ----- dist/nginx.conf | 75 ---------------------- dist/{arch => }/nginx/arsse-fcgi.conf | 0 dist/{arch => }/nginx/arsse-loc.conf | 0 dist/nginx/arsse.conf | 17 +++++ dist/{arch => }/nginx/example.conf | 2 +- dist/{arch => }/php-fpm.conf | 4 +- dist/{arch => systemd}/arsse-fetch.service | 19 +++--- dist/{arch => systemd}/arsse.service | 6 +- dist/sysuser.conf | 1 + dist/tmpfiles.conf | 1 + 16 files changed, 59 insertions(+), 135 deletions(-) delete mode 100644 dist/arch/nginx/arsse.conf delete mode 100644 dist/arch/sysuser.conf delete mode 100644 dist/arch/tmpfiles.conf rename dist/{arch/arsse.sh => arsse} (80%) delete mode 100644 dist/arsse.service delete mode 100644 dist/nginx.conf rename dist/{arch => }/nginx/arsse-fcgi.conf (100%) rename dist/{arch => }/nginx/arsse-loc.conf (100%) create mode 100644 dist/nginx/arsse.conf rename dist/{arch => }/nginx/example.conf (88%) rename dist/{arch => }/php-fpm.conf (73%) rename dist/{arch => systemd}/arsse-fetch.service (66%) rename dist/{arch => systemd}/arsse.service (100%) create mode 100644 dist/sysuser.conf create mode 100644 dist/tmpfiles.conf diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index b44991c9..8d1f2fb5 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -7,11 +7,13 @@ arch=("any") url="https://thearsse.com/" license=("MIT") depends=() -makedepends=("git" "php" "php-intl" "composer") +makedepends=() checkdepends=() -optdepends=("php-pgsql: PostgreSQL database support" - "nginx: HTTP server" - "apache: HTTP server") +optdepends=("nginx: HTTP server" + "apache: HTTP server" + "percona-server: Alternate database" + "postgresql: Alternate database" + "php-pgsql: PostgreSQL database support") backup=("etc/webapps/arsse/config.php" "etc/php/php-fpm.d/arsse.conf") install= changelog= @@ -19,23 +21,32 @@ source=("arsse-0.9.1.tar.gz") md5sums=("SKIP") package() { + # define runtime dependencies depends=("php" "php-intl" "php-sqlite" "php-fpm") + # create most directories necessary forn the final package cd "$pkgdir" mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "etc/php/php-fpm.d/" "etc/webapps/arsse" "etc/webapps/arsse/nginx" + #copy requisite files cd "$srcdir/arsse" cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" cp -r manual/* "$pkgdir/usr/share/doc/arsse" cp LICENSE AUTHORS "$pkgdir/usr/share/licenses/arsse" - cp dist/arch/*.service "$pkgdir/usr/lib/systemd/system" - cp dist/arch/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" - cp dist/arch/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" - cp dist/arch/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" - cp -r dist/arch/nginx config.defaults.php "$pkgdir/etc/webapps/arsse" + cp dist/systemd/* "$pkgdir/usr/lib/systemd/system" + cp dist/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" + cp dist/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" + cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" + cp -r dist/nginx config.defaults.php "$pkgdir/etc/webapps/arsse" cd "$pkgdir" + # adjust permissions, just in case chmod -R u=rwX,g=rX,o=rX * - chmod u=r etc/webapps/arsse/ + # create a symbolic link for the configuration file ln -sT "/etc/webapps/arsse/config.php" "usr/share/webapps/arsse/config.php" + # copy files requiring special permissions cd "$srcdir/arsse" - install -DTm755 dist/arch/arsse.sh "$pkgdir/usr/bin/arsse" + install -Dm755 dist/arsse "$pkgdir/usr/bin" install -Dm640 dist/arch/config.php "$pkgdir/etc/webapps/arsse" + # patch generic configuration files to use Arch-specific paths and identifiers + sed -ise 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"* + sed -ise 's/\/var\/run\/php\//\/run\/php-fpm\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf" + sed -ise 's/www-data/http/' "$pkgdir/etc/php/php-fpm.d/arsse.conf" } diff --git a/dist/arch/nginx/arsse.conf b/dist/arch/nginx/arsse.conf deleted file mode 100644 index dd45d5a5..00000000 --- a/dist/arch/nginx/arsse.conf +++ /dev/null @@ -1,17 +0,0 @@ -root /usr/share/webapps/arsse/www; - -location @arsse { - # HTTP authentication may be enabled for this location, though this may impact some features - fastcgi_pass unix:/run/php-fpm/arsse.sock; - fastcgi_param SCRIPT_FILENAME /usr/share/webapps/arsse/arsse.php; - include /etc/webapps/arsse/nginx/arsse-fcgi.conf; -} - -location @arsse_public { - # HTTP authentication should not be enabled for this location - fastcgi_pass unix:/run/php-fpm/arsse.sock; - fastcgi_param SCRIPT_FILENAME /usr/share/webapps/arsse/arsse.php; - include /etc/webapps/arsse/nginx/arsse-fcgi.conf; -} - -include /etc/webapps/arsse/nginx/arsse-loc.conf; diff --git a/dist/arch/sysuser.conf b/dist/arch/sysuser.conf deleted file mode 100644 index 9f936e4c..00000000 --- a/dist/arch/sysuser.conf +++ /dev/null @@ -1 +0,0 @@ -u arsse - "The Arsse" /usr/lib/arsse - diff --git a/dist/arch/tmpfiles.conf b/dist/arch/tmpfiles.conf deleted file mode 100644 index 8c1e510e..00000000 --- a/dist/arch/tmpfiles.conf +++ /dev/null @@ -1 +0,0 @@ -z /etc/webapps/arsse/config.php - root arsse - - diff --git a/dist/arch/arsse.sh b/dist/arsse similarity index 80% rename from dist/arch/arsse.sh rename to dist/arsse index e34ffd87..b4c56e4a 100644 --- a/dist/arch/arsse.sh +++ b/dist/arsse @@ -7,4 +7,4 @@ if (posix_geteuid() == 0) { posix_setuid($info['uid']); } } -require "/usr/share/webapps/arsse/arsse.php"; +require "/usr/share/arsse/arsse.php"; diff --git a/dist/arsse.service b/dist/arsse.service deleted file mode 100644 index 0adcdae4..00000000 --- a/dist/arsse.service +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=The Arsse feed fetching service -After=network.target mysql.service postgresql.service - -[Service] -User=www-data -Group=www-data -WorkingDirectory=/usr/share/arsse -Type=simple -StandardOutput=null -StandardError=syslog -ExecStart=/usr/bin/env php /usr/share/arsse/arsse.php daemon - -[Install] -WantedBy=multi-user.target diff --git a/dist/nginx.conf b/dist/nginx.conf deleted file mode 100644 index c12ff214..00000000 --- a/dist/nginx.conf +++ /dev/null @@ -1,75 +0,0 @@ -server { - server_name example.com; - listen 80; # adding HTTPS configuration is highly recommended - root /usr/share/arsse/www; # adjust according to your installation path - - location / { - try_files $uri $uri/ =404; - } - - location @arsse { - fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; # adjust according to your system configuration - fastcgi_pass_header Authorization; # required if the Arsse is to perform its own HTTP authentication - fastcgi_pass_request_body on; - fastcgi_pass_request_headers on; - fastcgi_intercept_errors off; - fastcgi_buffering off; - fastcgi_param SCRIPT_FILENAME /usr/share/arsse/arsse.php; # adjust according to your installation path - fastcgi_param REQUEST_METHOD $request_method; - fastcgi_param CONTENT_TYPE $content_type; - fastcgi_param CONTENT_LENGTH $content_length; - fastcgi_param REQUEST_URI $uri; - fastcgi_param QUERY_STRING $query_string; - fastcgi_param HTTPS $https if_not_empty; - fastcgi_param REMOTE_USER $remote_user; - } - - # Nextcloud News protocol - location /index.php/apps/news/api { - try_files $uri @arsse; - - location ~ ^/index\.php/apps/news/api/?$ { - # this path should not be behind HTTP authentication - try_files $uri @arsse; - } - } - - # Tiny Tiny RSS protocol - location /tt-rss/api { - try_files $uri @arsse; - } - - # Tiny Tiny RSS feed icons - location /tt-rss/feed-icons/ { - try_files $uri @arsse; - } - - # Tiny Tiny RSS special-feed icons; these are static files - location /tt-rss/images/ { - # this path should not be behind HTTP authentication - try_files $uri =404; - } - - # Fever protocol - location /fever/ { - # this path should not be behind HTTP authentication - try_files $uri @arsse; - } - - # Miniflux protocol - location /v1/ { - try_files $uri @arsse; - } - - # Miniflux version number - location /version { - # this path should not be behind HTTP authentication - try_files $uri @arsse; - } - - # Miniflux "health check" - location /healthcheck { - # this path should not be behind HTTP authentication - try_files $uri @arsse; - } -} diff --git a/dist/arch/nginx/arsse-fcgi.conf b/dist/nginx/arsse-fcgi.conf similarity index 100% rename from dist/arch/nginx/arsse-fcgi.conf rename to dist/nginx/arsse-fcgi.conf diff --git a/dist/arch/nginx/arsse-loc.conf b/dist/nginx/arsse-loc.conf similarity index 100% rename from dist/arch/nginx/arsse-loc.conf rename to dist/nginx/arsse-loc.conf diff --git a/dist/nginx/arsse.conf b/dist/nginx/arsse.conf new file mode 100644 index 00000000..fe5721e8 --- /dev/null +++ b/dist/nginx/arsse.conf @@ -0,0 +1,17 @@ +root /usr/share/arsse/www; + +location @arsse { + # HTTP authentication may be enabled for this location, though this may impact some features + fastcgi_pass unix:/var/run/php/arsse.sock; + fastcgi_param SCRIPT_FILENAME /usr/share/arsse/arsse.php; + include /etc/arsse/nginx/arsse-fcgi.conf; +} + +location @arsse_public { + # HTTP authentication should not be enabled for this location + fastcgi_pass unix:/var/run/php/arsse.sock; + fastcgi_param SCRIPT_FILENAME /usr/share/arsse/arsse.php; + include /etc/arsse/nginx/arsse-fcgi.conf; +} + +include /etc/arsse/nginx/arsse-loc.conf; diff --git a/dist/arch/nginx/example.conf b/dist/nginx/example.conf similarity index 88% rename from dist/arch/nginx/example.conf rename to dist/nginx/example.conf index efaecd62..571a6385 100644 --- a/dist/arch/nginx/example.conf +++ b/dist/nginx/example.conf @@ -9,5 +9,5 @@ server { ssl_certificate_key /etc/letsencrypt/live/news.example.com/privkey.pem; ssl_trusted_certificate /etc/letsencrypt/live/news.example.com/chain.pem; - include /etc/webapps/arsse/nginx/arsse.conf; + include /etc/arsse/nginx/arsse.conf; } diff --git a/dist/arch/php-fpm.conf b/dist/php-fpm.conf similarity index 73% rename from dist/arch/php-fpm.conf rename to dist/php-fpm.conf index 4d15ae13..f1edc41c 100644 --- a/dist/arch/php-fpm.conf +++ b/dist/php-fpm.conf @@ -1,9 +1,9 @@ [arsse] user = arsse group = arsse -listen = /run/php-fpm/arsse.sock +listen = /var/run/php/arsse.sock listen.owner = arsse -listen.group = http +listen.group = www-data pm = dynamic pm.max_children = 5 pm.start_servers = 2 diff --git a/dist/arch/arsse-fetch.service b/dist/systemd/arsse-fetch.service similarity index 66% rename from dist/arch/arsse-fetch.service rename to dist/systemd/arsse-fetch.service index 78688f58..76b16e0d 100644 --- a/dist/arch/arsse-fetch.service +++ b/dist/systemd/arsse-fetch.service @@ -3,11 +3,14 @@ Description=The Arsse newsfeed fetching service Documentation=https://thearsse.com/manual/ PartOf=arsse.service +[Install] +WantedBy=multi-user.target + [Service] User=arsse Group=arsse Type=simple -WorkingDirectory=/usr/share/webapps/arsse +WorkingDirectory=/usr/share/arsse ExecStart=/usr/bin/arsse daemon ProtectProc=invisible @@ -15,11 +18,7 @@ NoNewPrivileges=true ProtectSystem=full ProtectHome=true StateDirectory=arsse -ConfigurationDirectory=webapps/arsse -ReadOnlyPaths=/ -ReadWriePaths=/var/lib/arsse -NoExecPaths=/ -ExecPaths=/usr/bin/php /usr/bin/php7 +ConfigurationDirectory=arsse PrivateTmp=true PrivateDevices=true RestrictSUIDSGID=true @@ -29,5 +28,9 @@ SyslogIdentifier=arsse Restart=on-failure RestartPreventStatus= -[Install] -WantedBy=multi-user.target +# These directives can be used for extra security, but are disabled for now for compatibility + +#ReadOnlyPaths=/ +#ReadWriePaths=/var/lib/arsse +#NoExecPaths=/ +#ExecPaths=/usr/bin/php /usr/bin/php7 diff --git a/dist/arch/arsse.service b/dist/systemd/arsse.service similarity index 100% rename from dist/arch/arsse.service rename to dist/systemd/arsse.service index 62ee4358..42e869fb 100644 --- a/dist/arch/arsse.service +++ b/dist/systemd/arsse.service @@ -5,9 +5,9 @@ Requires=arsse-fetch.service BindsTo=php-fpm.service After=php-fpm.service +[Install] +WantedBy=multi-user.target + [Service] Type=oneshot RemainAfterExit=true - -[Install] -WantedBy=multi-user.target diff --git a/dist/sysuser.conf b/dist/sysuser.conf new file mode 100644 index 00000000..cd708c3f --- /dev/null +++ b/dist/sysuser.conf @@ -0,0 +1 @@ +u arsse - "The Arsse" /var/lib/arsse - diff --git a/dist/tmpfiles.conf b/dist/tmpfiles.conf new file mode 100644 index 00000000..fa1af726 --- /dev/null +++ b/dist/tmpfiles.conf @@ -0,0 +1 @@ +z /etc/arsse/config.php - root arsse - - From 6d790c5efd15525c7f0b20f32f8d42c5c7808ec5 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 19 May 2021 18:59:20 -0400 Subject: [PATCH 239/366] Add prototype for new Apache configuration Needs testing --- dist/apache.conf | 23 --------------------- dist/apache/arsse-loc.conf | 41 ++++++++++++++++++++++++++++++++++++++ dist/apache/arsse.conf | 10 ++++++++++ dist/apache/example.conf | 9 +++++++++ dist/arch/PKGBUILD | 6 +++--- 5 files changed, 63 insertions(+), 26 deletions(-) delete mode 100644 dist/apache.conf create mode 100644 dist/apache/arsse-loc.conf create mode 100644 dist/apache/arsse.conf create mode 100644 dist/apache/example.conf diff --git a/dist/apache.conf b/dist/apache.conf deleted file mode 100644 index c0122967..00000000 --- a/dist/apache.conf +++ /dev/null @@ -1,23 +0,0 @@ -# N.B. the unix:/var/run/php/php7.2-fpm.sock path used repeatedly below will -# vary from system to system and will be probably need to be changed - - - ServerName localhost - # adjust according to your installation path - DocumentRoot /usr/share/arsse/www - - # adjust according to your installation path - ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/usr/share/arsse/arsse.php" - ProxyPreserveHost On - - # Nextcloud News v1.2, Tiny Tiny RSS API, TT-RSS newsfeed icons, Miniflux API - - ProxyPass "unix:/var/run/php/php7.2-fpm.sock|fcgi://localhost/usr/share/arsse" - - - # Nextcloud News API detection, Fever API, Miniflux miscellanies - - # these locations should not be behind HTTP authentication - ProxyPass "unix:/var/run/php/php7.2-fpm.sock|fcgi://localhost/usr/share/arsse" - - diff --git a/dist/apache/arsse-loc.conf b/dist/apache/arsse-loc.conf new file mode 100644 index 00000000..5b7a92ec --- /dev/null +++ b/dist/apache/arsse-loc.conf @@ -0,0 +1,41 @@ +# Nextcloud News version list + + ProxyPass ${ARSSE_PROXY} + + +# Nextcloud News protocol + + ProxyPass ${ARSSE_PROXY} + + +# Tiny Tiny RSS protocol + + ProxyPass ${ARSSE_PROXY} + + +# Tiny Tiny RSS feed icons + + ProxyPass ${ARSSE_PROXY} + + +# NOTE: The DocumentRoot directive will dictate whether TT-RSS static images are served correctly + +# Fever protocol + + ProxyPass ${ARSSE_PROXY} + + +# Miniflux protocol + + ProxyPass ${ARSSE_PROXY} + + +# Miniflux version number + + ProxyPass ${ARSSE_PROXY} + + +# Miniflux "health check" + + ProxyPass ${ARSSE_PROXY} + diff --git a/dist/apache/arsse.conf b/dist/apache/arsse.conf new file mode 100644 index 00000000..469dc8ec --- /dev/null +++ b/dist/apache/arsse.conf @@ -0,0 +1,10 @@ +Define ARSSE_CONF "/etc/arsse/apache/" +Define ARSSE_DATA "/usr/share/arsse/" +Define ARSSE_PROXY "unix:/var/run/php/arsse.sock|fcgi://localhost${ARSSE_DATA}" + +DocumentRoot "${ARSSE_DATA}www" + +ProxyPreserveHost On +ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "${ARSSE_DATA}arsse.php" + +Include "${ARSSE_CONF}arsse-loc.conf" diff --git a/dist/apache/example.conf b/dist/apache/example.conf new file mode 100644 index 00000000..0e1f356f --- /dev/null +++ b/dist/apache/example.conf @@ -0,0 +1,9 @@ + + ServerName "news.example.com" + SSLEngine On + + SSLCertificateFile "/etc/letsencrypt/live/news.example.com/fullchain.pem" + SSLCertificateKeyFile "/etc/letsencrypt/live/news.example.com/privkey.pem" + + Include "/etc/arsse/apache/arsse.conf" + diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index 8d1f2fb5..c9c07c14 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -35,7 +35,7 @@ package() { cp dist/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" cp dist/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" - cp -r dist/nginx config.defaults.php "$pkgdir/etc/webapps/arsse" + cp -r dist/nginx dist/apache config.defaults.php "$pkgdir/etc/webapps/arsse" cd "$pkgdir" # adjust permissions, just in case chmod -R u=rwX,g=rX,o=rX * @@ -46,7 +46,7 @@ package() { install -Dm755 dist/arsse "$pkgdir/usr/bin" install -Dm640 dist/arch/config.php "$pkgdir/etc/webapps/arsse" # patch generic configuration files to use Arch-specific paths and identifiers - sed -ise 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"* - sed -ise 's/\/var\/run\/php\//\/run\/php-fpm\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf" + sed -ise 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"* + sed -ise 's/\/var\/run\/php\//\/run\/php-fpm\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf" sed -ise 's/www-data/http/' "$pkgdir/etc/php/php-fpm.d/arsse.conf" } From 6c750d2dc0b4494c15bdcf381ae570b2f35517a4 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 19 May 2021 23:03:10 -0400 Subject: [PATCH 240/366] Documentation for installing on Arch Documentations for Debian still needs to be amended --- .../020_Getting_Started/010_Requirements.md | 2 +- .../010_On_Arch_Linux.md | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md diff --git a/docs/en/020_Getting_Started/010_Requirements.md b/docs/en/020_Getting_Started/010_Requirements.md index 7b3b6ef1..4f528298 100644 --- a/docs/en/020_Getting_Started/010_Requirements.md +++ b/docs/en/020_Getting_Started/010_Requirements.md @@ -11,6 +11,6 @@ The Arsse has the following requirements: - [curl](http://php.net/manual/en/book.curl.php) (optional) - Privileges either to create and run systemd services, or to run cron jobs -Instructions for how to satisfy the PHP extension requirements for Debian systems are included in the next section. +Instructions for how to satisfy the PHP extension requirements for Debian and Arch Linux systems are included in the next section. It is also be possible to run The Arsse on other operating systems (including Windows) and with other Web servers, but the configuration required to do so is not documented in this manual. diff --git a/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md b/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md new file mode 100644 index 00000000..0f8ce1e6 --- /dev/null +++ b/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md @@ -0,0 +1,31 @@ +_[TOC] + +# Downloading The Arse + +The Arsse is available from the [Arch User Repository](https://aur.archlinux.org/) as packages `arsse` and `arsse-git`. The latter should normally only be used to test bug fixes. + +Generic release tarballs may also be downloaded [from our Web site](https://thearsse.com). The `PKGBUILD` file (found under `arsse/dist/arch/`) can then be extracted alongside the tarball and used to build the `arsse` package. + +# Installation + +For illustrative purposes, this document assumes the `yay` [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) will be used to download, build, and install The Arsse. This section summarises the steps necessary to configure and use The Arsse after installtion: + +```sh +# Install the package +sudo yay -S arsse +# Enable the necessary PHP extensions; curl is optional but recommended; pdo_sqlite may be used instead of sqlite, but this is not recommended +sudo sed -ie 's/^;\(extension=\(curl\|iconv\|intl\|sqlite3\)\)$/\1/' /etc/php/php.ini +# Enable the necessary systemd units +sudo systemctl enable php-fpm arsse +sudo systemctl restart php-fpm arsse +``` + +Note that the above is the most concise process, not necessarily the recommended one. In particular [it is recommended](https://wiki.archlinux.org/title/PHP#Extensions) to use `/etc/php/conf.d/` to enable extensions rather than editing `php.ini` as done above. + +# Next steps + +If using a database other than SQLite, you will likely want to [set it up](Database_Setup) before doing anything else. + +In order for the various synchronization protocols to work, a Web server [must be configured](Web_Server_Configuration), and in order for The Arsse to serve users, those users [must be created](/en/Using_The_Arsse/Managing_Users). + +You may also want to review the `config.defaults.php` file included in the download package and create [a configuration file](Configuration), though The Arsse can function even without using a configuration file. \ No newline at end of file From 3f401f1cfa7c4625145b9fef32e7143833f42405 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 19 May 2021 23:10:01 -0400 Subject: [PATCH 241/366] Fix typo --- docs/en/020_Getting_Started/020_Download_and_Installation.md | 2 +- .../020_Download_and_Installation/010_On_Arch_Linux.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/020_Getting_Started/020_Download_and_Installation.md b/docs/en/020_Getting_Started/020_Download_and_Installation.md index ed47a7d1..19f81082 100644 --- a/docs/en/020_Getting_Started/020_Download_and_Installation.md +++ b/docs/en/020_Getting_Started/020_Download_and_Installation.md @@ -1,6 +1,6 @@ [TOC] -# Downloading The Arse +# Downloading The Arsse The latest version of The Arsse can be downloaded [from our Web site](https://thearsse.com/). If installing an older release from our archives, the attachments named _arsse-x.x.x.tar.gz_ should be used rather than those marked "Source Code". diff --git a/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md b/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md index 0f8ce1e6..333fd9b4 100644 --- a/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md +++ b/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md @@ -1,6 +1,6 @@ _[TOC] -# Downloading The Arse +# Downloading The Arsse The Arsse is available from the [Arch User Repository](https://aur.archlinux.org/) as packages `arsse` and `arsse-git`. The latter should normally only be used to test bug fixes. From 61eb4a252e06ce148d855abfccc7ff26f4550a52 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 20 May 2021 09:05:00 -0400 Subject: [PATCH 242/366] Fix doc URLs --- .../020_Download_and_Installation/010_On_Arch_Linux.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md b/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md index 333fd9b4..fa34b6e1 100644 --- a/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md +++ b/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md @@ -24,8 +24,8 @@ Note that the above is the most concise process, not necessarily the recommended # Next steps -If using a database other than SQLite, you will likely want to [set it up](Database_Setup) before doing anything else. +If using a database other than SQLite, you will likely want to [set it up](en/Getting_Started/Database_Setup) before doing anything else. -In order for the various synchronization protocols to work, a Web server [must be configured](Web_Server_Configuration), and in order for The Arsse to serve users, those users [must be created](/en/Using_The_Arsse/Managing_Users). +In order for the various synchronization protocols to work, a Web server [must be configured](en/Getting_Started/Web_Server_Configuration), and in order for The Arsse to serve users, those users [must be created](/en/Using_The_Arsse/Managing_Users). -You may also want to review the `config.defaults.php` file included in the download package and create [a configuration file](Configuration), though The Arsse can function even without using a configuration file. \ No newline at end of file +You may also want to review the `config.defaults.php` file included in `/etc/webapps/arsse/` or consult [the documentation for the configuration file](en/Getting_Started/Configuration), though The Arsse should function with the default configuration. \ No newline at end of file From 2260b7cc50ec27f1fc2ed3f5b5f473653ebc9198 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 20 May 2021 10:20:08 -0400 Subject: [PATCH 243/366] Back up all Web server configuration --- dist/arch/PKGBUILD | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index c9c07c14..259158f1 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -14,7 +14,15 @@ optdepends=("nginx: HTTP server" "percona-server: Alternate database" "postgresql: Alternate database" "php-pgsql: PostgreSQL database support") -backup=("etc/webapps/arsse/config.php" "etc/php/php-fpm.d/arsse.conf") +backup=("etc/webapps/arsse/config.php" + "etc/php/php-fpm.d/arsse.conf" + "etc/webapps/arsse/nginx/example.conf" + "etc/webapps/arsse/nginx/arsse.conf" + "etc/webapps/arsse/nginx/arsse-loc.conf" + "etc/webapps/arsse/nginx/arsse-fcgi.conf" + "etc/webapps/arsse/apache/example.conf" + "etc/webapps/arsse/apache/arsse.conf" + "etc/webapps/arsse/apache/arsse-loc.conf") install= changelog= source=("arsse-0.9.1.tar.gz") From 3f3f449da17a6220a27909789b677a0e1aa2d13b Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 20 May 2021 10:20:32 -0400 Subject: [PATCH 244/366] Re-organize manual --- .../010_On_Arch_Linux.md | 24 ++-- .../020_On_Debian_and_Ubuntu.md} | 22 +++- .../030_Web_Server_Configuration.md | 124 ------------------ 3 files changed, 33 insertions(+), 137 deletions(-) rename docs/en/020_Getting_Started/{020_Download_and_Installation.md => 020_Download_and_Installation/020_On_Debian_and_Ubuntu.md} (59%) delete mode 100644 docs/en/020_Getting_Started/030_Web_Server_Configuration.md diff --git a/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md b/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md index fa34b6e1..191af00d 100644 --- a/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md +++ b/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md @@ -1,10 +1,10 @@ -_[TOC] +[TOC] # Downloading The Arsse -The Arsse is available from the [Arch User Repository](https://aur.archlinux.org/) as packages `arsse` and `arsse-git`. The latter should normally only be used to test bug fixes. +Since version 0.9.2 The Arsse is available from the [Arch User Repository](https://aur.archlinux.org/) as packages `arsse` and `arsse-git`. The latter should normally only be used to test bug fixes. -Generic release tarballs may also be downloaded [from our Web site](https://thearsse.com). The `PKGBUILD` file (found under `arsse/dist/arch/`) can then be extracted alongside the tarball and used to build the `arsse` package. +Generic release tarballs may also be downloaded [from our Web site](https://thearsse.com), and the `PKGBUILD` file (found under `arsse/dist/arch/`) can then be extracted alongside the tarball and used to build the `arsse` package. Installing directly from the generic release tarball without producing an Arch package is not recommended as the package-building process performs various adjustments to handle Arch peculiarities. # Installation @@ -13,19 +13,25 @@ For illustrative purposes, this document assumes the `yay` [AUR helper](https:// ```sh # Install the package sudo yay -S arsse -# Enable the necessary PHP extensions; curl is optional but recommended; pdo_sqlite may be used instead of sqlite, but this is not recommended +# Enable the necessary PHP extensions; curl is optional but recommended; pdo_sqlite may be used instead of sqlite3, but this is not recommended sudo sed -ie 's/^;\(extension=\(curl\|iconv\|intl\|sqlite3\)\)$/\1/' /etc/php/php.ini -# Enable the necessary systemd units +# Enable and start the necessary systemd units sudo systemctl enable php-fpm arsse sudo systemctl restart php-fpm arsse ``` -Note that the above is the most concise process, not necessarily the recommended one. In particular [it is recommended](https://wiki.archlinux.org/title/PHP#Extensions) to use `/etc/php/conf.d/` to enable extensions rather than editing `php.ini` as done above. +Note that the above is the most concise process, not necessarily the recommended one. In particular [it is recommended](https://wiki.archlinux.org/title/PHP#Extensions) to use `/etc/php/conf.d/` to enable PHP extensions rather than editing `php.ini` as done above. + +The PHP extensions listed in [the requirements](/en/Getting_Started/Requirements) not mentioned above are compiled into Arch's PHP binaries and thus always enabled. + +# Web server configuration + +Sample configuration for both Nginx and Apache HTTPd can be found in `/etc/webapps/arsse/nginx/` and `/etc/webapps/arsse/apache/`, respectively. The `example.conf` files are basic virtual host examples; the other files they include should normally be usable without modification, but may be modified if desired. # Next steps -If using a database other than SQLite, you will likely want to [set it up](en/Getting_Started/Database_Setup) before doing anything else. +If using a database other than SQLite, you will likely want to [set it up](/en/Getting_Started/Database_Setup) before doing anything else. -In order for the various synchronization protocols to work, a Web server [must be configured](en/Getting_Started/Web_Server_Configuration), and in order for The Arsse to serve users, those users [must be created](/en/Using_The_Arsse/Managing_Users). +In order for The Arsse to serve users, those users [must be created](/en/Using_The_Arsse/Managing_Users). -You may also want to review the `config.defaults.php` file included in `/etc/webapps/arsse/` or consult [the documentation for the configuration file](en/Getting_Started/Configuration), though The Arsse should function with the default configuration. \ No newline at end of file +You may also want to review the `config.defaults.php` file included in `/etc/webapps/arsse/` or consult [the documentation for the configuration file](/en/Getting_Started/Configuration), though The Arsse should function with the default configuration. \ No newline at end of file diff --git a/docs/en/020_Getting_Started/020_Download_and_Installation.md b/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md similarity index 59% rename from docs/en/020_Getting_Started/020_Download_and_Installation.md rename to docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md index 19f81082..e753d438 100644 --- a/docs/en/020_Getting_Started/020_Download_and_Installation.md +++ b/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md @@ -8,7 +8,7 @@ Installation from source code is also possible, but the release packages are rec # Installation -In order for The Arsse to function correctly, [its requirements](Requirements) must first be satisfied. The process of installing the required PHP extensions differs from one system to the next, but on Debian the following series of commands should do: +In order for The Arsse to function correctly, its requirements must first be satisfied. The following series of commands should do so: ```sh # Install PHP; this assumes the FastCGI process manager will be used @@ -30,12 +30,26 @@ sudo chown -R www-data:www-data "/usr/share/arsse" sudo chmod o+rwX "/usr/share/arsse" ``` +# Web server configuration + +Sample configuration for both Nginx and Apache HTTPd can be found in `dist/nginx/` and `dist/apache/`, respectively. The `example.conf` files are basic virtual host examples; the other files they include should normally be usable without modification, but may be modified if desired. + +In order to use Apache HTTPd the FastCGI proxy module must be enabled and the server restarted: + +```sh +sudo a2enmod proxy proxy_fcgi +sudo systemctl restart apache2 +``` + +No additional set-up is required for Nginx. + + # Next steps -If using a database other than SQLite, you will likely want to [set it up](Database_Setup) before doing anything else. +If using a database other than SQLite, you will likely want to [set it up](/en/Getting_Started/Database_Setup) before doing anything else. -In order for the various synchronization protocols to work, a Web server [must be configured](Web_Server_Configuration), and in order for The Arsse to serve users, those users [must be created](/en/Using_The_Arsse/Managing_Users). +In order for The Arsse to serve users, those users [must be created](/en/Using_The_Arsse/Managing_Users). -You may also want to review the `config.defaults.php` file included in the download package and create [a configuration file](Configuration), though The Arsse can function even without using a configuration file. +You may also want to review the `config.defaults.php` file included in the download package and create [a configuration file](/en/Getting_Started/Configuration), though The Arsse can function even without using a configuration file. Finally, The Arsse's [newsfeed refreshing service](/en/Using_The_Arsse/Keeping_Newsfeeds_Up_to_Date) needs to be installed in order for news to actually be fetched from the Internet. diff --git a/docs/en/020_Getting_Started/030_Web_Server_Configuration.md b/docs/en/020_Getting_Started/030_Web_Server_Configuration.md deleted file mode 100644 index 8f0371e9..00000000 --- a/docs/en/020_Getting_Started/030_Web_Server_Configuration.md +++ /dev/null @@ -1,124 +0,0 @@ -[TOC] - -# Preface - -As a PHP application, The Arsse requires the aid of a Web server in order to communicate with clients. How to install and configure a Web server in general is outside the scope of this document, but this section provides examples and advice for Web server configuration specific to The Arsse. Any server capable of interfacing with PHP should work, though we have only tested Nginx and Apache 2.4. - -Samples included here only cover the bare minimum for configuring a virtual host. In particular, configuration for HTTPS (which is highly recommended) is omitted for the sake of clarity - -# Configuration for Nginx - -```nginx -server { - server_name example.com; - listen 80; # adding HTTPS configuration is highly recommended - root /usr/share/arsse/www; # adjust according to your installation path - - location / { - try_files $uri $uri/ =404; - } - - location @arsse { - fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; # adjust according to your system configuration - fastcgi_pass_header Authorization; # required if the Arsse is to perform its own HTTP authentication - fastcgi_pass_request_body on; - fastcgi_pass_request_headers on; - fastcgi_intercept_errors off; - fastcgi_buffering off; - fastcgi_param SCRIPT_FILENAME /usr/share/arsse/arsse.php; # adjust according to your installation path - fastcgi_param REQUEST_METHOD $request_method; - fastcgi_param CONTENT_TYPE $content_type; - fastcgi_param CONTENT_LENGTH $content_length; - fastcgi_param REQUEST_URI $uri; - fastcgi_param QUERY_STRING $query_string; - fastcgi_param HTTPS $https if_not_empty; - fastcgi_param REMOTE_USER $remote_user; - } - - # Nextcloud News protocol - location /index.php/apps/news/api { - try_files $uri @arsse; - - location ~ ^/index\.php/apps/news/api/?$ { - # this path should not be behind HTTP authentication - try_files $uri @arsse; - } - } - - # Tiny Tiny RSS protocol - location /tt-rss/api { - try_files $uri @arsse; - } - - # Tiny Tiny RSS feed icons - location /tt-rss/feed-icons/ { - try_files $uri @arsse; - } - - # Tiny Tiny RSS special-feed icons; these are static files - location /tt-rss/images/ { - # this path should not be behind HTTP authentication - try_files $uri =404; - } - - # Fever protocol - location /fever/ { - # this path should not be behind HTTP authentication - try_files $uri @arsse; - } - - # Miniflux protocol - location /v1/ { - try_files $uri @arsse; - } - - # Miniflux version number - location /version { - # this path should not be behind HTTP authentication - try_files $uri @arsse; - } - - # Miniflux "health check" - location /healthcheck { - # this path should not be behind HTTP authentication - try_files $uri @arsse; - } -} -``` - -# Configuration for Apache2 - -There are many ways for Apache to interface with PHP, but the recommended way today is to use `mod_proxy` and `mod_proxy_fcgi` to communicate with PHP-FPM. If necessary you can enable these modules on Debian systems using the following commands: - -```sh -sudo a2enmod proxy proxy_fcgi -sudo systemctl restart apache2 -``` - -Afterward the follow virtual host configuration should work, after modifying path as appropriate: - -```apache -# N.B. the unix:/var/run/php/php7.2-fpm.sock path used repeatedly below will -# vary from system to system and will be probably need to be changed - - - ServerName localhost - # adjust according to your installation path - DocumentRoot /usr/share/arsse/www - - # adjust according to your installation path - ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/usr/share/arsse/arsse.php" - ProxyPreserveHost On - - # Nextcloud News v1.2, Tiny Tiny RSS API, TT-RSS newsfeed icons, Miniflux API - - ProxyPass "unix:/var/run/php/php7.2-fpm.sock|fcgi://localhost/usr/share/arsse" - - - # Nextcloud News API detection, Fever API, Miniflux miscellanies - - # these locations should not be behind HTTP authentication - ProxyPass "unix:/var/run/php/php7.2-fpm.sock|fcgi://localhost/usr/share/arsse" - - -``` From 073f6b3c39f9beafc67d0fd62549928289f8d194 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 20 May 2021 17:47:02 -0400 Subject: [PATCH 245/366] Prototype Debian control file and other changes --- RoboFile.php | 64 ++++++++++++++++++++++++++++++--------------- dist/arch/PKGBUILD | 3 ++- dist/debian/control | 32 +++++++++++++++++++++++ 3 files changed, 77 insertions(+), 22 deletions(-) create mode 100644 dist/debian/control diff --git a/RoboFile.php b/RoboFile.php index 8bb5991e..4ad7e3c5 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -158,7 +158,7 @@ class RoboFile extends \Robo\Tasks { public function package(string $version = null): Result { // establish which commit to package $version = $version ?? $this->askDefault("Commit to package:", "HEAD"); - $archive = BASE."arsse-$version.tar.gz"; + $archive = "arsse-$version.tar.gz"; // start a collection $t = $this->collectionBuilder(); // create a temporary directory @@ -215,30 +215,20 @@ class RoboFile extends \Robo\Tasks { return $t->run(); } - /** Packages a given commit of the software into an Arch package - * - * The version to package may be any Git tree-ish identifier: a tag, a branch, - * or any commit hash. If none is provided on the command line, Robo will prompt - * for a commit to package; the default is "HEAD". - */ - public function packageArch(string $version = null): Result { - // establish which commit to package - $version = $version ?? $this->askDefault("Commit to package:", "HEAD"); - $archive = BASE."arsse-$version.tar.gz"; + /** Packages a release tarball into an Arch package */ + public function packageArch(string $tarball): Result { + $dir = dirname($tarball); // start a collection $t = $this->collectionBuilder(); - // create a tarball - $t->addCode(function() use ($version) { - return $this->package($version); - }); - // extract the PKGBUILD from the just-created archive and build it - $t->addCode(function() use ($archive) { + // extract the PKGBUILD from the tarball + $t->addCode(function() use ($tarball, $dir) { // because Robo doesn't support extracting a single file we have to do it ourselves - (new \Archive_Tar($archive))->extractList("arsse/dist/arch/PKGBUILD", BASE, "arsse/dist/arch/", false); + (new \Archive_Tar($tarball))->extractList("arsse/dist/arch/PKGBUILD", $dir,"arsse/dist/arch/", false); // perform a do-nothing filesystem operation since we need a Robo task result - return $this->taskFilesystemStack()->chmod(BASE."PKGBUILD", 0644)->run(); - })->completion($this->taskFilesystemStack()->remove(BASE."PKGBUILD")); - $t->taskExec("makepkg -Ccf")->dir(BASE); + return $this->taskFilesystemStack()->chmod("PKGBUILD", 0644)->dir($dir)->run(); + })->completion($this->taskFilesystemStack()->remove("PKGBUILD")->dir($dir)); + // build the package + $t->taskExec("makepkg -Ccf")->dir($dir); return $t->run(); } @@ -285,4 +275,36 @@ class RoboFile extends \Robo\Tasks { // execute the collection return $t->run(); } + + protected function parseChangelog(string $text, string $targetVersion): array { + $baseVersion = preg_replace('/^(\d+(?:\.\d+)*).*/', "$1", $targetVersion); + $lines = preg_split('/[\r\n]+/', trim($text)); + $version = ""; + $section = ""; + $out = []; + $l = 0; + $expected = "version"; + for ($a = 0; $a < sizeof($lines);) { + $l = rtrim($lines[$a++]); + Process: + if (in_array($expected, ["version", "section"]) && preg_match('/^Version (\d+(?:\.\d+)*) \(([\d\?]{4}-[\d\?]{2}-[\d\?]{2})\)\s*$/', $l, $m)) { + $version = $m[1]; + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $m[2])) { + // uncertain dates are allowed only for the top version, and only if it does not match the target version (otherwise we have forgot to set the correct date before tagging) + if (!$out && $targetVersion !== $version) { + // use today's date; local time is fine + $date = date("Y-m-d"); + } else { + throw new \Exception("CHANGELOG: Date at line $a is incomplete"); + } + } else { + $date = $m[2]; + } + $out[$version] = ['date' => $date, 'features' => [], 'fixes' => [], 'changes' => []]; + $expected = "separator"; + } elseif ($expected === "separator" && $length = strlen($lines[$a - 2]) && preg_match('/^={'.$length.'}$/', $l)) { + // verify that the next line is blank + } + } + } } diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index 259158f1..5f7154b5 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -2,7 +2,7 @@ pkgname="arsse" pkgver=0.9.1 pkgrel=1 epoch= -pkgdesc="RSS/Atom newsfeed synchronization server" +pkgdesc="Multi-protocol RSS/Atom newsfeed synchronization server" arch=("any") url="https://thearsse.com/" license=("MIT") @@ -57,4 +57,5 @@ package() { sed -ise 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"* sed -ise 's/\/var\/run\/php\//\/run\/php-fpm\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf" sed -ise 's/www-data/http/' "$pkgdir/etc/php/php-fpm.d/arsse.conf" + sed -ie 's/^WorkingDirectory=.*$/WorkingDirectory=\/usr\/share\/webapps\/arsse/' -e 's/^ConfigurationDirectory=.*$/ConfigurationDirectory=webapps\/arsse/' "$pkgdir/usr/lib/systemd/system/arsse-fetch.service" } diff --git a/dist/debian/control b/dist/debian/control new file mode 100644 index 00000000..99221ddf --- /dev/null +++ b/dist/debian/control @@ -0,0 +1,32 @@ +Source: arsse +Maintainer: J. King +Section: contrib/net +Priority: optional +Standards-Version: 4.5.1 +Homepage: https://thearsse.com/ +Vcs-Browser: https://code.mensbeam.com/MensBeam/arsse/ +Vcs-Git: https://code.mensbeam.com/MensBeam/arsse/ + +Package: arsse +Architecture: all +Section: contrib/net +Priority: optional +Essential: no +Homepage: https://thearsse.com/ +Description: Multi-protocol RSS/Atom newsfeed synchronization server + The Arsse bridges the gap between multiple existing newsfeed aggregator + client protocols such as Tiny Tiny RSS, Nextcloud News and Miniflux, + allowing you to use compatible clients for many protocols with a single + server. +Depends: ${misc:Depends}, + dbconfig-mysql | dbconfig-pgsql | dbconfig-sqlite3 | dbconfig-no-thanks, + php (>= 7.1.0), + php-cli, + php-intl, + php-json, + php-xml, + php-sqlite3 | php-mysql | php-pgsql +Recommends: apache2 | nginx, + php-fpm, + php-curl, + ca-certificates From 16174f11b6523d361e80f87a27b2fbfb34df5f14 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 20 May 2021 23:38:03 -0400 Subject: [PATCH 246/366] Add changelog parsing to packaging task --- CHANGELOG | 5 +- RoboFile.php | 189 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 144 insertions(+), 50 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b0f89fc2..eea0e88b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -129,7 +129,7 @@ Bug fixes: Version 0.6.1 (2019-01-23) ========================== -Bug Fixes: +Bug fixes: - Unify SQL timeout settings - Correctly escape shell command in subprocess service driver - Correctly allow null time intervals in configuration when appropriate @@ -249,4 +249,5 @@ Bug fixes: Version 0.1.0 (2017-08-29) ========================== -Initial release +New features: +- Initial release diff --git a/RoboFile.php b/RoboFile.php index 4ad7e3c5..b2938440 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -157,34 +157,39 @@ class RoboFile extends \Robo\Tasks { */ public function package(string $version = null): Result { // establish which commit to package - $version = $version ?? $this->askDefault("Commit to package:", "HEAD"); - $archive = "arsse-$version.tar.gz"; + $commit = $version ?? $this->askDefault("Commit to package:", "HEAD"); // start a collection $t = $this->collectionBuilder(); // create a temporary directory $dir = $t->tmpDir().\DIRECTORY_SEPARATOR; // create a Git worktree for the selected commit in the temp location - $t->taskExec("git worktree add ".escapeshellarg($dir)." ".escapeshellarg($version)) - ->completion($this->taskFilesystemStack()->remove($dir)) - ->completion($this->taskExec("git worktree prune")); + $result = $this->taskExec("git worktree add ".escapeshellarg($dir)." ".escapeshellarg($commit))->run(); + if ($result->getExitCode() > 0) { + return $result; + } + // get useable version strings from Git + $version = trim(`git -C "$dir" describe --tags`); + $archVersion = preg_replace('/^([^-]+)-(\d+)-(\w+)$/', "$1.r$2.$3", $version); + // name the generic release tarball + $tarball = "arsse-$version.tar.gz"; + // generate the Debian changelog; this also validates our original changelog + $debianChangelog = changelogDebian(changelogParse(file_get_contents($dir."CHANGELOG"), $version), $version); + // save commit description to VERSION file for use by packaging + $t->addTask($this->taskWriteToFile($dir."VERSION")->text($version)); + // save the Debian changelog + $t->addTask($this->taskWriteToFile($dir."dist/debian/changelog")->text($debianChangelog)); // patch the Arch PKGBUILD file with the correct version string - $t->addCode(function () use ($dir) { - $ver = trim(preg_replace('/^([^-]+)-(\d+)-(\w+)$/', "$1.r$2.$3", `git -C "$dir" describe --tags`)); - return $this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^pkgver=.*$/m')->to("pkgver=$ver")->run(); - }); + $t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^pkgver=.*$/m')->to("pkgver=$archVersion")); // patch the Arch PKGBUILD file with the correct source file - $t->addCode(function () use ($dir, $archive) { - $tar = basename($archive); - return $this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^source=\("arsse-[^"]+"\)$/m')->to("source=(\"$tar\")")->run(); - }); + $t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^source=\("arsse-[^"]+"\)$/m')->to('source=("'.basename($tarball).'")')); // perform Composer installation in the temp location with dev dependencies - $t->taskComposerInstall()->dir($dir); + $t->addTask($this->taskComposerInstall()->arg("-q")->dir($dir)); // generate the manual - $t->taskExec(escapeshellarg($dir."robo")." manual")->dir($dir); + $t->addTask($this->taskExec(escapeshellarg($dir."robo")." manual")->dir($dir)); // perform Composer installation in the temp location for final output - $t->taskComposerInstall()->dir($dir)->noDev()->optimizeAutoloader()->arg("--no-scripts"); + $t->addTask($this->taskComposerInstall()->dir($dir)->noDev()->optimizeAutoloader()->arg("--no-scripts")); // delete unwanted files - $t->taskFilesystemStack()->remove([ + $t->addTask($this->taskFilesystemStack()->remove([ $dir.".git", $dir.".gitignore", $dir.".gitattributes", @@ -204,15 +209,19 @@ class RoboFile extends \Robo\Tasks { $dir."package.json", $dir."yarn.lock", $dir."postcss.config.js", - ]); + ])); // generate a sample configuration file - $t->taskExec(escapeshellarg(\PHP_BINARY)." arsse.php conf save-defaults config.defaults.php")->dir($dir); + $t->addTask($this->taskExec(escapeshellarg(\PHP_BINARY)." arsse.php conf save-defaults config.defaults.php")->dir($dir)); // remove any existing archive - $t->taskFilesystemStack()->remove($archive); + $t->addTask($this->taskFilesystemStack()->remove($tarball)); // package it all up - $t->taskPack($archive)->addDir("arsse", $dir); + $t->addTask($this->taskPack($tarball)->addDir("arsse", $dir)); // execute the collection - return $t->run(); + $result = $t->run(); + // remove the Git worktree + $this->taskFilesystemStack()->remove($dir)->run(); + $this->taskExec("git worktree prune")->run(); + return $result; } /** Packages a release tarball into an Arch package */ @@ -276,35 +285,119 @@ class RoboFile extends \Robo\Tasks { return $t->run(); } - protected function parseChangelog(string $text, string $targetVersion): array { - $baseVersion = preg_replace('/^(\d+(?:\.\d+)*).*/', "$1", $targetVersion); - $lines = preg_split('/[\r\n]+/', trim($text)); - $version = ""; - $section = ""; - $out = []; - $l = 0; - $expected = "version"; - for ($a = 0; $a < sizeof($lines);) { - $l = rtrim($lines[$a++]); - Process: - if (in_array($expected, ["version", "section"]) && preg_match('/^Version (\d+(?:\.\d+)*) \(([\d\?]{4}-[\d\?]{2}-[\d\?]{2})\)\s*$/', $l, $m)) { - $version = $m[1]; - if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $m[2])) { - // uncertain dates are allowed only for the top version, and only if it does not match the target version (otherwise we have forgot to set the correct date before tagging) - if (!$out && $targetVersion !== $version) { - // use today's date; local time is fine - $date = date("Y-m-d"); - } else { - throw new \Exception("CHANGELOG: Date at line $a is incomplete"); - } + public function changelog() { + echo changelogDebian(changelogParse(file_get_contents("CHANGELOG"), "0.9.1-r26"), "0.9.1-r26"); + } +} + +function changelogParse(string $text, string $targetVersion): array { + $lines = preg_split('/\r?\n/', $text); + $version = ""; + $section = ""; + $out = []; + $entry = []; + $expected = ["version"]; + for ($a = 0; $a < sizeof($lines);) { + $l = rtrim($lines[$a++]); + if (in_array("version", $expected) && preg_match('/^Version (\d+(?:\.\d+)*) \(([\d\?]{4}-[\d\?]{2}-[\d\?]{2})\)\s*$/', $l, $m)) { + $version = $m[1]; + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $m[2])) { + // uncertain dates are allowed only for the top version, and only if it does not match the target version (otherwise we have forgotten to set the correct date before tagging) + if (!$out && $targetVersion !== $version) { + // use today's date; local time is fine + $date = date("Y-m-d"); } else { - $date = $m[2]; + throw new \Exception("CHANGELOG: Date at line $a is incomplete"); } - $out[$version] = ['date' => $date, 'features' => [], 'fixes' => [], 'changes' => []]; - $expected = "separator"; - } elseif ($expected === "separator" && $length = strlen($lines[$a - 2]) && preg_match('/^={'.$length.'}$/', $l)) { - // verify that the next line is blank + } else { + $date = $m[2]; + } + if ($entry) { + $out[] = $entry; + } + $entry = ['version' => $version, 'date' => $date, 'features' => [], 'fixes' => [], 'changes' => []]; + $expected = ["separator"]; + } elseif (in_array("separator", $expected) && preg_match('/^=+/', $l)) { + $length = strlen($lines[$a - 2]); + if (strlen($l) !== $length) { + throw new \Exception("CHANGELOG: Separator at line $a is of incorrect length"); + } + $expected = ["blank line"]; + $section = ""; + } elseif (in_array("blank line", $expected) && $l === "") { + $expected = [ + '' => ["features section", "fixes section", "changes section"], + 'features' => ["fixes section", "changes section", "version"], + 'fixes' => ["changes section", "version"], + 'changes' => ["version"], + ][$section]; + $expected[] = "end-of-file"; + } elseif (in_array("features section", $expected) && $l === "New features:") { + $section = "features"; + $expected = ["item"]; + } elseif (in_array("fixes section", $expected) && $l === "Bug fixes:") { + $section = "fixes"; + $expected = ["item"]; + } elseif (in_array("changes section", $expected) && $l === "Changes:") { + $section = "changes"; + $expected = ["item"]; + } elseif (in_array("item", $expected) && preg_match('/^- (\w.*)$/', $l, $m)) { + $entry[$section][] = $m[1]; + $expected = ["item", "continuation", "blank line"]; + } elseif (in_array("continuation", $expected) && preg_match('/^ (\w.*)$/', $l, $m)) { + $last = sizeof($entry[$section]) - 1; + $entry[$section][$last] .= "\n".$m[1]; + } else { + if (sizeof($expected) > 1) { + throw new \Exception("CHANGELOG: Expected one of [".implode(", ", $expected)."] at line $a"); + } else { + throw new \Exception("CHANGELOG: Expected ".$expected[0]." at line $a"); } } } + if (!in_array("end-of-file", $expected)) { + if (sizeof($expected) > 1) { + throw new \Exception("CHANGELOG: Expected one of [".implode(", ", $expected)."] at end of file"); + } else { + throw new \Exception("CHANGELOG: Expected ".$expected[0]." at end of file"); + } + } + $out[] = $entry; + return $out; } + +function changelogDebian(array $log, string $targetVersion): string { + $latest = $log[0]['version']; + $baseVersion = preg_replace('/^(\d+(?:\.\d+)*).*/', "$1", $targetVersion); + if ($baseVersion !== $targetVersion && version_compare($latest, $baseVersion, ">")) { + // if the changelog contains an entry for a future version, change its version number to match the target version instead of using the future version + $log[0]['version'] = $targetVersion; + } else { + // otherwise synthesize a changelog entry for the changes since the last tag + array_unshift($log, ['version' => $targetVersion, 'date' => date("Y-m-d"), 'features' => [], 'fixes' => [], 'changes' => ["Unspecified changes"]]); + } + $out = ""; + foreach ($log as $entry) { + $out .= "arsse (".$entry['version']."-1) unstable; urgency=low\n"; + if ($entry['features']) { + $out .= "\n [ New features ]\n"; + foreach ($entry['features'] as $item) { + $out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n"; + } + } + if ($entry['fixes']) { + $out .= "\n [ Bug fixes ]\n"; + foreach ($entry['fixes'] as $item) { + $out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n"; + } + } + if ($entry['changes']) { + $out .= "\n [ Other changes ]\n"; + foreach ($entry['changes'] as $item) { + $out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n"; + } + } + $out .= "\n -- The Arsse team ".\DateTimeImmutable::createFromFormat("Y-m-d", $entry['date'], new \DateTimeZone("UTC"))->format("D, d M Y")." 00:00:00 +0000\n\n"; + } + return $out; +} \ No newline at end of file From 38cb1059b2482b3bce452badeca307ca7ea5987d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 20 May 2021 23:53:25 -0400 Subject: [PATCH 247/366] Shorten output of packaging task --- RoboFile.php | 202 +++++++++++++++++++++++++-------------------------- 1 file changed, 100 insertions(+), 102 deletions(-) diff --git a/RoboFile.php b/RoboFile.php index b2938440..f8b1c530 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -173,7 +173,7 @@ class RoboFile extends \Robo\Tasks { // name the generic release tarball $tarball = "arsse-$version.tar.gz"; // generate the Debian changelog; this also validates our original changelog - $debianChangelog = changelogDebian(changelogParse(file_get_contents($dir."CHANGELOG"), $version), $version); + $debianChangelog = $this->changelogDebian($this->changelogParse(file_get_contents($dir."CHANGELOG"), $version), $version); // save commit description to VERSION file for use by packaging $t->addTask($this->taskWriteToFile($dir."VERSION")->text($version)); // save the Debian changelog @@ -185,9 +185,11 @@ class RoboFile extends \Robo\Tasks { // perform Composer installation in the temp location with dev dependencies $t->addTask($this->taskComposerInstall()->arg("-q")->dir($dir)); // generate the manual - $t->addTask($this->taskExec(escapeshellarg($dir."robo")." manual")->dir($dir)); + $t->addCode(function() { + return $this->manual(["-q"]); + }); // perform Composer installation in the temp location for final output - $t->addTask($this->taskComposerInstall()->dir($dir)->noDev()->optimizeAutoloader()->arg("--no-scripts")); + $t->addTask($this->taskComposerInstall()->dir($dir)->noDev()->optimizeAutoloader()->arg("--no-scripts")->arg("-q")); // delete unwanted files $t->addTask($this->taskFilesystemStack()->remove([ $dir.".git", @@ -285,119 +287,115 @@ class RoboFile extends \Robo\Tasks { return $t->run(); } - public function changelog() { - echo changelogDebian(changelogParse(file_get_contents("CHANGELOG"), "0.9.1-r26"), "0.9.1-r26"); - } -} - -function changelogParse(string $text, string $targetVersion): array { - $lines = preg_split('/\r?\n/', $text); - $version = ""; - $section = ""; - $out = []; - $entry = []; - $expected = ["version"]; - for ($a = 0; $a < sizeof($lines);) { - $l = rtrim($lines[$a++]); - if (in_array("version", $expected) && preg_match('/^Version (\d+(?:\.\d+)*) \(([\d\?]{4}-[\d\?]{2}-[\d\?]{2})\)\s*$/', $l, $m)) { - $version = $m[1]; - if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $m[2])) { - // uncertain dates are allowed only for the top version, and only if it does not match the target version (otherwise we have forgotten to set the correct date before tagging) - if (!$out && $targetVersion !== $version) { - // use today's date; local time is fine - $date = date("Y-m-d"); + protected function changelogParse(string $text, string $targetVersion): array { + $lines = preg_split('/\r?\n/', $text); + $version = ""; + $section = ""; + $out = []; + $entry = []; + $expected = ["version"]; + for ($a = 0; $a < sizeof($lines);) { + $l = rtrim($lines[$a++]); + if (in_array("version", $expected) && preg_match('/^Version (\d+(?:\.\d+)*) \(([\d\?]{4}-[\d\?]{2}-[\d\?]{2})\)\s*$/', $l, $m)) { + $version = $m[1]; + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $m[2])) { + // uncertain dates are allowed only for the top version, and only if it does not match the target version (otherwise we have forgotten to set the correct date before tagging) + if (!$out && $targetVersion !== $version) { + // use today's date; local time is fine + $date = date("Y-m-d"); + } else { + throw new \Exception("CHANGELOG: Date at line $a is incomplete"); + } } else { - throw new \Exception("CHANGELOG: Date at line $a is incomplete"); + $date = $m[2]; } + if ($entry) { + $out[] = $entry; + } + $entry = ['version' => $version, 'date' => $date, 'features' => [], 'fixes' => [], 'changes' => []]; + $expected = ["separator"]; + } elseif (in_array("separator", $expected) && preg_match('/^=+/', $l)) { + $length = strlen($lines[$a - 2]); + if (strlen($l) !== $length) { + throw new \Exception("CHANGELOG: Separator at line $a is of incorrect length"); + } + $expected = ["blank line"]; + $section = ""; + } elseif (in_array("blank line", $expected) && $l === "") { + $expected = [ + '' => ["features section", "fixes section", "changes section"], + 'features' => ["fixes section", "changes section", "version"], + 'fixes' => ["changes section", "version"], + 'changes' => ["version"], + ][$section]; + $expected[] = "end-of-file"; + } elseif (in_array("features section", $expected) && $l === "New features:") { + $section = "features"; + $expected = ["item"]; + } elseif (in_array("fixes section", $expected) && $l === "Bug fixes:") { + $section = "fixes"; + $expected = ["item"]; + } elseif (in_array("changes section", $expected) && $l === "Changes:") { + $section = "changes"; + $expected = ["item"]; + } elseif (in_array("item", $expected) && preg_match('/^- (\w.*)$/', $l, $m)) { + $entry[$section][] = $m[1]; + $expected = ["item", "continuation", "blank line"]; + } elseif (in_array("continuation", $expected) && preg_match('/^ (\w.*)$/', $l, $m)) { + $last = sizeof($entry[$section]) - 1; + $entry[$section][$last] .= "\n".$m[1]; } else { - $date = $m[2]; + if (sizeof($expected) > 1) { + throw new \Exception("CHANGELOG: Expected one of [".implode(", ", $expected)."] at line $a"); + } else { + throw new \Exception("CHANGELOG: Expected ".$expected[0]." at line $a"); + } } - if ($entry) { - $out[] = $entry; - } - $entry = ['version' => $version, 'date' => $date, 'features' => [], 'fixes' => [], 'changes' => []]; - $expected = ["separator"]; - } elseif (in_array("separator", $expected) && preg_match('/^=+/', $l)) { - $length = strlen($lines[$a - 2]); - if (strlen($l) !== $length) { - throw new \Exception("CHANGELOG: Separator at line $a is of incorrect length"); - } - $expected = ["blank line"]; - $section = ""; - } elseif (in_array("blank line", $expected) && $l === "") { - $expected = [ - '' => ["features section", "fixes section", "changes section"], - 'features' => ["fixes section", "changes section", "version"], - 'fixes' => ["changes section", "version"], - 'changes' => ["version"], - ][$section]; - $expected[] = "end-of-file"; - } elseif (in_array("features section", $expected) && $l === "New features:") { - $section = "features"; - $expected = ["item"]; - } elseif (in_array("fixes section", $expected) && $l === "Bug fixes:") { - $section = "fixes"; - $expected = ["item"]; - } elseif (in_array("changes section", $expected) && $l === "Changes:") { - $section = "changes"; - $expected = ["item"]; - } elseif (in_array("item", $expected) && preg_match('/^- (\w.*)$/', $l, $m)) { - $entry[$section][] = $m[1]; - $expected = ["item", "continuation", "blank line"]; - } elseif (in_array("continuation", $expected) && preg_match('/^ (\w.*)$/', $l, $m)) { - $last = sizeof($entry[$section]) - 1; - $entry[$section][$last] .= "\n".$m[1]; - } else { + } + if (!in_array("end-of-file", $expected)) { if (sizeof($expected) > 1) { - throw new \Exception("CHANGELOG: Expected one of [".implode(", ", $expected)."] at line $a"); + throw new \Exception("CHANGELOG: Expected one of [".implode(", ", $expected)."] at end of file"); } else { - throw new \Exception("CHANGELOG: Expected ".$expected[0]." at line $a"); + throw new \Exception("CHANGELOG: Expected ".$expected[0]." at end of file"); } } + $out[] = $entry; + return $out; } - if (!in_array("end-of-file", $expected)) { - if (sizeof($expected) > 1) { - throw new \Exception("CHANGELOG: Expected one of [".implode(", ", $expected)."] at end of file"); + + protected function changelogDebian(array $log, string $targetVersion): string { + $latest = $log[0]['version']; + $baseVersion = preg_replace('/^(\d+(?:\.\d+)*).*/', "$1", $targetVersion); + if ($baseVersion !== $targetVersion && version_compare($latest, $baseVersion, ">")) { + // if the changelog contains an entry for a future version, change its version number to match the target version instead of using the future version + $log[0]['version'] = $targetVersion; } else { - throw new \Exception("CHANGELOG: Expected ".$expected[0]." at end of file"); + // otherwise synthesize a changelog entry for the changes since the last tag + array_unshift($log, ['version' => $targetVersion, 'date' => date("Y-m-d"), 'features' => [], 'fixes' => [], 'changes' => ["Unspecified changes"]]); } - } - $out[] = $entry; - return $out; -} - -function changelogDebian(array $log, string $targetVersion): string { - $latest = $log[0]['version']; - $baseVersion = preg_replace('/^(\d+(?:\.\d+)*).*/', "$1", $targetVersion); - if ($baseVersion !== $targetVersion && version_compare($latest, $baseVersion, ">")) { - // if the changelog contains an entry for a future version, change its version number to match the target version instead of using the future version - $log[0]['version'] = $targetVersion; - } else { - // otherwise synthesize a changelog entry for the changes since the last tag - array_unshift($log, ['version' => $targetVersion, 'date' => date("Y-m-d"), 'features' => [], 'fixes' => [], 'changes' => ["Unspecified changes"]]); - } - $out = ""; - foreach ($log as $entry) { - $out .= "arsse (".$entry['version']."-1) unstable; urgency=low\n"; - if ($entry['features']) { - $out .= "\n [ New features ]\n"; - foreach ($entry['features'] as $item) { - $out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n"; + $out = ""; + foreach ($log as $entry) { + $out .= "arsse (".$entry['version']."-1) unstable; urgency=low\n"; + if ($entry['features']) { + $out .= "\n [ New features ]\n"; + foreach ($entry['features'] as $item) { + $out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n"; + } } - } - if ($entry['fixes']) { - $out .= "\n [ Bug fixes ]\n"; - foreach ($entry['fixes'] as $item) { - $out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n"; + if ($entry['fixes']) { + $out .= "\n [ Bug fixes ]\n"; + foreach ($entry['fixes'] as $item) { + $out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n"; + } } - } - if ($entry['changes']) { - $out .= "\n [ Other changes ]\n"; - foreach ($entry['changes'] as $item) { - $out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n"; + if ($entry['changes']) { + $out .= "\n [ Other changes ]\n"; + foreach ($entry['changes'] as $item) { + $out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n"; + } } + $out .= "\n -- The Arsse team ".\DateTimeImmutable::createFromFormat("Y-m-d", $entry['date'], new \DateTimeZone("UTC"))->format("D, d M Y")." 00:00:00 +0000\n\n"; } - $out .= "\n -- The Arsse team ".\DateTimeImmutable::createFromFormat("Y-m-d", $entry['date'], new \DateTimeZone("UTC"))->format("D, d M Y")." 00:00:00 +0000\n\n"; + return $out; } - return $out; } \ No newline at end of file From d031d931a5c9678ed303af1d07ad64df873bba03 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 21 May 2021 06:43:17 -0400 Subject: [PATCH 248/366] Tidy up the Robo file further --- RoboFile.php | 117 ++++++++++++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 57 deletions(-) diff --git a/RoboFile.php b/RoboFile.php index f8b1c530..6b4dbc50 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -163,66 +163,69 @@ class RoboFile extends \Robo\Tasks { // create a temporary directory $dir = $t->tmpDir().\DIRECTORY_SEPARATOR; // create a Git worktree for the selected commit in the temp location - $result = $this->taskExec("git worktree add ".escapeshellarg($dir)." ".escapeshellarg($commit))->run(); + $result = $this->taskExec("git worktree add ".escapeshellarg($dir)." ".escapeshellarg($commit))->dir(BASE)->run(); if ($result->getExitCode() > 0) { return $result; } - // get useable version strings from Git - $version = trim(`git -C "$dir" describe --tags`); - $archVersion = preg_replace('/^([^-]+)-(\d+)-(\w+)$/', "$1.r$2.$3", $version); - // name the generic release tarball - $tarball = "arsse-$version.tar.gz"; - // generate the Debian changelog; this also validates our original changelog - $debianChangelog = $this->changelogDebian($this->changelogParse(file_get_contents($dir."CHANGELOG"), $version), $version); - // save commit description to VERSION file for use by packaging - $t->addTask($this->taskWriteToFile($dir."VERSION")->text($version)); - // save the Debian changelog - $t->addTask($this->taskWriteToFile($dir."dist/debian/changelog")->text($debianChangelog)); - // patch the Arch PKGBUILD file with the correct version string - $t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^pkgver=.*$/m')->to("pkgver=$archVersion")); - // patch the Arch PKGBUILD file with the correct source file - $t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^source=\("arsse-[^"]+"\)$/m')->to('source=("'.basename($tarball).'")')); - // perform Composer installation in the temp location with dev dependencies - $t->addTask($this->taskComposerInstall()->arg("-q")->dir($dir)); - // generate the manual - $t->addCode(function() { - return $this->manual(["-q"]); - }); - // perform Composer installation in the temp location for final output - $t->addTask($this->taskComposerInstall()->dir($dir)->noDev()->optimizeAutoloader()->arg("--no-scripts")->arg("-q")); - // delete unwanted files - $t->addTask($this->taskFilesystemStack()->remove([ - $dir.".git", - $dir.".gitignore", - $dir.".gitattributes", - $dir."composer.json", - $dir."composer.lock", - $dir.".php_cs.dist", - $dir."phpdoc.dist.xml", - $dir."build.xml", - $dir."RoboFile.php", - $dir."CONTRIBUTING.md", - $dir."docs", - $dir."tests", - $dir."vendor-bin", - $dir."vendor/bin", - $dir."robo", - $dir."robo.bat", - $dir."package.json", - $dir."yarn.lock", - $dir."postcss.config.js", - ])); - // generate a sample configuration file - $t->addTask($this->taskExec(escapeshellarg(\PHP_BINARY)." arsse.php conf save-defaults config.defaults.php")->dir($dir)); - // remove any existing archive - $t->addTask($this->taskFilesystemStack()->remove($tarball)); - // package it all up - $t->addTask($this->taskPack($tarball)->addDir("arsse", $dir)); - // execute the collection - $result = $t->run(); - // remove the Git worktree - $this->taskFilesystemStack()->remove($dir)->run(); - $this->taskExec("git worktree prune")->run(); + try { + // get useable version strings from Git + $version = trim(`git -C "$dir" describe --tags`); + $archVersion = preg_replace('/^([^-]+)-(\d+)-(\w+)$/', "$1.r$2.$3", $version); + // name the generic release tarball + $tarball = "arsse-$version.tar.gz"; + // generate the Debian changelog; this also validates our original changelog + $debianChangelog = $this->changelogDebian($this->changelogParse(file_get_contents($dir."CHANGELOG"), $version), $version); + // save commit description to VERSION file for use by packaging + $t->addTask($this->taskWriteToFile($dir."VERSION")->text($version)); + // save the Debian changelog + $t->addTask($this->taskWriteToFile($dir."dist/debian/changelog")->text($debianChangelog)); + // patch the Arch PKGBUILD file with the correct version string + $t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^pkgver=.*$/m')->to("pkgver=$archVersion")); + // patch the Arch PKGBUILD file with the correct source file + $t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^source=\("arsse-[^"]+"\)$/m')->to('source=("'.basename($tarball).'")')); + // perform Composer installation in the temp location with dev dependencies + $t->addTask($this->taskComposerInstall()->arg("-q")->dir($dir)); + // generate the manual + $t->addCode(function() { + return $this->manual(["-q"]); + }); + // perform Composer installation in the temp location for final output + $t->addTask($this->taskComposerInstall()->dir($dir)->noDev()->optimizeAutoloader()->arg("--no-scripts")->arg("-q")); + // delete unwanted files + $t->addTask($this->taskFilesystemStack()->remove([ + $dir.".git", + $dir.".gitignore", + $dir.".gitattributes", + $dir."composer.json", + $dir."composer.lock", + $dir.".php_cs.dist", + $dir."phpdoc.dist.xml", + $dir."build.xml", + $dir."RoboFile.php", + $dir."CONTRIBUTING.md", + $dir."docs", + $dir."tests", + $dir."vendor-bin", + $dir."vendor/bin", + $dir."robo", + $dir."robo.bat", + $dir."package.json", + $dir."yarn.lock", + $dir."postcss.config.js", + ])); + // generate a sample configuration file + $t->addTask($this->taskExec(escapeshellarg(\PHP_BINARY)." arsse.php conf save-defaults config.defaults.php")->dir($dir)); + // remove any existing archive + $t->addTask($this->taskFilesystemStack()->remove($tarball)); + // package it all up + $t->addTask($this->taskPack($tarball)->addDir("arsse", $dir)); + // execute the collection + $result = $t->run(); + } finally { + // remove the Git worktree + $this->taskFilesystemStack()->remove($dir)->run(); + $this->taskExec("git worktree prune")->dir(BASE)->run(); + } return $result; } From 3537e74d495c55008dbaeaead52ed23381132d66 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 21 May 2021 12:11:50 -0400 Subject: [PATCH 249/366] Update dependencies --- composer.lock | 26 ++-- vendor-bin/csfixer/composer.lock | 230 +++++++++++++++++++------------ vendor-bin/daux/composer.lock | 124 ++++++++--------- vendor-bin/phpunit/composer.lock | 12 +- vendor-bin/robo/composer.lock | 118 ++++++++-------- 5 files changed, 283 insertions(+), 227 deletions(-) diff --git a/composer.lock b/composer.lock index 1a90a287..0a3791c8 100644 --- a/composer.lock +++ b/composer.lock @@ -184,16 +184,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "1.8.1", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "35ea11d335fd638b5882ff1725228b3d35496ab1" + "reference": "dc960a912984efb74d0a90222870c72c87f10c91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/35ea11d335fd638b5882ff1725228b3d35496ab1", - "reference": "35ea11d335fd638b5882ff1725228b3d35496ab1", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91", "shasum": "" }, "require": { @@ -253,9 +253,9 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.8.1" + "source": "https://github.com/guzzle/psr7/tree/1.8.2" }, - "time": "2021-03-21T16:25:00+00:00" + "time": "2021-04-26T09:17:50+00:00" }, { "name": "hosteurope/password-generator", @@ -951,16 +951,16 @@ }, { "name": "psr/log", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { @@ -984,7 +984,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -995,9 +995,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.3" + "source": "https://github.com/php-fig/log/tree/1.1.4" }, - "time": "2020-03-23T09:12:05+00:00" + "time": "2021-05-03T11:20:27+00:00" }, { "name": "ralouphie/getallheaders", diff --git a/vendor-bin/csfixer/composer.lock b/vendor-bin/csfixer/composer.lock index 17edd012..3defdabb 100644 --- a/vendor-bin/csfixer/composer.lock +++ b/vendor-bin/csfixer/composer.lock @@ -90,16 +90,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.4.6", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "f27e06cd9675801df441b3656569b328e04aa37c" + "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f27e06cd9675801df441b3656569b328e04aa37c", - "reference": "f27e06cd9675801df441b3656569b328e04aa37c", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/964adcdd3a28bf9ed5d9ac6450064e0d71ed7496", + "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496", "shasum": "" }, "require": { @@ -134,7 +134,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/1.4.6" + "source": "https://github.com/composer/xdebug-handler/tree/2.0.1" }, "funding": [ { @@ -150,32 +150,34 @@ "type": "tidelift" } ], - "time": "2021-03-25T17:01:18+00:00" + "time": "2021-05-05T19:37:51+00:00" }, { "name": "doctrine/annotations", - "version": "1.12.1", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b" + "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/b17c5014ef81d212ac539f07a1001832df1b6d3b", - "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f", + "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f", "shasum": "" }, "require": { "doctrine/lexer": "1.*", "ext-tokenizer": "*", - "php": "^7.1 || ^8.0" + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" }, "require-dev": { - "doctrine/cache": "1.*", + "doctrine/cache": "^1.11 || ^2.0", "doctrine/coding-standard": "^6.0 || ^8.1", "phpstan/phpstan": "^0.12.20", - "phpunit/phpunit": "^7.5 || ^9.1.5" + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", + "symfony/cache": "^4.4 || ^5.2" }, "type": "library", "autoload": { @@ -218,9 +220,9 @@ ], "support": { "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.12.1" + "source": "https://github.com/doctrine/annotations/tree/1.13.1" }, - "time": "2021-02-21T21:00:45+00:00" + "time": "2021-05-16T18:07:53+00:00" }, { "name": "doctrine/lexer", @@ -304,21 +306,21 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.18.5", + "version": "v2.19.0", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "e0f6d05c8b157f50029ca6c65c19ed2694f475bf" + "reference": "d5b8a9d852b292c2f8a035200fa6844b1f82300b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/e0f6d05c8b157f50029ca6c65c19ed2694f475bf", - "reference": "e0f6d05c8b157f50029ca6c65c19ed2694f475bf", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/d5b8a9d852b292c2f8a035200fa6844b1f82300b", + "reference": "d5b8a9d852b292c2f8a035200fa6844b1f82300b", "shasum": "" }, "require": { "composer/semver": "^1.4 || ^2.0 || ^3.0", - "composer/xdebug-handler": "^1.2", + "composer/xdebug-handler": "^1.2 || ^2.0", "doctrine/annotations": "^1.2", "ext-json": "*", "ext-tokenizer": "*", @@ -361,6 +363,11 @@ "php-cs-fixer" ], "type": "application", + "extra": { + "branch-alias": { + "dev-master": "2.19-dev" + } + }, "autoload": { "psr-4": { "PhpCsFixer\\": "src/" @@ -396,7 +403,7 @@ "description": "A tool to automatically fix PHP code style", "support": { "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.18.5" + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.19.0" }, "funding": [ { @@ -404,7 +411,7 @@ "type": "github" } ], - "time": "2021-04-06T18:37:33+00:00" + "time": "2021-05-03T21:43:24+00:00" }, { "name": "php-cs-fixer/diff", @@ -461,6 +468,55 @@ }, "time": "2020-10-14T08:39:05+00:00" }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, { "name": "psr/container", "version": "1.1.1", @@ -561,16 +617,16 @@ }, { "name": "psr/log", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { @@ -594,7 +650,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -605,22 +661,22 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.3" + "source": "https://github.com/php-fig/log/tree/1.1.4" }, - "time": "2020-03-23T09:12:05+00:00" + "time": "2021-05-03T11:20:27+00:00" }, { "name": "symfony/console", - "version": "v5.2.6", + "version": "v5.2.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d" + "reference": "864568fdc0208b3eba3638b6000b69d2386e6768" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/35f039df40a3b335ebf310f244cb242b3a83ac8d", - "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d", + "url": "https://api.github.com/repos/symfony/console/zipball/864568fdc0208b3eba3638b6000b69d2386e6768", + "reference": "864568fdc0208b3eba3638b6000b69d2386e6768", "shasum": "" }, "require": { @@ -688,7 +744,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.2.6" + "source": "https://github.com/symfony/console/tree/v5.2.8" }, "funding": [ { @@ -704,20 +760,20 @@ "type": "tidelift" } ], - "time": "2021-03-28T09:42:18+00:00" + "time": "2021-05-11T15:45:21+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", "shasum": "" }, "require": { @@ -726,7 +782,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -755,7 +811,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/master" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" }, "funding": [ { @@ -771,7 +827,7 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2021-03-23T23:28:01+00:00" }, { "name": "symfony/event-dispatcher", @@ -860,16 +916,16 @@ }, { "name": "symfony/event-dispatcher-contracts", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "0ba7d54483095a198fa51781bc608d17e84dffa2" + "reference": "69fee1ad2332a7cbab3aca13591953da9cdb7a11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/0ba7d54483095a198fa51781bc608d17e84dffa2", - "reference": "0ba7d54483095a198fa51781bc608d17e84dffa2", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/69fee1ad2332a7cbab3aca13591953da9cdb7a11", + "reference": "69fee1ad2332a7cbab3aca13591953da9cdb7a11", "shasum": "" }, "require": { @@ -882,7 +938,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -919,7 +975,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.2.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.4.0" }, "funding": [ { @@ -935,20 +991,20 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2021-03-23T23:28:01+00:00" }, { "name": "symfony/filesystem", - "version": "v5.2.6", + "version": "v5.2.7", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "8c86a82f51658188119e62cff0a050a12d09836f" + "reference": "056e92acc21d977c37e6ea8e97374b2a6c8551b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/8c86a82f51658188119e62cff0a050a12d09836f", - "reference": "8c86a82f51658188119e62cff0a050a12d09836f", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/056e92acc21d977c37e6ea8e97374b2a6c8551b0", + "reference": "056e92acc21d977c37e6ea8e97374b2a6c8551b0", "shasum": "" }, "require": { @@ -981,7 +1037,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.2.6" + "source": "https://github.com/symfony/filesystem/tree/v5.2.7" }, "funding": [ { @@ -997,20 +1053,20 @@ "type": "tidelift" } ], - "time": "2021-03-28T14:30:26+00:00" + "time": "2021-04-01T10:42:13+00:00" }, { "name": "symfony/finder", - "version": "v5.2.4", + "version": "v5.2.9", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "0d639a0943822626290d169965804f79400e6a04" + "reference": "ccccb9d48ca42757dd12f2ca4bf857a4e217d90d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", - "reference": "0d639a0943822626290d169965804f79400e6a04", + "url": "https://api.github.com/repos/symfony/finder/zipball/ccccb9d48ca42757dd12f2ca4bf857a4e217d90d", + "reference": "ccccb9d48ca42757dd12f2ca4bf857a4e217d90d", "shasum": "" }, "require": { @@ -1042,7 +1098,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.2.4" + "source": "https://github.com/symfony/finder/tree/v5.2.9" }, "funding": [ { @@ -1058,7 +1114,7 @@ "type": "tidelift" } ], - "time": "2021-02-15T18:55:04+00:00" + "time": "2021-05-16T13:07:46+00:00" }, { "name": "symfony/options-resolver", @@ -1761,16 +1817,16 @@ }, { "name": "symfony/process", - "version": "v5.2.4", + "version": "v5.2.7", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f" + "reference": "98cb8eeb72e55d4196dd1e36f1f16e7b3a9a088e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/313a38f09c77fbcdc1d223e57d368cea76a2fd2f", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f", + "url": "https://api.github.com/repos/symfony/process/zipball/98cb8eeb72e55d4196dd1e36f1f16e7b3a9a088e", + "reference": "98cb8eeb72e55d4196dd1e36f1f16e7b3a9a088e", "shasum": "" }, "require": { @@ -1803,7 +1859,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.2.4" + "source": "https://github.com/symfony/process/tree/v5.3.0-BETA1" }, "funding": [ { @@ -1819,25 +1875,25 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:15:41+00:00" + "time": "2021-04-08T10:27:02+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", "shasum": "" }, "require": { "php": ">=7.2.5", - "psr/container": "^1.0" + "psr/container": "^1.1" }, "suggest": { "symfony/service-implementation": "" @@ -1845,7 +1901,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -1882,7 +1938,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/master" + "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" }, "funding": [ { @@ -1898,20 +1954,20 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2021-04-01T10:43:52+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.2.4", + "version": "v5.2.7", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "b12274acfab9d9850c52583d136a24398cdf1a0c" + "reference": "d99310c33e833def36419c284f60e8027d359678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b12274acfab9d9850c52583d136a24398cdf1a0c", - "reference": "b12274acfab9d9850c52583d136a24398cdf1a0c", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/d99310c33e833def36419c284f60e8027d359678", + "reference": "d99310c33e833def36419c284f60e8027d359678", "shasum": "" }, "require": { @@ -1944,7 +2000,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.2.4" + "source": "https://github.com/symfony/stopwatch/tree/v5.3.0-BETA1" }, "funding": [ { @@ -1960,20 +2016,20 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:15:41+00:00" + "time": "2021-03-29T15:28:41+00:00" }, { "name": "symfony/string", - "version": "v5.2.6", + "version": "v5.2.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572" + "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", - "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", + "url": "https://api.github.com/repos/symfony/string/zipball/01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db", + "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db", "shasum": "" }, "require": { @@ -2027,7 +2083,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.2.6" + "source": "https://github.com/symfony/string/tree/v5.2.8" }, "funding": [ { @@ -2043,7 +2099,7 @@ "type": "tidelift" } ], - "time": "2021-03-17T17:12:15+00:00" + "time": "2021-05-10T14:56:10+00:00" } ], "aliases": [], diff --git a/vendor-bin/daux/composer.lock b/vendor-bin/daux/composer.lock index 6debf534..c9075d2e 100644 --- a/vendor-bin/daux/composer.lock +++ b/vendor-bin/daux/composer.lock @@ -241,16 +241,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "1.8.1", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "35ea11d335fd638b5882ff1725228b3d35496ab1" + "reference": "dc960a912984efb74d0a90222870c72c87f10c91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/35ea11d335fd638b5882ff1725228b3d35496ab1", - "reference": "35ea11d335fd638b5882ff1725228b3d35496ab1", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91", "shasum": "" }, "require": { @@ -310,22 +310,22 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.8.1" + "source": "https://github.com/guzzle/psr7/tree/1.8.2" }, - "time": "2021-03-21T16:25:00+00:00" + "time": "2021-04-26T09:17:50+00:00" }, { "name": "league/commonmark", - "version": "1.5.8", + "version": "1.6.2", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf" + "reference": "7d70d2f19c84bcc16275ea47edabee24747352eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf", - "reference": "08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/7d70d2f19c84bcc16275ea47edabee24747352eb", + "reference": "7d70d2f19c84bcc16275ea47edabee24747352eb", "shasum": "" }, "require": { @@ -413,7 +413,7 @@ "type": "tidelift" } ], - "time": "2021-03-28T18:51:39+00:00" + "time": "2021-05-12T11:39:41+00:00" }, { "name": "league/plates", @@ -754,16 +754,16 @@ }, { "name": "symfony/console", - "version": "v5.2.6", + "version": "v5.2.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d" + "reference": "864568fdc0208b3eba3638b6000b69d2386e6768" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/35f039df40a3b335ebf310f244cb242b3a83ac8d", - "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d", + "url": "https://api.github.com/repos/symfony/console/zipball/864568fdc0208b3eba3638b6000b69d2386e6768", + "reference": "864568fdc0208b3eba3638b6000b69d2386e6768", "shasum": "" }, "require": { @@ -831,7 +831,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.2.6" + "source": "https://github.com/symfony/console/tree/v5.2.8" }, "funding": [ { @@ -847,20 +847,20 @@ "type": "tidelift" } ], - "time": "2021-03-28T09:42:18+00:00" + "time": "2021-05-11T15:45:21+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", "shasum": "" }, "require": { @@ -869,7 +869,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -898,7 +898,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/master" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" }, "funding": [ { @@ -914,20 +914,20 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2021-03-23T23:28:01+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.2.4", + "version": "v5.2.8", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "54499baea7f7418bce7b5ec92770fd0799e8e9bf" + "reference": "e8fbbab7c4a71592985019477532629cb2e142dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/54499baea7f7418bce7b5ec92770fd0799e8e9bf", - "reference": "54499baea7f7418bce7b5ec92770fd0799e8e9bf", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e8fbbab7c4a71592985019477532629cb2e142dc", + "reference": "e8fbbab7c4a71592985019477532629cb2e142dc", "shasum": "" }, "require": { @@ -971,7 +971,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.2.4" + "source": "https://github.com/symfony/http-foundation/tree/v5.2.8" }, "funding": [ { @@ -987,20 +987,20 @@ "type": "tidelift" } ], - "time": "2021-02-25T17:16:57+00:00" + "time": "2021-05-07T13:41:16+00:00" }, { "name": "symfony/mime", - "version": "v5.2.6", + "version": "v5.2.9", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "1b2092244374cbe48ae733673f2ca0818b37197b" + "reference": "64258e870f8cc75c3dae986201ea2df58c210b52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/1b2092244374cbe48ae733673f2ca0818b37197b", - "reference": "1b2092244374cbe48ae733673f2ca0818b37197b", + "url": "https://api.github.com/repos/symfony/mime/zipball/64258e870f8cc75c3dae986201ea2df58c210b52", + "reference": "64258e870f8cc75c3dae986201ea2df58c210b52", "shasum": "" }, "require": { @@ -1054,7 +1054,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.2.6" + "source": "https://github.com/symfony/mime/tree/v5.2.9" }, "funding": [ { @@ -1070,7 +1070,7 @@ "type": "tidelift" } ], - "time": "2021-03-12T13:18:39+00:00" + "time": "2021-05-16T13:07:46+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1810,16 +1810,16 @@ }, { "name": "symfony/process", - "version": "v5.2.4", + "version": "v5.2.7", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f" + "reference": "98cb8eeb72e55d4196dd1e36f1f16e7b3a9a088e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/313a38f09c77fbcdc1d223e57d368cea76a2fd2f", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f", + "url": "https://api.github.com/repos/symfony/process/zipball/98cb8eeb72e55d4196dd1e36f1f16e7b3a9a088e", + "reference": "98cb8eeb72e55d4196dd1e36f1f16e7b3a9a088e", "shasum": "" }, "require": { @@ -1852,7 +1852,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.2.4" + "source": "https://github.com/symfony/process/tree/v5.3.0-BETA1" }, "funding": [ { @@ -1868,25 +1868,25 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:15:41+00:00" + "time": "2021-04-08T10:27:02+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", "shasum": "" }, "require": { "php": ">=7.2.5", - "psr/container": "^1.0" + "psr/container": "^1.1" }, "suggest": { "symfony/service-implementation": "" @@ -1894,7 +1894,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -1931,7 +1931,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/master" + "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" }, "funding": [ { @@ -1947,20 +1947,20 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2021-04-01T10:43:52+00:00" }, { "name": "symfony/string", - "version": "v5.2.6", + "version": "v5.2.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572" + "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", - "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", + "url": "https://api.github.com/repos/symfony/string/zipball/01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db", + "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db", "shasum": "" }, "require": { @@ -2014,7 +2014,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.2.6" + "source": "https://github.com/symfony/string/tree/v5.2.8" }, "funding": [ { @@ -2030,20 +2030,20 @@ "type": "tidelift" } ], - "time": "2021-03-17T17:12:15+00:00" + "time": "2021-05-10T14:56:10+00:00" }, { "name": "symfony/yaml", - "version": "v5.2.5", + "version": "v5.2.9", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "298a08ddda623485208506fcee08817807a251dd" + "reference": "d23115e4a3d50520abddccdbec9514baab1084c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/298a08ddda623485208506fcee08817807a251dd", - "reference": "298a08ddda623485208506fcee08817807a251dd", + "url": "https://api.github.com/repos/symfony/yaml/zipball/d23115e4a3d50520abddccdbec9514baab1084c8", + "reference": "d23115e4a3d50520abddccdbec9514baab1084c8", "shasum": "" }, "require": { @@ -2089,7 +2089,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.2.5" + "source": "https://github.com/symfony/yaml/tree/v5.2.9" }, "funding": [ { @@ -2105,7 +2105,7 @@ "type": "tidelift" } ], - "time": "2021-03-06T07:59:01+00:00" + "time": "2021-05-16T13:07:46+00:00" }, { "name": "webuni/front-matter", diff --git a/vendor-bin/phpunit/composer.lock b/vendor-bin/phpunit/composer.lock index d440548c..bd098686 100644 --- a/vendor-bin/phpunit/composer.lock +++ b/vendor-bin/phpunit/composer.lock @@ -446,16 +446,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.10.4", + "version": "v4.10.5", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e" + "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c6d052fc58cb876152f89f532b95a8d7907e7f0e", - "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4432ba399e47c66624bc73c8c0f811e5c109576f", + "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f", "shasum": "" }, "require": { @@ -496,9 +496,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.4" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.5" }, - "time": "2020-12-20T10:01:03+00:00" + "time": "2021-05-03T19:11:20+00:00" }, { "name": "phar-io/manifest", diff --git a/vendor-bin/robo/composer.lock b/vendor-bin/robo/composer.lock index 24c4530d..e20d815d 100644 --- a/vendor-bin/robo/composer.lock +++ b/vendor-bin/robo/composer.lock @@ -917,16 +917,16 @@ }, { "name": "psr/log", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { @@ -950,7 +950,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -961,9 +961,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.3" + "source": "https://github.com/php-fig/log/tree/1.1.4" }, - "time": "2020-03-23T09:12:05+00:00" + "time": "2021-05-03T11:20:27+00:00" }, { "name": "symfony/console", @@ -1058,16 +1058,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", "shasum": "" }, "require": { @@ -1076,7 +1076,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -1105,7 +1105,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/master" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" }, "funding": [ { @@ -1121,7 +1121,7 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2021-03-23T23:28:01+00:00" }, { "name": "symfony/event-dispatcher", @@ -1210,16 +1210,16 @@ }, { "name": "symfony/event-dispatcher-contracts", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "0ba7d54483095a198fa51781bc608d17e84dffa2" + "reference": "69fee1ad2332a7cbab3aca13591953da9cdb7a11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/0ba7d54483095a198fa51781bc608d17e84dffa2", - "reference": "0ba7d54483095a198fa51781bc608d17e84dffa2", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/69fee1ad2332a7cbab3aca13591953da9cdb7a11", + "reference": "69fee1ad2332a7cbab3aca13591953da9cdb7a11", "shasum": "" }, "require": { @@ -1232,7 +1232,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -1269,7 +1269,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.2.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.4.0" }, "funding": [ { @@ -1285,20 +1285,20 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2021-03-23T23:28:01+00:00" }, { "name": "symfony/filesystem", - "version": "v5.2.6", + "version": "v5.2.7", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "8c86a82f51658188119e62cff0a050a12d09836f" + "reference": "056e92acc21d977c37e6ea8e97374b2a6c8551b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/8c86a82f51658188119e62cff0a050a12d09836f", - "reference": "8c86a82f51658188119e62cff0a050a12d09836f", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/056e92acc21d977c37e6ea8e97374b2a6c8551b0", + "reference": "056e92acc21d977c37e6ea8e97374b2a6c8551b0", "shasum": "" }, "require": { @@ -1331,7 +1331,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.2.6" + "source": "https://github.com/symfony/filesystem/tree/v5.2.7" }, "funding": [ { @@ -1347,20 +1347,20 @@ "type": "tidelift" } ], - "time": "2021-03-28T14:30:26+00:00" + "time": "2021-04-01T10:42:13+00:00" }, { "name": "symfony/finder", - "version": "v5.2.4", + "version": "v5.2.9", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "0d639a0943822626290d169965804f79400e6a04" + "reference": "ccccb9d48ca42757dd12f2ca4bf857a4e217d90d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", - "reference": "0d639a0943822626290d169965804f79400e6a04", + "url": "https://api.github.com/repos/symfony/finder/zipball/ccccb9d48ca42757dd12f2ca4bf857a4e217d90d", + "reference": "ccccb9d48ca42757dd12f2ca4bf857a4e217d90d", "shasum": "" }, "require": { @@ -1392,7 +1392,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.2.4" + "source": "https://github.com/symfony/finder/tree/v5.2.9" }, "funding": [ { @@ -1408,7 +1408,7 @@ "type": "tidelift" } ], - "time": "2021-02-15T18:55:04+00:00" + "time": "2021-05-16T13:07:46+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1898,16 +1898,16 @@ }, { "name": "symfony/process", - "version": "v5.2.4", + "version": "v5.2.7", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f" + "reference": "98cb8eeb72e55d4196dd1e36f1f16e7b3a9a088e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/313a38f09c77fbcdc1d223e57d368cea76a2fd2f", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f", + "url": "https://api.github.com/repos/symfony/process/zipball/98cb8eeb72e55d4196dd1e36f1f16e7b3a9a088e", + "reference": "98cb8eeb72e55d4196dd1e36f1f16e7b3a9a088e", "shasum": "" }, "require": { @@ -1940,7 +1940,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.2.4" + "source": "https://github.com/symfony/process/tree/v5.3.0-BETA1" }, "funding": [ { @@ -1956,25 +1956,25 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:15:41+00:00" + "time": "2021-04-08T10:27:02+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", "shasum": "" }, "require": { "php": ">=7.2.5", - "psr/container": "^1.0" + "psr/container": "^1.1" }, "suggest": { "symfony/service-implementation": "" @@ -1982,7 +1982,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -2019,7 +2019,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/master" + "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" }, "funding": [ { @@ -2035,20 +2035,20 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2021-04-01T10:43:52+00:00" }, { "name": "symfony/string", - "version": "v5.2.6", + "version": "v5.2.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572" + "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", - "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", + "url": "https://api.github.com/repos/symfony/string/zipball/01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db", + "reference": "01b35eb64cac8467c3f94cd0ce2d0d376bb7d1db", "shasum": "" }, "require": { @@ -2102,7 +2102,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.2.6" + "source": "https://github.com/symfony/string/tree/v5.2.8" }, "funding": [ { @@ -2118,20 +2118,20 @@ "type": "tidelift" } ], - "time": "2021-03-17T17:12:15+00:00" + "time": "2021-05-10T14:56:10+00:00" }, { "name": "symfony/yaml", - "version": "v5.2.5", + "version": "v5.2.9", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "298a08ddda623485208506fcee08817807a251dd" + "reference": "d23115e4a3d50520abddccdbec9514baab1084c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/298a08ddda623485208506fcee08817807a251dd", - "reference": "298a08ddda623485208506fcee08817807a251dd", + "url": "https://api.github.com/repos/symfony/yaml/zipball/d23115e4a3d50520abddccdbec9514baab1084c8", + "reference": "d23115e4a3d50520abddccdbec9514baab1084c8", "shasum": "" }, "require": { @@ -2177,7 +2177,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.2.5" + "source": "https://github.com/symfony/yaml/tree/v5.2.9" }, "funding": [ { @@ -2193,7 +2193,7 @@ "type": "tidelift" } ], - "time": "2021-03-06T07:59:01+00:00" + "time": "2021-05-16T13:07:46+00:00" } ], "aliases": [], From 3c9f4dd66fd854b243e73eecc14dee8ba9e08de1 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 21 May 2021 12:51:20 -0400 Subject: [PATCH 250/366] Prototype Debian rules file --- RoboFile.php | 20 ++++++++++++++++++++ dist/debian/rules | 6 ++++++ 2 files changed, 26 insertions(+) create mode 100644 dist/debian/rules diff --git a/RoboFile.php b/RoboFile.php index 6b4dbc50..f869c33c 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -246,6 +246,26 @@ class RoboFile extends \Robo\Tasks { return $t->run(); } + /** Packages a release tarball into a Debian package */ + public function packageDeb(string $tarball): Result { + $t = $this->collectionBuilder(); + $dir = $t->tmpDir().\DIRECTORY_SEPARATOR; + // name the "orig" tarball + $orig = $dir.str_replace(".tar.gz", ".orig.tar.gz", str_replace("arsse-", "arsse_", basename($tarball))); + // copy the tarball + $t->addTask($this->taskFilesystemStack()->copy($tarball, $orig)); + // extract the tarball and keep all "dist files" + $t->addCode(function() use ($tarball, $dir) { + // because Robo doesn't support extracting a single file we have to do it ourselves + (new \Archive_Tar($tarball))->extract($dir, false); + // perform a do-nothing filesystem operation since we need a Robo task result + return $this->taskFilesystemStack()->rename($dir."arsse", $dir."src")->run(); + }); + $t->addTask($this->taskFilesystemStack()->mirror($dir."src/dist", $dir)); + $t->addTask($this->taskExec("deber")->dir($dir)); + return $t->run(); + } + /** Generates static manual pages in the "manual" directory * * The resultant files are suitable for offline viewing and inclusion into release builds diff --git a/dist/debian/rules b/dist/debian/rules new file mode 100644 index 00000000..626d6fa2 --- /dev/null +++ b/dist/debian/rules @@ -0,0 +1,6 @@ +#!/usr/bin/make -f + +DH_VERBOSE = 1 + +%: + dh $@ \ No newline at end of file From b7909d7cd302f4037b777bf2d08849454ff4b990 Mon Sep 17 00:00:00 2001 From: "J. KIng" Date: Fri, 21 May 2021 15:13:23 -0400 Subject: [PATCH 251/366] Downgrade tool dependencies for Ubuntu --- composer.lock | 2 +- vendor-bin/csfixer/composer.lock | 62 +++++--------------------------- vendor-bin/daux/composer.lock | 2 +- vendor-bin/phpunit/composer.lock | 2 +- vendor-bin/robo/composer.lock | 2 +- 5 files changed, 12 insertions(+), 58 deletions(-) diff --git a/composer.lock b/composer.lock index 0a3791c8..69572863 100644 --- a/composer.lock +++ b/composer.lock @@ -1359,5 +1359,5 @@ "platform-overrides": { "php": "7.1.33" }, - "plugin-api-version": "2.0.0" + "plugin-api-version": "1.1.0" } diff --git a/vendor-bin/csfixer/composer.lock b/vendor-bin/csfixer/composer.lock index 3defdabb..7d1cf599 100644 --- a/vendor-bin/csfixer/composer.lock +++ b/vendor-bin/csfixer/composer.lock @@ -131,11 +131,6 @@ "Xdebug", "performance" ], - "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/2.0.1" - }, "funding": [ { "url": "https://packagist.com", @@ -218,10 +213,6 @@ "docblock", "parser" ], - "support": { - "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.13.1" - }, "time": "2021-05-16T18:07:53+00:00" }, { @@ -401,10 +392,6 @@ } ], "description": "A tool to automatically fix PHP code style", - "support": { - "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.19.0" - }, "funding": [ { "url": "https://github.com/keradus", @@ -470,20 +457,20 @@ }, { "name": "psr/cache", - "version": "3.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/cache.git", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": ">=5.3.0" }, "type": "library", "extra": { @@ -503,7 +490,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "homepage": "http://www.php-fig.org/" } ], "description": "Common interface for caching libraries", @@ -512,10 +499,7 @@ "psr", "psr-6" ], - "support": { - "source": "https://github.com/php-fig/cache/tree/3.0.0" - }, - "time": "2021-02-03T23:26:27+00:00" + "time": "2016-08-06T20:24:11+00:00" }, { "name": "psr/container", @@ -660,9 +644,6 @@ "psr", "psr-3" ], - "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" - }, "time": "2021-05-03T11:20:27+00:00" }, { @@ -743,9 +724,6 @@ "console", "terminal" ], - "support": { - "source": "https://github.com/symfony/console/tree/v5.2.8" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -810,9 +788,6 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -974,9 +949,6 @@ "interoperability", "standards" ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.4.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1036,9 +1008,6 @@ ], "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.2.7" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1097,9 +1066,6 @@ ], "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v5.2.9" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1858,9 +1824,6 @@ ], "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v5.3.0-BETA1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1937,9 +1900,6 @@ "interoperability", "standards" ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1999,9 +1959,6 @@ ], "description": "Provides a way to profile code", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.3.0-BETA1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2082,9 +2039,6 @@ "utf-8", "utf8" ], - "support": { - "source": "https://github.com/symfony/string/tree/v5.2.8" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2109,5 +2063,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.0.0" + "plugin-api-version": "1.1.0" } diff --git a/vendor-bin/daux/composer.lock b/vendor-bin/daux/composer.lock index c9075d2e..58d91333 100644 --- a/vendor-bin/daux/composer.lock +++ b/vendor-bin/daux/composer.lock @@ -2187,5 +2187,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.0.0" + "plugin-api-version": "1.1.0" } diff --git a/vendor-bin/phpunit/composer.lock b/vendor-bin/phpunit/composer.lock index bd098686..b7e235e0 100644 --- a/vendor-bin/phpunit/composer.lock +++ b/vendor-bin/phpunit/composer.lock @@ -2516,5 +2516,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.0.0" + "plugin-api-version": "1.1.0" } diff --git a/vendor-bin/robo/composer.lock b/vendor-bin/robo/composer.lock index e20d815d..584e19fa 100644 --- a/vendor-bin/robo/composer.lock +++ b/vendor-bin/robo/composer.lock @@ -2203,5 +2203,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.0.0" + "plugin-api-version": "1.1.0" } From e653fb3f737723d40a3905d136d2d18e67b27e50 Mon Sep 17 00:00:00 2001 From: "J. KIng" Date: Fri, 21 May 2021 21:11:22 -0400 Subject: [PATCH 252/366] Enhancements to Debian files --- RoboFile.php | 42 +++++++++++++++++++++++++-------------- dist/debian/copyright | 33 ++++++++++++++++++++++++++++++ dist/debian/rules | 3 ++- dist/debian/source/format | 1 + 4 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 dist/debian/copyright create mode 100644 dist/debian/source/format diff --git a/RoboFile.php b/RoboFile.php index f869c33c..dbffa4c2 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -248,20 +248,28 @@ class RoboFile extends \Robo\Tasks { /** Packages a release tarball into a Debian package */ public function packageDeb(string $tarball): Result { + // determine the "upstream" (tagged) version + if (preg_match('/^arsse-(\d+(?:\.\d+)*)/', basename($tarball, $m))) { + $version = $m[1]; + $base = $dir."arsse-$version"; + } else { + throw new \Exception("Tarball is not named correctly"); + } $t = $this->collectionBuilder(); - $dir = $t->tmpDir().\DIRECTORY_SEPARATOR; - // name the "orig" tarball - $orig = $dir.str_replace(".tar.gz", ".orig.tar.gz", str_replace("arsse-", "arsse_", basename($tarball))); + $dir = $t->workDir("~/temp2").\DIRECTORY_SEPARATOR; // copy the tarball $t->addTask($this->taskFilesystemStack()->copy($tarball, $orig)); - // extract the tarball and keep all "dist files" + // extract the tarball $t->addCode(function() use ($tarball, $dir) { - // because Robo doesn't support extracting a single file we have to do it ourselves + // Robo's extract task is broken, so we do it manually (new \Archive_Tar($tarball))->extract($dir, false); - // perform a do-nothing filesystem operation since we need a Robo task result - return $this->taskFilesystemStack()->rename($dir."arsse", $dir."src")->run(); + // "temp.orig" is a special directory name to Debian's "quilt" format + return $this->taskFilesystemStack()->rename($dir."arsse", $dir."temp.orig")->run(); }); - $t->addTask($this->taskFilesystemStack()->mirror($dir."src/dist", $dir)); + // create a directory with the package name and "upstream" version; this is also special to Debian + $t->addTask($this->taskFilesystemStack()->mkdir($base)); + // copy relevant files to the directory + $t->addTask($this->taskFilesystemStack()->mirror($dir."temp.orig/dist", $base)); $t->addTask($this->taskExec("deber")->dir($dir)); return $t->run(); } @@ -334,7 +342,7 @@ class RoboFile extends \Robo\Tasks { } if ($entry) { $out[] = $entry; - } + } $entry = ['version' => $version, 'date' => $date, 'features' => [], 'fixes' => [], 'changes' => []]; $expected = ["separator"]; } elseif (in_array("separator", $expected) && preg_match('/^=+/', $l)) { @@ -398,27 +406,31 @@ class RoboFile extends \Robo\Tasks { } $out = ""; foreach ($log as $entry) { - $out .= "arsse (".$entry['version']."-1) unstable; urgency=low\n"; + // normalize the version string + preg_match('/^(\d+(?:\.\d+)*)(?:-(\d+)-.+)?$/', $entry['version'], $m); + $version = $m[1]."-".($m[2] ?: "1"); + // output the entry + $out .= "arsse ($version) UNRELEASED; urgency=low\n"; if ($entry['features']) { - $out .= "\n [ New features ]\n"; + $out .= "\n"; foreach ($entry['features'] as $item) { $out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n"; } } if ($entry['fixes']) { - $out .= "\n [ Bug fixes ]\n"; + $out .= "\n"; foreach ($entry['fixes'] as $item) { $out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n"; } } if ($entry['changes']) { - $out .= "\n [ Other changes ]\n"; + $out .= "\n"; foreach ($entry['changes'] as $item) { $out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n"; } } - $out .= "\n -- The Arsse team ".\DateTimeImmutable::createFromFormat("Y-m-d", $entry['date'], new \DateTimeZone("UTC"))->format("D, d M Y")." 00:00:00 +0000\n\n"; + $out .= "\n -- The Arsse team ".\DateTimeImmutable::createFromFormat("Y-m-d", $entry['date'], new \DateTimeZone("UTC"))->format("D, d M Y")." 00:00:00 +0000\n\n"; } return $out; } -} \ No newline at end of file +} diff --git a/dist/debian/copyright b/dist/debian/copyright new file mode 100644 index 00000000..29222cd9 --- /dev/null +++ b/dist/debian/copyright @@ -0,0 +1,33 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: arsse +Upstream-Contact: J. King +Source: https://code.mensbeam.com/MensBeam/arsse/ + +Files: * +Copyright: 2017 J. King + 2017 Dustin Wilson +License: Expat + +License: Expat + Copyright (c) 2017 J. King, Dustin Wilson + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. diff --git a/dist/debian/rules b/dist/debian/rules index 626d6fa2..8ff98a72 100644 --- a/dist/debian/rules +++ b/dist/debian/rules @@ -3,4 +3,5 @@ DH_VERBOSE = 1 %: - dh $@ \ No newline at end of file + dh $@ + diff --git a/dist/debian/source/format b/dist/debian/source/format new file mode 100644 index 00000000..163aaf8d --- /dev/null +++ b/dist/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) From 0de964780997ed7212507dbc4a8ac0af1f10b14c Mon Sep 17 00:00:00 2001 From: "J. KIng" Date: Fri, 21 May 2021 22:03:40 -0400 Subject: [PATCH 253/366] Add compat file --- RoboFile.php | 18 ++++++++---------- dist/debian/compat | 1 + 2 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 dist/debian/compat diff --git a/RoboFile.php b/RoboFile.php index dbffa4c2..8cd5f58d 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -248,29 +248,27 @@ class RoboFile extends \Robo\Tasks { /** Packages a release tarball into a Debian package */ public function packageDeb(string $tarball): Result { + $t = $this->collectionBuilder(); + $dir = $t->workDir("/home/jking/temp").\DIRECTORY_SEPARATOR; // determine the "upstream" (tagged) version - if (preg_match('/^arsse-(\d+(?:\.\d+)*)/', basename($tarball, $m))) { + if (preg_match('/^arsse-(\d+(?:\.\d+)*)/', basename($tarball), $m)) { $version = $m[1]; $base = $dir."arsse-$version"; } else { throw new \Exception("Tarball is not named correctly"); } - $t = $this->collectionBuilder(); - $dir = $t->workDir("~/temp2").\DIRECTORY_SEPARATOR; - // copy the tarball - $t->addTask($this->taskFilesystemStack()->copy($tarball, $orig)); // extract the tarball - $t->addCode(function() use ($tarball, $dir) { + $t->addCode(function() use ($tarball, $dir, $base) { // Robo's extract task is broken, so we do it manually (new \Archive_Tar($tarball))->extract($dir, false); - // "temp.orig" is a special directory name to Debian's "quilt" format - return $this->taskFilesystemStack()->rename($dir."arsse", $dir."temp.orig")->run(); + // "$package-$version.orig" is a special directory name to Debian's "quilt" format + return $this->taskFilesystemStack()->rename($dir."arsse", "$base.orig")->run(); }); // create a directory with the package name and "upstream" version; this is also special to Debian $t->addTask($this->taskFilesystemStack()->mkdir($base)); // copy relevant files to the directory - $t->addTask($this->taskFilesystemStack()->mirror($dir."temp.orig/dist", $base)); - $t->addTask($this->taskExec("deber")->dir($dir)); + $t->addTask($this->taskFilesystemStack()->mirror("$base.orig/dist", $base)); + //$t->addTask($this->taskExec("deber")->dir($dir)); return $t->run(); } diff --git a/dist/debian/compat b/dist/debian/compat new file mode 100644 index 00000000..f599e28b --- /dev/null +++ b/dist/debian/compat @@ -0,0 +1 @@ +10 From f844c17a9414352ada4b86dcfef2792eed6d6415 Mon Sep 17 00:00:00 2001 From: "J. KIng" Date: Sat, 22 May 2021 07:16:48 -0400 Subject: [PATCH 254/366] More Debian fixes --- RoboFile.php | 20 ++++++++++---------- dist/debian/control | 1 - dist/debian/copyright | 6 +++--- dist/debian/rules | 0 4 files changed, 13 insertions(+), 14 deletions(-) mode change 100644 => 100755 dist/debian/rules diff --git a/RoboFile.php b/RoboFile.php index 8cd5f58d..8d6b8c0a 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -248,26 +248,26 @@ class RoboFile extends \Robo\Tasks { /** Packages a release tarball into a Debian package */ public function packageDeb(string $tarball): Result { - $t = $this->collectionBuilder(); - $dir = $t->workDir("/home/jking/temp").\DIRECTORY_SEPARATOR; // determine the "upstream" (tagged) version if (preg_match('/^arsse-(\d+(?:\.\d+)*)/', basename($tarball), $m)) { $version = $m[1]; - $base = $dir."arsse-$version"; } else { throw new \Exception("Tarball is not named correctly"); } - // extract the tarball + // start a task collection and create a temporary directory + $t = $this->collectionBuilder(); + $dir = $t->workDir("/home/jking/temp").\DIRECTORY_SEPARATOR; + $base = $dir."arsse-$version".\DIRECTORY_SEPARATOR; + // start by extracting the tarball $t->addCode(function() use ($tarball, $dir, $base) { // Robo's extract task is broken, so we do it manually (new \Archive_Tar($tarball))->extract($dir, false); - // "$package-$version.orig" is a special directory name to Debian's "quilt" format - return $this->taskFilesystemStack()->rename($dir."arsse", "$base.orig")->run(); + return $this->taskFilesystemStack()->rename($dir."arsse", $base)->run(); }); - // create a directory with the package name and "upstream" version; this is also special to Debian - $t->addTask($this->taskFilesystemStack()->mkdir($base)); - // copy relevant files to the directory - $t->addTask($this->taskFilesystemStack()->mirror("$base.orig/dist", $base)); + // re-pack the tarball using specific names special to debuild + $t->addTask($this->taskPack($dir."arsse_$version.orig.tar.gz")->addDir("arsse-$version", $base)); + // copy Debian files to lower down in the tree + $t->addTask($this->taskFilesystemStack()->mirror($base."dist/debian", $base."debian")); //$t->addTask($this->taskExec("deber")->dir($dir)); return $t->run(); } diff --git a/dist/debian/control b/dist/debian/control index 99221ddf..2ea9726c 100644 --- a/dist/debian/control +++ b/dist/debian/control @@ -11,7 +11,6 @@ Package: arsse Architecture: all Section: contrib/net Priority: optional -Essential: no Homepage: https://thearsse.com/ Description: Multi-protocol RSS/Atom newsfeed synchronization server The Arsse bridges the gap between multiple existing newsfeed aggregator diff --git a/dist/debian/copyright b/dist/debian/copyright index 29222cd9..87ca14e4 100644 --- a/dist/debian/copyright +++ b/dist/debian/copyright @@ -10,7 +10,7 @@ License: Expat License: Expat Copyright (c) 2017 J. King, Dustin Wilson - + . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without @@ -19,10 +19,10 @@ License: Expat copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - + . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - + . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND diff --git a/dist/debian/rules b/dist/debian/rules old mode 100644 new mode 100755 From de552907469b796b078334499ae604e1f4e9a4ec Mon Sep 17 00:00:00 2001 From: "J. KIng" Date: Sat, 22 May 2021 15:05:31 -0400 Subject: [PATCH 255/366] Fix build dependencies for Deb package --- dist/debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/dist/debian/control b/dist/debian/control index 2ea9726c..e44560fc 100644 --- a/dist/debian/control +++ b/dist/debian/control @@ -17,6 +17,7 @@ Description: Multi-protocol RSS/Atom newsfeed synchronization server client protocols such as Tiny Tiny RSS, Nextcloud News and Miniflux, allowing you to use compatible clients for many protocols with a single server. +Build-Depends: debhelper Depends: ${misc:Depends}, dbconfig-mysql | dbconfig-pgsql | dbconfig-sqlite3 | dbconfig-no-thanks, php (>= 7.1.0), From 11fc83da60a97a17700d9c6ada0f01a3de992fa3 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 23 May 2021 12:24:42 -0400 Subject: [PATCH 256/366] Significant edits to the manual --- RoboFile.php | 4 +- .../020_Getting_Started/010_Requirements.md | 16 ------ .../010_On_Arch_Linux.md | 14 +++++- .../020_On_Debian_and_Ubuntu.md | 46 +++++++++++++---- .../040_Database_Setup/000_SQLite.md | 2 +- .../040_Database_Setup/010_PostgreSQL.md | 2 +- .../040_Database_Setup/020_MySQL.md | 2 +- .../020_Getting_Started/050_Configuration.md | 2 +- docs/en/020_Getting_Started/index.md | 21 +++++++- .../025_Using_The_Arsse/010_Managing_Users.md | 22 ++++----- .../020_Importing_and_Exporting.md | 8 +-- .../030_Keeping_Newsfeeds_Up_to_Date.md | 49 ------------------- .../025_Using_The_Arsse/030_Other_Topics.md | 30 ++++++++++++ .../040_Upgrading_to_a_New_Version.md | 12 ----- docs/en/025_Using_The_Arsse/index.md | 15 +----- 15 files changed, 121 insertions(+), 124 deletions(-) delete mode 100644 docs/en/020_Getting_Started/010_Requirements.md delete mode 100644 docs/en/025_Using_The_Arsse/030_Keeping_Newsfeeds_Up_to_Date.md create mode 100644 docs/en/025_Using_The_Arsse/030_Other_Topics.md delete mode 100644 docs/en/025_Using_The_Arsse/040_Upgrading_to_a_New_Version.md diff --git a/RoboFile.php b/RoboFile.php index 8d6b8c0a..41473c4f 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -256,7 +256,7 @@ class RoboFile extends \Robo\Tasks { } // start a task collection and create a temporary directory $t = $this->collectionBuilder(); - $dir = $t->workDir("/home/jking/temp").\DIRECTORY_SEPARATOR; + $dir = $t->tmpDir().\DIRECTORY_SEPARATOR; $base = $dir."arsse-$version".\DIRECTORY_SEPARATOR; // start by extracting the tarball $t->addCode(function() use ($tarball, $dir, $base) { @@ -268,7 +268,7 @@ class RoboFile extends \Robo\Tasks { $t->addTask($this->taskPack($dir."arsse_$version.orig.tar.gz")->addDir("arsse-$version", $base)); // copy Debian files to lower down in the tree $t->addTask($this->taskFilesystemStack()->mirror($base."dist/debian", $base."debian")); - //$t->addTask($this->taskExec("deber")->dir($dir)); + $t->addTask($this->taskExec("deber")->dir($dir)); return $t->run(); } diff --git a/docs/en/020_Getting_Started/010_Requirements.md b/docs/en/020_Getting_Started/010_Requirements.md deleted file mode 100644 index 4f528298..00000000 --- a/docs/en/020_Getting_Started/010_Requirements.md +++ /dev/null @@ -1,16 +0,0 @@ -The Arsse has the following requirements: - -- A Linux server running Nginx or Apache 2.4 -- PHP 7.1.0 or later with the following extensions: - - [intl](http://php.net/manual/en/book.intl.php), [json](http://php.net/manual/en/book.json.php), [hash](http://php.net/manual/en/book.hash.php), and [dom](http://php.net/manual/en/book.dom.php) - - [simplexml](http://php.net/manual/en/book.simplexml.php), and [iconv](http://php.net/manual/en/book.iconv.php) - - One of: - - [sqlite3](http://php.net/manual/en/book.sqlite3.php) or [pdo_sqlite](http://php.net/manual/en/ref.pdo-sqlite.php) for SQLite databases - - [pgsql](http://php.net/manual/en/book.pgsql.php) or [pdo_pgsql](http://php.net/manual/en/ref.pdo-pgsql.php) for PostgreSQL 10 or later databases - - [mysqli](http://php.net/manual/en/book.mysqli.php) or [pdo_mysql](http://php.net/manual/en/ref.pdo-mysql.php) for MySQL/Percona 8.0.11 or later databases - - [curl](http://php.net/manual/en/book.curl.php) (optional) -- Privileges either to create and run systemd services, or to run cron jobs - -Instructions for how to satisfy the PHP extension requirements for Debian and Arch Linux systems are included in the next section. - -It is also be possible to run The Arsse on other operating systems (including Windows) and with other Web servers, but the configuration required to do so is not documented in this manual. diff --git a/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md b/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md index 191af00d..4044daeb 100644 --- a/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md +++ b/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md @@ -22,7 +22,7 @@ sudo systemctl restart php-fpm arsse Note that the above is the most concise process, not necessarily the recommended one. In particular [it is recommended](https://wiki.archlinux.org/title/PHP#Extensions) to use `/etc/php/conf.d/` to enable PHP extensions rather than editing `php.ini` as done above. -The PHP extensions listed in [the requirements](/en/Getting_Started/Requirements) not mentioned above are compiled into Arch's PHP binaries and thus always enabled. +The PHP extensions listed in [the requirements](/en/Getting_Started/index) not mentioned above are compiled into Arch's PHP binaries and thus always enabled. # Web server configuration @@ -34,4 +34,14 @@ If using a database other than SQLite, you will likely want to [set it up](/en/G In order for The Arsse to serve users, those users [must be created](/en/Using_The_Arsse/Managing_Users). -You may also want to review the `config.defaults.php` file included in `/etc/webapps/arsse/` or consult [the documentation for the configuration file](/en/Getting_Started/Configuration), though The Arsse should function with the default configuration. \ No newline at end of file +You may also want to review the `config.defaults.php` file included in `/etc/webapps/arsse/` or consult [the documentation for the configuration file](/en/Getting_Started/Configuration), though The Arsse should function with the default configuration. + +# Upgrading + +Upgrading The Arsse is done as like any other package. By default The Arsse will perform any required database schema upgrades when the new version is executed, so the service does need to be restarted: + +```sh +sudo systemctl restart arsse +``` + +Occasionally changes to Web server configuration have been required, such as when new protocols become supported; these changes are always explicit in the `UPGRADING` file. diff --git a/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md b/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md index e753d438..9b9e2111 100644 --- a/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md +++ b/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md @@ -8,7 +8,7 @@ Installation from source code is also possible, but the release packages are rec # Installation -In order for The Arsse to function correctly, its requirements must first be satisfied. The following series of commands should do so: +Presently installing The Arsse on Debian systems is a manual process. The first step is to install its dependencies: ```sh # Install PHP; this assumes the FastCGI process manager will be used @@ -19,20 +19,39 @@ sudo apt install php-intl php-json php-xml php-curl sudo apt install php-sqlite3 php-pgsql php-mysql ``` -Then, it's a simple matter of unpacking the archive someplace (`/usr/share/arsse` is the recommended location on Debian systems, but it can be anywhere) and setting permissions: +Next its files must be unpacked into their requisite locations: ```sh # Unpack the archive sudo tar -xzf arsse-x.x.x.tar.gz -C "/usr/share" -# Make the user running the Web server the owner of the files -sudo chown -R www-data:www-data "/usr/share/arsse" -# Ensure the owner can create files such as the SQLite database -sudo chmod o+rwX "/usr/share/arsse" +# Create necessary directories +sudo mkdir -p /etc/arsse /etc/sysusers.d /etc/tmpfiles.d +# Find the PHP version +php_ver=`phpquery -V` +# Move configuration files to their proper locations +cd /usr/share/arsse/dist +sudo mv systemd/* /etc/systemd/system/ +sudo mv sysusers.conf /etc/sysusers.d/arsse.conf +sudo mv tmpfiles.conf /etc/tmpfiles.d/arsse.conf +sudo mv config.php nginx apache /etc/arsse/ +sudo mv php-fpm.conf /etc/php/$php_ver/fpm/pool.d/arsse.conf +# Move the administration executable +sudo mv arsse /usr/bin/ +``` + +Finally, services must be restarted to apply the new configurations, and The Arsse's service also started: + +```sh +sudo systemctl restart systemd-sysusers +sudo systemctl restart systemd-tmpfiles +sudo systemctl restart php$php_ver-fpm +sudo systemctl reenable arsse +sudo systemctl restart arsse ``` # Web server configuration -Sample configuration for both Nginx and Apache HTTPd can be found in `dist/nginx/` and `dist/apache/`, respectively. The `example.conf` files are basic virtual host examples; the other files they include should normally be usable without modification, but may be modified if desired. +Sample configuration for both Nginx and Apache HTTPd can be found in `/etc/arsse/nginx/` and `/etc/arsse/apache/`, respectively. The `example.conf` files are basic virtual host examples; the other files they include should normally be usable without modification, but may be modified if desired. In order to use Apache HTTPd the FastCGI proxy module must be enabled and the server restarted: @@ -43,7 +62,6 @@ sudo systemctl restart apache2 No additional set-up is required for Nginx. - # Next steps If using a database other than SQLite, you will likely want to [set it up](/en/Getting_Started/Database_Setup) before doing anything else. @@ -52,4 +70,14 @@ In order for The Arsse to serve users, those users [must be created](/en/Using_T You may also want to review the `config.defaults.php` file included in the download package and create [a configuration file](/en/Getting_Started/Configuration), though The Arsse can function even without using a configuration file. -Finally, The Arsse's [newsfeed refreshing service](/en/Using_The_Arsse/Keeping_Newsfeeds_Up_to_Date) needs to be installed in order for news to actually be fetched from the Internet. +# Upgrading + +Upgrading The Arsse is simple: + +1. Download the latest release +2. Check the `UPGRADING` file for any special notes +3. Stop the newsfeed refreshing service if it is running +4. Install the new version per the process above +6. Start the newsfeed refreshing service + +By default The Arsse will perform any required database schema upgrades when the new version is executed. Occasionally changes to Web server configuration have been required, such as when new protocols become supported; these changes are always explicit in the `UPGRADING` file. diff --git a/docs/en/020_Getting_Started/040_Database_Setup/000_SQLite.md b/docs/en/020_Getting_Started/040_Database_Setup/000_SQLite.md index 91971fac..1aa5992c 100644 --- a/docs/en/020_Getting_Started/040_Database_Setup/000_SQLite.md +++ b/docs/en/020_Getting_Started/040_Database_Setup/000_SQLite.md @@ -8,7 +8,7 @@
Minimum version
3.8.3
Configuration
-
General, Specific
+
General, Specific
SQLite requires very little set-up. By default the database will be created at the root of The Arsse's program directory (e.g. `/usr/share/arsse/arsse.db`), but this can be changed with the [`dbSQLite3File` setting](/en/Getting_Started/Configuration#page_dbSQLite3File). diff --git a/docs/en/020_Getting_Started/040_Database_Setup/010_PostgreSQL.md b/docs/en/020_Getting_Started/040_Database_Setup/010_PostgreSQL.md index 51272426..135fd243 100644 --- a/docs/en/020_Getting_Started/040_Database_Setup/010_PostgreSQL.md +++ b/docs/en/020_Getting_Started/040_Database_Setup/010_PostgreSQL.md @@ -8,7 +8,7 @@
Minimum version
10
Configuration
-
General, Specific
+
General, Specific
If for whatever reason an SQLite database does not suit your configuration, PostgreSQL is the best alternative. It is functionally equivalent to SQLite in every way. diff --git a/docs/en/020_Getting_Started/040_Database_Setup/020_MySQL.md b/docs/en/020_Getting_Started/040_Database_Setup/020_MySQL.md index 4676e346..1eafe1ed 100644 --- a/docs/en/020_Getting_Started/040_Database_Setup/020_MySQL.md +++ b/docs/en/020_Getting_Started/040_Database_Setup/020_MySQL.md @@ -8,7 +8,7 @@
Minimum version
8.0.11
Configuration
-
General, Specific
+
General, Specific
While MySQL can be used as a database for The Arsse, this is **not recommended** due to MySQL's technical limitations. It is fully functional, but may fail with some newsfeeds where other database systems do not. Additionally, it is particularly important before upgrading from one version of The Arsse to the next to back up your database: a failure in a database upgrade can corrupt your database much more easily than when using other database systems. diff --git a/docs/en/020_Getting_Started/050_Configuration.md b/docs/en/020_Getting_Started/050_Configuration.md index a07442c8..f93a2a7c 100644 --- a/docs/en/020_Getting_Started/050_Configuration.md +++ b/docs/en/020_Getting_Started/050_Configuration.md @@ -321,7 +321,7 @@ It is also possible to specify the fully-qualified name of a class which impleme The interval the newsfeed fetching service observes between checks for new articles. Note that requests to foreign servers are not necessarily made at this frequency: each newsfeed is assigned its own time at which to be next retrieved. This setting instead defines the length of time the fetching service will sleep between periods of activity. -Consult "[How Often Newsfeeds Are Fetched](/en/Using_The_Arsse/Keeping_Newsfeeds_Up_to_Date#page_Appendix-how-often-newsfeeds-are-fetched)" for details on how often newsfeeds are fetched. +Consult "[How Often Newsfeeds Are Fetched](/en/Using_The_Arsse/Other_Topics#page_How_often_newsfeeds_are_fetched)" for details on newsfeed update frequency. ### serviceQueueWidth diff --git a/docs/en/020_Getting_Started/index.md b/docs/en/020_Getting_Started/index.md index fa592565..5aec617e 100644 --- a/docs/en/020_Getting_Started/index.md +++ b/docs/en/020_Getting_Started/index.md @@ -1,3 +1,20 @@ -Presently installing and setting up The Arsse is a manual process. We hope to have pre-configured installation packages available for various operating systems eventually, but for now the pages in this section should help get you up and running. +Presently installing and setting up The Arsse involves some manual labour. We have packages for Arch Linux and hope to have installation packages available for other operating systems eventually, but for now the pages in this section should help get you up and running on Arch Linux or Debian-based systems, with Nginx or Apache HTTPd. -Though The Arsse itself makes no assumptions about the operating system which hosts it, we use and have the most experience with Debian; the instructions contained here therefore are for Debian systems and will probably either not work with other systems or not be consistent with their conventions. Nevertheless, they should still serve as a useful guide. +It is also be possible to run The Arsse on other operating systems (including Windows) and with other Web servers, but the configuration required to do so is not documented in this manual. + +# Requirements + +For reference, The Arsse has the following requirements: + +- A Linux server running Nginx or Apache 2.4 +- PHP 7.1.0 or later with the following extensions: + - [intl](http://php.net/manual/en/book.intl.php), [json](http://php.net/manual/en/book.json.php), [hash](http://php.net/manual/en/book.hash.php), and [dom](http://php.net/manual/en/book.dom.php) + - [simplexml](http://php.net/manual/en/book.simplexml.php), and [iconv](http://php.net/manual/en/book.iconv.php) + - One of: + - [sqlite3](http://php.net/manual/en/book.sqlite3.php) or [pdo_sqlite](http://php.net/manual/en/ref.pdo-sqlite.php) for SQLite databases + - [pgsql](http://php.net/manual/en/book.pgsql.php) or [pdo_pgsql](http://php.net/manual/en/ref.pdo-pgsql.php) for PostgreSQL 10 or later databases + - [mysqli](http://php.net/manual/en/book.mysqli.php) or [pdo_mysql](http://php.net/manual/en/ref.pdo-mysql.php) for MySQL/Percona 8.0.11 or later databases + - [curl](http://php.net/manual/en/book.curl.php) (optional) +- Privileges either to create and run systemd services, or to run cron jobs + +Instructions for how to satisfy the PHP extension requirements for Arch Linux and Debian systems are included in the next section. diff --git a/docs/en/025_Using_The_Arsse/010_Managing_Users.md b/docs/en/025_Using_The_Arsse/010_Managing_Users.md index 53f2da09..b5d529f3 100644 --- a/docs/en/025_Using_The_Arsse/010_Managing_Users.md +++ b/docs/en/025_Using_The_Arsse/010_Managing_Users.md @@ -9,13 +9,13 @@ This section describes in brief some CLI commands. Please read [the general note When first installed, The Arsse has no users configured. You may add users by executing the following command: ```sh -sudo -u www-data php arsse.php user add "user@example.com" "example password" +sudo arsse user add "user@example.com" "example password" ``` The password argument is optional: if no password is provided, a random one is generated and printed out: ```console -$ sudo -u www-data php arsse.php user add "jane.doe" +$ sudo arsse user add "jane.doe" Ji0ivMYqi6gKxQK1MHuE ``` @@ -24,13 +24,13 @@ Ji0ivMYqi6gKxQK1MHuE Setting a user's password is nearly identical to adding a user: ```sh -sudo -u www-data php arsse.php user set-pass "user@example.com" "new password" +sudo arsse user set-pass "user@example.com" "new password" ``` As when adding a user, the password argument is optional: if no password is provided, a random one is generated and printed out: ```console -$ sudo -u www-data php arsse.php user set-pass "jane.doe" +$ sudo arsse user set-pass "jane.doe" Ummn173XjbJT4J3Gnx0a ``` @@ -39,13 +39,13 @@ Ummn173XjbJT4J3Gnx0a Before a user can make use of [the Fever protocol](/en/Supported_Protocols/Fever), a Fever-specific password for that user must be set. It is _highly recommended_ that this not be the samer as the user's main password. The password can be set by adding the `--fever` option to the normal password-changing command: ```sh -sudo -u www-data php arsse.php user set-pass --fever "user@example.com" "fever password" +sudo arsse user set-pass --fever "user@example.com" "fever password" ``` As when setting a main password, the password argument is optional: if no password is provided, a random one is generated and printed out: ```console -$ sudo -u www-data php arsse.php user set-pass --fever "jane.doe" +$ sudo arsse user set-pass --fever "jane.doe" YfZJHq4fNTRUKDYhzQdR ``` @@ -54,16 +54,16 @@ YfZJHq4fNTRUKDYhzQdR [Miniflux](/en/Supported_Protocols/Miniflux) clients may optionally log in using tokens: randomly-generated strings which act as persistent passwords. For now these must be generated using the command-line interface: ```console -$ sudo -u www-data php arsse.php token create "jane.doe" +$ sudo arsse token create "jane.doe" xRK0huUE9KHNHf_x_H8JG0oRDo4t_WV44whBtr8Ckf0= ``` Multiple tokens may be generated for use with different clients, and descriptive labels can be assigned for later identification: ```console -$ sudo -u www-data php arsse.php token create "jane.doe" Newsflash +$ sudo arsse token create "jane.doe" Newsflash xRK0huUE9KHNHf_x_H8JG0oRDo4t_WV44whBtr8Ckf0= -$ sudo -u www-data php arsse.php token create "jane.doe" Reminiflux +$ sudo arsse token create "jane.doe" Reminiflux L7asI2X_d-krinGJd1GsiRdFm2o06ZUlgD22H913hK4= ``` @@ -76,13 +76,13 @@ Users may also have various metadata properties set. These largely exist for com The flag may be changed using the following command: ```sh -sudo -u www-data php arsse.php user set "jane.doe" admin true +sudo arsse user set "jane.doe" admin true ``` As a shortcut it is also possible to create administrators directly: ```sh -sudo -u www-data php arsse.php user add "user@example.com" "example password" --admin +sudo arsse user add "user@example.com" "example password" --admin ``` Please consult the integrated help for more details on metadata and their effects. diff --git a/docs/en/025_Using_The_Arsse/020_Importing_and_Exporting.md b/docs/en/025_Using_The_Arsse/020_Importing_and_Exporting.md index f1afb7d6..482c477f 100644 --- a/docs/en/025_Using_The_Arsse/020_Importing_and_Exporting.md +++ b/docs/en/025_Using_The_Arsse/020_Importing_and_Exporting.md @@ -9,7 +9,7 @@ This section describes in brief some CLI commands. Please read [the general note It's possible to import not only newsfeeds but also folders and Fever groups using OPML files. The process is simple: ```sh -sudo -u www-data php arsse.php import "user@example.com" "subscriptions.opml" +sudo arsse import "user@example.com" "subscriptions.opml" ``` The importer is forgiving, but some OPML files may fail, with the reason printed out. Files are either imported in total, or not at all. @@ -19,7 +19,7 @@ The importer is forgiving, but some OPML files may fail, with the reason printed It's possible to export not only newsfeeds but also folders and Fever groups to OPML files. The process is simple: ```sh -sudo -u www-data php arsse.php export "user@example.com" "subscriptions.opml" +sudo arsse export "user@example.com" "subscriptions.opml" ``` The output might look like this: @@ -46,9 +46,9 @@ Not all protocols supported by The Arsse allow modifying newsfeeds or folders, e ```sh # export your newsfeeds -sudo -u www-data php arsse.php export "user@example.com" "subscriptions.opml" +sudo arsse export "user@example.com" "subscriptions.opml" # make any changes you want in your editor of choice nano "subscriptions.opml" # re-import the modified information -sudo -u www-data php arsse.php import "user@example.com" "subscriptions.opml" --replace +sudo arsse import "user@example.com" "subscriptions.opml" --replace ``` diff --git a/docs/en/025_Using_The_Arsse/030_Keeping_Newsfeeds_Up_to_Date.md b/docs/en/025_Using_The_Arsse/030_Keeping_Newsfeeds_Up_to_Date.md deleted file mode 100644 index 837de686..00000000 --- a/docs/en/025_Using_The_Arsse/030_Keeping_Newsfeeds_Up_to_Date.md +++ /dev/null @@ -1,49 +0,0 @@ -[TOC] - -# Preface - -In normal operation The Arsse is expected to regularly check whether newsfeeds might have new articles, then fetch them and process them to present new or updated articles to clients. This can be achieved either by having The Arsse operate a persistent background process (termed a [daemon](https://en.wikipedia.org/wiki/Daemon_(computing)) or service), or by using an external scheduler to periodically perform single checks. Normally a daemon is preferred. - -There are many ways to administer daemons, and many schedulers can be used. This section outlines a few, but many other arrangements are possible. - -# As a daemon via systemd - -The Arsse includes a sample systemd service unit file which can be used to quickly get a daemon running with the following procedure: - -```sh -# Copy the service unit -sudo cp "/usr/share/arsse/dist/arsse.service" "/etc/systemd/system" -# Modify the unit file if needed -sudoedit "/etc/systemd/system/arsse.service" -# Enable and start the service -sudo systemctl enable --now arsse -``` - -The Arsse's feed updater can then be manipulated as with any other service. Consult [the `systemctl` manual](https://www.freedesktop.org/software/systemd/man/systemctl.html) for details. - -# As a cron job - -Keeping newsfeeds updated with [cron](https://en.wikipedia.org/wiki/Cron) is not difficult. Simply run the following command: - - -```sh -sudo crontab -u www-data -e -``` - -And add a line such as this one: - -``` -*/2 * * * * /usr/bin/env php /usr/share/arsse/arsse.php refresh-all -``` - -Thereafter The Arsse's will be scheduled to check newsfeeds every two minutes. Consult the manual pages for the `crontab` [format](http://man7.org/linux/man-pages/man5/crontab.5.html) and [command](http://man7.org/linux/man-pages/man1/crontab.1.html) for details. - -# Appendix: how often newsfeeds are fetched - -Though by default The Arsse will wake up every two minutes, newsfeeds are not actually downloaded so frequently. Instead, each newsfeed is assigned a time at which it should next be fetched, and once that time is reached a [conditional request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests) is made. The interval between requests for a particular newsfeed can vary from 15 minutes to 24 hours based on multiple factors such as: - -- The length of time since the newsfeed last changed -- The interval between publishing of articles in the newsfeed -- Whether the last fetch or last several fetches resulted in error - -As a general rule, newsfeeds which change frequently are checked frequently, and those which change seldom are fetched at most daily. diff --git a/docs/en/025_Using_The_Arsse/030_Other_Topics.md b/docs/en/025_Using_The_Arsse/030_Other_Topics.md new file mode 100644 index 00000000..1fe0c4c3 --- /dev/null +++ b/docs/en/025_Using_The_Arsse/030_Other_Topics.md @@ -0,0 +1,30 @@ +[TOC] + +# Refreshing newsfeeds with a cron job + +Normally The Arsse has a systemd service which checks newsfeeds for updates and processes them into its database for the user. If for whatever reason this is not practical a [cron](https://en.wikipedia.org/wiki/Cron) job may be used instead. + +Keeping newsfeeds updated with cron is not difficult. Simply run the following command: + + +```sh +sudo crontab -u arsse -e +``` + +And add a line such as this one: + +``` +*/2 * * * * /usr/bin/arsse refresh-all +``` + +Thereafter The Arsse's will be scheduled to check newsfeeds every two minutes. Consult the manual pages for the `crontab` [format](http://man7.org/linux/man-pages/man5/crontab.5.html) and [command](http://man7.org/linux/man-pages/man1/crontab.1.html) for details. + +# How often newsfeeds are fetched + +Though by default The Arsse will wake up every two minutes, newsfeeds are not actually downloaded so frequently. Instead, each newsfeed is assigned a time at which it should next be fetched, and once that time is reached a [conditional request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests) is made. The interval between requests for a particular newsfeed can vary from 15 minutes to 24 hours based on multiple factors such as: + +- The length of time since the newsfeed last changed +- The interval between publishing of articles in the newsfeed +- Whether the last fetch or last several fetches resulted in error + +As a general rule, newsfeeds which change frequently are checked frequently, and those which change seldom are fetched at most daily. diff --git a/docs/en/025_Using_The_Arsse/040_Upgrading_to_a_New_Version.md b/docs/en/025_Using_The_Arsse/040_Upgrading_to_a_New_Version.md deleted file mode 100644 index 2b1afa14..00000000 --- a/docs/en/025_Using_The_Arsse/040_Upgrading_to_a_New_Version.md +++ /dev/null @@ -1,12 +0,0 @@ -Upgrading The Arsse is usually simple: - -1. Download the latest release -2. Check the `UPGRADING` file for any special notes -3. Stop the newsfeed refreshing service if it is running -4. Extract the new version on top of the old one -5. Ensure permissions are still correct -6. Restart the newsfeed refreshing service - -By default The Arsse will perform any required database schema upgrades when the new version is executed, and release packages contain all newly required library dependencies. - -Occasionally changes to Web server configuration have been required, when new protocols become supported; such changes are always explicit in the `UPGRADING` file diff --git a/docs/en/025_Using_The_Arsse/index.md b/docs/en/025_Using_The_Arsse/index.md index 923d6666..a6c215ef 100644 --- a/docs/en/025_Using_The_Arsse/index.md +++ b/docs/en/025_Using_The_Arsse/index.md @@ -2,19 +2,8 @@ This section details a few administrative tasks which may need to be performed after installing The Arsse. As no Web-based administrative interface is included, these tasks are generally performed via command line interface. -Though this section describes some commands briefly, complete documentation of The Arsse's command line interface is not included in this manual. Documentation for CLI commands can instead be viewed with the CLI itself by executing `php arsse.php --help`. +Though this section describes some commands briefly, complete documentation of The Arsse's command line interface is not included in this manual. Documentation for CLI commands can instead be viewed with the CLI itself by executing `arsse --help`. # A Note on Command Invocation -Particularly if using an SQLite database, it's important that administrative commands be executed as the same user who owns The Arsse's files. To that end the examples in this section all use the verbose formulation `sudo -u www-data php arsse.php` (with `www-data` being the user under which Web servers run in Debian), but it is possible to simplify invocation to `sudo arsse` if an executable file named `arsse` is created somewhere in the sudo path with the following content: - -```php -#! /usr/bin/env php - Date: Sun, 23 May 2021 12:46:13 -0400 Subject: [PATCH 257/366] Update changelog --- CHANGELOG | 1 + dist/arch/PKGBUILD | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index eea0e88b..e31169f5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ Bug fixes: Changes: - Packages for Arch Linux are now available (see manual for details) +- Numerous improvements to the manual Version 0.9.1 (2021-03-18) ========================== diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index 5f7154b5..cc53ae23 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -31,7 +31,7 @@ md5sums=("SKIP") package() { # define runtime dependencies depends=("php" "php-intl" "php-sqlite" "php-fpm") - # create most directories necessary forn the final package + # create most directories necessary for the final package cd "$pkgdir" mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "etc/php/php-fpm.d/" "etc/webapps/arsse" "etc/webapps/arsse/nginx" #copy requisite files From 0236b42052980ec10a4bfd4e2bd0e35afd12fef1 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 23 May 2021 17:57:50 -0400 Subject: [PATCH 258/366] Use tmpfiles to create link to config file --- dist/arch/PKGBUILD | 11 +++-------- dist/tmpfiles.conf | 4 +++- lib/Arsse.php | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index cc53ae23..7c291362 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -1,5 +1,6 @@ +# Maintainer: J. King pkgname="arsse" -pkgver=0.9.1 +pkgver=0.9.2 pkgrel=1 epoch= pkgdesc="Multi-protocol RSS/Atom newsfeed synchronization server" @@ -23,9 +24,7 @@ backup=("etc/webapps/arsse/config.php" "etc/webapps/arsse/apache/example.conf" "etc/webapps/arsse/apache/arsse.conf" "etc/webapps/arsse/apache/arsse-loc.conf") -install= -changelog= -source=("arsse-0.9.1.tar.gz") +source=("arsse-0.9.2.tar.gz") md5sums=("SKIP") package() { @@ -45,10 +44,6 @@ package() { cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" cp -r dist/nginx dist/apache config.defaults.php "$pkgdir/etc/webapps/arsse" cd "$pkgdir" - # adjust permissions, just in case - chmod -R u=rwX,g=rX,o=rX * - # create a symbolic link for the configuration file - ln -sT "/etc/webapps/arsse/config.php" "usr/share/webapps/arsse/config.php" # copy files requiring special permissions cd "$srcdir/arsse" install -Dm755 dist/arsse "$pkgdir/usr/bin" diff --git a/dist/tmpfiles.conf b/dist/tmpfiles.conf index fa1af726..bb629eb0 100644 --- a/dist/tmpfiles.conf +++ b/dist/tmpfiles.conf @@ -1 +1,3 @@ -z /etc/arsse/config.php - root arsse - - +z /usr/bin/arsse 0755 root arsse - - +z /etc/arsse/config.php 0640 root arsse - - +L /usr/share/arsse/config.php - root arsse - /etc/arsse/config.php \ No newline at end of file diff --git a/lib/Arsse.php b/lib/Arsse.php index 84223cb3..331944aa 100644 --- a/lib/Arsse.php +++ b/lib/Arsse.php @@ -7,7 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse; class Arsse { - public const VERSION = "0.9.1"; + public const VERSION = "0.9.2"; /** @var Factory */ public static $obj; From 9eabfd0f270722839e63de391058e3f8f5d48736 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 23 May 2021 19:01:51 -0400 Subject: [PATCH 259/366] Fix up sed usage in PKGBUILD --- dist/arch/PKGBUILD | 10 +++--- dist/arch/PKGBUILD-git | 72 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 dist/arch/PKGBUILD-git diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index 7c291362..064a8b4b 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -46,11 +46,11 @@ package() { cd "$pkgdir" # copy files requiring special permissions cd "$srcdir/arsse" - install -Dm755 dist/arsse "$pkgdir/usr/bin" + install -Dm755 dist/arsse "$pkgdir/usr/bin/arsse" install -Dm640 dist/arch/config.php "$pkgdir/etc/webapps/arsse" # patch generic configuration files to use Arch-specific paths and identifiers - sed -ise 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"* - sed -ise 's/\/var\/run\/php\//\/run\/php-fpm\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf" - sed -ise 's/www-data/http/' "$pkgdir/etc/php/php-fpm.d/arsse.conf" - sed -ie 's/^WorkingDirectory=.*$/WorkingDirectory=\/usr\/share\/webapps\/arsse/' -e 's/^ConfigurationDirectory=.*$/ConfigurationDirectory=webapps\/arsse/' "$pkgdir/usr/lib/systemd/system/arsse-fetch.service" + sed -i -se 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"* + sed -i -se 's/\/var\/run\/php\//\/run\/php-fpm\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf" + sed -i -se 's/www-data/http/' "$pkgdir/etc/php/php-fpm.d/arsse.conf" + sed -i -e 's/^WorkingDirectory=.*$/WorkingDirectory=\/usr\/share\/webapps\/arsse/' -e 's/^ConfigurationDirectory=.*$/ConfigurationDirectory=webapps\/arsse/' "$pkgdir/usr/lib/systemd/system/arsse-fetch.service" } diff --git a/dist/arch/PKGBUILD-git b/dist/arch/PKGBUILD-git new file mode 100644 index 00000000..943cf515 --- /dev/null +++ b/dist/arch/PKGBUILD-git @@ -0,0 +1,72 @@ +# Maintainer: J. King +pkgname="arsse-git" +pkgver=0.9.1.r44.g0236b42 +pkgrel=1 +epoch= +pkgdesc="Multi-protocol RSS/Atom newsfeed synchronization server" +arch=("any") +url="https://thearsse.com/" +license=("MIT") +provides=("arsse") +conflicts=("arsse") +depends=("php" "php-intl" "php-sqlite") +makedepends=("composer") +checkdepends=() +optdepends=("nginx: HTTP server" + "apache: HTTP server" + "percona-server: Alternate database" + "postgresql: Alternate database" + "php-pgsql: PostgreSQL database support") +backup=("etc/webapps/arsse/config.php" + "etc/php/php-fpm.d/arsse.conf" + "etc/webapps/arsse/nginx/example.conf" + "etc/webapps/arsse/nginx/arsse.conf" + "etc/webapps/arsse/nginx/arsse-loc.conf" + "etc/webapps/arsse/nginx/arsse-fcgi.conf" + "etc/webapps/arsse/apache/example.conf" + "etc/webapps/arsse/apache/arsse.conf" + "etc/webapps/arsse/apache/arsse-loc.conf") +source=("git+https://code.mensbeam.com/MensBeam/arsse/") +md5sums=("SKIP") + +pkgver() { + cd "arsse" + git describe --tags | sed 's/\([^-]*-g\)/r\1/;s/-/./g' +} + +build() { + cd "$srcdir/arsse" + composer install + ./robo manual + composer install --no-dev -o --no-scripts + php arsse.php conf save-defaults config.defaults.php + rm -r vendor/bin +} + +package() { + # define runtime dependencies + depends=("php" "php-intl" "php-sqlite" "php-fpm") + # create most directories necessary for the final package + cd "$pkgdir" + mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "etc/php/php-fpm.d/" "etc/webapps/arsse" "etc/webapps/arsse/nginx" + #copy requisite files + cd "$srcdir/arsse" + cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" + cp -r manual/* "$pkgdir/usr/share/doc/arsse" + cp LICENSE AUTHORS "$pkgdir/usr/share/licenses/arsse" + cp dist/systemd/* "$pkgdir/usr/lib/systemd/system" + cp dist/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" + cp dist/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" + cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" + cp -r dist/nginx dist/apache config.defaults.php "$pkgdir/etc/webapps/arsse" + cd "$pkgdir" + # copy files requiring special permissions + cd "$srcdir/arsse" + install -Dm755 dist/arsse "$pkgdir/usr/bin/arsse" + install -Dm640 dist/arch/config.php "$pkgdir/etc/webapps/arsse" + # patch generic configuration files to use Arch-specific paths and identifiers + sed -i -se 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"* + sed -i -se 's/\/var\/run\/php\//\/run\/php-fpm\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf" + sed -i -se 's/www-data/http/' "$pkgdir/etc/php/php-fpm.d/arsse.conf" + sed -i -e 's/^WorkingDirectory=.*$/WorkingDirectory=\/usr\/share\/webapps\/arsse/' -e 's/^ConfigurationDirectory=.*$/ConfigurationDirectory=webapps\/arsse/' "$pkgdir/usr/lib/systemd/system/arsse-fetch.service" +} From 2ccfb1fd33c550d09bc2779bb814876c4277106e Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 23 May 2021 22:03:47 -0400 Subject: [PATCH 260/366] Fix packaging process --- RoboFile.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/RoboFile.php b/RoboFile.php index 41473c4f..00883f76 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -186,9 +186,7 @@ class RoboFile extends \Robo\Tasks { // perform Composer installation in the temp location with dev dependencies $t->addTask($this->taskComposerInstall()->arg("-q")->dir($dir)); // generate the manual - $t->addCode(function() { - return $this->manual(["-q"]); - }); + $t->addTask($this->taskExec("./robo manual -q")->dir($dir)); // perform Composer installation in the temp location for final output $t->addTask($this->taskComposerInstall()->dir($dir)->noDev()->optimizeAutoloader()->arg("--no-scripts")->arg("-q")); // delete unwanted files @@ -231,18 +229,18 @@ class RoboFile extends \Robo\Tasks { /** Packages a release tarball into an Arch package */ public function packageArch(string $tarball): Result { - $dir = dirname($tarball); + $dir = dirname($tarball).\DIRECTORY_SEPARATOR; // start a collection $t = $this->collectionBuilder(); // extract the PKGBUILD from the tarball $t->addCode(function() use ($tarball, $dir) { // because Robo doesn't support extracting a single file we have to do it ourselves - (new \Archive_Tar($tarball))->extractList("arsse/dist/arch/PKGBUILD", $dir,"arsse/dist/arch/", false); + (new \Archive_Tar($tarball))->extractList("arsse/dist/arch/PKGBUILD", $dir, "arsse/dist/arch/", false); // perform a do-nothing filesystem operation since we need a Robo task result - return $this->taskFilesystemStack()->chmod("PKGBUILD", 0644)->dir($dir)->run(); - })->completion($this->taskFilesystemStack()->remove("PKGBUILD")->dir($dir)); + return $this->taskFilesystemStack()->chmod($dir."PKGBUILD", 0644)->run(); + })->completion($this->taskFilesystemStack()->remove($dir."PKGBUILD")); // build the package - $t->taskExec("makepkg -Ccf")->dir($dir); + $t->addTask($this->taskExec("makepkg -Ccf")->dir($dir)); return $t->run(); } From 1055611940323fcc9450a16eb384583b94c1fdb2 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 24 May 2021 12:40:11 -0400 Subject: [PATCH 261/366] Add version constraints to Arch dependencies --- dist/arch/PKGBUILD | 8 ++++---- dist/arch/PKGBUILD-git | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index 064a8b4b..e3d1a4c7 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -11,10 +11,10 @@ depends=() makedepends=() checkdepends=() optdepends=("nginx: HTTP server" - "apache: HTTP server" + "apache>=2.4: HTTP server" "percona-server: Alternate database" - "postgresql: Alternate database" - "php-pgsql: PostgreSQL database support") + "postgresql>=10: Alternate database" + "php-pgsql>=7.1: PostgreSQL database support") backup=("etc/webapps/arsse/config.php" "etc/php/php-fpm.d/arsse.conf" "etc/webapps/arsse/nginx/example.conf" @@ -29,7 +29,7 @@ md5sums=("SKIP") package() { # define runtime dependencies - depends=("php" "php-intl" "php-sqlite" "php-fpm") + depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1" "php-fpm>=7.1") # create most directories necessary for the final package cd "$pkgdir" mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "etc/php/php-fpm.d/" "etc/webapps/arsse" "etc/webapps/arsse/nginx" diff --git a/dist/arch/PKGBUILD-git b/dist/arch/PKGBUILD-git index 943cf515..dcfdbd89 100644 --- a/dist/arch/PKGBUILD-git +++ b/dist/arch/PKGBUILD-git @@ -1,22 +1,22 @@ # Maintainer: J. King pkgname="arsse-git" -pkgver=0.9.1.r44.g0236b42 +pkgver=0.9.2 pkgrel=1 epoch= -pkgdesc="Multi-protocol RSS/Atom newsfeed synchronization server" +pkgdesc="Multi-protocol RSS/Atom newsfeed synchronization server, bugfix-testing version" arch=("any") url="https://thearsse.com/" license=("MIT") provides=("arsse") conflicts=("arsse") -depends=("php" "php-intl" "php-sqlite") +depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1") makedepends=("composer") checkdepends=() optdepends=("nginx: HTTP server" - "apache: HTTP server" + "apache>=2.4: HTTP server" "percona-server: Alternate database" - "postgresql: Alternate database" - "php-pgsql: PostgreSQL database support") + "postgresql>=10: Alternate database" + "php-pgsql>=7.1: PostgreSQL database support") backup=("etc/webapps/arsse/config.php" "etc/php/php-fpm.d/arsse.conf" "etc/webapps/arsse/nginx/example.conf" @@ -45,7 +45,7 @@ build() { package() { # define runtime dependencies - depends=("php" "php-intl" "php-sqlite" "php-fpm") + depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1" "php-fpm>=7.1") # create most directories necessary for the final package cd "$pkgdir" mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "etc/php/php-fpm.d/" "etc/webapps/arsse" "etc/webapps/arsse/nginx" From f0bf55f9cffa08e0923286137ebdfaa2c069e3ff Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 24 May 2021 14:07:11 -0400 Subject: [PATCH 262/366] Add ExecStart to parent systemd unit --- dist/systemd/arsse.service | 1 + 1 file changed, 1 insertion(+) diff --git a/dist/systemd/arsse.service b/dist/systemd/arsse.service index 42e869fb..99da8585 100644 --- a/dist/systemd/arsse.service +++ b/dist/systemd/arsse.service @@ -11,3 +11,4 @@ WantedBy=multi-user.target [Service] Type=oneshot RemainAfterExit=true +ExecStart=/usr/bin/true From 86d82a2586c9b9ab36f217b5a33f1bfc8d39af59 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 24 May 2021 14:50:21 -0400 Subject: [PATCH 263/366] Use global flag when replacing with sed --- dist/arch/PKGBUILD | 8 ++++---- dist/arch/PKGBUILD-git | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index e3d1a4c7..dd35407f 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -49,8 +49,8 @@ package() { install -Dm755 dist/arsse "$pkgdir/usr/bin/arsse" install -Dm640 dist/arch/config.php "$pkgdir/etc/webapps/arsse" # patch generic configuration files to use Arch-specific paths and identifiers - sed -i -se 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"* - sed -i -se 's/\/var\/run\/php\//\/run\/php-fpm\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf" - sed -i -se 's/www-data/http/' "$pkgdir/etc/php/php-fpm.d/arsse.conf" - sed -i -e 's/^WorkingDirectory=.*$/WorkingDirectory=\/usr\/share\/webapps\/arsse/' -e 's/^ConfigurationDirectory=.*$/ConfigurationDirectory=webapps\/arsse/' "$pkgdir/usr/lib/systemd/system/arsse-fetch.service" + sed -i -se 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//g' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"* + sed -i -se 's/\/var\/run\/php\//\/run\/php-fpm\//g' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf" + sed -i -se 's/www-data/http/g' "$pkgdir/etc/php/php-fpm.d/arsse.conf" + sed -i -e 's/^WorkingDirectory=.*$/WorkingDirectory=\/usr\/share\/webapps\/arsse/g' -e 's/^ConfigurationDirectory=.*$/ConfigurationDirectory=webapps\/arsse/g' "$pkgdir/usr/lib/systemd/system/arsse-fetch.service" } diff --git a/dist/arch/PKGBUILD-git b/dist/arch/PKGBUILD-git index dcfdbd89..bb43eb49 100644 --- a/dist/arch/PKGBUILD-git +++ b/dist/arch/PKGBUILD-git @@ -65,8 +65,8 @@ package() { install -Dm755 dist/arsse "$pkgdir/usr/bin/arsse" install -Dm640 dist/arch/config.php "$pkgdir/etc/webapps/arsse" # patch generic configuration files to use Arch-specific paths and identifiers - sed -i -se 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"* - sed -i -se 's/\/var\/run\/php\//\/run\/php-fpm\//' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf" - sed -i -se 's/www-data/http/' "$pkgdir/etc/php/php-fpm.d/arsse.conf" - sed -i -e 's/^WorkingDirectory=.*$/WorkingDirectory=\/usr\/share\/webapps\/arsse/' -e 's/^ConfigurationDirectory=.*$/ConfigurationDirectory=webapps\/arsse/' "$pkgdir/usr/lib/systemd/system/arsse-fetch.service" + sed -i -se 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//g' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"* + sed -i -se 's/\/var\/run\/php\//\/run\/php-fpm\//g' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf" + sed -i -se 's/www-data/http/g' "$pkgdir/etc/php/php-fpm.d/arsse.conf" + sed -i -e 's/^WorkingDirectory=.*$/WorkingDirectory=\/usr\/share\/webapps\/arsse/g' -e 's/^ConfigurationDirectory=.*$/ConfigurationDirectory=webapps\/arsse/g' "$pkgdir/usr/lib/systemd/system/arsse-fetch.service" } From a81bd0e45c52a963391f6bb79f3032c6ee7ce5c9 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 24 May 2021 14:51:21 -0400 Subject: [PATCH 264/366] Add whitespace --- dist/tmpfiles.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/tmpfiles.conf b/dist/tmpfiles.conf index bb629eb0..416c95d6 100644 --- a/dist/tmpfiles.conf +++ b/dist/tmpfiles.conf @@ -1,3 +1,3 @@ z /usr/bin/arsse 0755 root arsse - - z /etc/arsse/config.php 0640 root arsse - - -L /usr/share/arsse/config.php - root arsse - /etc/arsse/config.php \ No newline at end of file +L /usr/share/arsse/config.php - root arsse - /etc/arsse/config.php From 32ca0c3fe495c37bbaabed19253414df3d1a1dbe Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 24 May 2021 19:22:37 -0400 Subject: [PATCH 265/366] Appease GitHub once and for all --- .gitignore | 14 +- package.json | 26 +- yarn.lock | 1170 -------------------------------------------------- 3 files changed, 22 insertions(+), 1188 deletions(-) delete mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore index b204061c..bea34346 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,5 @@ -# Temporary files and dependencies +# Temporary files -/vendor/ -/vendor-bin/*/vendor -/node_modules /documentation/ /manual/ /tests/coverage/ @@ -12,9 +9,16 @@ /arsse.db* /config.php /.php_cs.cache -/yarn-error.log /tests/.phpunit.result.cache +# Dependencies + +/vendor/ +/vendor-bin/*/vendor +/node_modules +/yarn.lock +/yarn-error.log + # Windows files diff --git a/package.json b/package.json index e7a3b351..96eab27f 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,17 @@ { "devDependencies": { - "autoprefixer": "^9.6.1", - "postcss": "^7.0.0", - "postcss-cli": "^7.1.1", - "postcss-color-function": "^4.1.0", - "postcss-csso": "^4.0.0", - "postcss-custom-media": "^7.0.8", - "postcss-custom-properties": "^9.0.2", - "postcss-discard-comments": "^4.0.2", - "postcss-import": "^12.0.1", - "postcss-media-minmax": "^4.0.0", - "postcss-nested": "^4.1.2", - "postcss-sassy-mixins": "^2.1.0", - "postcss-scss": "^2.0.0" + "autoprefixer": "*", + "postcss": "*", + "postcss-cli": "*", + "postcss-color-function": "*", + "postcss-csso": "*", + "postcss-custom-media": "*", + "postcss-custom-properties": "*", + "postcss-discard-comments": "*", + "postcss-import": "*", + "postcss-media-minmax": "*", + "postcss-nested": "*", + "postcss-sassy-mixins": "*", + "postcss-scss": "*" } } diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 24fe8bd7..00000000 --- a/yarn.lock +++ /dev/null @@ -1,1170 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@nodelib/fs.scandir@2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" - integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== - dependencies: - "@nodelib/fs.stat" "2.0.4" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" - integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" - integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== - dependencies: - "@nodelib/fs.scandir" "2.1.4" - fastq "^1.6.0" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -anymatch@~3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -autoprefixer@^9.6.1: - version "9.8.6" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" - integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== - dependencies: - browserslist "^4.12.0" - caniuse-lite "^1.0.30001109" - colorette "^1.2.1" - normalize-range "^0.1.2" - num2fraction "^1.2.2" - postcss "^7.0.32" - postcss-value-parser "^4.1.0" - -balanced-match@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" - integrity sha1-tQS9BYabOSWd0MXvw12EMXbczEo= - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^3.0.1, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -browserslist@^4.12.0: - version "4.16.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" - integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== - dependencies: - caniuse-lite "^1.0.30001181" - colorette "^1.2.1" - electron-to-chromium "^1.3.649" - escalade "^3.1.1" - node-releases "^1.1.70" - -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001181: - version "1.0.30001208" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz#a999014a35cebd4f98c405930a057a0d75352eb9" - integrity sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA== - -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.1, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chokidar@^3.3.0: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.3.1" - -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= - -color-convert@^1.3.0, color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" - integrity sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE= - dependencies: - color-name "^1.0.0" - -color@^0.11.0: - version "0.11.4" - resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" - integrity sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q= - dependencies: - clone "^1.0.2" - color-convert "^1.3.0" - color-string "^0.3.0" - -colorette@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -cosmiconfig@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - -css-color-function@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.3.tgz#8ed24c2c0205073339fafa004bc8c141fccb282e" - integrity sha1-jtJMLAIFBzM5+voAS8jBQfzLKC4= - dependencies: - balanced-match "0.1.0" - color "^0.11.0" - debug "^3.1.0" - rgb "~0.1.0" - -css-tree@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -csso@^4.0.2: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== - dependencies: - css-tree "^1.1.2" - -debug@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -dependency-graph@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.9.0.tgz#11aed7e203bc8b00f48356d92db27b265c445318" - integrity sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -electron-to-chromium@^1.3.649: - version "1.3.710" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.710.tgz#b33d316e5d6de92b916e766d8a478d19796ffe11" - integrity sha512-b3r0E2o4yc7mNmBeJviejF1rEx49PUBi+2NPa7jHEX3arkAXnVgLhR0YmV8oi6/Qf3HH2a8xzQmCjHNH0IpXWQ== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -fast-glob@^3.1.1: - version "3.2.5" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" - integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" - merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" - -fastq@^1.6.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" - integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== - dependencies: - reusify "^1.0.4" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -fs-extra@^9.0.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fsevents@~2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-stdin@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" - integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== - -glob-parent@^5.1.0, glob-parent@~5.1.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" - integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globby@^11.0.0: - version "11.0.3" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" - integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - -graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -ignore@^5.1.4: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== - -import-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" - integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= - dependencies: - import-from "^2.1.0" - -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" - -import-from@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" - integrity sha1-M1238qev/VOqpHHUuAId7ja387E= - dependencies: - resolve-from "^3.0.0" - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ip-regex@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" - integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-core-module@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" - integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== - dependencies: - has "^1.0.3" - -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-url-superb@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-url-superb/-/is-url-superb-3.0.0.tgz#b9a1da878a1ac73659047d1e6f4ef22c209d3e25" - integrity sha512-3faQP+wHCGDQT1qReM5zCPx2mxoal6DzbzquFlCYJLWyy4WPTved33ea2xFbX37z4NoriEwZGIYhFtx8RUB5wQ== - dependencies: - url-regex "^5.0.0" - -js-base64@^2.1.9: - version "2.6.4" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" - integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== - -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash@^4.17.11: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log-symbols@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" - integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== - dependencies: - chalk "^2.0.1" - -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== - -merge2@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== - dependencies: - braces "^3.0.1" - picomatch "^2.0.5" - -"minimatch@2 || 3": - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -node-releases@^1.1.70: - version "1.1.71" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" - integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= - -num2fraction@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" - integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - -pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -postcss-cli@^7.1.1: - version "7.1.2" - resolved "https://registry.yarnpkg.com/postcss-cli/-/postcss-cli-7.1.2.tgz#ba8d5d918b644bd18e80ad2c698064d4c0da51cd" - integrity sha512-3mlEmN1v2NVuosMWZM2tP8bgZn7rO5PYxRRrXtdSyL5KipcgBDjJ9ct8/LKxImMCJJi3x5nYhCGFJOkGyEqXBQ== - dependencies: - chalk "^4.0.0" - chokidar "^3.3.0" - dependency-graph "^0.9.0" - fs-extra "^9.0.0" - get-stdin "^8.0.0" - globby "^11.0.0" - postcss "^7.0.0" - postcss-load-config "^2.0.0" - postcss-reporter "^6.0.0" - pretty-hrtime "^1.0.3" - read-cache "^1.0.0" - yargs "^15.0.2" - -postcss-color-function@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.1.0.tgz#b6f9355e07b12fcc5c34dab957834769b03d8f57" - integrity sha512-2/fuv6mP5Lt03XbRpVfMdGC8lRP1sykme+H1bR4ARyOmSMB8LPSjcL6EAI1iX6dqUF+jNEvKIVVXhan1w/oFDQ== - dependencies: - css-color-function "~1.3.3" - postcss "^6.0.23" - postcss-message-helpers "^2.0.0" - postcss-value-parser "^3.3.1" - -postcss-csso@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-csso/-/postcss-csso-4.0.0.tgz#30fef9303ecbeb0424dab1228275416fc7186a50" - integrity sha512-Yh9Ug0w3+T/LZIh1vGJQY8+hE13yFRHpINoAmgOhvu9lBmG1jyHkAprGHEHlGjWODJzB4DCNBVBb6Cs0QEoglQ== - dependencies: - csso "^4.0.2" - -postcss-custom-media@^7.0.8: - version "7.0.8" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz#fffd13ffeffad73621be5f387076a28b00294e0c" - integrity sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg== - dependencies: - postcss "^7.0.14" - -postcss-custom-properties@^9.0.2: - version "9.2.0" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-9.2.0.tgz#80bae0d6e0c510245ace7ede95ac527712ea24e7" - integrity sha512-IFRV7LwapFkNa3MtvFpw+MEhgyUpaVZ62VlR5EM0AbmnGbNhU9qIE8u02vgUbl1gLkHK6sterEavamVPOwdE8g== - dependencies: - postcss "^7.0.17" - postcss-values-parser "^3.0.5" - -postcss-discard-comments@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" - integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== - dependencies: - postcss "^7.0.0" - -postcss-import@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153" - integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw== - dependencies: - postcss "^7.0.1" - postcss-value-parser "^3.2.3" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-load-config@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a" - integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw== - dependencies: - cosmiconfig "^5.0.0" - import-cwd "^2.0.0" - -postcss-media-minmax@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" - integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== - dependencies: - postcss "^7.0.2" - -postcss-message-helpers@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" - integrity sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4= - -postcss-nested@^4.1.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-4.2.3.tgz#c6f255b0a720549776d220d00c4b70cd244136f6" - integrity sha512-rOv0W1HquRCamWy2kFl3QazJMMe1ku6rCFoAAH+9AcxdbpDeBr6k968MLWuLjvjMcGEip01ak09hKOEgpK9hvw== - dependencies: - postcss "^7.0.32" - postcss-selector-parser "^6.0.2" - -postcss-reporter@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-6.0.1.tgz#7c055120060a97c8837b4e48215661aafb74245f" - integrity sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw== - dependencies: - chalk "^2.4.1" - lodash "^4.17.11" - log-symbols "^2.2.0" - postcss "^7.0.7" - -postcss-sassy-mixins@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-sassy-mixins/-/postcss-sassy-mixins-2.1.0.tgz#368f200946bfdef6a8b12d68c0f6379b9a222f26" - integrity sha1-No8gCUa/3vaosS1owPY3m5oiLyY= - dependencies: - glob "^6.0.4" - postcss "^5.0.14" - postcss-simple-vars "^1.2.0" - -postcss-scss@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.1.1.tgz#ec3a75fa29a55e016b90bf3269026c53c1d2b383" - integrity sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA== - dependencies: - postcss "^7.0.6" - -postcss-selector-parser@^6.0.2: - version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" - integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== - dependencies: - cssesc "^3.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - util-deprecate "^1.0.2" - -postcss-simple-vars@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-1.2.0.tgz#2e6689921144b74114e765353275a3c32143f150" - integrity sha1-LmaJkhFEt0EU52U1MnWjwyFD8VA= - dependencies: - postcss "^5.0.13" - -postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" - integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== - -postcss-value-parser@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== - -postcss-values-parser@^3.0.5: - version "3.2.1" - resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-3.2.1.tgz#55114607de6631338ba8728d3e9c15785adcc027" - integrity sha512-SQ7/88VE9LhJh9gc27/hqnSU/aZaREVJcRVccXBmajgP2RkjdJzNyH/a9GCVMI5nsRhT0jC5HpUMwfkz81DVVg== - dependencies: - color-name "^1.1.4" - is-url-superb "^3.0.0" - postcss "^7.0.5" - url-regex "^5.0.0" - -postcss@^5.0.13, postcss@^5.0.14: - version "5.2.18" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" - integrity sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg== - dependencies: - chalk "^1.1.3" - js-base64 "^2.1.9" - source-map "^0.5.6" - supports-color "^3.2.3" - -postcss@^6.0.23: - version "6.0.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" - integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== - dependencies: - chalk "^2.4.1" - source-map "^0.6.1" - supports-color "^5.4.0" - -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6, postcss@^7.0.7: - version "7.0.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -pretty-hrtime@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" - integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -read-cache@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" - integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q= - dependencies: - pify "^2.3.0" - -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== - dependencies: - picomatch "^2.2.1" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -resolve@^1.1.7: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== - dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rgb@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" - integrity sha1-vieykej+/+rBvZlylyG/pA/AN7U= - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" - integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= - dependencies: - has-flag "^1.0.0" - -supports-color@^5.3.0, supports-color@^5.4.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -tlds@^1.203.0: - version "1.219.0" - resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.219.0.tgz#7e636062386a1f3c9184356de93d40842ffe91d9" - integrity sha512-o4g9c8kXCmTDwUnK/9HpTT9o/GNH85KCvs+S5SgUw5yILdECvMmTGzK7ngoWMp97P5tfYr8fZeF16YhgV/l90A== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - -url-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/url-regex/-/url-regex-5.0.0.tgz#8f5456ab83d898d18b2f91753a702649b873273a" - integrity sha512-O08GjTiAFNsSlrUWfqF1jH0H1W3m35ZyadHrGv5krdnmPPoxP27oDTqux/579PtaroiSGm5yma6KT1mHFH6Y/g== - dependencies: - ip-regex "^4.1.0" - tlds "^1.203.0" - -util-deprecate@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -y18n@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== - -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@^15.0.2: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" From f9cbac2c3187c9a3b328a154201c9079c6b23b8a Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 25 May 2021 15:13:18 -0400 Subject: [PATCH 266/366] Hopefully fix Apache configuration --- dist/apache/arsse.conf | 14 +++++++------- .../020_On_Debian_and_Ubuntu.md | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dist/apache/arsse.conf b/dist/apache/arsse.conf index 469dc8ec..f4d759ee 100644 --- a/dist/apache/arsse.conf +++ b/dist/apache/arsse.conf @@ -1,10 +1,10 @@ -Define ARSSE_CONF "/etc/arsse/apache/" -Define ARSSE_DATA "/usr/share/arsse/" -Define ARSSE_PROXY "unix:/var/run/php/arsse.sock|fcgi://localhost${ARSSE_DATA}" - -DocumentRoot "${ARSSE_DATA}www" +DocumentRoot "/usr/share/arsse/www" + + Require all granted + +Define ARSSE_PROXY "unix:/var/run/php/arsse.sock|fcgi://localhost/usr/share/arsse/" ProxyPreserveHost On -ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "${ARSSE_DATA}arsse.php" +ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/usr/share/arsse/arsse.php" -Include "${ARSSE_CONF}arsse-loc.conf" +Include "/etc/arsse/apache/arsse-loc.conf" diff --git a/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md b/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md index 9b9e2111..6dc0fba7 100644 --- a/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md +++ b/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md @@ -43,7 +43,7 @@ Finally, services must be restarted to apply the new configurations, and The Ars ```sh sudo systemctl restart systemd-sysusers -sudo systemctl restart systemd-tmpfiles +sudo systemd-tmpfiles --create sudo systemctl restart php$php_ver-fpm sudo systemctl reenable arsse sudo systemctl restart arsse From 6c84b2199e7eb24633d8b3d0cfbbe1e2ab02c9c0 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 25 May 2021 17:03:08 -0400 Subject: [PATCH 267/366] More Apache fixes --- dist/apache/arsse-loc.conf | 7 ------- dist/apache/arsse.conf | 3 ++- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/dist/apache/arsse-loc.conf b/dist/apache/arsse-loc.conf index 5b7a92ec..611c1fc6 100644 --- a/dist/apache/arsse-loc.conf +++ b/dist/apache/arsse-loc.conf @@ -1,8 +1,3 @@ -# Nextcloud News version list - - ProxyPass ${ARSSE_PROXY} - - # Nextcloud News protocol ProxyPass ${ARSSE_PROXY} @@ -18,8 +13,6 @@ ProxyPass ${ARSSE_PROXY} -# NOTE: The DocumentRoot directive will dictate whether TT-RSS static images are served correctly - # Fever protocol ProxyPass ${ARSSE_PROXY} diff --git a/dist/apache/arsse.conf b/dist/apache/arsse.conf index f4d759ee..b16c80e8 100644 --- a/dist/apache/arsse.conf +++ b/dist/apache/arsse.conf @@ -5,6 +5,7 @@ DocumentRoot "/usr/share/arsse/www" Define ARSSE_PROXY "unix:/var/run/php/arsse.sock|fcgi://localhost/usr/share/arsse/" ProxyPreserveHost On -ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/usr/share/arsse/arsse.php" +ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/usr/share/arsse/arsse.php" +ProxyFCGISetEnvIf "-n req('Authorization')" HTTP_AUTHORIZATION "%{req:Authorization}" Include "/etc/arsse/apache/arsse-loc.conf" From 3be6c9984db9f9918cd6a9fcdc0ec62ccca5dd82 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 25 May 2021 17:16:40 -0400 Subject: [PATCH 268/366] Update Apache documentation in manual --- .../010_On_Arch_Linux.md | 11 ++++++++++- .../020_On_Debian_and_Ubuntu.md | 4 ++-- docs/en/020_Getting_Started/index.md | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md b/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md index 4044daeb..cd7a58ac 100644 --- a/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md +++ b/docs/en/020_Getting_Started/020_Download_and_Installation/010_On_Arch_Linux.md @@ -26,7 +26,16 @@ The PHP extensions listed in [the requirements](/en/Getting_Started/index) not m # Web server configuration -Sample configuration for both Nginx and Apache HTTPd can be found in `/etc/webapps/arsse/nginx/` and `/etc/webapps/arsse/apache/`, respectively. The `example.conf` files are basic virtual host examples; the other files they include should normally be usable without modification, but may be modified if desired. +Sample configuration for both Nginx and Apache HTTP Server can be found in `/etc/webapps/arsse/nginx/` and `/etc/webapps/arsse/apache/`, respectively. The `example.conf` files are basic virtual host examples; the other files they include should normally be usable without modification, but may be modified if desired. + +If using Apache HTTP Server the `mod_proxy` and `mod_proxy_fcgi` modules must be enabled. This can be achieved by adding the following lines to your virtual host or global configuration: + +```apache +LoadModule proxy_module modules/mod_proxy.so +LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so +``` + +No additional set-up is required for Nginx. # Next steps diff --git a/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md b/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md index 6dc0fba7..ea8c0d92 100644 --- a/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md +++ b/docs/en/020_Getting_Started/020_Download_and_Installation/020_On_Debian_and_Ubuntu.md @@ -51,9 +51,9 @@ sudo systemctl restart arsse # Web server configuration -Sample configuration for both Nginx and Apache HTTPd can be found in `/etc/arsse/nginx/` and `/etc/arsse/apache/`, respectively. The `example.conf` files are basic virtual host examples; the other files they include should normally be usable without modification, but may be modified if desired. +Sample configuration for both Nginx and Apache HTTP Server can be found in `/etc/arsse/nginx/` and `/etc/arsse/apache/`, respectively. The `example.conf` files are basic virtual host examples; the other files they include should normally be usable without modification, but may be modified if desired. -In order to use Apache HTTPd the FastCGI proxy module must be enabled and the server restarted: +In order to use Apache HTTP Server the FastCGI proxy module must be enabled and the server restarted: ```sh sudo a2enmod proxy proxy_fcgi diff --git a/docs/en/020_Getting_Started/index.md b/docs/en/020_Getting_Started/index.md index 5aec617e..35b4a570 100644 --- a/docs/en/020_Getting_Started/index.md +++ b/docs/en/020_Getting_Started/index.md @@ -1,4 +1,4 @@ -Presently installing and setting up The Arsse involves some manual labour. We have packages for Arch Linux and hope to have installation packages available for other operating systems eventually, but for now the pages in this section should help get you up and running on Arch Linux or Debian-based systems, with Nginx or Apache HTTPd. +Presently installing and setting up The Arsse involves some manual labour. We have packages for Arch Linux and hope to have installation packages available for other operating systems eventually, but for now the pages in this section should help get you up and running on Arch Linux or Debian-based systems, with Nginx or Apache HTTP Server. It is also be possible to run The Arsse on other operating systems (including Windows) and with other Web servers, but the configuration required to do so is not documented in this manual. From b5bbdc2bc6a1b5c4471013115885038c04f04610 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 25 May 2021 17:22:48 -0400 Subject: [PATCH 269/366] Date release --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e31169f5..fbafbe69 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -Version 0.9.2 (2021-??-??) +Version 0.9.2 (2021-05-25) ========================== Bug fixes: From 18846c19cb6b6bc01ab00afa93b5a7c8d54aa739 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 27 May 2021 19:00:29 -0400 Subject: [PATCH 270/366] Add install list for Debian package --- dist/debian/arsse.install | 16 ++++++++++++++++ dist/debian/rules | 3 +-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 dist/debian/arsse.install diff --git a/dist/debian/arsse.install b/dist/debian/arsse.install new file mode 100644 index 00000000..346d16a4 --- /dev/null +++ b/dist/debian/arsse.install @@ -0,0 +1,16 @@ +lib usr/share/arsse/ +locale usr/share/arsse/ +sql usr/share/arsse/ +vendor usr/share/arsse/ +www usr/share/arsse/ +CHANGELOG usr/share/arsse/ +UPGRADING usr/share/arsse/ +README.md usr/share/arsse/ +arsse.php usr/share/arsse/ + +dist/arsse usr/bin/ +manual usr/share/doc/arsse/ +dist/nginx etc/arsse/ +dist/apache etc/arsse/ +dist/config.php etc/arsse +config.defaults.php etc/arsse/ diff --git a/dist/debian/rules b/dist/debian/rules index 8ff98a72..66a0a490 100755 --- a/dist/debian/rules +++ b/dist/debian/rules @@ -3,5 +3,4 @@ DH_VERBOSE = 1 %: - dh $@ - + dh $@ --with systemd From 758a02d667b952fec55a679a35fc9545eb70b15d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 27 May 2021 19:39:53 -0400 Subject: [PATCH 271/366] Move generic configuration file --- dist/arch/PKGBUILD | 2 +- dist/arch/PKGBUILD-git | 2 +- dist/{arch => }/config.php | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename dist/{arch => }/config.php (100%) diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index dd35407f..48a6dc33 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -47,7 +47,7 @@ package() { # copy files requiring special permissions cd "$srcdir/arsse" install -Dm755 dist/arsse "$pkgdir/usr/bin/arsse" - install -Dm640 dist/arch/config.php "$pkgdir/etc/webapps/arsse" + install -Dm640 dist/config.php "$pkgdir/etc/webapps/arsse" # patch generic configuration files to use Arch-specific paths and identifiers sed -i -se 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//g' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"* sed -i -se 's/\/var\/run\/php\//\/run\/php-fpm\//g' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf" diff --git a/dist/arch/PKGBUILD-git b/dist/arch/PKGBUILD-git index bb43eb49..4249f0a1 100644 --- a/dist/arch/PKGBUILD-git +++ b/dist/arch/PKGBUILD-git @@ -63,7 +63,7 @@ package() { # copy files requiring special permissions cd "$srcdir/arsse" install -Dm755 dist/arsse "$pkgdir/usr/bin/arsse" - install -Dm640 dist/arch/config.php "$pkgdir/etc/webapps/arsse" + install -Dm640 dist/config.php "$pkgdir/etc/webapps/arsse" # patch generic configuration files to use Arch-specific paths and identifiers sed -i -se 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//g' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"* sed -i -se 's/\/var\/run\/php\//\/run\/php-fpm\//g' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf" diff --git a/dist/arch/config.php b/dist/config.php similarity index 100% rename from dist/arch/config.php rename to dist/config.php From 281760be71a0764bcfd1d7607bbcfa197aaa5070 Mon Sep 17 00:00:00 2001 From: "J. KIng" Date: Fri, 28 May 2021 08:29:49 -0400 Subject: [PATCH 272/366] Address some lintian complaints --- dist/debian/control | 8 ++++---- dist/debian/lintian-overrides | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 dist/debian/lintian-overrides diff --git a/dist/debian/control b/dist/debian/control index e44560fc..09cd9eff 100644 --- a/dist/debian/control +++ b/dist/debian/control @@ -6,6 +6,7 @@ Standards-Version: 4.5.1 Homepage: https://thearsse.com/ Vcs-Browser: https://code.mensbeam.com/MensBeam/arsse/ Vcs-Git: https://code.mensbeam.com/MensBeam/arsse/ +Build-Depends: debhelper, dh-systemd Package: arsse Architecture: all @@ -17,16 +18,15 @@ Description: Multi-protocol RSS/Atom newsfeed synchronization server client protocols such as Tiny Tiny RSS, Nextcloud News and Miniflux, allowing you to use compatible clients for many protocols with a single server. -Build-Depends: debhelper Depends: ${misc:Depends}, - dbconfig-mysql | dbconfig-pgsql | dbconfig-sqlite3 | dbconfig-no-thanks, + dbconfig-sqlite3 | dbconfig-pgsql | dbconfig-no-thanks, php (>= 7.1.0), php-cli, php-intl, php-json, php-xml, - php-sqlite3 | php-mysql | php-pgsql -Recommends: apache2 | nginx, + php-sqlite3 | php-pgsql +Recommends: nginx | apache2, php-fpm, php-curl, ca-certificates diff --git a/dist/debian/lintian-overrides b/dist/debian/lintian-overrides new file mode 100644 index 00000000..94cdc640 --- /dev/null +++ b/dist/debian/lintian-overrides @@ -0,0 +1,6 @@ +# We make reference to "Tiny Tiny RSS" +spelling-error-in-description Tiny Tiny (duplicate word) Tiny +# The manual for DrUUID (a dependency includes a harmless "up" link +privacy-breach-generic usr/share/arsse/vendor/jkingweb/druuid/documentation/manual.html [] (http://jkingweb.ca/code/) +# Development environment is slightly out of date +source: newer-standards-version 4.5.1 (current is 4.5.0) From 14d3cdfe58725fcfc42606e044c0558d0a0bc8b2 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 28 May 2021 12:33:52 -0400 Subject: [PATCH 273/366] Hopefully fix some Debian problems --- .gitignore | 7 ++++--- RoboFile.php | 4 ++-- dist/arsse | 2 +- dist/debian/rules | 7 +++++++ 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index bea34346..b4883827 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,12 @@ # Temporary files +/temp/ /documentation/ /manual/ /tests/coverage/ -/dist/arch/arsse -/dist/arch/src -/dist/arch/pkg +/dist/arch/arsse/ +/dist/arch/src/ +/dist/arch/pkg/ /arsse.db* /config.php /.php_cs.cache diff --git a/RoboFile.php b/RoboFile.php index 00883f76..fefd24dc 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -254,7 +254,7 @@ class RoboFile extends \Robo\Tasks { } // start a task collection and create a temporary directory $t = $this->collectionBuilder(); - $dir = $t->tmpDir().\DIRECTORY_SEPARATOR; + $dir = $t->workDir(BASE."temp").\DIRECTORY_SEPARATOR; $base = $dir."arsse-$version".\DIRECTORY_SEPARATOR; // start by extracting the tarball $t->addCode(function() use ($tarball, $dir, $base) { @@ -266,7 +266,7 @@ class RoboFile extends \Robo\Tasks { $t->addTask($this->taskPack($dir."arsse_$version.orig.tar.gz")->addDir("arsse-$version", $base)); // copy Debian files to lower down in the tree $t->addTask($this->taskFilesystemStack()->mirror($base."dist/debian", $base."debian")); - $t->addTask($this->taskExec("deber")->dir($dir)); + $t->addTask($this->taskExec("deber")->dir($base)); return $t->run(); } diff --git a/dist/arsse b/dist/arsse index b4c56e4a..987c1596 100644 --- a/dist/arsse +++ b/dist/arsse @@ -1,4 +1,4 @@ -#! /usr/bin/php +#! /usr/bin/env php Date: Fri, 28 May 2021 16:23:42 -0400 Subject: [PATCH 274/366] Fix more lintian complaints --- RoboFile.php | 12 +++++++++++- dist/debian/control | 2 +- dist/debian/lintian-overrides | 4 +--- dist/debian/rules | 9 +-------- dist/debian/source/lintian-overrides | 2 ++ 5 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 dist/debian/source/lintian-overrides diff --git a/RoboFile.php b/RoboFile.php index fefd24dc..8c107fe3 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -211,6 +211,16 @@ class RoboFile extends \Robo\Tasks { $dir."yarn.lock", $dir."postcss.config.js", ])); + $t->addCode(function() use ($dir) { + // Remove files which lintian complains about; they're otherwise harmless + $files = []; + foreach (new \CallbackFilterIterator(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir."vendor", \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS)), function($v, $k, $i) { + return preg_match('/\/\.git(?:ignore|attributes|modules)$/', $v); + }) as $f) { + $files[] = $f; + } + return $this->taskFilesystemStack()->remove($files)->run(); + }); // generate a sample configuration file $t->addTask($this->taskExec(escapeshellarg(\PHP_BINARY)." arsse.php conf save-defaults config.defaults.php")->dir($dir)); // remove any existing archive @@ -266,7 +276,7 @@ class RoboFile extends \Robo\Tasks { $t->addTask($this->taskPack($dir."arsse_$version.orig.tar.gz")->addDir("arsse-$version", $base)); // copy Debian files to lower down in the tree $t->addTask($this->taskFilesystemStack()->mirror($base."dist/debian", $base."debian")); - $t->addTask($this->taskExec("deber")->dir($base)); + //$t->addTask($this->taskExec("deber")->dir($base)); return $t->run(); } diff --git a/dist/debian/control b/dist/debian/control index 09cd9eff..da006c2f 100644 --- a/dist/debian/control +++ b/dist/debian/control @@ -6,7 +6,7 @@ Standards-Version: 4.5.1 Homepage: https://thearsse.com/ Vcs-Browser: https://code.mensbeam.com/MensBeam/arsse/ Vcs-Git: https://code.mensbeam.com/MensBeam/arsse/ -Build-Depends: debhelper, dh-systemd +Build-Depends: debhelper Package: arsse Architecture: all diff --git a/dist/debian/lintian-overrides b/dist/debian/lintian-overrides index 94cdc640..3e67ae32 100644 --- a/dist/debian/lintian-overrides +++ b/dist/debian/lintian-overrides @@ -1,6 +1,4 @@ # We make reference to "Tiny Tiny RSS" spelling-error-in-description Tiny Tiny (duplicate word) Tiny -# The manual for DrUUID (a dependency includes a harmless "up" link +# The manual for DrUUID (a dependency) includes a harmless "up" link privacy-breach-generic usr/share/arsse/vendor/jkingweb/druuid/documentation/manual.html [] (http://jkingweb.ca/code/) -# Development environment is slightly out of date -source: newer-standards-version 4.5.1 (current is 4.5.0) diff --git a/dist/debian/rules b/dist/debian/rules index a011c6b8..26f2a18c 100755 --- a/dist/debian/rules +++ b/dist/debian/rules @@ -3,11 +3,4 @@ DH_VERBOSE = 1 %: - dh $@ --with systemd - -override_dh_install: - # Run the normal dh_install - dh_install - # Satisfy lintian's complaints about VCS control files - rm -f debian/arsse/vendor/**/.gitignore - rm -f debian/arsse/vendor/**/.gitattributes + dh $@ diff --git a/dist/debian/source/lintian-overrides b/dist/debian/source/lintian-overrides new file mode 100644 index 00000000..ab33538b --- /dev/null +++ b/dist/debian/source/lintian-overrides @@ -0,0 +1,2 @@ +# Development environment is slightly out of date +newer-standards-version From d4569c77a9aa565c7ad5689506573a16e54499d6 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 29 May 2021 11:09:11 -0400 Subject: [PATCH 275/366] Add database location to tmpfiles --- dist/tmpfiles.conf | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dist/tmpfiles.conf b/dist/tmpfiles.conf index 416c95d6..f5e6ed18 100644 --- a/dist/tmpfiles.conf +++ b/dist/tmpfiles.conf @@ -1,3 +1,4 @@ -z /usr/bin/arsse 0755 root arsse - - -z /etc/arsse/config.php 0640 root arsse - - -L /usr/share/arsse/config.php - root arsse - /etc/arsse/config.php +z /usr/bin/arsse 0755 root arsse - - +z /etc/arsse/config.php 0640 root arsse - - +L /usr/share/arsse/config.php - root arsse - /etc/arsse/config.php +d /var/lib/arsse 0750 arsse arsse - - From 6cc9f967288e3e37d5c4cefa9016276ebe055c83 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 29 May 2021 11:13:19 -0400 Subject: [PATCH 276/366] Prototype manual page --- README.md | 5 ++- RoboFile.php | 13 +++++++ dist/arch/PKGBUILD | 3 +- dist/arch/PKGBUILD-git | 4 +- .../999_The_Command-Line_Manual_Page.md | 37 +++++++++++++++++++ 5 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 docs/en/025_Using_The_Arsse/999_The_Command-Line_Manual_Page.md diff --git a/README.md b/README.md index 74831aa4..ad8cdc25 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,11 @@ The manual employs a custom theme derived from the standard Daux theme. If the s Producing a release package is done by running `./robo package`. This performs the following operations: - Duplicates a working tree with the commit (usually a release tag) to package -- Generates the manual +- Generates UNIX manual pages with [Pandoc](https://pandoc.org/) +- Generates the HTML manual - Installs runtime Composer dependencies with an optimized autoloader - Deletes numerous unneeded files - Exports the default configuration of The Arsse to a file - Compresses the remaining files into a tarball -Due to the first step, [Git](https://git-scm.com/) is required to package a release. +Due to the first two steps, [Git](https://git-scm.com/) and [Pandoc](https://pandoc.org/) are required in PATH to package a release. diff --git a/RoboFile.php b/RoboFile.php index 8c107fe3..cd2c7a90 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -171,6 +171,8 @@ class RoboFile extends \Robo\Tasks { // get useable version strings from Git $version = trim(`git -C "$dir" describe --tags`); $archVersion = preg_replace('/^([^-]+)-(\d+)-(\w+)$/', "$1.r$2.$3", $version); + // generate manpages + $t->addTask($this->taskExec("./robo manpage")->dir($dir)); // name the generic release tarball $tarball = "arsse-$version.tar.gz"; // generate the Debian changelog; this also validates our original changelog @@ -324,6 +326,17 @@ class RoboFile extends \Robo\Tasks { return $t->run(); } + /** Generates the "arsse" command's manual page (UNIX man page) + * + * This requires that the Pandoc document converter be installed and + * available in $PATH. + */ + public function manpage(): Result { + $src = BASE."docs/en/025_Using_The_Arsse/999_The_Command-Line_Manual_Page.md"; + $out = BASE."dist/manpage"; + return $this->taskExec("pandoc -s -f markdown -t man -o ".escapeshellarg($out)." ".escapeshellarg($src))->run(); + } + protected function changelogParse(string $text, string $targetVersion): array { $lines = preg_split('/\r?\n/', $text); $version = ""; diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index 48a6dc33..a264a3eb 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -32,7 +32,7 @@ package() { depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1" "php-fpm>=7.1") # create most directories necessary for the final package cd "$pkgdir" - mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "etc/php/php-fpm.d/" "etc/webapps/arsse" "etc/webapps/arsse/nginx" + mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "usr/share/man/man1", "etc/php/php-fpm.d" "etc/webapps/arsse" #copy requisite files cd "$srcdir/arsse" cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" @@ -42,6 +42,7 @@ package() { cp dist/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" cp dist/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" + cp dist/manpage "$pkgdir/usr/share/man/man1/arsse.1" cp -r dist/nginx dist/apache config.defaults.php "$pkgdir/etc/webapps/arsse" cd "$pkgdir" # copy files requiring special permissions diff --git a/dist/arch/PKGBUILD-git b/dist/arch/PKGBUILD-git index 4249f0a1..fb25a490 100644 --- a/dist/arch/PKGBUILD-git +++ b/dist/arch/PKGBUILD-git @@ -37,6 +37,7 @@ pkgver() { build() { cd "$srcdir/arsse" composer install + ./robo manpage ./robo manual composer install --no-dev -o --no-scripts php arsse.php conf save-defaults config.defaults.php @@ -48,7 +49,7 @@ package() { depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1" "php-fpm>=7.1") # create most directories necessary for the final package cd "$pkgdir" - mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "etc/php/php-fpm.d/" "etc/webapps/arsse" "etc/webapps/arsse/nginx" + mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "usr/share/man/man1", "etc/php/php-fpm.d" "etc/webapps/arsse" #copy requisite files cd "$srcdir/arsse" cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" @@ -58,6 +59,7 @@ package() { cp dist/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" cp dist/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" + cp dist/manpage "$pkgdir/usr/share/man/man1/arsse.1" cp -r dist/nginx dist/apache config.defaults.php "$pkgdir/etc/webapps/arsse" cd "$pkgdir" # copy files requiring special permissions diff --git a/docs/en/025_Using_The_Arsse/999_The_Command-Line_Manual_Page.md b/docs/en/025_Using_The_Arsse/999_The_Command-Line_Manual_Page.md new file mode 100644 index 00000000..454c28d6 --- /dev/null +++ b/docs/en/025_Using_The_Arsse/999_The_Command-Line_Manual_Page.md @@ -0,0 +1,37 @@ +% ARSSE(1) arsse 0.9.2 +% J. King +% 2021-05-28 + +# NAME + +arsse - manage an instance of The Advanced RSS Environment (The Arsse) + +# SYNOPSIS + +**arsse** <*command*> [<*args*>]\ +**arsse** --version\ +**arsse** -h|--help + +# DESCRIPTION + +**arsse** allows a sufficiently privileged user to perform various administrative operations related to The Arsse, including: + +- Adding and removing users +- Managing passwords and authentication tokens +- Importing and exporting OPML newsfeed-lists + +These are documented in the next section **PRIMARY COMMANDS**. Further, seldom-used commands are documented in the following section **ADDITIONAL COMMANDS**. + +# PRIMARY COMMANDS + +## Managing users + +**arsse user [list]** + +: Displays a simple list of user names with one entry per line + +**arsse user add** <*username*> [<*password*>] [--admin] + +: Adds a new user to the database with the specified username and password. If <*password*> is omitted a random password will be generated and printed. + +: The **--admin** flag may be used to mark the user as an administrator. This has no meaning within the context of The Arsse as a whole, but it is used for access control in the Miniflux and Nextcloud News protocols. \ No newline at end of file From e439dd82778c468491a665a364f4a18714119bef Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 29 May 2021 12:26:51 -0400 Subject: [PATCH 277/366] Fix manpage in Arch PKGBUILD --- RoboFile.php | 6 +++--- dist/arch/PKGBUILD | 2 +- dist/arch/PKGBUILD-git | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/RoboFile.php b/RoboFile.php index cd2c7a90..4e1cdb58 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -171,8 +171,6 @@ class RoboFile extends \Robo\Tasks { // get useable version strings from Git $version = trim(`git -C "$dir" describe --tags`); $archVersion = preg_replace('/^([^-]+)-(\d+)-(\w+)$/', "$1.r$2.$3", $version); - // generate manpages - $t->addTask($this->taskExec("./robo manpage")->dir($dir)); // name the generic release tarball $tarball = "arsse-$version.tar.gz"; // generate the Debian changelog; this also validates our original changelog @@ -187,7 +185,9 @@ class RoboFile extends \Robo\Tasks { $t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^source=\("arsse-[^"]+"\)$/m')->to('source=("'.basename($tarball).'")')); // perform Composer installation in the temp location with dev dependencies $t->addTask($this->taskComposerInstall()->arg("-q")->dir($dir)); - // generate the manual + // generate manpages + $t->addTask($this->taskExec("./robo manpage")->dir($dir)); + // generate the HTML manual $t->addTask($this->taskExec("./robo manual -q")->dir($dir)); // perform Composer installation in the temp location for final output $t->addTask($this->taskComposerInstall()->dir($dir)->noDev()->optimizeAutoloader()->arg("--no-scripts")->arg("-q")); diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index a264a3eb..ad4b6711 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -42,7 +42,7 @@ package() { cp dist/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" cp dist/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" - cp dist/manpage "$pkgdir/usr/share/man/man1/arsse.1" + cp -T dist/manpage "$pkgdir/usr/share/man/man1/arsse.1" cp -r dist/nginx dist/apache config.defaults.php "$pkgdir/etc/webapps/arsse" cd "$pkgdir" # copy files requiring special permissions diff --git a/dist/arch/PKGBUILD-git b/dist/arch/PKGBUILD-git index fb25a490..104a2992 100644 --- a/dist/arch/PKGBUILD-git +++ b/dist/arch/PKGBUILD-git @@ -59,7 +59,7 @@ package() { cp dist/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" cp dist/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" - cp dist/manpage "$pkgdir/usr/share/man/man1/arsse.1" + cp -T dist/manpage "$pkgdir/usr/share/man/man1/arsse.1" cp -r dist/nginx dist/apache config.defaults.php "$pkgdir/etc/webapps/arsse" cd "$pkgdir" # copy files requiring special permissions From 176aac0ad79604cd2fae9fe509e768f48165f0f5 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 29 May 2021 12:37:32 -0400 Subject: [PATCH 278/366] Fix stupid typo properly --- dist/arch/PKGBUILD | 4 ++-- dist/arch/PKGBUILD-git | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index ad4b6711..7d4d7d49 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -32,7 +32,7 @@ package() { depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1" "php-fpm>=7.1") # create most directories necessary for the final package cd "$pkgdir" - mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "usr/share/man/man1", "etc/php/php-fpm.d" "etc/webapps/arsse" + mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "usr/share/man/man1" "etc/php/php-fpm.d" "etc/webapps/arsse" #copy requisite files cd "$srcdir/arsse" cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" @@ -42,7 +42,7 @@ package() { cp dist/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" cp dist/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" - cp -T dist/manpage "$pkgdir/usr/share/man/man1/arsse.1" + cp dist/manpage "$pkgdir/usr/share/man/man1/arsse.1" cp -r dist/nginx dist/apache config.defaults.php "$pkgdir/etc/webapps/arsse" cd "$pkgdir" # copy files requiring special permissions diff --git a/dist/arch/PKGBUILD-git b/dist/arch/PKGBUILD-git index 104a2992..41bae532 100644 --- a/dist/arch/PKGBUILD-git +++ b/dist/arch/PKGBUILD-git @@ -49,7 +49,7 @@ package() { depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1" "php-fpm>=7.1") # create most directories necessary for the final package cd "$pkgdir" - mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "usr/share/man/man1", "etc/php/php-fpm.d" "etc/webapps/arsse" + mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "usr/share/man/man1" "etc/php/php-fpm.d" "etc/webapps/arsse" #copy requisite files cd "$srcdir/arsse" cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" @@ -59,7 +59,7 @@ package() { cp dist/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" cp dist/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" - cp -T dist/manpage "$pkgdir/usr/share/man/man1/arsse.1" + cp dist/manpage "$pkgdir/usr/share/man/man1/arsse.1" cp -r dist/nginx dist/apache config.defaults.php "$pkgdir/etc/webapps/arsse" cd "$pkgdir" # copy files requiring special permissions From d3a983e7f0e9f3b8f7af08fd24393c95c4bba0ad Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 29 May 2021 14:05:30 -0400 Subject: [PATCH 279/366] Move the markdown manpage Daux uses Cmmonmark, which does not support indention, required for proper formatting of manual pages. Consequently, the manul page will instead be standalone. --- RoboFile.php | 2 +- .../999_The_Command-Line_Manual_Page.md => manpage.md} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/{en/025_Using_The_Arsse/999_The_Command-Line_Manual_Page.md => manpage.md} (100%) diff --git a/RoboFile.php b/RoboFile.php index 4e1cdb58..acd93976 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -332,7 +332,7 @@ class RoboFile extends \Robo\Tasks { * available in $PATH. */ public function manpage(): Result { - $src = BASE."docs/en/025_Using_The_Arsse/999_The_Command-Line_Manual_Page.md"; + $src = BASE."docs/manpage.md"; $out = BASE."dist/manpage"; return $this->taskExec("pandoc -s -f markdown -t man -o ".escapeshellarg($out)." ".escapeshellarg($src))->run(); } diff --git a/docs/en/025_Using_The_Arsse/999_The_Command-Line_Manual_Page.md b/docs/manpage.md similarity index 100% rename from docs/en/025_Using_The_Arsse/999_The_Command-Line_Manual_Page.md rename to docs/manpage.md From 2ec7acc50b33d61a0b8a30682099bc30bb8cbb1d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 29 May 2021 14:13:45 -0400 Subject: [PATCH 280/366] Turn off "smart" character substitution in Pandoc --- RoboFile.php | 2 +- docs/manpage.md | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RoboFile.php b/RoboFile.php index acd93976..1049f232 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -334,7 +334,7 @@ class RoboFile extends \Robo\Tasks { public function manpage(): Result { $src = BASE."docs/manpage.md"; $out = BASE."dist/manpage"; - return $this->taskExec("pandoc -s -f markdown -t man -o ".escapeshellarg($out)." ".escapeshellarg($src))->run(); + return $this->taskExec("pandoc -s -f markdown-smart -t man -o ".escapeshellarg($out)." ".escapeshellarg($src))->run(); } protected function changelogParse(string $text, string $targetVersion): array { diff --git a/docs/manpage.md b/docs/manpage.md index 454c28d6..d2e116de 100644 --- a/docs/manpage.md +++ b/docs/manpage.md @@ -34,4 +34,8 @@ These are documented in the next section **PRIMARY COMMANDS**. Further, seldom-u : Adds a new user to the database with the specified username and password. If <*password*> is omitted a random password will be generated and printed. -: The **--admin** flag may be used to mark the user as an administrator. This has no meaning within the context of The Arsse as a whole, but it is used for access control in the Miniflux and Nextcloud News protocols. \ No newline at end of file + The **--admin** flag may be used to mark the user as an administrator. This has no meaning within the context of The Arsse as a whole, but it is used control access to certain features in the Miniflux and Nextcloud News protocols. + +**arsse user remove** <*username*> + +: Immediately removes a user from the database. All associated data (folders, subscriptions, etc.) are also removed. From 3e55ab3849d6a5c4d3067238944763c673d09205 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 29 May 2021 15:44:51 -0400 Subject: [PATCH 281/366] Move man pages to their own directory --- .gitignore | 1 + RoboFile.php | 12 +++++++++--- docs/manpage.md => manpages/en.md | 0 3 files changed, 10 insertions(+), 3 deletions(-) rename docs/manpage.md => manpages/en.md (100%) diff --git a/.gitignore b/.gitignore index b4883827..f81d4dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /dist/arch/arsse/ /dist/arch/src/ /dist/arch/pkg/ +/dist/man/ /arsse.db* /config.php /.php_cs.cache diff --git a/RoboFile.php b/RoboFile.php index 1049f232..964164ad 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -204,6 +204,7 @@ class RoboFile extends \Robo\Tasks { $dir."RoboFile.php", $dir."CONTRIBUTING.md", $dir."docs", + $dir."manpages", $dir."tests", $dir."vendor-bin", $dir."vendor/bin", @@ -332,9 +333,14 @@ class RoboFile extends \Robo\Tasks { * available in $PATH. */ public function manpage(): Result { - $src = BASE."docs/manpage.md"; - $out = BASE."dist/manpage"; - return $this->taskExec("pandoc -s -f markdown-smart -t man -o ".escapeshellarg($out)." ".escapeshellarg($src))->run(); + $p = $this->taskParallelExec(); + $man = [ + 'en' => "man1/arsse.1", + ]; + foreach($man as $src => $out) { + $p->process("pandoc -s -f markdown-smart -t man -o ".escapeshellarg(BASE."dist/$out")." ".escapeshellarg(BASE."manpages/$src.md")); + } + return $p->run(); } protected function changelogParse(string $text, string $targetVersion): array { diff --git a/docs/manpage.md b/manpages/en.md similarity index 100% rename from docs/manpage.md rename to manpages/en.md From 92823d5bc27a681367dcb77545cb33f237770953 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 29 May 2021 16:19:52 -0400 Subject: [PATCH 282/366] Create directories before executing Pandoc --- RoboFile.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/RoboFile.php b/RoboFile.php index 964164ad..51406244 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -333,14 +333,19 @@ class RoboFile extends \Robo\Tasks { * available in $PATH. */ public function manpage(): Result { + $t = $this->collectionBuilder(); $p = $this->taskParallelExec(); $man = [ 'en' => "man1/arsse.1", ]; foreach($man as $src => $out) { - $p->process("pandoc -s -f markdown-smart -t man -o ".escapeshellarg(BASE."dist/$out")." ".escapeshellarg(BASE."manpages/$src.md")); + $src = BASE."manpages/$src.md"; + $out = BASE."dist/man/$out"; + $t->addTask($this->taskFilesystemStack()->mkdir(dirname($out), 0755)); + $p->process("pandoc -s -f markdown-smart -t man -o ".escapeshellarg($out)." ".escapeshellarg($src)); } - return $p->run(); + $t->addTask($p); + return $t->run(); } protected function changelogParse(string $text, string $targetVersion): array { From 46c88f584f4ea1443c68b612a58a197a63a7af6b Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 29 May 2021 16:29:53 -0400 Subject: [PATCH 283/366] Fix copying of man page in PKGBUILDs --- dist/arch/PKGBUILD | 6 +++--- dist/arch/PKGBUILD-git | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dist/arch/PKGBUILD b/dist/arch/PKGBUILD index 7d4d7d49..872f06b7 100644 --- a/dist/arch/PKGBUILD +++ b/dist/arch/PKGBUILD @@ -32,8 +32,8 @@ package() { depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1" "php-fpm>=7.1") # create most directories necessary for the final package cd "$pkgdir" - mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "usr/share/man/man1" "etc/php/php-fpm.d" "etc/webapps/arsse" - #copy requisite files + mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "etc/php/php-fpm.d" "etc/webapps/arsse" + # copy requisite files cd "$srcdir/arsse" cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" cp -r manual/* "$pkgdir/usr/share/doc/arsse" @@ -42,7 +42,7 @@ package() { cp dist/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" cp dist/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" - cp dist/manpage "$pkgdir/usr/share/man/man1/arsse.1" + cp -r dist/man "$pkgdir/usr/share" cp -r dist/nginx dist/apache config.defaults.php "$pkgdir/etc/webapps/arsse" cd "$pkgdir" # copy files requiring special permissions diff --git a/dist/arch/PKGBUILD-git b/dist/arch/PKGBUILD-git index 41bae532..1de2bfd8 100644 --- a/dist/arch/PKGBUILD-git +++ b/dist/arch/PKGBUILD-git @@ -49,8 +49,8 @@ package() { depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1" "php-fpm>=7.1") # create most directories necessary for the final package cd "$pkgdir" - mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "usr/share/man/man1" "etc/php/php-fpm.d" "etc/webapps/arsse" - #copy requisite files + mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "etc/php/php-fpm.d" "etc/webapps/arsse" + # copy requisite files cd "$srcdir/arsse" cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse" cp -r manual/* "$pkgdir/usr/share/doc/arsse" @@ -59,7 +59,7 @@ package() { cp dist/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf" cp dist/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf" - cp dist/manpage "$pkgdir/usr/share/man/man1/arsse.1" + cp -r dist/man "$pkgdir/usr/share" cp -r dist/nginx dist/apache config.defaults.php "$pkgdir/etc/webapps/arsse" cd "$pkgdir" # copy files requiring special permissions From 88487d27a20ab47da3bad697ad7c22dc4ad47e77 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 29 May 2021 17:40:20 -0400 Subject: [PATCH 284/366] Expand manual page --- manpages/en.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/manpages/en.md b/manpages/en.md index d2e116de..15ab3049 100644 --- a/manpages/en.md +++ b/manpages/en.md @@ -16,7 +16,7 @@ arsse - manage an instance of The Advanced RSS Environment (The Arsse) **arsse** allows a sufficiently privileged user to perform various administrative operations related to The Arsse, including: -- Adding and removing users +- Adding and removing users and managing their metadata - Managing passwords and authentication tokens - Importing and exporting OPML newsfeed-lists @@ -24,7 +24,7 @@ These are documented in the next section **PRIMARY COMMANDS**. Further, seldom-u # PRIMARY COMMANDS -## Managing users +## Managing users and metadata **arsse user [list]** @@ -34,8 +34,28 @@ These are documented in the next section **PRIMARY COMMANDS**. Further, seldom-u : Adds a new user to the database with the specified username and password. If <*password*> is omitted a random password will be generated and printed. - The **--admin** flag may be used to mark the user as an administrator. This has no meaning within the context of The Arsse as a whole, but it is used control access to certain features in the Miniflux and Nextcloud News protocols. +: The **--admin** flag may be used to mark the user as an administrator. This has no meaning within the context of The Arsse as a whole, but it is used control access to certain features in the Miniflux and Nextcloud News protocols. **arsse user remove** <*username*> : Immediately removes a user from the database. All associated data (folders, subscriptions, etc.) are also removed. + +**arsse user show** <*username*> + +: Displays a table of metadata properties and their assigned values for <*username*>. These properties are primarily used by the Miniflux protocol. Consult the section **USER METADATA** for details. + +**arsse user set** <*username*> <*property*> <*value*> + +: Sets a metadata property for a user. These properties are primarily used by the Miniflux protocol. Consult the section **USER METADATA** for details. + +**arsse user unset** <*username*> <*property*> + +: Clears a metadata property for a user. The property is thereafter set to its default value, which is protocol-dependent. + +## Managing passwords and authentication tokens + +**arsse user set-pass** <*username*> [<*password*>] [--fever] + +: Changes a user's password to the specified value. If no password is specified, a random password will be generated and printed. +\ +: The **--fever** option sets a user's Fever protocol password instead of their general password. As Fever requires that passwords be stored insecurely, users do not have Fever passwords by default, and logging in to the Fever protocol is disabled until a password is set. It is highly recommended that a user's Fever password be different from their general password. From 62d49e0d3cfe89c5aaa9af699593e85c48b3fb73 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 29 May 2021 21:48:02 -0400 Subject: [PATCH 285/366] Fill out most of the manual page Removed most of the online help as a consequence since maintaining both is frought --- lib/CLI.php | 211 +++---------------------------------------------- manpages/en.md | 165 +++++++++++++++++++++++++++++++++----- 2 files changed, 158 insertions(+), 218 deletions(-) diff --git a/lib/CLI.php b/lib/CLI.php index f892bddd..c867b3cd 100644 --- a/lib/CLI.php +++ b/lib/CLI.php @@ -13,223 +13,34 @@ use JKingWeb\Arsse\REST\Miniflux\Token as Miniflux; class CLI { public const USAGE = << - arsse.php conf save-defaults [] arsse.php user [list] arsse.php user add [] [--admin] arsse.php user remove arsse.php user show arsse.php user set arsse.php user unset - arsse.php user set-pass [] - [--oldpass=] [--fever] - arsse.php user unset-pass - [--oldpass=] [--fever] + arsse.php user set-pass [] [--fever] + arsse.php user unset-pass [--fever] arsse.php user auth [--fever] arsse.php token list arsse.php token create [
Functional, but has some display glitches.
NX NewsWebExtremely basic client.
reminiflux -

Three-pane alternative front-end for Minflux.

+

Three-pane alternative front-end for Minflux. Does not include functionality for managing feeds. Requires token authentication.

+
Tiny Tiny RSS Reader +

-

Does not (yet) support HTTP authentication.

+

Does not (yet) support HTTP authentication. Does not include functionality for managing feeds.

MinifluttAndroid
NewsJet RSS Android
maxiflux Web Level of functionality unclear.
MinifluttAndroidDoes not display articles ([see bug](https://github.com/DocMarty84/miniflutt/issues/3))
Newsie
NX NewsWeb -
ReadKit macOS
Tiny Tiny RSS ReaderWeb -

-
ttrss Sailfish
NX NewsWeb

Rentalware - For the software to be usable (you can't even add feeds otherwise) a subscription fee must be paid.

+

Support HTTP authentication with Fever.

Currently keeps showing items in the unread badge which have already been read.

Does not display articles ([see bug](https://github.com/DocMarty84/miniflutt/issues/3))Does not display articles (see bug)
Newsie