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:
parent
c393dfc42b
commit
e74a3ae3cb
9 changed files with 408 additions and 284 deletions
|
@ -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) {
|
||||
|
|
178
lib/Database.php
178
lib/Database.php
|
@ -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 = [
|
||||
|
|
|
@ -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()];
|
||||
}
|
||||
|
|
|
@ -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
73
lib/Misc/ValueInfo.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
252
locale/en.php
252
locale/en.php
|
@ -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',
|
||||
];
|
|
@ -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]);
|
||||
|
|
|
@ -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]);
|
||||
|
|
Loading…
Reference in a new issue