mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
Passed code through linter
This commit is contained in:
parent
28201ba573
commit
f7e50fe95d
145 changed files with 1728 additions and 1556 deletions
25
.php_cs.dist
Normal file
25
.php_cs.dist
Normal file
|
@ -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);
|
|
@ -1,8 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
require_once __DIR__.DIRECTORY_SEPARATOR."bootstrap.php";
|
require_once __DIR__.DIRECTORY_SEPARATOR."bootstrap.php";
|
||||||
|
|
||||||
if(\PHP_SAPI=="cli") {
|
if (\PHP_SAPI=="cli") {
|
||||||
// initialize the CLI; this automatically handles --help and --version
|
// initialize the CLI; this automatically handles --help and --version
|
||||||
$cli = new CLI;
|
$cli = new CLI;
|
||||||
// handle other CLI requests; some do not require configuration
|
// handle other CLI requests; some do not require configuration
|
||||||
|
@ -10,9 +11,9 @@ if(\PHP_SAPI=="cli") {
|
||||||
} else {
|
} else {
|
||||||
// load configuration
|
// load configuration
|
||||||
Arsse::load(new Conf());
|
Arsse::load(new Conf());
|
||||||
if(file_exists(BASE."config.php")) {
|
if (file_exists(BASE."config.php")) {
|
||||||
Arsse::$conf->importFile(BASE."config.php");
|
Arsse::$conf->importFile(BASE."config.php");
|
||||||
}
|
}
|
||||||
// handle Web requests
|
// handle Web requests
|
||||||
(new REST)->dispatch()->output();
|
(new REST)->dispatch()->output();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
abstract class AbstractException extends \Exception {
|
abstract class AbstractException extends \Exception {
|
||||||
|
|
||||||
const CODES = [
|
const CODES = [
|
||||||
"Exception.uncoded" => -1,
|
"Exception.uncoded" => -1,
|
||||||
"Exception.unknown" => 10000,
|
"Exception.unknown" => 10000,
|
||||||
|
@ -71,13 +70,13 @@ abstract class AbstractException extends \Exception {
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
|
public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
|
||||||
if($msgID=="") {
|
if ($msgID=="") {
|
||||||
$msg = "Exception.unknown";
|
$msg = "Exception.unknown";
|
||||||
$code = 10000;
|
$code = 10000;
|
||||||
} else {
|
} else {
|
||||||
$class = get_called_class();
|
$class = get_called_class();
|
||||||
$codeID = str_replace("\\", "/", str_replace(NS_BASE, "", $class)).".$msgID";
|
$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);
|
throw new Exception("uncoded", $codeID);
|
||||||
} else {
|
} else {
|
||||||
$code = self::CODES[$codeID];
|
$code = self::CODES[$codeID];
|
||||||
|
@ -87,4 +86,4 @@ abstract class AbstractException extends \Exception {
|
||||||
}
|
}
|
||||||
parent::__construct($msg, $code, $e);
|
parent::__construct($msg, $code, $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,11 @@ class Arsse {
|
||||||
/** @var User */
|
/** @var User */
|
||||||
public static $user;
|
public static $user;
|
||||||
|
|
||||||
static function load(Conf $conf) {
|
public static function load(Conf $conf) {
|
||||||
static::$lang = new Lang();
|
static::$lang = new Lang();
|
||||||
static::$conf = $conf;
|
static::$conf = $conf;
|
||||||
static::$lang->set($conf->lang);
|
static::$lang->set($conf->lang);
|
||||||
static::$db = new Database();
|
static::$db = new Database();
|
||||||
static::$user = new User();
|
static::$user = new User();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
34
lib/CLI.php
34
lib/CLI.php
|
@ -22,8 +22,8 @@ configuration to a sample file.
|
||||||
USAGE_TEXT;
|
USAGE_TEXT;
|
||||||
}
|
}
|
||||||
|
|
||||||
function __construct(array $argv = null) {
|
public function __construct(array $argv = null) {
|
||||||
if(is_null($argv)) {
|
if (is_null($argv)) {
|
||||||
$argv = array_slice($_SERVER['argv'], 1);
|
$argv = array_slice($_SERVER['argv'], 1);
|
||||||
}
|
}
|
||||||
$this->args = \Docopt::handle($this->usage(), [
|
$this->args = \Docopt::handle($this->usage(), [
|
||||||
|
@ -36,7 +36,7 @@ USAGE_TEXT;
|
||||||
protected function loadConf(): bool {
|
protected function loadConf(): bool {
|
||||||
// FIXME: this should be a method of the Conf class
|
// FIXME: this should be a method of the Conf class
|
||||||
Arsse::load(new Conf());
|
Arsse::load(new Conf());
|
||||||
if(file_exists(BASE."config.php")) {
|
if (file_exists(BASE."config.php")) {
|
||||||
Arsse::$conf->importFile(BASE."config.php");
|
Arsse::$conf->importFile(BASE."config.php");
|
||||||
}
|
}
|
||||||
// command-line operations will never respect authorization
|
// command-line operations will never respect authorization
|
||||||
|
@ -44,52 +44,52 @@ USAGE_TEXT;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatch(array $args = null): int {
|
public function dispatch(array $args = null): int {
|
||||||
// act on command line
|
// act on command line
|
||||||
if(is_null($args)) {
|
if (is_null($args)) {
|
||||||
$args = $this->args;
|
$args = $this->args;
|
||||||
}
|
}
|
||||||
if($this->command("daemon", $args)) {
|
if ($this->command("daemon", $args)) {
|
||||||
$this->loadConf();
|
$this->loadConf();
|
||||||
return $this->daemon();
|
return $this->daemon();
|
||||||
} else if($this->command("feed refresh", $args)) {
|
} elseif ($this->command("feed refresh", $args)) {
|
||||||
$this->loadConf();
|
$this->loadConf();
|
||||||
return $this->feedRefresh((int) $args['<n>']);
|
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>']);
|
return $this->confSaveDefaults($args['<file>']);
|
||||||
} else if($this->command("user add", $args)) {
|
} elseif ($this->command("user add", $args)) {
|
||||||
$this->loadConf();
|
$this->loadConf();
|
||||||
return $this->userAdd($args['<username>'], $args['<password>']);
|
return $this->userAdd($args['<username>'], $args['<password>']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function command($cmd, $args): bool {
|
protected function command($cmd, $args): bool {
|
||||||
foreach(explode(" ", $cmd) as $part) {
|
foreach (explode(" ", $cmd) as $part) {
|
||||||
if(!$args[$part]) {
|
if (!$args[$part]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function daemon(bool $loop = true): int {
|
public function daemon(bool $loop = true): int {
|
||||||
(new Service)->watch($loop);
|
(new Service)->watch($loop);
|
||||||
return 0; // FIXME: should return the exception code of thrown exceptions
|
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
|
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);
|
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);
|
$passwd = Arsse::$user->add($user, $password);
|
||||||
if(is_null($password)) {
|
if (is_null($password)) {
|
||||||
echo $passwd;
|
echo $passwd;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
41
lib/Conf.php
41
lib/Conf.php
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
/** Class for loading, saving, and querying configuration
|
/** 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.
|
* 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. */
|
* All public properties are configuration parameters that may be set by the server administrator. */
|
||||||
class Conf {
|
class Conf {
|
||||||
|
@ -57,50 +57,50 @@ class Conf {
|
||||||
public $purgeFeeds = "PT24H";
|
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)
|
/** @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 */
|
* @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)
|
/** @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 */
|
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations */
|
||||||
public $purgeArticlesUnread = "P21D";
|
public $purgeArticlesUnread = "P21D";
|
||||||
|
|
||||||
/** Creates a new configuration object
|
/** Creates a new configuration object
|
||||||
* @param string $import_file Optional file to read configuration data from
|
* @param string $import_file Optional file to read configuration data from
|
||||||
* @see self::importFile() */
|
* @see self::importFile() */
|
||||||
public function __construct(string $import_file = "") {
|
public function __construct(string $import_file = "") {
|
||||||
if($import_file != "") {
|
if ($import_file != "") {
|
||||||
$this->importFile($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.
|
* 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 */
|
* @param string $file Full path and file name for the file to import */
|
||||||
public function importFile(string $file): self {
|
public function importFile(string $file): self {
|
||||||
if(!file_exists($file)) {
|
if (!file_exists($file)) {
|
||||||
throw new Conf\Exception("fileMissing", $file);
|
throw new Conf\Exception("fileMissing", $file);
|
||||||
} else if(!is_readable($file)) {
|
} elseif (!is_readable($file)) {
|
||||||
throw new Conf\Exception("fileUnreadable", $file);
|
throw new Conf\Exception("fileUnreadable", $file);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ob_start();
|
ob_start();
|
||||||
$arr = (@include $file);
|
$arr = (@include $file);
|
||||||
} catch(\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$arr = null;
|
$arr = null;
|
||||||
} finally {
|
} finally {
|
||||||
ob_end_clean();
|
ob_end_clean();
|
||||||
}
|
}
|
||||||
if(!is_array($arr)) {
|
if (!is_array($arr)) {
|
||||||
throw new Conf\Exception("fileCorrupt", $file);
|
throw new Conf\Exception("fileCorrupt", $file);
|
||||||
}
|
}
|
||||||
return $this->import($arr);
|
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.
|
* 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 */
|
* @param mixed[] $arr Array of configuration parameters to export */
|
||||||
public function import(array $arr): self {
|
public function import(array $arr): self {
|
||||||
foreach($arr as $key => $value) {
|
foreach ($arr as $key => $value) {
|
||||||
$this->$key = $value;
|
$this->$key = $value;
|
||||||
}
|
}
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -112,13 +112,13 @@ class Conf {
|
||||||
$ref = new self;
|
$ref = new self;
|
||||||
$out = [];
|
$out = [];
|
||||||
$conf = new \ReflectionObject($this);
|
$conf = new \ReflectionObject($this);
|
||||||
foreach($conf->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
|
foreach ($conf->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
|
||||||
$name = $prop->name;
|
$name = $prop->name;
|
||||||
// add the property to the output if the value is scalar and either:
|
// add the property to the output if the value is scalar and either:
|
||||||
// 1. full output has been requested
|
// 1. full output has been requested
|
||||||
// 2. the property is not defined in the class
|
// 2. the property is not defined in the class
|
||||||
// 3. it differs from the default
|
// 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;
|
$out[$name] = $this->$name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,31 +132,32 @@ class Conf {
|
||||||
$arr = $this->export($full);
|
$arr = $this->export($full);
|
||||||
$conf = new \ReflectionObject($this);
|
$conf = new \ReflectionObject($this);
|
||||||
$out = "<?php return [".PHP_EOL;
|
$out = "<?php return [".PHP_EOL;
|
||||||
foreach($arr as $prop => $value) {
|
foreach ($arr as $prop => $value) {
|
||||||
$match = null;
|
$match = null;
|
||||||
$doc = $comment = "";
|
$doc = $comment = "";
|
||||||
// retrieve the property's docblock, if it exists
|
// retrieve the property's docblock, if it exists
|
||||||
try {
|
try {
|
||||||
$doc = (new \ReflectionProperty(self::class, $prop))->getDocComment();
|
$doc = (new \ReflectionProperty(self::class, $prop))->getDocComment();
|
||||||
} catch(\ReflectionException $e) {}
|
} catch (\ReflectionException $e) {
|
||||||
if($doc) {
|
}
|
||||||
|
if ($doc) {
|
||||||
// parse the docblock to extract the property description
|
// 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];
|
$comment = $match[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// append the docblock description if there is one, or an empty comment otherwise
|
// append the docblock description if there is one, or an empty comment otherwise
|
||||||
$out .= " // ".$comment.PHP_EOL;
|
$out .= " // ".$comment.PHP_EOL;
|
||||||
// append the property and an export of its value to the output
|
// 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;
|
$out .= "];".PHP_EOL;
|
||||||
// write the configuration representation to the requested file
|
// 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
|
// if it fails throw an exception
|
||||||
$err = file_exists($file) ? "fileUnwritable" : "fileUncreatable";
|
$err = file_exists($file) ? "fileUnwritable" : "fileUncreatable";
|
||||||
throw new Conf\Exception($err, $file);
|
throw new Conf\Exception($err, $file);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Conf;
|
namespace JKingWeb\Arsse\Conf;
|
||||||
|
|
||||||
class Exception extends \JKingWeb\Arsse\AbstractException {
|
class Exception extends \JKingWeb\Arsse\AbstractException {
|
||||||
}
|
}
|
||||||
|
|
289
lib/Database.php
289
lib/Database.php
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use PasswordGenerator\Generator as PassGen;
|
use PasswordGenerator\Generator as PassGen;
|
||||||
use JKingWeb\Arsse\Misc\Query;
|
use JKingWeb\Arsse\Misc\Query;
|
||||||
use JKingWeb\Arsse\Misc\Context;
|
use JKingWeb\Arsse\Misc\Context;
|
||||||
|
@ -10,13 +11,13 @@ class Database {
|
||||||
const SCHEMA_VERSION = 1;
|
const SCHEMA_VERSION = 1;
|
||||||
|
|
||||||
/** @var Db\Driver */
|
/** @var Db\Driver */
|
||||||
public $db;
|
public $db;
|
||||||
|
|
||||||
public function __construct($initialize = true) {
|
public function __construct($initialize = true) {
|
||||||
$driver = Arsse::$conf->dbDriver;
|
$driver = Arsse::$conf->dbDriver;
|
||||||
$this->db = new $driver();
|
$this->db = new $driver();
|
||||||
$ver = $this->db->schemaVersion();
|
$ver = $this->db->schemaVersion();
|
||||||
if($initialize && $ver < self::SCHEMA_VERSION) {
|
if ($initialize && $ver < self::SCHEMA_VERSION) {
|
||||||
$this->db->schemaUpdate(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'];
|
return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static public function driverList(): array {
|
public static function driverList(): array {
|
||||||
$sep = \DIRECTORY_SEPARATOR;
|
$sep = \DIRECTORY_SEPARATOR;
|
||||||
$path = __DIR__.$sep."Db".$sep;
|
$path = __DIR__.$sep."Db".$sep;
|
||||||
$classes = [];
|
$classes = [];
|
||||||
foreach(glob($path."*".$sep."Driver.php") as $file) {
|
foreach (glob($path."*".$sep."Driver.php") as $file) {
|
||||||
$name = basename(dirname($file));
|
$name = basename(dirname($file));
|
||||||
$class = NS_BASE."Db\\$name\\Driver";
|
$class = NS_BASE."Db\\$name\\Driver";
|
||||||
$classes[$class] = $class::driverName();
|
$classes[$class] = $class::driverName();
|
||||||
|
@ -42,7 +43,7 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function driverSchemaUpdate(): bool {
|
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 $this->db->schemaUpdate(self::SCHEMA_VERSION);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -54,8 +55,8 @@ class Database {
|
||||||
[], // binding types
|
[], // binding types
|
||||||
[], // binding values
|
[], // binding values
|
||||||
];
|
];
|
||||||
foreach($valid as $prop => $type) {
|
foreach ($valid as $prop => $type) {
|
||||||
if(!array_key_exists($prop, $props)) {
|
if (!array_key_exists($prop, $props)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$out[0][] = "$prop = ?";
|
$out[0][] = "$prop = ?";
|
||||||
|
@ -72,9 +73,9 @@ class Database {
|
||||||
[], // binding types
|
[], // binding types
|
||||||
];
|
];
|
||||||
// the query clause is just a series of question marks separated by commas
|
// 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
|
// 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;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ class Database {
|
||||||
|
|
||||||
public function metaSet(string $key, $value, string $type = "str"): bool {
|
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();
|
$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();
|
$out = $this->db->prepare("INSERT INTO arsse_meta(key,value) values(?,?)", "str", $type)->run($key, $value)->changes();
|
||||||
}
|
}
|
||||||
return (bool) $out;
|
return (bool) $out;
|
||||||
|
@ -99,23 +100,23 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function userExists(string $user): bool {
|
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]);
|
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();
|
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 {
|
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]);
|
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]);
|
throw new User\Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
if($password===null) {
|
if ($password===null) {
|
||||||
$password = (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get();
|
$password = (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get();
|
||||||
}
|
}
|
||||||
$hash = "";
|
$hash = "";
|
||||||
if(strlen($password) > 0) {
|
if (strlen($password) > 0) {
|
||||||
$hash = password_hash($password, \PASSWORD_DEFAULT);
|
$hash = password_hash($password, \PASSWORD_DEFAULT);
|
||||||
}
|
}
|
||||||
$this->db->prepare("INSERT INTO arsse_users(id,password) values(?,?)", "str", "str")->runArray([$user,$hash]);
|
$this->db->prepare("INSERT INTO arsse_users(id,password) values(?,?)", "str", "str")->runArray([$user,$hash]);
|
||||||
|
@ -123,10 +124,10 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function userRemove(string $user): bool {
|
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]);
|
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]);
|
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -134,20 +135,20 @@ class Database {
|
||||||
|
|
||||||
public function userList(string $domain = null): array {
|
public function userList(string $domain = null): array {
|
||||||
$out = [];
|
$out = [];
|
||||||
if($domain !== null) {
|
if ($domain !== null) {
|
||||||
if(!Arsse::$user->authorize("@".$domain, __FUNCTION__)) {
|
if (!Arsse::$user->authorize("@".$domain, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
|
||||||
}
|
}
|
||||||
$domain = str_replace(["\\","%","_"],["\\\\", "\\%", "\\_"], $domain);
|
$domain = str_replace(["\\","%","_"], ["\\\\", "\\%", "\\_"], $domain);
|
||||||
$domain = "%@".$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'];
|
$out[] = $user['id'];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if(!Arsse::$user->authorize("", __FUNCTION__)) {
|
if (!Arsse::$user->authorize("", __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]);
|
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'];
|
$out[] = $user['id'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,25 +156,25 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function userPasswordGet(string $user): string {
|
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]);
|
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]);
|
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();
|
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 {
|
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]);
|
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]);
|
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
if($password===null) {
|
if ($password===null) {
|
||||||
$password = (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get();
|
$password = (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get();
|
||||||
}
|
}
|
||||||
$hash = "";
|
$hash = "";
|
||||||
if(strlen($password) > 0) {
|
if (strlen($password) > 0) {
|
||||||
$hash = password_hash($password, \PASSWORD_DEFAULT);
|
$hash = password_hash($password, \PASSWORD_DEFAULT);
|
||||||
}
|
}
|
||||||
$this->db->prepare("UPDATE arsse_users set password = ? where id is ?", "str", "str")->run($hash, $user);
|
$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 {
|
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]);
|
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();
|
$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]);
|
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
return $prop;
|
return $prop;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function userPropertiesSet(string $user, array $properties): array {
|
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]);
|
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]);
|
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
$valid = [ // FIXME: add future properties
|
$valid = [ // FIXME: add future properties
|
||||||
|
@ -206,16 +207,16 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function userRightsGet(string $user): int {
|
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]);
|
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();
|
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 {
|
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]);
|
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]);
|
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);
|
$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 {
|
public function folderAdd(string $user, array $data): int {
|
||||||
// If the user isn't authorized to perform this action then throw an exception.
|
// 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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
// if the desired folder name is missing or invalid, throw an exception
|
// 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"]);
|
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"]);
|
throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "name"]);
|
||||||
}
|
}
|
||||||
// normalize folder's parent, if there is one
|
// normalize folder's parent, if there is one
|
||||||
$parent = array_key_exists("parent", $data) ? (int) $data['parent'] : 0;
|
$parent = array_key_exists("parent", $data) ? (int) $data['parent'] : 0;
|
||||||
if($parent===0) {
|
if ($parent===0) {
|
||||||
// if no parent is specified, do nothing
|
// if no parent is specified, do nothing
|
||||||
$parent = null;
|
$parent = null;
|
||||||
} else {
|
} 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
|
// 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();
|
$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]);
|
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
|
// 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?
|
// 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
|
throw new Db\ExceptionInput("constraintViolation"); // FIXME: There needs to be a practical message here
|
||||||
}
|
}
|
||||||
// actually perform the insert (!)
|
// actually perform the insert (!)
|
||||||
|
@ -256,17 +257,17 @@ class Database {
|
||||||
|
|
||||||
public function folderList(string $user, int $parent = null, bool $recursive = true): Db\Result {
|
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 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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
// check to make sure the parent exists, if one is specified
|
// check to make sure the parent exists, if one is specified
|
||||||
if(!is_null($parent)) {
|
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 (!$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]);
|
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 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);
|
return $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and parent is ?", "str", "int")->run($user, $parent);
|
||||||
} else {
|
} else {
|
||||||
return $this->db->prepare(
|
return $this->db->prepare(
|
||||||
|
@ -277,45 +278,45 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function folderRemove(string $user, int $id): bool {
|
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]);
|
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();
|
$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]);
|
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function folderPropertiesGet(string $user, int $id): array {
|
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]);
|
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();
|
$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]);
|
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
|
||||||
}
|
}
|
||||||
return $props;
|
return $props;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function folderPropertiesSet(string $user, int $id, array $data): bool {
|
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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
// validate the folder ID and, if specified, the parent to move it to
|
// validate the folder ID and, if specified, the parent to move it to
|
||||||
$parent = null;
|
$parent = null;
|
||||||
if(array_key_exists("parent", $data)) {
|
if (array_key_exists("parent", $data)) {
|
||||||
$parent = $data['parent'];
|
$parent = $data['parent'];
|
||||||
}
|
}
|
||||||
$f = $this->folderValidateId($user, $id, $parent, true);
|
$f = $this->folderValidateId($user, $id, $parent, true);
|
||||||
// if a new name is specified, validate it
|
// if a new name is specified, validate it
|
||||||
if(array_key_exists("name", $data)) {
|
if (array_key_exists("name", $data)) {
|
||||||
$this->folderValidateName($data['name']);
|
$this->folderValidateName($data['name']);
|
||||||
}
|
}
|
||||||
$data = array_merge($f, $data);
|
$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)
|
// 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();
|
$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
|
throw new Db\ExceptionInput("constraintViolation"); // FIXME: There needs to be a practical message here
|
||||||
}
|
}
|
||||||
$valid = [
|
$valid = [
|
||||||
|
@ -327,32 +328,32 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function folderValidateId(string $user, int $id = null, int $parent = null, bool $subject = false): array {
|
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 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
|
throw new Db\ExceptionInput("circularDependence", ["action" => $this->caller(), "field" => "parent", 'id' => $parent]); // @codeCoverageIgnore
|
||||||
}
|
}
|
||||||
return ['name' => null, 'parent' => null];
|
return ['name' => null, 'parent' => null];
|
||||||
}
|
}
|
||||||
// check whether the folder exists and is owned by the user
|
// 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();
|
$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]);
|
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 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)
|
// 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(
|
$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) ".
|
"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 ?",
|
"SELECT id,(id not in (select id from folders)) as valid from arsse_folders where owner is ? and id is ?",
|
||||||
"str", "int", "str", "int"
|
"str", "int", "str", "int"
|
||||||
)->run($user, $id, $user, $parent)->getRow();
|
)->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
|
// 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]);
|
throw new Db\ExceptionInput("idMissing", ["action" => $this->caller(), "field" => "parent", 'id' => $parent]);
|
||||||
} else {
|
} else {
|
||||||
// if using the desired parent would create a circular dependence, throw a different exception
|
// 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]);
|
throw new Db\ExceptionInput("circularDependence", ["action" => $this->caller(), "field" => "parent", 'id' => $parent]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -362,9 +363,9 @@ class Database {
|
||||||
|
|
||||||
protected function folderValidateName($name): bool {
|
protected function folderValidateName($name): bool {
|
||||||
$name = (string) $name;
|
$name = (string) $name;
|
||||||
if(!strlen($name)) {
|
if (!strlen($name)) {
|
||||||
throw new Db\ExceptionInput("missing", ["action" => $this->caller(), "field" => "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"]);
|
throw new Db\ExceptionInput("whitespace", ["action" => $this->caller(), "field" => "name"]);
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
|
@ -372,18 +373,18 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = ""): int {
|
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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
// check to see if the feed exists
|
// 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();
|
$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
|
// 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();
|
$feedID = $this->db->prepare('INSERT INTO arsse_feeds(url,username,password) values(?,?,?)', 'str', 'str', 'str')->run($url, $fetchUser, $fetchPassword)->lastId();
|
||||||
try {
|
try {
|
||||||
// perform an initial update on the newly added feed
|
// perform an initial update on the newly added feed
|
||||||
$this->feedUpdate($feedID, true);
|
$this->feedUpdate($feedID, true);
|
||||||
} catch(\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// if the update fails, delete the feed we just added
|
// if the update fails, delete the feed we just added
|
||||||
$this->db->prepare('DELETE from arsse_feeds where id is ?', 'int')->run($feedID);
|
$this->db->prepare('DELETE from arsse_feeds where id is ?', 'int')->run($feedID);
|
||||||
throw $e;
|
throw $e;
|
||||||
|
@ -394,7 +395,7 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function subscriptionList(string $user, int $folder = null, int $id = null): Db\Result {
|
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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
// create a complex query
|
// 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
|
$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
|
// 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");
|
$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
|
// 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
|
// if an ID is specified, add a suitable WHERE condition and bindings
|
||||||
$q->setWhere("arsse_subscriptions.id is ?", "int", $id);
|
$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
|
// if a folder is specified, make sure it exists
|
||||||
$this->folderValidateId($user, $folder);
|
$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
|
// 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 {
|
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]);
|
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();
|
$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]);
|
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function subscriptionPropertiesGet(string $user, int $id): array {
|
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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
// disable authorization checks for the list call
|
// disable authorization checks for the list call
|
||||||
Arsse::$user->authorizationEnabled(false);
|
Arsse::$user->authorizationEnabled(false);
|
||||||
$sub = $this->subscriptionList($user, null, $id)->getRow();
|
$sub = $this->subscriptionList($user, null, $id)->getRow();
|
||||||
Arsse::$user->authorizationEnabled(true);
|
Arsse::$user->authorizationEnabled(true);
|
||||||
if(!$sub) {
|
if (!$sub) {
|
||||||
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]);
|
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]);
|
||||||
}
|
}
|
||||||
return $sub;
|
return $sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function subscriptionPropertiesSet(string $user, int $id, array $data): bool {
|
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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
$tr = $this->db->begin();
|
$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
|
// 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]);
|
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
|
// ensure the target folder exists and belong to the user
|
||||||
$this->folderValidateId($user, $data['folder']);
|
$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 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'];
|
$title = (string) $data['title'];
|
||||||
if(!strlen($title)) {
|
if (!strlen($title)) {
|
||||||
throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "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"]);
|
throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "title"]);
|
||||||
}
|
}
|
||||||
$data['title'] = $title;
|
$data['title'] = $title;
|
||||||
|
@ -494,7 +495,7 @@ class Database {
|
||||||
|
|
||||||
protected function subscriptionValidateId(string $user, int $id): array {
|
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();
|
$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]);
|
throw new Db\ExceptionInput("idMissing", ["action" => $this->caller(), "field" => "subscription", 'id' => $id]);
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
|
@ -502,14 +503,14 @@ class Database {
|
||||||
|
|
||||||
public function feedListStale(): array {
|
public function feedListStale(): array {
|
||||||
$feeds = $this->db->query("SELECT id from arsse_feeds where next_fetch <= CURRENT_TIMESTAMP")->getAll();
|
$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 {
|
public function feedUpdate(int $feedID, bool $throwError = false): bool {
|
||||||
$tr = $this->db->begin();
|
$tr = $this->db->begin();
|
||||||
// check to make sure the feed exists
|
// 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();
|
$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]);
|
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
|
// 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
|
// error instead of failing; if other exceptions are thrown, we should simply roll back
|
||||||
try {
|
try {
|
||||||
$feed = new Feed($feedID, $f['url'], (string) Date::transform($f['modified'], "http", "sql"), $f['etag'], $f['username'], $f['password'], $scrape);
|
$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
|
// 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);
|
$this->db->prepare("UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ? WHERE id is ?", 'datetime', 'int')->run($feed->nextFetch, $feedID);
|
||||||
$tr->commit();
|
$tr->commit();
|
||||||
|
@ -528,38 +529,38 @@ class Database {
|
||||||
} catch (Feed\Exception $e) {
|
} catch (Feed\Exception $e) {
|
||||||
// update the database with the resultant error and the next fetch time, incrementing the error count
|
// update the database with the resultant error and the next fetch time, incrementing the error count
|
||||||
$this->db->prepare(
|
$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'
|
'datetime', 'str', 'int'
|
||||||
)->run(Feed::nextFetchOnError($f['err_count']), $e->getMessage(),$feedID);
|
)->run(Feed::nextFetchOnError($f['err_count']), $e->getMessage(), $feedID);
|
||||||
$tr->commit();
|
$tr->commit();
|
||||||
if($throwError) {
|
if ($throwError) {
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
//prepare the necessary statements to perform the update
|
//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');
|
$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');
|
$qInsertCategory = $this->db->prepare("INSERT INTO arsse_categories(article,name) values(?,?)", 'int', 'str');
|
||||||
$qInsertEdition = $this->db->prepare("INSERT INTO arsse_editions(article) values(?)", 'int');
|
$qInsertEdition = $this->db->prepare("INSERT INTO arsse_editions(article) values(?)", 'int');
|
||||||
}
|
}
|
||||||
if(sizeof($feed->newItems)) {
|
if (sizeof($feed->newItems)) {
|
||||||
$qInsertArticle = $this->db->prepare(
|
$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(?,?,?,?,?,?,?,?,?,?,?)",
|
"INSERT INTO arsse_articles(url,title,author,published,edited,guid,content,url_title_hash,url_content_hash,title_content_hash,feed) values(?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
'str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str', 'int'
|
'str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str', 'int'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if(sizeof($feed->changedItems)) {
|
if (sizeof($feed->changedItems)) {
|
||||||
$qDeleteEnclosures = $this->db->prepare("DELETE FROM arsse_enclosures WHERE article is ?", 'int');
|
$qDeleteEnclosures = $this->db->prepare("DELETE FROM arsse_enclosures WHERE article is ?", 'int');
|
||||||
$qDeleteCategories = $this->db->prepare("DELETE FROM arsse_categories 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');
|
$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(
|
$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'
|
'str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str', 'int'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// actually perform updates
|
// actually perform updates
|
||||||
foreach($feed->newItems as $article) {
|
foreach ($feed->newItems as $article) {
|
||||||
$articleID = $qInsertArticle->run(
|
$articleID = $qInsertArticle->run(
|
||||||
$article->url,
|
$article->url,
|
||||||
$article->title,
|
$article->title,
|
||||||
|
@ -573,15 +574,15 @@ class Database {
|
||||||
$article->titleContentHash,
|
$article->titleContentHash,
|
||||||
$feedID
|
$feedID
|
||||||
)->lastId();
|
)->lastId();
|
||||||
if($article->enclosureUrl) {
|
if ($article->enclosureUrl) {
|
||||||
$qInsertEnclosure->run($articleID,$article->enclosureUrl,$article->enclosureType);
|
$qInsertEnclosure->run($articleID, $article->enclosureUrl, $article->enclosureType);
|
||||||
}
|
}
|
||||||
foreach($article->categories as $c) {
|
foreach ($article->categories as $c) {
|
||||||
$qInsertCategory->run($articleID, $c);
|
$qInsertCategory->run($articleID, $c);
|
||||||
}
|
}
|
||||||
$qInsertEdition->run($articleID);
|
$qInsertEdition->run($articleID);
|
||||||
}
|
}
|
||||||
foreach($feed->changedItems as $articleID => $article) {
|
foreach ($feed->changedItems as $articleID => $article) {
|
||||||
$qUpdateArticle->run(
|
$qUpdateArticle->run(
|
||||||
$article->url,
|
$article->url,
|
||||||
$article->title,
|
$article->title,
|
||||||
|
@ -597,10 +598,10 @@ class Database {
|
||||||
);
|
);
|
||||||
$qDeleteEnclosures->run($articleID);
|
$qDeleteEnclosures->run($articleID);
|
||||||
$qDeleteCategories->run($articleID);
|
$qDeleteCategories->run($articleID);
|
||||||
if($article->enclosureUrl) {
|
if ($article->enclosureUrl) {
|
||||||
$qInsertEnclosure->run($articleID,$article->enclosureUrl,$article->enclosureType);
|
$qInsertEnclosure->run($articleID, $article->enclosureUrl, $article->enclosureType);
|
||||||
}
|
}
|
||||||
foreach($article->categories as $c) {
|
foreach ($article->categories as $c) {
|
||||||
$qInsertCategory->run($articleID, $c);
|
$qInsertCategory->run($articleID, $c);
|
||||||
}
|
}
|
||||||
$qInsertEdition->run($articleID);
|
$qInsertEdition->run($articleID);
|
||||||
|
@ -608,7 +609,7 @@ class Database {
|
||||||
}
|
}
|
||||||
// lastly update the feed database itself with updated information.
|
// lastly update the feed database itself with updated information.
|
||||||
$this->db->prepare(
|
$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'
|
'str', 'str', 'str', 'str', 'datetime', 'str', 'datetime', 'int', 'int'
|
||||||
)->run(
|
)->run(
|
||||||
$feed->data->feedUrl,
|
$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)");
|
$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
|
// finally delete feeds that have been orphaned longer than the retention period
|
||||||
$limit = Date::normalize("now");
|
$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
|
// if there is a retention period specified, compute it; otherwise feed are deleted immediatelty
|
||||||
$limit->sub(new \DateInterval(Arsse::$conf->purgeFeeds));
|
$limit->sub(new \DateInterval(Arsse::$conf->purgeFeeds));
|
||||||
}
|
}
|
||||||
|
@ -645,29 +646,29 @@ class Database {
|
||||||
|
|
||||||
public function feedMatchLatest(int $feedID, int $count): Db\Result {
|
public function feedMatchLatest(int $feedID, int $count): Db\Result {
|
||||||
return $this->db->prepare(
|
return $this->db->prepare(
|
||||||
"SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed 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'
|
'int', 'int'
|
||||||
)->run($feedID, $count);
|
)->run($feedID, $count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function feedMatchIds(int $feedID, array $ids = [], array $hashesUT = [], array $hashesUC = [], array $hashesTC = []): Db\Result {
|
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
|
// 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($cHashUT, $tHashUT) = $this->generateIn($hashesUT, "str");
|
||||||
list($cHashUC, $tHashUC) = $this->generateIn($hashesUC, "str");
|
list($cHashUC, $tHashUC) = $this->generateIn($hashesUC, "str");
|
||||||
list($cHashTC, $tHashTC) = $this->generateIn($hashesTC, "str");
|
list($cHashTC, $tHashTC) = $this->generateIn($hashesTC, "str");
|
||||||
// perform the query
|
// perform the query
|
||||||
return $articles = $this->db->prepare(
|
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
|
'int', $tId, $tHashUT, $tHashUC, $tHashTC
|
||||||
)->run($feedID, $ids, $hashesUT, $hashesUC, $hashesTC);
|
)->run($feedID, $ids, $hashesUT, $hashesUC, $hashesTC);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function articleList(string $user, Context $context = null): Db\Result {
|
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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
if(!$context) {
|
if (!$context) {
|
||||||
$context = new Context;
|
$context = new Context;
|
||||||
}
|
}
|
||||||
$q = new Query(
|
$q = new Query(
|
||||||
|
@ -696,12 +697,12 @@ class Database {
|
||||||
$q->setOrder("edition".($context->reverse ? " desc" : ""));
|
$q->setOrder("edition".($context->reverse ? " desc" : ""));
|
||||||
$q->setLimit($context->limit, $context->offset);
|
$q->setLimit($context->limit, $context->offset);
|
||||||
$q->setCTE("user(user)", "SELECT ?", "str", $user);
|
$q->setCTE("user(user)", "SELECT ?", "str", $user);
|
||||||
if($context->subscription()) {
|
if ($context->subscription()) {
|
||||||
// if a subscription is specified, make sure it exists
|
// if a subscription is specified, make sure it exists
|
||||||
$id = $this->subscriptionValidateId($user, $context->subscription)['feed'];
|
$id = $this->subscriptionValidateId($user, $context->subscription)['feed'];
|
||||||
// add a basic CTE that will join in only the requested subscription
|
// add a basic CTE that will join in only the requested subscription
|
||||||
$q->setCTE("subscribed_feeds(id,sub)", "SELECT ?,?", ["int","int"], [$id,$context->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
|
// if a folder is specified, make sure it exists
|
||||||
$this->folderValidateId($user, $context->folder);
|
$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
|
// 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");
|
$q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user is owner");
|
||||||
}
|
}
|
||||||
// filter based on edition offset
|
// filter based on edition offset
|
||||||
if($context->oldestEdition()) {
|
if ($context->oldestEdition()) {
|
||||||
$q->setWhere("edition >= ?", "int", $context->oldestEdition);
|
$q->setWhere("edition >= ?", "int", $context->oldestEdition);
|
||||||
}
|
}
|
||||||
if($context->latestEdition()) {
|
if ($context->latestEdition()) {
|
||||||
$q->setWhere("edition <= ?", "int", $context->latestEdition);
|
$q->setWhere("edition <= ?", "int", $context->latestEdition);
|
||||||
}
|
}
|
||||||
// filter based on lastmod time
|
// filter based on lastmod time
|
||||||
if($context->modifiedSince()) {
|
if ($context->modifiedSince()) {
|
||||||
$q->setWhere("modified_date >= ?", "datetime", $context->modifiedSince);
|
$q->setWhere("modified_date >= ?", "datetime", $context->modifiedSince);
|
||||||
}
|
}
|
||||||
if($context->notModifiedSince()) {
|
if ($context->notModifiedSince()) {
|
||||||
$q->setWhere("modified_date <= ?", "datetime", $context->notModifiedSince);
|
$q->setWhere("modified_date <= ?", "datetime", $context->notModifiedSince);
|
||||||
}
|
}
|
||||||
// filter for un/read and un/starred status if specified
|
// filter for un/read and un/starred status if specified
|
||||||
if($context->unread()) {
|
if ($context->unread()) {
|
||||||
$q->setWhere("unread is ?", "bool", $context->unread);
|
$q->setWhere("unread is ?", "bool", $context->unread);
|
||||||
}
|
}
|
||||||
if($context->starred()) {
|
if ($context->starred()) {
|
||||||
$q->setWhere("starred is ?", "bool", $context->starred);
|
$q->setWhere("starred is ?", "bool", $context->starred);
|
||||||
}
|
}
|
||||||
// perform the query and return results
|
// perform the query and return results
|
||||||
|
@ -738,10 +739,10 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function articleMark(string $user, array $data, Context $context = null): bool {
|
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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
if(!$context) {
|
if (!$context) {
|
||||||
$context = new Context;
|
$context = new Context;
|
||||||
}
|
}
|
||||||
// sanitize input
|
// sanitize input
|
||||||
|
@ -771,19 +772,19 @@ class Database {
|
||||||
// wrap this UPDATE and INSERT together into a transaction
|
// wrap this UPDATE and INSERT together into a transaction
|
||||||
$tr = $this->begin();
|
$tr = $this->begin();
|
||||||
// if an edition context is specified, make sure it's valid
|
// if an edition context is specified, make sure it's valid
|
||||||
if($context->edition()) {
|
if ($context->edition()) {
|
||||||
// make sure the edition exists
|
// make sure the edition exists
|
||||||
$edition = $this->articleValidateEdition($user, $context->edition);
|
$edition = $this->articleValidateEdition($user, $context->edition);
|
||||||
// if the edition is not the latest, do not mark the read flag
|
// if the edition is not the latest, do not mark the read flag
|
||||||
if(!$edition['current']) {
|
if (!$edition['current']) {
|
||||||
$values[0] = null;
|
$values[0] = null;
|
||||||
}
|
}
|
||||||
} else if($context->article()) {
|
} elseif ($context->article()) {
|
||||||
// otherwise if an article context is specified, make sure it's valid
|
// otherwise if an article context is specified, make sure it's valid
|
||||||
$this->articleValidateId($user, $context->article);
|
$this->articleValidateId($user, $context->article);
|
||||||
}
|
}
|
||||||
// execute each query in sequence
|
// 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
|
// 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(
|
$q = new Query(
|
||||||
"SELECT
|
"SELECT
|
||||||
|
@ -802,12 +803,12 @@ class Database {
|
||||||
$q->setCTE("user(user)", "SELECT ?", "str", $user);
|
$q->setCTE("user(user)", "SELECT ?", "str", $user);
|
||||||
// common table expression with the values to set
|
// common table expression with the values to set
|
||||||
$q->setCTE("target_values(read,starred)", "SELECT ?,?", ["bool","bool"], $values);
|
$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
|
// if a subscription is specified, make sure it exists
|
||||||
$id = $this->subscriptionValidateId($user, $context->subscription)['feed'];
|
$id = $this->subscriptionValidateId($user, $context->subscription)['feed'];
|
||||||
// add a basic CTE that will join in only the requested subscription
|
// 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");
|
$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
|
// if a folder is specified, make sure it exists
|
||||||
$this->folderValidateId($user, $context->folder);
|
$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
|
// 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
|
// 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");
|
$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
|
// if an edition is specified, filter for its previously identified article
|
||||||
$q->setWhere("arsse_articles.id is ?", "int", $edition['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)
|
// if an article is specified, filter for it (it has already been validated above)
|
||||||
$q->setWhere("arsse_articles.id is ?", "int", $context->article);
|
$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 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
|
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
|
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");
|
list($inParams, $inTypes) = $this->generateIn($context->editions, "int");
|
||||||
|
@ -839,15 +840,15 @@ class Database {
|
||||||
$context->editions
|
$context->editions
|
||||||
);
|
);
|
||||||
$q->setWhere("arsse_articles.id in (select id from requested_articles)");
|
$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 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
|
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
|
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");
|
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)",
|
"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,
|
$inTypes,
|
||||||
$context->articles
|
$context->articles
|
||||||
|
@ -858,17 +859,17 @@ class Database {
|
||||||
$q->setCTE("requested_articles(id,edition)", "SELECT 'empty','table' where 1 is 0");
|
$q->setCTE("requested_articles(id,edition)", "SELECT 'empty','table' where 1 is 0");
|
||||||
}
|
}
|
||||||
// filter based on edition offset
|
// filter based on edition offset
|
||||||
if($context->oldestEdition()) {
|
if ($context->oldestEdition()) {
|
||||||
$q->setWhere("edition >= ?", "int", $context->oldestEdition);
|
$q->setWhere("edition >= ?", "int", $context->oldestEdition);
|
||||||
}
|
}
|
||||||
if($context->latestEdition()) {
|
if ($context->latestEdition()) {
|
||||||
$q->setWhere("edition <= ?", "int", $context->latestEdition);
|
$q->setWhere("edition <= ?", "int", $context->latestEdition);
|
||||||
}
|
}
|
||||||
// filter based on lastmod time
|
// filter based on lastmod time
|
||||||
if($context->modifiedSince()) {
|
if ($context->modifiedSince()) {
|
||||||
$q->setWhere("modified_date >= ?", "datetime", $context->modifiedSince);
|
$q->setWhere("modified_date >= ?", "datetime", $context->modifiedSince);
|
||||||
}
|
}
|
||||||
if($context->notModifiedSince()) {
|
if ($context->notModifiedSince()) {
|
||||||
$q->setWhere("modified_date <= ?", "datetime", $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
|
// 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 {
|
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]);
|
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();
|
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;
|
$limitRead = null;
|
||||||
$limitUnread = null;
|
$limitUnread = null;
|
||||||
if(Arsse::$conf->purgeArticlesRead) {
|
if (Arsse::$conf->purgeArticlesRead) {
|
||||||
$limitRead = Date::sub(Arsse::$conf->purgeArticlesRead);
|
$limitRead = Date::sub(Arsse::$conf->purgeArticlesRead);
|
||||||
}
|
}
|
||||||
if(Arsse::$conf->purgeArticlesUnread) {
|
if (Arsse::$conf->purgeArticlesUnread) {
|
||||||
$limitUnread = Date::sub(Arsse::$conf->purgeArticlesUnread);
|
$limitUnread = Date::sub(Arsse::$conf->purgeArticlesUnread);
|
||||||
}
|
}
|
||||||
$feeds = $this->db->query("SELECT id, size from arsse_feeds")->getAll();
|
$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);
|
$query->run($feed['id'], $feed['size'], $limitUnread, $limitRead);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -938,7 +939,7 @@ class Database {
|
||||||
arsse_articles.id is ? and arsse_subscriptions.owner is ?",
|
arsse_articles.id is ? and arsse_subscriptions.owner is ?",
|
||||||
"int", "str"
|
"int", "str"
|
||||||
)->run($id, $user)->getRow();
|
)->run($id, $user)->getRow();
|
||||||
if(!$out) {
|
if (!$out) {
|
||||||
throw new Db\ExceptionInput("subjectMissing", ["action" => $this->caller(), "field" => "article", 'id' => $id]);
|
throw new Db\ExceptionInput("subjectMissing", ["action" => $this->caller(), "field" => "article", 'id' => $id]);
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
|
@ -958,21 +959,21 @@ class Database {
|
||||||
edition is ? and arsse_subscriptions.owner is ?",
|
edition is ? and arsse_subscriptions.owner is ?",
|
||||||
"int", "str"
|
"int", "str"
|
||||||
)->run($id, $user)->getRow();
|
)->run($id, $user)->getRow();
|
||||||
if(!$out) {
|
if (!$out) {
|
||||||
throw new Db\ExceptionInput("subjectMissing", ["action" => $this->caller(), "field" => "edition", 'id' => $id]);
|
throw new Db\ExceptionInput("subjectMissing", ["action" => $this->caller(), "field" => "edition", 'id' => $id]);
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function editionLatest(string $user, Context $context = null): int {
|
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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
if(!$context) {
|
if (!$context) {
|
||||||
$context = new 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");
|
$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
|
// if a subscription is specified, make sure it exists
|
||||||
$id = $this->subscriptionValidateId($user, $context->subscription)['feed'];
|
$id = $this->subscriptionValidateId($user, $context->subscription)['feed'];
|
||||||
// a simple WHERE clause is required here
|
// 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();
|
return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,16 +7,16 @@ abstract class AbstractDriver implements Driver {
|
||||||
protected $transDepth = 0;
|
protected $transDepth = 0;
|
||||||
protected $transStatus = [];
|
protected $transStatus = [];
|
||||||
|
|
||||||
public abstract function prepareArray(string $query, array $paramTypes): Statement;
|
abstract public function prepareArray(string $query, array $paramTypes): Statement;
|
||||||
protected abstract function lock(): bool;
|
abstract protected function lock(): bool;
|
||||||
protected abstract function unlock(bool $rollback = false) : bool;
|
abstract protected function unlock(bool $rollback = false) : bool;
|
||||||
|
|
||||||
/** @codeCoverageIgnore */
|
/** @codeCoverageIgnore */
|
||||||
public function schemaVersion(): int {
|
public function schemaVersion(): int {
|
||||||
// FIXME: generic schemaVersion() will need to be covered for database engines other than SQLite
|
// FIXME: generic schemaVersion() will need to be covered for database engines other than SQLite
|
||||||
try {
|
try {
|
||||||
return (int) $this->query("SELECT value from arsse_meta where key is schema_version")->getValue();
|
return (int) $this->query("SELECT value from arsse_meta where key is schema_version")->getValue();
|
||||||
} catch(Exception $e) {
|
} catch (Exception $e) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ abstract class AbstractDriver implements Driver {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function savepointCreate(bool $lock = false): int {
|
public function savepointCreate(bool $lock = false): int {
|
||||||
if($lock && !$this->transDepth) {
|
if ($lock && !$this->transDepth) {
|
||||||
$this->lock();
|
$this->lock();
|
||||||
$this->locked = true;
|
$this->locked = true;
|
||||||
}
|
}
|
||||||
|
@ -36,17 +36,17 @@ abstract class AbstractDriver implements Driver {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function savepointRelease(int $index = null): bool {
|
public function savepointRelease(int $index = null): bool {
|
||||||
if(is_null($index)) {
|
if (is_null($index)) {
|
||||||
$index = $this->transDepth;
|
$index = $this->transDepth;
|
||||||
}
|
}
|
||||||
if(array_key_exists($index, $this->transStatus)) {
|
if (array_key_exists($index, $this->transStatus)) {
|
||||||
switch($this->transStatus[$index]) {
|
switch ($this->transStatus[$index]) {
|
||||||
case self::TR_PEND:
|
case self::TR_PEND:
|
||||||
$this->exec("RELEASE SAVEPOINT arsse_".$index);
|
$this->exec("RELEASE SAVEPOINT arsse_".$index);
|
||||||
$this->transStatus[$index] = self::TR_COMMIT;
|
$this->transStatus[$index] = self::TR_COMMIT;
|
||||||
$a = $index;
|
$a = $index;
|
||||||
while(++$a && $a <= $this->transDepth) {
|
while (++$a && $a <= $this->transDepth) {
|
||||||
if($this->transStatus[$a] <= self::TR_PEND) {
|
if ($this->transStatus[$a] <= self::TR_PEND) {
|
||||||
$this->transStatus[$a] = self::TR_PEND_COMMIT;
|
$this->transStatus[$a] = self::TR_PEND_COMMIT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,13 +66,13 @@ abstract class AbstractDriver implements Driver {
|
||||||
default:
|
default:
|
||||||
throw new Exception("unknownSavepointStatus", $this->transStatus[$index]); //@codeCoverageIgnore
|
throw new Exception("unknownSavepointStatus", $this->transStatus[$index]); //@codeCoverageIgnore
|
||||||
}
|
}
|
||||||
if($index==$this->transDepth) {
|
if ($index==$this->transDepth) {
|
||||||
while($this->transDepth > 0 && $this->transStatus[$this->transDepth] > self::TR_PEND) {
|
while ($this->transDepth > 0 && $this->transStatus[$this->transDepth] > self::TR_PEND) {
|
||||||
array_pop($this->transStatus);
|
array_pop($this->transStatus);
|
||||||
$this->transDepth--;
|
$this->transDepth--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!$this->transDepth && $this->locked) {
|
if (!$this->transDepth && $this->locked) {
|
||||||
$this->unlock();
|
$this->unlock();
|
||||||
$this->locked = false;
|
$this->locked = false;
|
||||||
}
|
}
|
||||||
|
@ -83,18 +83,18 @@ abstract class AbstractDriver implements Driver {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function savepointUndo(int $index = null): bool {
|
public function savepointUndo(int $index = null): bool {
|
||||||
if(is_null($index)) {
|
if (is_null($index)) {
|
||||||
$index = $this->transDepth;
|
$index = $this->transDepth;
|
||||||
}
|
}
|
||||||
if(array_key_exists($index, $this->transStatus)) {
|
if (array_key_exists($index, $this->transStatus)) {
|
||||||
switch($this->transStatus[$index]) {
|
switch ($this->transStatus[$index]) {
|
||||||
case self::TR_PEND:
|
case self::TR_PEND:
|
||||||
$this->exec("ROLLBACK TRANSACTION TO SAVEPOINT arsse_".$index);
|
$this->exec("ROLLBACK TRANSACTION TO SAVEPOINT arsse_".$index);
|
||||||
$this->exec("RELEASE SAVEPOINT arsse_".$index);
|
$this->exec("RELEASE SAVEPOINT arsse_".$index);
|
||||||
$this->transStatus[$index] = self::TR_ROLLBACK;
|
$this->transStatus[$index] = self::TR_ROLLBACK;
|
||||||
$a = $index;
|
$a = $index;
|
||||||
while(++$a && $a <= $this->transDepth) {
|
while (++$a && $a <= $this->transDepth) {
|
||||||
if($this->transStatus[$a] <= self::TR_PEND) {
|
if ($this->transStatus[$a] <= self::TR_PEND) {
|
||||||
$this->transStatus[$a] = self::TR_PEND_ROLLBACK;
|
$this->transStatus[$a] = self::TR_PEND_ROLLBACK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,13 +114,13 @@ abstract class AbstractDriver implements Driver {
|
||||||
default:
|
default:
|
||||||
throw new Exception("unknownSavepointStatus", $this->transStatus[$index]); //@codeCoverageIgnore
|
throw new Exception("unknownSavepointStatus", $this->transStatus[$index]); //@codeCoverageIgnore
|
||||||
}
|
}
|
||||||
if($index==$this->transDepth) {
|
if ($index==$this->transDepth) {
|
||||||
while($this->transDepth > 0 && $this->transStatus[$this->transDepth] > self::TR_PEND) {
|
while ($this->transDepth > 0 && $this->transStatus[$this->transDepth] > self::TR_PEND) {
|
||||||
array_pop($this->transStatus);
|
array_pop($this->transStatus);
|
||||||
$this->transDepth--;
|
$this->transDepth--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!$this->transDepth && $this->locked) {
|
if (!$this->transDepth && $this->locked) {
|
||||||
$this->unlock(true);
|
$this->unlock(true);
|
||||||
$this->locked = false;
|
$this->locked = false;
|
||||||
}
|
}
|
||||||
|
@ -133,4 +133,4 @@ abstract class AbstractDriver implements Driver {
|
||||||
public function prepare(string $query, ...$paramType): Statement {
|
public function prepare(string $query, ...$paramType): Statement {
|
||||||
return $this->prepareArray($query, $paramType);
|
return $this->prepareArray($query, $paramType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Db;
|
namespace JKingWeb\Arsse\Db;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
|
|
||||||
abstract class AbstractStatement implements Statement {
|
abstract class AbstractStatement implements Statement {
|
||||||
|
|
||||||
protected $types = [];
|
protected $types = [];
|
||||||
protected $isNullable = [];
|
protected $isNullable = [];
|
||||||
protected $values = ['pre' => [], 'post' => []];
|
protected $values = ['pre' => [], 'post' => []];
|
||||||
|
|
||||||
abstract function runArray(array $values = []): Result;
|
abstract public function runArray(array $values = []): Result;
|
||||||
|
|
||||||
public function run(...$values): Result {
|
public function run(...$values): Result {
|
||||||
return $this->runArray($values);
|
return $this->runArray($values);
|
||||||
|
@ -20,23 +20,23 @@ abstract class AbstractStatement implements Statement {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rebindArray(array $bindings, bool $append = false): bool {
|
public function rebindArray(array $bindings, bool $append = false): bool {
|
||||||
if(!$append) {
|
if (!$append) {
|
||||||
$this->types = [];
|
$this->types = [];
|
||||||
}
|
}
|
||||||
foreach($bindings as $binding) {
|
foreach ($bindings as $binding) {
|
||||||
if(is_array($binding)) {
|
if (is_array($binding)) {
|
||||||
// recursively flatten any arrays, which may be provided for SET or IN() clauses
|
// recursively flatten any arrays, which may be provided for SET or IN() clauses
|
||||||
$this->rebindArray($binding, true);
|
$this->rebindArray($binding, true);
|
||||||
} else {
|
} else {
|
||||||
$binding = trim(strtolower($binding));
|
$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
|
// "strict" types' values may never be null; null values will later be cast to the type specified
|
||||||
$this->isNullable[] = false;
|
$this->isNullable[] = false;
|
||||||
$binding = substr($binding, 7);
|
$binding = substr($binding, 7);
|
||||||
} else {
|
} else {
|
||||||
$this->isNullable[] = true;
|
$this->isNullable[] = true;
|
||||||
}
|
}
|
||||||
if(!array_key_exists($binding, self::TYPES)) {
|
if (!array_key_exists($binding, self::TYPES)) {
|
||||||
throw new Exception("paramTypeInvalid", $binding); // @codeCoverageIgnore
|
throw new Exception("paramTypeInvalid", $binding); // @codeCoverageIgnore
|
||||||
}
|
}
|
||||||
$this->types[] = self::TYPES[$binding];
|
$this->types[] = self::TYPES[$binding];
|
||||||
|
@ -46,19 +46,19 @@ abstract class AbstractStatement implements Statement {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function cast($v, string $t, bool $nullable) {
|
protected function cast($v, string $t, bool $nullable) {
|
||||||
switch($t) {
|
switch ($t) {
|
||||||
case "date":
|
case "date":
|
||||||
if(is_null($v) && !$nullable) {
|
if (is_null($v) && !$nullable) {
|
||||||
$v = 0;
|
$v = 0;
|
||||||
}
|
}
|
||||||
return Date::transform($v, "date");
|
return Date::transform($v, "date");
|
||||||
case "time":
|
case "time":
|
||||||
if(is_null($v) && !$nullable) {
|
if (is_null($v) && !$nullable) {
|
||||||
$v = 0;
|
$v = 0;
|
||||||
}
|
}
|
||||||
return Date::transform($v, "time");
|
return Date::transform($v, "time");
|
||||||
case "datetime":
|
case "datetime":
|
||||||
if(is_null($v) && !$nullable) {
|
if (is_null($v) && !$nullable) {
|
||||||
$v = 0;
|
$v = 0;
|
||||||
}
|
}
|
||||||
return Date::transform($v, "sql");
|
return Date::transform($v, "sql");
|
||||||
|
@ -68,15 +68,15 @@ abstract class AbstractStatement implements Statement {
|
||||||
case "binary":
|
case "binary":
|
||||||
case "string":
|
case "string":
|
||||||
case "boolean":
|
case "boolean":
|
||||||
if($t=="binary") {
|
if ($t=="binary") {
|
||||||
$t = "string";
|
$t = "string";
|
||||||
}
|
}
|
||||||
if($v instanceof \DateTimeInterface) {
|
if ($v instanceof \DateTimeInterface) {
|
||||||
if($t=="string") {
|
if ($t=="string") {
|
||||||
return Date::transform($v, "sql");
|
return Date::transform($v, "sql");
|
||||||
} else {
|
} else {
|
||||||
$v = $v->getTimestamp();
|
$v = $v->getTimestamp();
|
||||||
settype($v, $t);
|
settype($v, $t);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
settype($v, $t);
|
settype($v, $t);
|
||||||
|
@ -86,4 +86,4 @@ abstract class AbstractStatement implements Statement {
|
||||||
throw new Exception("paramTypeUnknown", $type); // @codeCoverageIgnore
|
throw new Exception("paramTypeUnknown", $type); // @codeCoverageIgnore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,26 +9,26 @@ interface Driver {
|
||||||
const TR_PEND_COMMIT = -1;
|
const TR_PEND_COMMIT = -1;
|
||||||
const TR_PEND_ROLLBACK = -2;
|
const TR_PEND_ROLLBACK = -2;
|
||||||
|
|
||||||
function __construct();
|
public function __construct();
|
||||||
// returns a human-friendly name for the driver (for display in installer, for example)
|
// returns a human-friendly name for the driver (for display in installer, for example)
|
||||||
static function driverName(): string;
|
public static function driverName(): string;
|
||||||
// returns the version of the scheme of the opened database; if uninitialized should return 0
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// ready a prepared statement for later execution
|
||||||
function prepare(string $query, ...$paramType): Statement;
|
public function prepare(string $query, ...$paramType): Statement;
|
||||||
function prepareArray(string $query, array $paramTypes): Statement;
|
public function prepareArray(string $query, array $paramTypes): Statement;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Db;
|
namespace JKingWeb\Arsse\Db;
|
||||||
|
|
||||||
class Exception extends \JKingWeb\Arsse\AbstractException {
|
class Exception extends \JKingWeb\Arsse\AbstractException {
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Db;
|
namespace JKingWeb\Arsse\Db;
|
||||||
|
|
||||||
class ExceptionInput extends \JKingWeb\Arsse\AbstractException {
|
class ExceptionInput extends \JKingWeb\Arsse\AbstractException {
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Db;
|
namespace JKingWeb\Arsse\Db;
|
||||||
|
|
||||||
class ExceptionSavepoint extends \JKingWeb\Arsse\AbstractException {
|
class ExceptionSavepoint extends \JKingWeb\Arsse\AbstractException {
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Db;
|
namespace JKingWeb\Arsse\Db;
|
||||||
|
|
||||||
class ExceptionTimeout extends \JKingWeb\Arsse\AbstractException {
|
class ExceptionTimeout extends \JKingWeb\Arsse\AbstractException {
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,16 +3,16 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Db;
|
namespace JKingWeb\Arsse\Db;
|
||||||
|
|
||||||
interface Result extends \Iterator {
|
interface Result extends \Iterator {
|
||||||
function current();
|
public function current();
|
||||||
function key();
|
public function key();
|
||||||
function next();
|
public function next();
|
||||||
function rewind();
|
public function rewind();
|
||||||
function valid();
|
public function valid();
|
||||||
|
|
||||||
function getRow();
|
public function getRow();
|
||||||
function getAll(): array;
|
public function getAll(): array;
|
||||||
function getValue();
|
public function getValue();
|
||||||
|
|
||||||
function changes();
|
public function changes();
|
||||||
function lastId();
|
public function lastId();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Db\SQLite3;
|
namespace JKingWeb\Arsse\Db\SQLite3;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
use JKingWeb\Arsse\Db\Exception;
|
use JKingWeb\Arsse\Db\Exception;
|
||||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||||
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
||||||
|
|
||||||
|
|
||||||
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||||
use ExceptionBuilder;
|
use ExceptionBuilder;
|
||||||
|
|
||||||
|
@ -18,11 +18,11 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
// check to make sure required extension is loaded
|
// check to make sure required extension is loaded
|
||||||
if(!class_exists("SQLite3")) {
|
if (!class_exists("SQLite3")) {
|
||||||
throw new Exception("extMissing", self::driverName()); // @codeCoverageIgnore
|
throw new Exception("extMissing", self::driverName()); // @codeCoverageIgnore
|
||||||
}
|
}
|
||||||
$dbFile = Arsse::$conf->dbSQLite3File;
|
$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
|
// if no database file is specified in the configuration, use a suitable default
|
||||||
$dbFile = \JKingWeb\Arsse\BASE."arsse.db";
|
$dbFile = \JKingWeb\Arsse\BASE."arsse.db";
|
||||||
}
|
}
|
||||||
|
@ -34,21 +34,21 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||||
$this->db->enableExceptions(true);
|
$this->db->enableExceptions(true);
|
||||||
$this->exec("PRAGMA journal_mode = wal");
|
$this->exec("PRAGMA journal_mode = wal");
|
||||||
$this->exec("PRAGMA foreign_keys = yes");
|
$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
|
// if opening the database doesn't work, check various pre-conditions to find out what the problem might be
|
||||||
$files = [
|
$files = [
|
||||||
$dbFile, // main database file
|
$dbFile, // main database file
|
||||||
$dbFile."-wal", // write-ahead log journal
|
$dbFile."-wal", // write-ahead log journal
|
||||||
$dbFile."-shm", // shared memory index
|
$dbFile."-shm", // shared memory index
|
||||||
];
|
];
|
||||||
foreach($files as $file) {
|
foreach ($files as $file) {
|
||||||
if(!file_exists($file) && !is_writable(dirname($file))) {
|
if (!file_exists($file) && !is_writable(dirname($file))) {
|
||||||
throw new Exception("fileUncreatable", $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);
|
throw new Exception("fileUnusable", $file);
|
||||||
} else if(!is_readable($file)) {
|
} elseif (!is_readable($file)) {
|
||||||
throw new Exception("fileUnreadable", $file);
|
throw new Exception("fileUnreadable", $file);
|
||||||
} else if(!is_writable($file)) {
|
} elseif (!is_writable($file)) {
|
||||||
throw new Exception("fileUnwritable", $file);
|
throw new Exception("fileUnwritable", $file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,12 +64,15 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __destruct() {
|
public function __destruct() {
|
||||||
try{$this->db->close();} catch(\Exception $e) {}
|
try {
|
||||||
|
$this->db->close();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
}
|
||||||
unset($this->db);
|
unset($this->db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static public function driverName(): string {
|
public static function driverName(): string {
|
||||||
return Arsse::$lang->msg("Driver.Db.SQLite3.Name");
|
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 {
|
public function schemaUpdate(int $to, string $basePath = null): bool {
|
||||||
$ver = $this->schemaVersion();
|
$ver = $this->schemaVersion();
|
||||||
if(!Arsse::$conf->dbAutoUpdate) {
|
if (!Arsse::$conf->dbAutoUpdate) {
|
||||||
throw new Exception("updateManual", ['version' => $ver, 'driver_name' => $this->driverName()]);
|
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()]);
|
throw new Exception("updateTooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]);
|
||||||
}
|
}
|
||||||
$sep = \DIRECTORY_SEPARATOR;
|
$sep = \DIRECTORY_SEPARATOR;
|
||||||
$path = ($basePath ?? \JKingWeb\Arsse\BASE."sql").$sep."SQLite3".$sep;
|
$path = ($basePath ?? \JKingWeb\Arsse\BASE."sql").$sep."SQLite3".$sep;
|
||||||
// lock the database
|
// lock the database
|
||||||
$this->savepointCreate(true);
|
$this->savepointCreate(true);
|
||||||
for($a = $this->schemaVersion(); $a < $to; $a++) {
|
for ($a = $this->schemaVersion(); $a < $to; $a++) {
|
||||||
$this->savepointCreate();
|
$this->savepointCreate();
|
||||||
try {
|
try {
|
||||||
$file = $path.$a.".sql";
|
$file = $path.$a.".sql";
|
||||||
if(!file_exists($file)) {
|
if (!file_exists($file)) {
|
||||||
throw new Exception("updateFileMissing", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
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]);
|
throw new Exception("updateFileUnreadable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
||||||
}
|
}
|
||||||
$sql = @file_get_contents($file);
|
$sql = @file_get_contents($file);
|
||||||
if($sql===false) {
|
if ($sql===false) {
|
||||||
throw new Exception("updateFileUnusable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); // @codeCoverageIgnore
|
throw new Exception("updateFileUnusable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); // @codeCoverageIgnore
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
$this->exec($sql);
|
$this->exec($sql);
|
||||||
} catch(\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
throw new Exception("updateFileError", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a, 'message' => $this->getError()]);
|
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]);
|
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
|
// undo any partial changes from the failed update
|
||||||
$this->savepointUndo();
|
$this->savepointUndo();
|
||||||
// commit any successful updates if updating by more than one version
|
// 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 {
|
public function exec(string $query): bool {
|
||||||
try {
|
try {
|
||||||
return (bool) $this->db->exec($query);
|
return (bool) $this->db->exec($query);
|
||||||
} catch(\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
|
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
|
||||||
throw new $excClass($excMsg, $excData);
|
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 {
|
public function query(string $query): \JKingWeb\Arsse\Db\Result {
|
||||||
try {
|
try {
|
||||||
$r = $this->db->query($query);
|
$r = $this->db->query($query);
|
||||||
} catch(\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
|
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
|
||||||
throw new $excClass($excMsg, $excData);
|
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 {
|
public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement {
|
||||||
try {
|
try {
|
||||||
$s = $this->db->prepare($query);
|
$s = $this->db->prepare($query);
|
||||||
} catch(\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
|
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
|
||||||
throw new $excClass($excMsg, $excData);
|
throw new $excClass($excMsg, $excData);
|
||||||
}
|
}
|
||||||
|
@ -167,4 +170,4 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||||
$this->exec((!$rollback) ? "COMMIT" : "ROLLBACK");
|
$this->exec((!$rollback) ? "COMMIT" : "ROLLBACK");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Db\SQLite3;
|
namespace JKingWeb\Arsse\Db\SQLite3;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Db\Exception;
|
use JKingWeb\Arsse\Db\Exception;
|
||||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||||
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
||||||
|
|
||||||
|
|
||||||
trait ExceptionBuilder {
|
trait ExceptionBuilder {
|
||||||
|
|
||||||
public function exceptionBuild() {
|
public function exceptionBuild() {
|
||||||
switch($this->db->lastErrorCode()) {
|
switch ($this->db->lastErrorCode()) {
|
||||||
case self::SQLITE_BUSY:
|
case self::SQLITE_BUSY:
|
||||||
return [ExceptionTimeout::class, 'general', $this->db->lastErrorMsg()];
|
return [ExceptionTimeout::class, 'general', $this->db->lastErrorMsg()];
|
||||||
case self::SQLITE_CONSTRAINT:
|
case self::SQLITE_CONSTRAINT:
|
||||||
|
@ -20,4 +19,4 @@ trait ExceptionBuilder {
|
||||||
return [Exception::class, 'engineErrorGeneral', $this->db->lastErrorMsg()];
|
return [Exception::class, 'engineErrorGeneral', $this->db->lastErrorMsg()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ class Result implements \JKingWeb\Arsse\Db\Result {
|
||||||
|
|
||||||
public function getValue() {
|
public function getValue() {
|
||||||
$this->next();
|
$this->next();
|
||||||
if($this->valid()) {
|
if ($this->valid()) {
|
||||||
$keys = array_keys($this->cur);
|
$keys = array_keys($this->cur);
|
||||||
return $this->cur[array_shift($keys)];
|
return $this->cur[array_shift($keys)];
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ class Result implements \JKingWeb\Arsse\Db\Result {
|
||||||
|
|
||||||
public function getAll(): array {
|
public function getAll(): array {
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach($this as $row) {
|
foreach ($this as $row) {
|
||||||
$out [] = $row;
|
$out [] = $row;
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
|
@ -52,7 +52,10 @@ class Result implements \JKingWeb\Arsse\Db\Result {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __destruct() {
|
public function __destruct() {
|
||||||
try{$this->set->finalize();} catch(\Throwable $e) {}
|
try {
|
||||||
|
$this->set->finalize();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
}
|
||||||
unset($this->set);
|
unset($this->set);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,4 +84,4 @@ class Result implements \JKingWeb\Arsse\Db\Result {
|
||||||
$this->cur = null;
|
$this->cur = null;
|
||||||
$this->set->reset();
|
$this->set->reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Db\SQLite3;
|
namespace JKingWeb\Arsse\Db\SQLite3;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Db\Exception;
|
use JKingWeb\Arsse\Db\Exception;
|
||||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||||
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
||||||
|
@ -33,7 +34,10 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __destruct() {
|
public function __destruct() {
|
||||||
try {$this->st->close();} catch(\Throwable $e) {}
|
try {
|
||||||
|
$this->st->close();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
}
|
||||||
unset($this->st);
|
unset($this->st);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +46,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
|
||||||
$this->bindValues($values);
|
$this->bindValues($values);
|
||||||
try {
|
try {
|
||||||
$r = $this->st->execute();
|
$r = $this->st->execute();
|
||||||
} catch(\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
|
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
|
||||||
throw new $excClass($excMsg, $excData);
|
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 {
|
protected function bindValues(array $values, int $offset = 0): int {
|
||||||
$a = $offset;
|
$a = $offset;
|
||||||
foreach($values as $value) {
|
foreach ($values as $value) {
|
||||||
if(is_array($value)) {
|
if (is_array($value)) {
|
||||||
// recursively flatten any arrays, which may be provided for SET or IN() clauses
|
// recursively flatten any arrays, which may be provided for SET or IN() clauses
|
||||||
$a += $this->bindValues($value, $a);
|
$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
|
// 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]));
|
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 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);
|
$this->st->bindValue($a+1, null, \SQLITE3_NULL);
|
||||||
} else {
|
} else {
|
||||||
// otherwise cast the value to the right type and bind the result
|
// otherwise cast the value to the right type and bind the result
|
||||||
$type = self::BINDINGS[$this->types[$a]];
|
$type = self::BINDINGS[$this->types[$a]];
|
||||||
$value = $this->cast($value, $this->types[$a], $this->isNullable[$a]);
|
$value = $this->cast($value, $this->types[$a], $this->isNullable[$a]);
|
||||||
// re-adjust for null casts
|
// re-adjust for null casts
|
||||||
if($value===null) {
|
if ($value===null) {
|
||||||
$type = \SQLITE3_NULL;
|
$type = \SQLITE3_NULL;
|
||||||
}
|
}
|
||||||
// perform binding
|
// perform binding
|
||||||
|
@ -81,4 +85,4 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
|
||||||
}
|
}
|
||||||
return $a - $offset;
|
return $a - $offset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,8 @@ interface Statement {
|
||||||
"bit" => "boolean",
|
"bit" => "boolean",
|
||||||
];
|
];
|
||||||
|
|
||||||
function run(...$values): Result;
|
public function run(...$values): Result;
|
||||||
function runArray(array $values = []): Result;
|
public function runArray(array $values = []): Result;
|
||||||
function rebind(...$bindings): bool;
|
public function rebind(...$bindings): bool;
|
||||||
function rebindArray(array $bindings): bool;
|
public function rebindArray(array $bindings): bool;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,39 +7,39 @@ class Transaction {
|
||||||
protected $pending = false;
|
protected $pending = false;
|
||||||
protected $drv;
|
protected $drv;
|
||||||
|
|
||||||
function __construct(Driver $drv, bool $lock = false) {
|
public function __construct(Driver $drv, bool $lock = false) {
|
||||||
$this->index = $drv->savepointCreate($lock);
|
$this->index = $drv->savepointCreate($lock);
|
||||||
$this->drv = $drv;
|
$this->drv = $drv;
|
||||||
$this->pending = true;
|
$this->pending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function __destruct() {
|
public function __destruct() {
|
||||||
if($this->pending) {
|
if ($this->pending) {
|
||||||
try {
|
try {
|
||||||
$this->drv->savepointUndo($this->index);
|
$this->drv->savepointUndo($this->index);
|
||||||
} catch(\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function commit(): bool {
|
public function commit(): bool {
|
||||||
$out = $this->drv->savepointRelease($this->index);
|
$out = $this->drv->savepointRelease($this->index);
|
||||||
$this->pending = false;
|
$this->pending = false;
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rollback(): bool {
|
public function rollback(): bool {
|
||||||
$out = $this->drv->savepointUndo($this->index);
|
$out = $this->drv->savepointUndo($this->index);
|
||||||
$this->pending = false;
|
$this->pending = false;
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIndex(): int {
|
public function getIndex(): int {
|
||||||
return $this->index;
|
return $this->index;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPending(): bool {
|
public function isPending(): bool {
|
||||||
return $this->pending;
|
return $this->pending;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
class Exception extends AbstractException {
|
class Exception extends AbstractException {
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,4 +6,4 @@ class ExceptionFatal extends AbstractException {
|
||||||
public function __construct($msg = "", $code = 0, $e = null) {
|
public function __construct($msg = "", $code = 0, $e = null) {
|
||||||
\Exception::__construct($msg, $code, $e);
|
\Exception::__construct($msg, $code, $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
121
lib/Feed.php
121
lib/Feed.php
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
use PicoFeed\PicoFeedException;
|
use PicoFeed\PicoFeedException;
|
||||||
use PicoFeed\Config\Config;
|
use PicoFeed\Config\Config;
|
||||||
|
@ -8,7 +9,7 @@ use PicoFeed\Reader\Reader;
|
||||||
use PicoFeed\Reader\Favicon;
|
use PicoFeed\Reader\Favicon;
|
||||||
use PicoFeed\Scraper\Scraper;
|
use PicoFeed\Scraper\Scraper;
|
||||||
|
|
||||||
class Feed {
|
class Feed {
|
||||||
public $data = null;
|
public $data = null;
|
||||||
public $favicon;
|
public $favicon;
|
||||||
public $parser;
|
public $parser;
|
||||||
|
@ -38,25 +39,25 @@ class Feed {
|
||||||
$this->download($url, $lastModified, $etag, $username, $password);
|
$this->download($url, $lastModified, $etag, $username, $password);
|
||||||
// format the HTTP Last-Modified date returned
|
// format the HTTP Last-Modified date returned
|
||||||
$lastMod = $this->resource->getLastModified();
|
$lastMod = $this->resource->getLastModified();
|
||||||
if(strlen($lastMod)) {
|
if (strlen($lastMod)) {
|
||||||
$this->lastModified = Date::normalize($lastMod, "http");
|
$this->lastModified = Date::normalize($lastMod, "http");
|
||||||
}
|
}
|
||||||
$this->modified = $this->resource->isModified();
|
$this->modified = $this->resource->isModified();
|
||||||
//parse the feed, if it has been modified
|
//parse the feed, if it has been modified
|
||||||
if($this->modified) {
|
if ($this->modified) {
|
||||||
$this->parse();
|
$this->parse();
|
||||||
// ascertain whether there are any articles not in the database
|
// ascertain whether there are any articles not in the database
|
||||||
$this->matchToDatabase($feedID);
|
$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 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();
|
$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
|
// 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;
|
$this->modified = false;
|
||||||
}
|
}
|
||||||
// if requested, scrape full content for any new and changed items
|
// if requested, scrape full content for any new and changed items
|
||||||
if($scrape) {
|
if ($scrape) {
|
||||||
$this->scrape();
|
$this->scrape();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,19 +108,19 @@ class Feed {
|
||||||
// id doesn't exist.
|
// id doesn't exist.
|
||||||
$content = $f->content.$f->enclosureUrl.$f->enclosureType;
|
$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 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 = "";
|
$f->urlTitleHash = "";
|
||||||
} else {
|
} else {
|
||||||
$f->urlTitleHash = hash('sha256', $f->url.$f->title);
|
$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 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) {
|
if (!strlen($content) && $f->url==$feed->siteUrl) {
|
||||||
$f->urlContentHash = "";
|
$f->urlContentHash = "";
|
||||||
} else {
|
} else {
|
||||||
$f->urlContentHash = hash('sha256', $f->url.$content);
|
$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 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 = "";
|
$f->titleContentHash = "";
|
||||||
} else {
|
} else {
|
||||||
$f->titleContentHash = hash('sha256', $f->title.$content);
|
$f->titleContentHash = hash('sha256', $f->title.$content);
|
||||||
|
@ -128,44 +129,44 @@ class Feed {
|
||||||
// prefer an Atom ID as the item's ID
|
// prefer an Atom ID as the item's ID
|
||||||
$id = (string) $f->xml->children('http://www.w3.org/2005/Atom')->id;
|
$id = (string) $f->xml->children('http://www.w3.org/2005/Atom')->id;
|
||||||
// otherwise use the RSS2 guid element
|
// otherwise use the RSS2 guid element
|
||||||
if(!strlen($id)) {
|
if (!strlen($id)) {
|
||||||
$id = (string) $f->xml->guid;
|
$id = (string) $f->xml->guid;
|
||||||
}
|
}
|
||||||
// otherwise use the Dublin Core identifier element
|
// 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;
|
$id = (string) $f->xml->children('http://purl.org/dc/elements/1.1/')->identifier;
|
||||||
}
|
}
|
||||||
// otherwise there is no ID; if there is one, hash it
|
// otherwise there is no ID; if there is one, hash it
|
||||||
if(strlen($id)) {
|
if (strlen($id)) {
|
||||||
$f->id = hash('sha256', $id);
|
$f->id = hash('sha256', $id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PicoFeed also doesn't gather up categories, so we do this as well
|
// PicoFeed also doesn't gather up categories, so we do this as well
|
||||||
$f->categories = [];
|
$f->categories = [];
|
||||||
// first add Atom 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
|
// if the category has a label, use that
|
||||||
$name = (string) $c->attributes()->label;
|
$name = (string) $c->attributes()->label;
|
||||||
// otherwise use the term
|
// otherwise use the term
|
||||||
if(!strlen($name)) {
|
if (!strlen($name)) {
|
||||||
$name = (string) $c->attributes()->term;
|
$name = (string) $c->attributes()->term;
|
||||||
}
|
}
|
||||||
// ... assuming it has that much
|
// ... assuming it has that much
|
||||||
if(strlen($name)) {
|
if (strlen($name)) {
|
||||||
$f->categories[] = $name;
|
$f->categories[] = $name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// next add RSS2 categories
|
// next add RSS2 categories
|
||||||
foreach($f->xml->children()->category as $c) {
|
foreach ($f->xml->children()->category as $c) {
|
||||||
$name = (string) $c;
|
$name = (string) $c;
|
||||||
if(strlen($name)) {
|
if (strlen($name)) {
|
||||||
$f->categories[] = $name;
|
$f->categories[] = $name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// and finally try Dublin Core subjects
|
// 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;
|
$name = (string) $c;
|
||||||
if(strlen($name)) {
|
if (strlen($name)) {
|
||||||
$f->categories[] = $name;
|
$f->categories[] = $name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,26 +179,26 @@ class Feed {
|
||||||
|
|
||||||
protected function deduplicateItems(array $items): array {
|
protected function deduplicateItems(array $items): array {
|
||||||
/* Rationale:
|
/* 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
|
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.
|
present within the feed.
|
||||||
*/
|
*/
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach($items as $item) {
|
foreach ($items as $item) {
|
||||||
foreach($out as $index => $check) {
|
foreach ($out as $index => $check) {
|
||||||
// if the two items both have IDs and they differ, they do not match, regardless of hashes
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
// if the two items have the same ID or any one hash matches, they are two versions of the same item
|
// 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->id && $check->id && $item->id == $check->id) ||
|
||||||
($item->urlTitleHash && $item->urlTitleHash == $check->urlTitleHash) ||
|
($item->urlTitleHash && $item->urlTitleHash == $check->urlTitleHash) ||
|
||||||
($item->urlContentHash && $item->urlContentHash == $check->urlContentHash) ||
|
($item->urlContentHash && $item->urlContentHash == $check->urlContentHash) ||
|
||||||
($item->titleContentHash && $item->titleContentHash == $check->titleContentHash)
|
($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
|
// the later item has an update date and the existing item does not
|
||||||
($item->updatedDate && !$check->updatedDate) ||
|
($item->updatedDate && !$check->updatedDate) ||
|
||||||
// the later item has an update date newer than the existing item's
|
// the later item has an update date newer than the existing item's
|
||||||
|
@ -224,7 +225,7 @@ class Feed {
|
||||||
// first perform deduplication on items
|
// first perform deduplication on items
|
||||||
$items = $this->deduplicateItems($this->data->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 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;
|
$this->newItems = $items;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -232,20 +233,20 @@ class Feed {
|
||||||
$articles = Arsse::$db->feedMatchLatest($feedID, sizeof($items))->getAll();
|
$articles = Arsse::$db->feedMatchLatest($feedID, sizeof($items))->getAll();
|
||||||
// perform a first pass matching the latest articles against items in the feed
|
// perform a first pass matching the latest articles against items in the feed
|
||||||
list($this->newItems, $this->changedItems) = $this->matchItems($items, $articles);
|
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
|
// 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 = [];
|
$ids = $hashesUT = $hashesUC = $hashesTC = [];
|
||||||
foreach($this->newItems as $i) {
|
foreach ($this->newItems as $i) {
|
||||||
if($i->id) {
|
if ($i->id) {
|
||||||
$ids[] = $i->id;
|
$ids[] = $i->id;
|
||||||
}
|
}
|
||||||
if($i->urlTitleHash) {
|
if ($i->urlTitleHash) {
|
||||||
$hashesUT[] = $i->urlTitleHash;
|
$hashesUT[] = $i->urlTitleHash;
|
||||||
}
|
}
|
||||||
if($i->urlContentHash) {
|
if ($i->urlContentHash) {
|
||||||
$hashesUC[] = $i->urlContentHash;
|
$hashesUC[] = $i->urlContentHash;
|
||||||
}
|
}
|
||||||
if($i->titleContentHash) {
|
if ($i->titleContentHash) {
|
||||||
$hashesTC[] = $i->titleContentHash;
|
$hashesTC[] = $i->titleContentHash;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -260,14 +261,14 @@ class Feed {
|
||||||
protected function matchItems(array $items, array $articles): array {
|
protected function matchItems(array $items, array $articles): array {
|
||||||
$new = $edited = [];
|
$new = $edited = [];
|
||||||
// iterate through the articles and for each determine whether it is existing, edited, or entirely new
|
// 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;
|
$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 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;
|
continue;
|
||||||
}
|
}
|
||||||
if(
|
if (
|
||||||
// the item matches if the GUID matches...
|
// the item matches if the GUID matches...
|
||||||
($i->id && $i->id === $a['guid']) ||
|
($i->id && $i->id === $a['guid']) ||
|
||||||
// ... or if any one of the hashes match
|
// ... or if any one of the hashes match
|
||||||
|
@ -275,13 +276,13 @@ class Feed {
|
||||||
($i->urlContentHash && $i->urlContentHash === $a['url_content_hash']) ||
|
($i->urlContentHash && $i->urlContentHash === $a['url_content_hash']) ||
|
||||||
($i->titleContentHash && $i->titleContentHash === $a['title_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
|
// 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
|
// we store the item index and database record ID as a key/value pair
|
||||||
$found = true;
|
$found = true;
|
||||||
$edited[$a['id']] = $i;
|
$edited[$a['id']] = $i;
|
||||||
break;
|
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
|
// if any of the hashes do not match, then the article has been edited
|
||||||
$found = true;
|
$found = true;
|
||||||
$edited[$a['id']] = $i;
|
$edited[$a['id']] = $i;
|
||||||
|
@ -293,7 +294,7 @@ class Feed {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!$found) {
|
if (!$found) {
|
||||||
$new[] = $i;
|
$new[] = $i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -302,7 +303,7 @@ class Feed {
|
||||||
|
|
||||||
protected function computeNextFetch(): \DateTime {
|
protected function computeNextFetch(): \DateTime {
|
||||||
$now = Date::normalize(time());
|
$now = Date::normalize(time());
|
||||||
if(!$this->modified) {
|
if (!$this->modified) {
|
||||||
$diff = $now->getTimestamp() - $this->lastModified->getTimestamp();
|
$diff = $now->getTimestamp() - $this->lastModified->getTimestamp();
|
||||||
$offset = $this->normalizeDateDiff($diff);
|
$offset = $this->normalizeDateDiff($diff);
|
||||||
$now->modify("+".$offset);
|
$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.
|
// interval is "less than 30m"). If there is no commonality, the feed is checked in 1 hour.
|
||||||
$offsets = [];
|
$offsets = [];
|
||||||
$dates = $this->gatherDates();
|
$dates = $this->gatherDates();
|
||||||
if(sizeof($dates) > 3) {
|
if (sizeof($dates) > 3) {
|
||||||
for($a = 0; $a < 3; $a++) {
|
for ($a = 0; $a < 3; $a++) {
|
||||||
$diff = $dates[$a] - $dates[$a+1];
|
$diff = $dates[$a] - $dates[$a+1];
|
||||||
$offsets[] = $this->normalizeDateDiff($diff);
|
$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]);
|
$now->modify("+".$offsets[0]);
|
||||||
} else if($offsets[1]==$offsets[2]) {
|
} elseif ($offsets[1]==$offsets[2]) {
|
||||||
$now->modify("+".$offsets[1]);
|
$now->modify("+".$offsets[1]);
|
||||||
} else {
|
} else {
|
||||||
$now->modify("+ 1 hour");
|
$now->modify("+ 1 hour");
|
||||||
|
@ -333,9 +334,9 @@ class Feed {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function nextFetchOnError($errCount): \DateTime {
|
public static function nextFetchOnError($errCount): \DateTime {
|
||||||
if($errCount < 3) {
|
if ($errCount < 3) {
|
||||||
$offset = "5 minutes";
|
$offset = "5 minutes";
|
||||||
} else if($errCount < 15) {
|
} elseif ($errCount < 15) {
|
||||||
$offset = "3 hours";
|
$offset = "3 hours";
|
||||||
} else {
|
} else {
|
||||||
$offset = "1 day";
|
$offset = "1 day";
|
||||||
|
@ -344,13 +345,13 @@ class Feed {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function normalizeDateDiff(int $diff): string {
|
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";
|
$offset = "15 minutes";
|
||||||
} else if($diff < (60 * 60)) { // less than an hour
|
} elseif ($diff < (60 * 60)) { // less than an hour
|
||||||
$offset = "30 minutes";
|
$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";
|
$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";
|
$offset = "1 day";
|
||||||
} else {
|
} else {
|
||||||
$offset = "3 hours";
|
$offset = "3 hours";
|
||||||
|
@ -359,11 +360,11 @@ class Feed {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function computeLastModified() {
|
protected function computeLastModified() {
|
||||||
if(!$this->modified) {
|
if (!$this->modified) {
|
||||||
return $this->lastModified;
|
return $this->lastModified;
|
||||||
}
|
}
|
||||||
$dates = $this->gatherDates();
|
$dates = $this->gatherDates();
|
||||||
if(sizeof($dates)) {
|
if (sizeof($dates)) {
|
||||||
return Date::normalize($dates[0]);
|
return Date::normalize($dates[0]);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
@ -372,11 +373,11 @@ class Feed {
|
||||||
|
|
||||||
protected function gatherDates(): array {
|
protected function gatherDates(): array {
|
||||||
$dates = [];
|
$dates = [];
|
||||||
foreach($this->data->items as $item) {
|
foreach ($this->data->items as $item) {
|
||||||
if($item->updatedDate) {
|
if ($item->updatedDate) {
|
||||||
$dates[] = $item->updatedDate->getTimestamp();
|
$dates[] = $item->updatedDate->getTimestamp();
|
||||||
}
|
}
|
||||||
if($item->publishedDate) {
|
if ($item->publishedDate) {
|
||||||
$dates[] = $item->publishedDate->getTimestamp();
|
$dates[] = $item->publishedDate->getTimestamp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -387,13 +388,13 @@ class Feed {
|
||||||
|
|
||||||
protected function scrape(): bool {
|
protected function scrape(): bool {
|
||||||
$scraper = new Scraper($this->config);
|
$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->setUrl($item->url);
|
||||||
$scraper->execute();
|
$scraper->execute();
|
||||||
if($scraper->hasRelevantContent()) {
|
if ($scraper->hasRelevantContent()) {
|
||||||
$item->content = $scraper->getFilteredContent();
|
$item->content = $scraper->getFilteredContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,4 +11,4 @@ class Exception extends \JKingWeb\Arsse\AbstractException {
|
||||||
$msgID = ($msgID !== $className) ? lcfirst($msgID) : '';
|
$msgID = ($msgID !== $className) ? lcfirst($msgID) : '';
|
||||||
parent::__construct($msgID, ['url' => $url], $e);
|
parent::__construct($msgID, ['url' => $url], $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
84
lib/Lang.php
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
|
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 $synched = false; // whether the wanted locale is actually loaded (lazy loading is used by default)
|
||||||
protected $wanted = self::DEFAULT; // the currently requested locale
|
protected $wanted = self::DEFAULT; // the currently requested locale
|
||||||
protected $locale = ""; // the currently loaded locale
|
protected $locale = ""; // the currently loaded locale
|
||||||
protected $loaded = []; // the cascade of loaded locale file names
|
protected $loaded = []; // the cascade of loaded locale file names
|
||||||
protected $strings = self::REQUIRED; // the loaded locale strings, merged
|
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;
|
$this->path = $path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function set(string $locale, bool $immediate = false): string {
|
public function set(string $locale, bool $immediate = false): string {
|
||||||
// make sure the Intl extension is loaded
|
// make sure the Intl extension is loaded
|
||||||
if(!static::$requirementsMet) {
|
if (!static::$requirementsMet) {
|
||||||
static::checkRequirements();
|
static::checkRequirements();
|
||||||
}
|
}
|
||||||
// if requesting the same locale as already wanted, just return (but load first if we've requested an immediate load)
|
// 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 ($locale==$this->wanted) {
|
||||||
if($immediate && !$this->synched) {
|
if ($immediate && !$this->synched) {
|
||||||
$this->load();
|
$this->load();
|
||||||
}
|
}
|
||||||
return $locale;
|
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 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();
|
$list = $this->listFiles();
|
||||||
// if the default locale is unavailable, this is (for now) an error
|
// 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);
|
throw new Lang\Exception("defaultFileMissing", self::DEFAULT);
|
||||||
}
|
}
|
||||||
$this->wanted = $this->match($locale, $list);
|
$this->wanted = $this->match($locale, $list);
|
||||||
|
@ -52,7 +52,7 @@ class Lang {
|
||||||
}
|
}
|
||||||
$this->synched = false;
|
$this->synched = false;
|
||||||
// load right now if asked to, otherwise load later when actually required
|
// load right now if asked to, otherwise load later when actually required
|
||||||
if($immediate) {
|
if ($immediate) {
|
||||||
$this->load();
|
$this->load();
|
||||||
}
|
}
|
||||||
return $this->wanted;
|
return $this->wanted;
|
||||||
|
@ -73,29 +73,33 @@ class Lang {
|
||||||
|
|
||||||
public function __invoke(string $msgID, $vars = null): string {
|
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 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->synched) {
|
||||||
if($this->wanted==self::DEFAULT) {
|
try {
|
||||||
$this->set("", true);
|
$this->load();
|
||||||
} else {
|
} catch (Lang\Exception $e) {
|
||||||
throw $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
|
// 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
|
// note that this is indicative of a programming error since the default locale should have all strings
|
||||||
if(!array_key_exists($msgID, $this->strings)) {
|
if (!array_key_exists($msgID, $this->strings)) {
|
||||||
throw new Lang\Exception("stringMissing", ['msgID' => $msgID, 'fileList' => implode(", ",$this->loaded)]);
|
throw new Lang\Exception("stringMissing", ['msgID' => $msgID, 'fileList' => implode(", ", $this->loaded)]);
|
||||||
}
|
}
|
||||||
$msg = $this->strings[$msgID];
|
$msg = $this->strings[$msgID];
|
||||||
// variables fed to MessageFormatter must be contained in an array
|
// 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
|
// 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 = [];
|
$vars = [];
|
||||||
} else if(!is_array($vars)) {
|
} elseif (!is_array($vars)) {
|
||||||
$vars = [$vars];
|
$vars = [$vars];
|
||||||
}
|
}
|
||||||
$msg = \MessageFormatter::formatMessage($this->locale, $msg, $vars);
|
$msg = \MessageFormatter::formatMessage($this->locale, $msg, $vars);
|
||||||
if($msg===false) {
|
if ($msg===false) {
|
||||||
throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ",$this->loaded)]);
|
throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ", $this->loaded)]);
|
||||||
}
|
}
|
||||||
return $msg;
|
return $msg;
|
||||||
}
|
}
|
||||||
|
@ -103,22 +107,22 @@ class Lang {
|
||||||
public function list(string $locale = ""): array {
|
public function list(string $locale = ""): array {
|
||||||
$out = [];
|
$out = [];
|
||||||
$files = $this->listFiles();
|
$files = $this->listFiles();
|
||||||
foreach($files as $tag) {
|
foreach ($files as $tag) {
|
||||||
$out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale);
|
$out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale);
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function match(string $locale, array $list = null): string {
|
public function match(string $locale, array $list = null): string {
|
||||||
if($list===null) {
|
if ($list===null) {
|
||||||
$list = $this->listFiles();
|
$list = $this->listFiles();
|
||||||
}
|
}
|
||||||
$default = ($this->locale=="") ? self::DEFAULT : $this->locale;
|
$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 {
|
protected static function checkRequirements(): bool {
|
||||||
if(!extension_loaded("intl")) {
|
if (!extension_loaded("intl")) {
|
||||||
throw new ExceptionFatal("The \"Intl\" extension is required, but not loaded");
|
throw new ExceptionFatal("The \"Intl\" extension is required, but not loaded");
|
||||||
}
|
}
|
||||||
static::$requirementsMet = true;
|
static::$requirementsMet = true;
|
||||||
|
@ -133,22 +137,22 @@ class Lang {
|
||||||
protected function listFiles(): array {
|
protected function listFiles(): array {
|
||||||
$out = $this->globFiles($this->path."*.php");
|
$out = $this->globFiles($this->path."*.php");
|
||||||
// trim the returned file paths to return just the language tag
|
// 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 = 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);
|
$file = substr($file, strrpos($file, "/")+1);
|
||||||
return strtolower(substr($file,0,strrpos($file,".")));
|
return strtolower(substr($file, 0, strrpos($file, ".")));
|
||||||
},$out);
|
}, $out);
|
||||||
// sort the results
|
// sort the results
|
||||||
natsort($out);
|
natsort($out);
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function load(): bool {
|
protected function load(): bool {
|
||||||
if(!self::$requirementsMet) {
|
if (!self::$requirementsMet) {
|
||||||
self::checkRequirements();
|
self::checkRequirements();
|
||||||
}
|
}
|
||||||
// if we've requested no locale (""), just load the fallback strings and return
|
// if we've requested no locale (""), just load the fallback strings and return
|
||||||
if($this->wanted=="") {
|
if ($this->wanted=="") {
|
||||||
$this->strings = self::REQUIRED;
|
$this->strings = self::REQUIRED;
|
||||||
$this->locale = $this->wanted;
|
$this->locale = $this->wanted;
|
||||||
$this->synched = true;
|
$this->synched = true;
|
||||||
|
@ -157,27 +161,27 @@ class Lang {
|
||||||
// decompose the requested locale from specific to general, building a list of files to load
|
// decompose the requested locale from specific to general, building a list of files to load
|
||||||
$tags = \Locale::parseLocale($this->wanted);
|
$tags = \Locale::parseLocale($this->wanted);
|
||||||
$files = [];
|
$files = [];
|
||||||
while(sizeof($tags) > 0) {
|
while (sizeof($tags) > 0) {
|
||||||
$files[] = strtolower(\Locale::composeLocale($tags));
|
$files[] = strtolower(\Locale::composeLocale($tags));
|
||||||
$tag = array_pop($tags);
|
$tag = array_pop($tags);
|
||||||
}
|
}
|
||||||
// include the default locale as the base if the most general locale requested is not the default
|
// 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;
|
$files[] = self::DEFAULT;
|
||||||
}
|
}
|
||||||
// save the list of files to be loaded for later reference
|
// save the list of files to be loaded for later reference
|
||||||
$loaded = $files;
|
$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")
|
// 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 = [];
|
$files = [];
|
||||||
foreach($loaded as $file) {
|
foreach ($loaded as $file) {
|
||||||
if($file==$this->locale) {
|
if ($file==$this->locale) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
$files[] = $file;
|
$files[] = $file;
|
||||||
}
|
}
|
||||||
// if we need to load all files, start with the fallback strings
|
// if we need to load all files, start with the fallback strings
|
||||||
$strings = [];
|
$strings = [];
|
||||||
if($files==$loaded) {
|
if ($files==$loaded) {
|
||||||
$strings[] = self::REQUIRED;
|
$strings[] = self::REQUIRED;
|
||||||
} else {
|
} else {
|
||||||
// otherwise start with the strings we already have if we're going from e.g. "fr" to "fr_ca"
|
// 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
|
// read files in reverse order
|
||||||
$files = array_reverse($files);
|
$files = array_reverse($files);
|
||||||
foreach($files as $file) {
|
foreach ($files as $file) {
|
||||||
if(!file_exists($this->path."$file.php")) {
|
if (!file_exists($this->path."$file.php")) {
|
||||||
throw new Lang\Exception("fileMissing", $file);
|
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);
|
throw new Lang\Exception("fileUnreadable", $file);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// we use output buffering in case the language file is corrupted
|
// we use output buffering in case the language file is corrupted
|
||||||
ob_start();
|
ob_start();
|
||||||
$arr = (include $this->path."$file.php");
|
$arr = (include $this->path."$file.php");
|
||||||
} catch(\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$arr = null;
|
$arr = null;
|
||||||
} finally {
|
} finally {
|
||||||
ob_end_clean();
|
ob_end_clean();
|
||||||
}
|
}
|
||||||
if(!is_array($arr)) {
|
if (!is_array($arr)) {
|
||||||
throw new Lang\Exception("fileCorrupt", $file);
|
throw new Lang\Exception("fileCorrupt", $file);
|
||||||
}
|
}
|
||||||
$strings[] = $arr;
|
$strings[] = $arr;
|
||||||
|
@ -212,4 +216,4 @@ class Lang {
|
||||||
$this->synched = true;
|
$this->synched = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Lang;
|
namespace JKingWeb\Arsse\Lang;
|
||||||
|
|
||||||
class Exception extends \JKingWeb\Arsse\AbstractException {
|
class Exception extends \JKingWeb\Arsse\AbstractException {
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Misc;
|
namespace JKingWeb\Arsse\Misc;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
|
|
||||||
class Context {
|
class Context {
|
||||||
public $reverse = false;
|
public $reverse = false;
|
||||||
public $limit = 0;
|
public $limit = 0;
|
||||||
public $offset = 0;
|
public $offset = 0;
|
||||||
|
@ -23,7 +24,7 @@ class Context {
|
||||||
protected $props = [];
|
protected $props = [];
|
||||||
|
|
||||||
protected function act(string $prop, int $set, $value) {
|
protected function act(string $prop, int $set, $value) {
|
||||||
if($set) {
|
if ($set) {
|
||||||
$this->props[$prop] = true;
|
$this->props[$prop] = true;
|
||||||
$this->$prop = $value;
|
$this->$prop = $value;
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -34,17 +35,17 @@ class Context {
|
||||||
|
|
||||||
protected function cleanArray(array $spec): array {
|
protected function cleanArray(array $spec): array {
|
||||||
$spec = array_values($spec);
|
$spec = array_values($spec);
|
||||||
for($a = 0; $a < sizeof($spec); $a++) {
|
for ($a = 0; $a < sizeof($spec); $a++) {
|
||||||
$id = $spec[$a];
|
$id = $spec[$a];
|
||||||
if(is_int($id) && $id > -1) {
|
if (is_int($id) && $id > -1) {
|
||||||
continue;
|
continue;
|
||||||
} else if(is_float($id) && !fmod($id, 1) && $id >= 0) {
|
} elseif (is_float($id) && !fmod($id, 1) && $id >= 0) {
|
||||||
$spec[$a] = (int) $id;
|
$spec[$a] = (int) $id;
|
||||||
continue;
|
continue;
|
||||||
} else if(is_string($id)) {
|
} elseif (is_string($id)) {
|
||||||
$ch1 = strval(@intval($id));
|
$ch1 = strval(@intval($id));
|
||||||
$ch2 = strval($id);
|
$ch2 = strval($id);
|
||||||
if($ch1 !== $ch2 || $id < 1) {
|
if ($ch1 !== $ch2 || $id < 1) {
|
||||||
$id = 0;
|
$id = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -55,71 +56,71 @@ class Context {
|
||||||
return array_values(array_filter($spec));
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
function modifiedSince($spec = null) {
|
public function modifiedSince($spec = null) {
|
||||||
$spec = Date::normalize($spec);
|
$spec = Date::normalize($spec);
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
function notModifiedSince($spec = null) {
|
public function notModifiedSince($spec = null) {
|
||||||
$spec = Date::normalize($spec);
|
$spec = Date::normalize($spec);
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $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);
|
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);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
function editions(array $spec = null) {
|
public function editions(array $spec = null) {
|
||||||
if($spec) {
|
if ($spec) {
|
||||||
$spec = $this->cleanArray($spec);
|
$spec = $this->cleanArray($spec);
|
||||||
}
|
}
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
function articles(array $spec = null) {
|
public function articles(array $spec = null) {
|
||||||
if($spec) {
|
if ($spec) {
|
||||||
$spec = $this->cleanArray($spec);
|
$spec = $this->cleanArray($spec);
|
||||||
}
|
}
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,13 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Misc;
|
namespace JKingWeb\Arsse\Misc;
|
||||||
|
|
||||||
class Date {
|
class Date {
|
||||||
|
public static function transform($date, string $outFormat = null, string $inFormat = null, bool $inLocal = false) {
|
||||||
static function transform($date, string $outFormat = null, string $inFormat = null, bool $inLocal = false) {
|
|
||||||
$date = self::normalize($date, $inFormat, $inLocal);
|
$date = self::normalize($date, $inFormat, $inLocal);
|
||||||
if(is_null($date) || is_null($outFormat)) {
|
if (is_null($date) || is_null($outFormat)) {
|
||||||
return $date;
|
return $date;
|
||||||
}
|
}
|
||||||
$outFormat = strtolower($outFormat);
|
$outFormat = strtolower($outFormat);
|
||||||
if($outFormat=="unix") {
|
if ($outFormat=="unix") {
|
||||||
return $date->getTimestamp();
|
return $date->getTimestamp();
|
||||||
}
|
}
|
||||||
switch ($outFormat) {
|
switch ($outFormat) {
|
||||||
|
@ -24,18 +23,18 @@ class Date {
|
||||||
return $date->format($f);
|
return $date->format($f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static function normalize($date, string $inFormat = null, bool $inLocal = false) {
|
public static function normalize($date, string $inFormat = null, bool $inLocal = false) {
|
||||||
if($date instanceof \DateTimeInterface) {
|
if ($date instanceof \DateTimeInterface) {
|
||||||
return $date;
|
return $date;
|
||||||
} else if(is_numeric($date)) {
|
} elseif (is_numeric($date)) {
|
||||||
$time = (int) $date;
|
$time = (int) $date;
|
||||||
} else if($date===null) {
|
} elseif ($date===null) {
|
||||||
return null;
|
return null;
|
||||||
} else if(is_string($date)) {
|
} elseif (is_string($date)) {
|
||||||
try {
|
try {
|
||||||
$tz = (!$inLocal) ? new \DateTimeZone("UTC") : null;
|
$tz = (!$inLocal) ? new \DateTimeZone("UTC") : null;
|
||||||
if(!is_null($inFormat)) {
|
if (!is_null($inFormat)) {
|
||||||
switch($inFormat) {
|
switch ($inFormat) {
|
||||||
case 'http': $f = "D, d M Y H:i:s \G\M\T"; break;
|
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 'iso8601': $f = "Y-m-d\TH:i:sP"; break;
|
||||||
case 'sql': $f = "Y-m-d H:i:s"; break;
|
case 'sql': $f = "Y-m-d H:i:s"; break;
|
||||||
|
@ -47,10 +46,10 @@ class Date {
|
||||||
} else {
|
} else {
|
||||||
return new \DateTime($date, $tz);
|
return new \DateTime($date, $tz);
|
||||||
}
|
}
|
||||||
} catch(\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else if (is_bool($date)) {
|
} elseif (is_bool($date)) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
$time = (int) $date;
|
$time = (int) $date;
|
||||||
|
@ -61,21 +60,21 @@ class Date {
|
||||||
return $d;
|
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);
|
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);
|
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());
|
$date = self::normalize($date ?? time());
|
||||||
if($date instanceof \DateTimeImmutable) {
|
if ($date instanceof \DateTimeImmutable) {
|
||||||
return $date->$func(new \DateInterval($interval));
|
return $date->$func(new \DateInterval($interval));
|
||||||
} else {
|
} else {
|
||||||
$date->$func(new \DateInterval($interval));
|
$date->$func(new \DateInterval($interval));
|
||||||
return $date;
|
return $date;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,42 +18,42 @@ class Query {
|
||||||
protected $offset = 0;
|
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);
|
$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;
|
$this->qBody = $body;
|
||||||
if(!is_null($types)) {
|
if (!is_null($types)) {
|
||||||
$this->tBody[] = $types;
|
$this->tBody[] = $types;
|
||||||
$this->vBody[] = $values;
|
$this->vBody[] = $values;
|
||||||
}
|
}
|
||||||
return true;
|
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)";
|
$this->qCTE[] = "$tableSpec as ($body)";
|
||||||
if(!is_null($types)) {
|
if (!is_null($types)) {
|
||||||
$this->tCTE[] = $types;
|
$this->tCTE[] = $types;
|
||||||
$this->vCTE[] = $values;
|
$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;
|
$this->jCTE[] = $join;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWhere(string $where, $types = null, $values = null): bool {
|
public function setWhere(string $where, $types = null, $values = null): bool {
|
||||||
$this->qWhere[] = $where;
|
$this->qWhere[] = $where;
|
||||||
if(!is_null($types)) {
|
if (!is_null($types)) {
|
||||||
$this->tWhere[] = $types;
|
$this->tWhere[] = $types;
|
||||||
$this->vWhere[] = $values;
|
$this->vWhere[] = $values;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOrder(string $order, bool $prepend = false): bool {
|
public function setOrder(string $order, bool $prepend = false): bool {
|
||||||
if($prepend) {
|
if ($prepend) {
|
||||||
array_unshift($this->order, $order);
|
array_unshift($this->order, $order);
|
||||||
} else {
|
} else {
|
||||||
$this->order[] = $order;
|
$this->order[] = $order;
|
||||||
|
@ -61,13 +61,13 @@ class Query {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLimit(int $limit, int $offset = 0): bool {
|
public function setLimit(int $limit, int $offset = 0): bool {
|
||||||
$this->limit = $limit;
|
$this->limit = $limit;
|
||||||
$this->offset = $offset;
|
$this->offset = $offset;
|
||||||
return true;
|
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
|
// 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
|
// 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]);
|
$this->setCTE($tableSpec, $this->buildQueryBody(), [$this->tBody, $this->tWhere], [$this->vBody, $this->vWhere]);
|
||||||
|
@ -78,16 +78,16 @@ class Query {
|
||||||
$this->tWhere = [];
|
$this->tWhere = [];
|
||||||
$this->vWhere = [];
|
$this->vWhere = [];
|
||||||
$this->order = [];
|
$this->order = [];
|
||||||
$this->setLimit(0,0);
|
$this->setLimit(0, 0);
|
||||||
if(strlen($join)) {
|
if (strlen($join)) {
|
||||||
$this->jCTE[] = $join;
|
$this->jCTE[] = $join;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function __toString(): string {
|
public function __toString(): string {
|
||||||
$out = "";
|
$out = "";
|
||||||
if(sizeof($this->qCTE)) {
|
if (sizeof($this->qCTE)) {
|
||||||
// start with common table expressions
|
// start with common table expressions
|
||||||
$out .= "WITH RECURSIVE ".implode(", ", $this->qCTE)." ";
|
$out .= "WITH RECURSIVE ".implode(", ", $this->qCTE)." ";
|
||||||
}
|
}
|
||||||
|
@ -96,31 +96,31 @@ class Query {
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQuery(): string {
|
public function getQuery(): string {
|
||||||
return $this->__toString();
|
return $this->__toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTypes(): array {
|
public function getTypes(): array {
|
||||||
return [$this->tCTE, $this->tBody, $this->tWhere];
|
return [$this->tCTE, $this->tBody, $this->tWhere];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getValues(): array {
|
public function getValues(): array {
|
||||||
return [$this->vCTE, $this->vBody, $this->vWhere];
|
return [$this->vCTE, $this->vBody, $this->vWhere];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWhereTypes(): array {
|
public function getWhereTypes(): array {
|
||||||
return $this->tWhere;
|
return $this->tWhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWhereValues(): array {
|
public function getWhereValues(): array {
|
||||||
return $this->vWhere;
|
return $this->vWhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCTETypes(): array {
|
public function getCTETypes(): array {
|
||||||
return $this->tCTE;
|
return $this->tCTE;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCTEValues(): array {
|
public function getCTEValues(): array {
|
||||||
return $this->vCTE;
|
return $this->vCTE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,25 +128,25 @@ class Query {
|
||||||
$out = "";
|
$out = "";
|
||||||
// add the body
|
// add the body
|
||||||
$out .= $this->qBody;
|
$out .= $this->qBody;
|
||||||
if(sizeof($this->qCTE)) {
|
if (sizeof($this->qCTE)) {
|
||||||
// add any joins against CTEs
|
// add any joins against CTEs
|
||||||
$out .= " ".implode(" ", $this->jCTE);
|
$out .= " ".implode(" ", $this->jCTE);
|
||||||
}
|
}
|
||||||
// add any WHERE terms
|
// add any WHERE terms
|
||||||
if(sizeof($this->qWhere)) {
|
if (sizeof($this->qWhere)) {
|
||||||
$out .= " WHERE ".implode(" AND ", $this->qWhere);
|
$out .= " WHERE ".implode(" AND ", $this->qWhere);
|
||||||
}
|
}
|
||||||
// add any ORDER BY terms
|
// add any ORDER BY terms
|
||||||
if(sizeof($this->order)) {
|
if (sizeof($this->order)) {
|
||||||
$out .= " ORDER BY ".implode(", ", $this->order);
|
$out .= " ORDER BY ".implode(", ", $this->order);
|
||||||
}
|
}
|
||||||
// add LIMIT and OFFSET if the former is specified
|
// add LIMIT and OFFSET if the former is specified
|
||||||
if($this->limit > 0) {
|
if ($this->limit > 0) {
|
||||||
$out .= " LIMIT ".$this->limit;
|
$out .= " LIMIT ".$this->limit;
|
||||||
if($this->offset > 0) {
|
if ($this->offset > 0) {
|
||||||
$out .= " OFFSET ".$this->offset;
|
$out .= " OFFSET ".$this->offset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
20
lib/REST.php
20
lib/REST.php
|
@ -27,31 +27,33 @@ class REST {
|
||||||
// CommaFeed https://www.commafeed.com/api/
|
// CommaFeed https://www.commafeed.com/api/
|
||||||
];
|
];
|
||||||
|
|
||||||
function __construct() {
|
public function __construct() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatch(REST\Request $req = null): REST\Response {
|
public function dispatch(REST\Request $req = null): REST\Response {
|
||||||
if($req===null) {
|
if ($req===null) {
|
||||||
$req = new REST\Request();
|
$req = new REST\Request();
|
||||||
}
|
}
|
||||||
$api = $this->apiMatch($req->url, $this->apis);
|
$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();
|
$req->refreshURL();
|
||||||
$class = $this->apis[$api]['class'];
|
$class = $this->apis[$api]['class'];
|
||||||
$drv = new $class();
|
$drv = new $class();
|
||||||
return $drv->dispatch($req);
|
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
|
// 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
|
// find a match
|
||||||
foreach($map as $id => $api) {
|
foreach ($map as $id => $api) {
|
||||||
if(strpos($url, $api['match'])===0) {
|
if (strpos($url, $api['match'])===0) {
|
||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// or throw an exception otherwise
|
// or throw an exception otherwise
|
||||||
throw new REST\Exception501();
|
throw new REST\Exception501();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\REST;
|
namespace JKingWeb\Arsse\REST;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
|
|
||||||
abstract class AbstractHandler implements Handler {
|
abstract class AbstractHandler implements Handler {
|
||||||
|
abstract public function __construct();
|
||||||
abstract function __construct();
|
abstract public function dispatch(Request $req): Response;
|
||||||
abstract function dispatch(Request $req): Response;
|
|
||||||
|
|
||||||
protected function fieldMapNames(array $data, array $map): array {
|
protected function fieldMapNames(array $data, array $map): array {
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach($map as $to => $from) {
|
foreach ($map as $to => $from) {
|
||||||
if(array_key_exists($from, $data)) {
|
if (array_key_exists($from, $data)) {
|
||||||
$out[$to] = $data[$from];
|
$out[$to] = $data[$from];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function fieldMapTypes(array $data, array $map, string $dateFormat = "sql"): array {
|
protected function fieldMapTypes(array $data, array $map, string $dateFormat = "sql"): array {
|
||||||
foreach($map as $key => $type) {
|
foreach ($map as $key => $type) {
|
||||||
if(array_key_exists($key, $data)) {
|
if (array_key_exists($key, $data)) {
|
||||||
if($type=="datetime" && $dateFormat != "sql") {
|
if ($type=="datetime" && $dateFormat != "sql") {
|
||||||
$data[$key] = Date::transform($data[$key], $dateFormat, "sql");
|
$data[$key] = Date::transform($data[$key], $dateFormat, "sql");
|
||||||
} else {
|
} else {
|
||||||
settype($data[$key], $type);
|
settype($data[$key], $type);
|
||||||
|
@ -39,18 +39,18 @@ abstract class AbstractHandler implements Handler {
|
||||||
|
|
||||||
protected function NormalizeInput(array $data, array $types, string $dateFormat = null): array {
|
protected function NormalizeInput(array $data, array $types, string $dateFormat = null): array {
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach($data as $key => $value) {
|
foreach ($data as $key => $value) {
|
||||||
if(!isset($types[$key])) {
|
if (!isset($types[$key])) {
|
||||||
$out[$key] = $value;
|
$out[$key] = $value;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if(is_null($value)) {
|
if (is_null($value)) {
|
||||||
$out[$key] = null;
|
$out[$key] = null;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
switch($types[$key]) {
|
switch ($types[$key]) {
|
||||||
case "int":
|
case "int":
|
||||||
if($this->validateInt($value)) {
|
if ($this->validateInt($value)) {
|
||||||
$out[$key] = (int) $value;
|
$out[$key] = (int) $value;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -58,31 +58,31 @@ abstract class AbstractHandler implements Handler {
|
||||||
$out[$key] = (string) $value;
|
$out[$key] = (string) $value;
|
||||||
break;
|
break;
|
||||||
case "bool":
|
case "bool":
|
||||||
if(is_bool($value)) {
|
if (is_bool($value)) {
|
||||||
$out[$key] = $value;
|
$out[$key] = $value;
|
||||||
} else if($this->validateInt($value)) {
|
} elseif ($this->validateInt($value)) {
|
||||||
$value = (int) $value;
|
$value = (int) $value;
|
||||||
if($value > -1 && $value < 2) {
|
if ($value > -1 && $value < 2) {
|
||||||
$out[$key] = $value;
|
$out[$key] = $value;
|
||||||
}
|
}
|
||||||
} else if(is_string($value)) {
|
} elseif (is_string($value)) {
|
||||||
$value = trim(strtolower($value));
|
$value = trim(strtolower($value));
|
||||||
if($value=="false") {
|
if ($value=="false") {
|
||||||
$out[$key] = false;
|
$out[$key] = false;
|
||||||
}
|
}
|
||||||
if($value=="true") {
|
if ($value=="true") {
|
||||||
$out[$key] = true;
|
$out[$key] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "float":
|
case "float":
|
||||||
if(is_numeric($value)) {
|
if (is_numeric($value)) {
|
||||||
$out[$key] = (float) $value;
|
$out[$key] = (float) $value;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "datetime":
|
case "datetime":
|
||||||
$t = Date::normalize($value, $dateFormat);
|
$t = Date::normalize($value, $dateFormat);
|
||||||
if($t) {
|
if ($t) {
|
||||||
$out[$key] = $t;
|
$out[$key] = $t;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -92,4 +92,4 @@ abstract class AbstractHandler implements Handler {
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\REST;
|
namespace JKingWeb\Arsse\REST;
|
||||||
|
|
||||||
class Exception extends \JKingWeb\Arsse\AbstractException {
|
class Exception extends \JKingWeb\Arsse\AbstractException {
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\REST;
|
namespace JKingWeb\Arsse\REST;
|
||||||
|
|
||||||
class Exception405 extends \Exception {
|
class Exception405 extends \Exception {
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\REST;
|
namespace JKingWeb\Arsse\REST;
|
||||||
|
|
||||||
class Exception501 extends \Exception {
|
class Exception501 extends \Exception {
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,6 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\REST;
|
namespace JKingWeb\Arsse\REST;
|
||||||
|
|
||||||
interface Handler {
|
interface Handler {
|
||||||
function __construct();
|
public function __construct();
|
||||||
function dispatch(Request $req): Response;
|
public function dispatch(Request $req): Response;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\REST\NextCloudNews;
|
namespace JKingWeb\Arsse\REST\NextCloudNews;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
use JKingWeb\Arsse\User;
|
use JKingWeb\Arsse\User;
|
||||||
use JKingWeb\Arsse\Service;
|
use JKingWeb\Arsse\Service;
|
||||||
|
@ -36,22 +37,22 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// 'items' => "array int", // just pass these through
|
// '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
|
// try to authenticate
|
||||||
if(!Arsse::$user->authHTTP()) {
|
if (!Arsse::$user->authHTTP()) {
|
||||||
return new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.self::REALM.'"']);
|
return new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.self::REALM.'"']);
|
||||||
}
|
}
|
||||||
// normalize the input
|
// 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 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']);
|
return new Response(415, "", "", ['Accept: application/json']);
|
||||||
}
|
}
|
||||||
$data = @json_decode($req->body, true);
|
$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"
|
// if the body could not be parsed as JSON, return "400 Bad Request"
|
||||||
return new Response(400);
|
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
|
// check to make sure the requested function is implemented
|
||||||
try {
|
try {
|
||||||
$func = $this->chooseCall($req->paths, $req->method);
|
$func = $this->chooseCall($req->paths, $req->method);
|
||||||
} catch(Exception501 $e) {
|
} catch (Exception501 $e) {
|
||||||
return new Response(501);
|
return new Response(501);
|
||||||
} catch(Exception405 $e) {
|
} catch (Exception405 $e) {
|
||||||
return new Response(405, "", "", ["Allow: ".$e->getMessage()]);
|
return new Response(405, "", "", ["Allow: ".$e->getMessage()]);
|
||||||
}
|
}
|
||||||
if(!method_exists($this, $func)) {
|
if (!method_exists($this, $func)) {
|
||||||
return new Response(501);
|
return new Response(501);
|
||||||
}
|
}
|
||||||
// dispatch
|
// dispatch
|
||||||
try {
|
try {
|
||||||
return $this->$func($req->paths, $data);
|
return $this->$func($req->paths, $data);
|
||||||
} catch(Exception $e) {
|
} catch (Exception $e) {
|
||||||
// if there was a REST exception return 400
|
// if there was a REST exception return 400
|
||||||
return new Response(400);
|
return new Response(400);
|
||||||
} catch(AbstractException $e) {
|
} catch (AbstractException $e) {
|
||||||
// if there was any other Arsse exception return 500
|
// if there was any other Arsse exception return 500
|
||||||
return new Response(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
|
// the first path element is the overall scope of the request
|
||||||
$scope = $url[0];
|
$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)
|
// 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++) {
|
for ($a = 0; $a < sizeof($url); $a++) {
|
||||||
if($this->validateInt($url[$a])) {
|
if ($this->validateInt($url[$a])) {
|
||||||
$url[$a] = "0";
|
$url[$a] = "0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// normalize the HTTP method to uppercase
|
// normalize the HTTP method to uppercase
|
||||||
$method = strtoupper($method);
|
$method = strtoupper($method);
|
||||||
// if the scope is not supported, return 501
|
// if the scope is not supported, return 501
|
||||||
if(!array_key_exists($scope, $choices)) {
|
if (!array_key_exists($scope, $choices)) {
|
||||||
throw new Exception501();
|
throw new Exception501();
|
||||||
}
|
}
|
||||||
// we now evaluate the supplied URL against every supported path for the selected scope
|
// 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
|
// 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
|
// add the scope to the path to match against and split it
|
||||||
$path = (string) $path;
|
$path = (string) $path;
|
||||||
$path = (strlen($path)) ? "$scope/$path" : $scope;
|
$path = (strlen($path)) ? "$scope/$path" : $scope;
|
||||||
$path = explode("/", $path);
|
$path = explode("/", $path);
|
||||||
if($path===$url) {
|
if ($path===$url) {
|
||||||
// if the path matches, make sure the method is allowed
|
// 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
|
// if it is allowed, return the object method to run
|
||||||
return $funcs[$method];
|
return $funcs[$method];
|
||||||
} else {
|
} else {
|
||||||
|
@ -230,8 +231,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
protected function folderAdd(array $url, array $data): Response {
|
protected function folderAdd(array $url, array $data): Response {
|
||||||
try {
|
try {
|
||||||
$folder = Arsse::$db->folderAdd(Arsse::$user->id, $data);
|
$folder = Arsse::$db->folderAdd(Arsse::$user->id, $data);
|
||||||
} catch(ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
switch($e->getCode()) {
|
switch ($e->getCode()) {
|
||||||
// folder already exists
|
// folder already exists
|
||||||
case 10236: return new Response(409);
|
case 10236: return new Response(409);
|
||||||
// folder name not acceptable
|
// folder name not acceptable
|
||||||
|
@ -250,7 +251,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// perform the deletion
|
// perform the deletion
|
||||||
try {
|
try {
|
||||||
Arsse::$db->folderRemove(Arsse::$user->id, (int) $url[1]);
|
Arsse::$db->folderRemove(Arsse::$user->id, (int) $url[1]);
|
||||||
} catch(ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
// folder does not exist
|
// folder does not exist
|
||||||
return new Response(404);
|
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)
|
// 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 {
|
protected function folderRename(array $url, array $data): Response {
|
||||||
// there must be some change to be made
|
// there must be some change to be made
|
||||||
if(!sizeof($data)) {
|
if (!sizeof($data)) {
|
||||||
return new Response(422);
|
return new Response(422);
|
||||||
}
|
}
|
||||||
// perform the edit
|
// perform the edit
|
||||||
try {
|
try {
|
||||||
Arsse::$db->folderPropertiesSet(Arsse::$user->id, (int) $url[1], $data);
|
Arsse::$db->folderPropertiesSet(Arsse::$user->id, (int) $url[1], $data);
|
||||||
} catch(ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
switch($e->getCode()) {
|
switch ($e->getCode()) {
|
||||||
// folder does not exist
|
// folder does not exist
|
||||||
case 10239: return new Response(404);
|
case 10239: return new Response(404);
|
||||||
// folder already exists
|
// folder already exists
|
||||||
|
@ -285,7 +286,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// mark all articles associated with a folder as read
|
// mark all articles associated with a folder as read
|
||||||
protected function folderMarkRead(array $url, array $data): Response {
|
protected function folderMarkRead(array $url, array $data): Response {
|
||||||
$c = new Context;
|
$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
|
// if the item ID is valid (i.e. an integer), add it to the context
|
||||||
$c->latestEdition($data['newestItemId']);
|
$c->latestEdition($data['newestItemId']);
|
||||||
} else {
|
} else {
|
||||||
|
@ -297,7 +298,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// perform the operation
|
// perform the operation
|
||||||
try {
|
try {
|
||||||
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
|
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
|
||||||
} catch(ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
// folder does not exist
|
// folder does not exist
|
||||||
return new Response(404);
|
return new Response(404);
|
||||||
}
|
}
|
||||||
|
@ -307,13 +308,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// return list of feeds which should be refreshed
|
// return list of feeds which should be refreshed
|
||||||
protected function feedListStale(array $url, array $data): Response {
|
protected function feedListStale(array $url, array $data): Response {
|
||||||
// function requires admin rights per spec
|
// 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);
|
return new Response(403);
|
||||||
}
|
}
|
||||||
// list stale feeds which should be checked for updates
|
// list stale feeds which should be checked for updates
|
||||||
$feeds = Arsse::$db->feedListStale();
|
$feeds = Arsse::$db->feedListStale();
|
||||||
$out = [];
|
$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
|
// since in our implementation feeds don't belong the users, the 'userId' field will always be an empty string
|
||||||
$out[] = ['id' => $feed, 'userId' => ""];
|
$out[] = ['id' => $feed, 'userId' => ""];
|
||||||
}
|
}
|
||||||
|
@ -323,16 +324,16 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// refresh a feed
|
// refresh a feed
|
||||||
protected function feedUpdate(array $url, array $data): Response {
|
protected function feedUpdate(array $url, array $data): Response {
|
||||||
// function requires admin rights per spec
|
// 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);
|
return new Response(403);
|
||||||
}
|
}
|
||||||
// perform an update of a single feed
|
// perform an update of a single feed
|
||||||
if(!isset($data['feedId'])) {
|
if (!isset($data['feedId'])) {
|
||||||
return new Response(422);
|
return new Response(422);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Arsse::$db->feedUpdate($data['feedId']);
|
Arsse::$db->feedUpdate($data['feedId']);
|
||||||
} catch(ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
return new Response(404);
|
return new Response(404);
|
||||||
}
|
}
|
||||||
return new Response(204);
|
return new Response(204);
|
||||||
|
@ -341,7 +342,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// add a new feed
|
// add a new feed
|
||||||
protected function subscriptionAdd(array $url, array $data): Response {
|
protected function subscriptionAdd(array $url, array $data): Response {
|
||||||
// normalize the feed URL
|
// normalize the feed URL
|
||||||
if(!isset($data['url'])) {
|
if (!isset($data['url'])) {
|
||||||
return new Response(422);
|
return new Response(422);
|
||||||
}
|
}
|
||||||
// normalize the folder ID, if specified; zero should be transformed to null
|
// 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();
|
$tr = Arsse::$db->begin();
|
||||||
try {
|
try {
|
||||||
$id = Arsse::$db->subscriptionAdd(Arsse::$user->id, $data['url']);
|
$id = Arsse::$db->subscriptionAdd(Arsse::$user->id, $data['url']);
|
||||||
} catch(ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
// feed already exists
|
// feed already exists
|
||||||
return new Response(409);
|
return new Response(409);
|
||||||
} catch(FeedException $e) {
|
} catch (FeedException $e) {
|
||||||
// feed could not be retrieved
|
// feed could not be retrieved
|
||||||
return new Response(422);
|
return new Response(422);
|
||||||
}
|
}
|
||||||
// if a folder was specified, move the feed to the correct folder; silently ignore errors
|
// if a folder was specified, move the feed to the correct folder; silently ignore errors
|
||||||
if($folder) {
|
if ($folder) {
|
||||||
try {
|
try {
|
||||||
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, ['folder' => $folder]);
|
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, ['folder' => $folder]);
|
||||||
} catch(ExceptionInput $e) {}
|
} catch (ExceptionInput $e) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$tr->commit();
|
$tr->commit();
|
||||||
// fetch the feed's metadata and format it appropriately
|
// 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);
|
$feed = $this->feedTranslate($feed);
|
||||||
$out = ['feeds' => [$feed]];
|
$out = ['feeds' => [$feed]];
|
||||||
$newest = Arsse::$db->editionLatest(Arsse::$user->id, (new Context)->subscription($id));
|
$newest = Arsse::$db->editionLatest(Arsse::$user->id, (new Context)->subscription($id));
|
||||||
if($newest) {
|
if ($newest) {
|
||||||
$out['newestItemId'] = $newest;
|
$out['newestItemId'] = $newest;
|
||||||
}
|
}
|
||||||
return new Response(200, $out);
|
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 {
|
protected function subscriptionList(array $url, array $data): Response {
|
||||||
$subs = Arsse::$db->subscriptionList(Arsse::$user->id);
|
$subs = Arsse::$db->subscriptionList(Arsse::$user->id);
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach($subs as $sub) {
|
foreach ($subs as $sub) {
|
||||||
$out[] = $this->feedTranslate($sub);
|
$out[] = $this->feedTranslate($sub);
|
||||||
}
|
}
|
||||||
$out = ['feeds' => $out];
|
$out = ['feeds' => $out];
|
||||||
$out['starredCount'] = Arsse::$db->articleStarredCount(Arsse::$user->id);
|
$out['starredCount'] = Arsse::$db->articleStarredCount(Arsse::$user->id);
|
||||||
$newest = Arsse::$db->editionLatest(Arsse::$user->id);
|
$newest = Arsse::$db->editionLatest(Arsse::$user->id);
|
||||||
if($newest) {
|
if ($newest) {
|
||||||
$out['newestItemId'] = $newest;
|
$out['newestItemId'] = $newest;
|
||||||
}
|
}
|
||||||
return new Response(200, $out);
|
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 {
|
protected function subscriptionRemove(array $url, array $data): Response {
|
||||||
try {
|
try {
|
||||||
Arsse::$db->subscriptionRemove(Arsse::$user->id, (int) $url[1]);
|
Arsse::$db->subscriptionRemove(Arsse::$user->id, (int) $url[1]);
|
||||||
} catch(ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
// feed does not exist
|
// feed does not exist
|
||||||
return new Response(404);
|
return new Response(404);
|
||||||
}
|
}
|
||||||
|
@ -406,7 +408,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
protected function subscriptionRename(array $url, array $data): Response {
|
protected function subscriptionRename(array $url, array $data): Response {
|
||||||
// normalize input
|
// normalize input
|
||||||
$in = [];
|
$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'];
|
$in['title'] = $data['feedTitle'];
|
||||||
} else {
|
} else {
|
||||||
return new Response(422);
|
return new Response(422);
|
||||||
|
@ -414,8 +416,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// perform the renaming
|
// perform the renaming
|
||||||
try {
|
try {
|
||||||
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], $in);
|
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], $in);
|
||||||
} catch(ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
switch($e->getCode()) {
|
switch ($e->getCode()) {
|
||||||
// subscription does not exist
|
// subscription does not exist
|
||||||
case 10239: return new Response(404);
|
case 10239: return new Response(404);
|
||||||
// name is invalid
|
// name is invalid
|
||||||
|
@ -432,7 +434,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
protected function subscriptionMove(array $url, array $data): Response {
|
protected function subscriptionMove(array $url, array $data): Response {
|
||||||
// normalize input
|
// normalize input
|
||||||
$in = [];
|
$in = [];
|
||||||
if(isset($data['folderId'])) {
|
if (isset($data['folderId'])) {
|
||||||
$in['folder'] = $data['folderId'] ? $data['folderId'] : null;
|
$in['folder'] = $data['folderId'] ? $data['folderId'] : null;
|
||||||
} else {
|
} else {
|
||||||
return new Response(422);
|
return new Response(422);
|
||||||
|
@ -440,8 +442,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// perform the move
|
// perform the move
|
||||||
try {
|
try {
|
||||||
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], $in);
|
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], $in);
|
||||||
} catch(ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
switch($e->getCode()) {
|
switch ($e->getCode()) {
|
||||||
// subscription does not exist
|
// subscription does not exist
|
||||||
case 10239: return new Response(404);
|
case 10239: return new Response(404);
|
||||||
// folder does not exist
|
// 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
|
// mark all articles associated with a subscription as read
|
||||||
protected function subscriptionMarkRead(array $url, array $data): Response {
|
protected function subscriptionMarkRead(array $url, array $data): Response {
|
||||||
$c = new Context;
|
$c = new Context;
|
||||||
if(isset($data['newestItemId'])) {
|
if (isset($data['newestItemId'])) {
|
||||||
$c->latestEdition($data['newestItemId']);
|
$c->latestEdition($data['newestItemId']);
|
||||||
} else {
|
} else {
|
||||||
// otherwise return an error
|
// otherwise return an error
|
||||||
|
@ -467,7 +469,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// perform the operation
|
// perform the operation
|
||||||
try {
|
try {
|
||||||
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
|
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
|
||||||
} catch(ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
// subscription does not exist
|
// subscription does not exist
|
||||||
return new Response(404);
|
return new Response(404);
|
||||||
}
|
}
|
||||||
|
@ -479,39 +481,39 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// set the context options supplied by the client
|
// set the context options supplied by the client
|
||||||
$c = new Context;
|
$c = new Context;
|
||||||
// set the batch size
|
// set the batch size
|
||||||
if(isset($data['batchSize']) && $data['batchSize'] > 0) {
|
if (isset($data['batchSize']) && $data['batchSize'] > 0) {
|
||||||
$c->limit($data['batchSize']);
|
$c->limit($data['batchSize']);
|
||||||
}
|
}
|
||||||
// set the order of returned items
|
// set the order of returned items
|
||||||
if(isset($data['oldestFirst']) && $data['oldestFirst']) {
|
if (isset($data['oldestFirst']) && $data['oldestFirst']) {
|
||||||
$c->reverse(false);
|
$c->reverse(false);
|
||||||
} else {
|
} else {
|
||||||
$c->reverse(true);
|
$c->reverse(true);
|
||||||
}
|
}
|
||||||
// set the edition mark-off; the database uses an or-equal comparison for internal consistency, but the protocol does not, so we must adjust by one
|
// set the edition mark-off; the database uses an or-equal comparison for internal consistency, but the protocol does not, so we must adjust by one
|
||||||
if(isset($data['offset']) && $data['offset'] > 0) {
|
if (isset($data['offset']) && $data['offset'] > 0) {
|
||||||
if($c->reverse) {
|
if ($c->reverse) {
|
||||||
$c->latestEdition($data['offset'] - 1);
|
$c->latestEdition($data['offset'] - 1);
|
||||||
} else {
|
} else {
|
||||||
$c->oldestEdition($data['offset'] + 1);
|
$c->oldestEdition($data['offset'] + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// set whether to only return unread
|
// set whether to only return unread
|
||||||
if(isset($data['getRead']) && !$data['getRead']) {
|
if (isset($data['getRead']) && !$data['getRead']) {
|
||||||
$c->unread(true);
|
$c->unread(true);
|
||||||
}
|
}
|
||||||
// if no type is specified assume 3 (All)
|
// if no type is specified assume 3 (All)
|
||||||
if(!isset($data['type'])) {
|
if (!isset($data['type'])) {
|
||||||
$data['type'] = 3;
|
$data['type'] = 3;
|
||||||
}
|
}
|
||||||
switch($data['type']) {
|
switch ($data['type']) {
|
||||||
case 0: // feed
|
case 0: // feed
|
||||||
if(isset($data['id'])) {
|
if (isset($data['id'])) {
|
||||||
$c->subscription($data['id']);
|
$c->subscription($data['id']);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 1: // folder
|
case 1: // folder
|
||||||
if(isset($data['id'])) {
|
if (isset($data['id'])) {
|
||||||
$c->folder($data['id']);
|
$c->folder($data['id']);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -522,18 +524,18 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// return all items
|
// return all items
|
||||||
}
|
}
|
||||||
// whether to return only updated items
|
// whether to return only updated items
|
||||||
if(isset($data['lastModified'])) {
|
if (isset($data['lastModified'])) {
|
||||||
$c->modifiedSince($data['lastModified']);
|
$c->modifiedSince($data['lastModified']);
|
||||||
}
|
}
|
||||||
// perform the fetch
|
// perform the fetch
|
||||||
try {
|
try {
|
||||||
$items = Arsse::$db->articleList(Arsse::$user->id, $c);
|
$items = Arsse::$db->articleList(Arsse::$user->id, $c);
|
||||||
} catch(ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
// ID of subscription or folder is not valid
|
// ID of subscription or folder is not valid
|
||||||
return new Response(422);
|
return new Response(422);
|
||||||
}
|
}
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach($items as $item) {
|
foreach ($items as $item) {
|
||||||
$out[] = $this->articleTranslate($item);
|
$out[] = $this->articleTranslate($item);
|
||||||
}
|
}
|
||||||
$out = ['items' => $out];
|
$out = ['items' => $out];
|
||||||
|
@ -543,7 +545,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// mark all articles as read
|
// mark all articles as read
|
||||||
protected function articleMarkReadAll(array $url, array $data): Response {
|
protected function articleMarkReadAll(array $url, array $data): Response {
|
||||||
$c = new Context;
|
$c = new Context;
|
||||||
if(isset($data['newestItemId'])) {
|
if (isset($data['newestItemId'])) {
|
||||||
// set the newest item ID as specified
|
// set the newest item ID as specified
|
||||||
$c->latestEdition($data['newestItemId']);
|
$c->latestEdition($data['newestItemId']);
|
||||||
} else {
|
} else {
|
||||||
|
@ -564,7 +566,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
$set = ($url[2]=="read");
|
$set = ($url[2]=="read");
|
||||||
try {
|
try {
|
||||||
Arsse::$db->articleMark(Arsse::$user->id, ['read' => $set], $c);
|
Arsse::$db->articleMark(Arsse::$user->id, ['read' => $set], $c);
|
||||||
} catch(ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
// ID is not valid
|
// ID is not valid
|
||||||
return new Response(404);
|
return new Response(404);
|
||||||
}
|
}
|
||||||
|
@ -580,7 +582,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
$set = ($url[3]=="star");
|
$set = ($url[3]=="star");
|
||||||
try {
|
try {
|
||||||
Arsse::$db->articleMark(Arsse::$user->id, ['starred' => $set], $c);
|
Arsse::$db->articleMark(Arsse::$user->id, ['starred' => $set], $c);
|
||||||
} catch(ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
// ID is not valid
|
// ID is not valid
|
||||||
return new Response(404);
|
return new Response(404);
|
||||||
}
|
}
|
||||||
|
@ -592,19 +594,20 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// determine whether to mark read or unread
|
// determine whether to mark read or unread
|
||||||
$set = ($url[1]=="read");
|
$set = ($url[1]=="read");
|
||||||
// if the input data is not at all valid, return an error
|
// 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);
|
return new Response(422);
|
||||||
}
|
}
|
||||||
// start a transaction and loop through the items
|
// start a transaction and loop through the items
|
||||||
$t = Arsse::$db->begin();
|
$t = Arsse::$db->begin();
|
||||||
$in = array_chunk($data['items'], 50);
|
$in = array_chunk($data['items'], 50);
|
||||||
for($a = 0; $a < sizeof($in); $a++) {
|
for ($a = 0; $a < sizeof($in); $a++) {
|
||||||
// initialize the matching context
|
// initialize the matching context
|
||||||
$c = new Context;
|
$c = new Context;
|
||||||
$c->editions($in[$a]);
|
$c->editions($in[$a]);
|
||||||
try {
|
try {
|
||||||
Arsse::$db->articleMark(Arsse::$user->id, ['read' => $set], $c);
|
Arsse::$db->articleMark(Arsse::$user->id, ['read' => $set], $c);
|
||||||
} catch(ExceptionInput $e) {}
|
} catch (ExceptionInput $e) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$t->commit();
|
$t->commit();
|
||||||
return new Response(204);
|
return new Response(204);
|
||||||
|
@ -615,19 +618,20 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// determine whether to mark starred or unstarred
|
// determine whether to mark starred or unstarred
|
||||||
$set = ($url[1]=="star");
|
$set = ($url[1]=="star");
|
||||||
// if the input data is not at all valid, return an error
|
// 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);
|
return new Response(422);
|
||||||
}
|
}
|
||||||
// start a transaction and loop through the items
|
// start a transaction and loop through the items
|
||||||
$t = Arsse::$db->begin();
|
$t = Arsse::$db->begin();
|
||||||
$in = array_chunk(array_column($data['items'], "guidHash"), 50);
|
$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
|
// initialize the matching context
|
||||||
$c = new Context;
|
$c = new Context;
|
||||||
$c->articles($in[$a]);
|
$c->articles($in[$a]);
|
||||||
try {
|
try {
|
||||||
Arsse::$db->articleMark(Arsse::$user->id, ['starred' => $set], $c);
|
Arsse::$db->articleMark(Arsse::$user->id, ['starred' => $set], $c);
|
||||||
} catch(ExceptionInput $e) {}
|
} catch (ExceptionInput $e) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$t->commit();
|
$t->commit();
|
||||||
return new Response(204);
|
return new Response(204);
|
||||||
|
@ -636,7 +640,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
protected function userStatus(array $url, array $data): Response {
|
protected function userStatus(array $url, array $data): Response {
|
||||||
$data = Arsse::$user->propertiesGet(Arsse::$user->id, true);
|
$data = Arsse::$user->propertiesGet(Arsse::$user->id, true);
|
||||||
// construct the avatar structure, if an image is available
|
// construct the avatar structure, if an image is available
|
||||||
if(isset($data['avatar'])) {
|
if (isset($data['avatar'])) {
|
||||||
$avatar = [
|
$avatar = [
|
||||||
'data' => base64_encode($data['avatar']['data']),
|
'data' => base64_encode($data['avatar']['data']),
|
||||||
'mime' => $data['avatar']['type'],
|
'mime' => $data['avatar']['type'],
|
||||||
|
@ -656,7 +660,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
|
|
||||||
protected function cleanupBefore(array $url, array $data): Response {
|
protected function cleanupBefore(array $url, array $data): Response {
|
||||||
// function requires admin rights per spec
|
// 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);
|
return new Response(403);
|
||||||
}
|
}
|
||||||
Service::cleanupPre();
|
Service::cleanupPre();
|
||||||
|
@ -665,7 +669,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
|
|
||||||
protected function cleanupAfter(array $url, array $data): Response {
|
protected function cleanupAfter(array $url, array $data): Response {
|
||||||
// function requires admin rights per spec
|
// 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);
|
return new Response(403);
|
||||||
}
|
}
|
||||||
Service::cleanupPost();
|
Service::cleanupPost();
|
||||||
|
@ -689,4 +693,4 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\REST\NextCloudNews;
|
namespace JKingWeb\Arsse\REST\NextCloudNews;
|
||||||
|
|
||||||
use JKingWeb\Arsse\REST\Response;
|
use JKingWeb\Arsse\REST\Response;
|
||||||
|
|
||||||
class Versions implements \JKingWeb\Arsse\REST\Handler {
|
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 a method other than GET was used, this is an error
|
||||||
if($req->method != "GET") {
|
if ($req->method != "GET") {
|
||||||
return new Response(405);
|
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
|
// if the request path is an empty string or just a slash, return the supported versions
|
||||||
$out = [
|
$out = [
|
||||||
'apiLevels' => [
|
'apiLevels' => [
|
||||||
|
@ -25,4 +26,4 @@ class Versions implements \JKingWeb\Arsse\REST\Handler {
|
||||||
return new Response(501);
|
return new Response(501);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,18 +11,18 @@ class Request {
|
||||||
public $type ="";
|
public $type ="";
|
||||||
public $body = "";
|
public $body = "";
|
||||||
|
|
||||||
function __construct(string $method = null, string $url = null, string $body = null, string $contentType = null) {
|
public function __construct(string $method = null, string $url = null, string $body = null, string $contentType = null) {
|
||||||
if(is_null($method)) {
|
if (is_null($method)) {
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
}
|
}
|
||||||
if(is_null($url)) {
|
if (is_null($url)) {
|
||||||
$url = $_SERVER['REQUEST_URI'];
|
$url = $_SERVER['REQUEST_URI'];
|
||||||
}
|
}
|
||||||
if(is_null($body)) {
|
if (is_null($body)) {
|
||||||
$body = file_get_contents("php://input");
|
$body = file_get_contents("php://input");
|
||||||
}
|
}
|
||||||
if(is_null($contentType)) {
|
if (is_null($contentType)) {
|
||||||
if(isset($_SERVER['HTTP_CONTENT_TYPE'])) {
|
if (isset($_SERVER['HTTP_CONTENT_TYPE'])) {
|
||||||
$contentType = $_SERVER['HTTP_CONTENT_TYPE'];
|
$contentType = $_SERVER['HTTP_CONTENT_TYPE'];
|
||||||
} else {
|
} else {
|
||||||
$contentType = "";
|
$contentType = "";
|
||||||
|
@ -47,17 +47,17 @@ class Request {
|
||||||
$parts = explode("?", $url);
|
$parts = explode("?", $url);
|
||||||
$out = ['path' => $parts[0], 'paths' => [''], 'query' => []];
|
$out = ['path' => $parts[0], 'paths' => [''], 'query' => []];
|
||||||
// if there is a query string, parse it
|
// if there is a query string, parse it
|
||||||
if(isset($parts[1])) {
|
if (isset($parts[1])) {
|
||||||
// split along & to get key-value pairs
|
// split along & to get key-value pairs
|
||||||
$query = explode("&", $parts[1]);
|
$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
|
// split each pair, into no more than two parts
|
||||||
$data = explode("=", $query[$a], 2);
|
$data = explode("=", $query[$a], 2);
|
||||||
// decode the key
|
// decode the key
|
||||||
$key = rawurldecode($data[0]);
|
$key = rawurldecode($data[0]);
|
||||||
// decode the value if there is one
|
// decode the value if there is one
|
||||||
$value = "";
|
$value = "";
|
||||||
if(isset($data[1])) {
|
if (isset($data[1])) {
|
||||||
$value = rawurldecode($data[1]);
|
$value = rawurldecode($data[1]);
|
||||||
}
|
}
|
||||||
// add the pair to the query output, overwriting earlier values for the same key, is present
|
// 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
|
// also include the path as a set of decoded elements
|
||||||
// if the path is an empty string or just / nothing needs be done
|
// 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']);
|
$paths = explode("/", $out['path']);
|
||||||
// remove the first and last empty elements, if present (they are artefacts of the splitting; others should remain)
|
// 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);
|
array_shift($paths);
|
||||||
}
|
}
|
||||||
if(!strlen($paths[sizeof($paths)-1])) {
|
if (!strlen($paths[sizeof($paths)-1])) {
|
||||||
array_pop($paths);
|
array_pop($paths);
|
||||||
}
|
}
|
||||||
// %-decode each path element
|
// %-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;
|
$out['paths'] = $paths;
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\REST;
|
namespace JKingWeb\Arsse\REST;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
|
|
||||||
class Response {
|
class Response {
|
||||||
|
@ -14,34 +15,34 @@ class Response {
|
||||||
public $fields;
|
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->code = $code;
|
||||||
$this->payload = $payload;
|
$this->payload = $payload;
|
||||||
$this->type = $type;
|
$this->type = $type;
|
||||||
$this->fields = $extraFields;
|
$this->fields = $extraFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
function output() {
|
public function output() {
|
||||||
if(!headers_sent()) {
|
if (!headers_sent()) {
|
||||||
try {
|
try {
|
||||||
$statusText = Arsse::$lang->msg("HTTP.Status.".$this->code);
|
$statusText = Arsse::$lang->msg("HTTP.Status.".$this->code);
|
||||||
} catch(\JKingWeb\Arsse\Lang\Exception $e) {
|
} catch (\JKingWeb\Arsse\Lang\Exception $e) {
|
||||||
$statusText = "";
|
$statusText = "";
|
||||||
}
|
}
|
||||||
header("Status: ".$this->code." ".$statusText);
|
header("Status: ".$this->code." ".$statusText);
|
||||||
$body = "";
|
$body = "";
|
||||||
if(!is_null($this->payload)) {
|
if (!is_null($this->payload)) {
|
||||||
header("Content-Type: ".$this->type);
|
header("Content-Type: ".$this->type);
|
||||||
switch($this->type) {
|
switch ($this->type) {
|
||||||
case self::T_JSON:
|
case self::T_JSON:
|
||||||
$body = (string) json_encode($this->payload,\JSON_PRETTY_PRINT);
|
$body = (string) json_encode($this->payload, \JSON_PRETTY_PRINT);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$body = (string) $this->payload;
|
$body = (string) $this->payload;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach($this->fields as $field) {
|
foreach ($this->fields as $field) {
|
||||||
header($field);
|
header($field);
|
||||||
}
|
}
|
||||||
echo $body;
|
echo $body;
|
||||||
|
@ -49,4 +50,4 @@ class Response {
|
||||||
throw new REST\Exception("headersSent");
|
throw new REST\Exception("headersSent");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
|
|
||||||
class Service {
|
class Service {
|
||||||
|
@ -10,11 +11,11 @@ class Service {
|
||||||
/** @var \DateInterval */
|
/** @var \DateInterval */
|
||||||
protected $interval;
|
protected $interval;
|
||||||
|
|
||||||
static public function driverList(): array {
|
public static function driverList(): array {
|
||||||
$sep = \DIRECTORY_SEPARATOR;
|
$sep = \DIRECTORY_SEPARATOR;
|
||||||
$path = __DIR__.$sep."Service".$sep;
|
$path = __DIR__.$sep."Service".$sep;
|
||||||
$classes = [];
|
$classes = [];
|
||||||
foreach(glob($path."*".$sep."Driver.php") as $file) {
|
foreach (glob($path."*".$sep."Driver.php") as $file) {
|
||||||
$name = basename(dirname($file));
|
$name = basename(dirname($file));
|
||||||
$class = NS_BASE."User\\$name\\Driver";
|
$class = NS_BASE."User\\$name\\Driver";
|
||||||
$classes[$class] = $class::driverName();
|
$classes[$class] = $class::driverName();
|
||||||
|
@ -23,26 +24,26 @@ class Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function interval(): \DateInterval {
|
public static function interval(): \DateInterval {
|
||||||
try{
|
try {
|
||||||
return new \DateInterval(Arsse::$conf->serviceFrequency);
|
return new \DateInterval(Arsse::$conf->serviceFrequency);
|
||||||
} catch(\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return new \DateInterval("PT2M");
|
return new \DateInterval("PT2M");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function __construct() {
|
public function __construct() {
|
||||||
$driver = Arsse::$conf->serviceDriver;
|
$driver = Arsse::$conf->serviceDriver;
|
||||||
$this->drv = new $driver();
|
$this->drv = new $driver();
|
||||||
$this->interval = static::interval();
|
$this->interval = static::interval();
|
||||||
}
|
}
|
||||||
|
|
||||||
function watch(bool $loop = true): \DateTimeInterface {
|
public function watch(bool $loop = true): \DateTimeInterface {
|
||||||
$t = new \DateTime();
|
$t = new \DateTime();
|
||||||
do {
|
do {
|
||||||
$this->checkIn();
|
$this->checkIn();
|
||||||
static::cleanupPre();
|
static::cleanupPre();
|
||||||
$list = Arsse::$db->feedListStale();
|
$list = Arsse::$db->feedListStale();
|
||||||
if($list) {
|
if ($list) {
|
||||||
$this->drv->queue(...$list);
|
$this->drv->queue(...$list);
|
||||||
$this->drv->exec();
|
$this->drv->exec();
|
||||||
$this->drv->clean();
|
$this->drv->clean();
|
||||||
|
@ -50,23 +51,23 @@ class Service {
|
||||||
}
|
}
|
||||||
static::cleanupPost();
|
static::cleanupPost();
|
||||||
$t->add($this->interval);
|
$t->add($this->interval);
|
||||||
if($loop) {
|
if ($loop) {
|
||||||
do {
|
do {
|
||||||
@time_sleep_until($t->getTimestamp());
|
@time_sleep_until($t->getTimestamp());
|
||||||
} while($t->getTimestamp() > time());
|
} while ($t->getTimestamp() > time());
|
||||||
}
|
}
|
||||||
} while($loop);
|
} while ($loop);
|
||||||
return $t;
|
return $t;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkIn(): bool {
|
public function checkIn(): bool {
|
||||||
return Arsse::$db->metaSet("service_last_checkin", time(), "datetime");
|
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");
|
$checkin = Arsse::$db->metaGet("service_last_checkin");
|
||||||
// if the service has never checked in, return false
|
// if the service has never checked in, return false
|
||||||
if(!$checkin) {
|
if (!$checkin) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// convert the check-in timestamp to a DateTime instance
|
// convert the check-in timestamp to a DateTime instance
|
||||||
|
@ -81,13 +82,13 @@ class Service {
|
||||||
return ($checkin >= $limit);
|
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
|
// mark unsubscribed feeds as orphaned and delete orphaned feeds that are beyond their retention period
|
||||||
return Arsse::$db->feedCleanup();
|
return Arsse::$db->feedCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
static function cleanupPost(): bool {
|
public static function cleanupPost(): bool {
|
||||||
// delete old articles, according to configured threasholds
|
// delete old articles, according to configured threasholds
|
||||||
return Arsse::$db->articleCleanup();
|
return Arsse::$db->articleCleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Service\Curl;
|
namespace JKingWeb\Arsse\Service\Curl;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
|
|
||||||
class Driver implements \JKingWeb\Arsse\Service\Driver {
|
class Driver implements \JKingWeb\Arsse\Service\Driver {
|
||||||
|
@ -8,15 +9,15 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
|
||||||
protected $queue;
|
protected $queue;
|
||||||
protected $handles = [];
|
protected $handles = [];
|
||||||
|
|
||||||
static function driverName(): string {
|
public static function driverName(): string {
|
||||||
return Arsse::$lang->msg("Driver.Service.Curl.Name");
|
return Arsse::$lang->msg("Driver.Service.Curl.Name");
|
||||||
}
|
}
|
||||||
|
|
||||||
static function requirementsMet(): bool {
|
public static function requirementsMet(): bool {
|
||||||
return extension_loaded("curl");
|
return extension_loaded("curl");
|
||||||
}
|
}
|
||||||
|
|
||||||
function __construct() {
|
public function __construct() {
|
||||||
//default curl options for individual requests
|
//default curl options for individual requests
|
||||||
$this->options = [
|
$this->options = [
|
||||||
\CURLOPT_URL => Arsse::$serviceCurlBase."index.php/apps/news/api/v1-2/feeds/update",
|
\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);
|
curl_multi_setopt($this->queue, \CURLMOPT_PIPELINING, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function queue(int ...$feeds): int {
|
public function queue(int ...$feeds): int {
|
||||||
foreach($feeds as $id) {
|
foreach ($feeds as $id) {
|
||||||
$h = curl_init();
|
$h = curl_init();
|
||||||
curl_setopt($h, \CURLOPT_POSTFIELDS, json_encode(['userId' => "", 'feedId' => $id]));
|
curl_setopt($h, \CURLOPT_POSTFIELDS, json_encode(['userId' => "", 'feedId' => $id]));
|
||||||
$this->handles[] = $h;
|
$this->handles[] = $h;
|
||||||
|
@ -52,7 +53,7 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
|
||||||
return sizeof($this->handles);
|
return sizeof($this->handles);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exec(): int {
|
public function exec(): int {
|
||||||
$active = 0;
|
$active = 0;
|
||||||
do {
|
do {
|
||||||
curl_multi_exec($this->queue, $active);
|
curl_multi_exec($this->queue, $active);
|
||||||
|
@ -61,12 +62,12 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
|
||||||
return Arsse::$conf->serviceQueueWidth - $active;
|
return Arsse::$conf->serviceQueueWidth - $active;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clean(): bool {
|
public function clean(): bool {
|
||||||
foreach($this->handles as $h) {
|
foreach ($this->handles as $h) {
|
||||||
curl_multi_remove_handle($this->queue, $h);
|
curl_multi_remove_handle($this->queue, $h);
|
||||||
curl_close($h);
|
curl_close($h);
|
||||||
}
|
}
|
||||||
$this->handles = [];
|
$this->handles = [];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Service;
|
namespace JKingWeb\Arsse\Service;
|
||||||
|
|
||||||
interface Driver {
|
interface Driver {
|
||||||
static function driverName(): string;
|
public static function driverName(): string;
|
||||||
static function requirementsMet(): bool;
|
public static function requirementsMet(): bool;
|
||||||
function queue(int ...$feeds): int;
|
public function queue(int ...$feeds): int;
|
||||||
function exec(): int;
|
public function exec(): int;
|
||||||
function clean(): bool;
|
public function clean(): bool;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,37 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Service\Forking;
|
namespace JKingWeb\Arsse\Service\Forking;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
|
|
||||||
class Driver implements \JKingWeb\Arsse\Service\Driver {
|
class Driver implements \JKingWeb\Arsse\Service\Driver {
|
||||||
protected $queue = [];
|
protected $queue = [];
|
||||||
|
|
||||||
static function driverName(): string {
|
public static function driverName(): string {
|
||||||
return Arsse::$lang->msg("Driver.Service.Forking.Name");
|
return Arsse::$lang->msg("Driver.Service.Forking.Name");
|
||||||
}
|
}
|
||||||
|
|
||||||
static function requirementsMet(): bool {
|
public static function requirementsMet(): bool {
|
||||||
return function_exists("popen");
|
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);
|
$this->queue = array_merge($this->queue, $feeds);
|
||||||
return sizeof($this->queue);
|
return sizeof($this->queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exec(): int {
|
public function exec(): int {
|
||||||
$pp = [];
|
$pp = [];
|
||||||
while($this->queue) {
|
while ($this->queue) {
|
||||||
$id = (int) array_shift($this->queue);
|
$id = (int) array_shift($this->queue);
|
||||||
$php = '"'.\PHP_BINARY.'"';
|
$php = '"'.\PHP_BINARY.'"';
|
||||||
$arsse = '"'.$_SERVER['argv'][0].'"';
|
$arsse = '"'.$_SERVER['argv'][0].'"';
|
||||||
array_push($pp, popen("$php $arsse feed refresh $id", "r"));
|
array_push($pp, popen("$php $arsse feed refresh $id", "r"));
|
||||||
}
|
}
|
||||||
while($pp) {
|
while ($pp) {
|
||||||
$p = array_pop($pp);
|
$p = array_pop($pp);
|
||||||
fgets($p); // TODO: log output
|
fgets($p); // TODO: log output
|
||||||
pclose($p);
|
pclose($p);
|
||||||
|
@ -38,8 +39,8 @@ class Driver implements \JKingWeb\Arsse\Service\Driver {
|
||||||
return Arsse::$conf->serviceQueueWidth - sizeof($this->queue);
|
return Arsse::$conf->serviceQueueWidth - sizeof($this->queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clean(): bool {
|
public function clean(): bool {
|
||||||
$this->queue = [];
|
$this->queue = [];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,39 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Service\Internal;
|
namespace JKingWeb\Arsse\Service\Internal;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
|
|
||||||
class Driver implements \JKingWeb\Arsse\Service\Driver {
|
class Driver implements \JKingWeb\Arsse\Service\Driver {
|
||||||
protected $queue = [];
|
protected $queue = [];
|
||||||
|
|
||||||
static function driverName(): string {
|
public static function driverName(): string {
|
||||||
return Arsse::$lang->msg("Driver.Service.Internal.Name");
|
return Arsse::$lang->msg("Driver.Service.Internal.Name");
|
||||||
}
|
}
|
||||||
|
|
||||||
static function requirementsMet(): bool {
|
public static function requirementsMet(): bool {
|
||||||
// this driver has no requirements
|
// this driver has no requirements
|
||||||
return true;
|
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);
|
$this->queue = array_merge($this->queue, $feeds);
|
||||||
return sizeof($this->queue);
|
return sizeof($this->queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exec(): int {
|
public function exec(): int {
|
||||||
while(sizeof($this->queue)) {
|
while (sizeof($this->queue)) {
|
||||||
$id = array_shift($this->queue);
|
$id = array_shift($this->queue);
|
||||||
Arsse::$db->feedUpdate($id);
|
Arsse::$db->feedUpdate($id);
|
||||||
}
|
}
|
||||||
return Arsse::$conf->serviceQueueWidth - sizeof($this->queue);
|
return Arsse::$conf->serviceQueueWidth - sizeof($this->queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clean(): bool {
|
public function clean(): bool {
|
||||||
$this->queue = [];
|
$this->queue = [];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
147
lib/User.php
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_MANAGER = 75; // able to act for any normal users on any domain; cannot elevate other users
|
||||||
const RIGHTS_GLOBAL_ADMIN = 100; // is completely unrestricted
|
const RIGHTS_GLOBAL_ADMIN = 100; // is completely unrestricted
|
||||||
|
|
||||||
public $id = null;
|
public $id = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var User\Driver
|
* @var User\Driver
|
||||||
|
@ -19,11 +19,11 @@ class User {
|
||||||
protected $authzSupported = 0;
|
protected $authzSupported = 0;
|
||||||
protected $actor = [];
|
protected $actor = [];
|
||||||
|
|
||||||
static public function driverList(): array {
|
public static function driverList(): array {
|
||||||
$sep = \DIRECTORY_SEPARATOR;
|
$sep = \DIRECTORY_SEPARATOR;
|
||||||
$path = __DIR__.$sep."User".$sep;
|
$path = __DIR__.$sep."User".$sep;
|
||||||
$classes = [];
|
$classes = [];
|
||||||
foreach(glob($path."*".$sep."Driver.php") as $file) {
|
foreach (glob($path."*".$sep."Driver.php") as $file) {
|
||||||
$name = basename(dirname($file));
|
$name = basename(dirname($file));
|
||||||
$class = NS_BASE."User\\$name\\Driver";
|
$class = NS_BASE."User\\$name\\Driver";
|
||||||
$classes[$class] = $class::driverName();
|
$classes[$class] = $class::driverName();
|
||||||
|
@ -37,72 +37,72 @@ class User {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __toString() {
|
public function __toString() {
|
||||||
if($this->id===null) {
|
if ($this->id===null) {
|
||||||
$this->credentials();
|
$this->credentials();
|
||||||
}
|
}
|
||||||
return (string) $this->id;
|
return (string) $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks whether the logged in user is authorized to act for the affected user (used especially when granting rights)
|
// 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 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;
|
return true;
|
||||||
}
|
}
|
||||||
// if we don't have a logged-in user, fetch credentials
|
// if we don't have a logged-in user, fetch credentials
|
||||||
if($this->id===null) {
|
if ($this->id===null) {
|
||||||
$this->credentials();
|
$this->credentials();
|
||||||
}
|
}
|
||||||
// if the affected user is the actor and the actor is not trying to grant themselves rights, accept the request
|
// 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;
|
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
|
// 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);
|
$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]);
|
throw new User\Exception("doesNotExist", ["action" => $action, "user" => $affectedUser]);
|
||||||
}
|
}
|
||||||
$this->authorizationEnabled(true);
|
$this->authorizationEnabled(true);
|
||||||
// get properties of actor if not already available
|
// get properties of actor if not already available
|
||||||
if(!sizeof($this->actor)) {
|
if (!sizeof($this->actor)) {
|
||||||
$this->actor = $this->propertiesGet(Arsse::$user->id);
|
$this->actor = $this->propertiesGet(Arsse::$user->id);
|
||||||
}
|
}
|
||||||
$rights = $this->actor["rights"];
|
$rights = $this->actor["rights"];
|
||||||
// if actor is a global admin, accept the request
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
// if actor is a common user, deny the request
|
// if actor is a common user, deny the request
|
||||||
if($rights==User\Driver::RIGHTS_NONE) {
|
if ($rights==User\Driver::RIGHTS_NONE) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// if actor is not some other sort of admin, deny the request
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
// if actor is a domain admin/manager and domains don't match, deny the request
|
// 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"];
|
$test = "@".$this->actor["domain"];
|
||||||
if(substr($affectedUser,-1*strlen($test)) != $test) {
|
if (substr($affectedUser, -1*strlen($test)) != $test) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// certain actions shouldn't check affected user's rights
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
if($action=="userRightsSet") {
|
if ($action=="userRightsSet") {
|
||||||
// setting rights above your own is not allowed
|
// setting rights above your own is not allowed
|
||||||
if($newRightsLevel > $rights) {
|
if ($newRightsLevel > $rights) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// setting yourself to rights you already have is harmless and can be allowed
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
// managers can only set their own rights, and only to normal user
|
// 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 (in_array($rights, [User\Driver::RIGHTS_DOMAIN_MANAGER, User\Driver::RIGHTS_GLOBAL_MANAGER])) {
|
||||||
if($this->id != $affectedUser || $newRightsLevel != User\Driver::RIGHTS_NONE) {
|
if ($this->id != $affectedUser || $newRightsLevel != User\Driver::RIGHTS_NONE) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -110,20 +110,20 @@ class User {
|
||||||
}
|
}
|
||||||
$affectedRights = $this->rightsGet($affectedUser);
|
$affectedRights = $this->rightsGet($affectedUser);
|
||||||
// managers can only act on themselves (checked above) or regular users
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
// domain admins canot act above themselves
|
// 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 false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function credentials(): array {
|
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']];
|
$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" => ""];
|
$out = ["user" => $_SERVER['REMOTE_USER'], "password" => ""];
|
||||||
} else {
|
} else {
|
||||||
$out = ["user" => "", "password" => ""];
|
$out = ["user" => "", "password" => ""];
|
||||||
|
@ -133,25 +133,25 @@ class User {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function auth(string $user = null, string $password = null): bool {
|
public function auth(string $user = null, string $password = null): bool {
|
||||||
if($user===null) {
|
if ($user===null) {
|
||||||
return $this->authHTTP();
|
return $this->authHTTP();
|
||||||
} else {
|
} else {
|
||||||
$this->id = $user;
|
$this->id = $user;
|
||||||
$this->actor = [];
|
$this->actor = [];
|
||||||
switch($this->u->driverFunctions("auth")) {
|
switch ($this->u->driverFunctions("auth")) {
|
||||||
case User\Driver::FUNC_EXTERNAL:
|
case User\Driver::FUNC_EXTERNAL:
|
||||||
if(Arsse::$conf->userPreAuth) {
|
if (Arsse::$conf->userPreAuth) {
|
||||||
$out = true;
|
$out = true;
|
||||||
} else {
|
} else {
|
||||||
$out = $this->u->auth($user, $password);
|
$out = $this->u->auth($user, $password);
|
||||||
}
|
}
|
||||||
if($out && !Arsse::$db->userExists($user)) {
|
if ($out && !Arsse::$db->userExists($user)) {
|
||||||
$this->autoProvision($user, $password);
|
$this->autoProvision($user, $password);
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
case User\Driver::FUNC_INTERNAL:
|
case User\Driver::FUNC_INTERNAL:
|
||||||
if(Arsse::$conf->userPreAuth) {
|
if (Arsse::$conf->userPreAuth) {
|
||||||
if(!Arsse::$db->userExists($user)) {
|
if (!Arsse::$db->userExists($user)) {
|
||||||
$this->autoProvision($user, $password);
|
$this->autoProvision($user, $password);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -166,7 +166,7 @@ class User {
|
||||||
|
|
||||||
public function authHTTP(): bool {
|
public function authHTTP(): bool {
|
||||||
$cred = $this->credentials();
|
$cred = $this->credentials();
|
||||||
if(!$cred["user"]) {
|
if (!$cred["user"]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return $this->auth($cred["user"], $cred["password"]);
|
return $this->auth($cred["user"], $cred["password"]);
|
||||||
|
@ -178,15 +178,15 @@ class User {
|
||||||
|
|
||||||
public function list(string $domain = null): array {
|
public function list(string $domain = null): array {
|
||||||
$func = "userList";
|
$func = "userList";
|
||||||
switch($this->u->driverFunctions($func)) {
|
switch ($this->u->driverFunctions($func)) {
|
||||||
case User\Driver::FUNC_EXTERNAL:
|
case User\Driver::FUNC_EXTERNAL:
|
||||||
// we handle authorization checks for external drivers
|
// we handle authorization checks for external drivers
|
||||||
if($domain===null) {
|
if ($domain===null) {
|
||||||
if(!$this->authorize("@".$domain, $func)) {
|
if (!$this->authorize("@".$domain, $func)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $domain]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $domain]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if(!$this->authorize("", $func)) {
|
if (!$this->authorize("", $func)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => "all users"]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => "all users"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -199,11 +199,11 @@ class User {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function authorizationEnabled(bool $setting = null): bool {
|
public function authorizationEnabled(bool $setting = null): bool {
|
||||||
if(is_null($setting)) {
|
if (is_null($setting)) {
|
||||||
return !$this->authz;
|
return !$this->authz;
|
||||||
}
|
}
|
||||||
$this->authz += ($setting ? -1 : 1);
|
$this->authz += ($setting ? -1 : 1);
|
||||||
if($this->authz < 0) {
|
if ($this->authz < 0) {
|
||||||
$this->authz = 0;
|
$this->authz = 0;
|
||||||
}
|
}
|
||||||
return !$this->authz;
|
return !$this->authz;
|
||||||
|
@ -211,14 +211,14 @@ class User {
|
||||||
|
|
||||||
public function exists(string $user): bool {
|
public function exists(string $user): bool {
|
||||||
$func = "userExists";
|
$func = "userExists";
|
||||||
switch($this->u->driverFunctions($func)) {
|
switch ($this->u->driverFunctions($func)) {
|
||||||
case User\Driver::FUNC_EXTERNAL:
|
case User\Driver::FUNC_EXTERNAL:
|
||||||
// we handle authorization checks for external drivers
|
// 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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
|
||||||
}
|
}
|
||||||
$out = $this->u->userExists($user);
|
$out = $this->u->userExists($user);
|
||||||
if($out && !Arsse::$db->userExists($user)) {
|
if ($out && !Arsse::$db->userExists($user)) {
|
||||||
$this->autoProvision($user, "");
|
$this->autoProvision($user, "");
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
|
@ -233,15 +233,15 @@ class User {
|
||||||
|
|
||||||
public function add($user, $password = null): string {
|
public function add($user, $password = null): string {
|
||||||
$func = "userAdd";
|
$func = "userAdd";
|
||||||
switch($this->u->driverFunctions($func)) {
|
switch ($this->u->driverFunctions($func)) {
|
||||||
case User\Driver::FUNC_EXTERNAL:
|
case User\Driver::FUNC_EXTERNAL:
|
||||||
// we handle authorization checks for external drivers
|
// 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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
|
||||||
}
|
}
|
||||||
$newPassword = $this->u->userAdd($user, $password);
|
$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 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);
|
$this->autoProvision($user, $newPassword);
|
||||||
}
|
}
|
||||||
return $newPassword;
|
return $newPassword;
|
||||||
|
@ -255,16 +255,16 @@ class User {
|
||||||
|
|
||||||
public function remove(string $user): bool {
|
public function remove(string $user): bool {
|
||||||
$func = "userRemove";
|
$func = "userRemove";
|
||||||
switch($this->u->driverFunctions($func)) {
|
switch ($this->u->driverFunctions($func)) {
|
||||||
case User\Driver::FUNC_EXTERNAL:
|
case User\Driver::FUNC_EXTERNAL:
|
||||||
// we handle authorization checks for external drivers
|
// 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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
|
||||||
}
|
}
|
||||||
$out = $this->u->userRemove($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 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);
|
Arsse::$db->userRemove($user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -279,14 +279,14 @@ class User {
|
||||||
|
|
||||||
public function passwordSet(string $user, string $newPassword = null, $oldPassword = null): string {
|
public function passwordSet(string $user, string $newPassword = null, $oldPassword = null): string {
|
||||||
$func = "userPasswordSet";
|
$func = "userPasswordSet";
|
||||||
switch($this->u->driverFunctions($func)) {
|
switch ($this->u->driverFunctions($func)) {
|
||||||
case User\Driver::FUNC_EXTERNAL:
|
case User\Driver::FUNC_EXTERNAL:
|
||||||
// we handle authorization checks for external drivers
|
// 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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
|
||||||
}
|
}
|
||||||
$out = $this->u->userPasswordSet($user, $newPassword, $oldPassword);
|
$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
|
// if the password change was successful and the user exists, set the internal password to the same value
|
||||||
Arsse::$db->userPasswordSet($user, $out);
|
Arsse::$db->userPasswordSet($user, $out);
|
||||||
} else {
|
} else {
|
||||||
|
@ -305,8 +305,8 @@ class User {
|
||||||
public function propertiesGet(string $user, bool $withAvatar = false): array {
|
public function propertiesGet(string $user, bool $withAvatar = false): array {
|
||||||
// prepare default values
|
// prepare default values
|
||||||
$domain = null;
|
$domain = null;
|
||||||
if(strrpos($user,"@")!==false) {
|
if (strrpos($user, "@")!==false) {
|
||||||
$domain = substr($user,strrpos($user,"@")+1);
|
$domain = substr($user, strrpos($user, "@")+1);
|
||||||
}
|
}
|
||||||
$init = [
|
$init = [
|
||||||
"id" => $user,
|
"id" => $user,
|
||||||
|
@ -315,19 +315,19 @@ class User {
|
||||||
"domain" => $domain
|
"domain" => $domain
|
||||||
];
|
];
|
||||||
$func = "userPropertiesGet";
|
$func = "userPropertiesGet";
|
||||||
switch($this->u->driverFunctions($func)) {
|
switch ($this->u->driverFunctions($func)) {
|
||||||
case User\Driver::FUNC_EXTERNAL:
|
case User\Driver::FUNC_EXTERNAL:
|
||||||
// we handle authorization checks for external drivers
|
// 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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
|
||||||
}
|
}
|
||||||
$out = array_merge($init, $this->u->userPropertiesGet($user));
|
$out = array_merge($init, $this->u->userPropertiesGet($user));
|
||||||
// remove password if it is return (not exhaustive, but...)
|
// remove password if it is return (not exhaustive, but...)
|
||||||
if(array_key_exists('password', $out)) {
|
if (array_key_exists('password', $out)) {
|
||||||
unset($out['password']);
|
unset($out['password']);
|
||||||
}
|
}
|
||||||
// if the user does not exist in the internal database, add it
|
// 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);
|
$this->autoProvision($user, "", $out);
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
|
@ -342,20 +342,20 @@ class User {
|
||||||
|
|
||||||
public function propertiesSet(string $user, array $properties): array {
|
public function propertiesSet(string $user, array $properties): array {
|
||||||
// remove from the array any values which should be set specially
|
// remove from the array any values which should be set specially
|
||||||
foreach(['id', 'domain', 'password', 'rights'] as $key) {
|
foreach (['id', 'domain', 'password', 'rights'] as $key) {
|
||||||
if(array_key_exists($key, $properties)) {
|
if (array_key_exists($key, $properties)) {
|
||||||
unset($properties[$key]);
|
unset($properties[$key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$func = "userPropertiesSet";
|
$func = "userPropertiesSet";
|
||||||
switch($this->u->driverFunctions($func)) {
|
switch ($this->u->driverFunctions($func)) {
|
||||||
case User\Driver::FUNC_EXTERNAL:
|
case User\Driver::FUNC_EXTERNAL:
|
||||||
// we handle authorization checks for external drivers
|
// 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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
|
||||||
}
|
}
|
||||||
$out = $this->u->userPropertiesSet($user, $properties);
|
$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
|
// if the property change was successful and the user exists, set the internal properties to the same values
|
||||||
Arsse::$db->userPropertiesSet($user, $out);
|
Arsse::$db->userPropertiesSet($user, $out);
|
||||||
} else {
|
} else {
|
||||||
|
@ -373,15 +373,15 @@ class User {
|
||||||
|
|
||||||
public function rightsGet(string $user): int {
|
public function rightsGet(string $user): int {
|
||||||
$func = "userRightsGet";
|
$func = "userRightsGet";
|
||||||
switch($this->u->driverFunctions($func)) {
|
switch ($this->u->driverFunctions($func)) {
|
||||||
case User\Driver::FUNC_EXTERNAL:
|
case User\Driver::FUNC_EXTERNAL:
|
||||||
// we handle authorization checks for external drivers
|
// 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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
|
||||||
}
|
}
|
||||||
$out = $this->u->userRightsGet($user);
|
$out = $this->u->userRightsGet($user);
|
||||||
// if the user does not exist in the internal database, add it
|
// 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);
|
$this->autoProvision($user, "", null, $out);
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
|
@ -396,20 +396,20 @@ class User {
|
||||||
|
|
||||||
public function rightsSet(string $user, int $level): bool {
|
public function rightsSet(string $user, int $level): bool {
|
||||||
$func = "userRightsSet";
|
$func = "userRightsSet";
|
||||||
switch($this->u->driverFunctions($func)) {
|
switch ($this->u->driverFunctions($func)) {
|
||||||
case User\Driver::FUNC_EXTERNAL:
|
case User\Driver::FUNC_EXTERNAL:
|
||||||
// we handle authorization checks for external drivers
|
// 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]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
|
||||||
}
|
}
|
||||||
$out = $this->u->userRightsSet($user, $level);
|
$out = $this->u->userRightsSet($user, $level);
|
||||||
// if the user does not exist in the internal database, add it
|
// 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();
|
$authz = $this->authorizationEnabled();
|
||||||
$this->authorizationEnabled(false);
|
$this->authorizationEnabled(false);
|
||||||
Arsse::$db->userRightsSet($user, $level);
|
Arsse::$db->userRightsSet($user, $level);
|
||||||
$this->authorizationEnabled($authz);
|
$this->authorizationEnabled($authz);
|
||||||
} else if($out) {
|
} elseif ($out) {
|
||||||
$this->autoProvision($user, "", null, $level);
|
$this->autoProvision($user, "", null, $level);
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
|
@ -429,13 +429,14 @@ class User {
|
||||||
// set the user rights
|
// set the user rights
|
||||||
Arsse::$db->userRightsSet($user, $rights);
|
Arsse::$db->userRightsSet($user, $rights);
|
||||||
// set the user properties...
|
// 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
|
// if nothing is provided but the driver uses an external function, try to get the current values from the external source
|
||||||
try {
|
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));
|
Arsse::$db->userPropertiesSet($user, $this->u->userPropertiesGet($user));
|
||||||
}
|
}
|
||||||
} catch(\Throwable $e) {}
|
} catch (\Throwable $e) {
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// otherwise if values are provided, use those
|
// otherwise if values are provided, use those
|
||||||
Arsse::$db->userPropertiesSet($user, $properties);
|
Arsse::$db->userPropertiesSet($user, $properties);
|
||||||
|
@ -444,4 +445,4 @@ class User {
|
||||||
$this->authorizationEnabled(true);
|
$this->authorizationEnabled(true);
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\User;
|
namespace JKingWeb\Arsse\User;
|
||||||
|
|
||||||
Interface Driver {
|
interface Driver {
|
||||||
const FUNC_NOT_IMPLEMENTED = 0;
|
const FUNC_NOT_IMPLEMENTED = 0;
|
||||||
const FUNC_INTERNAL = 1;
|
const FUNC_INTERNAL = 1;
|
||||||
const FUNC_EXTERNAL = 2;
|
const FUNC_EXTERNAL = 2;
|
||||||
|
@ -14,29 +14,29 @@ Interface Driver {
|
||||||
const RIGHTS_GLOBAL_ADMIN = 100; // is completely unrestricted
|
const RIGHTS_GLOBAL_ADMIN = 100; // is completely unrestricted
|
||||||
|
|
||||||
// returns an instance of a class implementing this interface.
|
// 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)
|
// 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
|
// 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
|
// 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
|
// checks whether a user exists
|
||||||
function userExists(string $user): bool;
|
public function userExists(string $user): bool;
|
||||||
// adds a user
|
// adds a user
|
||||||
function userAdd(string $user, string $password = null): string;
|
public function userAdd(string $user, string $password = null): string;
|
||||||
// removes a user
|
// removes a user
|
||||||
function userRemove(string $user): bool;
|
public function userRemove(string $user): bool;
|
||||||
// lists all users
|
// 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
|
// 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)
|
// gets user metadata (currently not useful)
|
||||||
function userPropertiesGet(string $user): array;
|
public function userPropertiesGet(string $user): array;
|
||||||
// sets user metadata (currently not useful)
|
// 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())
|
// 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
|
// sets a user's access level
|
||||||
function userRightsSet(string $user, int $level): bool;
|
public function userRightsSet(string $user, int $level): bool;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\User;
|
namespace JKingWeb\Arsse\User;
|
||||||
|
|
||||||
class Exception extends \JKingWeb\Arsse\AbstractException {
|
class Exception extends \JKingWeb\Arsse\AbstractException {
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\User;
|
namespace JKingWeb\Arsse\User;
|
||||||
|
|
||||||
class ExceptionAuthz extends Exception {
|
class ExceptionAuthz extends Exception {
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,4 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\User;
|
namespace JKingWeb\Arsse\User;
|
||||||
|
|
||||||
class ExceptionNotImplemented extends Exception {
|
class ExceptionNotImplemented extends Exception {
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,15 +19,15 @@ final class Driver implements \JKingWeb\Arsse\User\Driver {
|
||||||
"userRightsSet" => self::FUNC_INTERNAL,
|
"userRightsSet" => self::FUNC_INTERNAL,
|
||||||
];
|
];
|
||||||
|
|
||||||
static public function driverName(): string {
|
public static function driverName(): string {
|
||||||
return Arsse::$lang->msg("Driver.User.Internal.Name");
|
return Arsse::$lang->msg("Driver.User.Internal.Name");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function driverFunctions(string $function = null) {
|
public function driverFunctions(string $function = null) {
|
||||||
if($function===null) {
|
if ($function===null) {
|
||||||
return $this->functions;
|
return $this->functions;
|
||||||
}
|
}
|
||||||
if(array_key_exists($function, $this->functions)) {
|
if (array_key_exists($function, $this->functions)) {
|
||||||
return $this->functions[$function];
|
return $this->functions[$function];
|
||||||
} else {
|
} else {
|
||||||
return self::FUNC_NOT_IMPLEMENTED;
|
return self::FUNC_NOT_IMPLEMENTED;
|
||||||
|
@ -35,4 +35,4 @@ final class Driver implements \JKingWeb\Arsse\User\Driver {
|
||||||
}
|
}
|
||||||
|
|
||||||
// see InternalFunctions.php for bulk of methods
|
// see InternalFunctions.php for bulk of methods
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\User\Internal;
|
namespace JKingWeb\Arsse\User\Internal;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
use JKingWeb\Arsse\User\Exception;
|
use JKingWeb\Arsse\User\Exception;
|
||||||
|
|
||||||
|
@ -10,51 +11,51 @@ trait InternalFunctions {
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function auth(string $user, string $password): bool {
|
public function auth(string $user, string $password): bool {
|
||||||
try {
|
try {
|
||||||
$hash = Arsse::$db->userPasswordGet($user);
|
$hash = Arsse::$db->userPasswordGet($user);
|
||||||
} catch(Exception $e) {
|
} catch (Exception $e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if($password==="" && $hash==="") {
|
if ($password==="" && $hash==="") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return password_verify($password, $hash);
|
return password_verify($password, $hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
function userExists(string $user): bool {
|
public function userExists(string $user): bool {
|
||||||
return Arsse::$db->userExists($user);
|
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);
|
return Arsse::$db->userAdd($user, $password);
|
||||||
}
|
}
|
||||||
|
|
||||||
function userRemove(string $user): bool {
|
public function userRemove(string $user): bool {
|
||||||
return Arsse::$db->userRemove($user);
|
return Arsse::$db->userRemove($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
function userList(string $domain = null): array {
|
public function userList(string $domain = null): array {
|
||||||
return Arsse::$db->userList($domain);
|
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);
|
return Arsse::$db->userPasswordSet($user, $newPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
function userPropertiesGet(string $user): array {
|
public function userPropertiesGet(string $user): array {
|
||||||
return Arsse::$db->userPropertiesGet($user);
|
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);
|
return Arsse::$db->userPropertiesSet($user, $properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
function userRightsGet(string $user): int {
|
public function userRightsGet(string $user): int {
|
||||||
return Arsse::$db->userRightsGet($user);
|
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);
|
return Arsse::$db->userRightsSet($user, $level);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use org\bovigo\vfs\vfsStream;
|
use org\bovigo\vfs\vfsStream;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\Conf */
|
/** @covers \JKingWeb\Arsse\Conf */
|
||||||
class TestConf extends Test\AbstractTest {
|
class TestConf extends Test\AbstractTest {
|
||||||
static $vfs;
|
public static $vfs;
|
||||||
static $path;
|
public static $path;
|
||||||
|
|
||||||
function setUp() {
|
public function setUp() {
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
self::$vfs = vfsStream::setup("root", null, [
|
self::$vfs = vfsStream::setup("root", null, [
|
||||||
'confGood' => '<?php return Array("lang" => "xx");',
|
'confGood' => '<?php return Array("lang" => "xx");',
|
||||||
|
@ -26,18 +27,18 @@ class TestConf extends Test\AbstractTest {
|
||||||
chmod(self::$path."confForbidden", 0000);
|
chmod(self::$path."confForbidden", 0000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tearDown() {
|
public function tearDown() {
|
||||||
self::$path = null;
|
self::$path = null;
|
||||||
self::$vfs = null;
|
self::$vfs = null;
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLoadDefaultValues() {
|
public function testLoadDefaultValues() {
|
||||||
$this->assertInstanceOf(Conf::class, new Conf());
|
$this->assertInstanceOf(Conf::class, new Conf());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @depends testLoadDefaultValues */
|
/** @depends testLoadDefaultValues */
|
||||||
function testImportFromArray() {
|
public function testImportFromArray() {
|
||||||
$arr = ['lang' => "xx"];
|
$arr = ['lang' => "xx"];
|
||||||
$conf = new Conf();
|
$conf = new Conf();
|
||||||
$conf->import($arr);
|
$conf->import($arr);
|
||||||
|
@ -45,7 +46,7 @@ class TestConf extends Test\AbstractTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @depends testImportFromArray */
|
/** @depends testImportFromArray */
|
||||||
function testImportFromFile() {
|
public function testImportFromFile() {
|
||||||
$conf = new Conf();
|
$conf = new Conf();
|
||||||
$conf->importFile(self::$path."confGood");
|
$conf->importFile(self::$path."confGood");
|
||||||
$this->assertEquals("xx", $conf->lang);
|
$this->assertEquals("xx", $conf->lang);
|
||||||
|
@ -54,43 +55,43 @@ class TestConf extends Test\AbstractTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @depends testImportFromFile */
|
/** @depends testImportFromFile */
|
||||||
function testImportFromMissingFile() {
|
public function testImportFromMissingFile() {
|
||||||
$this->assertException("fileMissing", "Conf");
|
$this->assertException("fileMissing", "Conf");
|
||||||
$conf = new Conf(self::$path."confMissing");
|
$conf = new Conf(self::$path."confMissing");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @depends testImportFromFile */
|
/** @depends testImportFromFile */
|
||||||
function testImportFromEmptyFile() {
|
public function testImportFromEmptyFile() {
|
||||||
$this->assertException("fileCorrupt", "Conf");
|
$this->assertException("fileCorrupt", "Conf");
|
||||||
$conf = new Conf(self::$path."confEmpty");
|
$conf = new Conf(self::$path."confEmpty");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @depends testImportFromFile */
|
/** @depends testImportFromFile */
|
||||||
function testImportFromFileWithoutReadPermission() {
|
public function testImportFromFileWithoutReadPermission() {
|
||||||
$this->assertException("fileUnreadable", "Conf");
|
$this->assertException("fileUnreadable", "Conf");
|
||||||
$conf = new Conf(self::$path."confUnreadable");
|
$conf = new Conf(self::$path."confUnreadable");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @depends testImportFromFile */
|
/** @depends testImportFromFile */
|
||||||
function testImportFromFileWhichIsNotAnArray() {
|
public function testImportFromFileWhichIsNotAnArray() {
|
||||||
$this->assertException("fileCorrupt", "Conf");
|
$this->assertException("fileCorrupt", "Conf");
|
||||||
$conf = new Conf(self::$path."confNotArray");
|
$conf = new Conf(self::$path."confNotArray");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @depends testImportFromFile */
|
/** @depends testImportFromFile */
|
||||||
function testImportFromFileWhichIsNotPhp() {
|
public function testImportFromFileWhichIsNotPhp() {
|
||||||
$this->assertException("fileCorrupt", "Conf");
|
$this->assertException("fileCorrupt", "Conf");
|
||||||
// this should not print the output of the non-PHP file
|
// this should not print the output of the non-PHP file
|
||||||
$conf = new Conf(self::$path."confNotPHP");
|
$conf = new Conf(self::$path."confNotPHP");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @depends testImportFromFile */
|
/** @depends testImportFromFile */
|
||||||
function testImportFromCorruptFile() {
|
public function testImportFromCorruptFile() {
|
||||||
$this->assertException("fileCorrupt", "Conf");
|
$this->assertException("fileCorrupt", "Conf");
|
||||||
$conf = new Conf(self::$path."confCorrupt");
|
$conf = new Conf(self::$path."confCorrupt");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testExportToArray() {
|
public function testExportToArray() {
|
||||||
$conf = new Conf();
|
$conf = new Conf();
|
||||||
$conf->lang = ["en", "fr"]; // should not be exported: not scalar
|
$conf->lang = ["en", "fr"]; // should not be exported: not scalar
|
||||||
$conf->dbSQLite3File = "test.db"; // should be exported: value changed
|
$conf->dbSQLite3File = "test.db"; // should be exported: value changed
|
||||||
|
@ -105,9 +106,9 @@ class TestConf extends Test\AbstractTest {
|
||||||
$this->assertArraySubset($exp, $res);
|
$this->assertArraySubset($exp, $res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @depends testExportToArray
|
/** @depends testExportToArray
|
||||||
* @depends testImportFromFile */
|
* @depends testImportFromFile */
|
||||||
function testExportToFile() {
|
public function testExportToFile() {
|
||||||
$conf = new Conf();
|
$conf = new Conf();
|
||||||
$conf->lang = ["en", "fr"]; // should not be exported: not scalar
|
$conf->lang = ["en", "fr"]; // should not be exported: not scalar
|
||||||
$conf->dbSQLite3File = "test.db"; // should be exported: value changed
|
$conf->dbSQLite3File = "test.db"; // should be exported: value changed
|
||||||
|
@ -125,12 +126,12 @@ class TestConf extends Test\AbstractTest {
|
||||||
$this->assertArraySubset($exp, $arr);
|
$this->assertArraySubset($exp, $arr);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testExportToFileWithoutWritePermission() {
|
public function testExportToFileWithoutWritePermission() {
|
||||||
$this->assertException("fileUnwritable", "Conf");
|
$this->assertException("fileUnwritable", "Conf");
|
||||||
(new Conf)->exportFile(self::$path."confUnreadable");
|
(new Conf)->exportFile(self::$path."confUnreadable");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testExportToFileWithoutCreatePermission() {
|
public function testExportToFileWithoutCreatePermission() {
|
||||||
$this->assertException("fileUncreatable", "Conf");
|
$this->assertException("fileUncreatable", "Conf");
|
||||||
(new Conf)->exportFile(self::$path."confForbidden/conf");
|
(new Conf)->exportFile(self::$path."confForbidden/conf");
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,4 +7,4 @@ class TestDatabaseArticleSQLite3 extends Test\AbstractTest {
|
||||||
use Test\Database\Setup;
|
use Test\Database\Setup;
|
||||||
use Test\Database\DriverSQLite3;
|
use Test\Database\DriverSQLite3;
|
||||||
use Test\Database\SeriesArticle;
|
use Test\Database\SeriesArticle;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,4 +7,4 @@ class TestDatabaseCleanupSQLite3 extends Test\AbstractTest {
|
||||||
use Test\Database\Setup;
|
use Test\Database\Setup;
|
||||||
use Test\Database\DriverSQLite3;
|
use Test\Database\DriverSQLite3;
|
||||||
use Test\Database\SeriesCleanup;
|
use Test\Database\SeriesCleanup;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,4 +7,4 @@ class TestDatabaseFeedSQLite3 extends Test\AbstractTest {
|
||||||
use Test\Database\Setup;
|
use Test\Database\Setup;
|
||||||
use Test\Database\DriverSQLite3;
|
use Test\Database\DriverSQLite3;
|
||||||
use Test\Database\SeriesFeed;
|
use Test\Database\SeriesFeed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,4 +7,4 @@ class TestDatabaseFolderSQLite3 extends Test\AbstractTest {
|
||||||
use Test\Database\Setup;
|
use Test\Database\Setup;
|
||||||
use Test\Database\DriverSQLite3;
|
use Test\Database\DriverSQLite3;
|
||||||
use Test\Database\SeriesFolder;
|
use Test\Database\SeriesFolder;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,4 +7,4 @@ class TestDatabaseMetaSQLite3 extends Test\AbstractTest {
|
||||||
use Test\Database\Setup;
|
use Test\Database\Setup;
|
||||||
use Test\Database\DriverSQLite3;
|
use Test\Database\DriverSQLite3;
|
||||||
use Test\Database\SeriesMeta;
|
use Test\Database\SeriesMeta;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,4 +7,4 @@ class TestDatabaseMiscellanySQLite3 extends Test\AbstractTest {
|
||||||
use Test\Database\Setup;
|
use Test\Database\Setup;
|
||||||
use Test\Database\DriverSQLite3;
|
use Test\Database\DriverSQLite3;
|
||||||
use Test\Database\SeriesMiscellany;
|
use Test\Database\SeriesMiscellany;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,4 +7,4 @@ class TestDatabaseSubscriptionSQLite3 extends Test\AbstractTest {
|
||||||
use Test\Database\Setup;
|
use Test\Database\Setup;
|
||||||
use Test\Database\DriverSQLite3;
|
use Test\Database\DriverSQLite3;
|
||||||
use Test\Database\SeriesSubscription;
|
use Test\Database\SeriesSubscription;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,4 +7,4 @@ class TestDatabaseUserSQLite3 extends Test\AbstractTest {
|
||||||
use Test\Database\Setup;
|
use Test\Database\Setup;
|
||||||
use Test\Database\DriverSQLite3;
|
use Test\Database\DriverSQLite3;
|
||||||
use Test\Database\SeriesUser;
|
use Test\Database\SeriesUser;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
use org\bovigo\vfs\vfsStream;
|
use org\bovigo\vfs\vfsStream;
|
||||||
use Phake;
|
use Phake;
|
||||||
|
@ -13,8 +14,8 @@ class TestDbDriverCreationSQLite3 extends Test\AbstractTest {
|
||||||
protected $drv;
|
protected $drv;
|
||||||
protected $ch;
|
protected $ch;
|
||||||
|
|
||||||
function setUp() {
|
public function setUp() {
|
||||||
if(!extension_loaded("sqlite3")) {
|
if (!extension_loaded("sqlite3")) {
|
||||||
$this->markTestSkipped("SQLite extension not loaded");
|
$this->markTestSkipped("SQLite extension not loaded");
|
||||||
}
|
}
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
|
@ -88,15 +89,15 @@ class TestDbDriverCreationSQLite3 extends Test\AbstractTest {
|
||||||
$this->path = $path = $vfs->url()."/";
|
$this->path = $path = $vfs->url()."/";
|
||||||
// set up access blocks
|
// set up access blocks
|
||||||
chmod($path."Cmain", 0555);
|
chmod($path."Cmain", 0555);
|
||||||
chmod($path."Cwal", 0555);
|
chmod($path."Cwal", 0555);
|
||||||
chmod($path."Cshm", 0555);
|
chmod($path."Cshm", 0555);
|
||||||
chmod($path."Rmain/arsse.db", 0333);
|
chmod($path."Rmain/arsse.db", 0333);
|
||||||
chmod($path."Rwal/arsse.db-wal", 0333);
|
chmod($path."Rwal/arsse.db-wal", 0333);
|
||||||
chmod($path."Rshm/arsse.db-shm", 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."Wwal/arsse.db-wal", 0555);
|
||||||
chmod($path."Wshm/arsse.db-shm", 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."Awal/arsse.db-wal", 0111);
|
||||||
chmod($path."Ashm/arsse.db-shm", 0111);
|
chmod($path."Ashm/arsse.db-shm", 0111);
|
||||||
// set up configuration
|
// set up configuration
|
||||||
|
@ -105,85 +106,85 @@ class TestDbDriverCreationSQLite3 extends Test\AbstractTest {
|
||||||
// set up database shim
|
// set up database shim
|
||||||
}
|
}
|
||||||
|
|
||||||
function tearDown() {
|
public function tearDown() {
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailToCreateDatabase() {
|
public function testFailToCreateDatabase() {
|
||||||
Arsse::$conf->dbSQLite3File = $this->path."Cmain/arsse.db";
|
Arsse::$conf->dbSQLite3File = $this->path."Cmain/arsse.db";
|
||||||
$this->assertException("fileUncreatable", "Db");
|
$this->assertException("fileUncreatable", "Db");
|
||||||
new Db\SQLite3\Driver;
|
new Db\SQLite3\Driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailToCreateJournal() {
|
public function testFailToCreateJournal() {
|
||||||
Arsse::$conf->dbSQLite3File = $this->path."Cwal/arsse.db";
|
Arsse::$conf->dbSQLite3File = $this->path."Cwal/arsse.db";
|
||||||
$this->assertException("fileUncreatable", "Db");
|
$this->assertException("fileUncreatable", "Db");
|
||||||
new Db\SQLite3\Driver;
|
new Db\SQLite3\Driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailToCreateSharedMmeory() {
|
public function testFailToCreateSharedMmeory() {
|
||||||
Arsse::$conf->dbSQLite3File = $this->path."Cshm/arsse.db";
|
Arsse::$conf->dbSQLite3File = $this->path."Cshm/arsse.db";
|
||||||
$this->assertException("fileUncreatable", "Db");
|
$this->assertException("fileUncreatable", "Db");
|
||||||
new Db\SQLite3\Driver;
|
new Db\SQLite3\Driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailToReadDatabase() {
|
public function testFailToReadDatabase() {
|
||||||
Arsse::$conf->dbSQLite3File = $this->path."Rmain/arsse.db";
|
Arsse::$conf->dbSQLite3File = $this->path."Rmain/arsse.db";
|
||||||
$this->assertException("fileUnreadable", "Db");
|
$this->assertException("fileUnreadable", "Db");
|
||||||
new Db\SQLite3\Driver;
|
new Db\SQLite3\Driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailToReadJournal() {
|
public function testFailToReadJournal() {
|
||||||
Arsse::$conf->dbSQLite3File = $this->path."Rwal/arsse.db";
|
Arsse::$conf->dbSQLite3File = $this->path."Rwal/arsse.db";
|
||||||
$this->assertException("fileUnreadable", "Db");
|
$this->assertException("fileUnreadable", "Db");
|
||||||
new Db\SQLite3\Driver;
|
new Db\SQLite3\Driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailToReadSharedMmeory() {
|
public function testFailToReadSharedMmeory() {
|
||||||
Arsse::$conf->dbSQLite3File = $this->path."Rshm/arsse.db";
|
Arsse::$conf->dbSQLite3File = $this->path."Rshm/arsse.db";
|
||||||
$this->assertException("fileUnreadable", "Db");
|
$this->assertException("fileUnreadable", "Db");
|
||||||
new Db\SQLite3\Driver;
|
new Db\SQLite3\Driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailToWriteToDatabase() {
|
public function testFailToWriteToDatabase() {
|
||||||
Arsse::$conf->dbSQLite3File = $this->path."Wmain/arsse.db";
|
Arsse::$conf->dbSQLite3File = $this->path."Wmain/arsse.db";
|
||||||
$this->assertException("fileUnwritable", "Db");
|
$this->assertException("fileUnwritable", "Db");
|
||||||
new Db\SQLite3\Driver;
|
new Db\SQLite3\Driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailToWriteToJournal() {
|
public function testFailToWriteToJournal() {
|
||||||
Arsse::$conf->dbSQLite3File = $this->path."Wwal/arsse.db";
|
Arsse::$conf->dbSQLite3File = $this->path."Wwal/arsse.db";
|
||||||
$this->assertException("fileUnwritable", "Db");
|
$this->assertException("fileUnwritable", "Db");
|
||||||
new Db\SQLite3\Driver;
|
new Db\SQLite3\Driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailToWriteToSharedMmeory() {
|
public function testFailToWriteToSharedMmeory() {
|
||||||
Arsse::$conf->dbSQLite3File = $this->path."Wshm/arsse.db";
|
Arsse::$conf->dbSQLite3File = $this->path."Wshm/arsse.db";
|
||||||
$this->assertException("fileUnwritable", "Db");
|
$this->assertException("fileUnwritable", "Db");
|
||||||
new Db\SQLite3\Driver;
|
new Db\SQLite3\Driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailToAccessDatabase() {
|
public function testFailToAccessDatabase() {
|
||||||
Arsse::$conf->dbSQLite3File = $this->path."Amain/arsse.db";
|
Arsse::$conf->dbSQLite3File = $this->path."Amain/arsse.db";
|
||||||
$this->assertException("fileUnusable", "Db");
|
$this->assertException("fileUnusable", "Db");
|
||||||
new Db\SQLite3\Driver;
|
new Db\SQLite3\Driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailToAccessJournal() {
|
public function testFailToAccessJournal() {
|
||||||
Arsse::$conf->dbSQLite3File = $this->path."Awal/arsse.db";
|
Arsse::$conf->dbSQLite3File = $this->path."Awal/arsse.db";
|
||||||
$this->assertException("fileUnusable", "Db");
|
$this->assertException("fileUnusable", "Db");
|
||||||
new Db\SQLite3\Driver;
|
new Db\SQLite3\Driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFailToAccessSharedMmeory() {
|
public function testFailToAccessSharedMmeory() {
|
||||||
Arsse::$conf->dbSQLite3File = $this->path."Ashm/arsse.db";
|
Arsse::$conf->dbSQLite3File = $this->path."Ashm/arsse.db";
|
||||||
$this->assertException("fileUnusable", "Db");
|
$this->assertException("fileUnusable", "Db");
|
||||||
new Db\SQLite3\Driver;
|
new Db\SQLite3\Driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testAssumeDatabaseCorruption() {
|
public function testAssumeDatabaseCorruption() {
|
||||||
Arsse::$conf->dbSQLite3File = $this->path."corrupt/arsse.db";
|
Arsse::$conf->dbSQLite3File = $this->path."corrupt/arsse.db";
|
||||||
$this->assertException("fileCorrupt", "Db");
|
$this->assertException("fileCorrupt", "Db");
|
||||||
new Db\SQLite3\Driver;
|
new Db\SQLite3\Driver;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \JKingWeb\Arsse\Db\SQLite3\Driver<extended>
|
* @covers \JKingWeb\Arsse\Db\SQLite3\Driver<extended>
|
||||||
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
|
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
|
||||||
class TestDbDriverSQLite3 extends Test\AbstractTest {
|
class TestDbDriverSQLite3 extends Test\AbstractTest {
|
||||||
|
@ -10,8 +10,8 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
|
||||||
protected $drv;
|
protected $drv;
|
||||||
protected $ch;
|
protected $ch;
|
||||||
|
|
||||||
function setUp() {
|
public function setUp() {
|
||||||
if(!extension_loaded("sqlite3")) {
|
if (!extension_loaded("sqlite3")) {
|
||||||
$this->markTestSkipped("SQLite extension not loaded");
|
$this->markTestSkipped("SQLite extension not loaded");
|
||||||
}
|
}
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
|
@ -24,110 +24,110 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
|
||||||
$this->ch->enableExceptions(true);
|
$this->ch->enableExceptions(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tearDown() {
|
public function tearDown() {
|
||||||
unset($this->drv);
|
unset($this->drv);
|
||||||
unset($this->ch);
|
unset($this->ch);
|
||||||
if(isset(Arsse::$conf)) {
|
if (isset(Arsse::$conf)) {
|
||||||
unlink(Arsse::$conf->dbSQLite3File);
|
unlink(Arsse::$conf->dbSQLite3File);
|
||||||
}
|
}
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFetchDriverName() {
|
public function testFetchDriverName() {
|
||||||
$class = Arsse::$conf->dbDriver;
|
$class = Arsse::$conf->dbDriver;
|
||||||
$this->assertTrue(strlen($class::driverName()) > 0);
|
$this->assertTrue(strlen($class::driverName()) > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testExecAValidStatement() {
|
public function testExecAValidStatement() {
|
||||||
$this->assertTrue($this->drv->exec("CREATE TABLE test(id integer primary key)"));
|
$this->assertTrue($this->drv->exec("CREATE TABLE test(id integer primary key)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testExecAnInvalidStatement() {
|
public function testExecAnInvalidStatement() {
|
||||||
$this->assertException("engineErrorGeneral", "Db");
|
$this->assertException("engineErrorGeneral", "Db");
|
||||||
$this->drv->exec("And the meek shall inherit the earth...");
|
$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->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"));
|
$this->assertEquals(2112, $this->ch->querySingle("SELECT id from test"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testExecTimeout() {
|
public function testExecTimeout() {
|
||||||
$this->ch->exec("BEGIN EXCLUSIVE TRANSACTION");
|
$this->ch->exec("BEGIN EXCLUSIVE TRANSACTION");
|
||||||
$this->assertException("general", "Db", "ExceptionTimeout");
|
$this->assertException("general", "Db", "ExceptionTimeout");
|
||||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
$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->drv->exec("CREATE TABLE test(id integer not null)");
|
||||||
$this->assertException("constraintViolation", "Db", "ExceptionInput");
|
$this->assertException("constraintViolation", "Db", "ExceptionInput");
|
||||||
$this->drv->exec("INSERT INTO test(id) values(null)");
|
$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->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||||
$this->assertException("typeViolation", "Db", "ExceptionInput");
|
$this->assertException("typeViolation", "Db", "ExceptionInput");
|
||||||
$this->drv->exec("INSERT INTO test(id) values('ook')");
|
$this->drv->exec("INSERT INTO test(id) values('ook')");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testMakeAValidQuery() {
|
public function testMakeAValidQuery() {
|
||||||
$this->assertInstanceOf(Db\Result::class, $this->drv->query("SELECT 1"));
|
$this->assertInstanceOf(Db\Result::class, $this->drv->query("SELECT 1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testMakeAnInvalidQuery() {
|
public function testMakeAnInvalidQuery() {
|
||||||
$this->assertException("engineErrorGeneral", "Db");
|
$this->assertException("engineErrorGeneral", "Db");
|
||||||
$this->drv->query("Apollo was astonished; Dionysus thought me mad");
|
$this->drv->query("Apollo was astonished; Dionysus thought me mad");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testQueryTimeout() {
|
public function testQueryTimeout() {
|
||||||
$this->ch->exec("BEGIN EXCLUSIVE TRANSACTION");
|
$this->ch->exec("BEGIN EXCLUSIVE TRANSACTION");
|
||||||
$this->assertException("general", "Db", "ExceptionTimeout");
|
$this->assertException("general", "Db", "ExceptionTimeout");
|
||||||
$this->drv->query("CREATE TABLE test(id integer primary key)");
|
$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->drv->exec("CREATE TABLE test(id integer not null)");
|
||||||
$this->assertException("constraintViolation", "Db", "ExceptionInput");
|
$this->assertException("constraintViolation", "Db", "ExceptionInput");
|
||||||
$this->drv->query("INSERT INTO test(id) values(null)");
|
$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->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||||
$this->assertException("typeViolation", "Db", "ExceptionInput");
|
$this->assertException("typeViolation", "Db", "ExceptionInput");
|
||||||
$this->drv->query("INSERT INTO test(id) values('ook')");
|
$this->drv->query("INSERT INTO test(id) values('ook')");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testPrepareAValidQuery() {
|
public function testPrepareAValidQuery() {
|
||||||
$s = $this->drv->prepare("SELECT ?, ?", "int", "int");
|
$s = $this->drv->prepare("SELECT ?, ?", "int", "int");
|
||||||
$this->assertInstanceOf(Db\Statement::class, $s);
|
$this->assertInstanceOf(Db\Statement::class, $s);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testPrepareAnInvalidQuery() {
|
public function testPrepareAnInvalidQuery() {
|
||||||
$this->assertException("engineErrorGeneral", "Db");
|
$this->assertException("engineErrorGeneral", "Db");
|
||||||
$s = $this->drv->prepare("This is an invalid query", "int", "int");
|
$s = $this->drv->prepare("This is an invalid query", "int", "int");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCreateASavepoint() {
|
public function testCreateASavepoint() {
|
||||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||||
$this->assertEquals(2, $this->drv->savepointCreate());
|
$this->assertEquals(2, $this->drv->savepointCreate());
|
||||||
$this->assertEquals(3, $this->drv->savepointCreate());
|
$this->assertEquals(3, $this->drv->savepointCreate());
|
||||||
}
|
}
|
||||||
|
|
||||||
function testReleaseASavepoint() {
|
public function testReleaseASavepoint() {
|
||||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||||
$this->assertEquals(true, $this->drv->savepointRelease());
|
$this->assertEquals(true, $this->drv->savepointRelease());
|
||||||
$this->assertException("invalid", "Db", "ExceptionSavepoint");
|
$this->assertException("invalid", "Db", "ExceptionSavepoint");
|
||||||
$this->drv->savepointRelease();
|
$this->drv->savepointRelease();
|
||||||
}
|
}
|
||||||
|
|
||||||
function testUndoASavepoint() {
|
public function testUndoASavepoint() {
|
||||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||||
$this->assertEquals(true, $this->drv->savepointUndo());
|
$this->assertEquals(true, $this->drv->savepointUndo());
|
||||||
$this->assertException("invalid", "Db", "ExceptionSavepoint");
|
$this->assertException("invalid", "Db", "ExceptionSavepoint");
|
||||||
$this->drv->savepointUndo();
|
$this->drv->savepointUndo();
|
||||||
}
|
}
|
||||||
|
|
||||||
function testManipulateSavepoints() {
|
public function testManipulateSavepoints() {
|
||||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||||
$this->assertEquals(2, $this->drv->savepointCreate());
|
$this->assertEquals(2, $this->drv->savepointCreate());
|
||||||
$this->assertEquals(3, $this->drv->savepointCreate());
|
$this->assertEquals(3, $this->drv->savepointCreate());
|
||||||
|
@ -144,7 +144,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
|
||||||
$this->drv->savepointRelease(2);
|
$this->drv->savepointRelease(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testManipulateSavepointsSomeMore() {
|
public function testManipulateSavepointsSomeMore() {
|
||||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||||
$this->assertEquals(2, $this->drv->savepointCreate());
|
$this->assertEquals(2, $this->drv->savepointCreate());
|
||||||
$this->assertEquals(3, $this->drv->savepointCreate());
|
$this->assertEquals(3, $this->drv->savepointCreate());
|
||||||
|
@ -155,7 +155,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
|
||||||
$this->drv->savepointUndo(2);
|
$this->drv->savepointUndo(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testBeginATransaction() {
|
public function testBeginATransaction() {
|
||||||
$select = "SELECT count(*) FROM test";
|
$select = "SELECT count(*) FROM test";
|
||||||
$insert = "INSERT INTO test(id) values(null)";
|
$insert = "INSERT INTO test(id) values(null)";
|
||||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
$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));
|
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCommitATransaction() {
|
public function testCommitATransaction() {
|
||||||
$select = "SELECT count(*) FROM test";
|
$select = "SELECT count(*) FROM test";
|
||||||
$insert = "INSERT INTO test(id) values(null)";
|
$insert = "INSERT INTO test(id) values(null)";
|
||||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
$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));
|
$this->assertEquals(1, $this->ch->querySingle($select));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testRollbackATransaction() {
|
public function testRollbackATransaction() {
|
||||||
$select = "SELECT count(*) FROM test";
|
$select = "SELECT count(*) FROM test";
|
||||||
$insert = "INSERT INTO test(id) values(null)";
|
$insert = "INSERT INTO test(id) values(null)";
|
||||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
$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));
|
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testBeginChainedTransactions() {
|
public function testBeginChainedTransactions() {
|
||||||
$select = "SELECT count(*) FROM test";
|
$select = "SELECT count(*) FROM test";
|
||||||
$insert = "INSERT INTO test(id) values(null)";
|
$insert = "INSERT INTO test(id) values(null)";
|
||||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
$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));
|
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCommitChainedTransactions() {
|
public function testCommitChainedTransactions() {
|
||||||
$select = "SELECT count(*) FROM test";
|
$select = "SELECT count(*) FROM test";
|
||||||
$insert = "INSERT INTO test(id) values(null)";
|
$insert = "INSERT INTO test(id) values(null)";
|
||||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
$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));
|
$this->assertEquals(2, $this->ch->querySingle($select));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCommitChainedTransactionsOutOfOrder() {
|
public function testCommitChainedTransactionsOutOfOrder() {
|
||||||
$select = "SELECT count(*) FROM test";
|
$select = "SELECT count(*) FROM test";
|
||||||
$insert = "INSERT INTO test(id) values(null)";
|
$insert = "INSERT INTO test(id) values(null)";
|
||||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||||
|
@ -243,7 +243,7 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
|
||||||
$tr2->commit();
|
$tr2->commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
function testRollbackChainedTransactions() {
|
public function testRollbackChainedTransactions() {
|
||||||
$select = "SELECT count(*) FROM test";
|
$select = "SELECT count(*) FROM test";
|
||||||
$insert = "INSERT INTO test(id) values(null)";
|
$insert = "INSERT INTO test(id) values(null)";
|
||||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
$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));
|
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testRollbackChainedTransactionsOutOfOrder() {
|
public function testRollbackChainedTransactionsOutOfOrder() {
|
||||||
$select = "SELECT count(*) FROM test";
|
$select = "SELECT count(*) FROM test";
|
||||||
$insert = "INSERT INTO test(id) values(null)";
|
$insert = "INSERT INTO test(id) values(null)";
|
||||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
$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));
|
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testPartiallyRollbackChainedTransactions() {
|
public function testPartiallyRollbackChainedTransactions() {
|
||||||
$select = "SELECT count(*) FROM test";
|
$select = "SELECT count(*) FROM test";
|
||||||
$insert = "INSERT INTO test(id) values(null)";
|
$insert = "INSERT INTO test(id) values(null)";
|
||||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
$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));
|
$this->assertEquals(1, $this->ch->querySingle($select));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFetchSchemaVersion() {
|
public function testFetchSchemaVersion() {
|
||||||
$this->assertSame(0, $this->drv->schemaVersion());
|
$this->assertSame(0, $this->drv->schemaVersion());
|
||||||
$this->drv->exec("PRAGMA user_version=1");
|
$this->drv->exec("PRAGMA user_version=1");
|
||||||
$this->assertSame(1, $this->drv->schemaVersion());
|
$this->assertSame(1, $this->drv->schemaVersion());
|
||||||
|
@ -311,17 +311,17 @@ class TestDbDriverSQLite3 extends Test\AbstractTest {
|
||||||
$this->assertSame(2, $this->drv->schemaVersion());
|
$this->assertSame(2, $this->drv->schemaVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLockTheDatabase() {
|
public function testLockTheDatabase() {
|
||||||
$this->drv->savepointCreate(true);
|
$this->drv->savepointCreate(true);
|
||||||
$this->assertException();
|
$this->assertException();
|
||||||
$this->ch->exec("CREATE TABLE test(id integer primary key)");
|
$this->ch->exec("CREATE TABLE test(id integer primary key)");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testUnlockTheDatabase() {
|
public function testUnlockTheDatabase() {
|
||||||
$this->drv->savepointCreate(true);
|
$this->drv->savepointCreate(true);
|
||||||
$this->drv->savepointRelease();
|
$this->drv->savepointRelease();
|
||||||
$this->drv->savepointCreate(true);
|
$this->drv->savepointCreate(true);
|
||||||
$this->drv->savepointUndo();
|
$this->drv->savepointUndo();
|
||||||
$this->assertSame(true, $this->ch->exec("CREATE TABLE test(id integer primary key)"));
|
$this->assertSame(true, $this->ch->exec("CREATE TABLE test(id integer primary key)"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,10 @@ namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\Db\SQLite3\Result<extended> */
|
/** @covers \JKingWeb\Arsse\Db\SQLite3\Result<extended> */
|
||||||
class TestDbResultSQLite3 extends Test\AbstractTest {
|
class TestDbResultSQLite3 extends Test\AbstractTest {
|
||||||
|
|
||||||
protected $c;
|
protected $c;
|
||||||
|
|
||||||
function setUp() {
|
public function setUp() {
|
||||||
if(!extension_loaded("sqlite3")) {
|
if (!extension_loaded("sqlite3")) {
|
||||||
$this->markTestSkipped("SQLite extension not loaded");
|
$this->markTestSkipped("SQLite extension not loaded");
|
||||||
}
|
}
|
||||||
$c = new \SQLite3(":memory:");
|
$c = new \SQLite3(":memory:");
|
||||||
|
@ -16,49 +15,49 @@ class TestDbResultSQLite3 extends Test\AbstractTest {
|
||||||
$this->c = $c;
|
$this->c = $c;
|
||||||
}
|
}
|
||||||
|
|
||||||
function tearDown() {
|
public function tearDown() {
|
||||||
$this->c->close();
|
$this->c->close();
|
||||||
unset($this->c);
|
unset($this->c);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testConstructResult() {
|
public function testConstructResult() {
|
||||||
$set = $this->c->query("SELECT 1");
|
$set = $this->c->query("SELECT 1");
|
||||||
$this->assertInstanceOf(Db\Result::class, new Db\SQLite3\Result($set));
|
$this->assertInstanceOf(Db\Result::class, new Db\SQLite3\Result($set));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testGetChangeCountAndLastInsertId() {
|
public function testGetChangeCountAndLastInsertId() {
|
||||||
$this->c->query("CREATE TABLE test(col)");
|
$this->c->query("CREATE TABLE test(col)");
|
||||||
$set = $this->c->query("INSERT INTO test(col) values(1)");
|
$set = $this->c->query("INSERT INTO test(col) values(1)");
|
||||||
$rows = $this->c->changes();
|
$rows = $this->c->changes();
|
||||||
$id = $this->c->lastInsertRowID();
|
$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($rows, $r->changes());
|
||||||
$this->assertEquals($id, $r->lastId());
|
$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");
|
$set = $this->c->query("SELECT 1 as col union select 2 as col union select 3 as col");
|
||||||
$rows = [];
|
$rows = [];
|
||||||
foreach(new Db\SQLite3\Result($set) as $index => $row) {
|
foreach (new Db\SQLite3\Result($set) as $index => $row) {
|
||||||
$rows[$index] = $row['col'];
|
$rows[$index] = $row['col'];
|
||||||
}
|
}
|
||||||
$this->assertEquals([0 => 1, 1 => 2, 2 => 3], $rows);
|
$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");
|
$set = $this->c->query("SELECT 1 as col union select 2 as col union select 3 as col");
|
||||||
$rows = [];
|
$rows = [];
|
||||||
$test = new Db\SQLite3\Result($set);
|
$test = new Db\SQLite3\Result($set);
|
||||||
foreach($test as $row) {
|
foreach ($test as $row) {
|
||||||
$rows[] = $row['col'];
|
$rows[] = $row['col'];
|
||||||
}
|
}
|
||||||
foreach($test as $row) {
|
foreach ($test as $row) {
|
||||||
$rows[] = $row['col'];
|
$rows[] = $row['col'];
|
||||||
}
|
}
|
||||||
$this->assertEquals([1,2,3,1,2,3], $rows);
|
$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");
|
$set = $this->c->query("SELECT 1867 as year union select 1970 as year union select 2112 as year");
|
||||||
$test = new Db\SQLite3\Result($set);
|
$test = new Db\SQLite3\Result($set);
|
||||||
$this->assertEquals(1867, $test->getValue());
|
$this->assertEquals(1867, $test->getValue());
|
||||||
|
@ -67,7 +66,7 @@ class TestDbResultSQLite3 extends Test\AbstractTest {
|
||||||
$this->assertSame(null, $test->getValue());
|
$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");
|
$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);
|
$test = new Db\SQLite3\Result($set);
|
||||||
$this->assertEquals(1867, $test->getValue());
|
$this->assertEquals(1867, $test->getValue());
|
||||||
|
@ -76,7 +75,7 @@ class TestDbResultSQLite3 extends Test\AbstractTest {
|
||||||
$this->assertSame(null, $test->getValue());
|
$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");
|
$set = $this->c->query("SELECT '2112' as album, '2112' as track union select 'Clockwork Angels' as album, 'The Wreckers' as track");
|
||||||
$rows = [
|
$rows = [
|
||||||
['album' => '2112', 'track' => '2112'],
|
['album' => '2112', 'track' => '2112'],
|
||||||
|
@ -88,4 +87,4 @@ class TestDbResultSQLite3 extends Test\AbstractTest {
|
||||||
$this->assertSame(null, $test->getRow());
|
$this->assertSame(null, $test->getRow());
|
||||||
$this->assertEquals($rows, $test->getAll());
|
$this->assertEquals($rows, $test->getAll());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
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\Statement<extended>
|
||||||
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
|
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
|
||||||
class TestDbStatementSQLite3 extends Test\AbstractTest {
|
class TestDbStatementSQLite3 extends Test\AbstractTest {
|
||||||
use Test\Db\BindingTests;
|
use Test\Db\BindingTests;
|
||||||
|
|
||||||
protected $c;
|
protected $c;
|
||||||
static protected $imp = Db\SQLite3\Statement::class;
|
protected static $imp = Db\SQLite3\Statement::class;
|
||||||
|
|
||||||
function setUp() {
|
public function setUp() {
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
if(!extension_loaded("sqlite3")) {
|
if (!extension_loaded("sqlite3")) {
|
||||||
$this->markTestSkipped("SQLite extension not loaded");
|
$this->markTestSkipped("SQLite extension not loaded");
|
||||||
}
|
}
|
||||||
$c = new \SQLite3(":memory:");
|
$c = new \SQLite3(":memory:");
|
||||||
|
@ -23,7 +23,7 @@ class TestDbStatementSQLite3 extends Test\AbstractTest {
|
||||||
$this->c = $c;
|
$this->c = $c;
|
||||||
}
|
}
|
||||||
|
|
||||||
function tearDown() {
|
public function tearDown() {
|
||||||
$this->c->close();
|
$this->c->close();
|
||||||
unset($this->c);
|
unset($this->c);
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ class TestDbStatementSQLite3 extends Test\AbstractTest {
|
||||||
$nativeStatement = $this->c->prepare("SELECT ? as value");
|
$nativeStatement = $this->c->prepare("SELECT ? as value");
|
||||||
$s = new self::$imp($this->c, $nativeStatement);
|
$s = new self::$imp($this->c, $nativeStatement);
|
||||||
$types = array_unique(Statement::TYPES);
|
$types = array_unique(Statement::TYPES);
|
||||||
foreach($types as $type) {
|
foreach ($types as $type) {
|
||||||
$s->rebindArray([$strict ? "strict $type" : $type]);
|
$s->rebindArray([$strict ? "strict $type" : $type]);
|
||||||
$val = $s->runArray([$input])->getRow()['value'];
|
$val = $s->runArray([$input])->getRow()['value'];
|
||||||
$this->assertSame($expectations[$type], $val, "Binding from type $type failed comparison.");
|
$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");
|
$nativeStatement = $this->c->prepare("SELECT ? as value");
|
||||||
$this->assertInstanceOf(Statement::class, new Db\SQLite3\Statement($this->c, $nativeStatement));
|
$this->assertInstanceOf(Statement::class, new Db\SQLite3\Statement($this->c, $nativeStatement));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testBindMissingValue() {
|
public function testBindMissingValue() {
|
||||||
$nativeStatement = $this->c->prepare("SELECT ? as value");
|
$nativeStatement = $this->c->prepare("SELECT ? as value");
|
||||||
$s = new self::$imp($this->c, $nativeStatement);
|
$s = new self::$imp($this->c, $nativeStatement);
|
||||||
$val = $s->runArray()->getRow()['value'];
|
$val = $s->runArray()->getRow()['value'];
|
||||||
$this->assertSame(null, $val);
|
$this->assertSame(null, $val);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testBindMultipleValues() {
|
public function testBindMultipleValues() {
|
||||||
$exp = [
|
$exp = [
|
||||||
'one' => 1,
|
'one' => 1,
|
||||||
'two' => 2,
|
'two' => 2,
|
||||||
|
@ -65,7 +65,7 @@ class TestDbStatementSQLite3 extends Test\AbstractTest {
|
||||||
$this->assertSame($exp, $val);
|
$this->assertSame($exp, $val);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testBindRecursively() {
|
public function testBindRecursively() {
|
||||||
$exp = [
|
$exp = [
|
||||||
'one' => 1,
|
'one' => 1,
|
||||||
'two' => 2,
|
'two' => 2,
|
||||||
|
@ -78,14 +78,14 @@ class TestDbStatementSQLite3 extends Test\AbstractTest {
|
||||||
$this->assertSame($exp, $val);
|
$this->assertSame($exp, $val);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testBindWithoutType() {
|
public function testBindWithoutType() {
|
||||||
$nativeStatement = $this->c->prepare("SELECT ? as value");
|
$nativeStatement = $this->c->prepare("SELECT ? as value");
|
||||||
$this->assertException("paramTypeMissing", "Db");
|
$this->assertException("paramTypeMissing", "Db");
|
||||||
$s = new self::$imp($this->c, $nativeStatement, []);
|
$s = new self::$imp($this->c, $nativeStatement, []);
|
||||||
$s->runArray([1]);
|
$s->runArray([1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testViolateConstraint() {
|
public function testViolateConstraint() {
|
||||||
$this->c->exec("CREATE TABLE test(id integer not null)");
|
$this->c->exec("CREATE TABLE test(id integer not null)");
|
||||||
$nativeStatement = $this->c->prepare("INSERT INTO test(id) values(?)");
|
$nativeStatement = $this->c->prepare("INSERT INTO test(id) values(?)");
|
||||||
$s = new self::$imp($this->c, $nativeStatement, ["int"]);
|
$s = new self::$imp($this->c, $nativeStatement, ["int"]);
|
||||||
|
@ -93,11 +93,11 @@ class TestDbStatementSQLite3 extends Test\AbstractTest {
|
||||||
$s->runArray([null]);
|
$s->runArray([null]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testMismatchTypes() {
|
public function testMismatchTypes() {
|
||||||
$this->c->exec("CREATE TABLE test(id integer primary key)");
|
$this->c->exec("CREATE TABLE test(id integer primary key)");
|
||||||
$nativeStatement = $this->c->prepare("INSERT INTO test(id) values(?)");
|
$nativeStatement = $this->c->prepare("INSERT INTO test(id) values(?)");
|
||||||
$s = new self::$imp($this->c, $nativeStatement, ["str"]);
|
$s = new self::$imp($this->c, $nativeStatement, ["str"]);
|
||||||
$this->assertException("typeViolation", "Db", "ExceptionInput");
|
$this->assertException("typeViolation", "Db", "ExceptionInput");
|
||||||
$s->runArray(['ook']);
|
$s->runArray(['ook']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
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\Driver<extended>
|
||||||
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
|
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
|
||||||
class TestDbUpdateSQLite3 extends Test\AbstractTest {
|
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 MINIMAL1 = "create table arsse_meta(key text primary key not null, value text); pragma user_version=1";
|
||||||
const MINIMAL2 = "pragma user_version=2";
|
const MINIMAL2 = "pragma user_version=2";
|
||||||
|
|
||||||
function setUp(Conf $conf = null) {
|
public function setUp(Conf $conf = null) {
|
||||||
if(!extension_loaded("sqlite3")) {
|
if (!extension_loaded("sqlite3")) {
|
||||||
$this->markTestSkipped("SQLite extension not loaded");
|
$this->markTestSkipped("SQLite extension not loaded");
|
||||||
}
|
}
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
$this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]);
|
$this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]);
|
||||||
if(!$conf) {
|
if (!$conf) {
|
||||||
$conf = new Conf();
|
$conf = new Conf();
|
||||||
}
|
}
|
||||||
$conf->dbDriver = Db\SQLite3\Driver::class;
|
$conf->dbDriver = Db\SQLite3\Driver::class;
|
||||||
|
@ -33,68 +33,68 @@ class TestDbUpdateSQLite3 extends Test\AbstractTest {
|
||||||
$this->drv = new Db\SQLite3\Driver(true);
|
$this->drv = new Db\SQLite3\Driver(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tearDown() {
|
public function tearDown() {
|
||||||
unset($this->drv);
|
unset($this->drv);
|
||||||
unset($this->data);
|
unset($this->data);
|
||||||
unset($this->vfs);
|
unset($this->vfs);
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLoadMissingFile() {
|
public function testLoadMissingFile() {
|
||||||
$this->assertException("updateFileMissing", "Db");
|
$this->assertException("updateFileMissing", "Db");
|
||||||
$this->drv->schemaUpdate(1, $this->base);
|
$this->drv->schemaUpdate(1, $this->base);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLoadUnreadableFile() {
|
public function testLoadUnreadableFile() {
|
||||||
touch($this->path."0.sql");
|
touch($this->path."0.sql");
|
||||||
chmod($this->path."0.sql", 0000);
|
chmod($this->path."0.sql", 0000);
|
||||||
$this->assertException("updateFileUnreadable", "Db");
|
$this->assertException("updateFileUnreadable", "Db");
|
||||||
$this->drv->schemaUpdate(1, $this->base);
|
$this->drv->schemaUpdate(1, $this->base);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLoadCorruptFile() {
|
public function testLoadCorruptFile() {
|
||||||
file_put_contents($this->path."0.sql", "This is a corrupt file");
|
file_put_contents($this->path."0.sql", "This is a corrupt file");
|
||||||
$this->assertException("updateFileError", "Db");
|
$this->assertException("updateFileError", "Db");
|
||||||
$this->drv->schemaUpdate(1, $this->base);
|
$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);");
|
file_put_contents($this->path."0.sql", "create table arsse_meta(key text primary key not null, value text);");
|
||||||
$this->assertException("updateFileIncomplete", "Db");
|
$this->assertException("updateFileIncomplete", "Db");
|
||||||
$this->drv->schemaUpdate(1, $this->base);
|
$this->drv->schemaUpdate(1, $this->base);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLoadCorrectFile() {
|
public function testLoadCorrectFile() {
|
||||||
file_put_contents($this->path."0.sql", self::MINIMAL1);
|
file_put_contents($this->path."0.sql", self::MINIMAL1);
|
||||||
$this->drv->schemaUpdate(1, $this->base);
|
$this->drv->schemaUpdate(1, $this->base);
|
||||||
$this->assertEquals(1, $this->drv->schemaVersion());
|
$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."0.sql", self::MINIMAL1);
|
||||||
file_put_contents($this->path."1.sql", "");
|
file_put_contents($this->path."1.sql", "");
|
||||||
$this->assertException("updateFileIncomplete", "Db");
|
$this->assertException("updateFileIncomplete", "Db");
|
||||||
try {
|
try {
|
||||||
$this->drv->schemaUpdate(2, $this->base);
|
$this->drv->schemaUpdate(2, $this->base);
|
||||||
} catch(Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->assertEquals(1, $this->drv->schemaVersion());
|
$this->assertEquals(1, $this->drv->schemaVersion());
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testPerformSequentialUpdate() {
|
public function testPerformSequentialUpdate() {
|
||||||
file_put_contents($this->path."0.sql", self::MINIMAL1);
|
file_put_contents($this->path."0.sql", self::MINIMAL1);
|
||||||
file_put_contents($this->path."1.sql", self::MINIMAL2);
|
file_put_contents($this->path."1.sql", self::MINIMAL2);
|
||||||
$this->drv->schemaUpdate(2, $this->base);
|
$this->drv->schemaUpdate(2, $this->base);
|
||||||
$this->assertEquals(2, $this->drv->schemaVersion());
|
$this->assertEquals(2, $this->drv->schemaVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
function testPerformActualUpdate() {
|
public function testPerformActualUpdate() {
|
||||||
$this->drv->schemaUpdate(Database::SCHEMA_VERSION);
|
$this->drv->schemaUpdate(Database::SCHEMA_VERSION);
|
||||||
$this->assertEquals(Database::SCHEMA_VERSION, $this->drv->schemaVersion());
|
$this->assertEquals(Database::SCHEMA_VERSION, $this->drv->schemaVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDeclineManualUpdate() {
|
public function testDeclineManualUpdate() {
|
||||||
// turn auto-updating off
|
// turn auto-updating off
|
||||||
$conf = new Conf();
|
$conf = new Conf();
|
||||||
$conf->dbAutoUpdate = false;
|
$conf->dbAutoUpdate = false;
|
||||||
|
@ -103,8 +103,8 @@ class TestDbUpdateSQLite3 extends Test\AbstractTest {
|
||||||
$this->drv->schemaUpdate(Database::SCHEMA_VERSION);
|
$this->drv->schemaUpdate(Database::SCHEMA_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDeclineDowngrade() {
|
public function testDeclineDowngrade() {
|
||||||
$this->assertException("updateTooNew", "Db");
|
$this->assertException("updateTooNew", "Db");
|
||||||
$this->drv->schemaUpdate(-1, $this->base);
|
$this->drv->schemaUpdate(-1, $this->base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Db\Transaction;
|
use JKingWeb\Arsse\Db\Transaction;
|
||||||
use Phake;
|
use Phake;
|
||||||
|
|
||||||
|
@ -9,7 +10,7 @@ use Phake;
|
||||||
class TestTransaction extends Test\AbstractTest {
|
class TestTransaction extends Test\AbstractTest {
|
||||||
protected $drv;
|
protected $drv;
|
||||||
|
|
||||||
function setUp() {
|
public function setUp() {
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
$drv = Phake::mock(Db\SQLite3\Driver::class);
|
$drv = Phake::mock(Db\SQLite3\Driver::class);
|
||||||
Phake::when($drv)->savepointRelease->thenReturn(true);
|
Phake::when($drv)->savepointRelease->thenReturn(true);
|
||||||
|
@ -18,7 +19,7 @@ class TestTransaction extends Test\AbstractTest {
|
||||||
$this->drv = $drv;
|
$this->drv = $drv;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testManipulateTransactions() {
|
public function testManipulateTransactions() {
|
||||||
$tr1 = new Transaction($this->drv);
|
$tr1 = new Transaction($this->drv);
|
||||||
$tr2 = new Transaction($this->drv);
|
$tr2 = new Transaction($this->drv);
|
||||||
Phake::verify($this->drv, Phake::times(2))->savepointCreate;
|
Phake::verify($this->drv, Phake::times(2))->savepointCreate;
|
||||||
|
@ -30,7 +31,7 @@ class TestTransaction extends Test\AbstractTest {
|
||||||
Phake::verify($this->drv)->savepointUndo(2);
|
Phake::verify($this->drv)->savepointUndo(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCloseTransactions() {
|
public function testCloseTransactions() {
|
||||||
$tr1 = new Transaction($this->drv);
|
$tr1 = new Transaction($this->drv);
|
||||||
$tr2 = new Transaction($this->drv);
|
$tr2 = new Transaction($this->drv);
|
||||||
$this->assertTrue($tr1->isPending());
|
$this->assertTrue($tr1->isPending());
|
||||||
|
@ -45,7 +46,7 @@ class TestTransaction extends Test\AbstractTest {
|
||||||
Phake::verify($this->drv)->savepointUndo(2);
|
Phake::verify($this->drv)->savepointUndo(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testIgnoreRollbackErrors() {
|
public function testIgnoreRollbackErrors() {
|
||||||
Phake::when($this->drv)->savepointUndo->thenThrow(new Db\ExceptionSavepoint("stale"));
|
Phake::when($this->drv)->savepointUndo->thenThrow(new Db\ExceptionSavepoint("stale"));
|
||||||
$tr1 = new Transaction($this->drv);
|
$tr1 = new Transaction($this->drv);
|
||||||
$tr2 = 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(1);
|
||||||
Phake::verify($this->drv)->savepointUndo(2);
|
Phake::verify($this->drv)->savepointUndo(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
Use Phake;
|
|
||||||
|
|
||||||
|
use Phake;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\AbstractException */
|
/** @covers \JKingWeb\Arsse\AbstractException */
|
||||||
class TestException extends Test\AbstractTest {
|
class TestException extends Test\AbstractTest {
|
||||||
function setUp() {
|
public function setUp() {
|
||||||
$this->clearData(false);
|
$this->clearData(false);
|
||||||
// create a mock Lang object so as not to create a dependency loop
|
// create a mock Lang object so as not to create a dependency loop
|
||||||
Arsse::$lang = Phake::mock(Lang::class);
|
Arsse::$lang = Phake::mock(Lang::class);
|
||||||
Phake::when(Arsse::$lang)->msg->thenReturn("");
|
Phake::when(Arsse::$lang)->msg->thenReturn("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function tearDown() {
|
public function tearDown() {
|
||||||
// verify calls to the mock Lang object
|
// verify calls to the mock Lang object
|
||||||
Phake::verify(Arsse::$lang, Phake::atLeast(0))->msg($this->isType("string"), $this->anything());
|
Phake::verify(Arsse::$lang, Phake::atLeast(0))->msg($this->isType("string"), $this->anything());
|
||||||
Phake::verifyNoOtherInteractions(Arsse::$lang);
|
Phake::verifyNoOtherInteractions(Arsse::$lang);
|
||||||
|
@ -21,7 +21,7 @@ class TestException extends Test\AbstractTest {
|
||||||
$this->clearData(true);
|
$this->clearData(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testBaseClass() {
|
public function testBaseClass() {
|
||||||
$this->assertException("unknown");
|
$this->assertException("unknown");
|
||||||
throw new Exception("unknown");
|
throw new Exception("unknown");
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ class TestException extends Test\AbstractTest {
|
||||||
/**
|
/**
|
||||||
* @depends testBaseClass
|
* @depends testBaseClass
|
||||||
*/
|
*/
|
||||||
function testBaseClassWithoutMessage() {
|
public function testBaseClassWithoutMessage() {
|
||||||
$this->assertException("unknown");
|
$this->assertException("unknown");
|
||||||
throw new Exception();
|
throw new Exception();
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ class TestException extends Test\AbstractTest {
|
||||||
/**
|
/**
|
||||||
* @depends testBaseClass
|
* @depends testBaseClass
|
||||||
*/
|
*/
|
||||||
function testDerivedClass() {
|
public function testDerivedClass() {
|
||||||
$this->assertException("fileMissing", "Lang");
|
$this->assertException("fileMissing", "Lang");
|
||||||
throw new Lang\Exception("fileMissing");
|
throw new Lang\Exception("fileMissing");
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ class TestException extends Test\AbstractTest {
|
||||||
/**
|
/**
|
||||||
* @depends testDerivedClass
|
* @depends testDerivedClass
|
||||||
*/
|
*/
|
||||||
function testDerivedClassWithMessageParameters() {
|
public function testDerivedClassWithMessageParameters() {
|
||||||
$this->assertException("fileMissing", "Lang");
|
$this->assertException("fileMissing", "Lang");
|
||||||
throw new Lang\Exception("fileMissing", "en");
|
throw new Lang\Exception("fileMissing", "en");
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ class TestException extends Test\AbstractTest {
|
||||||
/**
|
/**
|
||||||
* @depends testBaseClass
|
* @depends testBaseClass
|
||||||
*/
|
*/
|
||||||
function testBaseClassWithUnknownCode() {
|
public function testBaseClassWithUnknownCode() {
|
||||||
$this->assertException("uncoded");
|
$this->assertException("uncoded");
|
||||||
throw new Exception("testThisExceptionMessageDoesNotExist");
|
throw new Exception("testThisExceptionMessageDoesNotExist");
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ class TestException extends Test\AbstractTest {
|
||||||
/**
|
/**
|
||||||
* @depends testBaseClassWithUnknownCode
|
* @depends testBaseClassWithUnknownCode
|
||||||
*/
|
*/
|
||||||
function testDerivedClassWithMissingMessage() {
|
public function testDerivedClassWithMissingMessage() {
|
||||||
$this->assertException("uncoded");
|
$this->assertException("uncoded");
|
||||||
throw new Lang\Exception("testThisExceptionMessageDoesNotExist");
|
throw new Lang\Exception("testThisExceptionMessageDoesNotExist");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
use Phake;
|
use Phake;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \JKingWeb\Arsse\Feed
|
* @covers \JKingWeb\Arsse\Feed
|
||||||
* @covers \JKingWeb\Arsse\Feed\Exception */
|
* @covers \JKingWeb\Arsse\Feed\Exception */
|
||||||
|
@ -80,8 +80,8 @@ class TestFeed extends Test\AbstractTest {
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
function setUp() {
|
public function setUp() {
|
||||||
if(!@file_get_contents(self::$host."IsUp")) {
|
if (!@file_get_contents(self::$host."IsUp")) {
|
||||||
$this->markTestSkipped("Test Web server is not accepting requests");
|
$this->markTestSkipped("Test Web server is not accepting requests");
|
||||||
}
|
}
|
||||||
$this->base = self::$host."Feed/";
|
$this->base = self::$host."Feed/";
|
||||||
|
@ -90,7 +90,7 @@ class TestFeed extends Test\AbstractTest {
|
||||||
Arsse::$db = Phake::mock(Database::class);
|
Arsse::$db = Phake::mock(Database::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testParseAFeed() {
|
public function testParseAFeed() {
|
||||||
// test that various properties are set on the feed and on items
|
// test that various properties are set on the feed and on items
|
||||||
$f = new Feed(null, $this->base."Parsing/Valid");
|
$f = new Feed(null, $this->base."Parsing/Valid");
|
||||||
$this->assertTrue(isset($f->lastModified));
|
$this->assertTrue(isset($f->lastModified));
|
||||||
|
@ -133,27 +133,27 @@ class TestFeed extends Test\AbstractTest {
|
||||||
$this->assertSame($categories, $f->data->items[5]->categories);
|
$this->assertSame($categories, $f->data->items[5]->categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testParseEntityExpansionAttack() {
|
public function testParseEntityExpansionAttack() {
|
||||||
$this->assertException("xmlEntity", "Feed");
|
$this->assertException("xmlEntity", "Feed");
|
||||||
new Feed(null, $this->base."Parsing/XEEAttack");
|
new Feed(null, $this->base."Parsing/XEEAttack");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testParseExternalEntityAttack() {
|
public function testParseExternalEntityAttack() {
|
||||||
$this->assertException("xmlEntity", "Feed");
|
$this->assertException("xmlEntity", "Feed");
|
||||||
new Feed(null, $this->base."Parsing/XXEAttack");
|
new Feed(null, $this->base."Parsing/XXEAttack");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testParseAnUnsupportedFeed() {
|
public function testParseAnUnsupportedFeed() {
|
||||||
$this->assertException("unsupportedFeedFormat", "Feed");
|
$this->assertException("unsupportedFeedFormat", "Feed");
|
||||||
new Feed(null, $this->base."Parsing/Unsupported");
|
new Feed(null, $this->base."Parsing/Unsupported");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testParseAMalformedFeed() {
|
public function testParseAMalformedFeed() {
|
||||||
$this->assertException("malformedXml", "Feed");
|
$this->assertException("malformedXml", "Feed");
|
||||||
new Feed(null, $this->base."Parsing/Malformed");
|
new Feed(null, $this->base."Parsing/Malformed");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDeduplicateFeedItems() {
|
public function testDeduplicateFeedItems() {
|
||||||
// duplicates with dates lead to the newest match being kept
|
// duplicates with dates lead to the newest match being kept
|
||||||
$t = strtotime("2002-05-19T15:21:36Z");
|
$t = strtotime("2002-05-19T15:21:36Z");
|
||||||
$f = new Feed(null, $this->base."Deduplication/Permalink-Dates");
|
$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);
|
$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
|
// upon 304, the client should re-use the caching header values it supplied the server
|
||||||
$t = time();
|
$t = time();
|
||||||
$e = "78567a";
|
$e = "78567a";
|
||||||
|
@ -198,7 +198,7 @@ class TestFeed extends Test\AbstractTest {
|
||||||
$this->assertSame($e, $f->resource->getETag());
|
$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
|
// these tests should trust the server-returned time, even in cases of obviously incorrect results
|
||||||
$t = time() - 2000;
|
$t = time() - 2000;
|
||||||
$f = new Feed(null, $this->base."Caching/200Past");
|
$f = new Feed(null, $this->base."Caching/200Past");
|
||||||
|
@ -226,11 +226,11 @@ class TestFeed extends Test\AbstractTest {
|
||||||
$this->assertTime($t, $f->lastModified);
|
$this->assertTime($t, $f->lastModified);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testComputeNextFetchOnError() {
|
public function testComputeNextFetchOnError() {
|
||||||
for($a = 0; $a < 100; $a++) {
|
for ($a = 0; $a < 100; $a++) {
|
||||||
if($a < 3) {
|
if ($a < 3) {
|
||||||
$this->assertTime("now + 5 minutes", Feed::nextFetchOnError($a));
|
$this->assertTime("now + 5 minutes", Feed::nextFetchOnError($a));
|
||||||
} else if($a < 15) {
|
} elseif ($a < 15) {
|
||||||
$this->assertTime("now + 3 hours", Feed::nextFetchOnError($a));
|
$this->assertTime("now + 3 hours", Feed::nextFetchOnError($a));
|
||||||
} else {
|
} else {
|
||||||
$this->assertTime("now + 1 day", Feed::nextFetchOnError($a));
|
$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
|
// if less than half an hour, check in 15 minutes
|
||||||
$t = strtotime("now");
|
$t = strtotime("now");
|
||||||
$f = new Feed(null, $this->base."NextFetch/NotModified?t=$t", Date::transform($t, "http"));
|
$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);
|
$this->assertTime($exp, $f->nextFetch);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testComputeNextFetchFrom200() {
|
public function testComputeNextFetchFrom200() {
|
||||||
// if less than half an hour, check in 15 minutes
|
// if less than half an hour, check in 15 minutes
|
||||||
$f = new Feed(null, $this->base."NextFetch/30m");
|
$f = new Feed(null, $this->base."NextFetch/30m");
|
||||||
$exp = strtotime("now + 15 minutes");
|
$exp = strtotime("now + 15 minutes");
|
||||||
|
@ -313,7 +313,7 @@ class TestFeed extends Test\AbstractTest {
|
||||||
$this->assertTime($exp, $f->nextFetch);
|
$this->assertTime($exp, $f->nextFetch);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testMatchLatestArticles() {
|
public function testMatchLatestArticles() {
|
||||||
Phake::when(Arsse::$db)->feedMatchLatest(1, $this->anything())->thenReturn(new Test\Result($this->latest));
|
Phake::when(Arsse::$db)->feedMatchLatest(1, $this->anything())->thenReturn(new Test\Result($this->latest));
|
||||||
$f = new Feed(1, $this->base."Matching/1");
|
$f = new Feed(1, $this->base."Matching/1");
|
||||||
$this->assertCount(0, $f->newItems);
|
$this->assertCount(0, $f->newItems);
|
||||||
|
@ -329,15 +329,15 @@ class TestFeed extends Test\AbstractTest {
|
||||||
$this->assertCount(2, $f->changedItems);
|
$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)->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));
|
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");
|
$f = new Feed(1, $this->base."Matching/5");
|
||||||
$this->assertCount(0, $f->newItems);
|
$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
|
// first make sure that the absence of scraping works as expected
|
||||||
$f = new Feed(null, $this->base."Scraping/Feed");
|
$f = new Feed(null, $this->base."Scraping/Feed");
|
||||||
$exp = "<p>Partial content</p>";
|
$exp = "<p>Partial content</p>";
|
||||||
|
@ -347,4 +347,4 @@ class TestFeed extends Test\AbstractTest {
|
||||||
$exp = "<p>Partial content, followed by more content</p>";
|
$exp = "<p>Partial content, followed by more content</p>";
|
||||||
$this->assertSame($exp, $f->newItems[0]->content);
|
$this->assertSame($exp, $f->newItems[0]->content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
Use Phake;
|
|
||||||
|
|
||||||
|
use Phake;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\Feed */
|
/** @covers \JKingWeb\Arsse\Feed */
|
||||||
class TestFeedFetching extends Test\AbstractTest {
|
class TestFeedFetching extends Test\AbstractTest {
|
||||||
protected static $host = "http://localhost:8000/";
|
protected static $host = "http://localhost:8000/";
|
||||||
protected $base = "";
|
protected $base = "";
|
||||||
|
|
||||||
function setUp() {
|
public function setUp() {
|
||||||
if(!extension_loaded('curl')) {
|
if (!extension_loaded('curl')) {
|
||||||
$this->markTestSkipped("Feed fetching tests are only accurate with curl enabled.");
|
$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->markTestSkipped("Test Web server is not accepting requests");
|
||||||
}
|
}
|
||||||
$this->base = self::$host."Feed/";
|
$this->base = self::$host."Feed/";
|
||||||
|
@ -20,50 +20,50 @@ class TestFeedFetching extends Test\AbstractTest {
|
||||||
Arsse::$conf = new Conf();
|
Arsse::$conf = new Conf();
|
||||||
}
|
}
|
||||||
|
|
||||||
function testHandle400() {
|
public function testHandle400() {
|
||||||
$this->assertException("unsupportedFeedFormat", "Feed");
|
$this->assertException("unsupportedFeedFormat", "Feed");
|
||||||
new Feed(null, $this->base."Fetching/Error?code=400");
|
new Feed(null, $this->base."Fetching/Error?code=400");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testHandle401() {
|
public function testHandle401() {
|
||||||
$this->assertException("unauthorized", "Feed");
|
$this->assertException("unauthorized", "Feed");
|
||||||
new Feed(null, $this->base."Fetching/Error?code=401");
|
new Feed(null, $this->base."Fetching/Error?code=401");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testHandle403() {
|
public function testHandle403() {
|
||||||
$this->assertException("forbidden", "Feed");
|
$this->assertException("forbidden", "Feed");
|
||||||
new Feed(null, $this->base."Fetching/Error?code=403");
|
new Feed(null, $this->base."Fetching/Error?code=403");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testHandle404() {
|
public function testHandle404() {
|
||||||
$this->assertException("invalidUrl", "Feed");
|
$this->assertException("invalidUrl", "Feed");
|
||||||
new Feed(null, $this->base."Fetching/Error?code=404");
|
new Feed(null, $this->base."Fetching/Error?code=404");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testHandle500() {
|
public function testHandle500() {
|
||||||
$this->assertException("unsupportedFeedFormat", "Feed");
|
$this->assertException("unsupportedFeedFormat", "Feed");
|
||||||
new Feed(null, $this->base."Fetching/Error?code=500");
|
new Feed(null, $this->base."Fetching/Error?code=500");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testHandleARedirectLoop() {
|
public function testHandleARedirectLoop() {
|
||||||
$this->assertException("maxRedirect", "Feed");
|
$this->assertException("maxRedirect", "Feed");
|
||||||
new Feed(null, $this->base."Fetching/EndlessLoop?i=0");
|
new Feed(null, $this->base."Fetching/EndlessLoop?i=0");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testHandleATimeout() {
|
public function testHandleATimeout() {
|
||||||
Arsse::$conf->fetchTimeout = 1;
|
Arsse::$conf->fetchTimeout = 1;
|
||||||
$this->assertException("timeout", "Feed");
|
$this->assertException("timeout", "Feed");
|
||||||
new Feed(null, $this->base."Fetching/Timeout");
|
new Feed(null, $this->base."Fetching/Timeout");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testHandleAnOverlyLargeFeed() {
|
public function testHandleAnOverlyLargeFeed() {
|
||||||
Arsse::$conf->fetchSizeLimit = 512;
|
Arsse::$conf->fetchSizeLimit = 512;
|
||||||
$this->assertException("maxSize", "Feed");
|
$this->assertException("maxSize", "Feed");
|
||||||
new Feed(null, $this->base."Fetching/TooLarge");
|
new Feed(null, $this->base."Fetching/TooLarge");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testHandleACertificateError() {
|
public function testHandleACertificateError() {
|
||||||
$this->assertException("invalidCertificate", "Feed");
|
$this->assertException("invalidCertificate", "Feed");
|
||||||
new Feed(null, "https://localhost:8000/");
|
new Feed(null, "https://localhost:8000/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use org\bovigo\vfs\vfsStream;
|
use org\bovigo\vfs\vfsStream;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\Lang */
|
/** @covers \JKingWeb\Arsse\Lang */
|
||||||
|
@ -11,14 +12,14 @@ class TestLang extends Test\AbstractTest {
|
||||||
public $path;
|
public $path;
|
||||||
public $l;
|
public $l;
|
||||||
|
|
||||||
function testListLanguages() {
|
public function testListLanguages() {
|
||||||
$this->assertCount(sizeof($this->files), $this->l->list("en"));
|
$this->assertCount(sizeof($this->files), $this->l->list("en"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @depends testListLanguages
|
* @depends testListLanguages
|
||||||
*/
|
*/
|
||||||
function testSetLanguage() {
|
public function testSetLanguage() {
|
||||||
$this->assertEquals("en", $this->l->set("en"));
|
$this->assertEquals("en", $this->l->set("en"));
|
||||||
$this->assertEquals("en_ca", $this->l->set("en_ca"));
|
$this->assertEquals("en_ca", $this->l->set("en_ca"));
|
||||||
$this->assertEquals("de", $this->l->set("de_ch"));
|
$this->assertEquals("de", $this->l->set("de_ch"));
|
||||||
|
@ -31,7 +32,7 @@ class TestLang extends Test\AbstractTest {
|
||||||
/**
|
/**
|
||||||
* @depends testSetLanguage
|
* @depends testSetLanguage
|
||||||
*/
|
*/
|
||||||
function testLoadInternalStrings() {
|
public function testLoadInternalStrings() {
|
||||||
$this->assertEquals("", $this->l->set("", true));
|
$this->assertEquals("", $this->l->set("", true));
|
||||||
$this->assertCount(sizeof(Lang::REQUIRED), $this->l->dump());
|
$this->assertCount(sizeof(Lang::REQUIRED), $this->l->dump());
|
||||||
}
|
}
|
||||||
|
@ -39,7 +40,7 @@ class TestLang extends Test\AbstractTest {
|
||||||
/**
|
/**
|
||||||
* @depends testLoadInternalStrings
|
* @depends testLoadInternalStrings
|
||||||
*/
|
*/
|
||||||
function testLoadDefaultLanguage() {
|
public function testLoadDefaultLanguage() {
|
||||||
$this->assertEquals(Lang::DEFAULT, $this->l->set(Lang::DEFAULT, true));
|
$this->assertEquals(Lang::DEFAULT, $this->l->set(Lang::DEFAULT, true));
|
||||||
$str = $this->l->dump();
|
$str = $this->l->dump();
|
||||||
$this->assertArrayHasKey('Exception.JKingWeb/Arsse/Exception.uncoded', $str);
|
$this->assertArrayHasKey('Exception.JKingWeb/Arsse/Exception.uncoded', $str);
|
||||||
|
@ -49,7 +50,7 @@ class TestLang extends Test\AbstractTest {
|
||||||
/**
|
/**
|
||||||
* @depends testLoadDefaultLanguage
|
* @depends testLoadDefaultLanguage
|
||||||
*/
|
*/
|
||||||
function testLoadSupplementaryLanguage() {
|
public function testLoadSupplementaryLanguage() {
|
||||||
$this->l->set(Lang::DEFAULT, true);
|
$this->l->set(Lang::DEFAULT, true);
|
||||||
$this->assertEquals("ja", $this->l->set("ja", true));
|
$this->assertEquals("ja", $this->l->set("ja", true));
|
||||||
$str = $this->l->dump();
|
$str = $this->l->dump();
|
||||||
|
@ -57,5 +58,4 @@ class TestLang extends Test\AbstractTest {
|
||||||
$this->assertArrayHasKey('Test.presentText', $str);
|
$this->assertArrayHasKey('Test.presentText', $str);
|
||||||
$this->assertArrayHasKey('Test.absentText', $str);
|
$this->assertArrayHasKey('Test.absentText', $str);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use org\bovigo\vfs\vfsStream;
|
use org\bovigo\vfs\vfsStream;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\Lang */
|
/** @covers \JKingWeb\Arsse\Lang */
|
||||||
|
@ -11,54 +12,54 @@ class TestLangErrors extends Test\AbstractTest {
|
||||||
public $path;
|
public $path;
|
||||||
public $l;
|
public $l;
|
||||||
|
|
||||||
function setUpSeries() {
|
public function setUpSeries() {
|
||||||
$this->l->set("", true);
|
$this->l->set("", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLoadEmptyFile() {
|
public function testLoadEmptyFile() {
|
||||||
$this->assertException("fileCorrupt", "Lang");
|
$this->assertException("fileCorrupt", "Lang");
|
||||||
$this->l->set("fr_ca", true);
|
$this->l->set("fr_ca", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLoadFileWhichDoesNotReturnAnArray() {
|
public function testLoadFileWhichDoesNotReturnAnArray() {
|
||||||
$this->assertException("fileCorrupt", "Lang");
|
$this->assertException("fileCorrupt", "Lang");
|
||||||
$this->l->set("it", true);
|
$this->l->set("it", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLoadFileWhichIsNotPhp() {
|
public function testLoadFileWhichIsNotPhp() {
|
||||||
$this->assertException("fileCorrupt", "Lang");
|
$this->assertException("fileCorrupt", "Lang");
|
||||||
$this->l->set("ko", true);
|
$this->l->set("ko", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLoadFileWhichIsCorrupt() {
|
public function testLoadFileWhichIsCorrupt() {
|
||||||
$this->assertException("fileCorrupt", "Lang");
|
$this->assertException("fileCorrupt", "Lang");
|
||||||
$this->l->set("zh", true);
|
$this->l->set("zh", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLoadFileWithooutReadPermission() {
|
public function testLoadFileWithooutReadPermission() {
|
||||||
$this->assertException("fileUnreadable", "Lang");
|
$this->assertException("fileUnreadable", "Lang");
|
||||||
$this->l->set("ru", true);
|
$this->l->set("ru", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLoadSubtagOfMissingLanguage() {
|
public function testLoadSubtagOfMissingLanguage() {
|
||||||
$this->assertException("fileMissing", "Lang");
|
$this->assertException("fileMissing", "Lang");
|
||||||
$this->l->set("pt_br", true);
|
$this->l->set("pt_br", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFetchInvalidMessage() {
|
public function testFetchInvalidMessage() {
|
||||||
$this->assertException("stringInvalid", "Lang");
|
$this->assertException("stringInvalid", "Lang");
|
||||||
$this->l->set("vi", true);
|
$this->l->set("vi", true);
|
||||||
$txt = $this->l->msg('Test.presentText');
|
$txt = $this->l->msg('Test.presentText');
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFetchMissingMessage() {
|
public function testFetchMissingMessage() {
|
||||||
$this->assertException("stringMissing", "Lang");
|
$this->assertException("stringMissing", "Lang");
|
||||||
$txt = $this->l->msg('Test.absentText');
|
$txt = $this->l->msg('Test.absentText');
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLoadMissingDefaultLanguage() {
|
public function testLoadMissingDefaultLanguage() {
|
||||||
unlink($this->path.Lang::DEFAULT.".php");
|
unlink($this->path.Lang::DEFAULT.".php");
|
||||||
$this->assertException("defaultFileMissing", "Lang");
|
$this->assertException("defaultFileMissing", "Lang");
|
||||||
$this->l->set("fr", true);
|
$this->l->set("fr", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use org\bovigo\vfs\vfsStream;
|
use org\bovigo\vfs\vfsStream;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\Lang */
|
/** @covers \JKingWeb\Arsse\Lang */
|
||||||
|
@ -11,11 +12,11 @@ class TestLangComplex extends Test\AbstractTest {
|
||||||
public $path;
|
public $path;
|
||||||
public $l;
|
public $l;
|
||||||
|
|
||||||
function setUpSeries() {
|
public function setUpSeries() {
|
||||||
$this->l->set(Lang::DEFAULT, true);
|
$this->l->set(Lang::DEFAULT, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLazyLoad() {
|
public function testLazyLoad() {
|
||||||
$this->l->set("ja");
|
$this->l->set("ja");
|
||||||
$this->assertArrayNotHasKey('Test.absentText', $this->l->dump());
|
$this->assertArrayNotHasKey('Test.absentText', $this->l->dump());
|
||||||
}
|
}
|
||||||
|
@ -23,14 +24,14 @@ class TestLangComplex extends Test\AbstractTest {
|
||||||
/**
|
/**
|
||||||
* @depends testLazyLoad
|
* @depends testLazyLoad
|
||||||
*/
|
*/
|
||||||
function testGetWantedAndLoadedLocale() {
|
public function testGetWantedAndLoadedLocale() {
|
||||||
$this->l->set("en", true);
|
$this->l->set("en", true);
|
||||||
$this->l->set("ja");
|
$this->l->set("ja");
|
||||||
$this->assertEquals("ja", $this->l->get());
|
$this->assertEquals("ja", $this->l->get());
|
||||||
$this->assertEquals("en", $this->l->get(true));
|
$this->assertEquals("en", $this->l->get(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testLoadCascadeOfFiles() {
|
public function testLoadCascadeOfFiles() {
|
||||||
$this->l->set("ja", true);
|
$this->l->set("ja", true);
|
||||||
$this->assertEquals("de", $this->l->set("de", true));
|
$this->assertEquals("de", $this->l->set("de", true));
|
||||||
$str = $this->l->dump();
|
$str = $this->l->dump();
|
||||||
|
@ -41,11 +42,11 @@ class TestLangComplex extends Test\AbstractTest {
|
||||||
/**
|
/**
|
||||||
* @depends testLoadCascadeOfFiles
|
* @depends testLoadCascadeOfFiles
|
||||||
*/
|
*/
|
||||||
function testLoadSubtag() {
|
public function testLoadSubtag() {
|
||||||
$this->assertEquals("en_ca", $this->l->set("en_ca", true));
|
$this->assertEquals("en_ca", $this->l->set("en_ca", true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFetchAMessage() {
|
public function testFetchAMessage() {
|
||||||
$this->l->set("de", true);
|
$this->l->set("de", true);
|
||||||
$this->assertEquals('und der Stein der Weisen', $this->l->msg('Test.presentText'));
|
$this->assertEquals('und der Stein der Weisen', $this->l->msg('Test.presentText'));
|
||||||
}
|
}
|
||||||
|
@ -53,7 +54,7 @@ class TestLangComplex extends Test\AbstractTest {
|
||||||
/**
|
/**
|
||||||
* @depends testFetchAMessage
|
* @depends testFetchAMessage
|
||||||
*/
|
*/
|
||||||
function testFetchAMessageWithMissingParameters() {
|
public function testFetchAMessageWithMissingParameters() {
|
||||||
$this->l->set("en_ca", true);
|
$this->l->set("en_ca", true);
|
||||||
$this->assertEquals('{0} and {1}', $this->l->msg('Test.presentText'));
|
$this->assertEquals('{0} and {1}', $this->l->msg('Test.presentText'));
|
||||||
}
|
}
|
||||||
|
@ -61,7 +62,7 @@ class TestLangComplex extends Test\AbstractTest {
|
||||||
/**
|
/**
|
||||||
* @depends testFetchAMessage
|
* @depends testFetchAMessage
|
||||||
*/
|
*/
|
||||||
function testFetchAMessageWithSingleNumericParameter() {
|
public function testFetchAMessageWithSingleNumericParameter() {
|
||||||
$this->l->set("en_ca", true);
|
$this->l->set("en_ca", true);
|
||||||
$this->assertEquals('Default language file "en" missing', $this->l->msg('Exception.JKingWeb/Arsse/Lang/Exception.defaultFileMissing', Lang::DEFAULT));
|
$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
|
* @depends testFetchAMessage
|
||||||
*/
|
*/
|
||||||
function testFetchAMessageWithMultipleNumericParameters() {
|
public function testFetchAMessageWithMultipleNumericParameters() {
|
||||||
$this->l->set("en_ca", true);
|
$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']));
|
$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
|
* @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']));
|
$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
|
* @depends testFetchAMessage
|
||||||
*/
|
*/
|
||||||
function testReloadDefaultStrings() {
|
public function testReloadDefaultStrings() {
|
||||||
$this->l->set("de", true);
|
$this->l->set("de", true);
|
||||||
$this->l->set("en", true);
|
$this->l->set("en", true);
|
||||||
$this->assertEquals('and the Philosopher\'s Stone', $this->l->msg('Test.presentText'));
|
$this->assertEquals('and the Philosopher\'s Stone', $this->l->msg('Test.presentText'));
|
||||||
|
@ -93,11 +94,11 @@ class TestLangComplex extends Test\AbstractTest {
|
||||||
/**
|
/**
|
||||||
* @depends testFetchAMessage
|
* @depends testFetchAMessage
|
||||||
*/
|
*/
|
||||||
function testReloadGeneralTagAfterSubtag() {
|
public function testReloadGeneralTagAfterSubtag() {
|
||||||
$this->l->set("en", true);
|
$this->l->set("en", true);
|
||||||
$this->l->set("en_us", true);
|
$this->l->set("en_us", true);
|
||||||
$this->assertEquals('and the Sorcerer\'s Stone', $this->l->msg('Test.presentText'));
|
$this->assertEquals('and the Sorcerer\'s Stone', $this->l->msg('Test.presentText'));
|
||||||
$this->l->set("en", true);
|
$this->l->set("en", true);
|
||||||
$this->assertEquals('and the Philosopher\'s Stone', $this->l->msg('Test.presentText'));
|
$this->assertEquals('and the Philosopher\'s Stone', $this->l->msg('Test.presentText'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
use JKingWeb\Arsse\Misc\Context;
|
|
||||||
|
|
||||||
|
use JKingWeb\Arsse\Misc\Context;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\Misc\Context */
|
/** @covers \JKingWeb\Arsse\Misc\Context */
|
||||||
class TestContext extends Test\AbstractTest {
|
class TestContext extends Test\AbstractTest {
|
||||||
function testVerifyInitialState() {
|
public function testVerifyInitialState() {
|
||||||
$c = new Context;
|
$c = new Context;
|
||||||
foreach((new \ReflectionObject($c))->getMethods(\ReflectionMethod::IS_PUBLIC) as $m) {
|
foreach ((new \ReflectionObject($c))->getMethods(\ReflectionMethod::IS_PUBLIC) as $m) {
|
||||||
if($m->isConstructor() || $m->isStatic()) {
|
if ($m->isConstructor() || $m->isStatic()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$method = $m->name;
|
$method = $m->name;
|
||||||
|
@ -18,7 +18,7 @@ class TestContext extends Test\AbstractTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testSetContextOptions() {
|
public function testSetContextOptions() {
|
||||||
$v = [
|
$v = [
|
||||||
'reverse' => true,
|
'reverse' => true,
|
||||||
'limit' => 10,
|
'limit' => 10,
|
||||||
|
@ -38,15 +38,15 @@ class TestContext extends Test\AbstractTest {
|
||||||
];
|
];
|
||||||
$times = ['modifiedSince','notModifiedSince'];
|
$times = ['modifiedSince','notModifiedSince'];
|
||||||
$c = new Context;
|
$c = new Context;
|
||||||
foreach((new \ReflectionObject($c))->getMethods(\ReflectionMethod::IS_PUBLIC) as $m) {
|
foreach ((new \ReflectionObject($c))->getMethods(\ReflectionMethod::IS_PUBLIC) as $m) {
|
||||||
if($m->isConstructor() || $m->isStatic()) {
|
if ($m->isConstructor() || $m->isStatic()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$method = $m->name;
|
$method = $m->name;
|
||||||
$this->assertArrayHasKey($method, $v, "Context method $method not included in test");
|
$this->assertArrayHasKey($method, $v, "Context method $method not included in test");
|
||||||
$this->assertInstanceOf(Context::class, $c->$method($v[$method]));
|
$this->assertInstanceOf(Context::class, $c->$method($v[$method]));
|
||||||
$this->assertTrue($c->$method());
|
$this->assertTrue($c->$method());
|
||||||
if(in_array($method, $times)) {
|
if (in_array($method, $times)) {
|
||||||
$this->assertTime($c->$method, $v[$method]);
|
$this->assertTime($c->$method, $v[$method]);
|
||||||
} else {
|
} else {
|
||||||
$this->assertSame($c->$method, $v[$method]);
|
$this->assertSame($c->$method, $v[$method]);
|
||||||
|
@ -54,13 +54,13 @@ class TestContext extends Test\AbstractTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCleanArrayValues() {
|
public function testCleanArrayValues() {
|
||||||
$methods = ["articles", "editions"];
|
$methods = ["articles", "editions"];
|
||||||
$in = [1, "2", 3.5, 3.0, "ook", 0, -20, true, false, null, new \DateTime(), -1.0];
|
$in = [1, "2", 3.5, 3.0, "ook", 0, -20, true, false, null, new \DateTime(), -1.0];
|
||||||
$out = [1,2, 3];
|
$out = [1,2, 3];
|
||||||
$c = new Context;
|
$c = new Context;
|
||||||
foreach($methods as $method) {
|
foreach ($methods as $method) {
|
||||||
$this->assertSame($out, $c->$method($in)->$method, "Context method $method did not return the expected results");
|
$this->assertSame($out, $c->$method($in)->$method, "Context method $method did not return the expected results");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use JKingWeb\Arsse\REST\Request;
|
use JKingWeb\Arsse\REST\Request;
|
||||||
use JKingWeb\Arsse\REST\Response;
|
use JKingWeb\Arsse\REST\Response;
|
||||||
use JKingWeb\Arsse\Test\Result;
|
use JKingWeb\Arsse\Test\Result;
|
||||||
|
@ -259,7 +260,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
function setUp() {
|
public function setUp() {
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
Arsse::$conf = new Conf();
|
Arsse::$conf = new Conf();
|
||||||
// create a mock user manager
|
// create a mock user manager
|
||||||
|
@ -268,16 +269,16 @@ class TestNCNV1_2 extends Test\AbstractTest {
|
||||||
Phake::when(Arsse::$user)->rightsGet->thenReturn(100);
|
Phake::when(Arsse::$user)->rightsGet->thenReturn(100);
|
||||||
Arsse::$user->id = "john.doe@example.com";
|
Arsse::$user->id = "john.doe@example.com";
|
||||||
// create a mock database interface
|
// 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));
|
Phake::when(Arsse::$db)->begin->thenReturn(Phake::mock(Transaction::class));
|
||||||
$this->h = new REST\NextCloudNews\V1_2();
|
$this->h = new REST\NextCloudNews\V1_2();
|
||||||
}
|
}
|
||||||
|
|
||||||
function tearDown() {
|
public function tearDown() {
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function testRespondToInvalidPaths() {
|
public function testRespondToInvalidPaths() {
|
||||||
$errs = [
|
$errs = [
|
||||||
501 => [
|
501 => [
|
||||||
['GET', "/"],
|
['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);
|
$exp = new Response(501);
|
||||||
list($method, $path) = $req;
|
list($method, $path) = $req;
|
||||||
$this->assertEquals($exp, $this->h->dispatch(new Request($method, $path)), "$method call to $path did not return 501.");
|
$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]);
|
$exp = new Response(405, "", "", ['Allow: '.$allow]);
|
||||||
foreach($cases as $req) {
|
foreach ($cases as $req) {
|
||||||
list($method, $path) = $req;
|
list($method, $path) = $req;
|
||||||
$this->assertEquals($exp, $this->h->dispatch(new Request($method, $path)), "$method call to $path did not return 405.");
|
$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']);
|
$exp = new Response(415, "", "", ['Accept: application/json']);
|
||||||
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/1", '<data/>', 'application/xml')));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/1", '<data/>', 'application/xml')));
|
||||||
$exp = new Response(400);
|
$exp = new Response(400);
|
||||||
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/1", '<data/>', 'application/json')));
|
$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);
|
Phake::when(Arsse::$user)->authHTTP->thenReturn(false);
|
||||||
$exp = new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.REST\NextCloudNews\V1_2::REALM.'"']);
|
$exp = new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.REST\NextCloudNews\V1_2::REALM.'"']);
|
||||||
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/")));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/")));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testListFolders() {
|
public function testListFolders() {
|
||||||
$list = [
|
$list = [
|
||||||
['id' => 1, 'name' => "Software", 'parent' => null],
|
['id' => 1, 'name' => "Software", 'parent' => null],
|
||||||
['id' => 12, 'name' => "Hardware", '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")));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/folders")));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testAddAFolder() {
|
public function testAddAFolder() {
|
||||||
$in = [
|
$in = [
|
||||||
["name" => "Software"],
|
["name" => "Software"],
|
||||||
["name" => "Hardware"],
|
["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')));
|
$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"));
|
Phake::when(Arsse::$db)->folderRemove(Arsse::$user->id, 1)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing"));
|
||||||
$exp = new Response(204);
|
$exp = new Response(204);
|
||||||
$this->assertEquals($exp, $this->h->dispatch(new Request("DELETE", "/folders/1")));
|
$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);
|
Phake::verify(Arsse::$db, Phake::times(2))->folderRemove(Arsse::$user->id, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testRenameAFolder() {
|
public function testRenameAFolder() {
|
||||||
$in = [
|
$in = [
|
||||||
["name" => "Software"],
|
["name" => "Software"],
|
||||||
["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')));
|
$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, [
|
$exp = new Response(200, [
|
||||||
'arsse_version' => \JKingWeb\Arsse\VERSION,
|
'arsse_version' => \JKingWeb\Arsse\VERSION,
|
||||||
'version' => REST\NextCloudNews\V1_2::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")));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/version")));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testListSubscriptions() {
|
public function testListSubscriptions() {
|
||||||
$exp1 = [
|
$exp1 = [
|
||||||
'feeds' => [],
|
'feeds' => [],
|
||||||
'starredCount' => 0,
|
'starredCount' => 0,
|
||||||
|
@ -452,7 +453,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
|
||||||
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds")));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds")));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testAddASubscription() {
|
public function testAddASubscription() {
|
||||||
$in = [
|
$in = [
|
||||||
['url' => "http://example.com/news.atom", 'folderId' => 3],
|
['url' => "http://example.com/news.atom", 'folderId' => 3],
|
||||||
['url' => "http://example.org/news.atom", 'folderId' => 8],
|
['url' => "http://example.org/news.atom", 'folderId' => 8],
|
||||||
|
@ -467,13 +468,13 @@ class TestNCNV1_2 extends Test\AbstractTest {
|
||||||
];
|
];
|
||||||
// set up the necessary mocks
|
// 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.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, 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(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, 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
|
// 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()));
|
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
|
// 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')));
|
$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"));
|
Phake::when(Arsse::$db)->subscriptionRemove(Arsse::$user->id, 1)->thenReturn(true)->thenThrow(new ExceptionInput("subjectMissing"));
|
||||||
$exp = new Response(204);
|
$exp = new Response(204);
|
||||||
$this->assertEquals($exp, $this->h->dispatch(new Request("DELETE", "/feeds/1")));
|
$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);
|
Phake::verify(Arsse::$db, Phake::times(2))->subscriptionRemove(Arsse::$user->id, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testMoveASubscription() {
|
public function testMoveASubscription() {
|
||||||
$in = [
|
$in = [
|
||||||
['folderId' => 0],
|
['folderId' => 0],
|
||||||
['folderId' => 42],
|
['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')));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/move", json_encode($in[4]), 'application/json')));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testRenameASubscription() {
|
public function testRenameASubscription() {
|
||||||
$in = [
|
$in = [
|
||||||
['feedTitle' => null],
|
['feedTitle' => null],
|
||||||
['feedTitle' => "Ook"],
|
['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')));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/1/rename", json_encode($in[6]), 'application/json')));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testListStaleFeeds() {
|
public function testListStaleFeeds() {
|
||||||
$out = [
|
$out = [
|
||||||
[
|
[
|
||||||
'id' => 42,
|
'id' => 42,
|
||||||
|
@ -569,7 +570,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
|
||||||
'userId' => "",
|
'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]);
|
$exp = new Response(200, ['feeds' => $out]);
|
||||||
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/all")));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/all")));
|
||||||
// retrieving the list when not an admin fails
|
// 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")));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/all")));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testUpdateAFeed() {
|
public function testUpdateAFeed() {
|
||||||
$in = [
|
$in = [
|
||||||
['feedId' => 42], // valid
|
['feedId' => 42], // valid
|
||||||
['feedId' => 2112], // feed does not exist
|
['feedId' => 2112], // feed does not exist
|
||||||
['feedId' => "ook"], // invalid ID
|
['feedId' => "ook"], // invalid ID
|
||||||
['feed' => 42], // invalid input
|
['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"));
|
Phake::when(Arsse::$db)->feedUpdate(2112)->thenThrow(new ExceptionInput("subjectMissing"));
|
||||||
$exp = new Response(204);
|
$exp = new Response(204);
|
||||||
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json')));
|
$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')));
|
$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']);
|
$res = new Result($this->articles['db']);
|
||||||
$t = new \DateTime;
|
$t = new \DateTime;
|
||||||
$in = [
|
$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));
|
Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testMarkAFolderRead() {
|
public function testMarkAFolderRead() {
|
||||||
$read = ['read' => true];
|
$read = ['read' => true];
|
||||||
$in = json_encode(['newestItemId' => 2112]);
|
$in = json_encode(['newestItemId' => 2112]);
|
||||||
Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->folder(1)->latestEdition(2112))->thenReturn(true);
|
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')));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/42/read", $in, 'application/json')));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testMarkASubscriptionRead() {
|
public function testMarkASubscriptionRead() {
|
||||||
$read = ['read' => true];
|
$read = ['read' => true];
|
||||||
$in = json_encode(['newestItemId' => 2112]);
|
$in = json_encode(['newestItemId' => 2112]);
|
||||||
Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->subscription(1)->latestEdition(2112))->thenReturn(true);
|
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')));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/42/read", $in, 'application/json')));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testMarkAllItemsRead() {
|
public function testMarkAllItemsRead() {
|
||||||
$read = ['read' => true];
|
$read = ['read' => true];
|
||||||
$in = json_encode(['newestItemId' => 2112]);
|
$in = json_encode(['newestItemId' => 2112]);
|
||||||
Phake::when(Arsse::$db)->articleMark(Arsse::$user->id, $read, (new Context)->latestEdition(2112))->thenReturn(true);
|
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")));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/items/read?newestItemId=ook")));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testChangeMarksOfASingleArticle() {
|
public function testChangeMarksOfASingleArticle() {
|
||||||
$read = ['read' => true];
|
$read = ['read' => true];
|
||||||
$unread = ['read' => false];
|
$unread = ['read' => false];
|
||||||
$star = ['starred' => true];
|
$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());
|
Phake::verify(Arsse::$db, Phake::times(8))->articleMark(Arsse::$user->id, $this->anything(), $this->anything());
|
||||||
}
|
}
|
||||||
|
|
||||||
function testChangeMarksOfMultipleArticles() {
|
public function testChangeMarksOfMultipleArticles() {
|
||||||
$read = ['read' => true];
|
$read = ['read' => true];
|
||||||
$unread = ['read' => false];
|
$unread = ['read' => false];
|
||||||
$star = ['starred' => true];
|
$star = ['starred' => true];
|
||||||
$unstar = ['starred' => false];
|
$unstar = ['starred' => false];
|
||||||
$in = [
|
$in = [
|
||||||
["ook","eek","ack"],
|
["ook","eek","ack"],
|
||||||
range(100,199),
|
range(100, 199),
|
||||||
range(100,149),
|
range(100, 149),
|
||||||
range(150,199),
|
range(150, 199),
|
||||||
];
|
];
|
||||||
$inStar = $in;
|
$inStar = $in;
|
||||||
for($a = 0; $a < sizeof($inStar); $a++) {
|
for ($a = 0; $a < sizeof($inStar); $a++) {
|
||||||
for($b = 0; $b < sizeof($inStar[$a]); $b++) {
|
for ($b = 0; $b < sizeof($inStar[$a]); $b++) {
|
||||||
$inStar[$a][$b] = ['feedId' => 2112, 'guidHash' => $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]));
|
Phake::verify(Arsse::$db)->articleMark(Arsse::$user->id, $unstar, (new Context)->articles($in[3]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testQueryTheServerStatus() {
|
public function testQueryTheServerStatus() {
|
||||||
$interval = Service::interval();
|
$interval = Service::interval();
|
||||||
$valid = (new \DateTimeImmutable("now", new \DateTimezone("UTC")))->sub($interval);
|
$valid = (new \DateTimeImmutable("now", new \DateTimezone("UTC")))->sub($interval);
|
||||||
$invalid = $valid->sub($interval)->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")));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/status")));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCleanUpBeforeUpdate() {
|
public function testCleanUpBeforeUpdate() {
|
||||||
Phake::when(Arsse::$db)->feedCleanup()->thenReturn(true);
|
Phake::when(Arsse::$db)->feedCleanup()->thenReturn(true);
|
||||||
$exp = new Response(204);
|
$exp = new Response(204);
|
||||||
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/cleanup/before-update")));
|
$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")));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/cleanup/before-update")));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCleanUpAfterUpdate() {
|
public function testCleanUpAfterUpdate() {
|
||||||
Phake::when(Arsse::$db)->articleCleanup()->thenReturn(true);
|
Phake::when(Arsse::$db)->articleCleanup()->thenReturn(true);
|
||||||
$exp = new Response(204);
|
$exp = new Response(204);
|
||||||
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/cleanup/after-update")));
|
$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);
|
$exp = new Response(403);
|
||||||
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/cleanup/after-update")));
|
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/cleanup/after-update")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use JKingWeb\Arsse\REST\Request;
|
use JKingWeb\Arsse\REST\Request;
|
||||||
use JKingWeb\Arsse\REST\Response;
|
use JKingWeb\Arsse\REST\Response;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\REST\NextCloudNews\Versions */
|
/** @covers \JKingWeb\Arsse\REST\NextCloudNews\Versions */
|
||||||
class TestNCNVersionDiscovery extends Test\AbstractTest {
|
class TestNCNVersionDiscovery extends Test\AbstractTest {
|
||||||
function setUp() {
|
public function setUp() {
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFetchVersionList() {
|
public function testFetchVersionList() {
|
||||||
$exp = new Response(200, ['apiLevels' => ['v1-2']]);
|
$exp = new Response(200, ['apiLevels' => ['v1-2']]);
|
||||||
$h = new REST\NextCloudNews\Versions();
|
$h = new REST\NextCloudNews\Versions();
|
||||||
$req = new Request("GET", "/");
|
$req = new Request("GET", "/");
|
||||||
|
@ -24,7 +25,7 @@ class TestNCNVersionDiscovery extends Test\AbstractTest {
|
||||||
$this->assertEquals($exp, $res);
|
$this->assertEquals($exp, $res);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testUseIncorrectMethod() {
|
public function testUseIncorrectMethod() {
|
||||||
$exp = new Response(405);
|
$exp = new Response(405);
|
||||||
$h = new REST\NextCloudNews\Versions();
|
$h = new REST\NextCloudNews\Versions();
|
||||||
$req = new Request("POST", "/");
|
$req = new Request("POST", "/");
|
||||||
|
@ -32,11 +33,11 @@ class TestNCNVersionDiscovery extends Test\AbstractTest {
|
||||||
$this->assertEquals($exp, $res);
|
$this->assertEquals($exp, $res);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testUseIncorrectPath() {
|
public function testUseIncorrectPath() {
|
||||||
$exp = new Response(501);
|
$exp = new Response(501);
|
||||||
$h = new REST\NextCloudNews\Versions();
|
$h = new REST\NextCloudNews\Versions();
|
||||||
$req = new Request("GET", "/ook");
|
$req = new Request("GET", "/ook");
|
||||||
$res = $h->dispatch($req);
|
$res = $h->dispatch($req);
|
||||||
$this->assertEquals($exp, $res);
|
$this->assertEquals($exp, $res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
use Phake;
|
use Phake;
|
||||||
|
|
||||||
|
@ -8,14 +9,14 @@ use Phake;
|
||||||
class TestService extends Test\AbstractTest {
|
class TestService extends Test\AbstractTest {
|
||||||
protected $srv;
|
protected $srv;
|
||||||
|
|
||||||
function setUp() {
|
public function setUp() {
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
Arsse::$conf = new Conf();
|
Arsse::$conf = new Conf();
|
||||||
Arsse::$db = Phake::mock(Database::class);
|
Arsse::$db = Phake::mock(Database::class);
|
||||||
$this->srv = new Service();
|
$this->srv = new Service();
|
||||||
}
|
}
|
||||||
|
|
||||||
function testComputeInterval() {
|
public function testComputeInterval() {
|
||||||
$in = [
|
$in = [
|
||||||
Arsse::$conf->serviceFrequency,
|
Arsse::$conf->serviceFrequency,
|
||||||
"PT2M",
|
"PT2M",
|
||||||
|
@ -24,21 +25,25 @@ class TestService extends Test\AbstractTest {
|
||||||
"5M",
|
"5M",
|
||||||
"interval",
|
"interval",
|
||||||
];
|
];
|
||||||
foreach($in as $index => $spec) {
|
foreach ($in as $index => $spec) {
|
||||||
try{$exp = new \DateInterval($spec);} catch(\Exception $e) {$exp = new \DateInterval("PT2M");}
|
try {
|
||||||
|
$exp = new \DateInterval($spec);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$exp = new \DateInterval("PT2M");
|
||||||
|
}
|
||||||
Arsse::$conf->serviceFrequency = $spec;
|
Arsse::$conf->serviceFrequency = $spec;
|
||||||
$this->assertEquals($exp, Service::interval(), "Interval #$index '$spec' was not correctly calculated");
|
$this->assertEquals($exp, Service::interval(), "Interval #$index '$spec' was not correctly calculated");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCheckIn() {
|
public function testCheckIn() {
|
||||||
$now = time();
|
$now = time();
|
||||||
$this->srv->checkIn();
|
$this->srv->checkIn();
|
||||||
Phake::verify(Arsse::$db)->metaSet("service_last_checkin", Phake::capture($then), "datetime");
|
Phake::verify(Arsse::$db)->metaSet("service_last_checkin", Phake::capture($then), "datetime");
|
||||||
$this->assertTime($now, $then);
|
$this->assertTime($now, $then);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testReportHavingCheckedIn() {
|
public function testReportHavingCheckedIn() {
|
||||||
// the mock's metaGet() returns null by default
|
// the mock's metaGet() returns null by default
|
||||||
$this->assertFalse(Service::hasCheckedIn());
|
$this->assertFalse(Service::hasCheckedIn());
|
||||||
$interval = Service::interval();
|
$interval = Service::interval();
|
||||||
|
@ -48,4 +53,4 @@ class TestService extends Test\AbstractTest {
|
||||||
$this->assertTrue(Service::hasCheckedIn());
|
$this->assertTrue(Service::hasCheckedIn());
|
||||||
$this->assertFalse(Service::hasCheckedIn());
|
$this->assertFalse(Service::hasCheckedIn());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use Phake;
|
use Phake;
|
||||||
|
|
||||||
/** @covers \JKingWeb\Arsse\User */
|
/** @covers \JKingWeb\Arsse\User */
|
||||||
|
@ -44,29 +45,29 @@ class TestAuthorization extends Test\AbstractTest {
|
||||||
|
|
||||||
protected $data;
|
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();
|
$this->clearData();
|
||||||
$conf = new Conf();
|
$conf = new Conf();
|
||||||
$conf->userDriver = $drv;
|
$conf->userDriver = $drv;
|
||||||
$conf->userPreAuth = false;
|
$conf->userPreAuth = false;
|
||||||
Arsse::$conf = $conf;
|
Arsse::$conf = $conf;
|
||||||
if($db !== null) {
|
if ($db !== null) {
|
||||||
Arsse::$db = new $db();
|
Arsse::$db = new $db();
|
||||||
}
|
}
|
||||||
Arsse::$user = Phake::partialMock(User::class);
|
Arsse::$user = Phake::partialMock(User::class);
|
||||||
Phake::when(Arsse::$user)->authorize->thenReturn(true);
|
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->add($user, "");
|
||||||
Arsse::$user->rightsSet($user, $level);
|
Arsse::$user->rightsSet($user, $level);
|
||||||
}
|
}
|
||||||
Phake::reset(Arsse::$user);
|
Phake::reset(Arsse::$user);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tearDown() {
|
public function tearDown() {
|
||||||
$this->clearData();
|
$this->clearData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function testToggleLogic() {
|
public function testToggleLogic() {
|
||||||
$this->assertTrue(Arsse::$user->authorizationEnabled());
|
$this->assertTrue(Arsse::$user->authorizationEnabled());
|
||||||
$this->assertTrue(Arsse::$user->authorizationEnabled(true));
|
$this->assertTrue(Arsse::$user->authorizationEnabled(true));
|
||||||
$this->assertFalse(Arsse::$user->authorizationEnabled(false));
|
$this->assertFalse(Arsse::$user->authorizationEnabled(false));
|
||||||
|
@ -75,8 +76,8 @@ class TestAuthorization extends Test\AbstractTest {
|
||||||
$this->assertTrue(Arsse::$user->authorizationEnabled(true));
|
$this->assertTrue(Arsse::$user->authorizationEnabled(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testSelfActionLogic() {
|
public function testSelfActionLogic() {
|
||||||
foreach(array_keys(self::USERS) as $user) {
|
foreach (array_keys(self::USERS) as $user) {
|
||||||
Arsse::$user->auth($user, "");
|
Arsse::$user->auth($user, "");
|
||||||
// users should be able to do basic actions for themselves
|
// users should be able to do basic actions for themselves
|
||||||
$this->assertTrue(Arsse::$user->authorize($user, "userExists"), "User $user could not act 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() {
|
public function testRegularUserLogic() {
|
||||||
foreach(self::USERS as $actor => $rights) {
|
foreach (self::USERS as $actor => $rights) {
|
||||||
if($rights != User\Driver::RIGHTS_NONE) {
|
if ($rights != User\Driver::RIGHTS_NONE) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Arsse::$user->auth($actor, "");
|
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
|
// 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, "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.");
|
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
|
||||||
} else {
|
} 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.");
|
$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
|
// 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.");
|
$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
|
// 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.");
|
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDomainManagerLogic() {
|
public function testDomainManagerLogic() {
|
||||||
foreach(self::USERS as $actor => $actorRights) {
|
foreach (self::USERS as $actor => $actorRights) {
|
||||||
if($actorRights != User\Driver::RIGHTS_DOMAIN_MANAGER) {
|
if ($actorRights != User\Driver::RIGHTS_DOMAIN_MANAGER) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$actorDomain = substr($actor,strrpos($actor,"@")+1);
|
$actorDomain = substr($actor, strrpos($actor, "@")+1);
|
||||||
Arsse::$user->auth($actor, "");
|
Arsse::$user->auth($actor, "");
|
||||||
foreach(self::USERS as $affected => $affectedRights) {
|
foreach (self::USERS as $affected => $affectedRights) {
|
||||||
$affectedDomain = substr($affected,strrpos($affected,"@")+1);
|
$affectedDomain = substr($affected, strrpos($affected, "@")+1);
|
||||||
// domain managers should be able to check any user on the same domain
|
// 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.");
|
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
|
||||||
} else {
|
} else {
|
||||||
$this->assertFalse(Arsse::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
|
$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
|
// 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.");
|
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
|
||||||
} else {
|
} else {
|
||||||
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
|
$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
|
// and they should only be able to set their own rights to regular user
|
||||||
foreach(self::LEVELS as $level) {
|
foreach (self::LEVELS as $level) {
|
||||||
if($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_DOMAIN_MANAGER])) {
|
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.");
|
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
|
||||||
} else {
|
} else {
|
||||||
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
|
$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
|
// they should also be able to list all users on their own domain
|
||||||
foreach(self::DOMAINS as $domain) {
|
foreach (self::DOMAINS as $domain) {
|
||||||
if($domain=="@".$actorDomain) {
|
if ($domain=="@".$actorDomain) {
|
||||||
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
|
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
|
||||||
} else {
|
} else {
|
||||||
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
|
$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() {
|
public function testDomainAdministratorLogic() {
|
||||||
foreach(self::USERS as $actor => $actorRights) {
|
foreach (self::USERS as $actor => $actorRights) {
|
||||||
if($actorRights != User\Driver::RIGHTS_DOMAIN_ADMIN) {
|
if ($actorRights != User\Driver::RIGHTS_DOMAIN_ADMIN) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$actorDomain = substr($actor,strrpos($actor,"@")+1);
|
$actorDomain = substr($actor, strrpos($actor, "@")+1);
|
||||||
Arsse::$user->auth($actor, "");
|
Arsse::$user->auth($actor, "");
|
||||||
$allowed = [User\Driver::RIGHTS_NONE,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN];
|
$allowed = [User\Driver::RIGHTS_NONE,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN];
|
||||||
foreach(self::USERS as $affected => $affectedRights) {
|
foreach (self::USERS as $affected => $affectedRights) {
|
||||||
$affectedDomain = substr($affected,strrpos($affected,"@")+1);
|
$affectedDomain = substr($affected, strrpos($affected, "@")+1);
|
||||||
// domain admins should be able to check any user on the same domain
|
// 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.");
|
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
|
||||||
} else {
|
} else {
|
||||||
$this->assertFalse(Arsse::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
|
$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
|
// 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.");
|
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
|
||||||
} else {
|
} else {
|
||||||
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
|
$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
|
// 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) {
|
foreach (self::LEVELS as $level) {
|
||||||
if($actorDomain==$affectedDomain && in_array($affectedRights, $allowed) && in_array($level, $allowed)) {
|
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.");
|
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
|
||||||
} else {
|
} else {
|
||||||
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
|
$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
|
// they should also be able to list all users on their own domain
|
||||||
foreach(self::DOMAINS as $domain) {
|
foreach (self::DOMAINS as $domain) {
|
||||||
if($domain=="@".$actorDomain) {
|
if ($domain=="@".$actorDomain) {
|
||||||
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
|
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
|
||||||
} else {
|
} else {
|
||||||
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
|
$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() {
|
public function testGlobalManagerLogic() {
|
||||||
foreach(self::USERS as $actor => $actorRights) {
|
foreach (self::USERS as $actor => $actorRights) {
|
||||||
if($actorRights != User\Driver::RIGHTS_GLOBAL_MANAGER) {
|
if ($actorRights != User\Driver::RIGHTS_GLOBAL_MANAGER) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$actorDomain = substr($actor,strrpos($actor,"@")+1);
|
$actorDomain = substr($actor, strrpos($actor, "@")+1);
|
||||||
Arsse::$user->auth($actor, "");
|
Arsse::$user->auth($actor, "");
|
||||||
foreach(self::USERS as $affected => $affectedRights) {
|
foreach (self::USERS as $affected => $affectedRights) {
|
||||||
$affectedDomain = substr($affected,strrpos($affected,"@")+1);
|
$affectedDomain = substr($affected, strrpos($affected, "@")+1);
|
||||||
// global managers should be able to check any user
|
// 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.");
|
$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
|
// 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.");
|
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
|
||||||
} else {
|
} else {
|
||||||
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
|
$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
|
// and they should only be able to set their own rights to regular user
|
||||||
foreach(self::LEVELS as $level) {
|
foreach (self::LEVELS as $level) {
|
||||||
if($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_GLOBAL_MANAGER])) {
|
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.");
|
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
|
||||||
} else {
|
} else {
|
||||||
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
|
$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
|
// 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.");
|
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testGlobalAdministratorLogic() {
|
public function testGlobalAdministratorLogic() {
|
||||||
foreach(self::USERS as $actor => $actorRights) {
|
foreach (self::USERS as $actor => $actorRights) {
|
||||||
if($actorRights != User\Driver::RIGHTS_GLOBAL_ADMIN) {
|
if ($actorRights != User\Driver::RIGHTS_GLOBAL_ADMIN) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Arsse::$user->auth($actor, "");
|
Arsse::$user->auth($actor, "");
|
||||||
// global admins can do anything
|
// 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, "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.");
|
$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.");
|
$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.");
|
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testInvalidLevelLogic() {
|
public function testInvalidLevelLogic() {
|
||||||
foreach(self::USERS as $actor => $rights) {
|
foreach (self::USERS as $actor => $rights) {
|
||||||
if(in_array($rights, self::LEVELS)) {
|
if (in_array($rights, self::LEVELS)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Arsse::$user->auth($actor, "");
|
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
|
// 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, "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.");
|
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
|
||||||
} else {
|
} 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.");
|
$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
|
// 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.");
|
$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
|
// 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.");
|
$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 = [
|
$tests = [
|
||||||
// methods of User class to test, with parameters besides affected user
|
// methods of User class to test, with parameters besides affected user
|
||||||
'exists' => [],
|
'exists' => [],
|
||||||
|
@ -295,7 +296,7 @@ class TestAuthorization extends Test\AbstractTest {
|
||||||
$this->assertCount(sizeof($tests), $this->checkExceptions("user@example.org", $tests));
|
$this->assertCount(sizeof($tests), $this->checkExceptions("user@example.org", $tests));
|
||||||
}
|
}
|
||||||
|
|
||||||
function testExternalExceptionLogic() {
|
public function testExternalExceptionLogic() {
|
||||||
// set up the test for an external driver
|
// set up the test for an external driver
|
||||||
$this->setUp(Test\User\DriverExternalMock::class, Test\User\Database::class);
|
$this->setUp(Test\User\DriverExternalMock::class, Test\User\Database::class);
|
||||||
// run the previous test with the external driver set up
|
// 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
|
// 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 {
|
protected function checkExceptions(string $user, $tests): array {
|
||||||
$err = [];
|
$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
|
// 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);
|
array_unshift($args, $user);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
call_user_func_array(array(Arsse::$user, $func), $args);
|
call_user_func_array(array(Arsse::$user, $func), $args);
|
||||||
} catch(User\ExceptionAuthz $e) {
|
} catch (User\ExceptionAuthz $e) {
|
||||||
$err[] = $func;
|
$err[] = $func;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $err;
|
return $err;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testMissingUserLogic() {
|
public function testMissingUserLogic() {
|
||||||
Arsse::$user->auth("gadm@example.com", "");
|
Arsse::$user->auth("gadm@example.com", "");
|
||||||
$this->assertTrue(Arsse::$user->authorize("user@example.com", "someFunction"));
|
$this->assertTrue(Arsse::$user->authorize("user@example.com", "someFunction"));
|
||||||
$this->assertException("doesNotExist", "User");
|
$this->assertException("doesNotExist", "User");
|
||||||
Arsse::$user->authorize("this_user_does_not_exist@example.org", "someFunction");
|
Arsse::$user->authorize("this_user_does_not_exist@example.org", "someFunction");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \JKingWeb\Arsse\User
|
* @covers \JKingWeb\Arsse\User
|
||||||
* @covers \JKingWeb\Arsse\User\Internal\Driver
|
* @covers \JKingWeb\Arsse\User\Internal\Driver
|
||||||
* @covers \JKingWeb\Arsse\User\Internal\InternalFunctions */
|
* @covers \JKingWeb\Arsse\User\Internal\InternalFunctions */
|
||||||
|
|
|
@ -10,4 +10,4 @@ class TestUserMockExternal extends Test\AbstractTest {
|
||||||
const USER2 = "jane.doe@example.com";
|
const USER2 = "jane.doe@example.com";
|
||||||
|
|
||||||
public $drv = Test\User\DriverExternalMock::class;
|
public $drv = Test\User\DriverExternalMock::class;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ class TestUserMockInternal extends Test\AbstractTest {
|
||||||
|
|
||||||
public $drv = Test\User\DriverInternalMock::class;
|
public $drv = Test\User\DriverInternalMock::class;
|
||||||
|
|
||||||
function setUpSeries() {
|
public function setUpSeries() {
|
||||||
Arsse::$db = null;
|
Arsse::$db = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,4 +11,4 @@
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
MESSAGE_BODY
|
MESSAGE_BODY
|
||||||
];
|
];
|
||||||
|
|
|
@ -27,4 +27,4 @@
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
MESSAGE_BODY
|
MESSAGE_BODY
|
||||||
];
|
];
|
||||||
|
|
|
@ -21,4 +21,4 @@
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
MESSAGE_BODY
|
MESSAGE_BODY
|
||||||
];
|
];
|
||||||
|
|
|
@ -11,4 +11,4 @@
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
MESSAGE_BODY
|
MESSAGE_BODY
|
||||||
];
|
];
|
||||||
|
|
|
@ -16,4 +16,4 @@
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
MESSAGE_BODY
|
MESSAGE_BODY
|
||||||
];
|
];
|
||||||
|
|
|
@ -17,4 +17,4 @@
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
MESSAGE_BODY
|
MESSAGE_BODY
|
||||||
];
|
];
|
||||||
|
|
|
@ -4,4 +4,4 @@
|
||||||
'fields' => [
|
'fields' => [
|
||||||
"ETag: ".$_SERVER['HTTP_IF_NONE_MATCH'],
|
"ETag: ".$_SERVER['HTTP_IF_NONE_MATCH'],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
@ -4,4 +4,4 @@
|
||||||
'fields' => [
|
'fields' => [
|
||||||
'Last-Modified: '.$_SERVER['HTTP_IF_MODIFIED_SINCE'],
|
'Last-Modified: '.$_SERVER['HTTP_IF_MODIFIED_SINCE'],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?php return [
|
<?php return [
|
||||||
'code' => 304,
|
'code' => 304,
|
||||||
'cache' => false,
|
'cache' => false,
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php return [
|
<?php return [
|
||||||
'code' => 304,
|
'code' => 304,
|
||||||
'lastMod' => random_int(0,2^31),
|
'lastMod' => random_int(0, 2^31),
|
||||||
'fields' => [
|
'fields' => [
|
||||||
"ETag: ".bin2hex(random_bytes(8)),
|
"ETag: ".bin2hex(random_bytes(8)),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
@ -38,4 +38,4 @@
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
MESSAGE_BODY
|
MESSAGE_BODY
|
||||||
];
|
];
|
||||||
|
|
|
@ -38,4 +38,4 @@
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
MESSAGE_BODY
|
MESSAGE_BODY
|
||||||
];
|
];
|
||||||
|
|
|
@ -38,4 +38,4 @@
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
MESSAGE_BODY
|
MESSAGE_BODY
|
||||||
];
|
];
|
||||||
|
|
|
@ -30,4 +30,4 @@
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
MESSAGE_BODY
|
MESSAGE_BODY
|
||||||
];
|
];
|
||||||
|
|
|
@ -34,4 +34,4 @@
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
MESSAGE_BODY
|
MESSAGE_BODY
|
||||||
];
|
];
|
||||||
|
|
|
@ -34,4 +34,4 @@
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
MESSAGE_BODY
|
MESSAGE_BODY
|
||||||
];
|
];
|
||||||
|
|
|
@ -34,4 +34,4 @@
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
MESSAGE_BODY
|
MESSAGE_BODY
|
||||||
];
|
];
|
||||||
|
|
|
@ -4,4 +4,4 @@
|
||||||
'fields' => [
|
'fields' => [
|
||||||
'Location: http://localhost:'.$_SERVER['SERVER_PORT'].$_SERVER['REQUEST_URI']."0",
|
'Location: http://localhost:'.$_SERVER['SERVER_PORT'].$_SERVER['REQUEST_URI']."0",
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue