From 77d628ed695633e2045f3f7ad2ee63a474ca7a35 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 13 Apr 2024 22:32:39 -0400 Subject: [PATCH] Add feed refresh link (#5) - Feed updates and refresh use the same logic - Fixed issue with logic around item add-or-update --- src/lib/Feed.php | 161 +++++++++++++++++++++++++++---------------- src/public/index.php | 14 +++- src/start.php | 7 +- 3 files changed, 117 insertions(+), 65 deletions(-) diff --git a/src/lib/Feed.php b/src/lib/Feed.php index 2a45ab8..3c5a6af 100644 --- a/src/lib/Feed.php +++ b/src/lib/Feed.php @@ -44,12 +44,25 @@ class Feed { } } + /** + * Get the value of a child element by its tag name + * + * @param DOMElement $element The parent element + * @param string $tagName The name of the tag whose value should be obtained + * @return string The value of the element (or "[element] not found" if that element does not exist) + */ + private static function eltValue(DOMElement $element, string $tagName): string { + $tags = $element->getElementsByTagName($tagName); + return $tags->length == 0 ? "$tagName not found" : $tags->item(0)->textContent; + } + /** * Retrieve the feed * * @param string $url - * @return array|DOMDocument[]|string[] ['ok' => feedXml, 'url' => actualUrl] if successful, ['error' => message] if - * not + * @return array|DOMDocument[]|string[]|DOMElement[] + * ['ok' => feedXml, 'url' => actualUrl, 'channel' => channel, 'updated' => updatedDate] if successful, + * ['error' => message] if not */ public static function retrieveFeed(string $url): array { $feedReq = curl_init($url); @@ -72,6 +85,24 @@ class Feed { } else { $result['ok'] = $parsed['ok']; $result['url'] = curl_getinfo($feedReq, CURLINFO_EFFECTIVE_URL); + + $channel = $result['ok']->getElementsByTagName('channel')->item(0); + if ($channel instanceof DOMElement) { + $result['channel'] = $channel; + } else { + return ['error' => "Channel element not found ($channel->nodeType)"]; + } + + // In Atom feeds, lastBuildDate contains the last time an item in the feed was updated; if that is not + // present, use the pubDate element instead + $updated = self::eltValue($channel, 'lastBuildDate'); + if ($updated == 'lastBuildDate not found') { + $updated = self::eltValue($channel, 'pubDate'); + if ($updated == 'pubDate not found') $updated = null; + } + $result['updated'] = Data::formatDate($updated); + return $result; + } } else { $result['error'] = "Prospective feed URL $url returned HTTP Code $code: $feedContent"; @@ -81,18 +112,6 @@ class Feed { return $result; } - /** - * Get the value of a child element by its tag name - * - * @param DOMElement $element The parent element - * @param string $tagName The name of the tag whose value should be obtained - * @return string The value of the element (or "[element] not found" if that element does not exist) - */ - private static function eltValue(DOMElement $element, string $tagName): string { - $tags = $element->getElementsByTagName($tagName); - return $tags->length == 0 ? "$tagName not found" : $tags->item(0)->textContent; - } - /** * Extract the fields we need to keep from the feed * @@ -186,10 +205,11 @@ class Feed { $exists = $existsQuery->execute(); if ($exists) { $existing = $exists->fetchArray(SQLITE3_ASSOC); - if ( $existing - && ( $existing['published_on'] != $item['published'] - || $existing['updated_on'] ?? '' != $item['updated'] ?? '')) { - self::updateItem($existing['id'], $item, $db); + if ($existing) { + if ( $existing['published_on'] != $item['published'] + || $existing['updated_on'] ?? '' != $item['updated'] ?? '') { + self::updateItem($existing['id'], $item, $db); + } } else { self::addItem($feedId, $item, $db); } @@ -204,24 +224,43 @@ class Feed { } /** - * Find the `` element and derive the published/last updated date from the feed + * Refresh a feed * - * @param DOMDocument $feed The feed from which the information should be extracted - * @return array|string[]|DOMElement[] ['channel' => channel, 'updated' => date] if successful, ['error' => message] - * if not + * @param string $url The URL of the feed to be refreshed + * @param SQLite3 $db A database connection to use to refresh the feed + * @return array|string[]|true[] ['ok' => true] if successful, ['error' => message] if not */ - private static function findChannelAndDate(DOMDocument $feed): array { - $channel = $feed->getElementsByTagName('channel')->item(0); - if (!$channel instanceof DOMElement) return [ 'error' => "Channel element not found ($channel->nodeType)" ]; + private static function refreshFeed(string $url, SQLite3 $db): array { + $feedQuery = $db->prepare('SELECT id FROM feed WHERE url = :url AND user_id = :user'); + $feedQuery->bindValue(':url', $url); + $feedQuery->bindValue(':user', $_REQUEST[Key::USER_ID]); + $feedResult = $feedQuery->execute(); + $feedId = $feedResult ? $feedResult->fetchArray(SQLITE3_NUM)[0] : -1; + if ($feedId < 0) return ['error' => "No feed for URL $url found"]; - // In Atom feeds, lastBuildDate contains the last time an item in the feed was updated; if that is not present, - // use the pubDate element instead - $updated = self::eltValue($channel, 'lastBuildDate'); - if ($updated == 'lastBuildDate not found') { - $updated = self::eltValue($channel, 'pubDate'); - if ($updated == 'pubDate not found') $updated = null; - } - return ['channel' => $channel, 'updated' => Data::formatDate($updated)]; + $feed = self::retrieveFeed($url); + if (array_key_exists('error', $feed)) return $feed; + + $itemUpdate = self::updateItems($feedId, $feed['channel'], $db); + if (array_key_exists('error', $itemUpdate)) return $itemUpdate; + + $urlUpdate = $url == $feed['url'] ? '' : ', url = :url'; + $feedUpdate = $db->prepare(<<bindValue(':title', self::eltValue($feed['channel'], 'title')); + $feedUpdate->bindValue(':updated', $feed['updated']); + $feedUpdate->bindValue(':checked', Data::formatDate('now')); + $feedUpdate->bindValue(':id', $feedId); + if ($urlUpdate != '') $feedUpdate->bindValue(':url', $feed['url']); + $feedUpdate->execute(); + + return ['ok' => true]; } /** @@ -234,25 +273,21 @@ class Feed { $feed = self::retrieveFeed($url); if (array_key_exists('error', $feed)) return $feed; - $channelAndDate = self::findChannelAndDate($feed['ok']); - if (array_key_exists('error', $channelAndDate)) return $channelAndDate; - $channel = $channelAndDate['channel']; - $query = $db->prepare(<<<'SQL' INSERT INTO feed (user_id, url, title, updated_on, checked_on) VALUES (:user, :url, :title, :updated, :checked) SQL); $query->bindValue(':user', $_REQUEST[Key::USER_ID]); $query->bindValue(':url', $feed['url']); - $query->bindValue(':title', self::eltValue($channel, 'title')); - $query->bindValue(':updated', $channelAndDate['updated']); + $query->bindValue(':title', self::eltValue($feed['channel'], 'title')); + $query->bindValue(':updated', $feed['updated']); $query->bindValue(':checked', Data::formatDate('now')); $result = $query->execute(); $feedId = $result ? $db->lastInsertRowID() : -1; if ($feedId < 0) return ['error' => $db->lastErrorMsg()]; - $result = self::updateItems($feedId, $channel, $db); + $result = self::updateItems($feedId, $feed['channel'], $db); if (array_key_exists('error', $result)) return $result; return ['ok' => $feedId]; @@ -266,29 +301,33 @@ class Feed { * @return bool[]|string[] [ 'ok' => true ] if successful, [ 'error' => message ] if not */ public static function update(array $existing, string $url, SQLite3 $db): array { - $feed = self::retrieveFeed($url); - if (array_key_exists('error', $feed)) return $feed; - - $channelAndDate = self::findChannelAndDate($feed['ok']); - if (array_key_exists('error', $channelAndDate)) return $channelAndDate; - $channel = $channelAndDate['channel']; - - $query = $db->prepare(<<<'SQL' - UPDATE feed - SET url = :url, title = :title, updated_on = :updated, checked_on = :checked - WHERE id = :id AND user_id = :user - SQL); - $query->bindValue(':url', $feed['url']); - $query->bindValue(':title', self::eltValue($channel, 'title')); - $query->bindValue(':updated', $channelAndDate['updated']); - $query->bindValue(':checked', Data::formatDate('now')); - $query->bindValue(':id', $existing['id']); - $query->bindValue(':user', $_REQUEST[Key::USER_ID]); + $query = $db->prepare('UPDATE feed SET url = :url WHERE id = :id AND user_id = :user'); + $query->bindValue(':url', $url); + $query->bindValue(':id', $existing['id']); + $query->bindValue(':user', $_REQUEST[Key::USER_ID]); $query->execute(); - $result = self::updateItems($existing['id'], $channel, $db); - if (array_key_exists('error', $result)) return $result; + return self::refreshFeed($url, $db); + } - return ['ok' => true]; + /** + * @param SQLite3 $db + * @return array|true[] ['ok => true] if successful, ['error' => message] if not (may have multiple error lines) + */ + public static function refreshAll(SQLite3 $db): array { + $query = $db->prepare('SELECT url FROM feed WHERE user_id = :user'); + $query->bindValue(':user', $_REQUEST[Key::USER_ID]); + $result = $query->execute(); + $url = $result ? $result->fetchArray(SQLITE3_NUM) : false; + if ($url) { + $errors = array(); + while ($url) { + $updateResult = self::refreshFeed($url[0], $db); + if (array_key_exists('error', $updateResult)) $errors[] = $updateResult['error']; + $url = $result->fetchArray(SQLITE3_NUM); + } + return sizeof($errors) == 0 ? ['ok' => true] : ['error' => implode("\n", $errors)]; + } + return ['error' => $db->lastErrorMsg()]; } } diff --git a/src/public/index.php b/src/public/index.php index 799d67d..b2d8f01 100644 --- a/src/public/index.php +++ b/src/public/index.php @@ -9,7 +9,17 @@ include '../start.php'; Security::verifyUser(); -$db = Data::getConnection(); +$db = Data::getConnection(); + +if (array_key_exists('refresh', $_GET)) { + $refreshResult = Feed::refreshAll($db); + if (array_key_exists('ok', $refreshResult)) { + add_info('All feeds refreshed successfully'); + } else { + add_error(nl2br($refreshResult['error'])); + } +} + $result = $db->query(<<<'SQL' SELECT item.id, item.title AS item_title, coalesce(item.updated_on, item.published_on) AS as_of, feed.title AS feed_title @@ -21,7 +31,7 @@ $result = $db->query(<<<'SQL' $item = $result ? $result->fetchArray(SQLITE3_ASSOC) : false; page_head('Welcome'); ?> -

Your Unread Items

+

Your Unread Items   (Refresh All Feeds)

diff --git a/src/start.php b/src/start.php index 78624af..48701ab 100644 --- a/src/start.php +++ b/src/start.php @@ -1,6 +1,8 @@