From 210377b4da14a135fe7d577359d5b38336a06057 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 25 May 2024 23:03:39 -0400 Subject: [PATCH] Move list retrieve/render to class (#15) - array_key_exists -> key_exists --- src/lib/Feed.php | 18 ++-- src/lib/ItemList.php | 172 ++++++++++++++++++++++++++++++++++++ src/lib/Security.php | 2 +- src/public/feed/index.php | 2 +- src/public/feed/items.php | 74 +++------------- src/public/index.php | 62 ++++--------- src/public/user/log-off.php | 2 +- src/public/user/log-on.php | 2 +- src/start.php | 7 +- 9 files changed, 215 insertions(+), 126 deletions(-) create mode 100644 src/lib/ItemList.php diff --git a/src/lib/Feed.php b/src/lib/Feed.php index 7dd5fca..8e37465 100644 --- a/src/lib/Feed.php +++ b/src/lib/Feed.php @@ -239,19 +239,19 @@ class Feed { $start = strtolower(strlen($doc['content']) >= 9 ? substr($doc['content'], 0, 9) : $doc['content']); if ($start == ' $derivedURL['error']]; + if (key_exists('error', $derivedURL)) return ['error' => $derivedURL['error']]; $feedURL = $derivedURL['ok']; if (!str_starts_with($feedURL, 'http')) { // Relative URL; feed should be retrieved in the context of the original URL $original = parse_url($url); - $port = array_key_exists('port', $original) ? ":{$original['port']}" : ''; + $port = key_exists('port', $original) ? ":{$original['port']}" : ''; $feedURL = $original['scheme'] . '://' . $original['host'] . $port . $feedURL; } $doc = self::retrieveDocument($feedURL); } $parsed = self::parseFeed($doc['content']); - if (array_key_exists('error', $parsed)) return ['error' => $parsed['error']]; + if (key_exists('error', $parsed)) return ['error' => $parsed['error']]; $extract = $parsed['ok']->getElementsByTagNameNS(self::ATOM_NS, 'feed')->length > 0 ? self::fromAtom(...) : self::fromRSS(...); @@ -388,7 +388,7 @@ class Feed { */ public static function refreshFeed(int $feedId, string $url, SQLite3 $db): array { $feedRetrieval = self::retrieveFeed($url); - if (array_key_exists('error', $feedRetrieval)) return $feedRetrieval; + if (key_exists('error', $feedRetrieval)) return $feedRetrieval; $feed = $feedRetrieval['ok']; $lastCheckedQuery = $db->prepare('SELECT checked_on FROM feed where id = :id'); @@ -399,7 +399,7 @@ class Feed { } $itemUpdate = self::updateItems($feedId, $feed, $lastChecked, $db); - if (array_key_exists('error', $itemUpdate)) return $itemUpdate; + if (key_exists('error', $itemUpdate)) return $itemUpdate; $urlUpdate = $url == $feed->url ? '' : ', url = :url'; $feedUpdate = $db->prepare(<<lastInsertRowID(); $result = self::updateItems($feedId, $feed, date_create_immutable(WWW_EPOCH), $db); - if (array_key_exists('error', $result)) return $result; + if (key_exists('error', $result)) return $result; return ['ok' => $feedId]; } @@ -504,12 +504,12 @@ class Feed { */ public static function refreshAll(SQLite3 $db): array { $feeds = self::retrieveAll($db, $_SESSION[Key::USER_ID]); - if (array_key_exists('error', $feeds)) return $feeds; + if (key_exists('error', $feeds)) return $feeds; $errors = []; array_walk($feeds, function ($feed) use ($db, &$errors) { $result = self::refreshFeed($feed['id'], $feed['url'], $db); - if (array_key_exists('error', $result)) $errors[] = $result['error']; + if (key_exists('error', $result)) $errors[] = $result['error']; }); return sizeof($errors) == 0 ? ['ok' => true] : ['error' => implode("\n", $errors)]; diff --git a/src/lib/ItemList.php b/src/lib/ItemList.php new file mode 100644 index 0000000..18f71b0 --- /dev/null +++ b/src/lib/ItemList.php @@ -0,0 +1,172 @@ +error != ''; + } + + /** @var bool Whether to render a link to the feed to which the item belongs */ + public bool $linkFeed = false; + + /** @var bool Whether to display the feed to which the item belongs */ + public bool $displayFeed = false; + + /** @var bool Whether to show read / bookmarked indicators on posts */ + public bool $showIndicators = false; + + /** + * Constructor + * + * @param SQLite3 $db The database connection (used to retrieve error information if the query fails) + * @param SQLite3Stmt $query The query to retrieve the items for this list + * @param string $itemType The type of item being displayed (unread, bookmark, etc.) + * @param string $returnURL The URL to which the item page should return once the item has been viewed + */ + private function __construct(SQLite3 $db, SQLite3Stmt $query, public string $itemType, public string $returnURL = '') { + $result = $query->execute(); + if (!$result) { + $this->error = Data::error($db)['error']; + } else { + $this->items = $result; + } + } + + /** + * Create an item list query + * + * @param SQLite3 $db The database connection to use to obtain items + * @param array $criteria One or more SQL WHERE conditions (will be combined with AND) + * @param array $parameters Parameters to be added to the query (key index 0, value index 1; optional) + * @return SQLite3Stmt The query, ready to be executed + */ + private static function makeQuery(SQLite3 $db, array $criteria, array $parameters = []): SQLite3Stmt { + $where = empty($criteria) ? '' : 'AND ' . implode(' AND ', $criteria); + $sql = <<prepare($sql); + $query->bindValue(':userId', $_SESSION[Key::USER_ID]); + foreach ($parameters as $param) $query->bindValue($param[0], $param[1]); + return $query; + } + + /** + * Create an item list with all the current user's bookmarked items + * + * @param SQLite3 $db The database connection to use to obtain items + * @return static An item list with all bookmarked items + */ + public static function allBookmarked(SQLite3 $db): static { + $list = new static($db, self::makeQuery($db, ['item.is_bookmarked = 1']), 'Bookmarked', '/?bookmarked'); + $list->linkFeed = true; + return $list; + } + + /** + * Create an item list with all the current user's unread items + * + * @param SQLite3 $db The database connection to use to obtain items + * @return static An item list with all unread items + */ + public static function allUnread(SQLite3 $db): static { + $list = new static($db, self::makeQuery($db, ['item.is_read = 0']), 'Unread'); + $list->linkFeed = true; + return $list; + } + + /** + * Create an item list with all items for the given feed + * + * @param int $feedId The ID of the feed for which items should be retrieved + * @param SQLite3 $db The database connection to use to obtain items + * @return static An item list with all items for the given feed + */ + public static function allForFeed(int $feedId, SQLite3 $db): static { + $list = new static($db, self::makeQuery($db, ['feed.id = :feed'], [[':feed', $feedId]]), '', + "/feed/items?id=$feedId"); + $list->showIndicators = true; + return $list; + } + + /** + * Create an item list with unread items for the given feed + * + * @param int $feedId The ID of the feed for which items should be retrieved + * @param SQLite3 $db The database connection to use to obtain items + * @return static An item list with unread items for the given feed + */ + public static function unreadForFeed(int $feedId, SQLite3 $db): static { + return new static($db, self::makeQuery($db, ['feed.id = :feed', 'item.is_read = 0'], [[':feed', $feedId]]), + 'Unread', "/feed/items?id=$feedId&unread"); + } + + /** + * Create an item list with bookmarked items for the given feed + * + * @param int $feedId The ID of the feed for which items should be retrieved + * @param SQLite3 $db The database connection to use to obtain items + * @return static An item list with bookmarked items for the given feed + */ + public static function bookmarkedForFeed(int $feedId, SQLite3 $db): static { + return new static($db, + self::makeQuery($db, ['feed.id = :feed', 'item.is_bookmarked = 1'], [[':feed', $feedId]]), 'Bookmarked', + "/feed/items?id=$feedId&bookmarked"); + } + + /** + * Render this item list + */ + public function render(): void { + if ($this->isError()) { ?> +

Error retrieving list:
error?>items->fetchArray(SQLITE3_ASSOC); + $return = $this->returnURL == '' ? '' : '&from=' . urlencode($this->returnURL); + echo '

'; + if ($item) { + while ($item) { ?> +


+ showIndicators) { + if (!$item['is_read']) echo 'Unread   '; + if ($item['is_bookmarked']) echo 'Bookmarked   '; + } + echo '' . date_time($item['as_of']) . ''; + if ($this->linkFeed) { + echo ' • ' . + hx_get("/feed/items?id={$item['feed_id']}&" . strtolower($this->itemType), + htmlentities($item['feed_title'])); + } elseif ($this->displayFeed) { + echo ' • ' . htmlentities($item['feed_title']); + } ?> + items->fetchArray(SQLITE3_ASSOC); + } + } else { ?> +

There are no itemType)?> items'; + } +} diff --git a/src/lib/Security.php b/src/lib/Security.php index d8e865b..68e2bbb 100644 --- a/src/lib/Security.php +++ b/src/lib/Security.php @@ -131,7 +131,7 @@ class Security { * @param bool $redirectIfAnonymous Whether to redirect the request if there is no user logged on */ public static function verifyUser(SQLite3 $db, bool $redirectIfAnonymous = true): void { - if (array_key_exists(Key::USER_ID, $_SESSION)) return; + if (key_exists(Key::USER_ID, $_SESSION)) return; if (SECURITY_MODEL == self::SINGLE_USER) self::logOnSingleUser($db); diff --git a/src/public/feed/index.php b/src/public/feed/index.php index 59a73e5..dd84544 100644 --- a/src/public/feed/index.php +++ b/src/public/feed/index.php @@ -35,7 +35,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { $toEdit = Feed::retrieveById($_POST['id'], $db); $result = $toEdit ? Feed::update($toEdit, $_POST['url'], $db) : ['error' => "Feed {$_POST['id']} not found"]; } - if (array_key_exists('ok', $result)) { + if (key_exists('ok', $result)) { add_info('Feed saved successfully'); frc_redirect('/feeds'); } diff --git a/src/public/feed/items.php b/src/public/feed/items.php index 30ba98f..a92269f 100644 --- a/src/public/feed/items.php +++ b/src/public/feed/items.php @@ -12,69 +12,19 @@ Security::verifyUser($db); if (!($feed = Feed::retrieveById($_GET['id'], $db))) not_found(); -/** Display a list of unread items for this feed */ -const TYPE_UNREAD = 0; - -/** Display a list of bookmarked items for this feed */ -const TYPE_BOOKMARKED = 1; - -/** Display all items for this feed */ -const TYPE_ALL = 2; - -$type = match (true) { - array_key_exists('unread', $_GET) => TYPE_UNREAD, - array_key_exists('bookmarked', $_GET) => TYPE_BOOKMARKED, - default => TYPE_ALL +$list = match (true) { + key_exists('unread', $_GET) => ItemList::unreadForFeed($feed['id'], $db), + key_exists('bookmarked', $_GET) => ItemList::bookmarkedForFeed($feed['id'], $db), + default => ItemList::allForFeed($feed['id'], $db) }; -$extraSQL = match ($type) { - TYPE_UNREAD => ' AND is_read = 0', - TYPE_BOOKMARKED => ' AND is_bookmarked = 1', - default => '' -}; -$itemQuery = $db->prepare(<<bindValue(':feed', $feed['id']); -if (!($itemResult = $itemQuery->execute())) add_error(Data::error($db)['error']); -$item = $itemResult ? $itemResult->fetchArray(SQLITE3_ASSOC) : false; - -$queryParam = match ($type) { - TYPE_UNREAD => '&unread', - TYPE_BOOKMARKED => '&bookmarked', - default => '' -}; -$thisURL = urlencode("/feed/items?id={$feed['id']}$queryParam"); - -$listType = match ($type) { - TYPE_UNREAD => 'Unread', - TYPE_BOOKMARKED => 'Bookmarked', - default => '' -}; - -page_head(($type != TYPE_ALL ? "$listType Items | " : '') . strip_tags($feed['title'])); -if ($type == TYPE_ALL) { ?> -

-

-
Items
-
-


- New   ' : ''?> - Bookmarked   ' : ''?> - fetchArray(SQLITE3_ASSOC); - } - } else { ?> -

There are no items -

-itemType != '' ? "$list->itemType Items | " : '') . strip_tags($feed['title'])); +if ($list->itemType == '') { + echo '

' . htmlentities($feed['title']) . '

'; +} else { + echo '

' . htmlentities($feed['title']) . '

'; + echo "
$list->itemType Items
"; +} +$list->render(); page_foot(); $db->close(); diff --git a/src/public/index.php b/src/public/index.php index a1df915..73307e7 100644 --- a/src/public/index.php +++ b/src/public/index.php @@ -10,60 +10,28 @@ include '../start.php'; $db = Data::getConnection(); Security::verifyUser($db); -if (array_key_exists('refresh', $_GET)) { +if (key_exists('refresh', $_GET)) { $refreshResult = Feed::refreshAll($db); - if (array_key_exists('ok', $refreshResult)) { + if (key_exists('ok', $refreshResult)) { add_info('All feeds refreshed successfully'); } else { add_error(nl2br($refreshResult['error'])); } } -if (key_exists('bookmarked', $_GET)) { - $itemCriteria = 'item.is_bookmarked = 1'; - $returnURL = '&from=' . urlencode('/?bookmarked'); - $type = 'Bookmarked'; -} else { - $itemCriteria = 'item.is_read = 0'; - $returnURL = ''; - $type = 'Unread'; +$list = match (true) { + key_exists('bookmarked', $_GET) => ItemList::allBookmarked($db), + default => ItemList::allUnread($db) +}; +$title = "Your $list->itemType Items"; + +page_head($title); +echo "

$title"; +if ($list->itemType == 'Unread') { + echo '   ' . hx_get('/?refresh', '(Refresh All Feeds)', 'class=refresh hx-indicator="closest h1"') + . 'Refreshing…'; } -$title = "Your $type Items"; - -$query = $db->prepare(<<bindValue(':userId', $_SESSION[Key::USER_ID]); -$result = $query->execute(); -$item = $result ? $result->fetchArray(SQLITE3_ASSOC) : false; - -page_head($title); ?> -

-   - - Refreshing… -

-
-


- • - - fetchArray(SQLITE3_ASSOC); - } -} else { ?> -

There are no items -

'; +$list->render(); page_foot(); $db->close(); diff --git a/src/public/user/log-off.php b/src/public/user/log-off.php index 5ba1f32..73fd811 100644 --- a/src/public/user/log-off.php +++ b/src/public/user/log-off.php @@ -5,6 +5,6 @@ include '../../start.php'; -if (array_key_exists(Key::USER_ID, $_SESSION)) session_destroy(); +if (key_exists(Key::USER_ID, $_SESSION)) session_destroy(); frc_redirect('/'); diff --git a/src/public/user/log-on.php b/src/public/user/log-on.php index 6632a7a..4a9c60d 100644 --- a/src/public/user/log-on.php +++ b/src/public/user/log-on.php @@ -5,7 +5,7 @@ $db = Data::getConnection(); Security::verifyUser($db, redirectIfAnonymous: false); // Users already logged on have no need of this page -if (array_key_exists(Key::USER_ID, $_SESSION)) frc_redirect('/'); +if (key_exists(Key::USER_ID, $_SESSION)) frc_redirect('/'); if ($_SERVER['REQUEST_METHOD'] == 'POST') { Security::logOnUser($_POST['email'] ?? '', $_POST['password'], $_POST['returnTo'] ?? null, $db); diff --git a/src/start.php b/src/start.php index f0fa0f0..3b5a686 100644 --- a/src/start.php +++ b/src/start.php @@ -16,7 +16,7 @@ session_start([ * @param string $message The message itself */ function add_message(string $level, string $message): void { - if (!array_key_exists(Key::USER_MSG, $_SESSION)) $_SESSION[Key::USER_MSG] = array(); + if (!key_exists(Key::USER_MSG, $_SESSION)) $_SESSION[Key::USER_MSG] = array(); $_SESSION[Key::USER_MSG][] = ['level' => $level, 'message' => $message]; } @@ -39,8 +39,7 @@ function add_info(string $message): void { } /** @var bool $is_htmx True if this request was initiated by htmx, false if not */ -$is_htmx = array_key_exists('HTTP_HX_REQUEST', $_SERVER) - && !array_key_exists('HTTP_HX_HISTORY_RESTORE_REQUEST', $_SERVER); +$is_htmx = key_exists('HTTP_HX_REQUEST', $_SERVER) && !key_exists('HTTP_HX_HISTORY_RESTORE_REQUEST', $_SERVER); /** * Render the title bar for the page @@ -54,7 +53,7 @@ function title_bar(): void {
prepare(<<<'SQL'