alpha1 #8

Merged
danieljsummers merged 18 commits from alpha1 into main 2024-04-15 22:48:10 +00:00
3 changed files with 117 additions and 65 deletions
Showing only changes of commit 77d628ed69 - Show all commits

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 * 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()];
} }
} }

View File

@ -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 &nbsp; <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) { ?>

View File

@ -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();