Browse Source

Passed code through linter

microsub
J. King 7 years ago
parent
commit
f7e50fe95d
  1. 25
      .php_cs.dist
  2. 7
      arsse.php
  3. 7
      lib/AbstractException.php
  4. 4
      lib/Arsse.php
  5. 34
      lib/CLI.php
  6. 41
      lib/Conf.php
  7. 2
      lib/Conf/Exception.php
  8. 289
      lib/Database.php
  9. 44
      lib/Db/AbstractDriver.php
  10. 32
      lib/Db/AbstractStatement.php
  11. 26
      lib/Db/Driver.php
  12. 2
      lib/Db/Exception.php
  13. 2
      lib/Db/ExceptionInput.php
  14. 2
      lib/Db/ExceptionSavepoint.php
  15. 2
      lib/Db/ExceptionTimeout.php
  16. 22
      lib/Db/Result.php
  17. 51
      lib/Db/SQLite3/Driver.php
  18. 7
      lib/Db/SQLite3/ExceptionBuilder.php
  19. 11
      lib/Db/SQLite3/Result.php
  20. 22
      lib/Db/SQLite3/Statement.php
  21. 10
      lib/Db/Statement.php
  22. 18
      lib/Db/Transaction.php
  23. 2
      lib/Exception.php
  24. 2
      lib/ExceptionFatal.php
  25. 121
      lib/Feed.php
  26. 2
      lib/Feed/Exception.php
  27. 84
      lib/Lang.php
  28. 2
      lib/Lang/Exception.php
  29. 51
      lib/Misc/Context.php
  30. 35
      lib/Misc/Date.php
  31. 58
      lib/Misc/Query.php
  32. 20
      lib/REST.php
  33. 46
      lib/REST/AbstractHandler.php
  34. 2
      lib/REST/Exception.php
  35. 2
      lib/REST/Exception405.php
  36. 2
      lib/REST/Exception501.php
  37. 6
      lib/REST/Handler.php
  38. 148
      lib/REST/NextCloudNews/V1_2.php
  39. 11
      lib/REST/NextCloudNews/Versions.php
  40. 32
      lib/REST/Request.php
  41. 21
      lib/REST/Response.php
  42. 33
      lib/Service.php
  43. 19
      lib/Service/Curl/Driver.php
  44. 12
      lib/Service/Driver.php
  45. 19
      lib/Service/Forking/Driver.php
  46. 17
      lib/Service/Internal/Driver.php
  47. 147
      lib/User.php
  48. 30
      lib/User/Driver.php
  49. 2
      lib/User/Exception.php
  50. 2
      lib/User/ExceptionAuthz.php
  51. 2
      lib/User/ExceptionNotImplemented.php
  52. 8
      lib/User/Internal/Driver.php
  53. 27
      lib/User/Internal/InternalFunctions.php
  54. 37
      tests/Conf/TestConf.php
  55. 2
      tests/Db/SQLite3/Database/TestDatabaseArticleSQLite3.php
  56. 2
      tests/Db/SQLite3/Database/TestDatabaseCleanupSQLite3.php
  57. 2
      tests/Db/SQLite3/Database/TestDatabaseFeedSQLite3.php
  58. 2
      tests/Db/SQLite3/Database/TestDatabaseFolderSQLite3.php
  59. 2
      tests/Db/SQLite3/Database/TestDatabaseMetaSQLite3.php
  60. 2
      tests/Db/SQLite3/Database/TestDatabaseMiscellanySQLite3.php
  61. 2
      tests/Db/SQLite3/Database/TestDatabaseSubscriptionSQLite3.php
  62. 2
      tests/Db/SQLite3/Database/TestDatabaseUserSQLite3.php
  63. 45
      tests/Db/SQLite3/TestDbDriverCreationSQLite3.php
  64. 74
      tests/Db/SQLite3/TestDbDriverSQLite3.php
  65. 31
      tests/Db/SQLite3/TestDbResultSQLite3.php
  66. 30
      tests/Db/SQLite3/TestDbStatementSQLite3.php
  67. 36
      tests/Db/SQLite3/TestDbUpdateSQLite3.php
  68. 11
      tests/Db/TestTransaction.php
  69. 18
      tests/Exception/TestException.php
  70. 44
      tests/Feed/TestFeed.php
  71. 28
      tests/Feed/TestFeedFetching.php
  72. 14
      tests/Lang/TestLang.php
  73. 23
      tests/Lang/TestLangErrors.php
  74. 27
      tests/Lang/testLangComplex.php
  75. 22
      tests/Misc/TestContext.php
  76. 85
      tests/REST/NextCloudNews/TestNCNV1_2.php
  77. 11
      tests/REST/NextCloudNews/TestNCNVersionDiscovery.php
  78. 19
      tests/Service/TestService.php
  79. 137
      tests/User/TestAuthorization.php
  80. 2
      tests/User/TestUserInternalDriver.php
  81. 2
      tests/User/TestUserMockExternal.php
  82. 2
      tests/User/TestUserMockInternal.php
  83. 2
      tests/docroot/Feed/Caching/200Future.php
  84. 2
      tests/docroot/Feed/Caching/200Multiple.php
  85. 2
      tests/docroot/Feed/Caching/200None.php
  86. 2
      tests/docroot/Feed/Caching/200Past.php
  87. 2
      tests/docroot/Feed/Caching/200PubDateOnly.php
  88. 2
      tests/docroot/Feed/Caching/200UpdateDate.php
  89. 2
      tests/docroot/Feed/Caching/304ETagOnly.php
  90. 2
      tests/docroot/Feed/Caching/304LastModOnly.php
  91. 2
      tests/docroot/Feed/Caching/304None.php
  92. 4
      tests/docroot/Feed/Caching/304Random.php
  93. 2
      tests/docroot/Feed/Deduplication/Hashes-Dates1.php
  94. 2
      tests/docroot/Feed/Deduplication/Hashes-Dates2.php
  95. 2
      tests/docroot/Feed/Deduplication/Hashes-Dates3.php
  96. 2
      tests/docroot/Feed/Deduplication/Hashes.php
  97. 2
      tests/docroot/Feed/Deduplication/ID-Dates.php
  98. 2
      tests/docroot/Feed/Deduplication/IdenticalHashes.php
  99. 2
      tests/docroot/Feed/Deduplication/Permalink-Dates.php
  100. 2
      tests/docroot/Feed/Fetching/EndlessLoop.php

25
.php_cs.dist

@ -0,0 +1,25 @@
<?php
namespace JKingWeb\Arsse;
require_once __DIR__.DIRECTORY_SEPARATOR."bootstrap.php";
$paths = [
__FILE__,
BASE."arsse.php",
BASE."lib",
BASE."tests",
];
$rules = [
'@PSR2' => true,
'braces' => ['position_after_functions_and_oop_constructs' => "same"],
];
$finder = \PhpCsFixer\Finder::create();
foreach ($paths as $path) {
if (is_file($path)) {
$finder = $finder->path($path);
} else {
$finder = $finder->in($path);
}
}
return \PhpCsFixer\Config::create()->setRules($rules)->setFinder($finder);

7
arsse.php

@ -1,8 +1,9 @@
<?php
namespace JKingWeb\Arsse;
require_once __DIR__.DIRECTORY_SEPARATOR."bootstrap.php";
if(\PHP_SAPI=="cli") {
if (\PHP_SAPI=="cli") {
// initialize the CLI; this automatically handles --help and --version
$cli = new CLI;
// handle other CLI requests; some do not require configuration
@ -10,9 +11,9 @@ if(\PHP_SAPI=="cli") {
} else {
// load configuration
Arsse::load(new Conf());
if(file_exists(BASE."config.php")) {
if (file_exists(BASE."config.php")) {
Arsse::$conf->importFile(BASE."config.php");
}
// handle Web requests
(new REST)->dispatch()->output();
}
}

7
lib/AbstractException.php

@ -3,7 +3,6 @@ declare(strict_types=1);
namespace JKingWeb\Arsse;
abstract class AbstractException extends \Exception {
const CODES = [
"Exception.uncoded" => -1,
"Exception.unknown" => 10000,
@ -71,13 +70,13 @@ abstract class AbstractException extends \Exception {
];
public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
if($msgID=="") {
if ($msgID=="") {
$msg = "Exception.unknown";
$code = 10000;
} else {
$class = get_called_class();
$codeID = str_replace("\\", "/", str_replace(NS_BASE, "", $class)).".$msgID";
if(!array_key_exists($codeID, self::CODES)) {
if (!array_key_exists($codeID, self::CODES)) {
throw new Exception("uncoded", $codeID);
} else {
$code = self::CODES[$codeID];
@ -87,4 +86,4 @@ abstract class AbstractException extends \Exception {
}
parent::__construct($msg, $code, $e);
}
}
}

4
lib/Arsse.php

@ -12,11 +12,11 @@ class Arsse {
/** @var User */
public static $user;
static function load(Conf $conf) {
public static function load(Conf $conf) {
static::$lang = new Lang();
static::$conf = $conf;
static::$lang->set($conf->lang);
static::$db = new Database();
static::$user = new User();
}
}
}

34
lib/CLI.php

@ -22,8 +22,8 @@ configuration to a sample file.
USAGE_TEXT;
}
function __construct(array $argv = null) {
if(is_null($argv)) {
public function __construct(array $argv = null) {
if (is_null($argv)) {
$argv = array_slice($_SERVER['argv'], 1);
}
$this->args = \Docopt::handle($this->usage(), [
@ -36,7 +36,7 @@ USAGE_TEXT;
protected function loadConf(): bool {
// FIXME: this should be a method of the Conf class
Arsse::load(new Conf());
if(file_exists(BASE."config.php")) {
if (file_exists(BASE."config.php")) {
Arsse::$conf->importFile(BASE."config.php");
}
// command-line operations will never respect authorization
@ -44,52 +44,52 @@ USAGE_TEXT;
return true;
}
function dispatch(array $args = null): int {
public function dispatch(array $args = null): int {
// act on command line
if(is_null($args)) {
if (is_null($args)) {
$args = $this->args;
}
if($this->command("daemon", $args)) {
if ($this->command("daemon", $args)) {
$this->loadConf();
return $this->daemon();
} else if($this->command("feed refresh", $args)) {
} elseif ($this->command("feed refresh", $args)) {
$this->loadConf();
return $this->feedRefresh((int) $args['<n>']);
} else if($this->command("conf save-defaults", $args)) {
} elseif ($this->command("conf save-defaults", $args)) {
return $this->confSaveDefaults($args['<file>']);
} else if($this->command("user add", $args)) {
} elseif ($this->command("user add", $args)) {
$this->loadConf();
return $this->userAdd($args['<username>'], $args['<password>']);
}
}
protected function command($cmd, $args): bool {
foreach(explode(" ", $cmd) as $part) {
if(!$args[$part]) {
foreach (explode(" ", $cmd) as $part) {
if (!$args[$part]) {
return false;
}
}
return true;
}
function daemon(bool $loop = true): int {
public function daemon(bool $loop = true): int {
(new Service)->watch($loop);
return 0; // FIXME: should return the exception code of thrown exceptions
}
function feedRefresh(int $id): int {
public function feedRefresh(int $id): int {
return (int) !Arsse::$db->feedUpdate($id); // FIXME: exception error codes should be returned here
}
function confSaveDefaults(string $file): int {
public function confSaveDefaults(string $file): int {
return (int) !(new Conf)->exportFile($file, true);
}
function userAdd(string $user, string $password = null): int {
public function userAdd(string $user, string $password = null): int {
$passwd = Arsse::$user->add($user, $password);
if(is_null($password)) {
if (is_null($password)) {
echo $passwd;
}
return 0;
}
}
}

41
lib/Conf.php

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace JKingWeb\Arsse;
/** Class for loading, saving, and querying configuration
*
*
* The Conf class serves both as a means of importing and querying configuration information, as well as a source for default parameters when a configuration file does not specify a value.
* All public properties are configuration parameters that may be set by the server administrator. */
class Conf {
@ -57,50 +57,50 @@ class Conf {
public $purgeFeeds = "PT24H";
/** @var string When to delete an unstarred article in the database after it has been marked read by all users, as an ISO 8601 duration (default: 7 days; empty string for never)
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
public $purgeArticlesRead = "P7D";
public $purgeArticlesRead = "P7D";
/** @var string When to delete an unstarred article in the database regardless of its read state, as an ISO 8601 duration (default: 21 days; empty string for never)
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
public $purgeArticlesUnread = "P21D";
public $purgeArticlesUnread = "P21D";
/** 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($import_file != "") {
if ($import_file != "") {
$this->importFile($import_file);
}
}
/** Layers configuration data from a file into an existing object
/** Layers configuration data from a file into an existing object
*
* The file must be a PHP script which return an array with keys that match the properties of the Conf class. Malformed files will throw an exception; unknown keys are silently ignored. Files may be imported is succession, though this is not currently used.
* @param string $file Full path and file name for the file to import */
public function importFile(string $file): self {
if(!file_exists($file)) {
if (!file_exists($file)) {
throw new Conf\Exception("fileMissing", $file);
} else if(!is_readable($file)) {
} elseif (!is_readable($file)) {
throw new Conf\Exception("fileUnreadable", $file);
}
try {
ob_start();
$arr = (@include $file);
} catch(\Throwable $e) {
} catch (\Throwable $e) {
$arr = null;
} finally {
ob_end_clean();
}
if(!is_array($arr)) {
if (!is_array($arr)) {
throw new Conf\Exception("fileCorrupt", $file);
}
return $this->import($arr);
}
/** Layers configuration data from an associative array into an existing object
/** Layers configuration data from an associative array into an existing object
*
* The input array must have keys that match the properties of the Conf class; unknown keys are silently ignored. Arrays may be imported is succession, though this is not currently used.
* @param mixed[] $arr Array of configuration parameters to export */
public function import(array $arr): self {
foreach($arr as $key => $value) {
foreach ($arr as $key => $value) {
$this->$key = $value;
}
return $this;
@ -112,13 +112,13 @@ class Conf {
$ref = new self;
$out = [];
$conf = new \ReflectionObject($this);
foreach($conf->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
foreach ($conf->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
$name = $prop->name;
// add the property to the output if the value is scalar and either:
// 1. full output has been requested
// 2. the property is not defined in the class
// 3. it differs from the default
if(is_scalar($this->$name) && ($full || !$prop->isDefault() || $this->$name !== $ref->$name)) {
if (is_scalar($this->$name) && ($full || !$prop->isDefault() || $this->$name !== $ref->$name)) {
$out[$name] = $this->$name;
}
}
@ -132,31 +132,32 @@ class Conf {
$arr = $this->export($full);
$conf = new \ReflectionObject($this);
$out = "<?php return [".PHP_EOL;
foreach($arr as $prop => $value) {
foreach ($arr as $prop => $value) {
$match = null;
$doc = $comment = "";
// retrieve the property's docblock, if it exists
try {
$doc = (new \ReflectionProperty(self::class, $prop))->getDocComment();
} catch(\ReflectionException $e) {}
if($doc) {
} catch (\ReflectionException $e) {
}
if ($doc) {
// parse the docblock to extract the property description
if(preg_match("<@var\s+\S+\s+(.+?)(?:\s*\*/)?$>m", $doc, $match)) {
if (preg_match("<@var\s+\S+\s+(.+?)(?:\s*\*/)?$>m", $doc, $match)) {
$comment = $match[1];
}
}
// append the docblock description if there is one, or an empty comment otherwise
$out .= " // ".$comment.PHP_EOL;
// append the property and an export of its value to the output
$out .= " ".var_export($prop, true)." => ".var_export($value,true).",".PHP_EOL;
$out .= " ".var_export($prop, true)." => ".var_export($value, true).",".PHP_EOL;
}
$out .= "];".PHP_EOL;
// write the configuration representation to the requested file
if(!@file_put_contents($file,$out)) {
if (!@file_put_contents($file, $out)) {
// if it fails throw an exception
$err = file_exists($file) ? "fileUnwritable" : "fileUncreatable";
throw new Conf\Exception($err, $file);
}
return true;
}
}
}

2
lib/Conf/Exception.php

@ -3,4 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Conf;
class Exception extends \JKingWeb\Arsse\AbstractException {
}
}

289
lib/Database.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use PasswordGenerator\Generator as PassGen;
use JKingWeb\Arsse\Misc\Query;
use JKingWeb\Arsse\Misc\Context;
@ -10,13 +11,13 @@ class Database {
const SCHEMA_VERSION = 1;
/** @var Db\Driver */
public $db;
public $db;
public function __construct($initialize = true) {
$driver = Arsse::$conf->dbDriver;
$this->db = new $driver();
$ver = $this->db->schemaVersion();
if($initialize && $ver < self::SCHEMA_VERSION) {
if ($initialize && $ver < self::SCHEMA_VERSION) {
$this->db->schemaUpdate(self::SCHEMA_VERSION);
}
}
@ -25,11 +26,11 @@ class Database {
return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function'];
}
static public function driverList(): array {
public static function driverList(): array {
$sep = \DIRECTORY_SEPARATOR;
$path = __DIR__.$sep."Db".$sep;
$classes = [];
foreach(glob($path."*".$sep."Driver.php") as $file) {
foreach (glob($path."*".$sep."Driver.php") as $file) {
$name = basename(dirname($file));
$class = NS_BASE."Db\\$name\\Driver";
$classes[$class] = $class::driverName();
@ -42,7 +43,7 @@ class Database {
}
public function driverSchemaUpdate(): bool {
if($this->db->schemaVersion() < self::SCHEMA_VERSION) {
if ($this->db->schemaVersion() < self::SCHEMA_VERSION) {
return $this->db->schemaUpdate(self::SCHEMA_VERSION);
}
return false;
@ -54,8 +55,8 @@ class Database {
[], // binding types
[], // binding values
];
foreach($valid as $prop => $type) {
if(!array_key_exists($prop, $props)) {
foreach ($valid as $prop => $type) {
if (!array_key_exists($prop, $props)) {
continue;
}
$out[0][] = "$prop = ?";
@ -72,9 +73,9 @@ class Database {
[], // binding types
];
// the query clause is just a series of question marks separated by commas
$out[0] = implode(",",array_fill(0,sizeof($values),"?"));
$out[0] = implode(",", array_fill(0, sizeof($values), "?"));
// the binding types are just a repetition of the supplied type
$out[1] = array_fill(0,sizeof($values),$type);
$out[1] = array_fill(0, sizeof($values), $type);
return $out;
}
@ -88,7 +89,7 @@ class Database {
public function metaSet(string $key, $value, string $type = "str"): bool {
$out = $this->db->prepare("UPDATE arsse_meta set value = ? where key is ?", $type, "str")->run($value, $key)->changes();
if(!$out) {
if (!$out) {
$out = $this->db->prepare("INSERT INTO arsse_meta(key,value) values(?,?)", "str", $type)->run($key, $value)->changes();
}
return (bool) $out;
@ -99,23 +100,23 @@ class Database {
}
public function userExists(string $user): bool {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
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 is ?", "str")->run($user)->getValue();
}
public function userAdd(string $user, string $password = null): string {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} else if($this->userExists($user)) {
} elseif ($this->userExists($user)) {
throw new User\Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]);
}
if($password===null) {
if ($password===null) {
$password = (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get();
}
$hash = "";
if(strlen($password) > 0) {
if (strlen($password) > 0) {
$hash = password_hash($password, \PASSWORD_DEFAULT);
}
$this->db->prepare("INSERT INTO arsse_users(id,password) values(?,?)", "str", "str")->runArray([$user,$hash]);
@ -123,10 +124,10 @@ class Database {
}
public function userRemove(string $user): bool {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
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 is ?", "str")->run($user)->changes() < 1) {
if ($this->db->prepare("DELETE from arsse_users where id is ?", "str")->run($user)->changes() < 1) {
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
}
return true;
@ -134,20 +135,20 @@ class Database {
public function userList(string $domain = null): array {
$out = [];
if($domain !== null) {
if(!Arsse::$user->authorize("@".$domain, __FUNCTION__)) {
if ($domain !== null) {
if (!Arsse::$user->authorize("@".$domain, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
}
$domain = str_replace(["\\","%","_"],["\\\\", "\\%", "\\_"], $domain);
$domain = str_replace(["\\","%","_"], ["\\\\", "\\%", "\\_"], $domain);
$domain = "%@".$domain;
foreach($this->db->prepare("SELECT id from arsse_users where id like ?", "str")->run($domain) as $user) {
foreach ($this->db->prepare("SELECT id from arsse_users where id like ?", "str")->run($domain) as $user) {
$out[] = $user['id'];
}
} else {
if(!Arsse::$user->authorize("", __FUNCTION__)) {
if (!Arsse::$user->authorize("", __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]);
}
foreach($this->db->query("SELECT id from arsse_users") as $user) {
foreach ($this->db->query("SELECT id from arsse_users") as $user) {
$out[] = $user['id'];
}
}
@ -155,25 +156,25 @@ class Database {
}
public function userPasswordGet(string $user): string {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} else if(!$this->userExists($user)) {
} elseif (!$this->userExists($user)) {
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
}
return (string) $this->db->prepare("SELECT password from arsse_users where id is ?", "str")->run($user)->getValue();
}
public function userPasswordSet(string $user, string $password = null): string {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} else if(!$this->userExists($user)) {
} elseif (!$this->userExists($user)) {
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
}
if($password===null) {
if ($password===null) {
$password = (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get();
}
$hash = "";
if(strlen($password) > 0) {
if (strlen($password) > 0) {
$hash = password_hash($password, \PASSWORD_DEFAULT);
}
$this->db->prepare("UPDATE arsse_users set password = ? where id is ?", "str", "str")->run($hash, $user);
@ -181,20 +182,20 @@ class Database {
}
public function userPropertiesGet(string $user): array {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
$prop = $this->db->prepare("SELECT name,rights from arsse_users where id is ?", "str")->run($user)->getRow();
if(!$prop) {
if (!$prop) {
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
}
return $prop;
}
public function userPropertiesSet(string $user, array $properties): array {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} else if(!$this->userExists($user)) {
} elseif (!$this->userExists($user)) {
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
}
$valid = [ // FIXME: add future properties
@ -206,16 +207,16 @@ class Database {
}
public function userRightsGet(string $user): int {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
return (int) $this->db->prepare("SELECT rights from arsse_users where id is ?", "str")->run($user)->getValue();
}
public function userRightsSet(string $user, int $rights): bool {
if(!Arsse::$user->authorize($user, __FUNCTION__, $rights)) {
if (!Arsse::$user->authorize($user, __FUNCTION__, $rights)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} else if(!$this->userExists($user)) {
} elseif (!$this->userExists($user)) {
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
}
$this->db->prepare("UPDATE arsse_users set rights = ? where id is ?", "int", "str")->run($rights, $user);
@ -224,30 +225,30 @@ class Database {
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__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// if the desired folder name is missing or invalid, throw an exception
if(!array_key_exists("name", $data) || $data['name']=="") {
if (!array_key_exists("name", $data) || $data['name']=="") {
throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "name"]);
} else if(!strlen(trim($data['name']))) {
} elseif (!strlen(trim($data['name']))) {
throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "name"]);
}
// normalize folder's parent, if there is one
$parent = array_key_exists("parent", $data) ? (int) $data['parent'] : 0;
if($parent===0) {
if ($parent===0) {
// if no parent is specified, do nothing
$parent = null;
} else {
// if a parent is specified, make sure it exists and belongs to the user; get its root (first-level) folder if it's a nested folder
$p = $this->db->prepare("SELECT id from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $parent)->getValue();
if(!$p) {
if (!$p) {
throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "parent", 'id' => $parent]);
}
}
// check if a folder by the same name already exists, because nulls are wonky in SQL
// FIXME: How should folder name be compared? Should a Unicode normalization be applied before comparison and insertion?
if($this->db->prepare("SELECT count(*) from arsse_folders where owner is ? and parent is ? and name is ?", "str", "int", "str")->run($user, $parent, $data['name'])->getValue() > 0) {
if ($this->db->prepare("SELECT count(*) from arsse_folders where owner is ? and parent is ? and name is ?", "str", "int", "str")->run($user, $parent, $data['name'])->getValue() > 0) {
throw new Db\ExceptionInput("constraintViolation"); // FIXME: There needs to be a practical message here
}
// actually perform the insert (!)
@ -256,17 +257,17 @@ class Database {
public function folderList(string $user, int $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__)) {
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
if(!is_null($parent)) {
if(!$this->db->prepare("SELECT count(*) from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $parent)->getValue()) {
if (!is_null($parent)) {
if (!$this->db->prepare("SELECT count(*) from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $parent)->getValue()) {
throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "parent", 'id' => $parent]);
}
}
// if we're not returning a recursive list we can use a simpler query
if(!$recursive) {
if (!$recursive) {
return $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and parent is ?", "str", "int")->run($user, $parent);
} else {
return $this->db->prepare(
@ -277,45 +278,45 @@ class Database {
}
public function folderRemove(string $user, int $id): bool {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
$changes = $this->db->prepare("DELETE FROM arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->changes();
if(!$changes) {
if (!$changes) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
}
return true;
}
public function folderPropertiesGet(string $user, int $id): array {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
$props = $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->getRow();
if(!$props) {
if (!$props) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
}
return $props;
}
public function folderPropertiesSet(string $user, int $id, array $data): bool {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// validate the folder ID and, if specified, the parent to move it to
$parent = null;
if(array_key_exists("parent", $data)) {
if (array_key_exists("parent", $data)) {
$parent = $data['parent'];
}
$f = $this->folderValidateId($user, $id, $parent, true);
// if a new name is specified, validate it
if(array_key_exists("name", $data)) {
if (array_key_exists("name", $data)) {
$this->folderValidateName($data['name']);
}
$data = array_merge($f, $data);
// check to make sure the target folder name/location would not create a duplicate (we must do this check because null is not distinct in SQL)
$existing = $this->db->prepare("SELECT id from arsse_folders where owner is ? and parent is ? and name is ?", "str", "int", "str")->run($user, $data['parent'], $data['name'])->getValue();
if(!is_null($existing) && $existing != $id) {
if (!is_null($existing) && $existing != $id) {
throw new Db\ExceptionInput("constraintViolation"); // FIXME: There needs to be a practical message here
}
$valid = [
@ -327,32 +328,32 @@ class Database {
}
protected function folderValidateId(string $user, int $id = null, int $parent = null, bool $subject = false): array {
if(is_null($id)) {
if (is_null($id)) {
// if no ID is specified this is a no-op, unless a parent is specified, which is always a circular dependence (the root cannot be moved)
if(!is_null($parent)) {
if (!is_null($parent)) {
throw new Db\ExceptionInput("circularDependence", ["action" => $this->caller(), "field" => "parent", 'id' => $parent]); // @codeCoverageIgnore
}
return ['name' => null, 'parent' => null];
}
// check whether the folder exists and is owned by the user
$f = $this->db->prepare("SELECT name,parent from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->getRow();
if(!$f) {
if (!$f) {
throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "folder", 'id' => $parent]);
}
// if we're moving a folder to a new parent, check that the parent is valid
if(!is_null($parent)) {
if (!is_null($parent)) {
// make sure both that the parent exists, and that the parent is not either the folder itself or one of its children (a circular dependence)
$p = $this->db->prepare(
"WITH RECURSIVE folders(id) as (SELECT id from arsse_folders where owner is ? and id is ? union select arsse_folders.id from arsse_folders join folders on arsse_folders.parent=folders.id) ".
"SELECT id,(id not in (select id from folders)) as valid from arsse_folders where owner is ? and id is ?",
"str", "int", "str", "int"
)->run($user, $id, $user, $parent)->getRow();
if(!$p) {
if (!$p) {
// if the parent doesn't exist or doesn't below to the user, throw an exception
throw new Db\ExceptionInput("idMissing", ["action" => $this->caller(), "field" => "parent", 'id' => $parent]);
} else {
// if using the desired parent would create a circular dependence, throw a different exception
if(!$p['valid']) {
if (!$p['valid']) {
throw new Db\ExceptionInput("circularDependence", ["action" => $this->caller(), "field" => "parent", 'id' => $parent]);
}
}
@ -362,9 +363,9 @@ class Database {
protected function folderValidateName($name): bool {
$name = (string) $name;
if(!strlen($name)) {
if (!strlen($name)) {
throw new Db\ExceptionInput("missing", ["action" => $this->caller(), "field" => "name"]);
} else if(!strlen(trim($name))) {
} elseif (!strlen(trim($name))) {
throw new Db\ExceptionInput("whitespace", ["action" => $this->caller(), "field" => "name"]);
} else {
return true;
@ -372,18 +373,18 @@ class Database {
}
public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = ""): int {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// check to see if the feed exists
$feedID = $this->db->prepare("SELECT id from arsse_feeds where url is ? and username is ? and password is ?", "str", "str", "str")->run($url, $fetchUser, $fetchPassword)->getValue();
if(is_null($feedID)) {
if (is_null($feedID)) {
// if the feed doesn't exist add it to the database; we do this unconditionally so as to lock SQLite databases for as little time as possible
$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);
} catch(\Throwable $e) {
} catch (\Throwable $e) {
// if the update fails, delete the feed we just added
$this->db->prepare('DELETE from arsse_feeds where id is ?', 'int')->run($feedID);
throw $e;
@ -394,7 +395,7 @@ class Database {
}
public function subscriptionList(string $user, int $folder = null, int $id = null): Db\Result {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// create a complex query
@ -415,11 +416,11 @@ class Database {
$q->setCTE("user(user)", "SELECT ?", "str", $user); // the subject user; this way we only have to pass it to prepare() once
// topmost folders belonging to the user
$q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders join user on owner is user where parent is null union select id,top from arsse_folders join topmost on parent=f_id");
if(!is_null($id)) {
if (!is_null($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 is ?", "int", $id);
} else if(!is_null($folder)) {
} elseif (!is_null($folder)) {
// if a folder is specified, make sure it exists
$this->folderValidateId($user, $folder);
// if it does exist, add a common table expression to list it and its children so that we select from the entire subtree
@ -431,50 +432,50 @@ class Database {
}
public function subscriptionRemove(string $user, int $id): bool {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
$changes = $this->db->prepare("DELETE from arsse_subscriptions where owner is ? and id is ?", "str", "int")->run($user, $id)->changes();
if(!$changes) {
if (!$changes) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
}
return true;
}
public function subscriptionPropertiesGet(string $user, int $id): array {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// disable authorization checks for the list call
Arsse::$user->authorizationEnabled(false);
$sub = $this->subscriptionList($user, null, $id)->getRow();
Arsse::$user->authorizationEnabled(true);
if(!$sub) {
if (!$sub) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]);
}
return $sub;
}
public function subscriptionPropertiesSet(string $user, int $id, array $data): bool {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
$tr = $this->db->begin();
if(!$this->db->prepare("SELECT count(*) from arsse_subscriptions where owner is ? and id is ?", "str", "int")->run($user, $id)->getValue()) {
if (!$this->db->prepare("SELECT count(*) from arsse_subscriptions where owner is ? and id is ?", "str", "int")->run($user, $id)->getValue()) {
// if the ID doesn't exist or doesn't belong to the user, throw an exception
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]);
}
if(array_key_exists("folder", $data)) {
if (array_key_exists("folder", $data)) {
// ensure the target folder exists and belong to the user
$this->folderValidateId($user, $data['folder']);
}
if(array_key_exists("title", $data)) {
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'])) {
if (!is_null($data['title'])) {
$title = (string) $data['title'];
if(!strlen($title)) {
if (!strlen($title)) {
throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "title"]);
} else if(!strlen(trim($title))) {
} elseif (!strlen(trim($title))) {
throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "title"]);
}
$data['title'] = $title;
@ -494,7 +495,7 @@ class Database {
protected function subscriptionValidateId(string $user, int $id): array {
$out = $this->db->prepare("SELECT feed from arsse_subscriptions where id is ? and owner is ?", "int", "str")->run($id, $user)->getRow();
if(!$out) {
if (!$out) {
throw new Db\ExceptionInput("idMissing", ["action" => $this->caller(), "field" => "subscription", 'id' => $id]);
}
return $out;
@ -502,14 +503,14 @@ class Database {
public function feedListStale(): array {
$feeds = $this->db->query("SELECT id from arsse_feeds where next_fetch <= CURRENT_TIMESTAMP")->getAll();
return array_column($feeds,'id');
return array_column($feeds, 'id');
}
public function feedUpdate(int $feedID, bool $throwError = false): bool {
$tr = $this->db->begin();
// check to make sure the feed exists
$f = $this->db->prepare("SELECT url, username, password, modified, etag, err_count, scrape FROM arsse_feeds where id is ?", "int")->run($feedID)->getRow();
if(!$f) {
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
@ -519,7 +520,7 @@ class Database {
// error instead of failing; if other exceptions are thrown, we should simply roll back
try {
$feed = new Feed($feedID, $f['url'], (string) Date::transform($f['modified'], "http", "sql"), $f['etag'], $f['username'], $f['password'], $scrape);
if(!$feed->modified) {
if (!$feed->modified) {
// if the feed hasn't changed, just compute the next fetch time and record it
$this->db->prepare("UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ? WHERE id is ?", 'datetime', 'int')->run($feed->nextFetch, $feedID);
$tr->commit();
@ -528,38 +529,38 @@ 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(
"UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ?, err_count = err_count + 1, err_msg = ? WHERE id is ?",
"UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ?, err_count = err_count + 1, err_msg = ? WHERE id is ?",
'datetime', 'str', 'int'
)->run(Feed::nextFetchOnError($f['err_count']), $e->getMessage(),$feedID);
)->run(Feed::nextFetchOnError($f['err_count']), $e->getMessage(), $feedID);
$tr->commit();
if($throwError) {
if ($throwError) {
throw $e;
}
return false;
}
//prepare the necessary statements to perform the update
if(sizeof($feed->newItems) || sizeof($feed->changedItems)) {
if (sizeof($feed->newItems) || sizeof($feed->changedItems)) {
$qInsertEnclosure = $this->db->prepare("INSERT INTO arsse_enclosures(article,url,type) values(?,?,?)", 'int', 'str', 'str');
$qInsertCategory = $this->db->prepare("INSERT INTO arsse_categories(article,name) values(?,?)", 'int', 'str');
$qInsertEdition = $this->db->prepare("INSERT INTO arsse_editions(article) values(?)", 'int');
}
if(sizeof($feed->newItems)) {
if (sizeof($feed->newItems)) {
$qInsertArticle = $this->db->prepare(
"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'
);
}
if(sizeof($feed->changedItems)) {
if (sizeof($feed->changedItems)) {
$qDeleteEnclosures = $this->db->prepare("DELETE FROM arsse_enclosures WHERE article is ?", 'int');
$qDeleteCategories = $this->db->prepare("DELETE FROM arsse_categories WHERE article is ?", 'int');
$qClearReadMarks = $this->db->prepare("UPDATE arsse_marks SET read = 0, modified = CURRENT_TIMESTAMP WHERE article is ? and read is 1", 'int');
$qUpdateArticle = $this->db->prepare(
"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 is ?",
"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 is ?",
'str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str', 'int'
);
}
// actually perform updates
foreach($feed->newItems as $article) {
foreach ($feed->newItems as $article) {
$articleID = $qInsertArticle->run(
$article->url,
$article->title,
@ -573,15 +574,15 @@ class Database {
$article->titleContentHash,
$feedID
)->lastId();
if($article->enclosureUrl) {
$qInsertEnclosure->run($articleID,$article->enclosureUrl,$article->enclosureType);
if ($article->enclosureUrl) {
$qInsertEnclosure->run($articleID, $article->enclosureUrl, $article->enclosureType);
}
foreach($article->categories as $c) {
foreach ($article->categories as $c) {
$qInsertCategory->run($articleID, $c);
}
$qInsertEdition->run($articleID);
}
foreach($feed->changedItems as $articleID => $article) {
foreach ($feed->changedItems as $articleID => $article) {
$qUpdateArticle->run(
$article->url,
$article->title,
@ -597,10 +598,10 @@ class Database {
);
$qDeleteEnclosures->run($articleID);
$qDeleteCategories->run($articleID);
if($article->enclosureUrl) {
$qInsertEnclosure->run($articleID,$article->enclosureUrl,$article->enclosureType);
if ($article->enclosureUrl) {
$qInsertEnclosure->run($articleID, $article->enclosureUrl, $article->enclosureType);
}
foreach($article->categories as $c) {
foreach ($article->categories as $c) {
$qInsertCategory->run($articleID, $c);
}
$qInsertEdition->run($articleID);
@ -608,7 +609,7 @@ class Database {
}
// lastly update the feed database itself with updated information.
$this->db->prepare(
"UPDATE arsse_feeds SET url = ?, title = ?, favicon = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ? WHERE id is ?",
"UPDATE arsse_feeds SET url = ?, title = ?, favicon = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ? WHERE id is ?",
'str', 'str', 'str', 'str', 'datetime', 'str', 'datetime', 'int', 'int'
)->run(
$feed->data->feedUrl,
@ -633,7 +634,7 @@ class Database {
$this->db->query("UPDATE arsse_feeds set orphaned = CURRENT_TIMESTAMP where orphaned is null and not exists(SELECT id from arsse_subscriptions where feed is arsse_feeds.id)");
// finally delete feeds that have been orphaned longer than the retention period
$limit = Date::normalize("now");
if(Arsse::$conf->purgeFeeds) {
if (Arsse::$conf->purgeFeeds) {
// if there is a retention period specified, compute it; otherwise feed are deleted immediatelty
$limit->sub(new \DateInterval(Arsse::$conf->purgeFeeds));
}
@ -645,29 +646,29 @@ class Database {
public function feedMatchLatest(int $feedID, int $count): Db\Result {
return $this->db->prepare(
"SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? ORDER BY modified desc, id desc limit ?",
"SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? ORDER BY modified desc, id desc limit ?",
'int', 'int'
)->run($feedID, $count);
}
public function feedMatchIds(int $feedID, array $ids = [], array $hashesUT = [], array $hashesUC = [], array $hashesTC = []): Db\Result {
// compile SQL IN() clauses and necessary type bindings for the four identifier lists
list($cId, $tId) = $this->generateIn($ids, "str");
list($cId, $tId) = $this->generateIn($ids, "str");
list($cHashUT, $tHashUT) = $this->generateIn($hashesUT, "str");
list($cHashUC, $tHashUC) = $this->generateIn($hashesUC, "str");
list($cHashTC, $tHashTC) = $this->generateIn($hashesTC, "str");
// perform the query
return $articles = $this->db->prepare(
"SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? and (guid in($cId) or url_title_hash in($cHashUT) or url_content_hash in($cHashUC) or title_content_hash in($cHashTC))",
"SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? 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
)->run($feedID, $ids, $hashesUT, $hashesUC, $hashesTC);
}
public function articleList(string $user, Context $context = null): Db\Result {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
if(!$context) {
if (!$context) {
$context = new Context;
}
$q = new Query(
@ -696,12 +697,12 @@ class Database {
$q->setOrder("edition".($context->reverse ? " desc" : ""));
$q->setLimit($context->limit, $context->offset);
$q->setCTE("user(user)", "SELECT ?", "str", $user);
if($context->subscription()) {
if ($context->subscription()) {
// if a subscription is specified, make sure it exists
$id = $this->subscriptionValidateId($user, $context->subscription)['feed'];
// add a basic CTE that will join in only the requested subscription
$q->setCTE("subscribed_feeds(id,sub)", "SELECT ?,?", ["int","int"], [$id,$context->subscription]);
} else if($context->folder()) {
} elseif ($context->folder()) {
// if a folder is specified, make sure it exists
$this->folderValidateId($user, $context->folder);
// if it does exist, add a common table expression to list it and its children so that we select from the entire subtree
@ -713,24 +714,24 @@ class Database {
$q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user is owner");
}
// filter based on edition offset
if($context->oldestEdition()) {
if ($context->oldestEdition()) {
$q->setWhere("edition >= ?", "int", $context->oldestEdition);
}
if($context->latestEdition()) {
if ($context->latestEdition()) {
$q->setWhere("edition <= ?", "int", $context->latestEdition);
}
// filter based on lastmod time
if($context->modifiedSince()) {
if ($context->modifiedSince()) {
$q->setWhere("modified_date >= ?", "datetime", $context->modifiedSince);
}
if($context->notModifiedSince()) {
if ($context->notModifiedSince()) {
$q->setWhere("modified_date <= ?", "datetime", $context->notModifiedSince);
}
// filter for un/read and un/starred status if specified
if($context->unread()) {
if ($context->unread()) {
$q->setWhere("unread is ?", "bool", $context->unread);
}
if($context->starred()) {
if ($context->starred()) {
$q->setWhere("starred is ?", "bool", $context->starred);
}
// perform the query and return results
@ -738,10 +739,10 @@ class Database {
}
public function articleMark(string $user, array $data, Context $context = null): bool {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
if(!$context) {
if (!$context) {
$context = new Context;
}
// sanitize input
@ -771,19 +772,19 @@ class Database {
// wrap this UPDATE and INSERT together into a transaction
$tr = $this->begin();
// if an edition context is specified, make sure it's valid
if($context->edition()) {
if ($context->edition()) {
// make sure the edition exists
$edition = $this->articleValidateEdition($user, $context->edition);
// if the edition is not the latest, do not mark the read flag
if(!$edition['current']) {
if (!$edition['current']) {
$values[0] = null;
}
} else if($context->article()) {
} elseif ($context->article()) {
// otherwise if an article context is specified, make sure it's valid
$this->articleValidateId($user, $context->article);
}
// execute each query in sequence
foreach($queries as $query) {
foreach ($queries as $query) {
// first build the query which will select the target articles; we will later turn this into a CTE for the actual query that manipulates the articles
$q = new Query(
"SELECT
@ -802,12 +803,12 @@ class Database {
$q->setCTE("user(user)", "SELECT ?", "str", $user);
// common table expression with the values to set
$q->setCTE("target_values(read,starred)", "SELECT ?,?", ["bool","bool"], $values);
if($context->subscription()) {
if ($context->subscription()) {
// if a subscription is specified, make sure it exists
$id = $this->subscriptionValidateId($user, $context->subscription)['feed'];
// add a basic CTE that will join in only the requested subscription
$q->setCTE("subscribed_feeds(id,sub)", "SELECT ?,?", ["int","int"], [$id,$context->subscription], "join subscribed_feeds on feed is subscribed_feeds.id");
} else if($context->folder()) {
} elseif ($context->folder()) {
// if a folder is specified, make sure it exists
$this->folderValidateId($user, $context->folder);
// if it does exist, add a common table expression to list it and its children so that we select from the entire subtree
@ -818,18 +819,18 @@ class Database {
// otherwise add a CTE for all the user's subscriptions
$q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user is owner", [], [], "join subscribed_feeds on feed is subscribed_feeds.id");
}
if($context->edition()) {
if ($context->edition()) {
// if an edition is specified, filter for its previously identified article
$q->setWhere("arsse_articles.id is ?", "int", $edition['article']);
} else if($context->article()) {
} elseif ($context->article()) {
// if an article is specified, filter for it (it has already been validated above)
$q->setWhere("arsse_articles.id is ?", "int", $context->article);
}
if($context->editions()) {
if ($context->editions()) {
// if multiple specific editions have been requested, prepare a CTE to list them and their articles
if(!$context->editions) {
if (!$context->editions) {
throw new Db\ExceptionInput("tooShort", ['field' => "editions", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element
} else if(sizeof($context->editions) > 50) {
} elseif (sizeof($context->editions) > 50) {
throw new Db\ExceptionInput("tooLong", ['field' => "editions", 'action' => __FUNCTION__, 'max' => 50]); // must not have more than 50 array elements
}
list($inParams, $inTypes) = $this->generateIn($context->editions, "int");
@ -839,15 +840,15 @@ class Database {
$context->editions
);
$q->setWhere("arsse_articles.id in (select id from requested_articles)");
} else if($context->articles()) {
} elseif ($context->articles()) {
// if multiple specific articles have been requested, prepare a CTE to list them and their articles
if(!$context->articles) {
if (!$context->articles) {
throw new Db\ExceptionInput("tooShort", ['field' => "articles", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element
} else if(sizeof($context->articles) > 50) {
} elseif (sizeof($context->articles) > 50) {
throw new Db\ExceptionInput("tooLong", ['field' => "articles", 'action' => __FUNCTION__, 'max' => 50]); // must not have more than 50 array elements
}
list($inParams, $inTypes) = $this->generateIn($context->articles, "int");
$q->setCTE("requested_articles(id,edition)",
$q->setCTE("requested_articles(id,edition)",
"SELECT id,(select max(id) from arsse_editions where article is arsse_articles.id) as edition from arsse_articles where arsse_articles.id in ($inParams)",
$inTypes,
$context->articles
@ -858,17 +859,17 @@ class Database {
$q->setCTE("requested_articles(id,edition)", "SELECT 'empty','table' where 1 is 0");
}
// filter based on edition offset
if($context->oldestEdition()) {
if ($context->oldestEdition()) {
$q->setWhere("edition >= ?", "int", $context->oldestEdition);
}
if($context->latestEdition()) {
if ($context->latestEdition()) {
$q->setWhere("edition <= ?", "int", $context->latestEdition);
}
// filter based on lastmod time
if($context->modifiedSince()) {
if ($context->modifiedSince()) {
$q->setWhere("modified_date >= ?", "datetime", $context->modifiedSince);
}
if($context->notModifiedSince()) {
if ($context->notModifiedSince()) {
$q->setWhere("modified_date <= ?", "datetime", $context->notModifiedSince);
}
// push the current query onto the CTE stack and execute the query we're actually interested in
@ -882,7 +883,7 @@ class Database {
}
public function articleStarredCount(string $user): int {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
return $this->db->prepare("SELECT count(*) from arsse_marks where starred is 1 and subscription in (select id from arsse_subscriptions where owner is ?)", "str")->run($user)->getValue();
@ -913,14 +914,14 @@ class Database {
);
$limitRead = null;
$limitUnread = null;
if(Arsse::$conf->purgeArticlesRead) {
if (Arsse::$conf->purgeArticlesRead) {
$limitRead = Date::sub(Arsse::$conf->purgeArticlesRead);
}
if(Arsse::$conf->purgeArticlesUnread) {
if (Arsse::$conf->purgeArticlesUnread) {
$limitUnread = Date::sub(Arsse::$conf->purgeArticlesUnread);
}
$feeds = $this->db->query("SELECT id, size from arsse_feeds")->getAll();
foreach($feeds as $feed) {
foreach ($feeds as $feed) {
$query->run($feed['id'], $feed['size'], $limitUnread, $limitRead);
}
return true;
@ -938,7 +939,7 @@ class Database {
arsse_articles.id is ? and arsse_subscriptions.owner is ?",
"int", "str"
)->run($id, $user)->getRow();
if(!$out) {
if (!$out) {
throw new Db\ExceptionInput("subjectMissing", ["action" => $this->caller(), "field" => "article", 'id' => $id]);
}
return $out;
@ -958,21 +959,21 @@ class Database {
edition is ? and arsse_subscriptions.owner is ?",
"int", "str"
)->run($id, $user)->getRow();
if(!$out) {
if (!$out) {
throw new Db\ExceptionInput("subjectMissing", ["action" => $this->caller(), "field" => "edition", 'id' => $id]);
}
return $out;
}
public function editionLatest(string $user, Context $context = null): int {
if(!Arsse::$user->authorize($user, __FUNCTION__)) {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
if(!$context) {
if (!$context) {
$context = new Context;
}
$q = new Query("SELECT max(arsse_editions.id) from arsse_editions left join arsse_articles on article is arsse_articles.id left join arsse_feeds on arsse_articles.feed is arsse_feeds.id");
if($context->subscription()) {
if ($context->subscription()) {
// if a subscription is specified, make sure it exists
$id = $this->subscriptionValidateId($user, $context->subscription)['feed'];
// a simple WHERE clause is required here
@ -983,4 +984,4 @@ class Database {
}
return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue();
}
}
}

44
lib/Db/AbstractDriver.php

@ -7,16 +7,16 @@ abstract class AbstractDriver implements Driver {
protected $transDepth = 0;
protected $transStatus = [];
public abstract function prepareArray(string $query, array $paramTypes): Statement;
protected abstract function lock(): bool;
protected abstract function unlock(bool $rollback = false) : bool;
abstract public function prepareArray(string $query, array $paramTypes): Statement;
abstract protected function lock(): bool;
abstract protected function unlock(bool $rollback = false) : bool;
/** @codeCoverageIgnore */
public function schemaVersion(): int {
// FIXME: generic schemaVersion() will need to be covered for database engines other than SQLite
try {
return (int) $this->query("SELECT value from arsse_meta where key is schema_version")->getValue();
} catch(Exception $e) {
} catch (Exception $e) {
return 0;
}
}
@ -26,7 +26,7 @@ abstract class AbstractDriver implements Driver {
}
public function savepointCreate(bool $lock = false): int {
if($lock && !$this->transDepth) {
if ($lock && !$this->transDepth) {
$this->lock();
$this->locked = true;
}
@ -36,17 +36,17 @@ abstract class AbstractDriver implements Driver {
}
public function savepointRelease(int $index = null): bool {
if(is_null($index)) {
if (is_null($index)) {
$index = $this->transDepth;
}
if(array_key_exists($index, $this->transStatus)) {
switch($this->transStatus[$index]) {
if (array_key_exists($index, $this->transStatus)) {
switch ($this->transStatus[$index]) {
case self::TR_PEND:
$this->exec("RELEASE SAVEPOINT arsse_".$index);
$this->transStatus[$index] = self::TR_COMMIT;
$a = $index;
while(++$a && $a <= $this->transDepth) {
if($this->transStatus[$a] <= self::TR_PEND) {
while (++$a && $a <= $this->transDepth) {
if ($this->transStatus[$a] <= self::TR_PEND) {
$this->transStatus[$a] = self::TR_PEND_COMMIT;
}
}
@ -66,13 +66,13 @@ abstract class AbstractDriver implements Driver {
default:
throw new Exception("unknownSavepointStatus", $this->transStatus[$index]); //@codeCoverageIgnore
}
if($index==$this->transDepth) {
while($this->transDepth > 0 && $this->transStatus[$this->transDepth] > self::TR_PEND) {
if ($index==$this->transDepth) {
while ($this->transDepth > 0 && $this->transStatus[$this->transDepth] > self::TR_PEND) {
array_pop($this->transStatus);
$this->transDepth--;
}
}
if(!$this->transDepth && $this->locked) {
if (!$this->transDepth && $this->locked) {
$this->unlock();
$this->locked = false;
}
@ -83,18 +83,18 @@ abstract class AbstractDriver implements Driver {
}
public function savepointUndo(int $index = null): bool {
if(is_null($index)) {
if (is_null($index)) {
$index = $this->transDepth;
}
if(array_key_exists($index, $this->transStatus)) {
switch($this->transStatus[$index]) {
if (array_key_exists($index, $this->transStatus)) {
switch ($this->transStatus[$index]) {
case self::TR_PEND:
$this->exec("ROLLBACK TRANSACTION TO SAVEPOINT arsse_".$index);
$this->exec("RELEASE SAVEPOINT arsse_".$index);
$this->transStatus[$index] = self::TR_ROLLBACK;
$a = $index;
while(++$a && $a <= $this->transDepth) {
if($this->transStatus[$a] <= self::TR_PEND) {
while (++$a && $a <= $this->transDepth) {
if ($this->transStatus[$a] <= self::TR_PEND) {
$this->transStatus[$a] = self::TR_PEND_ROLLBACK;
}
}
@ -114,13 +114,13 @@ abstract class AbstractDriver implements Driver {
default:
throw new Exception("unknownSavepointStatus", $this->transStatus[$index]); //@codeCoverageIgnore
}
if($index==$this->transDepth) {
while($this->transDepth > 0 && $this->transStatus[$this->transDepth] > self::TR_PEND) {
if ($index==$this->transDepth) {
while ($this->transDepth > 0 && $this->transStatus[$this->transDepth] > self::TR_PEND) {
array_pop($this->transStatus);
$this->transDepth--;
}
}
if(!$this->transDepth && $this->locked) {
if (!$this->transDepth && $this->locked) {
$this->unlock(true);
$this->locked = false;
}
@ -133,4 +133,4 @@ abstract class AbstractDriver implements Driver {
public function prepare(string $query, ...$paramType): Statement {
return $this->prepareArray($query, $paramType);
}
}
}

32
lib/Db/AbstractStatement.php

@ -1,15 +1,15 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
use JKingWeb\Arsse\Misc\Date;
abstract class AbstractStatement implements Statement {
protected $types = [];
protected $isNullable = [];
protected $values = ['pre' => [], 'post' => []];
abstract function runArray(array $values = []): Result;
abstract public function runArray(array $values = []): Result;
public function run(...$values): Result {
return $this->runArray($values);
@ -20,23 +20,23 @@ abstract class AbstractStatement implements Statement {
}
public function rebindArray(array $bindings, bool $append = false): bool {
if(!$append) {
if (!$append) {
$this->types = [];
}
foreach($bindings as $binding) {
if(is_array($binding)) {
foreach ($bindings as $binding) {
if (is_array($binding)) {
// recursively flatten any arrays, which may be provided for SET or IN() clauses
$this->rebindArray($binding, true);
} else {
$binding = trim(strtolower($binding));
if(strpos($binding, "strict ")===0) {
if (strpos($binding, "strict ")===0) {
// "strict" types' values may never be null; null values will later be cast to the type specified
$this->isNullable[] = false;
$binding = substr($binding, 7);
} else {
$this->isNullable[] = true;
}
if(!array_key_exists($binding, self::TYPES)) {
if (!array_key_exists($binding, self::TYPES)) {
throw new Exception("paramTypeInvalid", $binding); // @codeCoverageIgnore
}
$this->types[] = self::TYPES[$binding];
@ -46,19 +46,19 @@ abstract class AbstractStatement implements Statement {
}
protected function cast($v, string $t, bool $nullable) {
switch($t) {
switch ($t) {
case "date":
if(is_null($v) && !$nullable) {
if (is_null($v) && !$nullable) {
$v = 0;
}
return Date::transform($v, "date");
case "time":
if(is_null($v) && !$nullable) {
if (is_null($v) && !$nullable) {
$v = 0;
}
return Date::transform($v, "time");
case "datetime":
if(is_null($v) && !$nullable) {
if (is_null($v) && !$nullable) {
$v = 0;
}
return Date::transform($v, "sql");
@ -68,15 +68,15 @@ abstract class AbstractStatement implements Statement {
case "binary":
case "string":
case "boolean":
if($t=="binary") {
if ($t=="binary") {
$t = "string";
}
if($v instanceof \DateTimeInterface) {
if($t=="string") {
if ($v instanceof \DateTimeInterface) {
if ($t=="string") {
return Date::transform($v, "sql");
} else {
$v = $v->getTimestamp();
settype($v, $t);
settype($v, $t);
}
} else {
settype($v, $t);
@ -86,4 +86,4 @@ abstract class AbstractStatement implements Statement {
throw new Exception("paramTypeUnknown", $type); // @codeCoverageIgnore
}
}
}
}

26
lib/Db/Driver.php

@ -9,26 +9,26 @@ interface Driver {
const TR_PEND_COMMIT = -1;
const TR_PEND_ROLLBACK = -2;
function __construct();
public function __construct();
// returns a human-friendly name for the driver (for display in installer, for example)
static function driverName(): string;
public static function driverName(): string;
// returns the version of the scheme of the opened database; if uninitialized should return 0
function schemaVersion(): int;
public function schemaVersion(): int;
// return a Transaction object
function begin(bool $lock = false): Transaction;
public function begin(bool $lock = false): Transaction;
// manually begin a real or synthetic transactions, with real or synthetic nesting
function savepointCreate(): int;
public function savepointCreate(): int;
// manually commit either the latest or all pending nested transactions
function savepointRelease(int $index = null): bool;
public function savepointRelease(int $index = null): bool;
// manually rollback either the latest or all pending nested transactions
function savepointUndo(int $index = null): bool;
public function savepointUndo(int $index = null): bool;
// attempt to perform an in-place upgrade of the database schema; this may be a no-op which always throws an exception
function schemaUpdate(int $to): bool;
public function schemaUpdate(int $to): bool;
// execute one or more unsanitized SQL queries and return an indication of success
function exec(string $query): bool;
public function exec(string $query): bool;
// perform a single unsanitized query and return a result set
function query(string $query): Result;
public function query(string $query): Result;
// ready a prepared statement for later execution
function prepare(string $query, ...$paramType): Statement;
function prepareArray(string $query, array $paramTypes): Statement;
}
public function prepare(string $query, ...$paramType): Statement;
public function prepareArray(string $query, array $paramTypes): Statement;
}

2
lib/Db/Exception.php

@ -3,4 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
class Exception extends \JKingWeb\Arsse\AbstractException {
}
}

2
lib/Db/ExceptionInput.php

@ -3,4 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
class ExceptionInput extends \JKingWeb\Arsse\AbstractException {
}
}

2
lib/Db/ExceptionSavepoint.php

@ -3,4 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
class ExceptionSavepoint extends \JKingWeb\Arsse\AbstractException {
}
}

2
lib/Db/ExceptionTimeout.php

@ -3,4 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
class ExceptionTimeout extends \JKingWeb\Arsse\AbstractException {
}
}

22
lib/Db/Result.php

@ -3,16 +3,16 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
interface Result extends \Iterator {
function current();
function key();
function next();
function rewind();
function valid();
public function current();
public function key();
public function next();
public function rewind();
public function valid();
function getRow();
function getAll(): array;
function getValue();
public function getRow();
public function getAll(): array;
public function getValue();
function changes();
function lastId();
}
public function changes();
public function lastId();
}

51
lib/Db/SQLite3/Driver.php

@ -1,12 +1,12 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Db\Exception;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout;
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
use ExceptionBuilder;
@ -18,11 +18,11 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
public function __construct() {
// check to make sure required extension is loaded
if(!class_exists("SQLite3")) {
if (!class_exists("SQLite3")) {
throw new Exception("extMissing", self::driverName()); // @codeCoverageIgnore
}
$dbFile = Arsse::$conf->dbSQLite3File;
if(is_null($dbFile)) {
if (is_null($dbFile)) {
// if no database file is specified in the configuration, use a suitable default
$dbFile = \JKingWeb\Arsse\BASE."arsse.db";
}
@ -34,21 +34,21 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
$this->db->enableExceptions(true);
$this->exec("PRAGMA journal_mode = wal");
$this->exec("PRAGMA foreign_keys = yes");
} catch(\Throwable $e) {
} catch (\Throwable $e) {
// if opening the database doesn't work, check various pre-conditions to find out what the problem might be
$files = [
$dbFile, // main database file
$dbFile."-wal", // write-ahead log journal
$dbFile."-shm", // shared memory index
];
foreach($files as $file) {
if(!file_exists($file) && !is_writable(dirname($file))) {
foreach ($files as $file) {
if (!file_exists($file) && !is_writable(dirname($file))) {
throw new Exception("fileUncreatable", $file);
} else if(!is_readable($file) && !is_writable($file)) {
} elseif (!is_readable($file) && !is_writable($file)) {
throw new Exception("fileUnusable", $file);
} else if(!is_readable($file)) {
} elseif (!is_readable($file)) {
throw new Exception("fileUnreadable", $file);
} else if(!is_writable($file)) {
} elseif (!is_writable($file)) {
throw new Exception("fileUnwritable", $file);
}
}
@ -64,12 +64,15 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
}
public function __destruct() {
try{$this->db->close();} catch(\Exception $e) {}
try {
$this->db->close();
} catch (\Exception $e) {
}
unset($this->db);
}
static public function driverName(): string {
public static function driverName(): string {
return Arsse::$lang->msg("Driver.Db.SQLite3.Name");
}
@ -79,37 +82,37 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
public function schemaUpdate(int $to, string $basePath = null): bool {
$ver = $this->schemaVersion();
if(!Arsse::$conf->dbAutoUpdate) {
if (!Arsse::$conf->dbAutoUpdate) {
throw new Exception("updateManual", ['version' => $ver, 'driver_name' => $this->driverName()]);
} else if($ver >= $to) {
} elseif ($ver >= $to) {
throw new Exception("updateTooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]);
}
$sep = \DIRECTORY_SEPARATOR;
$path = ($basePath ?? \JKingWeb\Arsse\BASE."sql").$sep."SQLite3".$sep;
// lock the database
$this->savepointCreate(true);
for($a = $this->schemaVersion(); $a < $to; $a++) {
for ($a = $this->schemaVersion(); $a < $to; $a++) {
$this->savepointCreate();
try {
$file = $path.$a.".sql";
if(!file_exists($file)) {
if (!file_exists($file)) {
throw new Exception("updateFileMissing", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
} else if(!is_readable($file)) {
} elseif (!is_readable($file)) {
throw new Exception("updateFileUnreadable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
}
$sql = @file_get_contents($file);
if($sql===false) {
if ($sql===false) {
throw new Exception("updateFileUnusable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); // @codeCoverageIgnore
}
try {
$this->exec($sql);
} catch(\Throwable $e) {
} catch (\Throwable $e) {
throw new Exception("updateFileError", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a, 'message' => $this->getError()]);
}
if($this->schemaVersion() != $a+1) {
if ($this->schemaVersion() != $a+1) {
throw new Exception("updateFileIncomplete", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
}
} catch(\Throwable $e) {
} catch (\Throwable $e) {
// undo any partial changes from the failed update
$this->savepointUndo();
// commit any successful updates if updating by more than one version
@ -130,7 +133,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
public function exec(string $query): bool {
try {
return (bool) $this->db->exec($query);
} catch(\Exception $e) {
} catch (\Exception $e) {
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
throw new $excClass($excMsg, $excData);
}
@ -139,7 +142,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
public function query(string $query): \JKingWeb\Arsse\Db\Result {
try {
$r = $this->db->query($query);
} catch(\Exception $e) {
} catch (\Exception $e) {
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
throw new $excClass($excMsg, $excData);
}
@ -151,7 +154,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement {
try {
$s = $this->db->prepare($query);
} catch(\Exception $e) {
} catch (\Exception $e) {
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
throw new $excClass($excMsg, $excData);
}
@ -167,4 +170,4 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
$this->exec((!$rollback) ? "COMMIT" : "ROLLBACK");
return true;
}
}
}

7
lib/Db/SQLite3/ExceptionBuilder.php

@ -1,15 +1,14 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3;
use JKingWeb\Arsse\Db\Exception;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout;
trait ExceptionBuilder {
public function exceptionBuild() {
switch($this->db->lastErrorCode()) {
switch ($this->db->lastErrorCode()) {
case self::SQLITE_BUSY:
return [ExceptionTimeout::class, 'general', $this->db->lastErrorMsg()];
case self::SQLITE_CONSTRAINT:
@ -20,4 +19,4 @@ trait ExceptionBuilder {
return [Exception::class, 'engineErrorGeneral', $this->db->lastErrorMsg()];
}
}
}
}

11
lib/Db/SQLite3/Result.php

@ -14,7 +14,7 @@ class Result implements \JKingWeb\Arsse\Db\Result {
public function getValue() {
$this->next();
if($this->valid()) {
if ($this->valid()) {
$keys = array_keys($this->cur);
return $this->cur[array_shift($keys)];
}
@ -28,7 +28,7 @@ class Result implements \JKingWeb\Arsse\Db\Result {
public function getAll(): array {
$out = [];
foreach($this as $row) {
foreach ($this as $row) {
$out [] = $row;
}
return $out;
@ -52,7 +52,10 @@ class Result implements \JKingWeb\Arsse\Db\Result {
}
public function __destruct() {
try{$this->set->finalize();} catch(\Throwable $e) {}
try {
$this->set->finalize();
} catch (\Throwable $e) {
}
unset($this->set);
}
@ -81,4 +84,4 @@ class Result implements \JKingWeb\Arsse\Db\Result {
$this->cur = null;
$this->set->reset();
}
}
}

22
lib/Db/SQLite3/Statement.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3;
use JKingWeb\Arsse\Db\Exception;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout;
@ -33,7 +34,10 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
}
public function __destruct() {
try {$this->st->close();} catch(\Throwable $e) {}
try {
$this->st->close();
} catch (\Throwable $e) {
}
unset($this->st);
}
@ -42,7 +46,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
$this->bindValues($values);
try {
$r = $this->st->execute();
} catch(\Exception $e) {
} catch (\Exception $e) {
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
throw new $excClass($excMsg, $excData);
}
@ -53,22 +57,22 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
protected function bindValues(array $values, int $offset = 0): int {
$a = $offset;
foreach($values as $value) {
if(is_array($value)) {
foreach ($values as $value) {
if (is_array($value)) {
// recursively flatten any arrays, which may be provided for SET or IN() clauses
$a += $this->bindValues($value, $a);
} else if(array_key_exists($a,$this->types)) {
} elseif (array_key_exists($a, $this->types)) {
// if the parameter type is something other than the known values, this is an error
assert(array_key_exists($this->types[$a], self::BINDINGS), new Exception("paramTypeUnknown", $this->types[$a]));
// if the parameter type is null or the value is null (and the type is nullable), just bind null
if($this->types[$a]=="null" || ($this->isNullable[$a] && is_null($value))) {
if ($this->types[$a]=="null" || ($this->isNullable[$a] && is_null($value))) {
$this->st->bindValue($a+1, null, \SQLITE3_NULL);
} else {
} else {
// otherwise cast the value to the right type and bind the result
$type = self::BINDINGS[$this->types[$a]];
$value = $this->cast($value, $this->types[$a], $this->isNullable[$a]);
// re-adjust for null casts
if($value===null) {
if ($value===null) {
$type = \SQLITE3_NULL;
}
// perform binding
@ -81,4 +85,4 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
}
return $a - $offset;
}
}
}

10
lib/Db/Statement.php

@ -27,8 +27,8 @@ interface Statement {
"bit" => "boolean",
];
function run(...$values): Result;
function runArray(array $values = []): Result;
function rebind(...$bindings): bool;
function rebindArray(array $bindings): bool;
}
public function run(...$values): Result;
public function runArray(array $values = []): Result;
public function rebind(...$bindings): bool;
public function rebindArray(array $bindings): bool;
}

18
lib/Db/Transaction.php

@ -7,39 +7,39 @@ class Transaction {
protected $pending = false;
protected $drv;
function __construct(Driver $drv, bool $lock = false) {
public function __construct(Driver $drv, bool $lock = false) {
$this->index = $drv->savepointCreate($lock);
$this->drv = $drv;
$this->pending = true;
}
function __destruct() {
if($this->pending) {
public function __destruct() {
if ($this->pending) {
try {
$this->drv->savepointUndo($this->index);
} catch(\Throwable $e) {
} catch (\Throwable $e) {
// do nothing
}
}
}
function commit(): bool {
public function commit(): bool {
$out = $this->drv->savepointRelease($this->index);
$this->pending = false;
return $out;
}
function rollback(): bool {
public function rollback(): bool {
$out = $this->drv->savepointUndo($this->index);
$this->pending = false;
return $out;
}
function getIndex(): int {
public function getIndex(): int {
return $this->index;
}
function isPending(): bool {
public function isPending(): bool {
return $this->pending;
}
}
}

2
lib/Exception.php

@ -3,4 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse;
class Exception extends AbstractException {
}
}

2
lib/ExceptionFatal.php

@ -6,4 +6,4 @@ class ExceptionFatal extends AbstractException {
public function __construct($msg = "", $code = 0, $e = null) {
\Exception::__construct($msg, $code, $e);
}
}
}

121
lib/Feed.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\Misc\Date;
use PicoFeed\PicoFeedException;
use PicoFeed\Config\Config;
@ -8,7 +9,7 @@ use PicoFeed\Reader\Reader;
use PicoFeed\Reader\Favicon;
use PicoFeed\Scraper\Scraper;
class Feed {
class Feed {
public $data = null;
public $favicon;
public $parser;
@ -38,25 +39,25 @@ class Feed {
$this->download($url, $lastModified, $etag, $username, $password);
// format the HTTP Last-Modified date returned
$lastMod = $this->resource->getLastModified();
if(strlen($lastMod)) {
if (strlen($lastMod)) {
$this->lastModified = Date::normalize($lastMod, "http");
}
$this->modified = $this->resource->isModified();
//parse the feed, if it has been modified
if($this->modified) {
if ($this->modified) {
$this->parse();
// ascertain whether there are any articles not in the database
$this->matchToDatabase($feedID);
// if caching header fields are not sent by the server, try to ascertain a last-modified date from the feed contents
if(!$this->lastModified) {
if (!$this->lastModified) {
$this->lastModified = $this->computeLastModified();
}
// 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)) {
if (!sizeof($this->newItems) && !sizeof($this->changedItems)) {
$this->modified = false;
}
// if requested, scrape full content for any new and changed items
if($scrape) {
if ($scrape) {
$this->scrape();
}
}
@ -107,19 +108,19 @@ class Feed {
// id doesn't exist.
$content = $f->content.$f->enclosureUrl.$f->enclosureType;
// if the item link URL and item title are both equal to the feed link URL, then the item has neither a link URL nor a title
if($f->url==$feed->siteUrl && $f->title==$feed->siteUrl) {
if ($f->url==$feed->siteUrl && $f->title==$feed->siteUrl) {
$f->urlTitleHash = "";
} else {
$f->urlTitleHash = hash('sha256', $f->url.$f->title);
}
// if the item link URL is equal to the feed link URL, it has no link URL; if there is additionally no content, these should not be hashed
if(!strlen($content) && $f->url==$feed->siteUrl) {
$f->urlContentHash = "";
if (!strlen($content) && $f->url==$feed->siteUrl) {
$f->urlContentHash = "";
} else {
$f->urlContentHash = hash('sha256', $f->url.$content);
}
// if the item's title is the same as its link URL, it has no title; if there is additionally no content, these should not be hashed
if(!strlen($content) && $f->title==$f->url) {
if (!strlen($content) && $f->title==$f->url) {
$f->titleContentHash = "";
} else {
$f->titleContentHash = hash('sha256', $f->title.$content);
@ -128,44 +129,44 @@ class Feed {
// prefer an Atom ID as the item's ID
$id = (string) $f->xml->children('http://www.w3.org/2005/Atom')->id;
// otherwise use the RSS2 guid element
if(!strlen($id)) {
if (!strlen($id)) {
$id = (string) $f->xml->guid;
}
// otherwise use the Dublin Core identifier element
if(!strlen($id)) {
if (!strlen($id)) {
$id = (string) $f->xml->children('http://purl.org/dc/elements/1.1/')->identifier;
}
// otherwise there is no ID; if there is one, hash it
if(strlen($id)) {
if (strlen($id)) {
$f->id = hash('sha256', $id);
}
// PicoFeed also doesn't gather up categories, so we do this as well
$f->categories = [];
// first add Atom categories
foreach($f->xml->children('http://www.w3.org/2005/Atom')->category as $c) {
foreach ($f->xml->children('http://www.w3.org/2005/Atom')->category as $c) {
// if the category has a label, use that
$name = (string) $c->attributes()->label;
// otherwise use the term
if(!strlen($name)) {
if (!strlen($name)) {
$name = (string) $c->attributes()->term;
}
// ... assuming it has that much
if(strlen($name)) {
if (strlen($name)) {
$f->categories[] = $name;
}
}
// next add RSS2 categories
foreach($f->xml->children()->category as $c) {
foreach ($f->xml->children()->category as $c) {
$name = (string) $c;
if(strlen($name)) {
if (strlen($name)) {
$f->categories[] = $name;
}
}
// and finally try Dublin Core subjects
foreach($f->xml->children('http://purl.org/dc/elements/1.1/')->subject as $c) {
foreach ($f->xml->children('http://purl.org/dc/elements/1.1/')->subject as $c) {
$name = (string) $c;
if(strlen($name)) {
if (strlen($name)) {
$f->categories[] = $name;
}
}
@ -178,26 +179,26 @@ class Feed {
protected function deduplicateItems(array $items): array {
/* Rationale:
Some newsfeeds (notably Planet) include multiple versions of an
Some newsfeeds (notably Planet) include multiple versions of an
item if it is updated. As we only care about the latest, we
try to remove any "old" versions of an item that might also be
try to remove any "old" versions of an item that might also be
present within the feed.
*/
$out = [];
foreach($items as $item) {
foreach($out as $index => $check) {
foreach ($items as $item) {
foreach ($out as $index => $check) {
// if the two items both have IDs and they differ, they do not match, regardless of hashes
if($item->id && $check->id && $item->id != $check->id) {
if ($item->id && $check->id && $item->id != $check->id) {
continue;
}
// if the two items have the same ID or any one hash matches, they are two versions of the same item
if(
if (
($item->id && $check->id && $item->id == $check->id) ||
($item->urlTitleHash && $item->urlTitleHash == $check->urlTitleHash) ||
($item->urlContentHash && $item->urlContentHash == $check->urlContentHash) ||
($item->titleContentHash && $item->titleContentHash == $check->titleContentHash)
) {
if(// because newsfeeds are usually order newest-first, the later item should only be used if...
if (// because newsfeeds are usually order newest-first, the later item should only be used if...
// the later item has an update date and the existing item does not
($item->updatedDate && !$check->updatedDate) ||
// the later item has an update date newer than the existing item's
@ -224,7 +225,7 @@ class Feed {
// 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)) {
if (is_null($feedID)) {
$this->newItems = $items;
return true;
}
@ -232,20 +233,20 @@ class Feed {
$articles = Arsse::$db->feedMatchLatest($feedID, sizeof($items))->getAll();
// perform a first pass matching the latest articles against items in the feed
list($this->newItems, $this->changedItems) = $this->matchItems($items, $articles);
if(sizeof($this->newItems) && sizeof($items) <= sizeof($articles)) {
if (sizeof($this->newItems) && sizeof($items) <= sizeof($articles)) {
// 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) {
if($i->id) {
foreach ($this->newItems as $i) {
if ($i->id) {
$ids[] = $i->id;
}
if($i->urlTitleHash) {
if ($i->urlTitleHash) {
$hashesUT[] = $i->urlTitleHash;
}
if($i->urlContentHash) {
if ($i->urlContentHash) {
$hashesUC[] = $i->urlContentHash;
}
if($i->titleContentHash) {
if ($i->titleContentHash) {
$hashesTC[] = $i->titleContentHash;
}
}
@ -260,14 +261,14 @@ class Feed {
protected function matchItems(array $items, array $articles): array {
$new = $edited = [];
// iterate through the articles and for each determine whether it is existing, edited, or entirely new
foreach($items as $i) {
foreach ($items as $i) {
$found = false;
foreach($articles as $a) {
foreach ($articles as $a) {
// if the item has an ID and it doesn't match the article ID, the two don't match, regardless of hashes
if($i->id && $i->id !== $a['guid']) {
if ($i->id && $i->id !== $a['guid']) {
continue;
}
if(
if (
// the item matches if the GUID matches...
($i->id && $i->id === $a['guid']) ||
// ... or if any one of the hashes match
@ -275,13 +276,13 @@ class Feed {
($i->urlContentHash && $i->urlContentHash === $a['url_content_hash']) ||
($i->titleContentHash && $i->titleContentHash === $a['title_content_hash'])
) {
if($i->updatedDate && Date::transform($i->updatedDate, "sql") !== $a['edited']) {
if ($i->updatedDate && Date::transform($i->updatedDate, "sql") !== $a['edited']) {
// if the item has an edit timestamp and it doesn't match that of the article in the database, the the article has been edited
// we store the item index and database record ID as a key/value pair
$found = true;
$edited[$a['id']] = $i;
break;
} else if($i->urlTitleHash !== $a['url_title_hash'] || $i->urlContentHash !== $a['url_content_hash'] || $i->titleContentHash !== $a['title_content_hash']) {
} elseif ($i->urlTitleHash !== $a['url_title_hash'] || $i->urlContentHash !== $a['url_content_hash'] || $i->titleContentHash !== $a['title_content_hash']) {
// if any of the hashes do not match, then the article has been edited
$found = true;
$edited[$a['id']] = $i;
@ -293,7 +294,7 @@ class Feed {
}
}
}
if(!$found) {
if (!$found) {
$new[] = $i;
}
}
@ -302,7 +303,7 @@ class Feed {
protected function computeNextFetch(): \DateTime {
$now = Date::normalize(time());
if(!$this->modified) {
if (!$this->modified) {
$diff = $now->getTimestamp() - $this->lastModified->getTimestamp();
$offset = $this->normalizeDateDiff($diff);
$now->modify("+".$offset);
@ -313,14 +314,14 @@ class Feed {
// interval is "less than 30m"). If there is no commonality, the feed is checked in 1 hour.
$offsets = [];
$dates = $this->gatherDates();
if(sizeof($dates) > 3) {
for($a = 0; $a < 3; $a++) {
if (sizeof($dates) > 3) {
for ($a = 0; $a < 3; $a++) {
$diff = $dates[$a] - $dates[$a+1];
$offsets[] = $this->normalizeDateDiff($diff);
}
if($offsets[0]==$offsets[1] || $offsets[0]==$offsets[2]) {
if ($offsets[0]==$offsets[1] || $offsets[0]==$offsets[2]) {
$now->modify("+".$offsets[0]);
} else if($offsets[1]==$offsets[2]) {
} elseif ($offsets[1]==$offsets[2]) {
$now->modify("+".$offsets[1]);
} else {
$now->modify("+ 1 hour");
@ -333,9 +334,9 @@ class Feed {
}
public static function nextFetchOnError($errCount): \DateTime {
if($errCount < 3) {
if ($errCount < 3) {
$offset = "5 minutes";
} else if($errCount < 15) {
} elseif ($errCount < 15) {
$offset = "3 hours";
} else {
$offset = "1 day";
@ -344,13 +345,13 @@ class Feed {
}
protected function normalizeDateDiff(int $diff): string {
if($diff < (30 * 60)) { // less than 30 minutes
if ($diff < (30 * 60)) { // less than 30 minutes
$offset = "15 minutes";
} else if($diff < (60 * 60)) { // less than an hour
} elseif ($diff < (60 * 60)) { // less than an hour
$offset = "30 minutes";
} else if($diff < (3 * 60 * 60)) { // less than three hours
} elseif ($diff < (3 * 60 * 60)) { // less than three hours
$offset = "1 hour";
} else if($diff >= (36 * 60 * 60)) { // more than 36 hours
} elseif ($diff >= (36 * 60 * 60)) { // more than 36 hours
$offset = "1 day";
} else {
$offset = "3 hours";
@ -359,11 +360,11 @@ class Feed {
}
protected function computeLastModified() {
if(!$this->modified) {
if (!$this->modified) {
return $this->lastModified;
}
$dates = $this->gatherDates();
if(sizeof($dates)) {
if (sizeof($dates)) {
return Date::normalize($dates[0]);
} else {
return null;
@ -372,11 +373,11 @@ class Feed {
protected function gatherDates(): array {
$dates = [];
foreach($this->data->items as $item) {
if($item->updatedDate) {
foreach ($this->data->items as $item) {
if ($item->updatedDate) {
$dates[] = $item->updatedDate->getTimestamp();
}
if($item->publishedDate) {
if ($item->publishedDate) {
$dates[] = $item->publishedDate->getTimestamp();
}
}
@ -387,13 +388,13 @@ class Feed {
protected function scrape(): bool {
$scraper = new Scraper($this->config);
foreach(array_merge($this->newItems, $this->changedItems) as $item) {
foreach (array_merge($this->newItems, $this->changedItems) as $item) {
$scraper->setUrl($item->url);
$scraper->execute();
if($scraper->hasRelevantContent()) {
if ($scraper->hasRelevantContent()) {
$item->content = $scraper->getFilteredContent();
}
}
return true;
}
}
}

2
lib/Feed/Exception.php

@ -11,4 +11,4 @@ class Exception extends \JKingWeb\Arsse\AbstractException {
$msgID = ($msgID !== $className) ? lcfirst($msgID) : '';
parent::__construct($msgID, ['url' => $url], $e);
}
}
}

84
lib/Lang.php

@ -16,34 +16,34 @@ class Lang {
];
public $path; // path to locale files; this is a public property to facilitate unit testing
static protected $requirementsMet = false; // whether the Intl extension is loaded
protected static $requirementsMet = false; // whether the Intl extension is loaded
protected $synched = false; // whether the wanted locale is actually loaded (lazy loading is used by default)
protected $wanted = self::DEFAULT; // the currently requested locale
protected $locale = ""; // the currently loaded locale
protected $loaded = []; // the cascade of loaded locale file names
protected $strings = self::REQUIRED; // the loaded locale strings, merged
function __construct(string $path = BASE."locale".DIRECTORY_SEPARATOR) {
public function __construct(string $path = BASE."locale".DIRECTORY_SEPARATOR) {
$this->path = $path;
}
public function set(string $locale, bool $immediate = false): string {
// make sure the Intl extension is loaded
if(!static::$requirementsMet) {
if (!static::$requirementsMet) {
static::checkRequirements();
}
// if requesting the same locale as already wanted, just return (but load first if we've requested an immediate load)
if($locale==$this->wanted) {
if($immediate && !$this->synched) {
if ($locale==$this->wanted) {
if ($immediate && !$this->synched) {
$this->load();
}
return $locale;
}
// if we've requested a locale other than the null locale, fetch the list of available files and find the closest match e.g. en_ca_somedialect -> en_ca
if($locale != "") {
if ($locale != "") {
$list = $this->listFiles();
// if the default locale is unavailable, this is (for now) an error
if(!in_array(self::DEFAULT, $list)) {
if (!in_array(self::DEFAULT, $list)) {
throw new Lang\Exception("defaultFileMissing", self::DEFAULT);
}
$this->wanted = $this->match($locale, $list);
@ -52,7 +52,7 @@ class Lang {
}
$this->synched = false;
// load right now if asked to, otherwise load later when actually required
if($immediate) {
if ($immediate) {
$this->load();
}
return $this->wanted;
@ -73,29 +73,33 @@ class Lang {
public function __invoke(string $msgID, $vars = null): string {
// if we're trying to load the system default language and it fails, we have a chicken and egg problem, so we catch the exception and load no language file instead
if(!$this->synched) try {$this->load();} catch(Lang\Exception $e) {
if($this->wanted==self::DEFAULT) {
$this->set("", true);
} else {
throw $e;
if (!$this->synched) {
try {
$this->load();
} catch (Lang\Exception $e) {
if ($this->wanted==self::DEFAULT) {
$this->set("", true);
} else {
throw $e;
}
}
}
// if the requested message is not present in any of the currently loaded language files, throw an exception
// note that this is indicative of a programming error since the default locale should have all strings
if(!array_key_exists($msgID, $this->strings)) {
throw new Lang\Exception("stringMissing", ['msgID' => $msgID, 'fileList' => implode(", ",$this->loaded)]);
if (!array_key_exists($msgID, $this->strings)) {
throw new Lang\Exception("stringMissing", ['msgID' => $msgID, 'fileList' => implode(", ", $this->loaded)]);
}
$msg = $this->strings[$msgID];
// variables fed to MessageFormatter must be contained in an array
if($vars===null) {
if ($vars===null) {
// even though strings not given parameters will not get formatted, we do not optimize this case away: we still want to catch invalid strings
$vars = [];
} else if(!is_array($vars)) {
} elseif (!is_array($vars)) {
$vars = [$vars];
}
$msg = \MessageFormatter::formatMessage($this->locale, $msg, $vars);
if($msg===false) {
throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ",$this->loaded)]);
if ($msg===false) {
throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ", $this->loaded)]);
}
return $msg;
}
@ -103,22 +107,22 @@ class Lang {
public function list(string $locale = ""): array {
$out = [];
$files = $this->listFiles();
foreach($files as $tag) {
foreach ($files as $tag) {
$out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale);
}
return $out;
}
public function match(string $locale, array $list = null): string {
if($list===null) {
if ($list===null) {
$list = $this->listFiles();
}
$default = ($this->locale=="") ? self::DEFAULT : $this->locale;
return \Locale::lookup($list,$locale, true, $default);
return \Locale::lookup($list, $locale, true, $default);
}
static protected function checkRequirements(): bool {
if(!extension_loaded("intl")) {
protected static function checkRequirements(): bool {
if (!extension_loaded("intl")) {
throw new ExceptionFatal("The \"Intl\" extension is required, but not loaded");
}
static::$requirementsMet = true;
@ -133,22 +137,22 @@ class Lang {
protected function listFiles(): array {
$out = $this->globFiles($this->path."*.php");
// trim the returned file paths to return just the language tag
$out = array_map(function($file) {
$out = array_map(function ($file) {
$file = str_replace(DIRECTORY_SEPARATOR, "/", $file); // we replace the directory separator because we don't use native paths in testing
$file = substr($file, strrpos($file, "/")+1);
return strtolower(substr($file,0,strrpos($file,".")));
},$out);
return strtolower(substr($file, 0, strrpos($file, ".")));
}, $out);
// sort the results
natsort($out);
return $out;
}
protected function load(): bool {
if(!self::$requirementsMet) {
if (!self::$requirementsMet) {
self::checkRequirements();
}
// if we've requested no locale (""), just load the fallback strings and return
if($this->wanted=="") {
if ($this->wanted=="") {
$this->strings = self::REQUIRED;
$this->locale = $this->wanted;
$this->synched = true;
@ -157,27 +161,27 @@ class Lang {
// decompose the requested locale from specific to general, building a list of files to load
$tags = \Locale::parseLocale($this->wanted);
$files = [];
while(sizeof($tags) > 0) {
while (sizeof($tags) > 0) {
$files[] = strtolower(\Locale::composeLocale($tags));
$tag = array_pop($tags);
}
// include the default locale as the base if the most general locale requested is not the default
if($tag != self::DEFAULT) {
if ($tag != self::DEFAULT) {
$files[] = self::DEFAULT;
}
// save the list of files to be loaded for later reference
$loaded = $files;
// reduce the list of files to be loaded to the minimum necessary (e.g. if we go from "fr" to "fr_ca", we don't need to load "fr" or "en")
$files = [];
foreach($loaded as $file) {
if($file==$this->locale) {
foreach ($loaded as $file) {
if ($file==$this->locale) {
break;
}
$files[] = $file;
}
// if we need to load all files, start with the fallback strings
$strings = [];
if($files==$loaded) {
if ($files==$loaded) {
$strings[] = self::REQUIRED;
} else {
// otherwise start with the strings we already have if we're going from e.g. "fr" to "fr_ca"
@ -185,22 +189,22 @@ class Lang {
}
// read files in reverse order
$files = array_reverse($files);
foreach($files as $file) {
if(!file_exists($this->path."$file.php")) {
foreach ($files as $file) {
if (!file_exists($this->path."$file.php")) {
throw new Lang\Exception("fileMissing", $file);
} else if(!is_readable($this->path."$file.php")) {
} elseif (!is_readable($this->path."$file.php")) {
throw new Lang\Exception("fileUnreadable", $file);
}
try {
// we use output buffering in case the language file is corrupted
ob_start();
$arr = (include $this->path."$file.php");
} catch(\Throwable $e) {
} catch (\Throwable $e) {
$arr = null;
} finally {
ob_end_clean();
}
if(!is_array($arr)) {
if (!is_array($arr)) {
throw new Lang\Exception("fileCorrupt", $file);
}
$strings[] = $arr;
@ -212,4 +216,4 @@ class Lang {
$this->synched = true;
return true;
}
}
}

2
lib/Lang/Exception.php

@ -3,4 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Lang;
class Exception extends \JKingWeb\Arsse\AbstractException {
}
}

51
lib/Misc/Context.php

@ -1,9 +1,10 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Misc;
use JKingWeb\Arsse\Misc\Date;
class Context {
class Context {
public $reverse = false;
public $limit = 0;
public $offset = 0;
@ -23,7 +24,7 @@ class Context {
protected $props = [];
protected function act(string $prop, int $set, $value) {
if($set) {
if ($set) {
$this->props[$prop] = true;
$this->$prop = $value;
return $this;
@ -34,17 +35,17 @@ class Context {
protected function cleanArray(array $spec): array {
$spec = array_values($spec);
for($a = 0; $a < sizeof($spec); $a++) {
for ($a = 0; $a < sizeof($spec); $a++) {
$id = $spec[$a];
if(is_int($id) && $id > -1) {
if (is_int($id) && $id > -1) {
continue;
} else if(is_float($id) && !fmod($id, 1) && $id >= 0) {
} elseif (is_float($id) && !fmod($id, 1) && $id >= 0) {
$spec[$a] = (int) $id;
continue;
} else if(is_string($id)) {
} elseif (is_string($id)) {
$ch1 = strval(@intval($id));
$ch2 = strval($id);
if($ch1 !== $ch2 || $id < 1) {
if ($ch1 !== $ch2 || $id < 1) {
$id = 0;
}
} else {
@ -55,71 +56,71 @@ class Context {
return array_values(array_filter($spec));
}
function reverse(bool $spec = null) {
public function reverse(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function limit(int $spec = null) {
public function limit(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function offset(int $spec = null) {
public function offset(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function folder(int $spec = null) {
public function folder(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function subscription(int $spec = null) {
public function subscription(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function latestEdition(int $spec = null) {
public function latestEdition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function oldestEdition(int $spec = null) {
public function oldestEdition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function unread(bool $spec = null) {
public function unread(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function starred(bool $spec = null) {
public function starred(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function modifiedSince($spec = null) {
public function modifiedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function notModifiedSince($spec = null) {
public function notModifiedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function edition(int $spec = null) {
public function edition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function article(int $spec = null) {
public function article(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function editions(array $spec = null) {
if($spec) {
public function editions(array $spec = null) {
if ($spec) {
$spec = $this->cleanArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
function articles(array $spec = null) {
if($spec) {
public function articles(array $spec = null) {
if ($spec) {
$spec = $this->cleanArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
}
}

35
lib/Misc/Date.php

@ -3,14 +3,13 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Misc;
class Date {
static function transform($date, string $outFormat = null, string $inFormat = null, bool $inLocal = false) {
public static function transform($date, string $outFormat = null, string $inFormat = null, bool $inLocal = false) {
$date = self::normalize($date, $inFormat, $inLocal);
if(is_null($date) || is_null($outFormat)) {
if (is_null($date) || is_null($outFormat)) {
return $date;
}
$outFormat = strtolower($outFormat);
if($outFormat=="unix") {
if ($outFormat=="unix") {
return $date->getTimestamp();
}
switch ($outFormat) {
@ -24,18 +23,18 @@ class Date {
return $date->format($f);
}
static function normalize($date, string $inFormat = null, bool $inLocal = false) {
if($date instanceof \DateTimeInterface) {
public static function normalize($date, string $inFormat = null, bool $inLocal = false) {
if ($date instanceof \DateTimeInterface) {
return $date;
} else if(is_numeric($date)) {
} elseif (is_numeric($date)) {
$time = (int) $date;
} else if($date===null) {
} elseif ($date===null) {
return null;
} else if(is_string($date)) {
} elseif (is_string($date)) {
try {
$tz = (!$inLocal) ? new \DateTimeZone("UTC") : null;
if(!is_null($inFormat)) {
switch($inFormat) {
if (!is_null($inFormat)) {
switch ($inFormat) {
case 'http': $f = "D, d M Y H:i:s \G\M\T"; break;
case 'iso8601': $f = "Y-m-d\TH:i:sP"; break;
case 'sql': $f = "Y-m-d H:i:s"; break;
@ -47,10 +46,10 @@ class Date {
} else {
return new \DateTime($date, $tz);
}
} catch(\Throwable $e) {
} catch (\Throwable $e) {
return null;
}
} else if (is_bool($date)) {
} elseif (is_bool($date)) {
return null;
} else {
$time = (int) $date;
@ -61,21 +60,21 @@ class Date {
return $d;
}
static function add(string $interval, $date = null): \DateTimeInterface {
public static function add(string $interval, $date = null): \DateTimeInterface {
return self::modify("add", $interval, $date);
}
static function sub(string $interval, $date = null): \DateTimeInterface {
public static function sub(string $interval, $date = null): \DateTimeInterface {
return self::modify("sub", $interval, $date);
}
static protected function modify(string $func, string $interval, $date = null): \DateTimeInterface {
protected static function modify(string $func, string $interval, $date = null): \DateTimeInterface {
$date = self::normalize($date ?? time());
if($date instanceof \DateTimeImmutable) {
if ($date instanceof \DateTimeImmutable) {
return $date->$func(new \DateInterval($interval));
} else {
$date->$func(new \DateInterval($interval));
return $date;
}
}
}
}

58
lib/Misc/Query.php

@ -18,42 +18,42 @@ class Query {
protected $offset = 0;
function __construct(string $body = "", $types = null, $values = null) {
public function __construct(string $body = "", $types = null, $values = null) {
$this->setBody($body, $types, $values);
}
function setBody(string $body = "", $types = null, $values = null): bool {
public function setBody(string $body = "", $types = null, $values = null): bool {
$this->qBody = $body;
if(!is_null($types)) {
if (!is_null($types)) {
$this->tBody[] = $types;
$this->vBody[] = $values;
}
return true;
}
function setCTE(string $tableSpec, string $body, $types = null, $values = null, string $join = ''): bool {
public function setCTE(string $tableSpec, string $body, $types = null, $values = null, string $join = ''): bool {
$this->qCTE[] = "$tableSpec as ($body)";
if(!is_null($types)) {
if (!is_null($types)) {
$this->tCTE[] = $types;
$this->vCTE[] = $values;
}
if(strlen($join)) { // the CTE might only participate in subqueries rather than a join on the main query
if (strlen($join)) { // the CTE might only participate in subqueries rather than a join on the main query
$this->jCTE[] = $join;
}
return true;
}
function setWhere(string $where, $types = null, $values = null): bool {
public function setWhere(string $where, $types = null, $values = null): bool {
$this->qWhere[] = $where;
if(!is_null($types)) {
if (!is_null($types)) {
$this->tWhere[] = $types;
$this->vWhere[] = $values;
}
return true;
}
function setOrder(string $order, bool $prepend = false): bool {
if($prepend) {
public function setOrder(string $order, bool $prepend = false): bool {
if ($prepend) {
array_unshift($this->order, $order);
} else {
$this->order[] = $order;
@ -61,13 +61,13 @@ class Query {
return true;
}
function setLimit(int $limit, int $offset = 0): bool {
public function setLimit(int $limit, int $offset = 0): bool {
$this->limit = $limit;
$this->offset = $offset;
return true;
}
function pushCTE(string $tableSpec, string $join = ''): bool {
public function pushCTE(string $tableSpec, string $join = ''): bool {
// this function takes the query body and converts it to a common table expression, putting it at the bottom of the existing CTE stack
// all WHERE, ORDER BY, and LIMIT parts belong to the new CTE and are removed from the main query
$this->setCTE($tableSpec, $this->buildQueryBody(), [$this->tBody, $this->tWhere], [$this->vBody, $this->vWhere]);
@ -78,16 +78,16 @@ class Query {
$this->tWhere = [];
$this->vWhere = [];
$this->order = [];
$this->setLimit(0,0);
if(strlen($join)) {
$this->setLimit(0, 0);
if (strlen($join)) {
$this->jCTE[] = $join;
}
return true;
}
function __toString(): string {
public function __toString(): string {
$out = "";
if(sizeof($this->qCTE)) {
if (sizeof($this->qCTE)) {
// start with common table expressions
$out .= "WITH RECURSIVE ".implode(", ", $this->qCTE)." ";
}
@ -96,31 +96,31 @@ class Query {
return $out;
}
function getQuery(): string {
public function getQuery(): string {
return $this->__toString();
}
function getTypes(): array {
public function getTypes(): array {
return [$this->tCTE, $this->tBody, $this->tWhere];
}
function getValues(): array {
public function getValues(): array {
return [$this->vCTE, $this->vBody, $this->vWhere];
}
function getWhereTypes(): array {
public function getWhereTypes(): array {
return $this->tWhere;
}
function getWhereValues(): array {
public function getWhereValues(): array {
return $this->vWhere;
}
function getCTETypes(): array {
public function getCTETypes(): array {
return $this->tCTE;
}
function getCTEValues(): array {
public function getCTEValues(): array {
return $this->vCTE;
}
@ -128,25 +128,25 @@ class Query {
$out = "";
// add the body
$out .= $this->qBody;
if(sizeof($this->qCTE)) {
if (sizeof($this->qCTE)) {
// add any joins against CTEs
$out .= " ".implode(" ", $this->jCTE);
}
// add any WHERE terms
if(sizeof($this->qWhere)) {
if (sizeof($this->qWhere)) {
$out .= " WHERE ".implode(" AND ", $this->qWhere);
}
// add any ORDER BY terms
if(sizeof($this->order)) {
if (sizeof($this->order)) {
$out .= " ORDER BY ".implode(", ", $this->order);
}
// add LIMIT and OFFSET if the former is specified
if($this->limit > 0) {
if ($this->limit > 0) {
$out .= " LIMIT ".$this->limit;
if($this->offset > 0) {
if ($this->offset > 0) {
$out .= " OFFSET ".$this->offset;
}
}
return $out;
}
}
}

20
lib/REST.php

@ -27,31 +27,33 @@ class REST {
// CommaFeed https://www.commafeed.com/api/
];
function __construct() {
public function __construct() {
}
function dispatch(REST\Request $req = null): REST\Response {
if($req===null) {
public function dispatch(REST\Request $req = null): REST\Response {
if ($req===null) {
$req = new REST\Request();
}
$api = $this->apiMatch($req->url, $this->apis);
$req->url = substr($req->url,strlen($this->apis[$api]['strip']));
$req->url = substr($req->url, strlen($this->apis[$api]['strip']));
$req->refreshURL();
$class = $this->apis[$api]['class'];
$drv = new $class();
return $drv->dispatch($req);
}
function apiMatch(string $url, array $map): string {
public function apiMatch(string $url, array $map): string {
// sort the API list so the longest URL prefixes come first
uasort($map, function($a, $b) {return (strlen($a['match']) <=> strlen($b['match'])) * -1;});
uasort($map, function ($a, $b) {
return (strlen($a['match']) <=> strlen($b['match'])) * -1;
});
// find a match
foreach($map as $id => $api) {
if(strpos($url, $api['match'])===0) {
foreach ($map as $id => $api) {
if (strpos($url, $api['match'])===0) {
return $id;
}
}
// or throw an exception otherwise
throw new REST\Exception501();
}
}
}

46
lib/REST/AbstractHandler.php

@ -1,27 +1,27 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\REST;
use JKingWeb\Arsse\Misc\Date;
abstract class AbstractHandler implements Handler {
abstract function __construct();
abstract function dispatch(Request $req): Response;
abstract public function __construct();
abstract public function dispatch(Request $req): Response;
protected function fieldMapNames(array $data, array $map): array {
$out = [];
foreach($map as $to => $from) {
if(array_key_exists($from, $data)) {
foreach ($map as $to => $from) {
if (array_key_exists($from, $data)) {
$out[$to] = $data[$from];
}
}
return $out;
}
}
protected function fieldMapTypes(array $data, array $map, string $dateFormat = "sql"): array {
foreach($map as $key => $type) {
if(array_key_exists($key, $data)) {
if($type=="datetime" && $dateFormat != "sql") {
foreach ($map as $key => $type) {
if (array_key_exists($key, $data)) {
if ($type=="datetime" && $dateFormat != "sql") {
$data[$key] = Date::transform($data[$key], $dateFormat, "sql");
} else {
settype($data[$key], $type);
@ -39,18 +39,18 @@ abstract class AbstractHandler implements Handler {
protected function NormalizeInput(array $data, array $types, string $dateFormat = null): array {
$out = [];
foreach($data as $key => $value) {
if(!isset($types[$key])) {
foreach ($data as $key => $value) {
if (!isset($types[$key])) {
$out[$key] = $value;
continue;
}
if(is_null($value)) {
if (is_null($value)) {
$out[$key] = null;
continue;
}
switch($types[$key]) {
switch ($types[$key]) {
case "int":
if($this->validateInt($value)) {
if ($this->validateInt($value)) {
$out[$key] = (int) $value;
}
break;
@ -58,31 +58,31 @@ abstract class AbstractHandler implements Handler {
$out[$key] = (string) $value;
break;
case "bool":
if(is_bool($value)) {
if (is_bool($value)) {
$out[$key] = $value;
} else if($this->validateInt($value)) {
} elseif ($this->validateInt($value)) {
$value = (int) $value;
if($value > -1 && $value < 2) {
if ($value > -1 && $value < 2) {
$out[$key] = $value;
}
} else if(is_string($value)) {
} elseif (is_string($value)) {
$value = trim(strtolower($value));
if($value=="false") {
if ($value=="false") {
$out[$key] = false;
}
if($value=="true") {
if ($value=="true") {
$out[$key] = true;
}
}
break;
case "float":
if(is_numeric($value)) {
if (is_numeric($value)) {
$out[$key] = (float) $value;
}
break;
case "datetime":
$t = Date::normalize($value, $dateFormat);
if($t) {
if ($t) {
$out[$key] = $t;
}
break;
@ -92,4 +92,4 @@ abstract class AbstractHandler implements Handler {
}
return $out;
}
}
}

2
lib/REST/Exception.php

@ -3,4 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\REST;
class Exception extends \JKingWeb\Arsse\AbstractException {
}
}

2
lib/REST/Exception405.php

@ -3,4 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\REST;
class Exception405 extends \Exception {
}
}

2
lib/REST/Exception501.php

@ -3,4 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\REST;
class Exception501 extends \Exception {
}
}

6
lib/REST/Handler.php

@ -3,6 +3,6 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\REST;
interface Handler {
function __construct();
function dispatch(Request $req): Response;
}
public function __construct();
public function dispatch(Request $req): Response;
}

148
lib/REST/NextCloudNews/V1_2.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\REST\NextCloudNews;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\User;
use JKingWeb\Arsse\Service;
@ -36,22 +37,22 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// 'items' => "array int", // just pass these through
];
function __construct() {
public function __construct() {
}
function dispatch(\JKingWeb\Arsse\REST\Request $req): Response {
public function dispatch(\JKingWeb\Arsse\REST\Request $req): Response {
// try to authenticate
if(!Arsse::$user->authHTTP()) {
if (!Arsse::$user->authHTTP()) {
return new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.self::REALM.'"']);
}
// normalize the input
if($req->body) {
if ($req->body) {
// if the entity body is not JSON according to content type, return "415 Unsupported Media Type"
if(!preg_match("<^application/json\b|^$>", $req->type)) {
if (!preg_match("<^application/json\b|^$>", $req->type)) {
return new Response(415, "", "", ['Accept: application/json']);
}
$data = @json_decode($req->body, true);
if(json_last_error() != \JSON_ERROR_NONE) {
if (json_last_error() != \JSON_ERROR_NONE) {
// if the body could not be parsed as JSON, return "400 Bad Request"
return new Response(400);
}
@ -66,21 +67,21 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// check to make sure the requested function is implemented
try {
$func = $this->chooseCall($req->paths, $req->method);
} catch(Exception501 $e) {
} catch (Exception501 $e) {
return new Response(501);
} catch(Exception405 $e) {
} catch (Exception405 $e) {
return new Response(405, "", "", ["Allow: ".$e->getMessage()]);
}
if(!method_exists($this, $func)) {
if (!method_exists($this, $func)) {
return new Response(501);
}
// dispatch
try {
return $this->$func($req->paths, $data);
} catch(Exception $e) {
} catch (Exception $e) {
// if there was a REST exception return 400
return new Response(400);
} catch(AbstractException $e) {
} catch (AbstractException $e) {
// if there was any other Arsse exception return 500
return new Response(500);
}
@ -133,27 +134,27 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// the first path element is the overall scope of the request
$scope = $url[0];
// any URL components which are only digits should be replaced with "0", for easier comparison (integer segments are IDs, and we don't care about the specific ID)
for($a = 0; $a < sizeof($url); $a++) {
if($this->validateInt($url[$a])) {
for ($a = 0; $a < sizeof($url); $a++) {
if ($this->validateInt($url[$a])) {
$url[$a] = "0";
}
}
// normalize the HTTP method to uppercase
$method = strtoupper($method);
// if the scope is not supported, return 501
if(!array_key_exists($scope, $choices)) {
if (!array_key_exists($scope, $choices)) {
throw new Exception501();
}
// 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
foreach($choices[$scope] as $path => $funcs) {
foreach ($choices[$scope] as $path => $funcs) {
// add the scope to the path to match against and split it
$path = (string) $path;
$path = (strlen($path)) ? "$scope/$path" : $scope;
$path = explode("/", $path);
if($path===$url) {
if ($path===$url) {
// if the path matches, make sure the method is allowed
if(array_key_exists($method,$funcs)) {
if (array_key_exists($method, $funcs)) {
// if it is allowed, return the object method to run
return $funcs[$method];
} else {
@ -230,8 +231,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected function folderAdd(array $url, array $data): Response {
try {
$folder = Arsse::$db->folderAdd(Arsse::$user->id, $data);
} catch(ExceptionInput $e) {
switch($e->getCode()) {
} catch (ExceptionInput $e) {
switch ($e->getCode()) {
// folder already exists
case 10236: return new Response(409);
// folder name not acceptable
@ -250,7 +251,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// perform the deletion
try {
Arsse::$db->folderRemove(Arsse::$user->id, (int) $url[1]);
} catch(ExceptionInput $e) {
} catch (ExceptionInput $e) {
// folder does not exist
return new Response(404);
}
@ -260,14 +261,14 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// rename a folder (also supports moving nesting folders, but this is not a feature of the API)
protected function folderRename(array $url, array $data): Response {
// there must be some change to be made
if(!sizeof($data)) {
if (!sizeof($data)) {
return new Response(422);
}
// perform the edit
try {
Arsse::$db->folderPropertiesSet(Arsse::$user->id, (int) $url[1], $data);
} catch(ExceptionInput $e) {
switch($e->getCode()) {
} catch (ExceptionInput $e) {
switch ($e->getCode()) {
// folder does not exist
case 10239: return new Response(404);
// folder already exists
@ -285,7 +286,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// mark all articles associated with a folder as read
protected function folderMarkRead(array $url, array $data): Response {
$c = new Context;
if(isset($data['newestItemId'])) {
if (isset($data['newestItemId'])) {
// if the item ID is valid (i.e. an integer), add it to the context
$c->latestEdition($data['newestItemId']);
} else {
@ -297,7 +298,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// perform the operation
try {
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
} catch(ExceptionInput $e) {
} catch (ExceptionInput $e) {
// folder does not exist
return new Response(404);
}
@ -307,13 +308,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// return list of feeds which should be refreshed
protected function feedListStale(array $url, array $data): Response {
// function requires admin rights per spec
if(Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
return new Response(403);
}
// list stale feeds which should be checked for updates
$feeds = Arsse::$db->feedListStale();
$out = [];
foreach($feeds as $feed) {
foreach ($feeds as $feed) {
// since in our implementation feeds don't belong the users, the 'userId' field will always be an empty string
$out[] = ['id' => $feed, 'userId' => ""];
}
@ -323,16 +324,16 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// refresh a feed
protected function feedUpdate(array $url, array $data): Response {
// function requires admin rights per spec
if(Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
return new Response(403);
}
// perform an update of a single feed
if(!isset($data['feedId'])) {
if (!isset($data['feedId'])) {
return new Response(422);
}
try {
Arsse::$db->feedUpdate($data['feedId']);
} catch(ExceptionInput $e) {
} catch (ExceptionInput $e) {
return new Response(404);
}
return new Response(204);
@ -341,7 +342,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// add a new feed
protected function subscriptionAdd(array $url, array $data): Response {
// normalize the feed URL
if(!isset($data['url'])) {
if (!isset($data['url'])) {
return new Response(422);
}
// normalize the folder ID, if specified; zero should be transformed to null
@ -350,18 +351,19 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
$tr = Arsse::$db->begin();
try {
$id = Arsse::$db->subscriptionAdd(Arsse::$user->id, $data['url']);
} catch(ExceptionInput $e) {
} catch (ExceptionInput $e) {
// feed already exists
return new Response(409);
} catch(FeedException $e) {
} catch (FeedException $e) {
// feed could not be retrieved
return new Response(422);
}
// if a folder was specified, move the feed to the correct folder; silently ignore errors
if($folder) {
if ($folder) {
try {
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, ['folder' => $folder]);
} catch(ExceptionInput $e) {}
} catch (ExceptionInput $e) {
}
}
$tr->commit();
// fetch the feed's metadata and format it appropriately
@ -369,7 +371,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
$feed = $this->feedTranslate($feed);
$out = ['feeds' => [$feed]];
$newest = Arsse::$db->editionLatest(Arsse::$user->id, (new Context)->subscription($id));
if($newest) {
if ($newest) {
$out['newestItemId'] = $newest;
}
return new Response(200, $out);
@ -379,13 +381,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected function subscriptionList(array $url, array $data): Response {
$subs = Arsse::$db->subscriptionList(Arsse::$user->id);
$out = [];
foreach($subs as $sub) {
foreach ($subs as $sub) {
$out[] = $this->feedTranslate($sub);
}
$out = ['feeds' => $out];
$out['starredCount'] = Arsse::$db->articleStarredCount(Arsse::$user->id);
$newest = Arsse::$db->editionLatest(Arsse::$user->id);
if($newest) {
if ($newest) {
$out['newestItemId'] = $newest;
}
return new Response(200, $out);
@ -395,7 +397,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected function subscriptionRemove(array $url, array $data): Response {
try {
Arsse::$db->subscriptionRemove(Arsse::$user->id, (int) $url[1]);
} catch(ExceptionInput $e) {
} catch (ExceptionInput $e) {
// feed does not exist
return new Response(404);
}
@ -406,7 +408,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected function subscriptionRename(array $url, array $data): Response {
// normalize input
$in = [];
if(array_key_exists('feedTitle', $data)) { // we use array_key_exists because null is a valid input
if (array_key_exists('feedTitle', $data)) { // we use array_key_exists because null is a valid input
$in['title'] = $data['feedTitle'];
} else {
return new Response(422);
@ -414,8 +416,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// perform the renaming
try {
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], $in);
} catch(ExceptionInput $e) {
switch($e->getCode()) {
} catch (ExceptionInput $e) {
switch ($e->getCode()) {
// subscription does not exist
case 10239: return new Response(404);
// name is invalid
@ -432,7 +434,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected function subscriptionMove(array $url, array $data): Response {
// normalize input
$in = [];
if(isset($data['folderId'])) {
if (isset($data['folderId'])) {
$in['folder'] = $data['folderId'] ? $data['folderId'] : null;
} else {
return new Response(422);
@ -440,8 +442,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// perform the move
try {
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], $in);
} catch(ExceptionInput $e) {
switch($e->getCode()) {
} catch (ExceptionInput $e) {
switch ($e->getCode()) {
// subscription does not exist
case 10239: return new Response(404);
// folder does not exist
@ -456,7 +458,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// mark all articles associated with a subscription as read
protected function subscriptionMarkRead(array $url, array $data): Response {
$c = new Context;
if(isset($data['newestItemId'])) {
if (isset($data['newestItemId'])) {
$c->latestEdition($data['newestItemId']);
} else {
// otherwise return an error
@ -467,7 +469,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// perform the operation
try {
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
} catch(ExceptionInput $e) {
} catch (ExceptionInput $e) {
// subscription does not exist
return new Response(404);
}
@ -479,39 +481,39 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// set the context options supplied by the client
$c = new Context;
// set the batch size
if(isset($data['batchSize']) && $data['batchSize'] > 0) {
if (isset($data['batchSize']) && $data['batchSize'] > 0) {
$c->limit($data['batchSize']);
}
// set the order of returned items
if(isset($data['oldestFirst']) && $data['oldestFirst']) {
if (isset($data['oldestFirst']) && $data['oldestFirst']) {
$c->reverse(false);
} else {
$c->reverse(true);
}
// set the edition mark-off; the database uses an or-equal comparison for internal consistency, but the protocol does not, so we must adjust by one
if(isset($data['offset']) && $data['offset'] > 0) {
if($c->reverse) {
if (isset($data['offset']) && $data['offset'] > 0) {
if ($c->reverse) {
$c->latestEdition($data['offset'] - 1);
} else {
$c->oldestEdition($data['offset'] + 1);
}
}
// set whether to only return unread
if(isset($data['getRead']) && !$data['getRead']) {
if (isset($data['getRead']) && !$data['getRead']) {
$c->unread(true);
}
// if no type is specified assume 3 (All)
if(!isset($data['type'])) {
if (!isset($data['type'])) {
$data['type'] = 3;
}
switch($data['type']) {
switch ($data['type']) {
case 0: // feed
if(isset($data['id'])) {
if (isset($data['id'])) {
$c->subscription($data['id']);
}
break;
case 1: // folder
if(isset($data['id'])) {
if (isset($data['id'])) {
$c->folder($data['id']);
}
break;
@ -522,18 +524,18 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// return all items
}
// whether to return only updated items
if(isset($data['lastModified'])) {
if (isset($data['lastModified'])) {
$c->modifiedSince($data['lastModified']);
}
// perform the fetch
try {
$items = Arsse::$db->articleList(Arsse::$user->id, $c);
} catch(ExceptionInput $e) {
} catch (ExceptionInput $e) {
// ID of subscription or folder is not valid
return new Response(422);
}
$out = [];
foreach($items as $item) {
foreach ($items as $item) {
$out[] = $this->articleTranslate($item);
}
$out = ['items' => $out];
@ -543,7 +545,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// mark all articles as read
protected function articleMarkReadAll(array $url, array $data): Response {
$c = new Context;
if(isset($data['newestItemId'])) {
if (isset($data['newestItemId'])) {
// set the newest item ID as specified
$c->latestEdition($data['newestItemId']);
} else {
@ -564,7 +566,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
$set = ($url[2]=="read");
try {
Arsse::$db->articleMark(Arsse::$user->id, ['read' => $set], $c);
} catch(ExceptionInput $e) {
} catch (ExceptionInput $e) {
// ID is not valid
return new Response(404);
}
@ -580,7 +582,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
$set = ($url[3]=="star");
try {
Arsse::$db->articleMark(Arsse::$user->id, ['starred' => $set], $c);
} catch(ExceptionInput $e) {
} catch (ExceptionInput $e) {
// ID is not valid
return new Response(404);
}
@ -592,19 +594,20 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// determine whether to mark read or unread
$set = ($url[1]=="read");
// if the input data is not at all valid, return an error
if(!isset($data['items']) || !is_array($data['items'])) {
if (!isset($data['items']) || !is_array($data['items'])) {
return new Response(422);
}
// start a transaction and loop through the items
$t = Arsse::$db->begin();
$in = array_chunk($data['items'], 50);
for($a = 0; $a < sizeof($in); $a++) {
for ($a = 0; $a < sizeof($in); $a++) {
// initialize the matching context
$c = new Context;
$c->editions($in[$a]);
try {
Arsse::$db->articleMark(Arsse::$user->id, ['read' => $set], $c);
} catch(ExceptionInput $e) {}
} catch (ExceptionInput $e) {
}
}
$t->commit();
return new Response(204);
@ -615,19 +618,20 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// determine whether to mark starred or unstarred
$set = ($url[1]=="star");
// if the input data is not at all valid, return an error
if(!isset($data['items']) || !is_array($data['items'])) {
if (!isset($data['items']) || !is_array($data['items'])) {
return new Response(422);
}
// start a transaction and loop through the items
$t = Arsse::$db->begin();
$in = array_chunk(array_column($data['items'], "guidHash"), 50);
for($a = 0; $a < sizeof($in); $a++) {
for ($a = 0; $a < sizeof($in); $a++) {
// initialize the matching context
$c = new Context;
$c->articles($in[$a]);
try {
Arsse::$db->articleMark(Arsse::$user->id, ['starred' => $set], $c);
} catch(ExceptionInput $e) {}
} catch (ExceptionInput $e) {
}
}
$t->commit();
return new Response(204);
@ -636,7 +640,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected function userStatus(array $url, array $data): Response {
$data = Arsse::$user->propertiesGet(Arsse::$user->id, true);
// construct the avatar structure, if an image is available
if(isset($data['avatar'])) {
if (isset($data['avatar'])) {
$avatar = [
'data' => base64_encode($data['avatar']['data']),
'mime' => $data['avatar']['type'],
@ -656,7 +660,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected function cleanupBefore(array $url, array $data): Response {
// function requires admin rights per spec
if(Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
return new Response(403);
}
Service::cleanupPre();
@ -665,7 +669,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected function cleanupAfter(array $url, array $data): Response {
// function requires admin rights per spec
if(Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
return new Response(403);
}
Service::cleanupPost();
@ -689,4 +693,4 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
]
]);
}
}
}

11
lib/REST/NextCloudNews/Versions.php

@ -1,18 +1,19 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\REST\NextCloudNews;
use JKingWeb\Arsse\REST\Response;
class Versions implements \JKingWeb\Arsse\REST\Handler {
function __construct() {
public function __construct() {
}
function dispatch(\JKingWeb\Arsse\REST\Request $req): Response {
public function dispatch(\JKingWeb\Arsse\REST\Request $req): Response {
// if a method other than GET was used, this is an error
if($req->method != "GET") {
if ($req->method != "GET") {
return new Response(405);
}
if(preg_match("<^/?$>",$req->path)) {
if (preg_match("<^/?$>", $req->path)) {
// if the request path is an empty string or just a slash, return the supported versions
$out = [
'apiLevels' => [
@ -25,4 +26,4 @@ class Versions implements \JKingWeb\Arsse\REST\Handler {
return new Response(501);
}
}
}
}

32
lib/REST/Request.php

@ -11,18 +11,18 @@ class Request {
public $type ="";
public $body = "";
function __construct(string $method = null, string $url = null, string $body = null, string $contentType = null) {
if(is_null($method)) {
public function __construct(string $method = null, string $url = null, string $body = null, string $contentType = null) {
if (is_null($method)) {
$method = $_SERVER['REQUEST_METHOD'];
}
if(is_null($url)) {
if (is_null($url)) {
$url = $_SERVER['REQUEST_URI'];
}
if(is_null($body)) {
}
if (is_null($body)) {
$body = file_get_contents("php://input");
}
if(is_null($contentType)) {
if(isset($_SERVER['HTTP_CONTENT_TYPE'])) {
if (is_null($contentType)) {
if (isset($_SERVER['HTTP_CONTENT_TYPE'])) {
$contentType = $_SERVER['HTTP_CONTENT_TYPE'];
} else {
$contentType = "";
@ -47,17 +47,17 @@ class Request {
$parts = explode("?", $url);
$out = ['path' => $parts[0], 'paths' => [''], 'query' => []];
// if there is a query string, parse it
if(isset($parts[1])) {
if (isset($parts[1])) {
// split along & to get key-value pairs
$query = explode("&", $parts[1]);
for($a = 0; $a < sizeof($query); $a++) {
for ($a = 0; $a < sizeof($query); $a++) {
// split each pair, into no more than two parts
$data = explode("=", $query[$a], 2);
// decode the key
$key = rawurldecode($data[0]);
// decode the value if there is one
$value = "";
if(isset($data[1])) {
if (isset($data[1])) {
$value = rawurldecode($data[1]);
}
// add the pair to the query output, overwriting earlier values for the same key, is present
@ -66,19 +66,21 @@ class Request {
}
// also include the path as a set of decoded elements
// if the path is an empty string or just / nothing needs be done
if(!in_array($out['path'],["/",""])) {
if (!in_array($out['path'], ["/",""])) {
$paths = explode("/", $out['path']);
// remove the first and last empty elements, if present (they are artefacts of the splitting; others should remain)
if(!strlen($paths[0])) {
if (!strlen($paths[0])) {
array_shift($paths);
}
if(!strlen($paths[sizeof($paths)-1])) {
if (!strlen($paths[sizeof($paths)-1])) {
array_pop($paths);
}
// %-decode each path element
$paths = array_map(function($v){return rawurldecode($v);}, $paths);
$paths = array_map(function ($v) {
return rawurldecode($v);
}, $paths);
$out['paths'] = $paths;
}
return $out;
}
}
}

21
lib/REST/Response.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\REST;
use JKingWeb\Arsse\Arsse;
class Response {
@ -14,34 +15,34 @@ class Response {
public $fields;
function __construct(int $code, $payload = null, string $type = self::T_JSON, array $extraFields = []) {
public function __construct(int $code, $payload = null, string $type = self::T_JSON, array $extraFields = []) {
$this->code = $code;
$this->payload = $payload;
$this->type = $type;
$this->fields = $extraFields;
}
function output() {
if(!headers_sent()) {
public function output() {
if (!headers_sent()) {
try {
$statusText = Arsse::$lang->msg("HTTP.Status.".$this->code);
} catch(\JKingWeb\Arsse\Lang\Exception $e) {
} catch (\JKingWeb\Arsse\Lang\Exception $e) {
$statusText = "";
}
header("Status: ".$this->code." ".$statusText);
$body = "";
if(!is_null($this->payload)) {
if (!is_null($this->payload)) {
header("Content-Type: ".$this->type);
switch($this->type) {
case self::T_JSON:
$body = (string) json_encode($this->payload,\JSON_PRETTY_PRINT);
switch ($this->type) {
case self::T_JSON:
$body = (string) json_encode($this->payload, \JSON_PRETTY_PRINT);
break;
default:
$body = (string) $this->payload;
break;
}
}
foreach($this->fields as $field) {
foreach ($this->fields as $field) {
header($field);
}
echo $body;
@ -49,4 +50,4 @@ class Response {
throw new REST\Exception("headersSent");
}
}
}
}

33
lib/Service.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\Misc\Date;
class Service {
@ -10,11 +11,11 @@ class Service {
/** @var \DateInterval */
protected $interval;
static public function driverList(): array {
public static function driverList(): array {
$sep = \DIRECTORY_SEPARATOR;
$path = __DIR__.$sep."Service".$sep;
$classes = [];
foreach(glob($path."*".$sep."Driver.php") as $file) {
foreach (glob($path."*".$sep."Driver.php") as $file) {
$name = basename(dirname($file));
$class = NS_BASE."User\\$name\\Driver";
$classes[$class] = $class::driverName();
@ -23,26 +24,26 @@ class Service {
}
public static function interval(): \DateInterval {
try{
try {
return new \DateInterval(Arsse::$conf->serviceFrequency);
} catch(\Exception $e) {
} catch (\Exception $e) {
return new \DateInterval("PT2M");
}
}
function __construct() {
public function __construct() {
$driver = Arsse::$conf->serviceDriver;
$this->drv = new $driver();
$this->interval = static::interval();
}
function watch(bool $loop = true): \DateTimeInterface {
public function watch(bool $loop = true): \DateTimeInterface {
$t = new \DateTime();
do {
$this->checkIn();
static::cleanupPre();
$list = Arsse::$db->feedListStale();
if($list) {
if ($list) {
$this->drv->queue(...$list);
$this->drv->exec();
$this->drv->clean();
@ -50,23 +51,23 @@ class Service {
}
static::cleanupPost();
$t->add($this->interval);
if($loop) {
if ($loop) {
do {
@time_sleep_until($t->getTimestamp());
} while($t->getTimestamp() > time());
} while ($t->getTimestamp() > time());
}
} while($loop);
} while ($loop);
return $t;
}
function checkIn(): bool {
public function checkIn(): bool {
return Arsse::$db->metaSet("service_last_checkin", time(), "datetime");
}
static function hasCheckedIn(): bool {
public static function hasCheckedIn(): bool {
$checkin = Arsse::$db->metaGet("service_last_checkin");
// if the service has never checked in, return false
if(!$checkin) {
if (!$checkin) {
return false;
}
// convert the check-in timestamp to a DateTime instance
@ -81,13 +82,13 @@ class Service {
return ($checkin >= $limit);
}
static function cleanupPre(): bool {
public static function cleanupPre(): bool {
// mark unsubscribed feeds as orphaned and delete orphaned feeds that are beyond their retention period
return Arsse::$db->feedCleanup();
}
static function cleanupPost(): bool {
public static function cleanupPost(): bool {
// delete old articles, according to configured threasholds
return Arsse::$db->articleCleanup();
}
}
}

19
lib/Service/Curl/Driver.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Service\Curl;
use JKingWeb\Arsse\Arsse;
class Driver implements \JKingWeb\Arsse\Service\Driver {
@ -8,15 +9,15 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
protected $queue;
protected $handles = [];
static function driverName(): string {
public static function driverName(): string {
return Arsse::$lang->msg("Driver.Service.Curl.Name");
}
static function requirementsMet(): bool {
public static function requirementsMet(): bool {
return extension_loaded("curl");
}
function __construct() {
public function __construct() {
//default curl options for individual requests
$this->options = [
\CURLOPT_URL => Arsse::$serviceCurlBase."index.php/apps/news/api/v1-2/feeds/update",
@ -42,8 +43,8 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
curl_multi_setopt($this->queue, \CURLMOPT_PIPELINING, 1);
}
function queue(int ...$feeds): int {
foreach($feeds as $id) {
public function queue(int ...$feeds): int {
foreach ($feeds as $id) {
$h = curl_init();
curl_setopt($h, \CURLOPT_POSTFIELDS, json_encode(['userId' => "", 'feedId' => $id]));
$this->handles[] = $h;
@ -52,7 +53,7 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
return sizeof($this->handles);
}
function exec(): int {
public function exec(): int {
$active = 0;
do {
curl_multi_exec($this->queue, $active);
@ -61,12 +62,12 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
return Arsse::$conf->serviceQueueWidth - $active;
}
function clean(): bool {
foreach($this->handles as $h) {
public function clean(): bool {
foreach ($this->handles as $h) {
curl_multi_remove_handle($this->queue, $h);
curl_close($h);
}
$this->handles = [];
return true;
}
}
}

12
lib/Service/Driver.php

@ -3,9 +3,9 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Service;
interface Driver {
static function driverName(): string;
static function requirementsMet(): bool;
function queue(int ...$feeds): int;
function exec(): int;
function clean(): bool;
}
public static function driverName(): string;
public static function requirementsMet(): bool;
public function queue(int ...$feeds): int;
public function exec(): int;
public function clean(): bool;
}

19
lib/Service/Forking/Driver.php

@ -1,36 +1,37 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Service\Forking;
use JKingWeb\Arsse\Arsse;
class Driver implements \JKingWeb\Arsse\Service\Driver {
protected $queue = [];
static function driverName(): string {
public static function driverName(): string {
return Arsse::$lang->msg("Driver.Service.Forking.Name");
}
static function requirementsMet(): bool {
public static function requirementsMet(): bool {
return function_exists("popen");
}
function __construct() {
public function __construct() {
}
function queue(int ...$feeds): int {
public function queue(int ...$feeds): int {
$this->queue = array_merge($this->queue, $feeds);
return sizeof($this->queue);
}
function exec(): int {
public function exec(): int {
$pp = [];
while($this->queue) {
while ($this->queue) {
$id = (int) array_shift($this->queue);
$php = '"'.\PHP_BINARY.'"';
$arsse = '"'.$_SERVER['argv'][0].'"';
array_push($pp, popen("$php $arsse feed refresh $id", "r"));
}
while($pp) {
while ($pp) {
$p = array_pop($pp);
fgets($p); // TODO: log output
pclose($p);
@ -38,8 +39,8 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
return Arsse::$conf->serviceQueueWidth - sizeof($this->queue);
}
function clean(): bool {
public function clean(): bool {
$this->queue = [];
return true;
}
}
}

17
lib/Service/Internal/Driver.php

@ -1,38 +1,39 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Service\Internal;
use JKingWeb\Arsse\Arsse;
class Driver implements \JKingWeb\Arsse\Service\Driver {
protected $queue = [];
static function driverName(): string {
public static function driverName(): string {
return Arsse::$lang->msg("Driver.Service.Internal.Name");
}
static function requirementsMet(): bool {
public static function requirementsMet(): bool {
// this driver has no requirements
return true;
}
function __construct() {
public function __construct() {
}
function queue(int ...$feeds): int {
public function queue(int ...$feeds): int {
$this->queue = array_merge($this->queue, $feeds);
return sizeof($this->queue);
}
function exec(): int {
while(sizeof($this->queue)) {
public function exec(): int {
while (sizeof($this->queue)) {
$id = array_shift($this->queue);
Arsse::$db->feedUpdate($id);
}
return Arsse::$conf->serviceQueueWidth - sizeof($this->queue);
}
function clean(): bool {
public function clean(): bool {
$this->queue = [];
return true;
}
}
}

147
lib/User.php

@ -9,7 +9,7 @@ class User {
const RIGHTS_GLOBAL_MANAGER = 75; // able to act for any normal users on any domain; cannot elevate other users
const RIGHTS_GLOBAL_ADMIN = 100; // is completely unrestricted
public $id = null;
public $id = null;
/**
* @var User\Driver
@ -19,11 +19,11 @@ class User {
protected $authzSupported = 0;
protected $actor = [];
static public function driverList(): array {
public static function driverList(): array {
$sep = \DIRECTORY_SEPARATOR;
$path = __DIR__.$sep."User".$sep;
$classes = [];
foreach(glob($path."*".$sep."Driver.php") as $file) {
foreach (glob($path."*".$sep."Driver.php") as $file) {
$name = basename(dirname($file));
$class = NS_BASE."User\\$name\\Driver";
$classes[$class] = $class::driverName();
@ -37,72 +37,72 @@ class User {
}
public function __toString() {
if($this->id===null) {
if ($this->id===null) {
$this->credentials();
}
return (string) $this->id;
}
// checks whether the logged in user is authorized to act for the affected user (used especially when granting rights)
function authorize(string $affectedUser, string $action, int $newRightsLevel = 0): bool {
public function authorize(string $affectedUser, string $action, int $newRightsLevel = 0): bool {
// if authorization checks are disabled (either because we're running the installer or the background updater) just return true
if(!$this->authorizationEnabled()) {
if (!$this->authorizationEnabled()) {
return true;
}
// if we don't have a logged-in user, fetch credentials
if($this->id===null) {
if ($this->id===null) {
$this->credentials();
}
// if the affected user is the actor and the actor is not trying to grant themselves rights, accept the request
if($affectedUser==Arsse::$user->id && $action != "userRightsSet") {
if ($affectedUser==Arsse::$user->id && $action != "userRightsSet") {
return true;
}
// if we're authorizing something other than a user function and the affected user is not the actor, make sure the affected user exists
$this->authorizationEnabled(false);
if(Arsse::$user->id != $affectedUser && strpos($action, "user")!==0 && !$this->exists($affectedUser)) {
if (Arsse::$user->id != $affectedUser && strpos($action, "user")!==0 && !$this->exists($affectedUser)) {
throw new User\Exception("doesNotExist", ["action" => $action, "user" => $affectedUser]);
}
$this->authorizationEnabled(true);
// get properties of actor if not already available
if(!sizeof($this->actor)) {
if (!sizeof($this->actor)) {
$this->actor = $this->propertiesGet(Arsse::$user->id);
}
$rights = $this->actor["rights"];
// if actor is a global admin, accept the request
if($rights==User\Driver::RIGHTS_GLOBAL_ADMIN) {
if ($rights==User\Driver::RIGHTS_GLOBAL_ADMIN) {
return true;
}
// if actor is a common user, deny the request
if($rights==User\Driver::RIGHTS_NONE) {
if ($rights==User\Driver::RIGHTS_NONE) {
return false;
}
// if actor is not some other sort of admin, deny the request
if(!in_array($rights,[User\Driver::RIGHTS_GLOBAL_MANAGER,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN],true)) {
if (!in_array($rights, [User\Driver::RIGHTS_GLOBAL_MANAGER,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN], true)) {
return false;
}
// if actor is a domain admin/manager and domains don't match, deny the request
if($this->actor["domain"] && $rights != User\Driver::RIGHTS_GLOBAL_MANAGER) {
if ($this->actor["domain"] && $rights != User\Driver::RIGHTS_GLOBAL_MANAGER) {
$test = "@".$this->actor["domain"];
if(substr($affectedUser,-1*strlen($test)) != $test) {
if (substr($affectedUser, -1*strlen($test)) != $test) {
return false;
}
}
// certain actions shouldn't check affected user's rights
if(in_array($action, ["userRightsGet","userExists","userList"], true)) {
if (in_array($action, ["userRightsGet","userExists","userList"], true)) {
return true;
}
if($action=="userRightsSet") {
if ($action=="userRightsSet") {
// setting rights above your own is not allowed
if($newRightsLevel > $rights) {
if ($newRightsLevel > $rights) {
return false;
}
// setting yourself to rights you already have is harmless and can be allowed
if($this->id==$affectedUser && $newRightsLevel==$rights) {
if ($this->id==$affectedUser && $newRightsLevel==$rights) {
return true;
}
// managers can only set their own rights, and only to normal user
if(in_array($rights, [User\Driver::RIGHTS_DOMAIN_MANAGER, User\Driver::RIGHTS_GLOBAL_MANAGER])) {
if($this->id != $affectedUser || $newRightsLevel != User\Driver::RIGHTS_NONE) {
if (in_array($rights, [User\Driver::RIGHTS_DOMAIN_MANAGER, User\Driver::RIGHTS_GLOBAL_MANAGER])) {
if ($this->id != $affectedUser || $newRightsLevel != User\Driver::RIGHTS_NONE) {
return false;
}
return true;
@ -110,20 +110,20 @@ class User {
}
$affectedRights = $this->rightsGet($affectedUser);
// managers can only act on themselves (checked above) or regular users
if(in_array($rights,[User\Driver::RIGHTS_GLOBAL_MANAGER,User\Driver::RIGHTS_DOMAIN_MANAGER]) && $affectedRights != User\Driver::RIGHTS_NONE) {
if (in_array($rights, [User\Driver::RIGHTS_GLOBAL_MANAGER,User\Driver::RIGHTS_DOMAIN_MANAGER]) && $affectedRights != User\Driver::RIGHTS_NONE) {
return false;
}
// domain admins canot act above themselves
if(!in_array($affectedRights,[User\Driver::RIGHTS_NONE,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN])) {
if (!in_array($affectedRights, [User\Driver::RIGHTS_NONE,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN])) {
return false;
}
return true;
}
public function credentials(): array {
if($_SERVER['PHP_AUTH_USER']) {
if ($_SERVER['PHP_AUTH_USER']) {
$out = ["user" => $_SERVER['PHP_AUTH_USER'], "password" => $_SERVER['PHP_AUTH_PW']];
} else if($_SERVER['REMOTE_USER']) {
} elseif ($_SERVER['REMOTE_USER']) {
$out = ["user" => $_SERVER['REMOTE_USER'], "password" => ""];
} else {
$out = ["user" => "", "password" => ""];
@ -133,25 +133,25 @@ class User {
}
public function auth(string $user = null, string $password = null): bool {
if($user===null) {
if ($user===null) {
return $this->authHTTP();
} else {
$this->id = $user;
$this->actor = [];
switch($this->u->driverFunctions("auth")) {
switch ($this->u->driverFunctions("auth")) {
case User\Driver::FUNC_EXTERNAL:
if(Arsse::$conf->userPreAuth) {
if (Arsse::$conf->userPreAuth) {
$out = true;
} else {
$out = $this->u->auth($user, $password);
}
if($out && !Arsse::$db->userExists($user)) {
if ($out && !Arsse::$db->userExists($user)) {
$this->autoProvision($user, $password);
}
return $out;
case User\Driver::FUNC_INTERNAL:
if(Arsse::$conf->userPreAuth) {
if(!Arsse::$db->userExists($user)) {
if (Arsse::$conf->userPreAuth) {
if (!Arsse::$db->userExists($user)) {
$this->autoProvision($user, $password);
}
return true;
@ -166,7 +166,7 @@ class User {
public function authHTTP(): bool {
$cred = $this->credentials();
if(!$cred["user"]) {
if (!$cred["user"]) {
return false;
}
return $this->auth($cred["user"], $cred["password"]);
@ -178,15 +178,15 @@ class User {
public function list(string $domain = null): array {
$func = "userList";
switch($this->u->driverFunctions($func)) {
switch ($this->u->driverFunctions($func)) {
case User\Driver::FUNC_EXTERNAL:
// we handle authorization checks for external drivers
if($domain===null) {
if(!$this->authorize("@".$domain, $func)) {
if ($domain===null) {
if (!$this->authorize("@".$domain, $func)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $domain]);
}
} else {
if(!$this->authorize("", $func)) {
if (!$this->authorize("", $func)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => "all users"]);
}
}
@ -199,11 +199,11 @@ class User {
}
public function authorizationEnabled(bool $setting = null): bool {
if(is_null($setting)) {
if (is_null($setting)) {
return !$this->authz;
}
$this->authz += ($setting ? -1 : 1);
if($this->authz < 0) {
if ($this->authz < 0) {
$this->authz = 0;
}
return !$this->authz;
@ -211,14 +211,14 @@ class User {
public function exists(string $user): bool {
$func = "userExists";
switch($this->u->driverFunctions($func)) {
switch ($this->u->driverFunctions($func)) {
case User\Driver::FUNC_EXTERNAL:
// we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) {
if (!$this->authorize($user, $func)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
}
$out = $this->u->userExists($user);
if($out && !Arsse::$db->userExists($user)) {
if ($out && !Arsse::$db->userExists($user)) {
$this->autoProvision($user, "");
}
return $out;
@ -233,15 +233,15 @@ class User {
public function add($user, $password = null): string {
$func = "userAdd";
switch($this->u->driverFunctions($func)) {
switch ($this->u->driverFunctions($func)) {
case User\Driver::FUNC_EXTERNAL:
// we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) {
if (!$this->authorize($user, $func)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
}
$newPassword = $this->u->userAdd($user, $password);
// if there was no exception and we don't have the user in the internal database, add it
if(!Arsse::$db->userExists($user)) {
if (!Arsse::$db->userExists($user)) {
$this->autoProvision($user, $newPassword);
}
return $newPassword;
@ -255,16 +255,16 @@ class User {
public function remove(string $user): bool {
$func = "userRemove";
switch($this->u->driverFunctions($func)) {
switch ($this->u->driverFunctions($func)) {
case User\Driver::FUNC_EXTERNAL:
// we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) {
if (!$this->authorize($user, $func)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
}
$out = $this->u->userRemove($user);
if($out && Arsse::$db->userExists($user)) {
if ($out && Arsse::$db->userExists($user)) {
// if the user was removed and we have it in our data, remove it there
if(!Arsse::$db->userExists($user)) {
if (!Arsse::$db->userExists($user)) {
Arsse::$db->userRemove($user);
}
}
@ -279,14 +279,14 @@ class User {
public function passwordSet(string $user, string $newPassword = null, $oldPassword = null): string {
$func = "userPasswordSet";
switch($this->u->driverFunctions($func)) {
switch ($this->u->driverFunctions($func)) {
case User\Driver::FUNC_EXTERNAL:
// we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) {
if (!$this->authorize($user, $func)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
}
$out = $this->u->userPasswordSet($user, $newPassword, $oldPassword);
if(Arsse::$db->userExists($user)) {
if (Arsse::$db->userExists($user)) {
// if the password change was successful and the user exists, set the internal password to the same value
Arsse::$db->userPasswordSet($user, $out);
} else {
@ -305,8 +305,8 @@ class User {
public function propertiesGet(string $user, bool $withAvatar = false): array {
// prepare default values
$domain = null;
if(strrpos($user,"@")!==false) {
$domain = substr($user,strrpos($user,"@")+1);
if (strrpos($user, "@")!==false) {
$domain = substr($user, strrpos($user, "@")+1);
}
$init = [
"id" => $user,
@ -315,19 +315,19 @@ class User {
"domain" => $domain
];
$func = "userPropertiesGet";
switch($this->u->driverFunctions($func)) {
switch ($this->u->driverFunctions($func)) {
case User\Driver::FUNC_EXTERNAL:
// we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) {
if (!$this->authorize($user, $func)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
}
$out = array_merge($init, $this->u->userPropertiesGet($user));
// remove password if it is return (not exhaustive, but...)
if(array_key_exists('password', $out)) {
if (array_key_exists('password', $out)) {
unset($out['password']);
}
// if the user does not exist in the internal database, add it
if(!Arsse::$db->userExists($user)) {
if (!Arsse::$db->userExists($user)) {
$this->autoProvision($user, "", $out);
}
return $out;
@ -342,20 +342,20 @@ class User {
public function propertiesSet(string $user, array $properties): array {
// remove from the array any values which should be set specially
foreach(['id', 'domain', 'password', 'rights'] as $key) {
if(array_key_exists($key, $properties)) {
foreach (['id', 'domain', 'password', 'rights'] as $key) {
if (array_key_exists($key, $properties)) {
unset($properties[$key]);
}
}
$func = "userPropertiesSet";
switch($this->u->driverFunctions($func)) {
switch ($this->u->driverFunctions($func)) {
case User\Driver::FUNC_EXTERNAL:
// we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) {
if (!$this->authorize($user, $func)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
}
$out = $this->u->userPropertiesSet($user, $properties);
if(Arsse::$db->userExists($user)) {
if (Arsse::$db->userExists($user)) {
// if the property change was successful and the user exists, set the internal properties to the same values
Arsse::$db->userPropertiesSet($user, $out);
} else {
@ -373,15 +373,15 @@ class User {
public function rightsGet(string $user): int {
$func = "userRightsGet";
switch($this->u->driverFunctions($func)) {
switch ($this->u->driverFunctions($func)) {
case User\Driver::FUNC_EXTERNAL:
// we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) {
if (!$this->authorize($user, $func)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
}
$out = $this->u->userRightsGet($user);
// if the user does not exist in the internal database, add it
if(!Arsse::$db->userExists($user)) {
if (!Arsse::$db->userExists($user)) {
$this->autoProvision($user, "", null, $out);
}
return $out;
@ -396,20 +396,20 @@ class User {
public function rightsSet(string $user, int $level): bool {
$func = "userRightsSet";
switch($this->u->driverFunctions($func)) {
switch ($this->u->driverFunctions($func)) {
case User\Driver::FUNC_EXTERNAL:
// we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) {
if (!$this->authorize($user, $func)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
}
$out = $this->u->userRightsSet($user, $level);
// if the user does not exist in the internal database, add it
if($out && Arsse::$db->userExists($user)) {
if ($out && Arsse::$db->userExists($user)) {
$authz = $this->authorizationEnabled();
$this->authorizationEnabled(false);
Arsse::$db->userRightsSet($user, $level);
$this->authorizationEnabled($authz);
} else if($out) {
} elseif ($out) {
$this->autoProvision($user, "", null, $level);
}
return $out;
@ -429,13 +429,14 @@ class User {
// set the user rights
Arsse::$db->userRightsSet($user, $rights);
// set the user properties...
if($properties===null) {
if ($properties===null) {
// if nothing is provided but the driver uses an external function, try to get the current values from the external source
try {
if($this->u->driverFunctions("userPropertiesGet")==User\Driver::FUNC_EXTERNAL) {
if ($this->u->driverFunctions("userPropertiesGet")==User\Driver::FUNC_EXTERNAL) {
Arsse::$db->userPropertiesSet($user, $this->u->userPropertiesGet($user));
}
} catch(\Throwable $e) {}
} catch (\Throwable $e) {
}
} else {
// otherwise if values are provided, use those
Arsse::$db->userPropertiesSet($user, $properties);
@ -444,4 +445,4 @@ class User {
$this->authorizationEnabled(true);
return $out;
}
}
}

30
lib/User/Driver.php

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace JKingWeb\Arsse\User;
Interface Driver {
interface Driver {
const FUNC_NOT_IMPLEMENTED = 0;
const FUNC_INTERNAL = 1;
const FUNC_EXTERNAL = 2;
@ -14,29 +14,29 @@ Interface Driver {
const RIGHTS_GLOBAL_ADMIN = 100; // is completely unrestricted
// returns an instance of a class implementing this interface.
function __construct();
public function __construct();
// returns a human-friendly name for the driver (for display in installer, for example)
static function driverName(): string;
public static function driverName(): string;
// returns an array (or single queried member of same) of methods defined by this interface and whether the class implements the internal function or a custom version
function driverFunctions(string $function = null);
public function driverFunctions(string $function = null);
// authenticates a user against their name and password
function auth(string $user, string $password): bool;
public function auth(string $user, string $password): bool;
// checks whether a user exists
function userExists(string $user): bool;
public function userExists(string $user): bool;
// adds a user
function userAdd(string $user, string $password = null): string;
public function userAdd(string $user, string $password = null): string;
// removes a user
function userRemove(string $user): bool;
public function userRemove(string $user): bool;
// lists all users
function userList(string $domain = null): array;
public function userList(string $domain = null): array;
// sets a user's password; if the driver does not require the old password, it may be ignored
function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): string;
public function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): string;
// gets user metadata (currently not useful)
function userPropertiesGet(string $user): array;
public function userPropertiesGet(string $user): array;
// sets user metadata (currently not useful)
function userPropertiesSet(string $user, array $properties): array;
public function userPropertiesSet(string $user, array $properties): array;
// returns a user's access level according to RIGHTS_* constants (or some custom semantics, if using custom implementation of authorize())
function userRightsGet(string $user): int;
public function userRightsGet(string $user): int;
// sets a user's access level
function userRightsSet(string $user, int $level): bool;
}
public function userRightsSet(string $user, int $level): bool;
}

2
lib/User/Exception.php

@ -3,4 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\User;
class Exception extends \JKingWeb\Arsse\AbstractException {
}
}

2
lib/User/ExceptionAuthz.php

@ -3,4 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\User;
class ExceptionAuthz extends Exception {
}
}

2
lib/User/ExceptionNotImplemented.php

@ -3,4 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\User;
class ExceptionNotImplemented extends Exception {
}
}

8
lib/User/Internal/Driver.php

@ -19,15 +19,15 @@ final class Driver implements \JKingWeb\Arsse\User\Driver {
"userRightsSet" => self::FUNC_INTERNAL,
];
static public function driverName(): string {
public static function driverName(): string {
return Arsse::$lang->msg("Driver.User.Internal.Name");
}
public function driverFunctions(string $function = null) {
if($function===null) {
if ($function===null) {
return $this->functions;
}
if(array_key_exists($function, $this->functions)) {
if (array_key_exists($function, $this->functions)) {
return $this->functions[$function];
} else {
return self::FUNC_NOT_IMPLEMENTED;
@ -35,4 +35,4 @@ final class Driver implements \JKingWeb\Arsse\User\Driver {
}
// see InternalFunctions.php for bulk of methods
}
}

27
lib/User/Internal/InternalFunctions.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\User\Internal;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\User\Exception;
@ -10,51 +11,51 @@ trait InternalFunctions {
public function __construct() {
}
function auth(string $user, string $password): bool {
public function auth(string $user, string $password): bool {
try {
$hash = Arsse::$db->userPasswordGet($user);
} catch(Exception $e) {
} catch (Exception $e) {
return false;
}
if($password==="" && $hash==="") {
if ($password==="" && $hash==="") {
return true;
}
return password_verify($password, $hash);
}
function userExists(string $user): bool {
public function userExists(string $user): bool {
return Arsse::$db->userExists($user);
}
function userAdd(string $user, string $password = null): string {
public function userAdd(string $user, string $password = null): string {
return Arsse::$db->userAdd($user, $password);
}
function userRemove(string $user): bool {
public function userRemove(string $user): bool {
return Arsse::$db->userRemove($user);
}
function userList(string $domain = null): array {
public function userList(string $domain = null): array {
return Arsse::$db->userList($domain);
}
function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): string {
public function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): string {
return Arsse::$db->userPasswordSet($user, $newPassword);
}
function userPropertiesGet(string $user): array {
public function userPropertiesGet(string $user): array {
return Arsse::$db->userPropertiesGet($user);
}
function userPropertiesSet(string $user, array $properties): array {
public function userPropertiesSet(string $user, array $properties): array {
return Arsse::$db->userPropertiesSet($user, $properties);
}
function userRightsGet(string $user): int {
public function userRightsGet(string $user): int {
return Arsse::$db->userRightsGet($user);
}
function userRightsSet(string $user, int $level): bool {
public function userRightsSet(string $user, int $level): bool {
return Arsse::$db->userRightsSet($user, $level);
}
}
}

37
tests/Conf/TestConf.php

@ -1,14 +1,15 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use org\bovigo\vfs\vfsStream;
/** @covers \JKingWeb\Arsse\Conf */
class TestConf extends Test\AbstractTest {
static $vfs;
static $path;
public static $vfs;
public static $path;
function setUp() {
public function setUp() {
$this->clearData();
self::$vfs = vfsStream::setup("root", null, [
'confGood' => '<?php return Array("lang" => "xx");',
@ -26,18 +27,18 @@ class TestConf extends Test\AbstractTest {
chmod(self::$path."confForbidden", 0000);
}
function tearDown() {
public function tearDown() {
self::$path = null;
self::$vfs = null;
$this->clearData();
}
function testLoadDefaultValues() {
public function testLoadDefaultValues() {
$this->assertInstanceOf(Conf::class, new Conf());
}
/** @depends testLoadDefaultValues */
function testImportFromArray() {
public function testImportFromArray() {
$arr = ['lang' => "xx"];
$conf = new Conf();
$conf->import($arr);
@ -45,7 +46,7 @@ class TestConf extends Test\AbstractTest {
}
/** @depends testImportFromArray */
function testImportFromFile() {
public function testImportFromFile() {
$conf = new Conf();
$conf->importFile(self::$path."confGood");
$this->assertEquals("xx", $conf->lang);
@ -54,43 +55,43 @@ class TestConf extends Test\AbstractTest {
}
/** @depends testImportFromFile */
function testImportFromMissingFile() {
public function testImportFromMissingFile() {
$this->assertException("fileMissing", "Conf");
$conf = new Conf(self::$path."confMissing");
}
/** @depends testImportFromFile */
function testImportFromEmptyFile() {
public function testImportFromEmptyFile() {
$this->assertException("fileCorrupt", "Conf");
$conf = new Conf(self::$path."confEmpty");
}
/** @depends testImportFromFile */
function testImportFromFileWithoutReadPermission() {
public function testImportFromFileWithoutReadPermission() {
$this->assertException("fileUnreadable", "Conf");
$conf = new Conf(self::$path."confUnreadable");
}
/** @depends testImportFromFile */
function testImportFromFileWhichIsNotAnArray() {
public function testImportFromFileWhichIsNotAnArray() {
$this->assertException("fileCorrupt", "Conf");
$conf = new Conf(self::$path."confNotArray");
}
/** @depends testImportFromFile */
function testImportFromFileWhichIsNotPhp() {
public function testImportFromFileWhichIsNotPhp() {
$this->assertException("fileCorrupt", "Conf");
// this should not print the output of the non-PHP file
$conf = new Conf(self::$path."confNotPHP");
}
/** @depends testImportFromFile */
function testImportFromCorruptFile() {
public function testImportFromCorruptFile() {
$this->assertException("fileCorrupt", "Conf");
$conf = new Conf(self::$path."confCorrupt");
}
function testExportToArray() {
public function testExportToArray() {
$conf = new Conf();
$conf->lang = ["en", "fr"]; // should not be exported: not scalar
$conf->dbSQLite3File = "test.db"; // should be exported: value changed
@ -105,9 +106,9 @@ class TestConf extends Test\AbstractTest {
$this->assertArraySubset($exp, $res);
}
/** @depends testExportToArray
/** @depends testExportToArray
* @depends testImportFromFile */
function testExportToFile() {
public function testExportToFile() {
$conf = new Conf();
$conf->lang = ["en", "fr"]; // should not be exported: not scalar
$conf->dbSQLite3File = "test.db"; // should be exported: value changed
@ -125,12 +126,12 @@ class TestConf extends Test\AbstractTest {
$this->assertArraySubset($exp, $arr);
}
function testExportToFileWithoutWritePermission() {
public function testExportToFileWithoutWritePermission() {
$this->assertException("fileUnwritable", "Conf");
(new Conf)->exportFile(self::$path."confUnreadable");
}
function testExportToFileWithoutCreatePermission() {
public function testExportToFileWithoutCreatePermission() {
$this->assertException("fileUncreatable", "Conf");
(new Conf)->exportFile(self::$path."confForbidden/conf");
}

2
tests/Db/SQLite3/Database/TestDatabaseArticleSQLite3.php

@ -7,4 +7,4 @@ class TestDatabaseArticleSQLite3 extends Test\AbstractTest {
use Test\Database\Setup;
use Test\Database\DriverSQLite3;
use Test\Database\SeriesArticle;
}
}

2
tests/Db/SQLite3/Database/TestDatabaseCleanupSQLite3.php

@ -7,4 +7,4 @@ class TestDatabaseCleanupSQLite3 extends Test\AbstractTest {
use Test\Database\Setup;
use Test\Database\DriverSQLite3;
use Test\Database\SeriesCleanup;
}
}

2
tests/Db/SQLite3/Database/TestDatabaseFeedSQLite3.php

@ -7,4 +7,4 @@ class TestDatabaseFeedSQLite3 extends Test\AbstractTest {
use Test\Database\Setup;
use Test\Database\DriverSQLite3;
use Test\Database\SeriesFeed;
}
}

2
tests/Db/SQLite3/Database/TestDatabaseFolderSQLite3.php

@ -7,4 +7,4 @@ class TestDatabaseFolderSQLite3 extends Test\AbstractTest {
use Test\Database\Setup;
use Test\Database\DriverSQLite3;
use Test\Database\SeriesFolder;
}
}

2
tests/Db/SQLite3/Database/TestDatabaseMetaSQLite3.php

@ -7,4 +7,4 @@ class TestDatabaseMetaSQLite3 extends Test\AbstractTest {
use Test\Database\Setup;
use Test\Database\DriverSQLite3;
use Test\Database\SeriesMeta;
}
}

2
tests/Db/SQLite3/Database/TestDatabaseMiscellanySQLite3.php

@ -7,4 +7,4 @@ class TestDatabaseMiscellanySQLite3 extends Test\AbstractTest {
use Test\Database\Setup;
use Test\Database\DriverSQLite3;
use Test\Database\SeriesMiscellany;
}
}

2
tests/Db/SQLite3/Database/TestDatabaseSubscriptionSQLite3.php

@ -7,4 +7,4 @@ class TestDatabaseSubscriptionSQLite3 extends Test\AbstractTest {
use Test\Database\Setup;
use Test\Database\DriverSQLite3;
use Test\Database\SeriesSubscription;
}
}

2
tests/Db/SQLite3/Database/TestDatabaseUserSQLite3.php

@ -7,4 +7,4 @@ class TestDatabaseUserSQLite3 extends Test\AbstractTest {
use Test\Database\Setup;
use Test\Database\DriverSQLite3;
use Test\Database\SeriesUser;
}
}

45
tests/Db/SQLite3/TestDbDriverCreationSQLite3.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\Arsse;
use org\bovigo\vfs\vfsStream;
use Phake;
@ -13,8 +14,8 @@ class TestDbDriverCreationSQLite3 extends Test\AbstractTest {
protected $drv;
protected $ch;
function setUp() {
if(!extension_loaded("sqlite3")) {
public function setUp() {
if (!extension_loaded("sqlite3")) {
$this->markTestSkipped("SQLite extension not loaded");
}
$this->clearData();
@ -88,15 +89,15 @@ class TestDbDriverCreationSQLite3 extends Test\AbstractTest {
$this->path = $path = $vfs->url()."/";
// set up access blocks
chmod($path."Cmain", 0555);
chmod($path."Cwal", 0555);
chmod($path."Cshm", 0555);
chmod($path."Rmain/arsse.db", 0333);
chmod($path."Cwal", 0555);
chmod($path."Cshm", 0555);
chmod($path."Rmain/arsse.db", 0333);
chmod($path."Rwal/arsse.db-wal", 0333);
chmod($path."Rshm/arsse.db-shm", 0333);
chmod($path."Wmain/arsse.db", 0555);
chmod($path."Wmain/arsse.db", 0555);
chmod($path."Wwal/arsse.db-wal", 0555);
chmod($path."Wshm/arsse.db-shm", 0555);
chmod($path."Amain/arsse.db", 0111);
chmod($path."Amain/arsse.db", 0111);
chmod($path."Awal/arsse.db-wal", 0111);
chmod($path."Ashm/arsse.db-shm", 0111);
// set up configuration
@ -105,85 +106,85 @@ class TestDbDriverCreationSQLite3 extends Test\AbstractTest {
// set up database shim
}
function tearDown() {
public function tearDown() {
$this->clearData();
}
function testFailToCreateDatabase() {
public function testFailToCreateDatabase() {
Arsse::$conf->dbSQLite3File = $this->path."Cmain/arsse.db";
$this->assertException("fileUncreatable", "Db");
new Db\SQLite3\Driver;
}
function testFailToCreateJournal() {
public function testFailToCreateJournal() {
Arsse::$conf->dbSQLite3File = $this->path."Cwal/arsse.db";
$this->assertException("fileUncreatable", "Db");
new Db\SQLite3\Driver;
}
function testFailToCreateSharedMmeory() {
public function testFailToCreateSharedMmeory() {
Arsse::$conf->dbSQLite3File = $this->path."Cshm/arsse.db";
$this->assertException("fileUncreatable", "Db");
new Db\SQLite3\Driver;
}
function testFailToReadDatabase() {
public function testFailToReadDatabase() {
Arsse::$conf->dbSQLite3File = $this->path."Rmain/arsse.db";
$this->assertException("fileUnreadable", "Db");
new Db\SQLite3\Driver;
}
function testFailToReadJournal() {
public function testFailToReadJournal() {
Arsse::$conf->dbSQLite3File = $this->path."Rwal/arsse.db";
$this->assertException("fileUnreadable", "Db");
new Db\SQLite3\Driver;
}
function testFailToReadSharedMmeory() {
public function testFailToReadSharedMmeory() {
Arsse::$conf->dbSQLite3File = $this->path."Rshm/arsse.db";
$this->assertException("fileUnreadable", "Db");
new Db\SQLite3\Driver;
}
function testFailToWriteToDatabase() {
public function testFailToWriteToDatabase() {
Arsse::$conf->dbSQLite3File = $this->path."Wmain/arsse.db";
$this->assertException("fileUnwritable", "Db");
new Db\SQLite3\Driver;
}
function testFailToWriteToJournal() {
public function testFailToWriteToJournal() {
Arsse::$conf->dbSQLite3File = $this->path."Wwal/arsse.db";
$this->assertException("fileUnwritable", "Db");
new Db\SQLite3\Driver;
}
function testFailToWriteToSharedMmeory() {
public function testFailToWriteToSharedMmeory() {
Arsse::$conf->dbSQLite3File = $this->path."Wshm/arsse.db";
$this->assertException("fileUnwritable", "Db");
new Db\SQLite3\Driver;
}
function testFailToAccessDatabase() {
public function testFailToAccessDatabase() {
Arsse::$conf->dbSQLite3File = $this->path."Amain/arsse.db";
$this->assertException("fileUnusable", "Db");
new Db\SQLite3\Driver;
}
function testFailToAccessJournal() {
public function testFailToAccessJournal() {
Arsse::$conf->dbSQLite3File = $this->path."Awal/arsse.db";
$this->assertException("fileUnusable", "Db");
new Db\SQLite3\Driver;
}
function testFailToAccessSharedMmeory() {
public function testFailToAccessSharedMmeory() {
Arsse::$conf->dbSQLite3File = $this->path."Ashm/arsse.db";
$this->assertException("fileUnusable", "Db");
new Db\SQLite3\Driver;
}
function testAssumeDatabaseCorruption() {
public function testAssumeDatabaseCorruption() {
Arsse::$conf->dbSQLite3File = $this->path."corrupt/arsse.db";
$this->assertException("fileCorrupt", "Db");
new Db\SQLite3\Driver;
}
}
}

74
tests/Db/SQLite3/TestDbDriverSQLite3.php

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace JKingWeb\Arsse;
/**
/**
* @covers \JKingWeb\Arsse\Db\SQLite3\Driver<extended>
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
class TestDbDriverSQLite3 extends Test\AbstractTest {
@ -10,8 +10,8 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
protected $drv;
protected $ch;
function setUp() {
if(!extension_loaded("sqlite3")) {
public function setUp() {
if (!extension_loaded("sqlite3")) {
$this->markTestSkipped("SQLite extension not loaded");
}
$this->clearData();
@ -24,110 +24,110 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
$this->ch->enableExceptions(true);
}
function tearDown() {
public function tearDown() {
unset($this->drv);
unset($this->ch);
if(isset(Arsse::$conf)) {
if (isset(Arsse::$conf)) {
unlink(Arsse::$conf->dbSQLite3File);
}
$this->clearData();
}
function testFetchDriverName() {
public function testFetchDriverName() {
$class = Arsse::$conf->dbDriver;
$this->assertTrue(strlen($class::driverName()) > 0);
}
function testExecAValidStatement() {
public function testExecAValidStatement() {
$this->assertTrue($this->drv->exec("CREATE TABLE test(id integer primary key)"));
}
function testExecAnInvalidStatement() {
public function testExecAnInvalidStatement() {
$this->assertException("engineErrorGeneral", "Db");
$this->drv->exec("And the meek shall inherit the earth...");
}
function testExecMultipleStatements() {
public function testExecMultipleStatements() {
$this->assertTrue($this->drv->exec("CREATE TABLE test(id integer primary key); INSERT INTO test(id) values(2112)"));
$this->assertEquals(2112, $this->ch->querySingle("SELECT id from test"));
}
function testExecTimeout() {
public function testExecTimeout() {
$this->ch->exec("BEGIN EXCLUSIVE TRANSACTION");
$this->assertException("general", "Db", "ExceptionTimeout");
$this->drv->exec("CREATE TABLE test(id integer primary key)");
}
function testExecConstraintViolation() {
public function testExecConstraintViolation() {
$this->drv->exec("CREATE TABLE test(id integer not null)");
$this->assertException("constraintViolation", "Db", "ExceptionInput");
$this->drv->exec("INSERT INTO test(id) values(null)");
}
function testExecTypeViolation() {
public function testExecTypeViolation() {
$this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->assertException("typeViolation", "Db", "ExceptionInput");
$this->drv->exec("INSERT INTO test(id) values('ook')");
}
function testMakeAValidQuery() {
public function testMakeAValidQuery() {
$this->assertInstanceOf(Db\Result::class, $this->drv->query("SELECT 1"));
}
function testMakeAnInvalidQuery() {
public function testMakeAnInvalidQuery() {
$this->assertException("engineErrorGeneral", "Db");
$this->drv->query("Apollo was astonished; Dionysus thought me mad");
}
function testQueryTimeout() {
public function testQueryTimeout() {
$this->ch->exec("BEGIN EXCLUSIVE TRANSACTION");
$this->assertException("general", "Db", "ExceptionTimeout");
$this->drv->query("CREATE TABLE test(id integer primary key)");
}
function testQueryConstraintViolation() {
public function testQueryConstraintViolation() {
$this->drv->exec("CREATE TABLE test(id integer not null)");
$this->assertException("constraintViolation", "Db", "ExceptionInput");
$this->drv->query("INSERT INTO test(id) values(null)");
}
function testQueryTypeViolation() {
public function testQueryTypeViolation() {
$this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->assertException("typeViolation", "Db", "ExceptionInput");
$this->drv->query("INSERT INTO test(id) values('ook')");
}
function testPrepareAValidQuery() {
public function testPrepareAValidQuery() {
$s = $this->drv->prepare("SELECT ?, ?", "int", "int");
$this->assertInstanceOf(Db\Statement::class, $s);
}
function testPrepareAnInvalidQuery() {
public function testPrepareAnInvalidQuery() {
$this->assertException("engineErrorGeneral", "Db");
$s = $this->drv->prepare("This is an invalid query", "int", "int");
}
function testCreateASavepoint() {
public function testCreateASavepoint() {
$this->assertEquals(1, $this->drv->savepointCreate());
$this->assertEquals(2, $this->drv->savepointCreate());
$this->assertEquals(3, $this->drv->savepointCreate());
}
function testReleaseASavepoint() {
public function testReleaseASavepoint() {
$this->assertEquals(1, $this->drv->savepointCreate());
$this->assertEquals(true, $this->drv->savepointRelease());
$this->assertException("invalid", "Db", "ExceptionSavepoint");
$this->drv->savepointRelease();
}
function testUndoASavepoint() {
public function testUndoASavepoint() {
$this->assertEquals(1, $this->drv->savepointCreate());
$this->assertEquals(true, $this->drv->savepointUndo());
$this->assertException("invalid", "Db", "ExceptionSavepoint");
$this->drv->savepointUndo();
}
function testManipulateSavepoints() {
public function testManipulateSavepoints() {
$this->assertEquals(1, $this->drv->savepointCreate());
$this->assertEquals(2, $this->drv->savepointCreate());
$this->assertEquals(3, $this->drv->savepointCreate());
@ -144,7 +144,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
$this->drv->savepointRelease(2);
}
function testManipulateSavepointsSomeMore() {
public function testManipulateSavepointsSomeMore() {
$this->assertEquals(1, $this->drv->savepointCreate());
$this->assertEquals(2, $this->drv->savepointCreate());
$this->assertEquals(3, $this->drv->savepointCreate());
@ -155,7 +155,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
$this->drv->savepointUndo(2);
}
function testBeginATransaction() {
public function testBeginATransaction() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$this->drv->exec("CREATE TABLE test(id integer primary key)");
@ -168,7 +168,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
$this->assertEquals(0, $this->ch->querySingle($select));
}
function testCommitATransaction() {
public function testCommitATransaction() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$this->drv->exec("CREATE TABLE test(id integer primary key)");
@ -181,7 +181,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
$this->assertEquals(1, $this->ch->querySingle($select));
}
function testRollbackATransaction() {
public function testRollbackATransaction() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$this->drv->exec("CREATE TABLE test(id integer primary key)");
@ -194,7 +194,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
$this->assertEquals(0, $this->ch->querySingle($select));
}
function testBeginChainedTransactions() {
public function testBeginChainedTransactions() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$this->drv->exec("CREATE TABLE test(id integer primary key)");
@ -208,7 +208,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
$this->assertEquals(0, $this->ch->querySingle($select));
}
function testCommitChainedTransactions() {
public function testCommitChainedTransactions() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$this->drv->exec("CREATE TABLE test(id integer primary key)");
@ -226,7 +226,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
$this->assertEquals(2, $this->ch->querySingle($select));
}
function testCommitChainedTransactionsOutOfOrder() {
public function testCommitChainedTransactionsOutOfOrder() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$this->drv->exec("CREATE TABLE test(id integer primary key)");
@ -243,7 +243,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
$tr2->commit();
}
function testRollbackChainedTransactions() {
public function testRollbackChainedTransactions() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$this->drv->exec("CREATE TABLE test(id integer primary key)");
@ -263,7 +263,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
$this->assertEquals(0, $this->ch->querySingle($select));
}
function testRollbackChainedTransactionsOutOfOrder() {
public function testRollbackChainedTransactionsOutOfOrder() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$this->drv->exec("CREATE TABLE test(id integer primary key)");
@ -283,7 +283,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
$this->assertEquals(0, $this->ch->querySingle($select));
}
function testPartiallyRollbackChainedTransactions() {
public function testPartiallyRollbackChainedTransactions() {
$select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)";
$this->drv->exec("CREATE TABLE test(id integer primary key)");
@ -303,7 +303,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
$this->assertEquals(1, $this->ch->querySingle($select));
}
function testFetchSchemaVersion() {
public function testFetchSchemaVersion() {
$this->assertSame(0, $this->drv->schemaVersion());
$this->drv->exec("PRAGMA user_version=1");
$this->assertSame(1, $this->drv->schemaVersion());
@ -311,17 +311,17 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
$this->assertSame(2, $this->drv->schemaVersion());
}
function testLockTheDatabase() {
public function testLockTheDatabase() {
$this->drv->savepointCreate(true);
$this->assertException();
$this->ch->exec("CREATE TABLE test(id integer primary key)");
}
function testUnlockTheDatabase() {
public function testUnlockTheDatabase() {
$this->drv->savepointCreate(true);
$this->drv->savepointRelease();
$this->drv->savepointCreate(true);
$this->drv->savepointUndo();
$this->assertSame(true, $this->ch->exec("CREATE TABLE test(id integer primary key)"));
}
}
}

31
tests/Db/SQLite3/TestDbResultSQLite3.php

@ -4,11 +4,10 @@ namespace JKingWeb\Arsse;
/** @covers \JKingWeb\Arsse\Db\SQLite3\Result<extended> */
class TestDbResultSQLite3 extends Test\AbstractTest {
protected $c;
function setUp() {
if(!extension_loaded("sqlite3")) {
public function setUp() {
if (!extension_loaded("sqlite3")) {
$this->markTestSkipped("SQLite extension not loaded");
}
$c = new \SQLite3(":memory:");
@ -16,49 +15,49 @@ class TestDbResultSQLite3 extends Test\AbstractTest {
$this->c = $c;
}
function tearDown() {
public function tearDown() {
$this->c->close();
unset($this->c);
}
function testConstructResult() {
public function testConstructResult() {
$set = $this->c->query("SELECT 1");
$this->assertInstanceOf(Db\Result::class, new Db\SQLite3\Result($set));
}
function testGetChangeCountAndLastInsertId() {
public function testGetChangeCountAndLastInsertId() {
$this->c->query("CREATE TABLE test(col)");
$set = $this->c->query("INSERT INTO test(col) values(1)");
$rows = $this->c->changes();
$id = $this->c->lastInsertRowID();
$r = new Db\SQLite3\Result($set,[$rows,$id]);
$r = new Db\SQLite3\Result($set, [$rows,$id]);
$this->assertEquals($rows, $r->changes());
$this->assertEquals($id, $r->lastId());
}
function testIterateOverResults() {
public function testIterateOverResults() {
$set = $this->c->query("SELECT 1 as col union select 2 as col union select 3 as col");
$rows = [];
foreach(new Db\SQLite3\Result($set) as $index => $row) {
foreach (new Db\SQLite3\Result($set) as $index => $row) {
$rows[$index] = $row['col'];
}
$this->assertEquals([0 => 1, 1 => 2, 2 => 3], $rows);
}
function testIterateOverResultsTwice() {
public function testIterateOverResultsTwice() {
$set = $this->c->query("SELECT 1 as col union select 2 as col union select 3 as col");
$rows = [];
$test = new Db\SQLite3\Result($set);
foreach($test as $row) {
foreach ($test as $row) {
$rows[] = $row['col'];
}
foreach($test as $row) {
foreach ($test as $row) {
$rows[] = $row['col'];
}
$this->assertEquals([1,2,3,1,2,3], $rows);
}
function testGetSingleValues() {
public function testGetSingleValues() {
$set = $this->c->query("SELECT 1867 as year union select 1970 as year union select 2112 as year");
$test = new Db\SQLite3\Result($set);
$this->assertEquals(1867, $test->getValue());
@ -67,7 +66,7 @@ class TestDbResultSQLite3 extends Test\AbstractTest {
$this->assertSame(null, $test->getValue());
}
function testGetFirstValuesOnly() {
public function testGetFirstValuesOnly() {
$set = $this->c->query("SELECT 1867 as year, 19 as century union select 1970 as year, 20 as century union select 2112 as year, 22 as century");
$test = new Db\SQLite3\Result($set);
$this->assertEquals(1867, $test->getValue());
@ -76,7 +75,7 @@ class TestDbResultSQLite3 extends Test\AbstractTest {
$this->assertSame(null, $test->getValue());
}
function testGetRows() {
public function testGetRows() {
$set = $this->c->query("SELECT '2112' as album, '2112' as track union select 'Clockwork Angels' as album, 'The Wreckers' as track");
$rows = [
['album' => '2112', 'track' => '2112'],
@ -88,4 +87,4 @@ class TestDbResultSQLite3 extends Test\AbstractTest {
$this->assertSame(null, $test->getRow());
$this->assertEquals($rows, $test->getAll());
}
}
}

30
tests/Db/SQLite3/TestDbStatementSQLite3.php

@ -1,21 +1,21 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\Db\Statement;
use JKingWeb\Arsse\Db\Statement;
/**
/**
* @covers \JKingWeb\Arsse\Db\SQLite3\Statement<extended>
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
class TestDbStatementSQLite3 extends Test\AbstractTest {
use Test\Db\BindingTests;
protected $c;
static protected $imp = Db\SQLite3\Statement::class;
protected static $imp = Db\SQLite3\Statement::class;
function setUp() {
public function setUp() {
$this->clearData();
if(!extension_loaded("sqlite3")) {
if (!extension_loaded("sqlite3")) {
$this->markTestSkipped("SQLite extension not loaded");
}
$c = new \SQLite3(":memory:");
@ -23,7 +23,7 @@ class TestDbStatementSQLite3 extends Test\AbstractTest {
$this->c = $c;
}
function tearDown() {
public function tearDown() {
$this->c->close();
unset($this->c);
}
@ -32,7 +32,7 @@ class TestDbStatementSQLite3 extends Test\AbstractTest {
$nativeStatement = $this->c->prepare("SELECT ? as value");
$s = new self::$imp($this->c, $nativeStatement);
$types = array_unique(Statement::TYPES);
foreach($types as $type) {
foreach ($types as $type) {
$s->rebindArray([$strict ? "strict $type" : $type]);
$val = $s->runArray([$input])->getRow()['value'];
$this->assertSame($expectations[$type], $val, "Binding from type $type failed comparison.");
@ -42,19 +42,19 @@ class TestDbStatementSQLite3 extends Test\AbstractTest {
}
}
function testConstructStatement() {
public function testConstructStatement() {
$nativeStatement = $this->c->prepare("SELECT ? as value");
$this->assertInstanceOf(Statement::class, new Db\SQLite3\Statement($this->c, $nativeStatement));
}
function testBindMissingValue() {
public function testBindMissingValue() {
$nativeStatement = $this->c->prepare("SELECT ? as value");
$s = new self::$imp($this->c, $nativeStatement);
$val = $s->runArray()->getRow()['value'];
$this->assertSame(null, $val);
}
function testBindMultipleValues() {
public function testBindMultipleValues() {
$exp = [
'one' => 1,
'two' => 2,
@ -65,7 +65,7 @@ class TestDbStatementSQLite3 extends Test\AbstractTest {
$this->assertSame($exp, $val);
}
function testBindRecursively() {
public function testBindRecursively() {
$exp = [
'one' => 1,
'two' => 2,
@ -78,14 +78,14 @@ class TestDbStatementSQLite3 extends Test\AbstractTest {
$this->assertSame($exp, $val);
}
function testBindWithoutType() {
public function testBindWithoutType() {
$nativeStatement = $this->c->prepare("SELECT ? as value");
$this->assertException("paramTypeMissing", "Db");
$s = new self::$imp($this->c, $nativeStatement, []);
$s->runArray([1]);
}
function testViolateConstraint() {
public function testViolateConstraint() {
$this->c->exec("CREATE TABLE test(id integer not null)");
$nativeStatement = $this->c->prepare("INSERT INTO test(id) values(?)");
$s = new self::$imp($this->c, $nativeStatement, ["int"]);
@ -93,11 +93,11 @@ class TestDbStatementSQLite3 extends Test\AbstractTest {
$s->runArray([null]);
}
function testMismatchTypes() {
public function testMismatchTypes() {
$this->c->exec("CREATE TABLE test(id integer primary key)");
$nativeStatement = $this->c->prepare("INSERT INTO test(id) values(?)");
$s = new self::$imp($this->c, $nativeStatement, ["str"]);
$this->assertException("typeViolation", "Db", "ExceptionInput");
$s->runArray(['ook']);
}
}
}

36
tests/Db/SQLite3/TestDbUpdateSQLite3.php

@ -1,10 +1,10 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStream;
/**
/**
* @covers \JKingWeb\Arsse\Db\SQLite3\Driver<extended>
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
class TestDbUpdateSQLite3 extends Test\AbstractTest {
@ -16,13 +16,13 @@ class TestDbUpdateSQLite3 extends Test\AbstractTest {
const MINIMAL1 = "create table arsse_meta(key text primary key not null, value text); pragma user_version=1";
const MINIMAL2 = "pragma user_version=2";
function setUp(Conf $conf = null) {
if(!extension_loaded("sqlite3")) {
public function setUp(Conf $conf = null) {
if (!extension_loaded("sqlite3")) {
$this->markTestSkipped("SQLite extension not loaded");
}
$this->clearData();
$this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]);
if(!$conf) {
if (!$conf) {
$conf = new Conf();
}
$conf->dbDriver = Db\SQLite3\Driver::class;
@ -33,68 +33,68 @@ class TestDbUpdateSQLite3 extends Test\AbstractTest {
$this->drv = new Db\SQLite3\Driver(true);
}
function tearDown() {
public function tearDown() {
unset($this->drv);
unset($this->data);
unset($this->vfs);
$this->clearData();
}
function testLoadMissingFile() {
public function testLoadMissingFile() {
$this->assertException("updateFileMissing", "Db");
$this->drv->schemaUpdate(1, $this->base);
}
function testLoadUnreadableFile() {
public function testLoadUnreadableFile() {
touch($this->path."0.sql");
chmod($this->path."0.sql", 0000);
$this->assertException("updateFileUnreadable", "Db");
$this->drv->schemaUpdate(1, $this->base);
}
function testLoadCorruptFile() {
public function testLoadCorruptFile() {
file_put_contents($this->path."0.sql", "This is a corrupt file");
$this->assertException("updateFileError", "Db");
$this->drv->schemaUpdate(1, $this->base);
}
function testLoadIncompleteFile() {
public function testLoadIncompleteFile() {
file_put_contents($this->path."0.sql", "create table arsse_meta(key text primary key not null, value text);");
$this->assertException("updateFileIncomplete", "Db");
$this->drv->schemaUpdate(1, $this->base);
}
function testLoadCorrectFile() {
public function testLoadCorrectFile() {
file_put_contents($this->path."0.sql", self::MINIMAL1);
$this->drv->schemaUpdate(1, $this->base);
$this->assertEquals(1, $this->drv->schemaVersion());
}
function testPerformPartialUpdate() {
public function testPerformPartialUpdate() {
file_put_contents($this->path."0.sql", self::MINIMAL1);
file_put_contents($this->path."1.sql", "");
$this->assertException("updateFileIncomplete", "Db");
try {
$this->drv->schemaUpdate(2, $this->base);
} catch(Exception $e) {
} catch (Exception $e) {
$this->assertEquals(1, $this->drv->schemaVersion());
throw $e;
}
}
function testPerformSequentialUpdate() {
public function testPerformSequentialUpdate() {
file_put_contents($this->path."0.sql", self::MINIMAL1);
file_put_contents($this->path."1.sql", self::MINIMAL2);
$this->drv->schemaUpdate(2, $this->base);
$this->assertEquals(2, $this->drv->schemaVersion());
}
function testPerformActualUpdate() {
public function testPerformActualUpdate() {
$this->drv->schemaUpdate(Database::SCHEMA_VERSION);
$this->assertEquals(Database::SCHEMA_VERSION, $this->drv->schemaVersion());
}
function testDeclineManualUpdate() {
public function testDeclineManualUpdate() {
// turn auto-updating off
$conf = new Conf();
$conf->dbAutoUpdate = false;
@ -103,8 +103,8 @@ class TestDbUpdateSQLite3 extends Test\AbstractTest {
$this->drv->schemaUpdate(Database::SCHEMA_VERSION);
}
function testDeclineDowngrade() {
public function testDeclineDowngrade() {
$this->assertException("updateTooNew", "Db");
$this->drv->schemaUpdate(-1, $this->base);
}
}
}

11
tests/Db/TestTransaction.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\Db\Transaction;
use Phake;
@ -9,7 +10,7 @@ use Phake;
class TestTransaction extends Test\AbstractTest {
protected $drv;
function setUp() {
public function setUp() {
$this->clearData();
$drv = Phake::mock(Db\SQLite3\Driver::class);
Phake::when($drv)->savepointRelease->thenReturn(true);
@ -18,7 +19,7 @@ class TestTransaction extends Test\AbstractTest {
$this->drv = $drv;
}
function testManipulateTransactions() {
public function testManipulateTransactions() {
$tr1 = new Transaction($this->drv);
$tr2 = new Transaction($this->drv);
Phake::verify($this->drv, Phake::times(2))->savepointCreate;
@ -30,7 +31,7 @@ class TestTransaction extends Test\AbstractTest {
Phake::verify($this->drv)->savepointUndo(2);
}
function testCloseTransactions() {
public function testCloseTransactions() {
$tr1 = new Transaction($this->drv);
$tr2 = new Transaction($this->drv);
$this->assertTrue($tr1->isPending());
@ -45,7 +46,7 @@ class TestTransaction extends Test\AbstractTest {
Phake::verify($this->drv)->savepointUndo(2);
}
function testIgnoreRollbackErrors() {
public function testIgnoreRollbackErrors() {
Phake::when($this->drv)->savepointUndo->thenThrow(new Db\ExceptionSavepoint("stale"));
$tr1 = new Transaction($this->drv);
$tr2 = new Transaction($this->drv);
@ -53,4 +54,4 @@ class TestTransaction extends Test\AbstractTest {
Phake::verify($this->drv)->savepointUndo(1);
Phake::verify($this->drv)->savepointUndo(2);
}
}
}

18
tests/Exception/TestException.php

@ -1,19 +1,19 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
Use Phake;
use Phake;
/** @covers \JKingWeb\Arsse\AbstractException */
class TestException extends Test\AbstractTest {
function setUp() {
public function setUp() {
$this->clearData(false);
// create a mock Lang object so as not to create a dependency loop
Arsse::$lang = Phake::mock(Lang::class);
Phake::when(Arsse::$lang)->msg->thenReturn("");
}
function tearDown() {
public function tearDown() {
// verify calls to the mock Lang object
Phake::verify(Arsse::$lang, Phake::atLeast(0))->msg($this->isType("string"), $this->anything());
Phake::verifyNoOtherInteractions(Arsse::$lang);
@ -21,7 +21,7 @@ class TestException extends Test\AbstractTest {
$this->clearData(true);
}
function testBaseClass() {
public function testBaseClass() {
$this->assertException("unknown");
throw new Exception("unknown");
}
@ -29,7 +29,7 @@ class TestException extends Test\AbstractTest {
/**
* @depends testBaseClass
*/
function testBaseClassWithoutMessage() {
public function testBaseClassWithoutMessage() {
$this->assertException("unknown");
throw new Exception();
}
@ -37,7 +37,7 @@ class TestException extends Test\AbstractTest {
/**
* @depends testBaseClass
*/
function testDerivedClass() {
public function testDerivedClass() {
$this->assertException("fileMissing", "Lang");
throw new Lang\Exception("fileMissing");
}
@ -45,7 +45,7 @@ class TestException extends Test\AbstractTest {
/**
* @depends testDerivedClass
*/
function testDerivedClassWithMessageParameters() {
public function testDerivedClassWithMessageParameters() {
$this->assertException("fileMissing", "Lang");
throw new Lang\Exception("fileMissing", "en");
}
@ -53,7 +53,7 @@ class TestException extends Test\AbstractTest {
/**
* @depends testBaseClass
*/
function testBaseClassWithUnknownCode() {
public function testBaseClassWithUnknownCode() {
$this->assertException("uncoded");
throw new Exception("testThisExceptionMessageDoesNotExist");
}
@ -61,7 +61,7 @@ class TestException extends Test\AbstractTest {
/**
* @depends testBaseClassWithUnknownCode
*/
function testDerivedClassWithMissingMessage() {
public function testDerivedClassWithMissingMessage() {
$this->assertException("uncoded");
throw new Lang\Exception("testThisExceptionMessageDoesNotExist");
}

44
tests/Feed/TestFeed.php

@ -1,10 +1,10 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\Misc\Date;
use Phake;
/**
* @covers \JKingWeb\Arsse\Feed
* @covers \JKingWeb\Arsse\Feed\Exception */
@ -80,8 +80,8 @@ class TestFeed extends Test\AbstractTest {
],
];
function setUp() {
if(!@file_get_contents(self::$host."IsUp")) {
public function setUp() {
if (!@file_get_contents(self::$host."IsUp")) {
$this->markTestSkipped("Test Web server is not accepting requests");
}
$this->base = self::$host."Feed/";
@ -90,7 +90,7 @@ class TestFeed extends Test\AbstractTest {
Arsse::$db = Phake::mock(Database::class);
}
function testParseAFeed() {
public function testParseAFeed() {
// test that various properties are set on the feed and on items
$f = new Feed(null, $this->base."Parsing/Valid");
$this->assertTrue(isset($f->lastModified));
@ -133,27 +133,27 @@ class TestFeed extends Test\AbstractTest {
$this->assertSame($categories, $f->data->items[5]->categories);
}
function testParseEntityExpansionAttack() {
public function testParseEntityExpansionAttack() {
$this->assertException("xmlEntity", "Feed");
new Feed(null, $this->base."Parsing/XEEAttack");
}
function testParseExternalEntityAttack() {
public function testParseExternalEntityAttack() {
$this->assertException("xmlEntity", "Feed");
new Feed(null, $this->base."Parsing/XXEAttack");
}
function testParseAnUnsupportedFeed() {
public function testParseAnUnsupportedFeed() {
$this->assertException("unsupportedFeedFormat", "Feed");
new Feed(null, $this->base."Parsing/Unsupported");
}
function testParseAMalformedFeed() {
public function testParseAMalformedFeed() {
$this->assertException("malformedXml", "Feed");
new Feed(null, $this->base."Parsing/Malformed");
}
function testDeduplicateFeedItems() {
public function testDeduplicateFeedItems() {
// duplicates with dates lead to the newest match being kept
$t = strtotime("2002-05-19T15:21:36Z");
$f = new Feed(null, $this->base."Deduplication/Permalink-Dates");
@ -180,7 +180,7 @@ class TestFeed extends Test\AbstractTest {
$this->assertSame("http://example.com/1", $f->newItems[0]->url);
}
function testHandleCacheHeadersOn304() {
public function testHandleCacheHeadersOn304() {
// upon 304, the client should re-use the caching header values it supplied the server
$t = time();
$e = "78567a";
@ -198,7 +198,7 @@ class TestFeed extends Test\AbstractTest {
$this->assertSame($e, $f->resource->getETag());
}
function testHandleCacheHeadersOn200() {
public function testHandleCacheHeadersOn200() {
// these tests should trust the server-returned time, even in cases of obviously incorrect results
$t = time() - 2000;
$f = new Feed(null, $this->base."Caching/200Past");
@ -226,11 +226,11 @@ class TestFeed extends Test\AbstractTest {
$this->assertTime($t, $f->lastModified);
}
function testComputeNextFetchOnError() {
for($a = 0; $a < 100; $a++) {
if($a < 3) {
public function testComputeNextFetchOnError() {
for ($a = 0; $a < 100; $a++) {
if ($a < 3) {
$this->assertTime("now + 5 minutes", Feed::nextFetchOnError($a));
} else if($a < 15) {
} elseif ($a < 15) {
$this->assertTime("now + 3 hours", Feed::nextFetchOnError($a));
} else {
$this->assertTime("now + 1 day", Feed::nextFetchOnError($a));
@ -238,7 +238,7 @@ class TestFeed extends Test\AbstractTest {
}
}
function testComputeNextFetchFrom304() {
public function testComputeNextFetchFrom304() {
// if less than half an hour, check in 15 minutes
$t = strtotime("now");
$f = new Feed(null, $this->base."NextFetch/NotModified?t=$t", Date::transform($t, "http"));
@ -286,7 +286,7 @@ class TestFeed extends Test\AbstractTest {
$this->assertTime($exp, $f->nextFetch);
}
function testComputeNextFetchFrom200() {
public function testComputeNextFetchFrom200() {
// if less than half an hour, check in 15 minutes
$f = new Feed(null, $this->base."NextFetch/30m");
$exp = strtotime("now + 15 minutes");
@ -313,7 +313,7 @@ class TestFeed extends Test\AbstractTest {
$this->assertTime($exp, $f->nextFetch);
}
function testMatchLatestArticles() {
public function testMatchLatestArticles() {
Phake::when(Arsse::$db)->feedMatchLatest(1, $this->anything())->thenReturn(new Test\Result($this->latest));
$f = new Feed(1, $this->base."Matching/1");
$this->assertCount(0, $f->newItems);
@ -329,15 +329,15 @@ class TestFeed extends Test\AbstractTest {
$this->assertCount(2, $f->changedItems);
}
function testMatchHistoricalArticles() {
public function testMatchHistoricalArticles() {
Phake::when(Arsse::$db)->feedMatchLatest(1, $this->anything())->thenReturn(new Test\Result($this->latest));
Phake::when(Arsse::$db)->feedMatchIds(1, $this->anything(), $this->anything(), $this->anything(), $this->anything())->thenReturn(new Test\Result($this->others));
$f = new Feed(1, $this->base."Matching/5");
$this->assertCount(0, $f->newItems);
$this->assertCount(0, $f->changedItems);
$this->assertCount(0, $f->changedItems);
}
function testScrapeFullContent() {
public function testScrapeFullContent() {
// first make sure that the absence of scraping works as expected
$f = new Feed(null, $this->base."Scraping/Feed");
$exp = "<p>Partial content</p>";
@ -347,4 +347,4 @@ class TestFeed extends Test\AbstractTest {
$exp = "<p>Partial content, followed by more content</p>";
$this->assertSame($exp, $f->newItems[0]->content);
}
}
}

28
tests/Feed/TestFeedFetching.php

@ -1,18 +1,18 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
Use Phake;
use Phake;
/** @covers \JKingWeb\Arsse\Feed */
class TestFeedFetching extends Test\AbstractTest {
protected static $host = "http://localhost:8000/";
protected $base = "";
function setUp() {
if(!extension_loaded('curl')) {
public function setUp() {
if (!extension_loaded('curl')) {
$this->markTestSkipped("Feed fetching tests are only accurate with curl enabled.");
} else if(!@file_get_contents(self::$host."IsUp")) {
} elseif (!@file_get_contents(self::$host."IsUp")) {
$this->markTestSkipped("Test Web server is not accepting requests");
}
$this->base = self::$host."Feed/";
@ -20,50 +20,50 @@ class TestFeedFetching extends Test\AbstractTest {
Arsse::$conf = new Conf();
}
function testHandle400() {
public function testHandle400() {
$this->assertException("unsupportedFeedFormat", "Feed");
new Feed(null, $this->base."Fetching/Error?code=400");
}
function testHandle401() {
public function testHandle401() {
$this->assertException("unauthorized", "Feed");
new Feed(null, $this->base."Fetching/Error?code=401");
}
function testHandle403() {
public function testHandle403() {
$this->assertException("forbidden", "Feed");
new Feed(null, $this->base."Fetching/Error?code=403");
}
function testHandle404() {
public function testHandle404() {
$this->assertException("invalidUrl", "Feed");
new Feed(null, $this->base."Fetching/Error?code=404");
}
function testHandle500() {
public function testHandle500() {
$this->assertException("unsupportedFeedFormat", "Feed");
new Feed(null, $this->base."Fetching/Error?code=500");
}
function testHandleARedirectLoop() {
public function testHandleARedirectLoop() {
$this->assertException("maxRedirect", "Feed");
new Feed(null, $this->base."Fetching/EndlessLoop?i=0");
}
function testHandleATimeout() {
public function testHandleATimeout() {
Arsse::$conf->fetchTimeout = 1;
$this->assertException("timeout", "Feed");
new Feed(null, $this->base."Fetching/Timeout");
}
function testHandleAnOverlyLargeFeed() {
public function testHandleAnOverlyLargeFeed() {
Arsse::$conf->fetchSizeLimit = 512;
$this->assertException("maxSize", "Feed");
new Feed(null, $this->base."Fetching/TooLarge");
}
function testHandleACertificateError() {
public function testHandleACertificateError() {
$this->assertException("invalidCertificate", "Feed");
new Feed(null, "https://localhost:8000/");
}
}
}

14
tests/Lang/TestLang.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use org\bovigo\vfs\vfsStream;
/** @covers \JKingWeb\Arsse\Lang */
@ -11,14 +12,14 @@ class TestLang extends Test\AbstractTest {
public $path;
public $l;
function testListLanguages() {
public function testListLanguages() {
$this->assertCount(sizeof($this->files), $this->l->list("en"));
}
/**
* @depends testListLanguages
*/
function testSetLanguage() {
public function testSetLanguage() {
$this->assertEquals("en", $this->l->set("en"));
$this->assertEquals("en_ca", $this->l->set("en_ca"));
$this->assertEquals("de", $this->l->set("de_ch"));
@ -31,7 +32,7 @@ class TestLang extends Test\AbstractTest {
/**
* @depends testSetLanguage
*/
function testLoadInternalStrings() {
public function testLoadInternalStrings() {
$this->assertEquals("", $this->l->set("", true));
$this->assertCount(sizeof(Lang::REQUIRED), $this->l->dump());
}
@ -39,7 +40,7 @@ class TestLang extends Test\AbstractTest {
/**
* @depends testLoadInternalStrings
*/
function testLoadDefaultLanguage() {
public function testLoadDefaultLanguage() {
$this->assertEquals(Lang::DEFAULT, $this->l->set(Lang::DEFAULT, true));
$str = $this->l->dump();
$this->assertArrayHasKey('Exception.JKingWeb/Arsse/Exception.uncoded', $str);
@ -49,7 +50,7 @@ class TestLang extends Test\AbstractTest {
/**
* @depends testLoadDefaultLanguage
*/
function testLoadSupplementaryLanguage() {
public function testLoadSupplementaryLanguage() {
$this->l->set(Lang::DEFAULT, true);
$this->assertEquals("ja", $this->l->set("ja", true));
$str = $this->l->dump();
@ -57,5 +58,4 @@ class TestLang extends Test\AbstractTest {
$this->assertArrayHasKey('Test.presentText', $str);
$this->assertArrayHasKey('Test.absentText', $str);
}
}
}

23
tests/Lang/TestLangErrors.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use org\bovigo\vfs\vfsStream;
/** @covers \JKingWeb\Arsse\Lang */
@ -11,54 +12,54 @@ class TestLangErrors extends Test\AbstractTest {
public $path;
public $l;
function setUpSeries() {
public function setUpSeries() {
$this->l->set("", true);
}
function testLoadEmptyFile() {
public function testLoadEmptyFile() {
$this->assertException("fileCorrupt", "Lang");
$this->l->set("fr_ca", true);
}
function testLoadFileWhichDoesNotReturnAnArray() {
public function testLoadFileWhichDoesNotReturnAnArray() {
$this->assertException("fileCorrupt", "Lang");
$this->l->set("it", true);
}
function testLoadFileWhichIsNotPhp() {
public function testLoadFileWhichIsNotPhp() {
$this->assertException("fileCorrupt", "Lang");
$this->l->set("ko", true);
}
function testLoadFileWhichIsCorrupt() {
public function testLoadFileWhichIsCorrupt() {
$this->assertException("fileCorrupt", "Lang");
$this->l->set("zh", true);
}
function testLoadFileWithooutReadPermission() {
public function testLoadFileWithooutReadPermission() {
$this->assertException("fileUnreadable", "Lang");
$this->l->set("ru", true);
}
function testLoadSubtagOfMissingLanguage() {
public function testLoadSubtagOfMissingLanguage() {
$this->assertException("fileMissing", "Lang");
$this->l->set("pt_br", true);
}
function testFetchInvalidMessage() {
public function testFetchInvalidMessage() {
$this->assertException("stringInvalid", "Lang");
$this->l->set("vi", true);
$txt = $this->l->msg('Test.presentText');
}
function testFetchMissingMessage() {
public function testFetchMissingMessage() {
$this->assertException("stringMissing", "Lang");
$txt = $this->l->msg('Test.absentText');
}
function testLoadMissingDefaultLanguage() {
public function testLoadMissingDefaultLanguage() {
unlink($this->path.Lang::DEFAULT.".php");
$this->assertException("defaultFileMissing", "Lang");
$this->l->set("fr", true);
}
}
}

27
tests/Lang/testLangComplex.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use org\bovigo\vfs\vfsStream;
/** @covers \JKingWeb\Arsse\Lang */
@ -11,11 +12,11 @@ class TestLangComplex extends Test\AbstractTest {
public $path;
public $l;
function setUpSeries() {
public function setUpSeries() {
$this->l->set(Lang::DEFAULT, true);
}
function testLazyLoad() {
public function testLazyLoad() {
$this->l->set("ja");
$this->assertArrayNotHasKey('Test.absentText', $this->l->dump());
}
@ -23,14 +24,14 @@ class TestLangComplex extends Test\AbstractTest {
/**
* @depends testLazyLoad
*/
function testGetWantedAndLoadedLocale() {
public function testGetWantedAndLoadedLocale() {
$this->l->set("en", true);
$this->l->set("ja");
$this->assertEquals("ja", $this->l->get());
$this->assertEquals("en", $this->l->get(true));
}
function testLoadCascadeOfFiles() {
public function testLoadCascadeOfFiles() {
$this->l->set("ja", true);
$this->assertEquals("de", $this->l->set("de", true));
$str = $this->l->dump();
@ -41,11 +42,11 @@ class TestLangComplex extends Test\AbstractTest {
/**
* @depends testLoadCascadeOfFiles
*/
function testLoadSubtag() {
public function testLoadSubtag() {
$this->assertEquals("en_ca", $this->l->set("en_ca", true));
}
function testFetchAMessage() {
public function testFetchAMessage() {
$this->l->set("de", true);
$this->assertEquals('und der Stein der Weisen', $this->l->msg('Test.presentText'));
}
@ -53,7 +54,7 @@ class TestLangComplex extends Test\AbstractTest {
/**
* @depends testFetchAMessage
*/
function testFetchAMessageWithMissingParameters() {
public function testFetchAMessageWithMissingParameters() {
$this->l->set("en_ca", true);
$this->assertEquals('{0} and {1}', $this->l->msg('Test.presentText'));
}
@ -61,7 +62,7 @@ class TestLangComplex extends Test\AbstractTest {
/**
* @depends testFetchAMessage
*/
function testFetchAMessageWithSingleNumericParameter() {
public function testFetchAMessageWithSingleNumericParameter() {
$this->l->set("en_ca", true);
$this->assertEquals('Default language file "en" missing', $this->l->msg('Exception.JKingWeb/Arsse/Lang/Exception.defaultFileMissing', Lang::DEFAULT));
}
@ -69,7 +70,7 @@ class TestLangComplex extends Test\AbstractTest {
/**
* @depends testFetchAMessage
*/
function testFetchAMessageWithMultipleNumericParameters() {
public function testFetchAMessageWithMultipleNumericParameters() {
$this->l->set("en_ca", true);
$this->assertEquals('Happy Rotter and the Philosopher\'s Stone', $this->l->msg('Test.presentText', ['Happy Rotter', 'the Philosopher\'s Stone']));
}
@ -77,14 +78,14 @@ class TestLangComplex extends Test\AbstractTest {
/**
* @depends testFetchAMessage
*/
function testFetchAMessageWithNamedParameters() {
public function testFetchAMessageWithNamedParameters() {
$this->assertEquals('Message string "Test.absentText" missing from all loaded language files (en)', $this->l->msg('Exception.JKingWeb/Arsse/Lang/Exception.stringMissing', ['msgID' => 'Test.absentText', 'fileList' => 'en']));
}
/**
* @depends testFetchAMessage
*/
function testReloadDefaultStrings() {
public function testReloadDefaultStrings() {
$this->l->set("de", true);
$this->l->set("en", true);
$this->assertEquals('and the Philosopher\'s Stone', $this->l->msg('Test.presentText'));
@ -93,11 +94,11 @@ class TestLangComplex extends Test\AbstractTest {
/**
* @depends testFetchAMessage
*/
function testReloadGeneralTagAfterSubtag() {
public function testReloadGeneralTagAfterSubtag() {
$this->l->set("en", true);
$this->l->set("en_us", true);
$this->assertEquals('and the Sorcerer\'s Stone', $this->l->msg('Test.presentText'));
$this->l->set("en", true);
$this->assertEquals('and the Philosopher\'s Stone', $this->l->msg('Test.presentText'));
}
}
}

22
tests/Misc/TestContext.php

@ -1,15 +1,15 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\Misc\Context;
use JKingWeb\Arsse\Misc\Context;
/** @covers \JKingWeb\Arsse\Misc\Context */
class TestContext extends Test\AbstractTest {
function testVerifyInitialState() {
public function testVerifyInitialState() {
$c = new Context;
foreach((new \ReflectionObject($c))->getMethods(\ReflectionMethod::IS_PUBLIC) as $m) {
if($m->isConstructor() || $m->isStatic()) {
foreach ((new \ReflectionObject($c))->getMethods(\ReflectionMethod::IS_PUBLIC) as $m) {
if ($m->isConstructor() || $m->isStatic()) {
continue;
}
$method = $m->name;
@ -18,7 +18,7 @@ class TestContext extends Test\AbstractTest {
}
}
function testSetContextOptions() {
public function testSetContextOptions() {
$v = [
'reverse' => true,
'limit' => 10,
@ -38,15 +38,15 @@ class TestContext extends Test\AbstractTest {
];
$times = ['modifiedSince','notModifiedSince'];
$c = new Context;
foreach((new \ReflectionObject($c))->getMethods(\ReflectionMethod::IS_PUBLIC) as $m) {
if($m->isConstructor() || $m->isStatic()) {
foreach ((new \ReflectionObject($c))->getMethods(\ReflectionMethod::IS_PUBLIC) as $m) {
if ($m->isConstructor() || $m->isStatic()) {
continue;
}
$method = $m->name;
$this->assertArrayHasKey($method, $v, "Context method $method not included in test");
$this->assertInstanceOf(Context::class, $c->$method($v[$method]));
$this->assertTrue($c->$method());
if(in_array($method, $times)) {
if (in_array($method, $times)) {
$this->assertTime($c->$method, $v[$method]);
} else {
$this->assertSame($c->$method, $v[$method]);
@ -54,13 +54,13 @@ class TestContext extends Test\AbstractTest {
}
}
function testCleanArrayValues() {
public function testCleanArrayValues() {
$methods = ["articles", "editions"];
$in = [1, "2", 3.5, 3.0, "ook", 0, -20, true, false, null, new \DateTime(), -1.0];
$out = [1,2, 3];
$c = new Context;
foreach($methods as $method) {
foreach ($methods as $method) {
$this->assertSame($out, $c->$method($in)->$method, "Context method $method did not return the expected results");
}
}
}
}

85
tests/REST/NextCloudNews/TestNCNV1_2.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\REST\Request;
use JKingWeb\Arsse\REST\Response;
use JKingWeb\Arsse\Test\Result;
@ -259,7 +260,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
],
];
function setUp() {
public function setUp() {
$this->clearData();
Arsse::$conf = new Conf();
// create a mock user manager
@ -268,16 +269,16 @@ class TestNCNV1_2 extends Test\AbstractTest {
Phake::when(Arsse::$user)->rightsGet->thenReturn(100);
Arsse::$user->id = "john.doe@example.com";
// create a mock database interface
Arsse::$db = Phake::mock(Database::Class);
Arsse::$db = Phake::mock(Database::class);
Phake::when(Arsse::$db)->begin->thenReturn(Phake::mock(Transaction::class));
$this->h = new REST\NextCloudNews\V1_2();
}
function tearDown() {
public function tearDown() {
$this->clearData();
}
function testRespondToInvalidPaths() {
public function testRespondToInvalidPaths() {
$errs = [
501 => [
['GET', "/"],
@ -309,34 +310,34 @@ class TestNCNV1_2 extends Test\AbstractTest {
],
],
];
foreach($errs[501] as $req) {
foreach ($errs[501] as $req) {
$exp = new Response(501);
list($method, $path) = $req;
$this->assertEquals($exp, $this->h->dispatch(new Request($method, $path)), "$method call to $path did not return 501.");
}
foreach($errs[405] as $allow => $cases) {
foreach ($errs[405] as $allow => $cases) {
$exp = new Response(405, "", "", ['Allow: '.$allow]);
foreach($cases as $req) {
foreach ($cases as $req) {
list($method, $path) = $req;
$this->assertEquals($exp, $this->h->dispatch(new Request($method, $path)), "$method call to $path did not return 405.");
}
}
}
function testRespondToInvalidInputTypes() {
public function testRespondToInvalidInputTypes() {
$exp = new Response(415, "", "", ['Accept: application/json']);
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/1", '<data/>', 'application/xml')));
$exp = new Response(400);
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/1", '<data/>', 'application/json')));
}
function testReceiveAuthenticationChallenge() {
public function testReceiveAuthenticationChallenge() {
Phake::when(Arsse::$user)->authHTTP->thenReturn(false);
$exp = new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.REST\NextCloudNews\V1_2::REALM.'"']);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/")));
}
function testListFolders() {
public function testListFolders() {
$list = [
['id' => 1, 'name' => "Software", 'parent' => null],
['id' => 12, 'name' => "Hardware", 'parent' => null],
@ -348,7 +349,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/folders")));
}
function testAddAFolder() {
public function testAddAFolder() {
$in = [
["name" => "Software"],
["name" => "Hardware"],
@ -387,7 +388,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/folders", json_encode($in[1]), 'application/json')));
}
function testRemoveAFolder() {
public function testRemoveAFolder() {
Phake::when(Arsse::$db)->folderRemove(Arsse::$user->id, 1)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing"));
$exp = new Response(204);
$this->assertEquals($exp, $this->h->dispatch(new Request("DELETE", "/folders/1")));
@ -397,7 +398,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
Phake::verify(Arsse::$db, Phake::times(2))->folderRemove(Arsse::$user->id, 1);
}
function testRenameAFolder() {
public function testRenameAFolder() {
$in = [
["name" => "Software"],
["name" => "Software"],
@ -425,7 +426,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/3", json_encode($in[0]), 'application/json')));
}
function testRetrieveServerVersion() {
public function testRetrieveServerVersion() {
$exp = new Response(200, [
'arsse_version' => \JKingWeb\Arsse\VERSION,
'version' => REST\NextCloudNews\V1_2::VERSION,
@ -433,7 +434,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/version")));
}
function testListSubscriptions() {
public function testListSubscriptions() {
$exp1 = [
'feeds' => [],
'starredCount' => 0,
@ -452,7 +453,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds")));
}
function testAddASubscription() {
public function testAddASubscription() {
$in = [
['url' => "http://example.com/news.atom", 'folderId' => 3],
['url' => "http://example.org/news.atom", 'folderId' => 8],
@ -467,13 +468,13 @@ class TestNCNV1_2 extends Test\AbstractTest {
];
// set up the necessary mocks
Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.com/news.atom")->thenReturn(2112)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call
Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.org/news.atom")->thenReturn( 42 )->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call
Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.org/news.atom")->thenReturn(42)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call
Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 2112)->thenReturn($this->feeds['db'][0]);
Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 42)->thenReturn($this->feeds['db'][1]);
Phake::when(Arsse::$db)->subscriptionPropertiesGet(Arsse::$user->id, 42)->thenReturn($this->feeds['db'][1]);
Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id, (new Context)->subscription(2112))->thenReturn(0);
Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id, (new Context)->subscription( 42))->thenReturn(4758915);
Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id, (new Context)->subscription(42))->thenReturn(4758915);
Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 2112, ['folder' => 3])->thenThrow(new ExceptionInput("idMissing")); // folder ID 3 does not exist
Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 42, ['folder' => 8])->thenReturn(true);
Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 42, ['folder' => 8])->thenReturn(true);
// set up a mock for a bad feed
Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.net/news.atom")->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.net/news.atom", new \PicoFeed\Client\InvalidUrlException()));
// add the subscriptions
@ -494,7 +495,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("POST", "/feeds", json_encode($in[3]), 'application/json')));
}
function testRemoveASubscription() {
public function testRemoveASubscription() {
Phake::when(Arsse::$db)->subscriptionRemove(Arsse::$user->id, 1)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing"));
$exp = new Response(204);
$this->assertEquals($exp, $this->h->dispatch(new Request("DELETE", "/feeds/1")));
@ -504,7 +505,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
Phake::verify(Arsse::$db, Phake::times(2))->subscriptionRemove(Arsse::$user->id, 1);
}
function testMoveASubscription() {
public function testMoveASubscription() {
$in = [
['folderId' => 0],
['folderId' => 42],
@ -528,7 +529,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[4]), 'application/json')));
}
function testRenameASubscription() {
public function testRenameASubscription() {
$in = [
['feedTitle' => null],
['feedTitle' => "Ook"],
@ -558,7 +559,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[6]), 'application/json')));
}
function testListStaleFeeds() {
public function testListStaleFeeds() {
$out = [
[
'id' => 42,
@ -569,7 +570,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
'userId' => "",
],
];
Phake::when(Arsse::$db)->feedListStale->thenReturn(array_column($out,"id"));
Phake::when(Arsse::$db)->feedListStale->thenReturn(array_column($out, "id"));
$exp = new Response(200, ['feeds' => $out]);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/all")));
// retrieving the list when not an admin fails
@ -578,14 +579,14 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/all")));
}
function testUpdateAFeed() {
public function testUpdateAFeed() {
$in = [
['feedId' => 42], // valid
['feedId' => 2112], // feed does not exist
['feedId' => "ook"], // invalid ID
['feed' => 42], // invalid input
];
Phake::when(Arsse::$db)->feedUpdate( 42)->thenReturn(true);
Phake::when(Arsse::$db)->feedUpdate(42)->thenReturn(true);
Phake::when(Arsse::$db)->feedUpdate(2112)->thenThrow(new ExceptionInput("subjectMissing"));
$exp = new Response(204);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json')));
@ -601,7 +602,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json')));
}
function testListArticles() {
public function testListArticles() {
$res = new Result($this->articles['db']);
$t = new \DateTime;
$in = [
@ -648,7 +649,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5));
}
function testMarkAFolderRead() {
public function testMarkAFolderRead() {
$read = ['read' => true];
$in = json_encode(['newestItemId' => 2112]);
Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->folder(1)->latestEdition(2112))->thenReturn(true);
@ -663,7 +664,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/42/read", $in, 'application/json')));
}
function testMarkASubscriptionRead() {
public function testMarkASubscriptionRead() {
$read = ['read' => true];
$in = json_encode(['newestItemId' => 2112]);
Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->subscription(1)->latestEdition(2112))->thenReturn(true);
@ -678,7 +679,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/42/read", $in, 'application/json')));
}
function testMarkAllItemsRead() {
public function testMarkAllItemsRead() {
$read = ['read' => true];
$in = json_encode(['newestItemId' => 2112]);
Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->latestEdition(2112))->thenReturn(true);
@ -690,7 +691,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read?newestItemId=ook")));
}
function testChangeMarksOfASingleArticle() {
public function testChangeMarksOfASingleArticle() {
$read = ['read' => true];
$unread = ['read' => false];
$star = ['starred' => true];
@ -716,20 +717,20 @@ class TestNCNV1_2 extends Test\AbstractTest {
Phake::verify(Arsse::$db, Phake::times(8))->articleMark(Arsse::$user->id, $this->anything(), $this->anything());
}
function testChangeMarksOfMultipleArticles() {
public function testChangeMarksOfMultipleArticles() {
$read = ['read' => true];
$unread = ['read' => false];
$star = ['starred' => true];
$unstar = ['starred' => false];
$in = [
["ook","eek","ack"],
range(100,199),
range(100,149),
range(150,199),
range(100, 199),
range(100, 149),
range(150, 199),
];
$inStar = $in;
for($a = 0; $a < sizeof($inStar); $a++) {
for($b = 0; $b < sizeof($inStar[$a]); $b++) {
for ($a = 0; $a < sizeof($inStar); $a++) {
for ($b = 0; $b < sizeof($inStar[$a]); $b++) {
$inStar[$a][$b] = ['feedId' => 2112, 'guidHash' => $inStar[$a][$b]];
}
}
@ -783,7 +784,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, $unstar, (new Context)->articles($in[3]));
}
function testQueryTheServerStatus() {
public function testQueryTheServerStatus() {
$interval = Service::interval();
$valid = (new \DateTimeImmutable("now", new \DateTimezone("UTC")))->sub($interval);
$invalid = $valid->sub($interval)->sub($interval);
@ -800,7 +801,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/status")));
}
function testCleanUpBeforeUpdate() {
public function testCleanUpBeforeUpdate() {
Phake::when(Arsse::$db)->feedCleanup()->thenReturn(true);
$exp = new Response(204);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/cleanup/before-update")));
@ -811,7 +812,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/cleanup/before-update")));
}
function testCleanUpAfterUpdate() {
public function testCleanUpAfterUpdate() {
Phake::when(Arsse::$db)->articleCleanup()->thenReturn(true);
$exp = new Response(204);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/cleanup/after-update")));
@ -821,4 +822,4 @@ class TestNCNV1_2 extends Test\AbstractTest {
$exp = new Response(403);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/cleanup/after-update")));
}
}
}

11
tests/REST/NextCloudNews/TestNCNVersionDiscovery.php

@ -1,16 +1,17 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\REST\Request;
use JKingWeb\Arsse\REST\Response;
/** @covers \JKingWeb\Arsse\REST\NextCloudNews\Versions */
class TestNCNVersionDiscovery extends Test\AbstractTest {
function setUp() {
public function setUp() {
$this->clearData();
}
function testFetchVersionList() {
public function testFetchVersionList() {
$exp = new Response(200, ['apiLevels' => ['v1-2']]);
$h = new REST\NextCloudNews\Versions();
$req = new Request("GET", "/");
@ -24,7 +25,7 @@ class TestNCNVersionDiscovery extends Test\AbstractTest {
$this->assertEquals($exp, $res);
}
function testUseIncorrectMethod() {
public function testUseIncorrectMethod() {
$exp = new Response(405);
$h = new REST\NextCloudNews\Versions();
$req = new Request("POST", "/");
@ -32,11 +33,11 @@ class TestNCNVersionDiscovery extends Test\AbstractTest {
$this->assertEquals($exp, $res);
}
function testUseIncorrectPath() {
public function testUseIncorrectPath() {
$exp = new Response(501);
$h = new REST\NextCloudNews\Versions();
$req = new Request("GET", "/ook");
$res = $h->dispatch($req);
$this->assertEquals($exp, $res);
}
}
}

19
tests/Service/TestService.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\Misc\Date;
use Phake;
@ -8,14 +9,14 @@ use Phake;
class TestService extends Test\AbstractTest {
protected $srv;
function setUp() {
public function setUp() {
$this->clearData();
Arsse::$conf = new Conf();
Arsse::$db = Phake::mock(Database::class);
$this->srv = new Service();
}
function testComputeInterval() {
public function testComputeInterval() {
$in = [
Arsse::$conf->serviceFrequency,
"PT2M",
@ -24,21 +25,25 @@ class TestService extends Test\AbstractTest {
"5M",
"interval",
];
foreach($in as $index => $spec) {
try{$exp = new \DateInterval($spec);} catch(\Exception $e) {$exp = new \DateInterval("PT2M");}
foreach ($in as $index => $spec) {
try {
$exp = new \DateInterval($spec);
} catch (\Exception $e) {
$exp = new \DateInterval("PT2M");
}
Arsse::$conf->serviceFrequency = $spec;
$this->assertEquals($exp, Service::interval(), "Interval #$index '$spec' was not correctly calculated");
}
}
function testCheckIn() {
public function testCheckIn() {
$now = time();
$this->srv->checkIn();
Phake::verify(Arsse::$db)->metaSet("service_last_checkin", Phake::capture($then), "datetime");
$this->assertTime($now, $then);
}
function testReportHavingCheckedIn() {
public function testReportHavingCheckedIn() {
// the mock's metaGet() returns null by default
$this->assertFalse(Service::hasCheckedIn());
$interval = Service::interval();
@ -48,4 +53,4 @@ class TestService extends Test\AbstractTest {
$this->assertTrue(Service::hasCheckedIn());
$this->assertFalse(Service::hasCheckedIn());
}
}
}

137
tests/User/TestAuthorization.php

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use Phake;
/** @covers \JKingWeb\Arsse\User */
@ -44,29 +45,29 @@ class TestAuthorization extends Test\AbstractTest {
protected $data;
function setUp(string $drv = Test\User\DriverInternalMock::class, string $db = null) {
public function setUp(string $drv = Test\User\DriverInternalMock::class, string $db = null) {
$this->clearData();
$conf = new Conf();
$conf->userDriver = $drv;
$conf->userPreAuth = false;
Arsse::$conf = $conf;
if($db !== null) {
if ($db !== null) {
Arsse::$db = new $db();
}
Arsse::$user = Phake::partialMock(User::class);
Phake::when(Arsse::$user)->authorize->thenReturn(true);
foreach(self::USERS as $user => $level) {
foreach (self::USERS as $user => $level) {
Arsse::$user->add($user, "");
Arsse::$user->rightsSet($user, $level);
}
Phake::reset(Arsse::$user);
}
function tearDown() {
public function tearDown() {
$this->clearData();
}
function testToggleLogic() {
public function testToggleLogic() {
$this->assertTrue(Arsse::$user->authorizationEnabled());
$this->assertTrue(Arsse::$user->authorizationEnabled(true));
$this->assertFalse(Arsse::$user->authorizationEnabled(false));
@ -75,8 +76,8 @@ class TestAuthorization extends Test\AbstractTest {
$this->assertTrue(Arsse::$user->authorizationEnabled(true));
}
function testSelfActionLogic() {
foreach(array_keys(self::USERS) as $user) {
public function testSelfActionLogic() {
foreach (array_keys(self::USERS) as $user) {
Arsse::$user->auth($user, "");
// users should be able to do basic actions for themselves
$this->assertTrue(Arsse::$user->authorize($user, "userExists"), "User $user could not act for themselves.");
@ -84,15 +85,15 @@ class TestAuthorization extends Test\AbstractTest {
}
}
function testRegularUserLogic() {
foreach(self::USERS as $actor => $rights) {
if($rights != User\Driver::RIGHTS_NONE) {
public function testRegularUserLogic() {
foreach (self::USERS as $actor => $rights) {
if ($rights != User\Driver::RIGHTS_NONE) {
continue;
}
Arsse::$user->auth($actor, "");
foreach(array_keys(self::USERS) as $affected) {
foreach (array_keys(self::USERS) as $affected) {
// regular users should only be able to act for themselves
if($actor==$affected) {
if ($actor==$affected) {
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
@ -100,41 +101,41 @@ class TestAuthorization extends Test\AbstractTest {
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should never be able to set rights
foreach(self::LEVELS as $level) {
foreach (self::LEVELS as $level) {
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
}
}
// they should not be able to list users
foreach(self::DOMAINS as $domain) {
foreach (self::DOMAINS as $domain) {
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
}
}
}
function testDomainManagerLogic() {
foreach(self::USERS as $actor => $actorRights) {
if($actorRights != User\Driver::RIGHTS_DOMAIN_MANAGER) {
public function testDomainManagerLogic() {
foreach (self::USERS as $actor => $actorRights) {
if ($actorRights != User\Driver::RIGHTS_DOMAIN_MANAGER) {
continue;
}
$actorDomain = substr($actor,strrpos($actor,"@")+1);
$actorDomain = substr($actor, strrpos($actor, "@")+1);
Arsse::$user->auth($actor, "");
foreach(self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected,strrpos($affected,"@")+1);
foreach (self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected, strrpos($affected, "@")+1);
// domain managers should be able to check any user on the same domain
if($actorDomain==$affectedDomain) {
if ($actorDomain==$affectedDomain) {
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should only be able to act for regular users on the same domain
if($actor==$affected || ($actorDomain==$affectedDomain && $affectedRights==User\Driver::RIGHTS_NONE)) {
if ($actor==$affected || ($actorDomain==$affectedDomain && $affectedRights==User\Driver::RIGHTS_NONE)) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// and they should only be able to set their own rights to regular user
foreach(self::LEVELS as $level) {
if($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_DOMAIN_MANAGER])) {
foreach (self::LEVELS as $level) {
if ($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_DOMAIN_MANAGER])) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
@ -142,8 +143,8 @@ class TestAuthorization extends Test\AbstractTest {
}
}
// they should also be able to list all users on their own domain
foreach(self::DOMAINS as $domain) {
if($domain=="@".$actorDomain) {
foreach (self::DOMAINS as $domain) {
if ($domain=="@".$actorDomain) {
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
@ -152,31 +153,31 @@ class TestAuthorization extends Test\AbstractTest {
}
}
function testDomainAdministratorLogic() {
foreach(self::USERS as $actor => $actorRights) {
if($actorRights != User\Driver::RIGHTS_DOMAIN_ADMIN) {
public function testDomainAdministratorLogic() {
foreach (self::USERS as $actor => $actorRights) {
if ($actorRights != User\Driver::RIGHTS_DOMAIN_ADMIN) {
continue;
}
$actorDomain = substr($actor,strrpos($actor,"@")+1);
$actorDomain = substr($actor, strrpos($actor, "@")+1);
Arsse::$user->auth($actor, "");
$allowed = [User\Driver::RIGHTS_NONE,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN];
foreach(self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected,strrpos($affected,"@")+1);
foreach (self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected, strrpos($affected, "@")+1);
// domain admins should be able to check any user on the same domain
if($actorDomain==$affectedDomain) {
if ($actorDomain==$affectedDomain) {
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should be able to act for any user on the same domain who is not a global manager or admin
if($actorDomain==$affectedDomain && in_array($affectedRights, $allowed)) {
if ($actorDomain==$affectedDomain && in_array($affectedRights, $allowed)) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should be able to set rights for any user on their domain who is not a global manager or admin, up to domain admin level
foreach(self::LEVELS as $level) {
if($actorDomain==$affectedDomain && in_array($affectedRights, $allowed) && in_array($level, $allowed)) {
foreach (self::LEVELS as $level) {
if ($actorDomain==$affectedDomain && in_array($affectedRights, $allowed) && in_array($level, $allowed)) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
@ -184,8 +185,8 @@ class TestAuthorization extends Test\AbstractTest {
}
}
// they should also be able to list all users on their own domain
foreach(self::DOMAINS as $domain) {
if($domain=="@".$actorDomain) {
foreach (self::DOMAINS as $domain) {
if ($domain=="@".$actorDomain) {
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
@ -194,26 +195,26 @@ class TestAuthorization extends Test\AbstractTest {
}
}
function testGlobalManagerLogic() {
foreach(self::USERS as $actor => $actorRights) {
if($actorRights != User\Driver::RIGHTS_GLOBAL_MANAGER) {
public function testGlobalManagerLogic() {
foreach (self::USERS as $actor => $actorRights) {
if ($actorRights != User\Driver::RIGHTS_GLOBAL_MANAGER) {
continue;
}
$actorDomain = substr($actor,strrpos($actor,"@")+1);
$actorDomain = substr($actor, strrpos($actor, "@")+1);
Arsse::$user->auth($actor, "");
foreach(self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected,strrpos($affected,"@")+1);
foreach (self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected, strrpos($affected, "@")+1);
// global managers should be able to check any user
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
// they should only be able to act for regular users
if($actor==$affected || $affectedRights==User\Driver::RIGHTS_NONE) {
if ($actor==$affected || $affectedRights==User\Driver::RIGHTS_NONE) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// and they should only be able to set their own rights to regular user
foreach(self::LEVELS as $level) {
if($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_GLOBAL_MANAGER])) {
foreach (self::LEVELS as $level) {
if ($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_GLOBAL_MANAGER])) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
@ -221,41 +222,41 @@ class TestAuthorization extends Test\AbstractTest {
}
}
// they should also be able to list all users
foreach(self::DOMAINS as $domain) {
foreach (self::DOMAINS as $domain) {
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
}
}
}
function testGlobalAdministratorLogic() {
foreach(self::USERS as $actor => $actorRights) {
if($actorRights != User\Driver::RIGHTS_GLOBAL_ADMIN) {
public function testGlobalAdministratorLogic() {
foreach (self::USERS as $actor => $actorRights) {
if ($actorRights != User\Driver::RIGHTS_GLOBAL_ADMIN) {
continue;
}
Arsse::$user->auth($actor, "");
// global admins can do anything
foreach(self::USERS as $affected => $affectedRights) {
foreach (self::USERS as $affected => $affectedRights) {
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
foreach(self::LEVELS as $level) {
foreach (self::LEVELS as $level) {
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
}
}
foreach(self::DOMAINS as $domain) {
foreach (self::DOMAINS as $domain) {
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
}
}
}
function testInvalidLevelLogic() {
foreach(self::USERS as $actor => $rights) {
if(in_array($rights, self::LEVELS)) {
public function testInvalidLevelLogic() {
foreach (self::USERS as $actor => $rights) {
if (in_array($rights, self::LEVELS)) {
continue;
}
Arsse::$user->auth($actor, "");
foreach(array_keys(self::USERS) as $affected) {
foreach (array_keys(self::USERS) as $affected) {
// users with unknown/invalid rights should be treated just like regular users and only be able to act for themselves
if($actor==$affected) {
if ($actor==$affected) {
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
@ -263,18 +264,18 @@ class TestAuthorization extends Test\AbstractTest {
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should never be able to set rights
foreach(self::LEVELS as $level) {
foreach (self::LEVELS as $level) {
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
}
}
// they should not be able to list users
foreach(self::DOMAINS as $domain) {
foreach (self::DOMAINS as $domain) {
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
}
}
}
function testInternalExceptionLogic() {
public function testInternalExceptionLogic() {
$tests = [
// methods of User class to test, with parameters besides affected user
'exists' => [],
@ -295,7 +296,7 @@ class TestAuthorization extends Test\AbstractTest {
$this->assertCount(sizeof($tests), $this->checkExceptions("user@example.org", $tests));
}
function testExternalExceptionLogic() {
public function testExternalExceptionLogic() {
// set up the test for an external driver
$this->setUp(Test\User\DriverExternalMock::class, Test\User\Database::class);
// run the previous test with the external driver set up
@ -306,24 +307,24 @@ class TestAuthorization extends Test\AbstractTest {
// calls each requested function with supplied arguments, catches authorization exceptions, and returns an array of caught failed calls
protected function checkExceptions(string $user, $tests): array {
$err = [];
foreach($tests as $func => $args) {
foreach ($tests as $func => $args) {
// list method does not take an affected user, so do not unshift for that one
if($func != "list") {
if ($func != "list") {
array_unshift($args, $user);
}
try {
call_user_func_array(array(Arsse::$user, $func), $args);
} catch(User\ExceptionAuthz $e) {
} catch (User\ExceptionAuthz $e) {
$err[] = $func;
}
}
return $err;
}
function testMissingUserLogic() {
public function testMissingUserLogic() {
Arsse::$user->auth("gadm@example.com", "");
$this->assertTrue(Arsse::$user->authorize("user@example.com", "someFunction"));
$this->assertException("doesNotExist", "User");
Arsse::$user->authorize("this_user_does_not_exist@example.org", "someFunction");
}
}
}

2
tests/User/TestUserInternalDriver.php

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace JKingWeb\Arsse;
/**
/**
* @covers \JKingWeb\Arsse\User
* @covers \JKingWeb\Arsse\User\Internal\Driver
* @covers \JKingWeb\Arsse\User\Internal\InternalFunctions */

2
tests/User/TestUserMockExternal.php

@ -10,4 +10,4 @@ class TestUserMockExternal extends Test\AbstractTest {
const USER2 = "jane.doe@example.com";
public $drv = Test\User\DriverExternalMock::class;
}
}

2
tests/User/TestUserMockInternal.php

@ -11,7 +11,7 @@ class TestUserMockInternal extends Test\AbstractTest {
public $drv = Test\User\DriverInternalMock::class;
function setUpSeries() {
public function setUpSeries() {
Arsse::$db = null;
}
}

2
tests/docroot/Feed/Caching/200Future.php

@ -11,4 +11,4 @@
</channel>
</rss>
MESSAGE_BODY
];
];

2
tests/docroot/Feed/Caching/200Multiple.php

@ -27,4 +27,4 @@
</channel>
</rss>
MESSAGE_BODY
];
];

2
tests/docroot/Feed/Caching/200None.php

@ -21,4 +21,4 @@
</channel>
</rss>
MESSAGE_BODY
];
];

2
tests/docroot/Feed/Caching/200Past.php

@ -11,4 +11,4 @@
</channel>
</rss>
MESSAGE_BODY
];
];

2
tests/docroot/Feed/Caching/200PubDateOnly.php

@ -16,4 +16,4 @@
</channel>
</rss>
MESSAGE_BODY
];
];

2
tests/docroot/Feed/Caching/200UpdateDate.php

@ -17,4 +17,4 @@
</channel>
</rss>
MESSAGE_BODY
];
];

2
tests/docroot/Feed/Caching/304ETagOnly.php

@ -4,4 +4,4 @@
'fields' => [
"ETag: ".$_SERVER['HTTP_IF_NONE_MATCH'],
],
];
];

2
tests/docroot/Feed/Caching/304LastModOnly.php

@ -4,4 +4,4 @@
'fields' => [
'Last-Modified: '.$_SERVER['HTTP_IF_MODIFIED_SINCE'],
],
];
];

2
tests/docroot/Feed/Caching/304None.php

@ -1,4 +1,4 @@
<?php return [
'code' => 304,
'cache' => false,
];
];

4
tests/docroot/Feed/Caching/304Random.php

@ -1,7 +1,7 @@
<?php return [
'code' => 304,
'lastMod' => random_int(0,2^31),
'lastMod' => random_int(0, 2^31),
'fields' => [
"ETag: ".bin2hex(random_bytes(8)),
],
];
];

2
tests/docroot/Feed/Deduplication/Hashes-Dates1.php

@ -38,4 +38,4 @@
</channel>
</rss>
MESSAGE_BODY
];
];

2
tests/docroot/Feed/Deduplication/Hashes-Dates2.php

@ -38,4 +38,4 @@
</channel>
</rss>
MESSAGE_BODY
];
];

2
tests/docroot/Feed/Deduplication/Hashes-Dates3.php

@ -38,4 +38,4 @@
</channel>
</rss>
MESSAGE_BODY
];
];

2
tests/docroot/Feed/Deduplication/Hashes.php

@ -30,4 +30,4 @@
</channel>
</rss>
MESSAGE_BODY
];
];

2
tests/docroot/Feed/Deduplication/ID-Dates.php

@ -34,4 +34,4 @@
</channel>
</rss>
MESSAGE_BODY
];
];

2
tests/docroot/Feed/Deduplication/IdenticalHashes.php

@ -34,4 +34,4 @@
</channel>
</rss>
MESSAGE_BODY
];
];

2
tests/docroot/Feed/Deduplication/Permalink-Dates.php

@ -34,4 +34,4 @@
</channel>
</rss>
MESSAGE_BODY
];
];

2
tests/docroot/Feed/Fetching/EndlessLoop.php

@ -4,4 +4,4 @@
'fields' => [
'Location: http://localhost:'.$_SERVER['SERVER_PORT'].$_SERVER['REQUEST_URI']."0",
]
];
];

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save