- 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
// 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 {
@ -303,70 +286,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
// 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();
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 {
// if we're moving a folder to a new parent, check that the parent is valid
return $f;
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(
protected function folderValidateMove(string $user, int $id = null, $parent = null, string $name = null) {
"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) ".
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);
}
}
return $f;
// 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);
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;
}
}
@ -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
// 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
@ -467,18 +490,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