diff --git a/lib/Database.php b/lib/Database.php index 11254fee..a7172f26 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -479,7 +479,7 @@ class Database { public function feedUpdate(int $feedID, bool $throwError = false): bool { $tr = $this->db->begin(); // check to make sure the feed exists - $f = $this->db->prepare('SELECT url, username, password, DATEFORMAT("http", modified) AS lastmodified, etag, err_count FROM arsse_feeds where id is ?', "int")->run($feedID)->getRow(); + $f = $this->db->prepare("SELECT url, username, password, DATEFORMAT('http', modified) AS lastmodified, etag, err_count FROM arsse_feeds where id is ?", "int")->run($feedID)->getRow(); if(!$f) throw new Db\ExceptionInput("subjectMissing", ["action" => __FUNCTION__, "field" => "feed", 'id' => $feedID]); // the Feed object throws an exception when there are problems, but that isn't ideal // here. When an exception is thrown it should update the database with the @@ -488,7 +488,7 @@ class Database { $feed = new Feed($feedID, $f['url'], (string)$f['lastmodified'], $f['etag'], $f['username'], $f['password']); if(!$feed->modified) { // 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 is ?", 'datetime', 'int')->run($feed->nextFetch, $feedID); $tr->commit(); return false; } @@ -510,16 +510,16 @@ class Database { } if(sizeof($feed->newItems)) { $qInsertArticle = $this->db->prepare( - 'INSERT INTO arsse_articles(url,title,author,published,edited,guid,content,url_title_hash,url_content_hash,title_content_hash,feed) values(?,?,?,?,?,?,?,?,?,?,?)', + "INSERT INTO arsse_articles(url,title,author,published,edited,guid,content,url_title_hash,url_content_hash,title_content_hash,feed) values(?,?,?,?,?,?,?,?,?,?,?)", 'str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str', 'int' ); } if(sizeof($feed->changedItems)) { - $qDeleteEnclosures = $this->db->prepare('DELETE FROM arsse_enclosures WHERE article is ?', 'int'); - $qDeleteCategories = $this->db->prepare('DELETE FROM arsse_categories WHERE article is ?', 'int'); - $qClearReadMarks = $this->db->prepare('UPDATE arsse_marks SET read = 0, modified = CURRENT_TIMESTAMP WHERE article is ? and read is 1', 'int'); + $qDeleteEnclosures = $this->db->prepare("DELETE FROM arsse_enclosures WHERE article is ?", 'int'); + $qDeleteCategories = $this->db->prepare("DELETE FROM arsse_categories WHERE article is ?", 'int'); + $qClearReadMarks = $this->db->prepare("UPDATE arsse_marks SET read = 0, modified = CURRENT_TIMESTAMP WHERE article is ? and read is 1", 'int'); $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 is ?", 'str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str', 'int' ); } @@ -573,7 +573,7 @@ class Database { } // lastly update the feed database itself with updated information. $this->db->prepare( - 'UPDATE arsse_feeds SET url = ?, title = ?, favicon = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = "", next_fetch = ? WHERE id is ?', + "UPDATE arsse_feeds SET url = ?, title = ?, favicon = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ? WHERE id is ?", 'str', 'str', 'str', 'str', 'datetime', 'str', 'datetime', 'int' )->run( $feed->data->feedUrl, @@ -591,21 +591,20 @@ class Database { public function feedMatchLatest(int $feedID, int $count): Db\Result { return $this->db->prepare( - 'SELECT id, DATEFORMAT("unix", edited) AS edited_date, 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, DATEFORMAT('unix', edited) AS edited_date, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? ORDER BY modified desc, id desc limit ?", 'int', 'int' )->run($feedID, $count); } public function feedMatchIds(int $feedID, array $ids = [], array $hashesUT = [], array $hashesUC = [], array $hashesTC = []): Db\Result { // compile SQL IN() clauses and necessary type bindings for the four identifier lists - list($cId, $tId) = $this->generateIn($ids, "str"); + list($cId, $tId) = $this->generateIn($ids, "str"); list($cHashUT, $tHashUT) = $this->generateIn($hashesUT, "str"); list($cHashUC, $tHashUC) = $this->generateIn($hashesUC, "str"); list($cHashTC, $tHashTC) = $this->generateIn($hashesTC, "str"); // perform the query return $articles = $this->db->prepare( - 'SELECT id, DATEFORMAT("unix", edited) AS edited_date, 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, DATEFORMAT('unix', edited) AS edited_date, 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))", 'int', $tId, $tHashUT, $tHashUC, $tHashTC )->run($feedID, $ids, $hashesUT, $hashesUC, $hashesTC); } diff --git a/lib/REST/NextCloudNews/V1_2.php b/lib/REST/NextCloudNews/V1_2.php index 1227d696..ccff17fa 100644 --- a/lib/REST/NextCloudNews/V1_2.php +++ b/lib/REST/NextCloudNews/V1_2.php @@ -11,12 +11,14 @@ use JKingWeb\Arsse\REST\Exception501; use JKingWeb\Arsse\REST\Exception405; class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { + const REALM = "NextCloud News API v1-2"; + function __construct() { } function dispatch(\JKingWeb\Arsse\REST\Request $req): Response { // try to authenticate - if(!Data::$user->authHTTP()) return new Response(401, "", "", ['WWW-Authenticate: Basic realm="NextCloud News API v1-2"']); + if(!Data::$user->authHTTP()) return new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.self::REALM.'"']); // only accept GET, POST, PUT, or DELETE if(!in_array($req->method, ["GET", "POST", "PUT", "DELETE"])) return new Response(405, "", "", ['Allow: GET, POST, PUT, DELETE']); // normalize the input @@ -229,6 +231,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // refresh a feed protected function feedUpdate(array $url, array $data): Response { + // function requires admin rights per spec + if(Data::$user->rightsGet(Data::$user->id)==User::RIGHTS_NONE) return new Response(403); // perform an update of a single feed if(!array_key_exists("feedId", $data)) return new Response(422); if(!$this->validateId($data['feedId'])) return new Response(404); @@ -237,7 +241,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { } catch(ExceptionInput $e) { return new Response(404); } - return new Response(200); + return new Response(204); } // add a new feed diff --git a/tests/REST/NextCloudNews/TestNCNV1_2.php b/tests/REST/NextCloudNews/TestNCNV1_2.php index dcfeaf9a..f0834f56 100644 --- a/tests/REST/NextCloudNews/TestNCNV1_2.php +++ b/tests/REST/NextCloudNews/TestNCNV1_2.php @@ -81,6 +81,7 @@ class TestNCNV1_2 extends \PHPUnit\Framework\TestCase { // create a mock user manager Data::$user = Phake::mock(User::class); Phake::when(Data::$user)->authHTTP->thenReturn(true); + Phake::when(Data::$user)->rightsGet->thenReturn(100); Data::$user->id = "john.doe@example.com"; // create a mock database interface Data::$db = Phake::mock(Database::Class); @@ -137,6 +138,12 @@ class TestNCNV1_2 extends \PHPUnit\Framework\TestCase { } } + function testReceiveAuthenticationChallenge() { + Phake::when(Data::$user)->authHTTP->thenReturn(false); + $exp = new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.REST\NextCloudNews\V1_2::REALM.'"']); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/"))); + } + function testListFolders() { $list = [ ['id' => 1, 'name' => "Software", 'parent' => null], @@ -348,4 +355,47 @@ class TestNCNV1_2 extends \PHPUnit\Framework\TestCase { $exp = new Response(404); $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/feeds/42/rename", json_encode($in[4]), 'application/json'))); } + + function testListStaleFeeds() { + $out = [ + [ + 'id' => 42, + 'userId' => "", + ], + [ + 'id' => 2112, + 'userId' => "", + ], + ]; + Phake::when(Data::$db)->feedListStale->thenReturn(array_column($out,"id")); + $exp = new Response(200, ['feeds' => $out]); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/all"))); + // retrieving the list when not an admin fails + Phake::when(Data::$user)->rightsGet->thenReturn(0); + $exp = new Response(403); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/all"))); + } + + function testUpdateAFeed() { + $in = [ + ['feedId' => 42], // valid + ['feedId' => 2112], // feed does not exist + ['feedId' => "ook"], // invalid ID + ['feed' => 42], // invalid input + ]; + Phake::when(Data::$db)->feedUpdate( 42)->thenReturn(true); + Phake::when(Data::$db)->feedUpdate(2112)->thenThrow(new \JKingWeb\Arsse\Db\ExceptionInput("subjectMissing")); + $exp = new Response(204); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json'))); + $exp = new Response(404); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[1]), 'application/json'))); + $exp = new Response(404); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[2]), 'application/json'))); + $exp = new Response(422); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[3]), 'application/json'))); + // retrieving the list when not an admin fails + Phake::when(Data::$user)->rightsGet->thenReturn(0); + $exp = new Response(403); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/feeds/update", json_encode($in[0]), 'application/json'))); + } } \ No newline at end of file diff --git a/tests/lib/Database/SeriesFeed.php b/tests/lib/Database/SeriesFeed.php index 9db29633..db78c0c8 100644 --- a/tests/lib/Database/SeriesFeed.php +++ b/tests/lib/Database/SeriesFeed.php @@ -9,6 +9,25 @@ use JKingWeb\Arsse\Feed\Exception as FeedException; use Phake; trait SeriesFeed { + protected $matches = [ + [ + 'id' => 4, + 'edited_date' => 946944000, // 2000-01-04T00:00:00Z + 'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180', + 'url_title_hash' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8', + 'url_content_hash' => 'f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3', + 'title_content_hash' => 'ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9', + ], + [ + 'id' => 5, + 'edited_date' => 947030400, // 2000-01-05T00:00:00Z + 'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41', + 'url_title_hash' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022', + 'url_content_hash' => '834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900', + 'title_content_hash' => '43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba', + ], + ]; + function setUpSeries() { $past = gmdate("Y-m-d H:i:s",strtotime("now - 1 minute")); $future = gmdate("Y-m-d H:i:s",strtotime("now + 1 minute")); @@ -54,6 +73,7 @@ trait SeriesFeed { [3,1,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:00','
Article content 3
','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b',$past], [4,1,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:00','Article content 4
','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9',$past], [5,1,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:00','Article content 5
','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba',$past], + [6,2,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:00','Article content 1
','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207',$past], ] ], 'arsse_editions' => [ @@ -112,6 +132,21 @@ trait SeriesFeed { $this->user = "john.doe@example.com"; } + function testListLatestItems() { + $this->assertResult($this->matches, Data::$db->feedMatchLatest(1,2)); + } + + function testMatchItemsById() { + $this->assertResult($this->matches, Data::$db->feedMatchIds(1, ['804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41'])); + foreach($this->matches as $m) { + $exp = [$m]; + $this->assertResult($exp, Data::$db->feedMatchIds(1, [], [$m['url_title_hash']])); + $this->assertResult($exp, Data::$db->feedMatchIds(1, [], [], [$m['url_content_hash']])); + $this->assertResult($exp, Data::$db->feedMatchIds(1, [], [], [], [$m['title_content_hash']])); + } + $this->assertResult([['id' => 1]], Data::$db->feedMatchIds(1, ['e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda'])); // this ID appears in both feed 1 and feed 2; only one result should be returned + } + function testUpdateAFeed() { // update a valid feed with both new and changed items Data::$db->feedUpdate(1); @@ -123,9 +158,9 @@ trait SeriesFeed { ]); $state['arsse_articles']['rows'][2] = [3,1,'http://example.com/3','Article title 3 (updated)','','2000-01-03 00:00:00','2000-01-03 00:00:00','Article content 3
','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','6cc99be662ef3486fef35a890123f18d74c29a32d714802d743c5b4ef713315a','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','d5faccc13bf8267850a1e8e61f95950a0f34167df2c8c58011c0aaa6367026ac',$now]; $state['arsse_articles']['rows'][3] = [4,1,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:01','Article content 4
','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9',$now]; - $state['arsse_articles']['rows'][5] = [6,1,'http://example.com/6','Article title 6','','2000-01-06 00:00:00','2000-01-06 00:00:00','Article content 6
','b3461ab8e8759eeb1d65a818c65051ec00c1dfbbb32a3c8f6999434e3e3b76ab','91d051a8e6749d014506848acd45e959af50bf876427c4f0e3a1ec0f04777b51','211d78b1a040d40d17e747a363cc283f58767b2e502630d8de9b8f1d5e941d18','5ed68ccb64243b8c1931241d2c9276274c3b1d87f223634aa7a1ab0141292ca7',$now]; + $state['arsse_articles']['rows'][] = [7,1,'http://example.com/6','Article title 6','','2000-01-06 00:00:00','2000-01-06 00:00:00','Article content 6
','b3461ab8e8759eeb1d65a818c65051ec00c1dfbbb32a3c8f6999434e3e3b76ab','91d051a8e6749d014506848acd45e959af50bf876427c4f0e3a1ec0f04777b51','211d78b1a040d40d17e747a363cc283f58767b2e502630d8de9b8f1d5e941d18','5ed68ccb64243b8c1931241d2c9276274c3b1d87f223634aa7a1ab0141292ca7',$now]; $state['arsse_editions']['rows'] = array_merge($state['arsse_editions']['rows'], [ - [6,6,$now], + [6,7,$now], [7,3,$now], [8,4,$now], ]);