alpha1 #8
							
								
								
									
										161
									
								
								src/lib/Feed.php
									
									
									
									
									
								
							
							
						
						
									
										161
									
								
								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 `<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->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()]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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'); ?>
 | ||||
| <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,6 +1,8 @@ | ||||
| <?php | ||||
| use JetBrains\PhpStorm\NoReturn; | ||||
| 
 | ||||
| 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)) { | ||||
|         require $file; | ||||
|         return true; | ||||
| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user