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

Remove use of SQLite IS operator; fixes #120

This commit is contained in:
J. King 2017-12-06 22:26:06 -05:00
parent 84bd624e94
commit 8d0dd15c8a

View file

@ -100,11 +100,11 @@ class Database {
} }
public function metaGet(string $key) { public function metaGet(string $key) {
return $this->db->prepare("SELECT value from arsse_meta where key is ?", "str")->run($key)->getValue(); return $this->db->prepare("SELECT value from arsse_meta where key = ?", "str")->run($key)->getValue();
} }
public function metaSet(string $key, $value, string $type = "str"): bool { public function metaSet(string $key, $value, string $type = "str"): bool {
$out = $this->db->prepare("UPDATE arsse_meta set value = ? where key is ?", $type, "str")->run($value, $key)->changes(); $out = $this->db->prepare("UPDATE arsse_meta set value = ? where key = ?", $type, "str")->run($value, $key)->changes();
if (!$out) { if (!$out) {
$out = $this->db->prepare("INSERT INTO arsse_meta(key,value) values(?,?)", "str", $type)->run($key, $value)->changes(); $out = $this->db->prepare("INSERT INTO arsse_meta(key,value) values(?,?)", "str", $type)->run($key, $value)->changes();
} }
@ -112,14 +112,14 @@ class Database {
} }
public function metaRemove(string $key): bool { public function metaRemove(string $key): bool {
return (bool) $this->db->prepare("DELETE from arsse_meta where key is ?", "str")->run($key)->changes(); return (bool) $this->db->prepare("DELETE from arsse_meta where key = ?", "str")->run($key)->changes();
} }
public function userExists(string $user): bool { public function userExists(string $user): bool {
if (!Arsse::$user->authorize($user, __FUNCTION__)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
return (bool) $this->db->prepare("SELECT count(*) from arsse_users where id is ?", "str")->run($user)->getValue(); return (bool) $this->db->prepare("SELECT count(*) from arsse_users where id = ?", "str")->run($user)->getValue();
} }
public function userAdd(string $user, string $password = null): string { public function userAdd(string $user, string $password = null): string {
@ -143,7 +143,7 @@ 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 ($this->db->prepare("DELETE from arsse_users where id is ?", "str")->run($user)->changes() < 1) { if ($this->db->prepare("DELETE from arsse_users where id = ?", "str")->run($user)->changes() < 1) {
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
} }
return true; return true;
@ -177,7 +177,7 @@ class Database {
} elseif (!$this->userExists($user)) { } elseif (!$this->userExists($user)) {
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
} }
return (string) $this->db->prepare("SELECT password from arsse_users where id is ?", "str")->run($user)->getValue(); return (string) $this->db->prepare("SELECT password from arsse_users where id = ?", "str")->run($user)->getValue();
} }
public function userPasswordSet(string $user, string $password = null): string { public function userPasswordSet(string $user, string $password = null): string {
@ -193,7 +193,7 @@ class Database {
if (strlen($password) > 0) { if (strlen($password) > 0) {
$hash = password_hash($password, \PASSWORD_DEFAULT); $hash = password_hash($password, \PASSWORD_DEFAULT);
} }
$this->db->prepare("UPDATE arsse_users set password = ? where id is ?", "str", "str")->run($hash, $user); $this->db->prepare("UPDATE arsse_users set password = ? where id = ?", "str", "str")->run($hash, $user);
return $password; return $password;
} }
@ -201,7 +201,7 @@ 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]);
} }
$prop = $this->db->prepare("SELECT name,rights from arsse_users where id is ?", "str")->run($user)->getRow(); $prop = $this->db->prepare("SELECT name,rights from arsse_users where id = ?", "str")->run($user)->getRow();
if (!$prop) { if (!$prop) {
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
} }
@ -222,7 +222,7 @@ class Database {
// if no changes would actually be applied, just return // if no changes would actually be applied, just return
return $this->userPropertiesGet($user); return $this->userPropertiesGet($user);
} }
$this->db->prepare("UPDATE arsse_users set $setClause where id is ?", $setTypes, "str")->run($setValues, $user); $this->db->prepare("UPDATE arsse_users set $setClause where id = ?", $setTypes, "str")->run($setValues, $user);
return $this->userPropertiesGet($user); return $this->userPropertiesGet($user);
} }
@ -230,7 +230,7 @@ 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]);
} }
return (int) $this->db->prepare("SELECT rights from arsse_users where id is ?", "str")->run($user)->getValue(); return (int) $this->db->prepare("SELECT rights from arsse_users where id = ?", "str")->run($user)->getValue();
} }
public function userRightsSet(string $user, int $rights): bool { public function userRightsSet(string $user, int $rights): bool {
@ -239,7 +239,7 @@ class Database {
} elseif (!$this->userExists($user)) { } elseif (!$this->userExists($user)) {
throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
} }
$this->db->prepare("UPDATE arsse_users set rights = ? where id is ?", "int", "str")->run($rights, $user); $this->db->prepare("UPDATE arsse_users set rights = ? where id = ?", "int", "str")->run($rights, $user);
return true; return true;
} }
@ -263,12 +263,12 @@ class Database {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
// delete the session and report success. // delete the session and report success.
return (bool) $this->db->prepare("DELETE FROM arsse_sessions where id is ? and user is ?", "str", "str")->run($id, $user)->changes(); return (bool) $this->db->prepare("DELETE FROM arsse_sessions where id = ? and user = ?", "str", "str")->run($id, $user)->changes();
} }
public function sessionResume(string $id): array { public function sessionResume(string $id): array {
$maxAge = Date::sub(Arsse::$conf->userSessionLifetime); $maxAge = Date::sub(Arsse::$conf->userSessionLifetime);
$out = $this->db->prepare("SELECT id,created,expires,user from arsse_sessions where id is ? and expires > CURRENT_TIMESTAMP and created > ?", "str", "datetime")->run($id, $maxAge)->getRow(); $out = $this->db->prepare("SELECT id,created,expires,user from arsse_sessions where id = ? and expires > CURRENT_TIMESTAMP and created > ?", "str", "datetime")->run($id, $maxAge)->getRow();
// if the session does not exist or is expired, throw an exception // if the session does not exist or is expired, throw an exception
if (!$out) { if (!$out) {
throw new User\ExceptionSession("invalid", $id); throw new User\ExceptionSession("invalid", $id);
@ -276,7 +276,7 @@ class Database {
// if we're more than half-way from the session expiring, renew it // if we're more than half-way from the session expiring, renew it
if ($this->sessionExpiringSoon(Date::normalize($out['expires'], "sql"))) { if ($this->sessionExpiringSoon(Date::normalize($out['expires'], "sql"))) {
$expires = Date::add(Arsse::$conf->userSessionTimeout); $expires = Date::add(Arsse::$conf->userSessionTimeout);
$this->db->prepare("UPDATE arsse_sessions set expires = ? where id is ?", "datetime", "str")->run($expires, $id); $this->db->prepare("UPDATE arsse_sessions set expires = ? where id = ?", "datetime", "str")->run($expires, $id);
} }
return $out; return $out;
} }
@ -319,15 +319,15 @@ class Database {
$q = new Query( $q = new Query(
"SELECT "SELECT
id,name,parent, id,name,parent,
(select count(*) from arsse_folders as parents where parents.parent is arsse_folders.id) as children, (select count(*) from arsse_folders as parents where coalesce(parents.parent,0) = coalesce(arsse_folders.id,0)) as children,
(select count(*) from arsse_subscriptions where folder is arsse_folders.id) as feeds (select count(*) from arsse_subscriptions where coalesce(folder,0) = coalesce(arsse_folders.id,0)) as feeds
FROM arsse_folders" FROM arsse_folders"
); );
if (!$recursive) { if (!$recursive) {
$q->setWhere("owner is ?", "str", $user); $q->setWhere("owner = ?", "str", $user);
$q->setWhere("parent is ?", "int", $parent); $q->setWhere("coalesce(parent,0) = ?", "strict int", $parent);
} else { } else {
$q->setCTE("folders", "SELECT id from arsse_folders where owner is ? and parent is ? union select arsse_folders.id from arsse_folders join folders on arsse_folders.parent=folders.id", ["str", "int"], [$user, $parent]); $q->setCTE("folders", "SELECT id from arsse_folders where owner = ? and coalesce(parent,0) = ? union select arsse_folders.id from arsse_folders join folders on arsse_folders.parent=folders.id", ["str", "strict int"], [$user, $parent]);
$q->setWhere("id in (SELECT id from folders)"); $q->setWhere("id in (SELECT id from folders)");
} }
$q->setOrder("name"); $q->setOrder("name");
@ -341,7 +341,7 @@ class Database {
if (!ValueInfo::id($id)) { if (!ValueInfo::id($id)) {
throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "folder", 'type' => "int > 0"]); throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "folder", 'type' => "int > 0"]);
} }
$changes = $this->db->prepare("DELETE FROM arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->changes(); $changes = $this->db->prepare("DELETE FROM arsse_folders where owner = ? and id = ?", "str", "int")->run($user, $id)->changes();
if (!$changes) { if (!$changes) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]); throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
} }
@ -355,7 +355,7 @@ class Database {
if (!ValueInfo::id($id)) { if (!ValueInfo::id($id)) {
throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "folder", 'type' => "int > 0"]); throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "folder", 'type' => "int > 0"]);
} }
$props = $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->getRow(); $props = $this->db->prepare("SELECT id,name,parent from arsse_folders where owner = ? and id = ?", "str", "int")->run($user, $id)->getRow();
if (!$props) { if (!$props) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]); throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
} }
@ -395,7 +395,7 @@ class Database {
'parent' => "int", 'parent' => "int",
]; ];
list($setClause, $setTypes, $setValues) = $this->generateSet($in, $valid); 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(); return (bool) $this->db->prepare("UPDATE arsse_folders set $setClause, modified = CURRENT_TIMESTAMP where owner = ? and id = ?", $setTypes, "str", "int")->run($setValues, $user, $id)->changes();
} }
protected function folderValidateId(string $user, $id = null, bool $subject = false): array { protected function folderValidateId(string $user, $id = null, bool $subject = false): array {
@ -408,7 +408,7 @@ class Database {
return ['id' => null, 'name' => null, 'parent' => null]; return ['id' => null, 'name' => null, 'parent' => null];
} }
// check whether the folder exists and is owned by the user // check whether the folder exists and is owned by the user
$f = $this->db->prepare("SELECT id,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 = ? and id = ?", "str", "int")->run($user, $id)->getRow();
if (!$f) { if (!$f) {
throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "folder", 'id' => $id]); throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "folder", 'id' => $id]);
} }
@ -442,13 +442,13 @@ class Database {
$p = $this->db->prepare( $p = $this->db->prepare(
"WITH RECURSIVE "WITH RECURSIVE
target as (select ? as user, ? as source, ? as dest, ? as rename), 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) folders as (SELECT id from arsse_folders join target on owner = user and coalesce(parent,0) = source union select arsse_folders.id as id from arsse_folders join folders on arsse_folders.parent=folders.id)
". ".
"SELECT "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, ((select dest from target) is null or exists(select id from arsse_folders join target on owner = user and coalesce(id,0) = coalesce(dest,0))) as extant,
not exists(select id from folders where id is (select dest from target)) as valid, not exists(select id from folders where id = coalesce((select dest from target),0)) 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 not exists(select id from arsse_folders join target on coalesce(parent,0) = coalesce(dest,0) and name = coalesce((select rename from target),(select name from arsse_folders join target on id = source))) as available
", "str", "int", "int", "str" ", "str", "strict int", "int", "str"
)->run($user, $id, $parent, $name)->getRow(); )->run($user, $id, $parent, $name)->getRow();
if (!$p['extant']) { if (!$p['extant']) {
// if the parent doesn't exist or doesn't below to the user, throw an exception // if the parent doesn't exist or doesn't below to the user, throw an exception
@ -475,7 +475,7 @@ class Database {
// make sure that a folder with the same prospective name and parent does not already exist: if the parent is null, // make sure that a folder with the same prospective name and parent does not already exist: if the parent is null,
// SQL will happily accept duplicates (null is not unique), so we must do this check ourselves // SQL will happily accept duplicates (null is not unique), so we must do this check ourselves
$parent = $parent ? $parent : null; $parent = $parent ? $parent : null;
if ($this->db->prepare("SELECT exists(select id from arsse_folders where parent is ? and name is ?)", "int", "str")->run($parent, $name)->getValue()) { if ($this->db->prepare("SELECT exists(select id from arsse_folders where coalesce(parent,0) = ? and name = ?)", "strict int", "str")->run($parent, $name)->getValue()) {
throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => "name"]); throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => "name"]);
} }
return true; return true;
@ -489,7 +489,7 @@ class Database {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
// check to see if the feed exists // check to see if the feed exists
$check = $this->db->prepare("SELECT id from arsse_feeds where url is ? and username is ? and password is ?", "str", "str", "str"); $check = $this->db->prepare("SELECT id from arsse_feeds where url = ? and username = ? and password = ?", "str", "str", "str");
$feedID = $check->run($url, $fetchUser, $fetchPassword)->getValue(); $feedID = $check->run($url, $fetchUser, $fetchPassword)->getValue();
if ($discover && is_null($feedID)) { if ($discover && is_null($feedID)) {
// if the feed doesn't exist, first perform discovery if requested and check for the existence of that URL // if the feed doesn't exist, first perform discovery if requested and check for the existence of that URL
@ -504,7 +504,7 @@ class Database {
$this->feedUpdate($feedID, true); $this->feedUpdate($feedID, true);
} catch (\Throwable $e) { } catch (\Throwable $e) {
// if the update fails, delete the feed we just added // if the update fails, delete the feed we just added
$this->db->prepare('DELETE from arsse_feeds where id is ?', 'int')->run($feedID); $this->db->prepare('DELETE from arsse_feeds where id = ?', 'int')->run($feedID);
throw $e; throw $e;
} }
} }
@ -526,9 +526,9 @@ class Database {
arsse_feeds.updated as updated, arsse_feeds.updated as updated,
topmost.top as top_folder, topmost.top as top_folder,
coalesce(arsse_subscriptions.title, arsse_feeds.title) as title, coalesce(arsse_subscriptions.title, arsse_feeds.title) as title,
(SELECT count(*) from arsse_articles where feed is arsse_subscriptions.feed) - (SELECT count(*) from arsse_marks where subscription is arsse_subscriptions.id and read is 1) as unread (SELECT count(*) from arsse_articles where feed = arsse_subscriptions.feed) - (SELECT count(*) from arsse_marks where subscription = arsse_subscriptions.id and read = 1) as unread
from arsse_subscriptions from arsse_subscriptions
join user on user is owner join user on user = owner
join arsse_feeds on feed = arsse_feeds.id join arsse_feeds on feed = arsse_feeds.id
left join topmost on folder=f_id" left join topmost on folder=f_id"
); );
@ -536,19 +536,19 @@ class Database {
// define common table expressions // define common table expressions
$q->setCTE("user(user)", "SELECT ?", "str", $user); // the subject user; this way we only have to pass it to prepare() once $q->setCTE("user(user)", "SELECT ?", "str", $user); // the subject user; this way we only have to pass it to prepare() once
// topmost folders belonging to the user // topmost folders belonging to the user
$q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders join user on owner is user where parent is null union select id,top from arsse_folders join topmost on parent=f_id"); $q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders join user on owner = user where parent is null union select id,top from arsse_folders join topmost on parent=f_id");
if ($id) { if ($id) {
// this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder // this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder
// if an ID is specified, add a suitable WHERE condition and bindings // if an ID is specified, add a suitable WHERE condition and bindings
$q->setWhere("arsse_subscriptions.id is ?", "int", $id); $q->setWhere("arsse_subscriptions.id = ?", "int", $id);
} elseif ($folder && $recursive) { } elseif ($folder && $recursive) {
// if a folder is specified and we're listing recursively, add a common table expression to list it and its children so that we select from the entire subtree // if a folder is specified and we're listing recursively, add a common table expression to list it and its children so that we select from the entire subtree
$q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent is folder", "int", $folder); $q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent = folder", "int", $folder);
// add a suitable WHERE condition // add a suitable WHERE condition
$q->setWhere("folder in (select folder from folders)"); $q->setWhere("folder in (select folder from folders)");
} elseif (!$recursive) { } elseif (!$recursive) {
// if we're not listing recursively, match against only the specified folder (even if it is null) // if we're not listing recursively, match against only the specified folder (even if it is null)
$q->setWhere("folder is ?", "int", $folder); $q->setWhere("coalesce(folder,0) = ?", "strict int", $folder);
} }
return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());
} }
@ -561,10 +561,10 @@ class Database {
$folder = $this->folderValidateId($user, $folder)['id']; $folder = $this->folderValidateId($user, $folder)['id'];
// create a complex query // create a complex query
$q = new Query("SELECT count(*) from arsse_subscriptions"); $q = new Query("SELECT count(*) from arsse_subscriptions");
$q->setWhere("owner is ?", "str", $user); $q->setWhere("owner = ?", "str", $user);
if ($folder) { if ($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 the specified folder exists, add a common table expression to list it and its children so that we select from the entire subtree
$q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent is folder", "int", $folder); $q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent = folder", "int", $folder);
// add a suitable WHERE condition // add a suitable WHERE condition
$q->setWhere("folder in (select folder from folders)"); $q->setWhere("folder in (select folder from folders)");
} }
@ -578,7 +578,7 @@ class Database {
if (!ValueInfo::id($id)) { if (!ValueInfo::id($id)) {
throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'type' => "int > 0"]); throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'type' => "int > 0"]);
} }
$changes = $this->db->prepare("DELETE from arsse_subscriptions where owner is ? and id is ?", "str", "int")->run($user, $id)->changes(); $changes = $this->db->prepare("DELETE from arsse_subscriptions where owner = ? and id = ?", "str", "int")->run($user, $id)->changes();
if (!$changes) { if (!$changes) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]); throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $id]);
} }
@ -637,20 +637,20 @@ class Database {
// if no changes would actually be applied, just return // if no changes would actually be applied, just return
return false; return false;
} }
$out = (bool) $this->db->prepare("UPDATE arsse_subscriptions set $setClause, modified = CURRENT_TIMESTAMP where owner is ? and id is ?", $setTypes, "str", "int")->run($setValues, $user, $id)->changes(); $out = (bool) $this->db->prepare("UPDATE arsse_subscriptions set $setClause, modified = CURRENT_TIMESTAMP where owner = ? and id = ?", $setTypes, "str", "int")->run($setValues, $user, $id)->changes();
$tr->commit(); $tr->commit();
return $out; return $out;
} }
public function subscriptionFavicon(int $id): string { public function subscriptionFavicon(int $id): string {
return (string) $this->db->prepare("SELECT favicon from arsse_feeds join arsse_subscriptions on feed is arsse_feeds.id where arsse_subscriptions.id is ?", "int")->run($id)->getValue(); return (string) $this->db->prepare("SELECT favicon from arsse_feeds join arsse_subscriptions on feed = arsse_feeds.id where arsse_subscriptions.id = ?", "int")->run($id)->getValue();
} }
protected function subscriptionValidateId(string $user, $id, bool $subject = false): array { protected function subscriptionValidateId(string $user, $id, bool $subject = false): array {
if (!ValueInfo::id($id)) { if (!ValueInfo::id($id)) {
throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "feed", 'type' => "int > 0"]); throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "feed", 'type' => "int > 0"]);
} }
$out = $this->db->prepare("SELECT id,feed from arsse_subscriptions where id is ? and owner is ?", "int", "str")->run($id, $user)->getRow(); $out = $this->db->prepare("SELECT id,feed from arsse_subscriptions where id = ? and owner = ?", "int", "str")->run($id, $user)->getRow();
if (!$out) { if (!$out) {
throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "subscription", 'id' => $id]); throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "subscription", 'id' => $id]);
} }
@ -667,7 +667,7 @@ class Database {
if (!ValueInfo::id($feedID)) { if (!ValueInfo::id($feedID)) {
throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID, 'type' => "int > 0"]); throw new Db\ExceptionInput("typeViolation", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID, 'type' => "int > 0"]);
} }
$f = $this->db->prepare("SELECT url, username, password, modified, etag, err_count, scrape FROM arsse_feeds where id is ?", "int")->run($feedID)->getRow(); $f = $this->db->prepare("SELECT url, username, password, modified, etag, err_count, scrape FROM arsse_feeds where id = ?", "int")->run($feedID)->getRow();
if (!$f) { if (!$f) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID]); throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID]);
} }
@ -680,13 +680,13 @@ class Database {
$feed = new Feed((int) $feedID, $f['url'], (string) Date::transform($f['modified'], "http", "sql"), $f['etag'], $f['username'], $f['password'], $scrape); $feed = new Feed((int) $feedID, $f['url'], (string) Date::transform($f['modified'], "http", "sql"), $f['etag'], $f['username'], $f['password'], $scrape);
if (!$feed->modified) { if (!$feed->modified) {
// if the feed hasn't changed, just compute the next fetch time and record it // if the feed hasn't changed, just compute the next fetch time and record it
$this->db->prepare("UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ? WHERE id is ?", 'datetime', 'int')->run($feed->nextFetch, $feedID); $this->db->prepare("UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ? WHERE id = ?", 'datetime', 'int')->run($feed->nextFetch, $feedID);
return false; return false;
} }
} catch (Feed\Exception $e) { } catch (Feed\Exception $e) {
// update the database with the resultant error and the next fetch time, incrementing the error count // update the database with the resultant error and the next fetch time, incrementing the error count
$this->db->prepare( $this->db->prepare(
"UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ?, err_count = err_count + 1, err_msg = ? WHERE id is ?", "UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ?, err_count = err_count + 1, err_msg = ? WHERE id = ?",
'datetime', 'str', 'int' 'datetime', 'str', 'int'
)->run(Feed::nextFetchOnError($f['err_count']), $e->getMessage(), $feedID); )->run(Feed::nextFetchOnError($f['err_count']), $e->getMessage(), $feedID);
if ($throwError) { if ($throwError) {
@ -707,11 +707,11 @@ class Database {
); );
} }
if (sizeof($feed->changedItems)) { if (sizeof($feed->changedItems)) {
$qDeleteEnclosures = $this->db->prepare("DELETE FROM arsse_enclosures WHERE article is ?", 'int'); $qDeleteEnclosures = $this->db->prepare("DELETE FROM arsse_enclosures WHERE article = ?", 'int');
$qDeleteCategories = $this->db->prepare("DELETE FROM arsse_categories WHERE article is ?", 'int'); $qDeleteCategories = $this->db->prepare("DELETE FROM arsse_categories WHERE article = ?", 'int');
$qClearReadMarks = $this->db->prepare("UPDATE arsse_marks SET read = 0, modified = CURRENT_TIMESTAMP WHERE article is ? and read is 1", 'int'); $qClearReadMarks = $this->db->prepare("UPDATE arsse_marks SET read = 0, modified = CURRENT_TIMESTAMP WHERE article = ? and read = 1", 'int');
$qUpdateArticle = $this->db->prepare( $qUpdateArticle = $this->db->prepare(
"UPDATE arsse_articles SET url = ?, title = ?, author = ?, published = ?, edited = ?, modified = CURRENT_TIMESTAMP, guid = ?, content = ?, url_title_hash = ?, url_content_hash = ?, title_content_hash = ? WHERE id is ?", "UPDATE arsse_articles SET url = ?, title = ?, author = ?, published = ?, edited = ?, modified = CURRENT_TIMESTAMP, guid = ?, content = ?, url_title_hash = ?, url_content_hash = ?, title_content_hash = ? WHERE id = ?",
'str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str', 'int' 'str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str', 'int'
); );
} }
@ -766,7 +766,7 @@ class Database {
} }
// lastly update the feed database itself with updated information. // lastly update the feed database itself with updated information.
$this->db->prepare( $this->db->prepare(
"UPDATE arsse_feeds SET url = ?, title = ?, favicon = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ? WHERE id is ?", "UPDATE arsse_feeds SET url = ?, title = ?, favicon = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ? WHERE id = ?",
'str', 'str', 'str', 'str', 'datetime', 'str', 'datetime', 'int', 'int' 'str', 'str', 'str', 'str', 'datetime', 'str', 'datetime', 'int', 'int'
)->run( )->run(
$feed->data->feedUrl, $feed->data->feedUrl,
@ -786,9 +786,9 @@ class Database {
public function feedCleanup(): bool { public function feedCleanup(): bool {
$tr = $this->begin(); $tr = $this->begin();
// first unmark any feeds which are no longer orphaned // first unmark any feeds which are no longer orphaned
$this->db->query("UPDATE arsse_feeds set orphaned = null where exists(SELECT id from arsse_subscriptions where feed is arsse_feeds.id)"); $this->db->query("UPDATE arsse_feeds set orphaned = null where exists(SELECT id from arsse_subscriptions where feed = arsse_feeds.id)");
// next mark any newly orphaned feeds with the current date and time // next mark any newly orphaned feeds with the current date and time
$this->db->query("UPDATE arsse_feeds set orphaned = CURRENT_TIMESTAMP where orphaned is null and not exists(SELECT id from arsse_subscriptions where feed is arsse_feeds.id)"); $this->db->query("UPDATE arsse_feeds set orphaned = CURRENT_TIMESTAMP where orphaned is null and not exists(SELECT id from arsse_subscriptions where feed = arsse_feeds.id)");
// finally delete feeds that have been orphaned longer than the retention period // finally delete feeds that have been orphaned longer than the retention period
$limit = Date::normalize("now"); $limit = Date::normalize("now");
if (Arsse::$conf->purgeFeeds) { if (Arsse::$conf->purgeFeeds) {
@ -803,7 +803,7 @@ class Database {
public function feedMatchLatest(int $feedID, int $count): Db\Result { public function feedMatchLatest(int $feedID, int $count): Db\Result {
return $this->db->prepare( return $this->db->prepare(
"SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? ORDER BY modified desc, id desc limit ?", "SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed = ? ORDER BY modified desc, id desc limit ?",
'int', 'int' 'int', 'int'
)->run($feedID, $count); )->run($feedID, $count);
} }
@ -816,7 +816,7 @@ class Database {
list($cHashTC, $tHashTC) = $this->generateIn($hashesTC, "str"); list($cHashTC, $tHashTC) = $this->generateIn($hashesTC, "str");
// perform the query // perform the query
return $articles = $this->db->prepare( return $articles = $this->db->prepare(
"SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? and (guid in($cId) or url_title_hash in($cHashUT) or url_content_hash in($cHashUC) or title_content_hash in($cHashTC))", "SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed = ? and (guid in($cId) or url_title_hash in($cHashUT) or url_content_hash in($cHashUC) or title_content_hash in($cHashTC))",
'int', $tId, $tHashUT, $tHashUC, $tHashTC 'int', $tId, $tHashUT, $tHashUC, $tHashTC
)->run($feedID, $ids, $hashesUT, $hashesUC, $hashesTC); )->run($feedID, $ids, $hashesUT, $hashesUC, $hashesTC);
} }
@ -834,12 +834,12 @@ class Database {
arsse_articles.modified as modified_date, arsse_articles.modified as modified_date,
max( max(
arsse_articles.modified, arsse_articles.modified,
coalesce((select modified from arsse_marks where article is arsse_articles.id and subscription in (select sub from subscribed_feeds)),''), coalesce((select modified from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),''),
coalesce((select modified from arsse_label_members where article is arsse_articles.id and subscription in (select sub from subscribed_feeds)),'') coalesce((select modified from arsse_label_members where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),'')
) as marked_date, ) as marked_date,
NOT (select count(*) from arsse_marks where article is arsse_articles.id and read is 1 and subscription in (select sub from subscribed_feeds)) as unread, NOT (select count(*) from arsse_marks where article = arsse_articles.id and read = 1 and subscription in (select sub from subscribed_feeds)) as unread,
(select count(*) from arsse_marks where article is arsse_articles.id and starred is 1 and subscription in (select sub from subscribed_feeds)) as starred, (select count(*) from arsse_marks where article = arsse_articles.id and starred = 1 and subscription in (select sub from subscribed_feeds)) as starred,
(select max(id) from arsse_editions where article is arsse_articles.id) as edition, (select max(id) from arsse_editions where article = arsse_articles.id) as edition,
subscribed_feeds.sub as subscription subscribed_feeds.sub as subscription
FROM arsse_articles" FROM arsse_articles"
); );
@ -849,29 +849,29 @@ class Database {
// if a subscription is specified, make sure it exists // if a subscription is specified, make sure it exists
$id = $this->subscriptionValidateId($user, $context->subscription)['feed']; $id = $this->subscriptionValidateId($user, $context->subscription)['feed'];
// add a basic CTE that will join in only the requested subscription // add a basic CTE that will join in only the requested subscription
$q->setCTE("subscribed_feeds(id,sub)", "SELECT ?,?", ["int","int"], [$id,$context->subscription], "join subscribed_feeds on feed is subscribed_feeds.id"); $q->setCTE("subscribed_feeds(id,sub)", "SELECT ?,?", ["int","int"], [$id,$context->subscription], "join subscribed_feeds on feed = subscribed_feeds.id");
} elseif ($context->folder()) { } elseif ($context->folder()) {
// if a folder is specified, make sure it exists // if a folder is specified, make sure it exists
$this->folderValidateId($user, $context->folder); $this->folderValidateId($user, $context->folder);
// if it does exist, add a common table expression to list it and its children so that we select from the entire subtree // if it does exist, add a common table expression to list it and its children so that we select from the entire subtree
$q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent is folder", "int", $context->folder); $q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent = folder", "int", $context->folder);
// add another CTE for the subscriptions within the folder // add another CTE for the subscriptions within the folder
$q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user is owner join folders on arsse_subscriptions.folder is folders.folder", [], [], "join subscribed_feeds on feed is subscribed_feeds.id"); $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user = owner join folders on arsse_subscriptions.folder = folders.folder", [], [], "join subscribed_feeds on feed = subscribed_feeds.id");
} elseif ($context->folderShallow()) { } elseif ($context->folderShallow()) {
// if a shallow folder is specified, make sure it exists // if a shallow folder is specified, make sure it exists
$this->folderValidateId($user, $context->folderShallow); $this->folderValidateId($user, $context->folderShallow);
// if it does exist, add a CTE with only its subscriptions (and not those of its descendents) // if it does exist, add a CTE with only its subscriptions (and not those of its descendents)
$q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user is owner and coalesce(folder,0) is ?", "strict int", $context->folderShallow, "join subscribed_feeds on feed is subscribed_feeds.id"); $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user = owner and coalesce(folder,0) = ?", "strict int", $context->folderShallow, "join subscribed_feeds on feed = subscribed_feeds.id");
} else { } else {
// otherwise add a CTE for all the user's subscriptions // otherwise add a CTE for all the user's subscriptions
$q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user is owner", [], [], "join subscribed_feeds on feed is subscribed_feeds.id"); $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user = owner", [], [], "join subscribed_feeds on feed = subscribed_feeds.id");
} }
if ($context->edition()) { if ($context->edition()) {
// if an edition is specified, filter for its previously identified article // if an edition is specified, filter for its previously identified article
$q->setWhere("arsse_articles.id is (select article from arsse_editions where id is ?)", "int", $context->edition); $q->setWhere("arsse_articles.id = (select article from arsse_editions where id = ?)", "int", $context->edition);
} elseif ($context->article()) { } elseif ($context->article()) {
// if an article is specified, filter for it (it has already been validated above) // if an article is specified, filter for it (it has already been validated above)
$q->setWhere("arsse_articles.id is ?", "int", $context->article); $q->setWhere("arsse_articles.id = ?", "int", $context->article);
} }
if ($context->editions()) { if ($context->editions()) {
// if multiple specific editions have been requested, prepare a CTE to list them and their articles // if multiple specific editions have been requested, prepare a CTE to list them and their articles
@ -896,19 +896,19 @@ class Database {
} }
list($inParams, $inTypes) = $this->generateIn($context->articles, "int"); list($inParams, $inTypes) = $this->generateIn($context->articles, "int");
$q->setCTE("requested_articles(id,edition)", $q->setCTE("requested_articles(id,edition)",
"SELECT id,(select max(id) from arsse_editions where article is arsse_articles.id) as edition from arsse_articles where arsse_articles.id in ($inParams)", "SELECT id,(select max(id) from arsse_editions where article = arsse_articles.id) as edition from arsse_articles where arsse_articles.id in ($inParams)",
$inTypes, $inTypes,
$context->articles $context->articles
); );
$q->setWhere("arsse_articles.id in (select id from requested_articles)"); $q->setWhere("arsse_articles.id in (select id from requested_articles)");
} else { } else {
// if neither list is specified, mock an empty table // if neither list is specified, mock an empty table
$q->setCTE("requested_articles(id,edition)", "SELECT 'empty','table' where 1 is 0"); $q->setCTE("requested_articles(id,edition)", "SELECT 'empty','table' where 1 = 0");
} }
// filter based on label by ID or name // filter based on label by ID or name
if ($context->labelled()) { if ($context->labelled()) {
// any label (true) or no label (false) // any label (true) or no label (false)
$q->setWhere((!$context->labelled ? "not " : "")."exists(select article from arsse_label_members where assigned is 1 and article is arsse_articles.id and subscription in (select sub from subscribed_feeds))"); $q->setWhere((!$context->labelled ? "not " : "")."exists(select article from arsse_label_members where assigned = 1 and article = arsse_articles.id and subscription in (select sub from subscribed_feeds))");
} elseif ($context->label() || $context->labelName()) { } elseif ($context->label() || $context->labelName()) {
// specific label ID or name // specific label ID or name
if ($context->label()) { if ($context->label()) {
@ -916,7 +916,7 @@ class Database {
} else { } else {
$id = $this->labelValidateId($user, $context->labelName, true)['id']; $id = $this->labelValidateId($user, $context->labelName, true)['id'];
} }
$q->setWhere("exists(select article from arsse_label_members where assigned is 1 and article is arsse_articles.id and label is ?)", "int", $id); $q->setWhere("exists(select article from arsse_label_members where assigned = 1 and article = arsse_articles.id and label = ?)", "int", $id);
} }
// filter based on article or edition offset // filter based on article or edition offset
if ($context->oldestArticle()) { if ($context->oldestArticle()) {
@ -946,14 +946,14 @@ class Database {
} }
// filter for un/read and un/starred status if specified // filter for un/read and un/starred status if specified
if ($context->unread()) { if ($context->unread()) {
$q->setWhere("unread is ?", "bool", $context->unread); $q->setWhere("unread = ?", "bool", $context->unread);
} }
if ($context->starred()) { if ($context->starred()) {
$q->setWhere("starred is ?", "bool", $context->starred); $q->setWhere("starred = ?", "bool", $context->starred);
} }
// filter based on whether the article has a note // filter based on whether the article has a note
if ($context->annotated()) { if ($context->annotated()) {
$q->setWhere((!$context->annotated ? "not " : "")."exists(select modified from arsse_marks where article is arsse_articles.id and note <> '' and subscription in (select sub from subscribed_feeds))"); $q->setWhere((!$context->annotated ? "not " : "")."exists(select modified from arsse_marks where article = arsse_articles.id and note <> '' and subscription in (select sub from subscribed_feeds))");
} }
// return the query // return the query
return $q; return $q;
@ -1003,7 +1003,7 @@ class Database {
// NOTE: the cases all cascade into each other: a given verbosity level is always a superset of the previous one // NOTE: the cases all cascade into each other: a given verbosity level is always a superset of the previous one
case self::LIST_FULL: // everything case self::LIST_FULL: // everything
$columns = array_merge($columns, [ $columns = array_merge($columns, [
"(select note from arsse_marks where article is arsse_articles.id and subscription in (select sub from subscribed_feeds)) as note", "(select note from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)) as note",
]); ]);
case self::LIST_TYPICAL: // conservative, plus content case self::LIST_TYPICAL: // conservative, plus content
$columns = array_merge($columns, [ $columns = array_merge($columns, [
@ -1015,7 +1015,7 @@ class Database {
$columns = array_merge($columns, [ $columns = array_merge($columns, [
"arsse_articles.url as url", "arsse_articles.url as url",
"arsse_articles.title as title", "arsse_articles.title as title",
"(select coalesce(arsse_subscriptions.title,arsse_feeds.title) from arsse_feeds join arsse_subscriptions on arsse_subscriptions.feed is arsse_feeds.id where arsse_feeds.id is arsse_articles.feed) as subscription_title", "(select coalesce(arsse_subscriptions.title,arsse_feeds.title) from arsse_feeds join arsse_subscriptions on arsse_subscriptions.feed = arsse_feeds.id where arsse_feeds.id = arsse_articles.feed) as subscription_title",
"author", "author",
"guid", "guid",
"published as published_date", "published as published_date",
@ -1034,7 +1034,7 @@ class Database {
$q = $this->articleQuery($user, $context, $columns); $q = $this->articleQuery($user, $context, $columns);
$q->setOrder("edited_date".($context->reverse ? " desc" : "")); $q->setOrder("edited_date".($context->reverse ? " desc" : ""));
$q->setOrder("edition".($context->reverse ? " desc" : "")); $q->setOrder("edition".($context->reverse ? " desc" : ""));
$q->setJoin("left join arsse_enclosures on arsse_enclosures.article is arsse_articles.id"); $q->setJoin("left join arsse_enclosures on arsse_enclosures.article = arsse_articles.id");
// perform the query and return results // perform the query and return results
return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());
} }
@ -1087,21 +1087,21 @@ class Database {
$queries = [ $queries = [
"UPDATE arsse_marks "UPDATE arsse_marks
set set
read = case when (select honour_read from target_articles where target_articles.id is article) is 1 then (select read from target_values) else read end, read = case when (select honour_read from target_articles where target_articles.id = article) = 1 then (select read from target_values) else read end,
starred = coalesce((select starred from target_values),starred), starred = coalesce((select starred from target_values),starred),
note = coalesce((select note from target_values),note), note = coalesce((select note from target_values),note),
modified = CURRENT_TIMESTAMP modified = CURRENT_TIMESTAMP
WHERE WHERE
subscription in (select sub from subscribed_feeds) subscription in (select sub from subscribed_feeds)
and article in (select id from target_articles where to_insert is 0 and (honour_read is 1 or honour_star is 1 or (select note from target_values) is not null))", and article in (select id from target_articles where to_insert = 0 and (honour_read = 1 or honour_star = 1 or (select note from target_values) is not null))",
"INSERT INTO arsse_marks(subscription,article,read,starred,note) "INSERT INTO arsse_marks(subscription,article,read,starred,note)
select select
(select id from arsse_subscriptions join user on user is owner where arsse_subscriptions.feed is target_articles.feed), (select id from arsse_subscriptions join user on user = owner where arsse_subscriptions.feed = target_articles.feed),
id, id,
coalesce((select read from target_values) * honour_read,0), coalesce((select read from target_values) * honour_read,0),
coalesce((select starred from target_values),0), coalesce((select starred from target_values),0),
coalesce((select note from target_values),'') coalesce((select note from target_values),'')
from target_articles where to_insert is 1 and (honour_read is 1 or honour_star is 1 or coalesce((select note from target_values),'') <> '')" from target_articles where to_insert = 1 and (honour_read = 1 or honour_star = 1 or coalesce((select note from target_values),'') <> '')"
]; ];
$out = 0; $out = 0;
// wrap this UPDATE and INSERT together into a transaction // wrap this UPDATE and INSERT together into a transaction
@ -1122,9 +1122,9 @@ class Database {
foreach ($queries as $query) { foreach ($queries as $query) {
// first build the query which will select the target articles; we will later turn this into a CTE for the actual query that manipulates the articles // first build the query which will select the target articles; we will later turn this into a CTE for the actual query that manipulates the articles
$q = $this->articleQuery($user, $context, [ $q = $this->articleQuery($user, $context, [
"(not exists(select article from arsse_marks where article is arsse_articles.id and subscription in (select sub from subscribed_feeds))) as to_insert", "(not exists(select article from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds))) as to_insert",
"((select read from target_values) is not null and (select read from target_values) is not (coalesce((select read from arsse_marks where article is arsse_articles.id and subscription in (select sub from subscribed_feeds)),0)) and (not exists(select * from requested_articles) or (select max(id) from arsse_editions where article is arsse_articles.id) in (select edition from requested_articles))) as honour_read", "((select read from target_values) is not null and (select read from target_values) <> (coalesce((select read from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),0)) and (not exists(select * from requested_articles) or (select max(id) from arsse_editions where article = arsse_articles.id) in (select edition from requested_articles))) as honour_read",
"((select starred from target_values) is not null and (select starred from target_values) is not (coalesce((select starred from arsse_marks where article is arsse_articles.id and subscription in (select sub from subscribed_feeds)),0))) as honour_star", "((select starred from target_values) is not null and (select starred from target_values) <> (coalesce((select starred from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),0))) as honour_star",
]); ]);
// common table expression with the values to set // common table expression with the values to set
$q->setCTE("target_values(read,starred,note)", "SELECT ?,?,?", ["bool","bool","str"], $values); $q->setCTE("target_values(read,starred,note)", "SELECT ?,?,?", ["bool","bool","str"], $values);
@ -1149,7 +1149,7 @@ class Database {
coalesce(sum(not read),0) as unread, coalesce(sum(not read),0) as unread,
coalesce(sum(read),0) as read coalesce(sum(read),0) as read
FROM ( FROM (
select read from arsse_marks where starred is 1 and subscription in (select id from arsse_subscriptions where owner is ?) select read from arsse_marks where starred = 1 and subscription in (select id from arsse_subscriptions where owner = ?)
)", "str" )", "str"
)->run($user)->getRow(); )->run($user)->getRow();
} }
@ -1159,7 +1159,7 @@ class Database {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
$id = $this->articleValidateId($user, $id)['article']; $id = $this->articleValidateId($user, $id)['article'];
$out = $this->db->prepare("SELECT id,name from arsse_labels where owner is ? and exists(select id from arsse_label_members where article is ? and label is arsse_labels.id and assigned is 1)", "str", "int")->run($user, $id)->getAll(); $out = $this->db->prepare("SELECT id,name from arsse_labels where owner = ? and exists(select id from arsse_label_members where article = ? and label = arsse_labels.id and assigned = 1)", "str", "int")->run($user, $id)->getAll();
if (!$out) { if (!$out) {
return $out; return $out;
} else { } else {
@ -1173,7 +1173,7 @@ class Database {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
$id = $this->articleValidateId($user, $id)['article']; $id = $this->articleValidateId($user, $id)['article'];
$out = $this->db->prepare("SELECT name from arsse_categories where article is ? order by name", "int")->run($id)->getAll(); $out = $this->db->prepare("SELECT name from arsse_categories where article = ? order by name", "int")->run($id)->getAll();
if (!$out) { if (!$out) {
return $out; return $out;
} else { } else {
@ -1186,22 +1186,22 @@ class Database {
$query = $this->db->prepare( $query = $this->db->prepare(
"WITH target_feed(id,subs) as (". "WITH target_feed(id,subs) as (".
"SELECT "SELECT
id, (select count(*) from arsse_subscriptions where feed is arsse_feeds.id) as subs id, (select count(*) from arsse_subscriptions where feed = arsse_feeds.id) as subs
from arsse_feeds where id is ?". from arsse_feeds where id = ?".
"), excepted_articles(id,edition) as (". "), excepted_articles(id,edition) as (".
"SELECT "SELECT
arsse_articles.id, (select max(id) from arsse_editions where article is arsse_articles.id) as edition arsse_articles.id, (select max(id) from arsse_editions where article = arsse_articles.id) as edition
from arsse_articles from arsse_articles
join target_feed on arsse_articles.feed is target_feed.id join target_feed on arsse_articles.feed = target_feed.id
order by edition desc limit ?". order by edition desc limit ?".
") ". ") ".
"DELETE from arsse_articles where "DELETE from arsse_articles where
feed is (select max(id) from target_feed) feed = (select max(id) from target_feed)
and id not in (select id from excepted_articles) and id not in (select id from excepted_articles)
and (select count(*) from arsse_marks where article is arsse_articles.id and starred is 1) is 0 and (select count(*) from arsse_marks where article = arsse_articles.id and starred = 1) = 0
and ( and (
coalesce((select max(modified) from arsse_marks where article is arsse_articles.id),modified) <= ? coalesce((select max(modified) from arsse_marks where article = arsse_articles.id),modified) <= ?
or ((select max(subs) from target_feed) is (select count(*) from arsse_marks where article is arsse_articles.id and read is 1) and coalesce((select max(modified) from arsse_marks where article is arsse_articles.id),modified) <= ?) or ((select max(subs) from target_feed) = (select count(*) from arsse_marks where article = arsse_articles.id and read = 1) and coalesce((select max(modified) from arsse_marks where article = arsse_articles.id),modified) <= ?)
) )
", "int", "int", "datetime", "datetime" ", "int", "int", "datetime", "datetime"
); );
@ -1227,12 +1227,12 @@ class Database {
$out = $this->db->prepare( $out = $this->db->prepare(
"SELECT "SELECT
arsse_articles.id as article, arsse_articles.id as article,
(select max(id) from arsse_editions where article is arsse_articles.id) as edition (select max(id) from arsse_editions where article = arsse_articles.id) as edition
FROM arsse_articles FROM arsse_articles
join arsse_feeds on arsse_feeds.id is arsse_articles.feed join arsse_feeds on arsse_feeds.id = arsse_articles.feed
join arsse_subscriptions on arsse_subscriptions.feed is arsse_feeds.id join arsse_subscriptions on arsse_subscriptions.feed = arsse_feeds.id
WHERE WHERE
arsse_articles.id is ? and arsse_subscriptions.owner is ?", arsse_articles.id = ? and arsse_subscriptions.owner = ?",
"int", "str" "int", "str"
)->run($id, $user)->getRow(); )->run($id, $user)->getRow();
if (!$out) { if (!$out) {
@ -1249,13 +1249,13 @@ class Database {
"SELECT "SELECT
arsse_editions.id as edition, arsse_editions.id as edition,
arsse_editions.article as article, arsse_editions.article as article,
(arsse_editions.id is (select max(id) from arsse_editions where article is arsse_editions.article)) as current (arsse_editions.id = (select max(id) from arsse_editions where article = arsse_editions.article)) as current
FROM arsse_editions FROM arsse_editions
join arsse_articles on arsse_editions.article is arsse_articles.id join arsse_articles on arsse_editions.article = arsse_articles.id
join arsse_feeds on arsse_feeds.id is arsse_articles.feed join arsse_feeds on arsse_feeds.id = arsse_articles.feed
join arsse_subscriptions on arsse_subscriptions.feed is arsse_feeds.id join arsse_subscriptions on arsse_subscriptions.feed = arsse_feeds.id
WHERE WHERE
edition is ? and arsse_subscriptions.owner is ?", edition = ? and arsse_subscriptions.owner = ?",
"int", "str" "int", "str"
)->run($id, $user)->getRow(); )->run($id, $user)->getRow();
if (!$out) { if (!$out) {
@ -1269,15 +1269,15 @@ class Database {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
$context = $context ?? new Context; $context = $context ?? new Context;
$q = new Query("SELECT max(arsse_editions.id) from arsse_editions left join arsse_articles on article is arsse_articles.id left join arsse_feeds on arsse_articles.feed is arsse_feeds.id"); $q = new Query("SELECT max(arsse_editions.id) from arsse_editions left join arsse_articles on article = arsse_articles.id left join arsse_feeds on arsse_articles.feed = arsse_feeds.id");
if ($context->subscription()) { if ($context->subscription()) {
// if a subscription is specified, make sure it exists // if a subscription is specified, make sure it exists
$id = $this->subscriptionValidateId($user, $context->subscription)['feed']; $id = $this->subscriptionValidateId($user, $context->subscription)['feed'];
// a simple WHERE clause is required here // a simple WHERE clause is required here
$q->setWhere("arsse_feeds.id is ?", "int", $id); $q->setWhere("arsse_feeds.id = ?", "int", $id);
} else { } else {
$q->setCTE("user(user)", "SELECT ?", "str", $user); $q->setCTE("user(user)", "SELECT ?", "str", $user);
$q->setCTE("feeds(feed)", "SELECT feed from arsse_subscriptions join user on user is owner", [], [], "join feeds on arsse_articles.feed is feeds.feed"); $q->setCTE("feeds(feed)", "SELECT feed from arsse_subscriptions join user on user = owner", [], [], "join feeds on arsse_articles.feed = feeds.feed");
} }
return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue();
} }
@ -1302,12 +1302,12 @@ class Database {
return $this->db->prepare( return $this->db->prepare(
"SELECT "SELECT
id,name, id,name,
(select count(*) from arsse_label_members where label is id and assigned is 1) as articles, (select count(*) from arsse_label_members where label = id and assigned = 1) as articles,
(select count(*) from arsse_label_members (select count(*) from arsse_label_members
join arsse_marks on arsse_label_members.article is arsse_marks.article and arsse_label_members.subscription is arsse_marks.subscription join arsse_marks on arsse_label_members.article = arsse_marks.article and arsse_label_members.subscription = arsse_marks.subscription
where label is id and assigned is 1 and read is 1 where label = id and assigned = 1 and read = 1
) as read ) as read
FROM arsse_labels where owner is ? and articles >= ? order by name FROM arsse_labels where owner = ? and articles >= ? order by name
", "str", "int" ", "str", "int"
)->run($user, !$includeEmpty); )->run($user, !$includeEmpty);
} }
@ -1319,7 +1319,7 @@ class Database {
$this->labelValidateId($user, $id, $byName, false); $this->labelValidateId($user, $id, $byName, false);
$field = $byName ? "name" : "id"; $field = $byName ? "name" : "id";
$type = $byName ? "str" : "int"; $type = $byName ? "str" : "int";
$changes = $this->db->prepare("DELETE FROM arsse_labels where owner is ? and $field is ?", "str", $type)->run($user, $id)->changes(); $changes = $this->db->prepare("DELETE FROM arsse_labels where owner = ? and $field = ?", "str", $type)->run($user, $id)->changes();
if (!$changes) { if (!$changes) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "label", 'id' => $id]); throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "label", 'id' => $id]);
} }
@ -1336,12 +1336,12 @@ class Database {
$out = $this->db->prepare( $out = $this->db->prepare(
"SELECT "SELECT
id,name, id,name,
(select count(*) from arsse_label_members where label is id and assigned is 1) as articles, (select count(*) from arsse_label_members where label = id and assigned = 1) as articles,
(select count(*) from arsse_label_members (select count(*) from arsse_label_members
join arsse_marks on arsse_label_members.article is arsse_marks.article and arsse_label_members.subscription is arsse_marks.subscription join arsse_marks on arsse_label_members.article = arsse_marks.article and arsse_label_members.subscription = arsse_marks.subscription
where label is id and assigned is 1 and read is 1 where label = id and assigned = 1 and read = 1
) as read ) as read
FROM arsse_labels where $field is ? and owner is ? FROM arsse_labels where $field = ? and owner = ?
", $type, "str" ", $type, "str"
)->run($id, $user)->getRow(); )->run($id, $user)->getRow();
if (!$out) { if (!$out) {
@ -1368,7 +1368,7 @@ class Database {
// if no changes would actually be applied, just return // if no changes would actually be applied, just return
return false; return false;
} }
$out = (bool) $this->db->prepare("UPDATE arsse_labels set $setClause, modified = CURRENT_TIMESTAMP where owner is ? and $field is ?", $setTypes, "str", $type)->run($setValues, $user, $id)->changes(); $out = (bool) $this->db->prepare("UPDATE arsse_labels set $setClause, modified = CURRENT_TIMESTAMP where owner = ? and $field = ?", $setTypes, "str", $type)->run($setValues, $user, $id)->changes();
if (!$out) { if (!$out) {
throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "label", 'id' => $id]); throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "label", 'id' => $id]);
} }
@ -1383,7 +1383,7 @@ class Database {
$this->labelValidateId($user, $id, $byName, false); $this->labelValidateId($user, $id, $byName, false);
$field = !$byName ? "id" : "name"; $field = !$byName ? "id" : "name";
$type = !$byName ? "int" : "str"; $type = !$byName ? "int" : "str";
$out = $this->db->prepare("SELECT article from arsse_label_members join arsse_labels on label is id where assigned is 1 and $field is ? and owner is ?", $type, "str")->run($id, $user)->getAll(); $out = $this->db->prepare("SELECT article from arsse_label_members join arsse_labels on label = id where assigned = 1 and $field = ? and owner = ?", $type, "str")->run($id, $user)->getAll();
if (!$out) { if (!$out) {
// if no results were returned, do a full validation on the label ID // if no results were returned, do a full validation on the label ID
$this->labelValidateId($user, $id, $byName, true, true); $this->labelValidateId($user, $id, $byName, true, true);
@ -1407,10 +1407,10 @@ class Database {
$tr = $this->begin(); $tr = $this->begin();
// first update any existing entries with the removal or re-addition of their association // first update any existing entries with the removal or re-addition of their association
$q = $this->articleQuery($user, $context); $q = $this->articleQuery($user, $context);
$q->setWhere("exists(select article from arsse_label_members where label is ? and article is arsse_articles.id)", "int", $id); $q->setWhere("exists(select article from arsse_label_members where label = ? and article = arsse_articles.id)", "int", $id);
$q->pushCTE("target_articles"); $q->pushCTE("target_articles");
$q->setBody( $q->setBody(
"UPDATE arsse_label_members set assigned = ?, modified = CURRENT_TIMESTAMP where label is ? and assigned is not ? and article in (select id from target_articles)", "UPDATE arsse_label_members set assigned = ?, modified = CURRENT_TIMESTAMP where label = ? and assigned = not ? and article in (select id from target_articles)",
["bool","int","bool"], ["bool","int","bool"],
[!$remove, $id, !$remove] [!$remove, $id, !$remove]
); );
@ -1418,14 +1418,14 @@ class Database {
// next, if we're not removing, add any new entries that need to be added // next, if we're not removing, add any new entries that need to be added
if (!$remove) { if (!$remove) {
$q = $this->articleQuery($user, $context); $q = $this->articleQuery($user, $context);
$q->setWhere("not exists(select article from arsse_label_members where label is ? and article is arsse_articles.id)", "int", $id); $q->setWhere("not exists(select article from arsse_label_members where label = ? and article = arsse_articles.id)", "int", $id);
$q->pushCTE("target_articles"); $q->pushCTE("target_articles");
$q->setBody( $q->setBody(
"INSERT INTO "INSERT INTO
arsse_label_members(label,article,subscription) arsse_label_members(label,article,subscription)
SELECT SELECT
?,id, ?,id,
(select id from arsse_subscriptions join user on user is owner where arsse_subscriptions.feed is target_articles.feed) (select id from arsse_subscriptions join user on user = owner where arsse_subscriptions.feed = target_articles.feed)
FROM target_articles", FROM target_articles",
"int", $id "int", $id
); );
@ -1446,7 +1446,7 @@ class Database {
} elseif ($checkDb) { } elseif ($checkDb) {
$field = !$byName ? "id" : "name"; $field = !$byName ? "id" : "name";
$type = !$byName ? "int" : "str"; $type = !$byName ? "int" : "str";
$l = $this->db->prepare("SELECT id,name from arsse_labels where $field is ? and owner is ?", $type, "str")->run($id, $user)->getRow(); $l = $this->db->prepare("SELECT id,name from arsse_labels where $field = ? and owner = ?", $type, "str")->run($id, $user)->getRow();
if (!$l) { if (!$l) {
throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "label", 'id' => $id]); throw new Db\ExceptionInput($subject ? "subjectMissing" : "idMissing", ["action" => $this->caller(), "field" => "label", 'id' => $id]);
} else { } else {