Add feed refresh link (#5)

- Feed updates and refresh use the same logic
- Fixed issue with logic around item add-or-update
This commit is contained in:
Daniel J. Summers 2024-04-13 22:32:39 -04:00
parent ee86eeef13
commit 77d628ed69
3 changed files with 117 additions and 65 deletions

View File

@ -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'] ?? '')) {
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 `<channel>` 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(<<<SQL
UPDATE feed
SET title = :title,
updated_on = :updated,
checked_on = :checked
$urlUpdate
WHERE id = :id
SQL);
$feedUpdate->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 = $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()];
}
}

View File

@ -10,6 +10,16 @@ include '../start.php';
Security::verifyUser();
$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'); ?>
<h1>Your Unread Items</h1>
<h1>Your Unread Items &nbsp; <a href=/?refresh><small><small><em>(Refresh All Feeds)</em></small></small></a></h1>
<article><?php
if ($item) {
while ($item) { ?>

View File

@ -1,4 +1,6 @@
<?php
use JetBrains\PhpStorm\NoReturn;
spl_autoload_register(function ($class) {
$file = implode(DIRECTORY_SEPARATOR, [__DIR__, 'lib', "$class.php"]);
if (file_exists($file)) {
@ -86,7 +88,8 @@ function page_foot(): void {
*
* @param string $value A local URL to which the user should be redirected
*/
function frc_redirect(string $value) {
#[NoReturn]
function frc_redirect(string $value): void {
if (str_starts_with($value, 'http')) {
http_response_code(400);
die();