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

Merge fixes from master

This commit is contained in:
J. King 2017-09-26 20:25:11 -04:00
commit 7b121c74ec
9 changed files with 410 additions and 287 deletions

View file

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

View file

@ -7,6 +7,7 @@ use JKingWeb\DrUUID\UUID;
use JKingWeb\Arsse\Misc\Query; use JKingWeb\Arsse\Misc\Query;
use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Misc\Context;
use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo;
class Database { class Database {
const SCHEMA_VERSION = 2; const SCHEMA_VERSION = 2;
@ -281,31 +282,13 @@ class Database {
if (!Arsse::$user->authorize($user, __FUNCTION__)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
// if the desired folder name is missing or invalid, throw an exception
if (!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 // normalize folder's parent, if there is one
$parent = array_key_exists("parent", $data) ? (int) $data['parent'] : 0; $parent = array_key_exists("parent", $data) ? $this->folderValidateId($user, $data['parent'])['id'] : null;
if ($parent===0) { // validate the folder name and parent (if specified); this also checks for duplicates
// if no parent is specified, do nothing $name = array_key_exists("name", $data) ? $data['name'] : "";
$parent = null; $this->folderValidateName($name, true, $parent);
} else { // actually perform the insert
// 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 return $this->db->prepare("INSERT INTO arsse_folders(owner,parent,name) values(?,?,?)", "str", "int", "str")->run($user, $parent, $name)->lastId();
$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();
} }
public function folderList(string $user, int $parent = null, bool $recursive = true): Db\Result { public function folderList(string $user, int $parent = null, bool $recursive = true): Db\Result {
@ -356,70 +339,110 @@ class Database {
if (!Arsse::$user->authorize($user, __FUNCTION__)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
// validate the folder ID and, if specified, the parent to move it to // verify the folder belongs to the user
$parent = null; $in = $this->folderValidateId($user, $id, true);
if (array_key_exists("parent", $data)) { $name = array_key_exists("name", $data);
$parent = $data['parent']; $parent = array_key_exists("parent", $data);
} if ($name && $parent) {
$f = $this->folderValidateId($user, $id, $parent, true); // if a new name and parent are specified, validate both together
// if a new name is specified, validate it
if (array_key_exists("name", $data)) {
$this->folderValidateName($data['name']); $this->folderValidateName($data['name']);
} $in['name'] = $data['name'];
$data = array_merge($f, $data); $in['parent'] = $this->folderValidateMove($user, $id, $data['parent'], $data['name']);
// 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) } elseif ($name) {
$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 a new name is specified, validate it
if (!is_null($existing) && $existing != $id) { $this->folderValidateName($data['name'], true, $in['parent']);
throw new Db\ExceptionInput("constraintViolation"); // FIXME: There needs to be a practical message here $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 = [ $valid = [
'name' => "str", 'name' => "str",
'parent' => "int", 'parent' => "int",
]; ];
list($setClause, $setTypes, $setValues) = $this->generateSet($data, $valid); list($setClause, $setTypes, $setValues) = $this->generateSet($in, $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(); 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 { protected function folderValidateId(string $user, $id = null, bool $subject = false): array {
if (is_null($id)) { $idInfo = ValueInfo::int($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 ($idInfo & (ValueInfo::NULL | ValueInfo::ZERO)) {
if (!is_null($parent)) { // if a null or zero ID is specified this is a no-op
throw new Db\ExceptionInput("circularDependence", ["action" => $this->caller(), "field" => "parent", 'id' => $parent]); // @codeCoverageIgnore return ['id' => null, 'name' => null, 'parent' => null];
} }
return ['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 // 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) { if (!$f) {
throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "folder", 'id' => $parent]); throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "folder", 'id' => $id]);
}
// 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]);
}
}
} }
return $f; return $f;
} }
protected function folderValidateName($name): bool { protected function folderValidateMove(string $user, int $id = null, $parent = null, string $name = null) {
$name = (string) $name; $errData = ["action" => $this->caller(), "field" => "parent", 'id' => $parent];
if (!strlen($name)) { 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"]); 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"]); 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 { } else {
return true; return true;
} }
@ -473,7 +496,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 // this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder
// if an ID is specified, add a suitable WHERE condition and bindings // if an ID is specified, add a suitable WHERE condition and bindings
$q->setWhere("arsse_subscriptions.id is ?", "int", $id); $q->setWhere("arsse_subscriptions.id is ?", "int", $id);
} elseif (!is_null($folder)) { } elseif ($folder) {
// if a folder is specified, make sure it exists // if a folder is specified, make sure it exists
$this->folderValidateId($user, $folder); $this->folderValidateId($user, $folder);
// if it does exist, add a common table expression to list it and its children so that we select from the entire subtree // if it does exist, add a common table expression to list it and its children so that we select from the entire subtree
@ -520,18 +543,19 @@ class Database {
} }
if (array_key_exists("folder", $data)) { if (array_key_exists("folder", $data)) {
// ensure the target folder exists and belong to the user // ensure the target folder exists and belong to the user
$this->folderValidateId($user, $data['folder']); $data['folder'] = $this->folderValidateId($user, $data['folder'])['id'];
} }
if (array_key_exists("title", $data)) { if (array_key_exists("title", $data)) {
// if the title is null, this signals intended use of the default title; otherwise make sure it's not effectively an empty string // if the title is null, this signals intended use of the default title; otherwise make sure it's not effectively an empty string
if (!is_null($data['title'])) { if (!is_null($data['title'])) {
$title = (string) $data['title']; $info = ValueInfo::str($data['title']);
if (!strlen($title)) { if ($info & ValueInfo::EMPTY) {
throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "title"]); 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"]); 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 = [ $valid = [

View file

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

View file

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Misc; namespace JKingWeb\Arsse\Misc;
use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo;
class Context { class Context {
public $reverse = false; public $reverse = false;
@ -36,22 +37,11 @@ class Context {
protected function cleanArray(array $spec): array { protected function cleanArray(array $spec): array {
$spec = array_values($spec); $spec = array_values($spec);
for ($a = 0; $a < sizeof($spec); $a++) { for ($a = 0; $a < sizeof($spec); $a++) {
$id = $spec[$a]; if(ValueInfo::int($spec[$a])===ValueInfo::VALID) {
if (is_int($id) && $id > -1) { $spec[$a] = (int) $spec[$a];
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;
}
} else { } else {
$id = 0; $spec[$a] = 0;
} }
$spec[$a] = (int) $id;
} }
return array_values(array_filter($spec)); 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; namespace JKingWeb\Arsse\REST;
use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo;
abstract class AbstractHandler implements Handler { abstract class AbstractHandler implements Handler {
abstract public function __construct(); abstract public function __construct();
@ -32,9 +33,7 @@ abstract class AbstractHandler implements Handler {
} }
protected function validateInt($id): bool { protected function validateInt($id): bool {
$ch1 = strval(@intval($id)); return (bool) (ValueInfo::int($id) & ValueInfo::VALID);
$ch2 = strval($id);
return ($ch1 === $ch2);
} }
protected function NormalizeInput(array $data, array $types, string $dateFormat = null): array { protected function NormalizeInput(array $data, array $types, string $dateFormat = null): array {

View file

@ -1,135 +1,137 @@
<?php <?php
return [ return [
'Driver.Db.SQLite3.Name' => 'SQLite 3', 'Driver.Db.SQLite3.Name' => 'SQLite 3',
'Driver.Service.Curl.Name' => 'HTTP (curl)', 'Driver.Service.Curl.Name' => 'HTTP (curl)',
'Driver.Service.Internal.Name' => 'Internal', 'Driver.Service.Internal.Name' => 'Internal',
'Driver.User.Internal.Name' => 'Internal', 'Driver.User.Internal.Name' => 'Internal',
'HTTP.Status.100' => 'Continue', 'HTTP.Status.100' => 'Continue',
'HTTP.Status.101' => 'Switching Protocols', 'HTTP.Status.101' => 'Switching Protocols',
'HTTP.Status.102' => 'Processing', 'HTTP.Status.102' => 'Processing',
'HTTP.Status.200' => 'OK', 'HTTP.Status.200' => 'OK',
'HTTP.Status.201' => 'Created', 'HTTP.Status.201' => 'Created',
'HTTP.Status.202' => 'Accepted', 'HTTP.Status.202' => 'Accepted',
'HTTP.Status.203' => 'Non-Authoritative Information', 'HTTP.Status.203' => 'Non-Authoritative Information',
'HTTP.Status.204' => 'No Content', 'HTTP.Status.204' => 'No Content',
'HTTP.Status.205' => 'Reset Content', 'HTTP.Status.205' => 'Reset Content',
'HTTP.Status.206' => 'Partial Content', 'HTTP.Status.206' => 'Partial Content',
'HTTP.Status.207' => 'Multi-Status', 'HTTP.Status.207' => 'Multi-Status',
'HTTP.Status.208' => 'Already Reported', 'HTTP.Status.208' => 'Already Reported',
'HTTP.Status.226' => 'IM Used', 'HTTP.Status.226' => 'IM Used',
'HTTP.Status.300' => 'Multiple Choice', 'HTTP.Status.300' => 'Multiple Choice',
'HTTP.Status.301' => 'Moved Permanently', 'HTTP.Status.301' => 'Moved Permanently',
'HTTP.Status.302' => 'Found', 'HTTP.Status.302' => 'Found',
'HTTP.Status.303' => 'See Other', 'HTTP.Status.303' => 'See Other',
'HTTP.Status.304' => 'Not Modified', 'HTTP.Status.304' => 'Not Modified',
'HTTP.Status.305' => 'Use Proxy', 'HTTP.Status.305' => 'Use Proxy',
'HTTP.Status.306' => 'Switch Proxy', 'HTTP.Status.306' => 'Switch Proxy',
'HTTP.Status.307' => 'Temporary Redirect', 'HTTP.Status.307' => 'Temporary Redirect',
'HTTP.Status.308' => 'Permanent Redirect', 'HTTP.Status.308' => 'Permanent Redirect',
'HTTP.Status.400' => 'Bad Request', 'HTTP.Status.400' => 'Bad Request',
'HTTP.Status.401' => 'Unauthorized', 'HTTP.Status.401' => 'Unauthorized',
'HTTP.Status.402' => 'Payment Required', 'HTTP.Status.402' => 'Payment Required',
'HTTP.Status.403' => 'Forbidden', 'HTTP.Status.403' => 'Forbidden',
'HTTP.Status.404' => 'Not Found', 'HTTP.Status.404' => 'Not Found',
'HTTP.Status.405' => 'Method Not Allowed', 'HTTP.Status.405' => 'Method Not Allowed',
'HTTP.Status.406' => 'Not Acceptable', 'HTTP.Status.406' => 'Not Acceptable',
'HTTP.Status.407' => 'Proxy Authentication Required', 'HTTP.Status.407' => 'Proxy Authentication Required',
'HTTP.Status.408' => 'Request Timeout', 'HTTP.Status.408' => 'Request Timeout',
'HTTP.Status.409' => 'Conflict', 'HTTP.Status.409' => 'Conflict',
'HTTP.Status.410' => 'Gone', 'HTTP.Status.410' => 'Gone',
'HTTP.Status.411' => 'Length Required', 'HTTP.Status.411' => 'Length Required',
'HTTP.Status.412' => 'Precondition Failed', 'HTTP.Status.412' => 'Precondition Failed',
'HTTP.Status.413' => 'Payload Too Large', 'HTTP.Status.413' => 'Payload Too Large',
'HTTP.Status.414' => 'URL Too Long', 'HTTP.Status.414' => 'URL Too Long',
'HTTP.Status.415' => 'Unsupported Media Type', 'HTTP.Status.415' => 'Unsupported Media Type',
'HTTP.Status.416' => 'Range Not Satisfiable', 'HTTP.Status.416' => 'Range Not Satisfiable',
'HTTP.Status.417' => 'Expectation Failed', 'HTTP.Status.417' => 'Expectation Failed',
'HTTP.Status.421' => 'Misdirected Request', 'HTTP.Status.421' => 'Misdirected Request',
'HTTP.Status.422' => 'Unprocessable Entity', 'HTTP.Status.422' => 'Unprocessable Entity',
'HTTP.Status.423' => 'Locked', 'HTTP.Status.423' => 'Locked',
'HTTP.Status.424' => 'Failed Depedency', 'HTTP.Status.424' => 'Failed Depedency',
'HTTP.Status.426' => 'Upgrade Required', 'HTTP.Status.426' => 'Upgrade Required',
'HTTP.Status.428' => 'Precondition Failed', 'HTTP.Status.428' => 'Precondition Failed',
'HTTP.Status.429' => 'Too Many Requests', 'HTTP.Status.429' => 'Too Many Requests',
'HTTP.Status.431' => 'Request Header Fields Too Large', 'HTTP.Status.431' => 'Request Header Fields Too Large',
'HTTP.Status.451' => 'Unavailable For Legal Reasons', 'HTTP.Status.451' => 'Unavailable For Legal Reasons',
'HTTP.Status.500' => 'Internal Server Error', 'HTTP.Status.500' => 'Internal Server Error',
'HTTP.Status.501' => 'Not Implemented', 'HTTP.Status.501' => 'Not Implemented',
'HTTP.Status.502' => 'Bad Gateway', 'HTTP.Status.502' => 'Bad Gateway',
'HTTP.Status.503' => 'Service Unavailable', 'HTTP.Status.503' => 'Service Unavailable',
'HTTP.Status.504' => 'Gateway Timeout', 'HTTP.Status.504' => 'Gateway Timeout',
'HTTP.Status.505' => 'HTTP Version Not Supported', 'HTTP.Status.505' => 'HTTP Version Not Supported',
'HTTP.Status.506' => 'Variant Also Negotiates', 'HTTP.Status.506' => 'Variant Also Negotiates',
'HTTP.Status.507' => 'Insufficient Storage', 'HTTP.Status.507' => 'Insufficient Storage',
'HTTP.Status.508' => 'Loop Detected', 'HTTP.Status.508' => 'Loop Detected',
'HTTP.Status.510' => 'Not Extended', 'HTTP.Status.510' => 'Not Extended',
'HTTP.Status.511' => 'Network Authentication Required', 'HTTP.Status.511' => 'Network Authentication Required',
// this should only be encountered in testing (because tests should cover all exceptions!) // 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 // this should not usually be encountered
'Exception.JKingWeb/Arsse/Exception.unknown' => 'An unknown error has occurred', '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.defaultFileMissing' => 'Default language file "{0}" missing',
'Exception.JKingWeb/Arsse/Lang/Exception.fileMissing' => 'Language file "{0}" is not available', '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.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.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.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/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.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.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.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.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/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.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.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.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.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.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.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.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.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.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.paramTypeMissing' => 'Prepared statement parameter type for parameter #{0} was not specified',
'Exception.JKingWeb/Arsse/Db/Exception.updateManual' => 'Exception.JKingWeb/Arsse/Db/Exception.updateManual' =>
'{from_version, select, '{from_version, select,
0 {{driver_name} database is configured for manual updates and is not initialized; please populate the database with the base schema} 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}} 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, '{from_version, select,
0 {{driver_name} database must be updated manually and is not initialized; please populate the database with the base schema} 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}} 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.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.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.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.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.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.updateTooNew' =>
'{difference, select, '{difference, select,
0 {Automatic updating of the {driver_name} database failed because it is already up to date with the requested version, {target}} 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}} 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.engineErrorGeneral' => '{0}',
'Exception.JKingWeb/Arsse/Db/Exception.unknownSavepointStatus' => 'Savepoint status code {0} not implemented', '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.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.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.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.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.typeViolation' => 'Field "{field}" of action "{action}" expects a value of type "{type}"',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.idMissing' => 'Referenced ID ({id}) in field "{field}" does not exist', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.subjectMissing' => '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.idMissing' => 'Referenced ID ({id}) in field "{field}" does not exist',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.constraintViolation' => '{0}', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.circularDependence' => 'Referenced ID ({id}) in field "{field}" creates a circular dependence',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.typeViolation' => '{0}', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.constraintViolation' => 'Specified value in field "{0}" already exists',
'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general' => '{0}', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineConstraintViolation' => '{0}',
'Exception.JKingWeb/Arsse/Db/ExceptionSavepoint.invalid' => 'Tried to {action} invalid savepoint {index}', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineTypeViolation' => '{0}',
'Exception.JKingWeb/Arsse/Db/ExceptionSavepoint.stale' => 'Tried to {action} stale savepoint {index}', 'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general' => '{0}',
'Exception.JKingWeb/Arsse/User/Exception.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists', 'Exception.JKingWeb/Arsse/Db/ExceptionSavepoint.invalid' => 'Tried to {action} invalid savepoint {index}',
'Exception.JKingWeb/Arsse/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist', 'Exception.JKingWeb/Arsse/Db/ExceptionSavepoint.stale' => 'Tried to {action} stale savepoint {index}',
'Exception.JKingWeb/Arsse/User/Exception.authMissing' => 'Please log in to proceed', 'Exception.JKingWeb/Arsse/User/Exception.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists',
'Exception.JKingWeb/Arsse/User/Exception.authFailed' => 'Authentication failed', 'Exception.JKingWeb/Arsse/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist',
'Exception.JKingWeb/Arsse/User/ExceptionAuthz.notAuthorized' => '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, '{action, select,
userList {{user, select, userList {{user, select,
global {Authenticated user is not authorized to view the global user list} global {Authenticated user is not authorized to view the global user list}
@ -137,16 +139,16 @@ return [
}} }}
other {Authenticated user is not authorized to perform the action "{action}" on behalf of {user}} other {Authenticated user is not authorized to perform the action "{action}" on behalf of {user}}
}', }',
'Exception.JKingWeb/Arsse/User/ExceptionSession.invalid' => 'Session with ID {0} does not exist', 'Exception.JKingWeb/Arsse/User/ExceptionSession.invalid' => 'Session with ID {0} does not exist',
'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.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.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.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.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.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.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.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.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.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.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.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]); 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() { public function testAddANestedFolderForTheWrongOwner() {
$this->assertException("idMissing", "Db", "ExceptionInput"); $this->assertException("idMissing", "Db", "ExceptionInput");
Arsse::$db->folderAdd("john.doe@example.com", ['name' => "Sociology", 'parent' => 4]); // folder ID 4 belongs to Jane 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); Arsse::$db->folderPropertiesGet("john.doe@example.com", 1);
} }
public function testMakeNoChangesToAFolder() {
$this->assertFalse(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, []));
}
public function testRenameAFolder() { public function testRenameAFolder() {
$this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['name' => "Opinion"])); $this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['name' => "Opinion"]));
Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderPropertiesSet"); 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' => " "])); $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() { public function testMoveAFolder() {
$this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['parent' => 5])); $this->assertTrue(Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['parent' => 5]));
Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderPropertiesSet"); Phake::verify(Arsse::$user)->authorize("john.doe@example.com", "folderPropertiesSet");
@ -242,6 +256,11 @@ trait SeriesFolder {
$this->compareExpectations($state); $this->compareExpectations($state);
} }
public function testMoveTheRootFolder() {
$this->assertException("circularDependence", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesSet("john.doe@example.com", 0, ['parent' => 1]);
}
public function testMoveAFolderToItsDescendant() { public function testMoveAFolderToItsDescendant() {
$this->assertException("circularDependence", "Db", "ExceptionInput"); $this->assertException("circularDependence", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesSet("john.doe@example.com", 1, ['parent' => 3]); 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]); 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() { public function testCauseAFolderCollision() {
$this->assertException("constraintViolation", "Db", "ExceptionInput"); $this->assertException("constraintViolation", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesSet("john.doe@example.com", 6, ['parent' => null]); 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() { public function testSetThePropertiesOfAMissingFolder() {
$this->assertException("subjectMissing", "Db", "ExceptionInput"); $this->assertException("subjectMissing", "Db", "ExceptionInput");
Arsse::$db->folderPropertiesSet("john.doe@example.com", 2112, ['parent' => null]); 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])); $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() { public function testSetThePropertiesOfAMissingSubscription() {
$this->assertException("subjectMissing", "Db", "ExceptionInput"); $this->assertException("subjectMissing", "Db", "ExceptionInput");
Arsse::$db->subscriptionPropertiesSet($this->user, 2112, ['folder' => null]); Arsse::$db->subscriptionPropertiesSet($this->user, 2112, ['folder' => null]);