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:
parent
ee86eeef13
commit
77d628ed69
155
src/lib/Feed.php
155
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
|
* Retrieve the feed
|
||||||
*
|
*
|
||||||
* @param string $url
|
* @param string $url
|
||||||
* @return array|DOMDocument[]|string[] ['ok' => feedXml, 'url' => actualUrl] if successful, ['error' => message] if
|
* @return array|DOMDocument[]|string[]|DOMElement[]
|
||||||
* not
|
* ['ok' => feedXml, 'url' => actualUrl, 'channel' => channel, 'updated' => updatedDate] if successful,
|
||||||
|
* ['error' => message] if not
|
||||||
*/
|
*/
|
||||||
public static function retrieveFeed(string $url): array {
|
public static function retrieveFeed(string $url): array {
|
||||||
$feedReq = curl_init($url);
|
$feedReq = curl_init($url);
|
||||||
|
@ -72,6 +85,24 @@ class Feed {
|
||||||
} else {
|
} else {
|
||||||
$result['ok'] = $parsed['ok'];
|
$result['ok'] = $parsed['ok'];
|
||||||
$result['url'] = curl_getinfo($feedReq, CURLINFO_EFFECTIVE_URL);
|
$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 {
|
} else {
|
||||||
$result['error'] = "Prospective feed URL $url returned HTTP Code $code: $feedContent";
|
$result['error'] = "Prospective feed URL $url returned HTTP Code $code: $feedContent";
|
||||||
|
@ -81,18 +112,6 @@ class Feed {
|
||||||
return $result;
|
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
|
* Extract the fields we need to keep from the feed
|
||||||
*
|
*
|
||||||
|
@ -186,10 +205,11 @@ class Feed {
|
||||||
$exists = $existsQuery->execute();
|
$exists = $existsQuery->execute();
|
||||||
if ($exists) {
|
if ($exists) {
|
||||||
$existing = $exists->fetchArray(SQLITE3_ASSOC);
|
$existing = $exists->fetchArray(SQLITE3_ASSOC);
|
||||||
if ( $existing
|
if ($existing) {
|
||||||
&& ( $existing['published_on'] != $item['published']
|
if ( $existing['published_on'] != $item['published']
|
||||||
|| $existing['updated_on'] ?? '' != $item['updated'] ?? '')) {
|
|| $existing['updated_on'] ?? '' != $item['updated'] ?? '') {
|
||||||
self::updateItem($existing['id'], $item, $db);
|
self::updateItem($existing['id'], $item, $db);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self::addItem($feedId, $item, $db);
|
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
|
* @param string $url The URL of the feed to be refreshed
|
||||||
* @return array|string[]|DOMElement[] ['channel' => channel, 'updated' => date] if successful, ['error' => message]
|
* @param SQLite3 $db A database connection to use to refresh the feed
|
||||||
* if not
|
* @return array|string[]|true[] ['ok' => true] if successful, ['error' => message] if not
|
||||||
*/
|
*/
|
||||||
private static function findChannelAndDate(DOMDocument $feed): array {
|
private static function refreshFeed(string $url, SQLite3 $db): array {
|
||||||
$channel = $feed->getElementsByTagName('channel')->item(0);
|
$feedQuery = $db->prepare('SELECT id FROM feed WHERE url = :url AND user_id = :user');
|
||||||
if (!$channel instanceof DOMElement) return [ 'error' => "Channel element not found ($channel->nodeType)" ];
|
$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,
|
$feed = self::retrieveFeed($url);
|
||||||
// use the pubDate element instead
|
if (array_key_exists('error', $feed)) return $feed;
|
||||||
$updated = self::eltValue($channel, 'lastBuildDate');
|
|
||||||
if ($updated == 'lastBuildDate not found') {
|
$itemUpdate = self::updateItems($feedId, $feed['channel'], $db);
|
||||||
$updated = self::eltValue($channel, 'pubDate');
|
if (array_key_exists('error', $itemUpdate)) return $itemUpdate;
|
||||||
if ($updated == 'pubDate not found') $updated = null;
|
|
||||||
}
|
$urlUpdate = $url == $feed['url'] ? '' : ', url = :url';
|
||||||
return ['channel' => $channel, 'updated' => Data::formatDate($updated)];
|
$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);
|
$feed = self::retrieveFeed($url);
|
||||||
if (array_key_exists('error', $feed)) return $feed;
|
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'
|
$query = $db->prepare(<<<'SQL'
|
||||||
INSERT INTO feed (user_id, url, title, updated_on, checked_on)
|
INSERT INTO feed (user_id, url, title, updated_on, checked_on)
|
||||||
VALUES (:user, :url, :title, :updated, :checked)
|
VALUES (:user, :url, :title, :updated, :checked)
|
||||||
SQL);
|
SQL);
|
||||||
$query->bindValue(':user', $_REQUEST[Key::USER_ID]);
|
$query->bindValue(':user', $_REQUEST[Key::USER_ID]);
|
||||||
$query->bindValue(':url', $feed['url']);
|
$query->bindValue(':url', $feed['url']);
|
||||||
$query->bindValue(':title', self::eltValue($channel, 'title'));
|
$query->bindValue(':title', self::eltValue($feed['channel'], 'title'));
|
||||||
$query->bindValue(':updated', $channelAndDate['updated']);
|
$query->bindValue(':updated', $feed['updated']);
|
||||||
$query->bindValue(':checked', Data::formatDate('now'));
|
$query->bindValue(':checked', Data::formatDate('now'));
|
||||||
$result = $query->execute();
|
$result = $query->execute();
|
||||||
|
|
||||||
$feedId = $result ? $db->lastInsertRowID() : -1;
|
$feedId = $result ? $db->lastInsertRowID() : -1;
|
||||||
if ($feedId < 0) return ['error' => $db->lastErrorMsg()];
|
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;
|
if (array_key_exists('error', $result)) return $result;
|
||||||
|
|
||||||
return ['ok' => $feedId];
|
return ['ok' => $feedId];
|
||||||
|
@ -266,29 +301,33 @@ class Feed {
|
||||||
* @return bool[]|string[] [ 'ok' => true ] if successful, [ 'error' => message ] if not
|
* @return bool[]|string[] [ 'ok' => true ] if successful, [ 'error' => message ] if not
|
||||||
*/
|
*/
|
||||||
public static function update(array $existing, string $url, SQLite3 $db): array {
|
public static function update(array $existing, string $url, SQLite3 $db): array {
|
||||||
$feed = self::retrieveFeed($url);
|
$query = $db->prepare('UPDATE feed SET url = :url WHERE id = :id AND user_id = :user');
|
||||||
if (array_key_exists('error', $feed)) return $feed;
|
$query->bindValue(':url', $url);
|
||||||
|
|
||||||
$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(':id', $existing['id']);
|
||||||
$query->bindValue(':user', $_REQUEST[Key::USER_ID]);
|
$query->bindValue(':user', $_REQUEST[Key::USER_ID]);
|
||||||
$query->execute();
|
$query->execute();
|
||||||
|
|
||||||
$result = self::updateItems($existing['id'], $channel, $db);
|
return self::refreshFeed($url, $db);
|
||||||
if (array_key_exists('error', $result)) return $result;
|
}
|
||||||
|
|
||||||
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()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,16 @@ include '../start.php';
|
||||||
Security::verifyUser();
|
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'
|
$result = $db->query(<<<'SQL'
|
||||||
SELECT item.id, item.title AS item_title, coalesce(item.updated_on, item.published_on) AS as_of,
|
SELECT item.id, item.title AS item_title, coalesce(item.updated_on, item.published_on) AS as_of,
|
||||||
feed.title AS feed_title
|
feed.title AS feed_title
|
||||||
|
@ -21,7 +31,7 @@ $result = $db->query(<<<'SQL'
|
||||||
$item = $result ? $result->fetchArray(SQLITE3_ASSOC) : false;
|
$item = $result ? $result->fetchArray(SQLITE3_ASSOC) : false;
|
||||||
|
|
||||||
page_head('Welcome'); ?>
|
page_head('Welcome'); ?>
|
||||||
<h1>Your Unread Items</h1>
|
<h1>Your Unread Items <a href=/?refresh><small><small><em>(Refresh All Feeds)</em></small></small></a></h1>
|
||||||
<article><?php
|
<article><?php
|
||||||
if ($item) {
|
if ($item) {
|
||||||
while ($item) { ?>
|
while ($item) { ?>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
use JetBrains\PhpStorm\NoReturn;
|
||||||
|
|
||||||
spl_autoload_register(function ($class) {
|
spl_autoload_register(function ($class) {
|
||||||
$file = implode(DIRECTORY_SEPARATOR, [ __DIR__, 'lib', "$class.php" ]);
|
$file = implode(DIRECTORY_SEPARATOR, [__DIR__, 'lib', "$class.php"]);
|
||||||
if (file_exists($file)) {
|
if (file_exists($file)) {
|
||||||
require $file;
|
require $file;
|
||||||
return true;
|
return true;
|
||||||
|
@ -86,7 +88,8 @@ function page_foot(): void {
|
||||||
*
|
*
|
||||||
* @param string $value A local URL to which the user should be redirected
|
* @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')) {
|
if (str_starts_with($value, 'http')) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
die();
|
die();
|
||||||
|
|
Loading…
Reference in New Issue
Block a user