beta4 changes (#26)

These changes are mostly in underlying libraries; however, this now uses the [inspired by F#](https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp) library to handle the feed parsing pipeline and optional return values

Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
2024-08-06 23:20:17 +00:00
parent dfd9a873f8
commit d06249aecd
35 changed files with 867 additions and 535 deletions
+16 -13
View File
@@ -1,11 +1,16 @@
<?php declare(strict_types=1);
<?php
/**
* Bookmark Partial Handler
*
* This will display a button which will either add or remove a bookmark for a given item.
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\InspiredByFSharp\Option;
use BitBadger\PDODocument\{DocumentException, Patch};
use FeedReaderCentral\{ItemWithFeed, Table};
@@ -13,24 +18,22 @@ include '../start.php';
FeedReaderCentral\Security::verifyUser();
$id = key_exists('id', $_GET) ? (int)$_GET['id'] : -1;
if (!$item = ItemWithFeed::retrieveById($id)) not_found();
$id = key_exists('id', $_GET) ? (int)$_GET['id'] : -1;
$item = ItemWithFeed::retrieveById($id)->getOrCall(not_found(...));
if (key_exists('action', $_GET)) {
$flag = match ($_GET['action']) {
'add' => 1,
'remove' => 0,
default => null
};
if (isset($flag)) {
(match ($_GET['action']) {
'add' => Option::Some(1),
'remove' => Option::Some(0),
default => Option::None(),
})->iter(function (int $flag) use ($id, &$item) {
try {
Patch::byId(Table::ITEM, $id, ['is_bookmarked' => $flag]);
Patch::byId(Table::Item, $id, ['is_bookmarked' => $flag]);
$item->is_bookmarked = $flag;
} catch (DocumentException $ex) {
add_error("$ex");
}
}
});
}
$action = $item->isBookmarked() ? 'remove' : 'add';
+7 -1
View File
@@ -1,4 +1,10 @@
<?php declare(strict_types=1);
<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
include '../../start.php';
+7 -1
View File
@@ -1,4 +1,10 @@
<?php declare(strict_types=1);
<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
include '../../start.php';
+7 -1
View File
@@ -1,4 +1,10 @@
<?php declare(strict_types=1);
<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
include '../../start.php';
+12 -6
View File
@@ -1,4 +1,10 @@
<?php declare(strict_types=1);
<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
include '../../start.php';
@@ -38,7 +44,7 @@ page_head('Security Modes | Documentation'); ?>
<p>In Single-User mode, the application uses a known e-mail address and password to mimic multi-user mode where that
user is always logged on. If you have been using the application this way, and decide that you want to run in
multi-user mode instead, you will need to update <code>SECURITY_MODEL</code> in <code>user-config.php</code> to
<code>Security::MULTI_USER</code>.
<code>Security::MultiUserMode</code>.
<p>The e-mail address used for Single-User mode is not allowed to log on in Multi-User mode. If you want to preserve
the feeds defined by the single user, use the CLI to replace its e-mail address and password.
<p><code>php-cli utils/user.php migrate-single-user dave@example.com Dav3sPas$wort</code>
@@ -49,13 +55,13 @@ page_head('Security Modes | Documentation'); ?>
displays feeds from the Single-User mode user. The information for the other users remains in the database,
though, so this change is not destructive.
<h2 id=change-single-to-pw>Changing from Single-User to Single-User with Password Mode</h2>
<p>Set <code>SECURITY_MODEL</code> in <code>user-config.php</code> to
<code>Security::SINGLE_USER_WITH_PASSWORD</code>, then use the <code>user</code> CLI utility to set a password.
<p>Set <code>SECURITY_MODEL</code> in <code>user-config.php</code> to <code>Security::SingleUserPasswordMode</code>,
then use the <code>user</code> CLI utility to set a password.
<p><code>php-cli util/user.php set-single-password aNiceC0mplexPassw0rd</code>
<h2 id=change-pw-to-single>Changing from Single-User with Password to Single-User Mode</h2>
<p>If you decide you do not want to enter a password, but want to maintain single-user mode, set
<code>SECURITY_MODEL</code> in <code>user-config.php</code> to <code>Security::SINGLE_USER</code>, then run the
<code>user</code> CLI utility to reset the single user back to its expected default.
<code>SECURITY_MODEL</code> in <code>user-config.php</code> to <code>Security::SingleUserMode</code>, then run
the <code>user</code> CLI utility to reset the single user back to its expected default.
<p><code>php-cli util/user.php reset-single-password</code>
</article><?php
page_foot();
+7 -1
View File
@@ -1,4 +1,10 @@
<?php declare(strict_types=1);
<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
include '../../start.php';
+42 -36
View File
@@ -1,11 +1,16 @@
<?php declare(strict_types=1);
<?php
/**
* Add/Edit/Delete Feed Page
*
* Allows users to add, edit, and delete feeds
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\InspiredByFSharp\Result;
use BitBadger\PDODocument\{Delete, DocumentException, Field};
use FeedReaderCentral\{Feed, Security, Table};
@@ -15,39 +20,40 @@ Security::verifyUser();
$feedId = key_exists('id', $_GET) ? (int)$_GET['id'] : -1;
if ($_SERVER['REQUEST_METHOD'] == 'DELETE') {
try {
if (!($feed = Feed::retrieveById($feedId))) not_found();
Delete::byFields(Table::ITEM, [Field::EQ('feed_id', $feed->id)]);
Delete::byId(Table::FEED, $feed->id);
add_info('Feed &ldquo;' . htmlentities($feed->title) . '&rdquo; deleted successfully');
frc_redirect('/feeds');
} catch (DocumentException $ex) {
add_error("$ex");
}
}
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
try {
$isNew = $_POST['id'] == '-1';
if ($isNew) {
$result = Feed::add($_POST['url']);
} else {
$feedId = (int)$_POST['id'];
$toEdit = Feed::retrieveById($feedId);
$result = $toEdit
? Feed::update($toEdit, $_POST['url'])
: ['error' => "Feed $feedId not found"];
}
if (key_exists('ok', $result)) {
add_info('Feed saved successfully');
switch ($_SERVER['REQUEST_METHOD']) {
case 'DELETE':
try {
$feed = Feed::retrieveById($feedId)->getOrCall(not_found(...));
Delete::byFields(Table::Item, [Field::EQ('feed_id', $feed->id)]);
Delete::byId(Table::Feed, $feed->id);
add_info('Feed &ldquo;' . htmlentities($feed->title) . '&rdquo; deleted successfully');
frc_redirect('/feeds');
} catch (DocumentException $ex) {
add_error("$ex");
}
add_error($result['error']);
$feedId = 'error';
} catch (DocumentException $ex) {
add_error("$ex");
}
break;
case 'POST':
try {
if ((int)$_POST['id'] === -1) {
$result = Feed::add($_POST['url']);
} else {
$feedId = (int)$_POST['id'];
$toEdit = Feed::retrieveById($feedId);
$result = $toEdit->isSome()
? Feed::update($toEdit->get(), $_POST['url'])
: Result::Error("Feed $feedId not found");
}
$result->iter(function () {
add_info('Feed saved successfully');
frc_redirect('/feeds');
});
add_error($result->getError());
$feedId = 'error';
} catch (DocumentException $ex) {
add_error("$ex");
}
break;
}
if ($feedId == -1) {
@@ -55,9 +61,9 @@ if ($feedId == -1) {
$feed = new Feed(id: -1);
} else {
$title = 'Edit RSS Feed';
if ($feedId == 'error') {
$feed = new Feed(id: (int)$_POST['id'], url: $_POST['url'] ?? '');
} elseif (!($feed = Feed::retrieveById((int)$feedId))) not_found();
$feed = $feedId == 'error'
? new Feed(id: (int)$_POST['id'], url: $_POST['url'] ?? '')
: Feed::retrieveById((int)$feedId)->getOrCall(not_found(...));
}
page_head($title); ?>
+10 -6
View File
@@ -1,19 +1,23 @@
<?php declare(strict_types=1);
<?php
/**
* Feed Item List Page
*
* Lists items in a given feed (all, unread, or bookmarked)
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use FeedReaderCentral\{Feed, ItemList};
include '../../start.php';
FeedReaderCentral\Security::verifyUser();
$id = key_exists('id', $_GET) ? (int)$_GET['id'] : -1;
if (!($feed = Feed::retrieveById($id))) not_found();
$id = key_exists('id', $_GET) ? (int)$_GET['id'] : -1;
$feed = Feed::retrieveById($id)->getOrCall(not_found(...));
$list = match (true) {
key_exists('unread', $_GET) => ItemList::unreadForFeed($feed->id),
@@ -21,8 +25,8 @@ $list = match (true) {
default => ItemList::allForFeed($feed->id)
};
page_head(($list->itemType != '' ? "$list->itemType Items | " : '') . strip_tags($feed->title));
if ($list->itemType == '') {
page_head(($list->itemType === '' ? '' : "$list->itemType Items | ") . strip_tags($feed->title));
if ($list->itemType === '') {
echo '<h1>' . htmlentities($feed->title) . '</h1>';
} else {
echo '<h1 class=item_heading>' . htmlentities($feed->title) . '</h1>';
+12 -8
View File
@@ -1,11 +1,15 @@
<?php declare(strict_types=1);
<?php
/**
* Feed Maintenance Page
*
* List feeds and provide links for maintenance actions
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\PDODocument\{Custom, Field, Query};
use BitBadger\PDODocument\Mapper\{ArrayMapper, DocumentMapper};
use FeedReaderCentral\{Feed, Key, Table};
@@ -14,19 +18,19 @@ include '../start.php';
FeedReaderCentral\Security::verifyUser();
$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 = Field::EQ('user_id', $_SESSION[Key::UserId], ':user');
$feeds = Custom::list(Query\Find::byFields(Table::Feed, [$field]) . " ORDER BY lower(data->>'title')",
$field->appendParameter([]), new DocumentMapper(Feed::class));
page_head('Your Feeds');
echo '<h1>Your Feeds</h1><article><p class=action_buttons>' . hx_get('/feed/?id=-1', 'Add Feed') . '</p>';
foreach ($feeds->items() as /** @var Feed $feed */ $feed) {
$item = Table::ITEM;
$feeds->iter(function (Feed $feed) {
$item = Table::Item;
$counts = Custom::single(<<<SQL
SELECT (SELECT COUNT(*) FROM $item WHERE data->>'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];
SQL, [':feed' => $feed->id], new ArrayMapper())->getOrDefault(['total' => 0, 'unread' => 0, 'marked' => 0]);
echo '<p><strong>' . htmlentities($feed->title) . '</strong><br>'
. '<span class=meta><em>Last Updated ' . date_time($feed->updated_on) . ' &bull; '
. 'As of ' . date_time($feed->checked_on) . '</em><br>' . hx_get("/feed/?id=$feed->id", 'Edit') . ' &bull; '
@@ -39,6 +43,6 @@ foreach ($feeds->items() as /** @var Feed $feed */ $feed) {
. ' hx-confirm="Are you sure you want to delete &ldquo;' . htmlspecialchars($feed->title)
. '&rdquo;? This will remove the feed and all its items, including unread and bookmarked.">Delete</a>'
. '</span>';
}
});
echo '</article>';
page_foot();
+9 -5
View File
@@ -1,11 +1,15 @@
<?php declare(strict_types=1);
<?php
/**
* Home Page
*
* Displays a list of unread or bookmarked items for the current user
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use FeedReaderCentral\{Feed, ItemList};
include '../start.php';
@@ -14,16 +18,16 @@ FeedReaderCentral\Security::verifyUser();
if (key_exists('refresh', $_GET)) {
$refreshResult = Feed::refreshAll();
if (key_exists('ok', $refreshResult)) {
if ($refreshResult->isOK()) {
add_info('All feeds refreshed successfully');
} else {
add_error(nl2br($refreshResult['error']));
add_error(nl2br($refreshResult->getError()));
}
}
$list = match (true) {
key_exists('bookmarked', $_GET) => ItemList::allBookmarked(),
default => ItemList::allUnread()
default => ItemList::allUnread(),
};
$title = "Your $list->itemType Items";
+32 -25
View File
@@ -1,11 +1,15 @@
<?php declare(strict_types=1);
<?php
/**
* Item View Page
*
* Retrieves and displays an item from a feed belonging to the current user
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\PDODocument\{Delete, DocumentException, Patch};
use FeedReaderCentral\{ItemWithFeed, Table};
@@ -16,35 +20,38 @@ FeedReaderCentral\Security::verifyUser();
$id = match (true) {
key_exists('id', $_POST) => (int)$_POST['id'],
key_exists('id', $_GET) => (int)$_GET['id'],
default => -1
default => -1,
};
$from = match ($_SERVER['REQUEST_METHOD']) {
'POST' => $_POST['from'],
default => $_GET['from'] ?? '/',
};
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
try {
// "Keep as New" button sends a POST request to reset the is_read flag before going back to the item list
if (ItemWithFeed::existsById($id)) {
Patch::byId(Table::ITEM, $id, ['is_read' => 0]);
switch ($_SERVER['REQUEST_METHOD']) {
case 'POST':
try {
// "Keep as New" button sends a POST request to reset the is_read flag before going back to the item list
if (ItemWithFeed::existsById($id)) {
Patch::byId(Table::Item, $id, ['is_read' => 0]);
}
frc_redirect($from);
} catch (DocumentException $ex) {
add_error("$ex");
}
frc_redirect($_POST['from']);
} catch (DocumentException $ex) {
add_error("$ex");
}
break;
case 'DELETE':
try {
if (ItemWithFeed::existsById($id)) Delete::byId(Table::Item, $id);
} catch (DocumentException $ex) {
add_error("$ex");
}
frc_redirect($from);
}
$from = $_GET['from'] ?? '/';
if ($_SERVER['REQUEST_METHOD'] == 'DELETE') {
try {
if (ItemWithFeed::existsById($id)) Delete::byId(Table::ITEM, $id);
} catch (DocumentException $ex) {
add_error("$ex");
}
frc_redirect($from);
}
if (!$item = ItemWithFeed::retrieveById($id)) not_found();
$item = ItemWithFeed::retrieveById($id)->getOrCall(not_found(...));
try {
Patch::byId(Table::ITEM, $id, ['is_read' => 1]);
Patch::byId(Table::Item, $id, ['is_read' => 1]);
} catch (DocumentException $ex) {
add_error("$ex");
}
+10 -6
View File
@@ -1,11 +1,15 @@
<?php declare(strict_types=1);
<?php
/**
* Item Search Page
*
* Search for items across all feeds
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
include '../start.php';
FeedReaderCentral\Security::verifyUser();
@@ -13,8 +17,8 @@ FeedReaderCentral\Security::verifyUser();
$search = $_GET['search'] ?? '';
$items = $_GET['items'] ?? 'all';
if ($search != '') {
$list = FeedReaderCentral\ItemList::matchingSearch($search, $items == 'bookmarked');
if ($search !== '') {
$list = FeedReaderCentral\ItemList::matchingSearch($search, $items === 'bookmarked');
}
page_head('Item Search'); ?>
@@ -28,8 +32,8 @@ page_head('Item Search'); ?>
<label>
Items to Search
<select name=items>
<option value=all <?=$items == 'all' ? ' selected' : ''?>>All</option>
<option value=bookmarked <?=$items == 'bookmarked' ? ' selected' : ''?>>Bookmarked</option>
<option value=all <?=$items === 'all' ? ' selected' : ''?>>All</option>
<option value=bookmarked <?=$items === 'bookmarked' ? ' selected' : ''?>>Bookmarked</option>
</select>
</label>
<span class=break></span>
+7 -3
View File
@@ -1,11 +1,15 @@
<?php declare(strict_types=1);
<?php
/**
* User Log Off Page
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
include '../../start.php';
if (key_exists(FeedReaderCentral\Key::USER_ID, $_SESSION)) session_destroy();
if (key_exists(FeedReaderCentral\Key::UserId, $_SESSION)) session_destroy();
frc_redirect('/');
+12 -7
View File
@@ -1,34 +1,39 @@
<?php declare(strict_types=1);
<?php
/**
* User Log On Page
*
* Accepts the user's e-mail address (multi-user) and password (multi-user or single-user-with-password) and attempts
* to log them on to Feed Reader Central
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
include '../../start.php';
use BitBadger\InspiredByFSharp\Option;
use FeedReaderCentral\{Key, Security};
Security::verifyUser(redirectIfAnonymous: false);
// Users already logged on have no need of this page
if (key_exists(Key::USER_ID, $_SESSION)) frc_redirect('/');
if (key_exists(Key::UserId, $_SESSION)) frc_redirect('/');
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
Security::logOnUser($_POST['email'] ?? '', $_POST['password'], $_POST['returnTo'] ?? null);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
Security::logOnUser($_POST['email'] ?? '', $_POST['password'], Option::of($_POST['returnTo'] ?? null));
// If we're still here, something didn't work; preserve the returnTo parameter
$_GET['returnTo'] = $_POST['returnTo'];
}
$isSingle = SECURITY_MODEL == Security::SINGLE_USER_WITH_PASSWORD;
$isSingle = SECURITY_MODEL === Security::SingleUserPasswordMode;
page_head('Log On'); ?>
<h1>Log On</h1>
<article>
<form method=POST action=/user/log-on><?php
if (($_GET['returnTo'] ?? '') != '') { ?>
if (($_GET['returnTo'] ?? '') !== '') { ?>
<input type=hidden name=returnTo value="<?=$_GET['returnTo']?>"><?php
}
if (!$isSingle) { ?>