1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 13:12:41 +00:00

Fix numerous bugs when adding or changing folders

- Specifying a non-integer parent no longer silently casts to 0 or 1
- Specifying a folder ID of 0 now always converts to null automatically
- Performing both a rename and move to root in the same operation no longer results in potential duplicates
- Calling folderSetProperties with an empty data array no peforms an update; it now returns false before the update call
- Modification timestamps are now actually updated when a folder is modified
- Constraint violation exceptions triggered by code (rather than the database) now print a message
- Renaming a folder or subscription to a non-string value (e.g. an array) throws an exception rather than silently casting
- Added tests to better cover all the above
- Centralized the normalization of integers and title strings into a new ValueInfo static class
This commit is contained in:
J. King 2017-09-26 16:45:41 -04:00
parent c393dfc42b
commit e74a3ae3cb
9 changed files with 408 additions and 284 deletions

View file

@ -4,69 +4,71 @@ namespace JKingWeb\Arsse;
abstract class AbstractException extends \Exception {
const CODES = [
"Exception.uncoded" => -1,
"Exception.unknown" => 10000,
"Lang/Exception.defaultFileMissing" => 10101,
"Lang/Exception.fileMissing" => 10102,
"Lang/Exception.fileUnreadable" => 10103,
"Lang/Exception.fileCorrupt" => 10104,
"Lang/Exception.stringMissing" => 10105,
"Lang/Exception.stringInvalid" => 10106,
"Db/Exception.extMissing" => 10201,
"Db/Exception.fileMissing" => 10202,
"Db/Exception.fileUnusable" => 10203,
"Db/Exception.fileUnreadable" => 10204,
"Db/Exception.fileUnwritable" => 10205,
"Db/Exception.fileUncreatable" => 10206,
"Db/Exception.fileCorrupt" => 10207,
"Db/Exception.updateTooNew" => 10211,
"Db/Exception.updateManual" => 10212,
"Db/Exception.updateManualOnly" => 10213,
"Db/Exception.updateFileMissing" => 10214,
"Db/Exception.updateFileUnusable" => 10215,
"Db/Exception.updateFileUnreadable" => 10216,
"Db/Exception.updateFileError" => 10217,
"Db/Exception.updateFileIncomplete" => 10218,
"Db/Exception.paramTypeInvalid" => 10221,
"Db/Exception.paramTypeUnknown" => 10222,
"Db/Exception.paramTypeMissing" => 10223,
"Db/Exception.engineErrorGeneral" => 10224, // this symbol may have engine-specific duplicates to accomodate engine-specific error string construction
"Db/Exception.unknownSavepointStatus" => 10225,
"Db/ExceptionSavepoint.invalid" => 10226,
"Db/ExceptionSavepoint.stale" => 10227,
"Db/ExceptionInput.missing" => 10231,
"Db/ExceptionInput.whitespace" => 10232,
"Db/ExceptionInput.tooLong" => 10233,
"Db/ExceptionInput.tooShort" => 10234,
"Db/ExceptionInput.idMissing" => 10235,
"Db/ExceptionInput.constraintViolation" => 10236,
"Db/ExceptionInput.typeViolation" => 10237,
"Db/ExceptionInput.circularDependence" => 10238,
"Db/ExceptionInput.subjectMissing" => 10239,
"Db/ExceptionTimeout.general" => 10241,
"Conf/Exception.fileMissing" => 10301,
"Conf/Exception.fileUnusable" => 10302,
"Conf/Exception.fileUnreadable" => 10303,
"Conf/Exception.fileUnwritable" => 10304,
"Conf/Exception.fileUncreatable" => 10305,
"Conf/Exception.fileCorrupt" => 10306,
"User/Exception.functionNotImplemented" => 10401,
"User/Exception.doesNotExist" => 10402,
"User/Exception.alreadyExists" => 10403,
"User/Exception.authMissing" => 10411,
"User/Exception.authFailed" => 10412,
"User/ExceptionAuthz.notAuthorized" => 10421,
"Feed/Exception.invalidCertificate" => 10501,
"Feed/Exception.invalidUrl" => 10502,
"Feed/Exception.maxRedirect" => 10503,
"Feed/Exception.maxSize" => 10504,
"Feed/Exception.timeout" => 10505,
"Feed/Exception.forbidden" => 10506,
"Feed/Exception.unauthorized" => 10507,
"Feed/Exception.malformedXml" => 10511,
"Feed/Exception.xmlEntity" => 10512,
"Feed/Exception.subscriptionNotFound" => 10521,
"Feed/Exception.unsupportedFeedFormat" => 10522,
"Exception.uncoded" => -1,
"Exception.unknown" => 10000,
"Lang/Exception.defaultFileMissing" => 10101,
"Lang/Exception.fileMissing" => 10102,
"Lang/Exception.fileUnreadable" => 10103,
"Lang/Exception.fileCorrupt" => 10104,
"Lang/Exception.stringMissing" => 10105,
"Lang/Exception.stringInvalid" => 10106,
"Db/Exception.extMissing" => 10201,
"Db/Exception.fileMissing" => 10202,
"Db/Exception.fileUnusable" => 10203,
"Db/Exception.fileUnreadable" => 10204,
"Db/Exception.fileUnwritable" => 10205,
"Db/Exception.fileUncreatable" => 10206,
"Db/Exception.fileCorrupt" => 10207,
"Db/Exception.updateTooNew" => 10211,
"Db/Exception.updateManual" => 10212,
"Db/Exception.updateManualOnly" => 10213,
"Db/Exception.updateFileMissing" => 10214,
"Db/Exception.updateFileUnusable" => 10215,
"Db/Exception.updateFileUnreadable" => 10216,
"Db/Exception.updateFileError" => 10217,
"Db/Exception.updateFileIncomplete" => 10218,
"Db/Exception.paramTypeInvalid" => 10221,
"Db/Exception.paramTypeUnknown" => 10222,
"Db/Exception.paramTypeMissing" => 10223,
"Db/Exception.engineErrorGeneral" => 10224, // this symbol may have engine-specific duplicates to accomodate engine-specific error string construction
"Db/Exception.unknownSavepointStatus" => 10225,
"Db/ExceptionSavepoint.invalid" => 10226,
"Db/ExceptionSavepoint.stale" => 10227,
"Db/ExceptionInput.missing" => 10231,
"Db/ExceptionInput.whitespace" => 10232,
"Db/ExceptionInput.tooLong" => 10233,
"Db/ExceptionInput.tooShort" => 10234,
"Db/ExceptionInput.idMissing" => 10235,
"Db/ExceptionInput.constraintViolation" => 10236,
"Db/ExceptionInput.engineConstraintViolation" => 10236,
"Db/ExceptionInput.typeViolation" => 10237,
"Db/ExceptionInput.engineTypeViolation" => 10237,
"Db/ExceptionInput.circularDependence" => 10238,
"Db/ExceptionInput.subjectMissing" => 10239,
"Db/ExceptionTimeout.general" => 10241,
"Conf/Exception.fileMissing" => 10301,
"Conf/Exception.fileUnusable" => 10302,
"Conf/Exception.fileUnreadable" => 10303,
"Conf/Exception.fileUnwritable" => 10304,
"Conf/Exception.fileUncreatable" => 10305,
"Conf/Exception.fileCorrupt" => 10306,
"User/Exception.functionNotImplemented" => 10401,
"User/Exception.doesNotExist" => 10402,
"User/Exception.alreadyExists" => 10403,
"User/Exception.authMissing" => 10411,
"User/Exception.authFailed" => 10412,
"User/ExceptionAuthz.notAuthorized" => 10421,
"Feed/Exception.invalidCertificate" => 10501,
"Feed/Exception.invalidUrl" => 10502,
"Feed/Exception.maxRedirect" => 10503,
"Feed/Exception.maxSize" => 10504,
"Feed/Exception.timeout" => 10505,
"Feed/Exception.forbidden" => 10506,
"Feed/Exception.unauthorized" => 10507,
"Feed/Exception.malformedXml" => 10511,
"Feed/Exception.xmlEntity" => 10512,
"Feed/Exception.subscriptionNotFound" => 10521,
"Feed/Exception.unsupportedFeedFormat" => 10522,
];
public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {

View file

@ -6,6 +6,7 @@ use PasswordGenerator\Generator as PassGen;
use JKingWeb\Arsse\Misc\Query;
use JKingWeb\Arsse\Misc\Context;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo;
class Database {
const SCHEMA_VERSION = 1;
@ -228,31 +229,13 @@ class Database {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// if the desired folder name is missing or invalid, throw an exception
if (!array_key_exists("name", $data) || $data['name']=="") {
throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "name"]);
} elseif (!strlen(trim($data['name']))) {
throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "name"]);
}
// normalize folder's parent, if there is one
$parent = array_key_exists("parent", $data) ? (int) $data['parent'] : 0;
if ($parent===0) {
// if no parent is specified, do nothing
$parent = null;
} else {
// if a parent is specified, make sure it exists and belongs to the user; get its root (first-level) folder if it's a nested folder
$p = $this->db->prepare("SELECT id from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $parent)->getValue();
if (!$p) {
throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "parent", 'id' => $parent]);
}
}
// check if a folder by the same name already exists, because nulls are wonky in SQL
// FIXME: How should folder name be compared? Should a Unicode normalization be applied before comparison and insertion?
if ($this->db->prepare("SELECT count(*) from arsse_folders where owner is ? and parent is ? and name is ?", "str", "int", "str")->run($user, $parent, $data['name'])->getValue() > 0) {
throw new Db\ExceptionInput("constraintViolation"); // FIXME: There needs to be a practical message here
}
// actually perform the insert (!)
return $this->db->prepare("INSERT INTO arsse_folders(owner,parent,name) values(?,?,?)", "str", "int", "str")->run($user, $parent, $data['name'])->lastId();
$parent = array_key_exists("parent", $data) ? $this->folderValidateId($user, $data['parent'])['id'] : null;
// validate the folder name and parent (if specified); this also checks for duplicates
$name = array_key_exists("name", $data) ? $data['name'] : "";
$this->folderValidateName($name, true, $parent);
// actually perform the insert
return $this->db->prepare("INSERT INTO arsse_folders(owner,parent,name) values(?,?,?)", "str", "int", "str")->run($user, $parent, $name)->lastId();
}
public function folderList(string $user, int $parent = null, bool $recursive = true): Db\Result {
@ -303,70 +286,110 @@ class Database {
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// validate the folder ID and, if specified, the parent to move it to
$parent = null;
if (array_key_exists("parent", $data)) {
$parent = $data['parent'];
}
$f = $this->folderValidateId($user, $id, $parent, true);
// if a new name is specified, validate it
if (array_key_exists("name", $data)) {
// verify the folder belongs to the user
$in = $this->folderValidateId($user, $id, true);
$name = array_key_exists("name", $data);
$parent = array_key_exists("parent", $data);
if ($name && $parent) {
// if a new name and parent are specified, validate both together
$this->folderValidateName($data['name']);
}
$data = array_merge($f, $data);
// check to make sure the target folder name/location would not create a duplicate (we must do this check because null is not distinct in SQL)
$existing = $this->db->prepare("SELECT id from arsse_folders where owner is ? and parent is ? and name is ?", "str", "int", "str")->run($user, $data['parent'], $data['name'])->getValue();
if (!is_null($existing) && $existing != $id) {
throw new Db\ExceptionInput("constraintViolation"); // FIXME: There needs to be a practical message here
$in['name'] = $data['name'];
$in['parent'] = $this->folderValidateMove($user, $id, $data['parent'], $data['name']);
} elseif ($name) {
// if a new name is specified, validate it
$this->folderValidateName($data['name'], true, $in['parent']);
$in['name'] = $data['name'];
} elseif ($parent) {
// if a new parent is specified, validate it
$in['parent'] = $this->folderValidateMove($user, $id, $data['parent']);
} else {
// if neither was specified, do nothing
return false;
}
$valid = [
'name' => "str",
'parent' => "int",
];
list($setClause, $setTypes, $setValues) = $this->generateSet($data, $valid);
return (bool) $this->db->prepare("UPDATE arsse_folders set $setClause where owner is ? and id is ?", $setTypes, "str", "int")->run($setValues, $user, $id)->changes();
list($setClause, $setTypes, $setValues) = $this->generateSet($in, $valid);
return (bool) $this->db->prepare("UPDATE arsse_folders set $setClause, modified = CURRENT_TIMESTAMP where owner is ? and id is ?", $setTypes, "str", "int")->run($setValues, $user, $id)->changes();
}
protected function folderValidateId(string $user, int $id = null, int $parent = null, bool $subject = false): array {
if (is_null($id)) {
// if no ID is specified this is a no-op, unless a parent is specified, which is always a circular dependence (the root cannot be moved)
if (!is_null($parent)) {
throw new Db\ExceptionInput("circularDependence", ["action" => $this->caller(), "field" => "parent", 'id' => $parent]); // @codeCoverageIgnore
}
return ['name' => null, 'parent' => null];
protected function folderValidateId(string $user, $id = null, bool $subject = false): array {
$idInfo = ValueInfo::int($id);
if ($idInfo & (ValueInfo::NULL | ValueInfo::ZERO)) {
// if a null or zero ID is specified this is a no-op
return ['id' => null, 'name' => null, 'parent' => null];
}
// if a negative integer or non-integer is specified this will always fail
if (!($idInfo & ValueInfo::VALID) || (($idInfo & ValueInfo::NEG))) {
throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "folder", 'id' => $id]);
}
// 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 id,name,parent from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->getRow();
if (!$f) {
throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "folder", 'id' => $parent]);
}
// if we're moving a folder to a new parent, check that the parent is valid
if (!is_null($parent)) {
// make sure both that the parent exists, and that the parent is not either the folder itself or one of its children (a circular dependence)
$p = $this->db->prepare(
"WITH RECURSIVE folders(id) as (SELECT id from arsse_folders where owner is ? and id is ? union select arsse_folders.id from arsse_folders join folders on arsse_folders.parent=folders.id) ".
"SELECT id,(id not in (select id from folders)) as valid from arsse_folders where owner is ? and id is ?",
"str", "int", "str", "int"
)->run($user, $id, $user, $parent)->getRow();
if (!$p) {
// if the parent doesn't exist or doesn't below to the user, throw an exception
throw new Db\ExceptionInput("idMissing", ["action" => $this->caller(), "field" => "parent", 'id' => $parent]);
} else {
// if using the desired parent would create a circular dependence, throw a different exception
if (!$p['valid']) {
throw new Db\ExceptionInput("circularDependence", ["action" => $this->caller(), "field" => "parent", 'id' => $parent]);
}
}
throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "folder", 'id' => $id]);
}
return $f;
}
protected function folderValidateName($name): bool {
$name = (string) $name;
if (!strlen($name)) {
protected function folderValidateMove(string $user, int $id = null, $parent = null, string $name = null) {
$errData = ["action" => $this->caller(), "field" => "parent", 'id' => $parent];
if (!$id) {
// the root cannot be moved
throw new Db\ExceptionInput("circularDependence", $errData);
}
$info = ValueInfo::int($parent);
// the root is always a valid parent
if ($info & (ValueInfo::NULL | ValueInfo::ZERO)) {
$parent = null;
} else {
// if a negative integer or non-integer is specified this will always fail
if (!($info & ValueInfo::VALID) || (($info & ValueInfo::NEG))) {
throw new Db\ExceptionInput("idMissing", $errData);
}
$parent = (int) $parent;
}
// if the target parent is the folder itself, this is a circular dependence
if ($id==$parent) {
throw new Db\ExceptionInput("circularDependence", $errData);
}
// make sure both that the prospective parent exists, and that the it is not one of its children (a circular dependence)
$p = $this->db->prepare(
"WITH RECURSIVE
target as (select ? as user, ? as source, ? as dest, ? as rename),
folders as (SELECT id from arsse_folders join target on owner is user and parent is source union select arsse_folders.id as id from arsse_folders join folders on arsse_folders.parent=folders.id)
".
"SELECT
((select dest from target) is null or exists(select id from arsse_folders join target on owner is user and id is dest)) as extant,
not exists(select id from folders where id is (select dest from target)) as valid,
not exists(select id from arsse_folders join target on parent is dest and name is coalesce((select rename from target),(select name from arsse_folders join target on id is source))) as available
", "str", "int", "int","str"
)->run($user, $id, $parent, $name)->getRow();
if (!$p['extant']) {
// if the parent doesn't exist or doesn't below to the user, throw an exception
throw new Db\ExceptionInput("idMissing", $errData);
} elseif (!$p['valid']) {
// if using the desired parent would create a circular dependence, throw a different exception
throw new Db\ExceptionInput("circularDependence", $errData);
} elseif (!$p['available']) {
throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => (is_null($name) ? "parent" : "name")]);
}
return $parent;
}
protected function folderValidateName($name, bool $checkDuplicates = false, int $parent = null): bool {
$info = ValueInfo::str($name);
if ($info & (ValueInfo::NULL | ValueInfo::EMPTY)) {
throw new Db\ExceptionInput("missing", ["action" => $this->caller(), "field" => "name"]);
} elseif (!strlen(trim($name))) {
} elseif ($info & ValueInfo::WHITE) {
throw new Db\ExceptionInput("whitespace", ["action" => $this->caller(), "field" => "name"]);
} elseif (!($info & ValueInfo::VALID)) {
throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "name", 'type' => "string"]);
} elseif($checkDuplicates) {
if ($this->db->prepare("SELECT exists(select id from arsse_folders where parent is ? and name is ?)", "int", "str")->run($parent, $name)->getValue()) {
throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => "name"]);
}
return true;
} else {
return true;
}
@ -420,7 +443,7 @@ class Database {
// this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder
// if an ID is specified, add a suitable WHERE condition and bindings
$q->setWhere("arsse_subscriptions.id is ?", "int", $id);
} elseif (!is_null($folder)) {
} elseif ($folder) {
// if a folder is specified, make sure it exists
$this->folderValidateId($user, $folder);
// if it does exist, add a common table expression to list it and its children so that we select from the entire subtree
@ -467,18 +490,19 @@ class Database {
}
if (array_key_exists("folder", $data)) {
// ensure the target folder exists and belong to the user
$this->folderValidateId($user, $data['folder']);
$data['folder'] = $this->folderValidateId($user, $data['folder'])['id'];
}
if (array_key_exists("title", $data)) {
// if the title is null, this signals intended use of the default title; otherwise make sure it's not effectively an empty string
if (!is_null($data['title'])) {
$title = (string) $data['title'];
if (!strlen($title)) {
$info = ValueInfo::str($data['title']);
if ($info & ValueInfo::EMPTY) {
throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "title"]);
} elseif (!strlen(trim($title))) {
} elseif ($info & ValueInfo::WHITE) {
throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "title"]);
} elseif (!($info & ValueInfo::VALID)) {
throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "title", 'type' => "string"]);
}
$data['title'] = $title;
}
}
$valid = [

View file

@ -12,9 +12,9 @@ trait ExceptionBuilder {
case self::SQLITE_BUSY:
return [ExceptionTimeout::class, 'general', $this->db->lastErrorMsg()];
case self::SQLITE_CONSTRAINT:
return [ExceptionInput::class, 'constraintViolation', $this->db->lastErrorMsg()];
return [ExceptionInput::class, 'engineConstraintViolation', $this->db->lastErrorMsg()];
case self::SQLITE_MISMATCH:
return [ExceptionInput::class, 'typeViolation', $this->db->lastErrorMsg()];
return [ExceptionInput::class, 'engineTypeViolation', $this->db->lastErrorMsg()];
default:
return [Exception::class, 'engineErrorGeneral', $this->db->lastErrorMsg()];
}

View file

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Misc;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo;
class Context {
public $reverse = false;
@ -36,22 +37,11 @@ class Context {
protected function cleanArray(array $spec): array {
$spec = array_values($spec);
for ($a = 0; $a < sizeof($spec); $a++) {
$id = $spec[$a];
if (is_int($id) && $id > -1) {
continue;
} elseif (is_float($id) && !fmod($id, 1) && $id >= 0) {
$spec[$a] = (int) $id;
continue;
} elseif (is_string($id)) {
$ch1 = strval(@intval($id));
$ch2 = strval($id);
if ($ch1 !== $ch2 || $id < 1) {
$id = 0;
}
if(ValueInfo::int($spec[$a])===ValueInfo::VALID) {
$spec[$a] = (int) $spec[$a];
} else {
$id = 0;
$spec[$a] = 0;
}
$spec[$a] = (int) $id;
}
return array_values(array_filter($spec));
}

73
lib/Misc/ValueInfo.php Normal file
View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Misc;
class ValueInfo {
// universal
const VALID = 1 << 0;
const NULL = 1 << 1;
// integers
const ZERO = 1 << 2;
const NEG = 1 << 3;
// strings
const EMPTY = 1 << 2;
const WHITE = 1 << 3;
static public function int($value): int {
$out = 0;
// check if the input is null
if (is_null($value)) {
$out += self::NULL;
}
// normalize the value to an integer or float if possible
if (is_string($value)) {
if (strval(@intval($value))===$value) {
$value = (int) $value;
} elseif (strval(@floatval($value))===$value) {
$value = (float) $value;
}
// the empty string is equivalent to null when evaluating an integer
if (!strlen((string) $value)) {
$out += self::NULL;
}
}
// if the value is not an integer or integral float, stop
if (!is_int($value) && (!is_float($value) || fmod($value, 1))) {
return $out;
}
// mark validity
$value = (int) $value;
$out += self::VALID;
// mark zeroness
if(!$value) {
$out += self::ZERO;
}
// mark negativeness
if ($value < 0) {
$out += self::NEG;
}
return $out;
}
static public function str($value): int {
$out = 0;
// check if the input is null
if (is_null($value)) {
$out += self::NULL;
}
// if the value is not scalar, it cannot be valid
if (!is_scalar($value)) {
return $out;
}
// mark validity
$out += self::VALID;
if (!strlen((string) $value)) {
// mark emptiness
$out += self::EMPTY;
} elseif (!strlen(trim((string) $value))) {
// mark whitespacedness
$out += self::WHITE;
}
return $out;
}
}

View file

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\REST;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo;
abstract class AbstractHandler implements Handler {
abstract public function __construct();
@ -32,9 +33,7 @@ abstract class AbstractHandler implements Handler {
}
protected function validateInt($id): bool {
$ch1 = strval(@intval($id));
$ch2 = strval($id);
return ($ch1 === $ch2);
return (bool) (ValueInfo::int($id) & ValueInfo::VALID);
}
protected function NormalizeInput(array $data, array $types, string $dateFormat = null): array {

View file

@ -1,135 +1,137 @@
<?php
return [
'Driver.Db.SQLite3.Name' => 'SQLite 3',
'Driver.Service.Curl.Name' => 'HTTP (curl)',
'Driver.Service.Internal.Name' => 'Internal',
'Driver.User.Internal.Name' => 'Internal',
'Driver.Db.SQLite3.Name' => 'SQLite 3',
'Driver.Service.Curl.Name' => 'HTTP (curl)',
'Driver.Service.Internal.Name' => 'Internal',
'Driver.User.Internal.Name' => 'Internal',
'HTTP.Status.100' => 'Continue',
'HTTP.Status.101' => 'Switching Protocols',
'HTTP.Status.102' => 'Processing',
'HTTP.Status.200' => 'OK',
'HTTP.Status.201' => 'Created',
'HTTP.Status.202' => 'Accepted',
'HTTP.Status.203' => 'Non-Authoritative Information',
'HTTP.Status.204' => 'No Content',
'HTTP.Status.205' => 'Reset Content',
'HTTP.Status.206' => 'Partial Content',
'HTTP.Status.207' => 'Multi-Status',
'HTTP.Status.208' => 'Already Reported',
'HTTP.Status.226' => 'IM Used',
'HTTP.Status.300' => 'Multiple Choice',
'HTTP.Status.301' => 'Moved Permanently',
'HTTP.Status.302' => 'Found',
'HTTP.Status.303' => 'See Other',
'HTTP.Status.304' => 'Not Modified',
'HTTP.Status.305' => 'Use Proxy',
'HTTP.Status.306' => 'Switch Proxy',
'HTTP.Status.307' => 'Temporary Redirect',
'HTTP.Status.308' => 'Permanent Redirect',
'HTTP.Status.400' => 'Bad Request',
'HTTP.Status.401' => 'Unauthorized',
'HTTP.Status.402' => 'Payment Required',
'HTTP.Status.403' => 'Forbidden',
'HTTP.Status.404' => 'Not Found',
'HTTP.Status.405' => 'Method Not Allowed',
'HTTP.Status.406' => 'Not Acceptable',
'HTTP.Status.407' => 'Proxy Authentication Required',
'HTTP.Status.408' => 'Request Timeout',
'HTTP.Status.409' => 'Conflict',
'HTTP.Status.410' => 'Gone',
'HTTP.Status.411' => 'Length Required',
'HTTP.Status.412' => 'Precondition Failed',
'HTTP.Status.413' => 'Payload Too Large',
'HTTP.Status.414' => 'URL Too Long',
'HTTP.Status.415' => 'Unsupported Media Type',
'HTTP.Status.416' => 'Range Not Satisfiable',
'HTTP.Status.417' => 'Expectation Failed',
'HTTP.Status.421' => 'Misdirected Request',
'HTTP.Status.422' => 'Unprocessable Entity',
'HTTP.Status.423' => 'Locked',
'HTTP.Status.424' => 'Failed Depedency',
'HTTP.Status.426' => 'Upgrade Required',
'HTTP.Status.428' => 'Precondition Failed',
'HTTP.Status.429' => 'Too Many Requests',
'HTTP.Status.431' => 'Request Header Fields Too Large',
'HTTP.Status.451' => 'Unavailable For Legal Reasons',
'HTTP.Status.500' => 'Internal Server Error',
'HTTP.Status.501' => 'Not Implemented',
'HTTP.Status.502' => 'Bad Gateway',
'HTTP.Status.503' => 'Service Unavailable',
'HTTP.Status.504' => 'Gateway Timeout',
'HTTP.Status.505' => 'HTTP Version Not Supported',
'HTTP.Status.506' => 'Variant Also Negotiates',
'HTTP.Status.507' => 'Insufficient Storage',
'HTTP.Status.508' => 'Loop Detected',
'HTTP.Status.510' => 'Not Extended',
'HTTP.Status.511' => 'Network Authentication Required',
'HTTP.Status.100' => 'Continue',
'HTTP.Status.101' => 'Switching Protocols',
'HTTP.Status.102' => 'Processing',
'HTTP.Status.200' => 'OK',
'HTTP.Status.201' => 'Created',
'HTTP.Status.202' => 'Accepted',
'HTTP.Status.203' => 'Non-Authoritative Information',
'HTTP.Status.204' => 'No Content',
'HTTP.Status.205' => 'Reset Content',
'HTTP.Status.206' => 'Partial Content',
'HTTP.Status.207' => 'Multi-Status',
'HTTP.Status.208' => 'Already Reported',
'HTTP.Status.226' => 'IM Used',
'HTTP.Status.300' => 'Multiple Choice',
'HTTP.Status.301' => 'Moved Permanently',
'HTTP.Status.302' => 'Found',
'HTTP.Status.303' => 'See Other',
'HTTP.Status.304' => 'Not Modified',
'HTTP.Status.305' => 'Use Proxy',
'HTTP.Status.306' => 'Switch Proxy',
'HTTP.Status.307' => 'Temporary Redirect',
'HTTP.Status.308' => 'Permanent Redirect',
'HTTP.Status.400' => 'Bad Request',
'HTTP.Status.401' => 'Unauthorized',
'HTTP.Status.402' => 'Payment Required',
'HTTP.Status.403' => 'Forbidden',
'HTTP.Status.404' => 'Not Found',
'HTTP.Status.405' => 'Method Not Allowed',
'HTTP.Status.406' => 'Not Acceptable',
'HTTP.Status.407' => 'Proxy Authentication Required',
'HTTP.Status.408' => 'Request Timeout',
'HTTP.Status.409' => 'Conflict',
'HTTP.Status.410' => 'Gone',
'HTTP.Status.411' => 'Length Required',
'HTTP.Status.412' => 'Precondition Failed',
'HTTP.Status.413' => 'Payload Too Large',
'HTTP.Status.414' => 'URL Too Long',
'HTTP.Status.415' => 'Unsupported Media Type',
'HTTP.Status.416' => 'Range Not Satisfiable',
'HTTP.Status.417' => 'Expectation Failed',
'HTTP.Status.421' => 'Misdirected Request',
'HTTP.Status.422' => 'Unprocessable Entity',
'HTTP.Status.423' => 'Locked',
'HTTP.Status.424' => 'Failed Depedency',
'HTTP.Status.426' => 'Upgrade Required',
'HTTP.Status.428' => 'Precondition Failed',
'HTTP.Status.429' => 'Too Many Requests',
'HTTP.Status.431' => 'Request Header Fields Too Large',
'HTTP.Status.451' => 'Unavailable For Legal Reasons',
'HTTP.Status.500' => 'Internal Server Error',
'HTTP.Status.501' => 'Not Implemented',
'HTTP.Status.502' => 'Bad Gateway',
'HTTP.Status.503' => 'Service Unavailable',
'HTTP.Status.504' => 'Gateway Timeout',
'HTTP.Status.505' => 'HTTP Version Not Supported',
'HTTP.Status.506' => 'Variant Also Negotiates',
'HTTP.Status.507' => 'Insufficient Storage',
'HTTP.Status.508' => 'Loop Detected',
'HTTP.Status.510' => 'Not Extended',
'HTTP.Status.511' => 'Network Authentication Required',
// this should only be encountered in testing (because tests should cover all exceptions!)
'Exception.JKingWeb/Arsse/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in AbstractException.php',
'Exception.JKingWeb/Arsse/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in AbstractException.php',
// this should not usually be encountered
'Exception.JKingWeb/Arsse/Exception.unknown' => 'An unknown error has occurred',
'Exception.JKingWeb/Arsse/Lang/Exception.defaultFileMissing' => 'Default language file "{0}" missing',
'Exception.JKingWeb/Arsse/Lang/Exception.fileMissing' => 'Language file "{0}" is not available',
'Exception.JKingWeb/Arsse/Lang/Exception.fileUnreadable' => 'Insufficient permissions to read language file "{0}"',
'Exception.JKingWeb/Arsse/Lang/Exception.fileCorrupt' => 'Language file "{0}" is corrupt or does not conform to expected format',
'Exception.JKingWeb/Arsse/Lang/Exception.stringMissing' => 'Message string "{msgID}" missing from all loaded language files ({fileList})',
'Exception.JKingWeb/Arsse/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})',
'Exception.JKingWeb/Arsse/Conf/Exception.fileMissing' => 'Configuration file "{0}" does not exist',
'Exception.JKingWeb/Arsse/Conf/Exception.fileUnreadable' => 'Insufficient permissions to read configuration file "{0}"',
'Exception.JKingWeb/Arsse/Conf/Exception.fileUncreatable' => 'Insufficient permissions to write new configuration file "{0}"',
'Exception.JKingWeb/Arsse/Conf/Exception.fileUnwritable' => 'Insufficient permissions to overwrite configuration file "{0}"',
'Exception.JKingWeb/Arsse/Conf/Exception.fileCorrupt' => 'Configuration file "{0}" is corrupt or does not conform to expected format',
'Exception.JKingWeb/Arsse/Db/Exception.extMissing' => 'Required PHP extension for driver "{0}" not installed',
'Exception.JKingWeb/Arsse/Db/Exception.fileMissing' => 'Database file "{0}" does not exist',
'Exception.JKingWeb/Arsse/Db/Exception.fileUnreadable' => 'Insufficient permissions to open database file "{0}" for reading',
'Exception.JKingWeb/Arsse/Db/Exception.fileUnwritable' => 'Insufficient permissions to open database file "{0}" for writing',
'Exception.JKingWeb/Arsse/Db/Exception.fileUnusable' => 'Insufficient permissions to open database file "{0}" for reading or writing',
'Exception.JKingWeb/Arsse/Db/Exception.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"',
'Exception.JKingWeb/Arsse/Db/Exception.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database',
'Exception.JKingWeb/Arsse/Db/Exception.paramTypeInvalid' => 'Prepared statement parameter type "{0}" is invalid',
'Exception.JKingWeb/Arsse/Db/Exception.paramTypeUnknown' => 'Prepared statement parameter type "{0}" is valid, but not implemented',
'Exception.JKingWeb/Arsse/Db/Exception.paramTypeMissing' => 'Prepared statement parameter type for parameter #{0} was not specified',
'Exception.JKingWeb/Arsse/Db/Exception.updateManual' =>
'Exception.JKingWeb/Arsse/Exception.unknown' => 'An unknown error has occurred',
'Exception.JKingWeb/Arsse/Lang/Exception.defaultFileMissing' => 'Default language file "{0}" missing',
'Exception.JKingWeb/Arsse/Lang/Exception.fileMissing' => 'Language file "{0}" is not available',
'Exception.JKingWeb/Arsse/Lang/Exception.fileUnreadable' => 'Insufficient permissions to read language file "{0}"',
'Exception.JKingWeb/Arsse/Lang/Exception.fileCorrupt' => 'Language file "{0}" is corrupt or does not conform to expected format',
'Exception.JKingWeb/Arsse/Lang/Exception.stringMissing' => 'Message string "{msgID}" missing from all loaded language files ({fileList})',
'Exception.JKingWeb/Arsse/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})',
'Exception.JKingWeb/Arsse/Conf/Exception.fileMissing' => 'Configuration file "{0}" does not exist',
'Exception.JKingWeb/Arsse/Conf/Exception.fileUnreadable' => 'Insufficient permissions to read configuration file "{0}"',
'Exception.JKingWeb/Arsse/Conf/Exception.fileUncreatable' => 'Insufficient permissions to write new configuration file "{0}"',
'Exception.JKingWeb/Arsse/Conf/Exception.fileUnwritable' => 'Insufficient permissions to overwrite configuration file "{0}"',
'Exception.JKingWeb/Arsse/Conf/Exception.fileCorrupt' => 'Configuration file "{0}" is corrupt or does not conform to expected format',
'Exception.JKingWeb/Arsse/Db/Exception.extMissing' => 'Required PHP extension for driver "{0}" not installed',
'Exception.JKingWeb/Arsse/Db/Exception.fileMissing' => 'Database file "{0}" does not exist',
'Exception.JKingWeb/Arsse/Db/Exception.fileUnreadable' => 'Insufficient permissions to open database file "{0}" for reading',
'Exception.JKingWeb/Arsse/Db/Exception.fileUnwritable' => 'Insufficient permissions to open database file "{0}" for writing',
'Exception.JKingWeb/Arsse/Db/Exception.fileUnusable' => 'Insufficient permissions to open database file "{0}" for reading or writing',
'Exception.JKingWeb/Arsse/Db/Exception.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"',
'Exception.JKingWeb/Arsse/Db/Exception.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database',
'Exception.JKingWeb/Arsse/Db/Exception.paramTypeInvalid' => 'Prepared statement parameter type "{0}" is invalid',
'Exception.JKingWeb/Arsse/Db/Exception.paramTypeUnknown' => 'Prepared statement parameter type "{0}" is valid, but not implemented',
'Exception.JKingWeb/Arsse/Db/Exception.paramTypeMissing' => 'Prepared statement parameter type for parameter #{0} was not specified',
'Exception.JKingWeb/Arsse/Db/Exception.updateManual' =>
'{from_version, select,
0 {{driver_name} database is configured for manual updates and is not initialized; please populate the database with the base schema}
other {{driver_name} database is configured for manual updates; please update from schema version {current} to version {target}}
}',
'Exception.JKingWeb/Arsse/Db/Exception.updateManualOnly' =>
'Exception.JKingWeb/Arsse/Db/Exception.updateManualOnly' =>
'{from_version, select,
0 {{driver_name} database must be updated manually and is not initialized; please populate the database with the base schema}
other {{driver_name} database must be updated manually; please update from schema version {current} to version {target}}
}',
'Exception.JKingWeb/Arsse/Db/Exception.updateFileMissing' => 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} not being available',
'Exception.JKingWeb/Arsse/Db/Exception.updateFileUnreadable' => 'Automatic updating of the {driver_name} database failed due to insufficient permissions to read instructions for updating from version {current}',
'Exception.JKingWeb/Arsse/Db/Exception.updateFileUnusable' => 'Automatic updating of the {driver_name} database failed due to an error reading instructions for updating from version {current}',
'Exception.JKingWeb/Arsse/Db/Exception.updateFileError' => 'Automatic updating of the {driver_name} database failed updating from version {current} with the following error: "{message}"',
'Exception.JKingWeb/Arsse/Db/Exception.updateFileIncomplete' => 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} being incomplete',
'Exception.JKingWeb/Arsse/Db/Exception.updateTooNew' =>
'Exception.JKingWeb/Arsse/Db/Exception.updateFileMissing' => 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} not being available',
'Exception.JKingWeb/Arsse/Db/Exception.updateFileUnreadable' => 'Automatic updating of the {driver_name} database failed due to insufficient permissions to read instructions for updating from version {current}',
'Exception.JKingWeb/Arsse/Db/Exception.updateFileUnusable' => 'Automatic updating of the {driver_name} database failed due to an error reading instructions for updating from version {current}',
'Exception.JKingWeb/Arsse/Db/Exception.updateFileError' => 'Automatic updating of the {driver_name} database failed updating from version {current} with the following error: "{message}"',
'Exception.JKingWeb/Arsse/Db/Exception.updateFileIncomplete' => 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} being incomplete',
'Exception.JKingWeb/Arsse/Db/Exception.updateTooNew' =>
'{difference, select,
0 {Automatic updating of the {driver_name} database failed because it is already up to date with the requested version, {target}}
other {Automatic updating of the {driver_name} database failed because its version, {current}, is newer than the requested version, {target}}
}',
'Exception.JKingWeb/Arsse/Db/Exception.engineErrorGeneral' => '{0}',
'Exception.JKingWeb/Arsse/Db/Exception.unknownSavepointStatus' => 'Savepoint status code {0} not implemented',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.missing' => 'Required field "{field}" missing while performing action "{action}"',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.whitespace' => 'Field "{field}" of action "{action}" may not contain only whitespace',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooLong' => 'Field "{field}" of action "{action}" has a maximum length of {max}',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooShort' => 'Field "{field}" of action "{action}" has a minimum length of {min}',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.subjectMissing' => 'Referenced ID ({id}) in field "{field}" does not exist',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.idMissing' => 'Referenced ID ({id}) in field "{field}" does not exist',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.circularDependence' => 'Referenced ID ({id}) in field "{field}" creates a circular dependence',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.constraintViolation' => '{0}',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.typeViolation' => '{0}',
'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general' => '{0}',
'Exception.JKingWeb/Arsse/Db/ExceptionSavepoint.invalid' => 'Tried to {action} invalid savepoint {index}',
'Exception.JKingWeb/Arsse/Db/ExceptionSavepoint.stale' => 'Tried to {action} stale savepoint {index}',
'Exception.JKingWeb/Arsse/User/Exception.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists',
'Exception.JKingWeb/Arsse/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist',
'Exception.JKingWeb/Arsse/User/Exception.authMissing' => 'Please log in to proceed',
'Exception.JKingWeb/Arsse/User/Exception.authFailed' => 'Authentication failed',
'Exception.JKingWeb/Arsse/User/ExceptionAuthz.notAuthorized' =>
'Exception.JKingWeb/Arsse/Db/Exception.engineErrorGeneral' => '{0}',
'Exception.JKingWeb/Arsse/Db/Exception.unknownSavepointStatus' => 'Savepoint status code {0} not implemented',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.missing' => 'Required field "{field}" missing while performing action "{action}"',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.whitespace' => 'Field "{field}" of action "{action}" may not contain only whitespace',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooLong' => 'Field "{field}" of action "{action}" has a maximum length of {max}',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooShort' => 'Field "{field}" of action "{action}" has a minimum length of {min}',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.typeViolation' => 'Field "{field}" of action "{action}" expects a value of type "{type}"',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.subjectMissing' => 'Referenced ID ({id}) in field "{field}" does not exist',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.idMissing' => 'Referenced ID ({id}) in field "{field}" does not exist',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.circularDependence' => 'Referenced ID ({id}) in field "{field}" creates a circular dependence',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.constraintViolation' => 'Specified value in field "{0}" already exists',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineConstraintViolation' => '{0}',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineTypeViolation' => '{0}',
'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general' => '{0}',
'Exception.JKingWeb/Arsse/Db/ExceptionSavepoint.invalid' => 'Tried to {action} invalid savepoint {index}',
'Exception.JKingWeb/Arsse/Db/ExceptionSavepoint.stale' => 'Tried to {action} stale savepoint {index}',
'Exception.JKingWeb/Arsse/User/Exception.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists',
'Exception.JKingWeb/Arsse/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist',
'Exception.JKingWeb/Arsse/User/Exception.authMissing' => 'Please log in to proceed',
'Exception.JKingWeb/Arsse/User/Exception.authFailed' => 'Authentication failed',
'Exception.JKingWeb/Arsse/User/ExceptionAuthz.notAuthorized' =>
'{action, select,
userList {{user, select,
global {Authenticated user is not authorized to view the global user list}
@ -137,15 +139,15 @@ return [
}}
other {Authenticated user is not authorized to perform the action "{action}" on behalf of {user}}
}',
'Exception.JKingWeb/Arsse/Feed/Exception.invalidCertificate' => 'Could not download feed "{url}" because its server is serving an invalid SSL certificate',
'Exception.JKingWeb/Arsse/Feed/Exception.invalidUrl' => 'Feed URL "{url}" is invalid',
'Exception.JKingWeb/Arsse/Feed/Exception.maxRedirect' => 'Could not download feed "{url}" because its server reached its maximum number of HTTP redirections',
'Exception.JKingWeb/Arsse/Feed/Exception.maxSize' => 'Could not download feed "{url}" because its size exceeds the maximum allowed on its server',
'Exception.JKingWeb/Arsse/Feed/Exception.timeout' => 'Could not download feed "{url}" because its server timed out',
'Exception.JKingWeb/Arsse/Feed/Exception.forbidden' => 'Could not download feed "{url}" because you do not have permission to access it',
'Exception.JKingWeb/Arsse/Feed/Exception.unauthorized' => 'Could not download feed "{url}" because you provided insufficient or invalid credentials',
'Exception.JKingWeb/Arsse/Feed/Exception.malformedXml' => 'Could not parse feed "{url}" because it is malformed',
'Exception.JKingWeb/Arsse/Feed/Exception.xmlEntity' => 'Refused to parse feed "{url}" because it contains an XXE attack',
'Exception.JKingWeb/Arsse/Feed/Exception.subscriptionNotFound' => 'Unable to find a feed at location "{url}"',
'Exception.JKingWeb/Arsse/Feed/Exception.unsupportedFeedFormat' => 'Feed "{url}" is of an unsupported format',
'Exception.JKingWeb/Arsse/Feed/Exception.invalidCertificate' => 'Could not download feed "{url}" because its server is serving an invalid SSL certificate',
'Exception.JKingWeb/Arsse/Feed/Exception.invalidUrl' => 'Feed URL "{url}" is invalid',
'Exception.JKingWeb/Arsse/Feed/Exception.maxRedirect' => 'Could not download feed "{url}" because its server reached its maximum number of HTTP redirections',
'Exception.JKingWeb/Arsse/Feed/Exception.maxSize' => 'Could not download feed "{url}" because its size exceeds the maximum allowed on its server',
'Exception.JKingWeb/Arsse/Feed/Exception.timeout' => 'Could not download feed "{url}" because its server timed out',
'Exception.JKingWeb/Arsse/Feed/Exception.forbidden' => 'Could not download feed "{url}" because you do not have permission to access it',
'Exception.JKingWeb/Arsse/Feed/Exception.unauthorized' => 'Could not download feed "{url}" because you provided insufficient or invalid credentials',
'Exception.JKingWeb/Arsse/Feed/Exception.malformedXml' => 'Could not parse feed "{url}" because it is malformed',
'Exception.JKingWeb/Arsse/Feed/Exception.xmlEntity' => 'Refused to parse feed "{url}" because it contains an XXE attack',
'Exception.JKingWeb/Arsse/Feed/Exception.subscriptionNotFound' => 'Unable to find a feed at location "{url}"',
'Exception.JKingWeb/Arsse/Feed/Exception.unsupportedFeedFormat' => 'Feed "{url}" is of an unsupported format',
];

View file

@ -76,6 +76,11 @@ trait SeriesFolder {
Arsse::$db->folderAdd("john.doe@example.com", ['name' => "Sociology", 'parent' => 2112]);
}
public function testAddANestedFolderToAnInvalidParent() {
$this->assertException("idMissing", "Db", "ExceptionInput");
Arsse::$db->folderAdd("john.doe@example.com", ['name' => "Sociology", 'parent' => "stringFolderId"]);
}
public function testAddANestedFolderForTheWrongOwner() {
$this->assertException("idMissing", "Db", "ExceptionInput");
Arsse::$db->folderAdd("john.doe@example.com", ['name' => "Sociology", 'parent' => 4]); // folder ID 4 belongs to Jane
@ -216,6 +221,10 @@ trait SeriesFolder {
Arsse::$db->folderPropertiesGet("john.doe@example.com", 1);
}
public function testMakeNoChangesToAFolder() {
$this->assertFalse(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, []));
}
public function testRenameAFolder() {
$this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['name' => "Opinion"]));
Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderPropertiesSet");
@ -234,6 +243,11 @@ trait SeriesFolder {
$this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['name' => " "]));
}
public function testRenameAFolderToAnInvalidValue() {
$this->assertException("typeViolation", "Db", "ExceptionInput");
$this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['name' => []]));
}
public function testMoveAFolder() {
$this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['parent' => 5]));
Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderPropertiesSet");
@ -242,6 +256,11 @@ trait SeriesFolder {
$this->compareExpectations($state);
}
public function testMoveTheRootFolder() {
$this->assertException("circularDependence", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesSet("john.doe@example.com", 0, ['parent' => 1]);
}
public function testMoveAFolderToItsDescendant() {
$this->assertException("circularDependence", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesSet("john.doe@example.com", 1, ['parent' => 3]);
@ -257,11 +276,21 @@ trait SeriesFolder {
Arsse::$db->folderPropertiesSet("john.doe@example.com", 1, ['parent' => 2112]);
}
public function testMoveAFolderToAnInvalidParent() {
$this->assertException("idMissing", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesSet("john.doe@example.com", 1, ['parent' => "ThisFolderDoesNotExist"]);
}
public function testCauseAFolderCollision() {
$this->assertException("constraintViolation", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['parent' => null]);
}
public function testCauseACompoundFolderCollision() {
$this->assertException("constraintViolation", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesSet("john.doe@example.com", 3, ['parent' => null, 'name' => "Technology"]);
}
public function testSetThePropertiesOfAMissingFolder() {
$this->assertException("subjectMissing", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesSet("john.doe@example.com", 2112, ['parent' => null]);

View file

@ -319,6 +319,11 @@ trait SeriesSubscription {
$this->assertTrue(Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => 0]));
}
public function testRenameASubscriptionToAnArray() {
$this->assertException("typeViolation", "Db", "ExceptionInput");
Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['title' => []]);
}
public function testSetThePropertiesOfAMissingSubscription() {
$this->assertException("subjectMissing", "Db", "ExceptionInput");
Arsse::$db->subscriptionPropertiesSet($this->user, 2112, ['folder' => null]);