alpha1 #8
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
|
||||
*
|
||||
* @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()];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <a href=/?refresh><small><small><em>(Refresh All Feeds)</em></small></small></a></h1>
|
||||
<article><?php
|
||||
if ($item) {
|
||||
while ($item) { ?>
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue
Block a user