diff --git a/lib/Database.php b/lib/Database.php index 856b6075..4da524a6 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -622,12 +622,12 @@ class Database { arsse_articles.id as id, arsse_articles.url as url, title,author,content,guid, - DATEFORMAT(?, published) as published, - DATEFORMAT(?, edited) as edited, + DATEFORMAT(?, published) as published_date, + DATEFORMAT(?, edited) as edited_date, DATEFORMAT(?, max( modified, coalesce((select modified from arsse_marks join user on user is owner where article is arsse_articles.id),'') - )) as modified, + )) as modified_date, NOT (select count(*) from arsse_marks join user on user is owner where article is arsse_articles.id and read is 1) as unread, (select count(*) from arsse_marks join user on user is owner where article is arsse_articles.id and starred is 1) as starred, (select max(id) from arsse_editions where article is arsse_articles.id) as edition, @@ -665,8 +665,8 @@ class Database { if($context->oldestEdition()) $q->setWhere("edition >= ?", "int", $context->oldestEdition); if($context->latestEdition()) $q->setWhere("edition <= ?", "int", $context->latestEdition); // filter based on lastmod time - if($context->modifiedSince()) $q->setWhere("modified >= ?", "datetime", $context->modifiedSince); - if($context->notModifiedSince()) $q->setWhere("modified <= ?", "datetime", $context->notModifiedSince); + if($context->modifiedSince()) $q->setWhere("modified_date >= ?", "datetime", $context->modifiedSince); + if($context->notModifiedSince()) $q->setWhere("modified_date <= ?", "datetime", $context->notModifiedSince); // filter for un/read and un/starred status if specified if($context->unread()) $q->setWhere("unread is ?", "bool", $context->unread); if($context->starred()) $q->setWhere("starred is ?", "bool", $context->starred); @@ -707,8 +707,8 @@ class Database { if($context->edition()) { // make sure the edition exists $edition = $this->articleValidateEdition($user, $context->edition); - // if the edition is not the latest, make no marks and return - if(!$edition['current']) return false; + // if the edition is not the latest, do not mark the read flag + if(!$edition['current']) $values[0] = null; } else if($context->article()) { // otherwise if an article context is specified, make sure it's valid $this->articleValidateId($user, $context->article); @@ -722,7 +722,7 @@ class Database { (select max(id) from arsse_editions where article is arsse_articles.id) as edition, max(arsse_articles.modified, coalesce((select modified from arsse_marks join user on user is owner where article is arsse_articles.id),'') - ) as modified, + ) as modified_date, ( not exists(select id from arsse_marks join user on user is owner where article is arsse_articles.id) and exists(select * from target_values where read is 1 or starred is 1) @@ -744,8 +744,10 @@ class Database { // common table expression with the values to set $q->setCTE("target_values(read,starred) as (select ?,?)", ["bool","bool"], $values); if($context->edition()) { + // if an edition is specified, filter for its previously identified article $q->setWhere("arsse_articles.id is ?", "int", $edition['article']); } else if($context->article()) { + // if an article is specified, filter for it (it has already been validated above) $q->setWhere("arsse_articles.id is ?", "int", $context->article); } else if($context->subscription()) { // if a subscription is specified, make sure it exists @@ -767,8 +769,8 @@ class Database { if($context->oldestEdition()) $q->setWhere("edition >= ?", "int", $context->oldestEdition); if($context->latestEdition()) $q->setWhere("edition <= ?", "int", $context->latestEdition); // filter based on lastmod time - if($context->modifiedSince()) $q->setWhere("modified >= ?", "datetime", $context->modifiedSince); - if($context->notModifiedSince()) $q->setWhere("modified <= ?", "datetime", $context->notModifiedSince); + if($context->modifiedSince()) $q->setWhere("modified_date >= ?", "datetime", $context->modifiedSince); + if($context->notModifiedSince()) $q->setWhere("modified_date <= ?", "datetime", $context->notModifiedSince); // push the current query onto the CTE stack and execute the query we're actually interested in $q->pushCTE( "target_articles(id,edition,modified,to_insert,to_update)", // CTE table specification @@ -799,7 +801,7 @@ class Database { return $out; } - public function articleValidateEdition(string $user, int $id): array { + protected function articleValidateEdition(string $user, int $id): array { $out = $this->db->prepare( "SELECT arsse_editions.id as edition, diff --git a/lib/Misc/DateFormatter.php b/lib/Misc/DateFormatter.php index 55438f5c..9039caaf 100644 --- a/lib/Misc/DateFormatter.php +++ b/lib/Misc/DateFormatter.php @@ -32,8 +32,11 @@ trait DateFormatter { } else if($date===null) { return null; } else if(is_string($date)) { - $time = strtotime($date); - if($time===false) return null; + try { + $time = (new \DateTime($date, new \DateTimeZone("UTC")))->getTimestamp(); + } catch(\Throwable $e) { + return null; + } } else if (is_bool($date)) { return null; } else { diff --git a/tests/lib/Database/SeriesArticle.php b/tests/lib/Database/SeriesArticle.php index 9b2a507b..0dead0c8 100644 --- a/tests/lib/Database/SeriesArticle.php +++ b/tests/lib/Database/SeriesArticle.php @@ -178,9 +178,9 @@ trait SeriesArticle { 'author' => '', 'content' => '
Article content 1
', 'guid' => 'e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda', - 'published' => 946684800, - 'edited' => 946684801, - 'modified' => 946688400, + 'published_date' => 946684800, + 'edited_date' => 946684801, + 'modified_date' => 946688400, 'unread' => 1, 'starred' => 0, 'edition' => 101, @@ -196,9 +196,9 @@ trait SeriesArticle { 'author' => '', 'content' => 'Article content 2
', 'guid' => '5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7', - 'published' => 946771200, - 'edited' => 946771202, - 'modified' => 946778400, + 'published_date' => 946771200, + 'edited_date' => 946771202, + 'modified_date' => 946778400, 'unread' => 0, 'starred' => 0, 'edition' => 202, @@ -214,9 +214,9 @@ trait SeriesArticle { 'author' => '', 'content' => 'Article content 3
', 'guid' => '31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92', - 'published' => 946857600, - 'edited' => 946857603, - 'modified' => 946868400, + 'published_date' => 946857600, + 'edited_date' => 946857603, + 'modified_date' => 946868400, 'unread' => 1, 'starred' => 1, 'edition' => 203, @@ -232,9 +232,9 @@ trait SeriesArticle { 'author' => '', 'content' => 'Article content 4
', 'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180', - 'published' => 946944000, - 'edited' => 946944004, - 'modified' => 946958400, + 'published_date' => 946944000, + 'edited_date' => 946944004, + 'modified_date' => 946958400, 'unread' => 0, 'starred' => 1, 'edition' => 204, @@ -250,9 +250,9 @@ trait SeriesArticle { 'author' => '', 'content' => 'Article content 5
', 'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41', - 'published' => 947030400, - 'edited' => 947030405, - 'modified' => 947048400, + 'published_date' => 947030400, + 'edited_date' => 947030405, + 'modified_date' => 947048400, 'unread' => 1, 'starred' => 0, 'edition' => 305, @@ -271,9 +271,17 @@ trait SeriesArticle { $this->data['arsse_articles']['rows'][] = [++$a,$b,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"]; $this->data['arsse_editions']['rows'][] = [$a,$a]; } + $this->checkTables = ['arsse_marks' => ["owner","article","read","starred","modified"],]; $this->user = "john.doe@example.net"; } + protected function compareIds(array $exp, Context $c) { + $ids = array_column($ids = Data::$db->articleList($this->user, $c)->getAll(), "id"); + sort($ids); + sort($exp); + $this->assertEquals($exp, $ids); + } + function testListArticlesCheckingContext() { $this->user = "john.doe@example.com"; // get all items for user @@ -328,9 +336,7 @@ trait SeriesArticle { function testMarkAllArticlesUnread() { Data::$db->articleMark($this->user, ['read'=>false]); $now = $this->dateTransform(time(), "sql"); - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][9][2] = 0; $state['arsse_marks']['rows'][9][4] = $now; $state['arsse_marks']['rows'][11][2] = 0; @@ -341,9 +347,7 @@ trait SeriesArticle { function testMarkAllArticlesRead() { Data::$db->articleMark($this->user, ['read'=>true]); $now = $this->dateTransform(time(), "sql"); - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][8][2] = 1; $state['arsse_marks']['rows'][8][4] = $now; $state['arsse_marks']['rows'][10][2] = 1; @@ -358,9 +362,7 @@ trait SeriesArticle { function testMarkAllArticlesUnstarred() { Data::$db->articleMark($this->user, ['starred'=>false]); $now = $this->dateTransform(time(), "sql"); - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][10][3] = 0; $state['arsse_marks']['rows'][10][4] = $now; $state['arsse_marks']['rows'][11][3] = 0; @@ -371,9 +373,7 @@ trait SeriesArticle { function testMarkAllArticlesStarred() { Data::$db->articleMark($this->user, ['starred'=>true]); $now = $this->dateTransform(time(), "sql"); - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][8][3] = 1; $state['arsse_marks']['rows'][8][4] = $now; $state['arsse_marks']['rows'][9][3] = 1; @@ -388,9 +388,7 @@ trait SeriesArticle { function testMarkAllArticlesUnreadAndUnstarred() { Data::$db->articleMark($this->user, ['read'=>false,'starred'=>false]); $now = $this->dateTransform(time(), "sql"); - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][9][2] = 0; $state['arsse_marks']['rows'][9][4] = $now; $state['arsse_marks']['rows'][10][3] = 0; @@ -404,9 +402,7 @@ trait SeriesArticle { function testMarkAllArticlesReadAndStarred() { Data::$db->articleMark($this->user, ['read'=>true,'starred'=>true]); $now = $this->dateTransform(time(), "sql"); - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][8][2] = 1; $state['arsse_marks']['rows'][8][3] = 1; $state['arsse_marks']['rows'][8][4] = $now; @@ -424,9 +420,7 @@ trait SeriesArticle { function testMarkAllArticlesUnreadAndStarred() { Data::$db->articleMark($this->user, ['read'=>false,'starred'=>true]); $now = $this->dateTransform(time(), "sql"); - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][8][3] = 1; $state['arsse_marks']['rows'][8][4] = $now; $state['arsse_marks']['rows'][9][2] = 0; @@ -444,9 +438,7 @@ trait SeriesArticle { function testMarkAllArticlesReadAndUnstarred() { Data::$db->articleMark($this->user, ['read'=>true,'starred'=>false]); $now = $this->dateTransform(time(), "sql"); - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][8][2] = 1; $state['arsse_marks']['rows'][8][4] = $now; $state['arsse_marks']['rows'][10][2] = 1; @@ -464,9 +456,7 @@ trait SeriesArticle { function testMarkATreeFolder() { Data::$db->articleMark($this->user, ['read'=>true], (new Context)->folder(7)); $now = $this->dateTransform(time(), "sql"); - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][] = [$this->user,5,1,0,$now]; $state['arsse_marks']['rows'][] = [$this->user,6,1,0,$now]; $state['arsse_marks']['rows'][] = [$this->user,7,1,0,$now]; @@ -477,9 +467,7 @@ trait SeriesArticle { function testMarkALeafFolder() { Data::$db->articleMark($this->user, ['read'=>true], (new Context)->folder(8)); $now = $this->dateTransform(time(), "sql"); - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][] = [$this->user,5,1,0,$now]; $state['arsse_marks']['rows'][] = [$this->user,6,1,0,$now]; $this->compareExpectations($state); @@ -493,9 +481,7 @@ trait SeriesArticle { function testMarkASubscription() { Data::$db->articleMark($this->user, ['read'=>true], (new Context)->subscription(13)); $now = $this->dateTransform(time(), "sql"); - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][] = [$this->user,5,1,0,$now]; $state['arsse_marks']['rows'][] = [$this->user,6,1,0,$now]; $this->compareExpectations($state); @@ -509,9 +495,7 @@ trait SeriesArticle { function testMarkAnArticle() { Data::$db->articleMark($this->user, ['starred'=>true], (new Context)->article(20)); $now = $this->dateTransform(time(), "sql"); - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][9][3] = 1; $state['arsse_marks']['rows'][9][4] = $now; $this->compareExpectations($state); @@ -525,19 +509,39 @@ trait SeriesArticle { function testMarkAnEdition() { Data::$db->articleMark($this->user, ['starred'=>true], (new Context)->edition(1001)); $now = $this->dateTransform(time(), "sql"); - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + $state = $this->primeExpectations($this->data, $this->checkTables); $state['arsse_marks']['rows'][9][3] = 1; $state['arsse_marks']['rows'][9][4] = $now; $this->compareExpectations($state); } - function testMarkAStaleEdition() { - Data::$db->articleMark($this->user, ['starred'=>true], (new Context)->edition(20)); // no changes occur - $state = $this->primeExpectations($this->data, [ - 'arsse_marks' => ["owner","article","read","starred","modified"], - ]); + function testMarkAStaleEditionUnread() { + Data::$db->articleMark($this->user, ['read'=>false], (new Context)->edition(20)); // no changes occur + $state = $this->primeExpectations($this->data, $this->checkTables); + $this->compareExpectations($state); + } + + function testMarkAStaleEditionStarred() { + Data::$db->articleMark($this->user, ['starred'=>true], (new Context)->edition(20)); + $now = $this->dateTransform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][9][3] = 1; + $state['arsse_marks']['rows'][9][4] = $now; + $this->compareExpectations($state); + } + + function testMarkAStaleEditionUnreadAndStarred() { + Data::$db->articleMark($this->user, ['read'=>false,'starred'=>true], (new Context)->edition(20)); // only starred is changed + $now = $this->dateTransform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][9][3] = 1; + $state['arsse_marks']['rows'][9][4] = $now; + $this->compareExpectations($state); + } + + function testMarkAStaleEditionUnreadAndUnstarred() { + Data::$db->articleMark($this->user, ['read'=>false,'starred'=>false], (new Context)->edition(20)); // no changes occur + $state = $this->primeExpectations($this->data, $this->checkTables); $this->compareExpectations($state); } @@ -546,10 +550,47 @@ trait SeriesArticle { Data::$db->articleMark($this->user, ['starred'=>true], (new Context)->edition(2)); } - protected function compareIds(array $exp, Context $c) { - $ids = array_column($ids = Data::$db->articleList($this->user, $c)->getAll(), "id"); - sort($ids); - sort($exp); - $this->assertEquals($exp, $ids); + function testMarkByOldestEdition() { + Data::$db->articleMark($this->user, ['starred'=>true], (new Context)->oldestEdition(19)); + $now = $this->dateTransform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][8][3] = 1; + $state['arsse_marks']['rows'][8][4] = $now; + $state['arsse_marks']['rows'][9][3] = 1; + $state['arsse_marks']['rows'][9][4] = $now; + $this->compareExpectations($state); + } + + function testMarkByLatestEdition() { + Data::$db->articleMark($this->user, ['starred'=>true], (new Context)->latestEdition(20)); + $now = $this->dateTransform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][8][3] = 1; + $state['arsse_marks']['rows'][8][4] = $now; + $state['arsse_marks']['rows'][] = [$this->user,5,0,1,$now]; + $state['arsse_marks']['rows'][] = [$this->user,6,0,1,$now]; + $state['arsse_marks']['rows'][] = [$this->user,7,0,1,$now]; + $state['arsse_marks']['rows'][] = [$this->user,8,0,1,$now]; + $this->compareExpectations($state); + } + + function testMarkByLastModified() { + Data::$db->articleMark($this->user, ['starred'=>true], (new Context)->modifiedSince('2017-01-01T00:00:00Z')); + $now = $this->dateTransform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][8][3] = 1; + $state['arsse_marks']['rows'][8][4] = $now; + $state['arsse_marks']['rows'][9][3] = 1; + $state['arsse_marks']['rows'][9][4] = $now; + $this->compareExpectations($state); + } + + function testMarkByNotLastModified() { + Data::$db->articleMark($this->user, ['starred'=>true], (new Context)->notModifiedSince('2000-01-01T00:00:00Z')); + $now = $this->dateTransform(time(), "sql"); + $state = $this->primeExpectations($this->data, $this->checkTables); + $state['arsse_marks']['rows'][] = [$this->user,5,0,1,$now]; + $state['arsse_marks']['rows'][] = [$this->user,7,0,1,$now]; + $this->compareExpectations($state); } } \ No newline at end of file