diff --git a/lib/Database.php b/lib/Database.php index 6a5f21c..25bbefb 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -356,30 +356,42 @@ class Database { public function subscriptionList(string $user, int $folder = null, int $id = null): Db\Result { if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - // check to make sure the folder exists, if one is specified + // lay out the base query parts + $queryCTE = ["topmost(f_id,top) as (select id,id from arsse_folders where owner is ? and parent is null union select id,top from arsse_folders join topmost on parent=f_id)"]; + $queryWhere = ["owner is ?"]; + $queryTypes = ["str", "str", "str", "str"]; + $queryValues = [$user, $this->dateFormatDefault, $user, $user]; + if(!is_null($folder)) { + // if a folder is specified, make sure it exists + $this->folderValidateId($user, $folder); + // if it does exist, add a common table expression to list it and its children so that we select from the entire subtree + array_unshift($queryCTE, "folders(folder) as (SELECT ? union select id from arsse_folders join folders on parent is folder)"); + // add a suitable WHERE condition and bindings + $queryWhere[] = "folder in (select folder from folders)"; + array_unshift($queryTypes, "int"); + array_unshift($queryValues, $folder); + } + if(!is_null($id)) { + // this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query + // if an ID is specified, add a suitable WHERE condition and bindings + $queryWhere[] = "arsse_subscriptions.id is ?"; + $queryTypes[] = "int"; + $queryValues[] = $id; + } + // stitch the query together + $queryCTE = "WITH RECURSIVE ".implode(", ", $queryCTE)." "; + $queryWhere = implode(" AND ", $queryWhere); $query = - "SELECT + $queryCTE."SELECT arsse_subscriptions.id, url,favicon,source,folder,pinned,err_count,err_msg,order_type, DATEFORMAT(?, added) as added, + topmost.top as top_folder, CASE WHEN arsse_subscriptions.title is not null THEN arsse_subscriptions.title ELSE arsse_feeds.title END as title, (SELECT count(*) from arsse_articles where feed is arsse_subscriptions.feed) - (SELECT count(*) from arsse_marks join arsse_articles on article = arsse_articles.id where owner is ? and feed is arsse_feeds.id and read is 1) as unread - from arsse_subscriptions join arsse_feeds on feed = arsse_feeds.id where owner is ?"; - $queryOrder = "order by pinned desc, title"; - $queryTypes = ["str", "str", "str"]; - $queryValues = [$this->dateFormatDefault, $user, $user]; - if(!is_null($folder)) { - $this->folderValidateId($user, $folder); - return $this->db->prepare( - "WITH RECURSIVE folders(folder) as (SELECT ? union select id from arsse_folders join folders on parent is folder) $query and folder in (select folder from folders) $queryOrder", - "int", $queryTypes - )->run($folder, $queryValues); - } else if(!is_null($id)) { - // this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query - return $this->db->prepare("$query and arsse_subscriptions.id is ? $queryOrder", $queryTypes, "int")->run($queryValues, $id); - } else { - return $this->db->prepare("$query $queryOrder", $queryTypes)->run($queryValues); - } + from arsse_subscriptions join arsse_feeds on feed = arsse_feeds.id left join topmost on folder=f_id where $queryWhere order by pinned desc, title"; + // execute the query + return $this->db->prepare($query, $queryTypes)->run($queryValues); } public function subscriptionRemove(string $user, int $id): bool { diff --git a/lib/REST/NextCloudNews/V1_2.php b/lib/REST/NextCloudNews/V1_2.php index 9061e5a..1227d69 100644 --- a/lib/REST/NextCloudNews/V1_2.php +++ b/lib/REST/NextCloudNews/V1_2.php @@ -180,19 +180,21 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { protected function feedTranslate(array $feed, bool $overwrite = false): array { // cast values $feed = $this->mapFieldTypes($feed, [ - 'folder' => "int", - 'pinned' => "bool", + 'top_folder' => "int", + 'pinned' => "bool", ]); // map fields to proper names $feed = $this->mapFieldNames($feed, [ 'source' => "link", 'favicon' => "faviconLink", - 'folder' => "folderId", + 'top_folder' => "folderId", 'unread' => "unreadCount", 'order_type' => "ordering", 'err_count' => "updateErrorCount", 'err_msg' => "lastUpdateError", ], $overwrite); + // remove the true folder since the protocol does not support nesting + unset($feed['folder']); return $feed; } diff --git a/tests/REST/NextCloudNews/TestNCNV1_2.php b/tests/REST/NextCloudNews/TestNCNV1_2.php index bb9a417..dcfeaf9 100644 --- a/tests/REST/NextCloudNews/TestNCNV1_2.php +++ b/tests/REST/NextCloudNews/TestNCNV1_2.php @@ -18,7 +18,8 @@ class TestNCNV1_2 extends \PHPUnit\Framework\TestCase { 'url' => 'http://example.com/news.atom', 'favicon' => 'http://example.com/favicon.png', 'source' => 'http://example.com/', - 'folder' => NULL, + 'folder' => null, + 'top_folder' => null, 'pinned' => 0, 'err_count' => 0, 'err_msg' => '', @@ -32,7 +33,8 @@ class TestNCNV1_2 extends \PHPUnit\Framework\TestCase { 'url' => 'http://example.org/news.atom', 'favicon' => 'http://example.org/favicon.png', 'source' => 'http://example.org/', - 'folder' => 8, + 'folder' => 12, + 'top_folder' => 8, 'pinned' => 1, 'err_count' => 0, 'err_msg' => '', diff --git a/tests/lib/Database/SeriesSubscription.php b/tests/lib/Database/SeriesSubscription.php index 691ef9e..8074d8e 100644 --- a/tests/lib/Database/SeriesSubscription.php +++ b/tests/lib/Database/SeriesSubscription.php @@ -189,6 +189,7 @@ trait SeriesSubscription { 'url' => "http://example.com/feed2", 'title' => "Eek", 'folder' => null, + 'top_folder' => null, 'unread' => 4, 'pinned' => 1, 'order_type' => 2, @@ -197,6 +198,7 @@ trait SeriesSubscription { 'url' => "http://example.com/feed3", 'title' => "Ook", 'folder' => 2, + 'top_folder' => 1, 'unread' => 2, 'pinned' => 0, 'order_type' => 1, @@ -215,6 +217,7 @@ trait SeriesSubscription { 'url' => "http://example.com/feed3", 'title' => "Ook", 'folder' => 2, + 'top_folder' => 1, 'unread' => 2, 'pinned' => 0, 'order_type' => 1,