From 610ab674753c46bdc065402631a43662d246efb6 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 31 May 2024 22:57:24 -0400 Subject: [PATCH] Move domain items up to lib - WIP on converting more complex queries --- src/composer.lock | 8 ++--- src/lib/Data.php | 1 - src/lib/Domain/User.php | 17 --------- src/lib/Feed.php | 2 -- src/lib/{Domain => }/Item.php | 33 ++++++++++++++++-- src/lib/ItemAndFeed.php | 2 -- src/lib/Security.php | 64 +++++++--------------------------- src/lib/{Domain => }/Table.php | 2 +- src/lib/User.php | 52 +++++++++++++++++++++++++++ src/public/bookmark.php | 14 ++++---- src/public/feed/index.php | 15 ++++---- src/public/feeds.php | 57 +++++++++++++++--------------- src/public/item.php | 59 +++++++++++++------------------ src/util/refresh.php | 7 ++-- src/util/search.php | 4 ++- src/util/user.php | 19 ++++++---- 16 files changed, 189 insertions(+), 167 deletions(-) delete mode 100644 src/lib/Domain/User.php rename src/lib/{Domain => }/Item.php (62%) rename src/lib/{Domain => }/Table.php (89%) create mode 100644 src/lib/User.php diff --git a/src/composer.lock b/src/composer.lock index 8767b95..fccb767 100644 --- a/src/composer.lock +++ b/src/composer.lock @@ -12,10 +12,10 @@ "source": { "type": "git", "url": "https://git.bitbadger.solutions/bit-badger/documents-common", - "reference": "60bf3a7d97f06d49db3cacb9a6a84b129a83daa6" + "reference": "30d3ad0621485d0797f2483424a6199ab0021c97" }, "type": "library", - "time": "2024-05-31T16:06:59+00:00" + "time": "2024-06-01T02:26:15+00:00" }, { "name": "bit-badger/documents-sqlite", @@ -23,14 +23,14 @@ "source": { "type": "git", "url": "https://git.bitbadger.solutions/bit-badger/documents-sqlite", - "reference": "9378a62e7ac190ef4bbffdd4330bf83bbe39def0" + "reference": "009ea77b7510fd13936ec4927e2390bccd5d5e70" }, "require": { "bit-badger/documents-common": "dev-conversion", "ext-sqlite3": "*" }, "type": "library", - "time": "2024-05-31T16:07:51+00:00" + "time": "2024-06-01T02:28:46+00:00" } ], "packages-dev": [], diff --git a/src/lib/Data.php b/src/lib/Data.php index d8ee22d..f3c54c7 100644 --- a/src/lib/Data.php +++ b/src/lib/Data.php @@ -9,7 +9,6 @@ use BitBadger\Documents\StringMapper; use DateTimeImmutable; use DateTimeInterface; use Exception; -use FeedReaderCentral\Domain\Table; use SQLite3; /** diff --git a/src/lib/Domain/User.php b/src/lib/Domain/User.php deleted file mode 100644 index 962f712..0000000 --- a/src/lib/Domain/User.php +++ /dev/null @@ -1,17 +0,0 @@ -qualifier = Table::ITEM; + $userField = Field::EQ('user_id', $_SESSION[Key::USER_ID], '@user'); + $userField->qualifier = Table::FEED; + $fields = [$idField, $userField]; + + $where = Query::whereByFields($fields); + $item = Table::ITEM; + $feed = Table::FEED; + return Custom::single( + "SELECT $item.data FROM $item INNER JOIN $feed ON $item.data->>'feed_id' = $feed.data->>'id' WHERE $where", + Parameters::addFields($fields, []), new JsonMapper(Item::class)); + } } diff --git a/src/lib/ItemAndFeed.php b/src/lib/ItemAndFeed.php index e5807e8..38dbb6a 100644 --- a/src/lib/ItemAndFeed.php +++ b/src/lib/ItemAndFeed.php @@ -5,8 +5,6 @@ namespace FeedReaderCentral; use BitBadger\Documents\JsonMapper; use BitBadger\Documents\Mapper; use FeedReaderCentral\Domain\Feed; -use FeedReaderCentral\Domain\Item; -use FeedReaderCentral\Domain\Table; /** * A combined item and feed (used for lists) diff --git a/src/lib/Security.php b/src/lib/Security.php index d75778a..7ce5829 100644 --- a/src/lib/Security.php +++ b/src/lib/Security.php @@ -1,12 +1,9 @@ prepare('SELECT * FROM frc_user WHERE email = :email'); -// $query->bindValue(':email', $email); -// $result = $query->execute(); -// return $result ? $result->fetchArray(SQLITE3_ASSOC) : false; - } - - /** - * Add a user - * - * @param string $email The e-mail address for the user - * @param string $password The user's password - * @param SQLite3 $db The data connection to use to add the user - */ - public static function addUser(string $email, string $password, SQLite3 $db): void { - $user = new User(); - $user->email = $email; - $user->password = $password; - Document::insert(Table::USER, $user, $db); -// $query = $db->prepare('INSERT INTO frc_user (email, password) VALUES (:email, :password)'); -// $query->bindValue(':email', $email); -// $query->bindValue(':password', password_hash($password, self::PW_ALGORITHM)); -// $query->execute(); - } - /** * Verify a user's password * @@ -72,15 +36,13 @@ class Security { * @param string $password The password provided by the user * @param string|null $returnTo The URL to which the user should be redirected * @param SQLite3 $db The database connection to use to verify the user's credentials + * @throws DocumentException if any is encountered */ - private static function verifyPassword(User $user, string $password, ?string $returnTo, SQLite3 $db): void { + private static function verifyPassword(User $user, string $password, ?string $returnTo, SQLite3 $db): void + { if (password_verify($password, $user->password)) { if (password_needs_rehash($user->password, self::PW_ALGORITHM)) { Patch::byId(Table::USER, $user->id, ['password' => password_hash($password, self::PW_ALGORITHM)], $db); -// $rehash = $db->prepare('UPDATE frc_user SET password = :hash WHERE id = :id'); -// $rehash->bindValue(':hash', password_hash($password, self::PW_ALGORITHM)); -// $rehash->bindValue(':id', $user['id']); -// $rehash->execute(); } $_SESSION[Key::USER_ID] = $user['id']; $_SESSION[Key::USER_EMAIL] = $user['email']; @@ -95,6 +57,7 @@ class Security { * @param string $password The password provided by the user * @param string|null $returnTo The URL to which the user should be redirected * @param SQLite3 $db The database connection to use to verify the user's credentials + * @throws DocumentException If any is encountered */ public static function logOnUser(string $email, string $password, ?string $returnTo, SQLite3 $db): void { if (SECURITY_MODEL == self::SINGLE_USER_WITH_PASSWORD) { @@ -106,7 +69,7 @@ class Security { } $dbEmail = $email; } - $user = self::findUserByEmail($dbEmail, $db); + $user = User::findByEmail($dbEmail); if ($user) self::verifyPassword($user, $password, $returnTo, $db); add_error('Invalid credentials; log on unsuccessful'); } @@ -117,26 +80,24 @@ class Security { * @param string $email The e-mail address of the user whose password should be updated * @param string $password The new password for this user * @param SQLite3 $db The database connection to use in updating the password + * @throws DocumentException If any is encountered */ public static function updatePassword(string $email, string $password, SQLite3 $db): void { - Patch::byField(Table::USER, Field::EQ('email', $email), + Patch::byFields(Table::USER, [Field::EQ('email', $email)], ['password' => password_hash($password, self::PW_ALGORITHM)], $db); -// $query = $db->prepare('UPDATE frc_user SET password = :password WHERE email = :email'); -// $query->bindValue(':password', password_hash($password, self::PW_ALGORITHM)); -// $query->bindValue(':email', $email); -// $query->execute(); } /** * Log on the single user * * @param SQLite3 $db The data connection to use to retrieve the user + * @throws DocumentException If any is encountered */ private static function logOnSingleUser(SQLite3 $db): void { - $user = self::findUserByEmail(self::SINGLE_USER_EMAIL, $db); + $user = User::findByEmail(self::SINGLE_USER_EMAIL); if (!$user) { - self::addUser(self::SINGLE_USER_EMAIL, self::SINGLE_USER_PASSWORD, $db); - $user = self::findUserByEmail(self::SINGLE_USER_EMAIL, $db); + User::add(self::SINGLE_USER_EMAIL, self::SINGLE_USER_PASSWORD, $db); + $user = User::findByEmail(self::SINGLE_USER_EMAIL); } self::verifyPassword($user, self::SINGLE_USER_PASSWORD, $_GET['returnTo'], $db); } @@ -146,6 +107,7 @@ class Security { * * @param SQLite3 $db The data connection to use if required * @param bool $redirectIfAnonymous Whether to redirect the request if there is no user logged on + * @throws DocumentException If any is encountered */ public static function verifyUser(SQLite3 $db, bool $redirectIfAnonymous = true): void { if (key_exists(Key::USER_ID, $_SESSION)) return; diff --git a/src/lib/Domain/Table.php b/src/lib/Table.php similarity index 89% rename from src/lib/Domain/Table.php rename to src/lib/Table.php index b9c5a95..c4e2b75 100644 --- a/src/lib/Domain/Table.php +++ b/src/lib/Table.php @@ -1,5 +1,5 @@ email = $email; + $user->password = $password; + Document::insert(Table::USER, $user, $db); + } + +} diff --git a/src/public/bookmark.php b/src/public/bookmark.php index b0ec25f..5100f12 100644 --- a/src/public/bookmark.php +++ b/src/public/bookmark.php @@ -10,10 +10,10 @@ use BitBadger\Documents\DocumentException; use BitBadger\Documents\SQLite\Find; use BitBadger\Documents\SQLite\Patch; use FeedReaderCentral\Data; -use FeedReaderCentral\Domain\Item; -use FeedReaderCentral\Domain\Table; +use FeedReaderCentral\Item; use FeedReaderCentral\Key; use FeedReaderCentral\Security; +use FeedReaderCentral\Table; include '../start.php'; @@ -33,11 +33,11 @@ $exists = $existsResult ? $existsResult->fetchArray(SQLITE3_ASSOC) : false; if (!$exists) not_found(); if (key_exists('action', $_GET)) { - if ($_GET['action'] == 'add') { - $flag = 1; - } elseif ($_GET['action'] == 'remove') { - $flag = 0; - } + $flag = match ($_GET['action']) { + 'add' => 1, + 'remove' => 0, + default => null + }; if (isset($flag)) { try { Patch::byId(Table::ITEM, $id, ['is_bookmarked' => $flag], $db); diff --git a/src/public/feed/index.php b/src/public/feed/index.php index 00e40fe..a442cdf 100644 --- a/src/public/feed/index.php +++ b/src/public/feed/index.php @@ -9,9 +9,9 @@ use BitBadger\Documents\DocumentException; use BitBadger\Documents\Field; use BitBadger\Documents\SQLite\Delete; use FeedReaderCentral\Data; -use FeedReaderCentral\Domain\Table; use FeedReaderCentral\Feed; use FeedReaderCentral\Security; +use FeedReaderCentral\Table; include '../../start.php'; @@ -57,12 +57,15 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { } if ($feedId == 'new') { - $title = 'Add RSS Feed'; - $feed = ['id' => $_GET['id'], 'url' => '']; + $title = 'Add RSS Feed'; + $feed = new Feed(); + $feed->id = $_GET['id']; } else { $title = 'Edit RSS Feed'; if ($feedId == 'error') { - $feed = ['id' => $_POST['id'] ?? '', 'url' => $_POST['url'] ?? '']; + $feed = new Feed(); + $feed->id = $_POST['id'] ?? ''; + $feed->url = $_POST['url'] ?? ''; } elseif (!($feed = Feed::retrieveById((int)$feedId))) not_found(); } @@ -70,10 +73,10 @@ page_head($title); ?>

- > + id?>> diff --git a/src/public/feeds.php b/src/public/feeds.php index c6af217..f975511 100644 --- a/src/public/feeds.php +++ b/src/public/feeds.php @@ -5,9 +5,16 @@ * List feeds and provide links for maintenance actions */ +use BitBadger\Documents\ArrayMapper; +use BitBadger\Documents\Field; +use BitBadger\Documents\JsonMapper; +use BitBadger\Documents\Query; +use BitBadger\Documents\SQLite\Custom; use FeedReaderCentral\Data; +use FeedReaderCentral\Feed; use FeedReaderCentral\Key; use FeedReaderCentral\Security; +use FeedReaderCentral\Table; include '../start.php'; @@ -15,42 +22,34 @@ $db = Data::getConnection(); Security::verifyUser($db); // TODO: adapt query when document list is done -$feedQuery = $db->prepare('SELECT * FROM feed WHERE user_id = :user ORDER BY lower(title)'); -$feedQuery->bindValue(':user', $_SESSION[Key::USER_ID]); -if (!($feedResult = $feedQuery->execute())) { - add_error(Data::error($db)['error']); -} +$field = Field::EQ('user_id', $_SESSION[Key::USER_ID], '@user'); +$feeds = Custom::list(Query\Find::byFields(Table::FEED, [$field]) . " ORDER BY lower(data->>'title')", + $field->toParameter(), new JsonMapper(Feed::class)); page_head('Your Feeds'); ?>

Your Feeds

fetchArray(SQLITE3_ASSOC)) { - $feedId = $feed['id']; - $countQuery = $db->prepare(<<<'SQL' - SELECT (SELECT COUNT(*) FROM item WHERE feed_id = :feed) AS total, - (SELECT COUNT(*) FROM item WHERE feed_id = :feed AND is_read = 0) AS unread, - (SELECT COUNT(*) FROM item WHERE feed_id = :feed AND is_bookmarked = 1) AS marked - SQL); - $countQuery->bindValue(':feed', $feed['id']); - $countResult = $countQuery->execute(); - $counts = $countResult - ? $countResult->fetchArray(SQLITE3_ASSOC) : ['total' => 0, 'unread' => 0, 'marked' => 0]; ?> -


- Last Updated • - As of
- • Read - 0 ? hx_get("/feed/items?id=$feedId&unread", 'Unread') : 'Unread'?> + iterator_apply($feeds->items(), function (Feed $feed) { + $item = Table::ITEM; + $counts = Custom::single(<<>'feed_id' = @feed) AS total, + (SELECT COUNT(*) FROM $item WHERE data->>'feed_id' = @feed AND data->>'is_read' = '0') AS unread, + (SELECT COUNT(*) FROM $item WHERE data->>'feed_id' = @feed AND data->>'is_bookmarked' = '1') AS marked + SQL, ['@feed' => $feed->id], new ArrayMapper()) ?? ['total' => 0, 'unread' => 0, 'marked' => 0]; ?> +

title)?>
+ Last Updated updated_on)?> • + As of checked_on)?>
+ id", 'Edit')?> • Read + 0 ? hx_get("/feed/items?id=$feed->id&unread", 'Unread') : 'Unread'?> () | - 0 ? hx_get("/feed/items?id=$feedId", 'All') : 'All'?> () | - 0 ? hx_get("/feed/items?id=$feedId&bookmarked", 'Bookmarked') : 'Bookmarked'?> + 0 ? hx_get("/feed/items?id=$feed->id", 'All') : 'All'?> () | + 0 ? hx_get("/feed/items?id=$feed->id&bookmarked", 'Bookmarked') : 'Bookmarked'?> () • - hx-delete=/feed/?id= - hx-confirm="Are you sure you want to delete “”? This will remove the feed and all its items, including unread and bookmarked.">Delete -
+ id?> hx-delete=/feed/?id=id?> + hx-confirm="Are you sure you want to delete “title)?>”? This will remove the feed and all its items, including unread and bookmarked.">Delete +

close(); diff --git a/src/public/item.php b/src/public/item.php index 8be3978..dc7bc3a 100644 --- a/src/public/item.php +++ b/src/public/item.php @@ -6,11 +6,14 @@ * Retrieves and displays an item from a feed belonging to the current user */ +use BitBadger\Documents\DocumentException; +use BitBadger\Documents\SQLite\Delete; use BitBadger\Documents\SQLite\Patch; use FeedReaderCentral\Data; -use FeedReaderCentral\Domain\Table; +use FeedReaderCentral\Item; use FeedReaderCentral\Key; use FeedReaderCentral\Security; +use FeedReaderCentral\Table; include '../start.php'; @@ -18,47 +21,33 @@ $db = Data::getConnection(); Security::verifyUser($db); if ($_SERVER['REQUEST_METHOD'] == 'POST') { - // "Keep as New" button sends a POST request to reset the is_read flag before going back to the list of unread items - $isValidQuery = $db->prepare(<<<'SQL' - SELECT COUNT(*) - FROM item INNER JOIN feed ON feed.id = item.feed_id - WHERE item.id = :id AND feed.user_id = :user - SQL); - $isValidQuery->bindValue(':id', $_POST['id']); - $isValidQuery->bindValue(':user', $_SESSION[Key::USER_ID]); - $isValidResult = $isValidQuery->execute(); - if ($isValidResult && $isValidResult->fetchArray(SQLITE3_NUM)[0] == 1) { - Patch::byId(Table::ITEM, $_POST['id'], ['is_read' => 0], $db); -// $keepUnread = $db->prepare('UPDATE item SET is_read = 0 WHERE id = :id'); -// $keepUnread->bindValue(':id', $_POST['id']); -// $keepUnread->execute(); + try { + // "Keep as New" button sends a POST request to reset the is_read flag before going back to the item list + if (Item::retrieveByIdForUser($_POST['id'])) { + Patch::byId(Table::ITEM, $_POST['id'], ['is_read' => 0], $db); + } + $db->close(); + frc_redirect($_POST['from']); + } catch (DocumentException $ex) { + add_error("$ex"); } - $db->close(); - frc_redirect($_POST['from']); } $from = $_GET['from'] ?? '/'; if ($_SERVER['REQUEST_METHOD'] == 'DELETE') { - $deleteQuery = $db->prepare(<<<'SQL' - DELETE FROM item - WHERE id IN ( - SELECT item.id - FROM item INNER JOIN feed ON feed.id = item.feed_id - WHERE item.id = :id - AND feed.user_id = :user) - SQL); - $deleteQuery->bindValue(':id', $_GET['id']); - $deleteQuery->bindValue(':user', $_SESSION[Key::USER_ID]); - if ($deleteQuery->execute()) { - add_info('Item deleted'); - } else { - add_error(Data::error($db)['error']); + try { + if (Item::retrieveByIdForUser($_GET['id'])) { + Delete::byId(Table::ITEM, $_GET['id'], $db); + } + } catch (DocumentException $ex) { + add_error("$ex"); } $db->close(); frc_redirect($from); } +// TODO: convert this query $query = $db->prepare(<<<'SQL' SELECT item.title AS item_title, item.item_link, item.published_on, item.updated_on, item.content, feed.title AS feed_title @@ -72,9 +61,11 @@ $result = $query->execute(); $item = $result ? $result->fetchArray(SQLITE3_ASSOC) : false; if ($item) { - $markRead = $db->prepare('UPDATE item SET is_read = 1 WHERE id = :id'); - $markRead->bindValue(':id', $_GET['id']); - $markRead->execute(); + try { + Patch::byId(Table::ITEM, $_GET['id'], ['is_read' => 1], $db); + } catch (DocumentException $ex) { + add_error("$ex"); + } } $published = date_time($item['published_on']); diff --git a/src/util/refresh.php b/src/util/refresh.php index 8bd1741..5368856 100644 --- a/src/util/refresh.php +++ b/src/util/refresh.php @@ -3,10 +3,9 @@ use BitBadger\Documents\DocumentException; use BitBadger\Documents\SQLite\Find; use FeedReaderCentral\Data; -use FeedReaderCentral\Domain\Feed as FeedDocument; -use FeedReaderCentral\Domain\Table; -use FeedReaderCentral\Domain\User; use FeedReaderCentral\Feed; +use FeedReaderCentral\Table; +use FeedReaderCentral\User; require __DIR__ . '/../cli-start.php'; @@ -41,7 +40,7 @@ function refresh_all(): void try { $users = []; - iterator_apply(Feed::retrieveAll()->items(), function (FeedDocument $feed) use ($db, $users) { + iterator_apply(Feed::retrieveAll()->items(), function (Feed $feed) use ($db, $users) { $result = Feed::refreshFeed($feed->id, $feed->url, $db); $userKey = "$feed->user_id"; if (!key_exists($userKey, $users)) $users[$userKey] = Find::byId(Table::USER, $feed->user_id, User::class); diff --git a/src/util/search.php b/src/util/search.php index f0c92f5..cb4a1dc 100644 --- a/src/util/search.php +++ b/src/util/search.php @@ -1,5 +1,6 @@ exec("INSERT INTO item_search (item_search) VALUES ('rebuild')"); printfn(PHP_EOL . 'Search index rebuilt'); + } catch (DocumentException $ex) { + printfn("$ex"); } finally { $db->close(); } diff --git a/src/util/user.php b/src/util/user.php index a9d63d1..70df6e9 100644 --- a/src/util/user.php +++ b/src/util/user.php @@ -6,8 +6,9 @@ use BitBadger\Documents\SQLite\Count; use BitBadger\Documents\SQLite\Delete; use BitBadger\Documents\SQLite\Patch; use FeedReaderCentral\Data; -use FeedReaderCentral\Domain\Table; use FeedReaderCentral\Security; +use FeedReaderCentral\Table; +use FeedReaderCentral\User; require __DIR__ . '/../cli-start.php'; @@ -99,15 +100,17 @@ function add_user(): void try { // Ensure there is not already a user with this e-mail address - $user = Security::findUserByEmail($argv[2], $db); + $user = User::findByEmail($argv[2]); if ($user) { printfn('A user with e-mail address "%s" already exists', $argv[2]); return; } - Security::addUser($argv[2], $argv[3], $db); + User::add($argv[2], $argv[3], $db); printfn('User "%s" with password "%s" added successfully', $argv[2], $argv[3]); + } catch (DocumentException $ex) { + printfn("$ex"); } finally { $db->close(); } @@ -134,7 +137,7 @@ function set_password(string $email, string $password): void $displayUser = display_user($email); // Ensure this user exists - $user = Security::findUserByEmail($email, $db); + $user = User::findByEmail($email); if (!$user) { printfn('No %s exists', $displayUser); return; @@ -145,6 +148,8 @@ function set_password(string $email, string $password): void $msg = $email == Security::SINGLE_USER_EMAIL && $password == Security::SINGLE_USER_PASSWORD ? 'reset' : sprintf('set to "%s"', $password); printfn('%s password %s successfully', init_cap($displayUser), $msg); + } catch (DocumentException $ex) { + printfn("$ex"); } finally { $db->close(); } @@ -163,7 +168,7 @@ function delete_user(string $email): void $displayUser = display_user($email); // Get the user for the provided e-mail address - $user = Security::findUserByEmail($email, $db); + $user = User::findByEmail($email); if (!$user) { printfn('No %s exists', $displayUser); return; @@ -195,6 +200,8 @@ function delete_user(string $email): void } catch (DocumentException $ex) { printfn("$ex"); } + } catch (DocumentException $ex) { + printfn("$ex"); } finally { $db->close(); } @@ -210,7 +217,7 @@ function migrate_single_user(): void $db = Data::getConnection(); try { - if (!$single = Security::findUserByEmail(Security::SINGLE_USER_EMAIL, $db)) { + if (!$single = User::findByEmail(Security::SINGLE_USER_EMAIL)) { printfn('There is no single-user mode user to be migrated'); return; }