beta4 changes #26

Merged
danieljsummers merged 7 commits from beta5 into main 2024-08-06 23:20:17 +00:00
19 changed files with 72 additions and 228 deletions
Showing only changes of commit f5feef177a - Show all commits

View File

@ -3,14 +3,12 @@
"minimum-stability": "beta", "minimum-stability": "beta",
"require": { "require": {
"php": ">=8.2", "php": ">=8.2",
"bit-badger/inspired-by-fsharp": "@dev",
"bit-badger/pdo-document": "^1", "bit-badger/pdo-document": "^1",
"ext-curl": "*", "ext-curl": "*",
"ext-dom": "*", "ext-dom": "*",
"ext-pdo": "*", "ext-pdo": "*",
"ext-readline": "*", "ext-readline": "*",
"ext-sqlite3": "*", "ext-sqlite3": "*"
"graham-campbell/result-type": "^1.1"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

162
src/composer.lock generated
View File

@ -4,24 +4,23 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "2caacda2b3694b265db846cd539e1ca3", "content-hash": "2966efd32e555ad8b63351673e75b5a5",
"packages": [ "packages": [
{ {
"name": "bit-badger/inspired-by-fsharp", "name": "bit-badger/inspired-by-fsharp",
"version": "dev-main", "version": "v1.0.0-beta1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp", "url": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp",
"reference": "7d25b9ea282c1e244fec6e14a1e1b251314824e4" "reference": "efb3a4461edcb23e0dd82068adeb0591240870b0"
}, },
"require": { "require": {
"php": "^8" "php": "^8.2"
}, },
"require-dev": { "require-dev": {
"phpoption/phpoption": "^1", "phpoption/phpoption": "^1",
"phpunit/phpunit": "^11" "phpunit/phpunit": "^11"
}, },
"default-branch": true,
"type": "library", "type": "library",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -50,21 +49,21 @@
"rss": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp.rss", "rss": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp.rss",
"source": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp" "source": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp"
}, },
"time": "2024-07-28T01:20:35+00:00" "time": "2024-07-28T21:35:11+00:00"
}, },
{ {
"name": "bit-badger/pdo-document", "name": "bit-badger/pdo-document",
"version": "v1.0.0-beta7", "version": "v1.0.0-beta8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://git.bitbadger.solutions/bit-badger/pdo-document", "url": "https://git.bitbadger.solutions/bit-badger/pdo-document",
"reference": "57d8f9ddc17169883f7dd77e51dea1443040858b" "reference": "039283224a173bd3e8a9bc5de0caf349ba7b58e6"
}, },
"require": { "require": {
"bit-badger/inspired-by-fsharp": "^1",
"ext-pdo": "*", "ext-pdo": "*",
"netresearch/jsonmapper": "^4", "netresearch/jsonmapper": "^4",
"php": ">=8.2", "php": ">=8.2"
"phpoption/phpoption": "^1.9"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11", "phpunit/phpunit": "^11",
@ -104,69 +103,7 @@
"rss": "https://git.bitbadger.solutions/bit-badger/pdo-document.rss", "rss": "https://git.bitbadger.solutions/bit-badger/pdo-document.rss",
"source": "https://git.bitbadger.solutions/bit-badger/pdo-document" "source": "https://git.bitbadger.solutions/bit-badger/pdo-document"
}, },
"time": "2024-07-25T00:57:23+00:00" "time": "2024-07-29T00:08:44+00:00"
},
{
"name": "graham-campbell/result-type",
"version": "v1.1.3",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3"
},
"require-dev": {
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"type": "library",
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
"type": "tidelift"
}
],
"time": "2024-07-20T21:45:45+00:00"
}, },
{ {
"name": "netresearch/jsonmapper", "name": "netresearch/jsonmapper",
@ -218,89 +155,12 @@
"source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1" "source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1"
}, },
"time": "2024-01-31T06:18:54+00:00" "time": "2024-01-31T06:18:54+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.3",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54",
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
"type": "tidelift"
}
],
"time": "2024-07-20T21:41:07+00:00"
} }
], ],
"packages-dev": [], "packages-dev": [],
"aliases": [], "aliases": [],
"minimum-stability": "beta", "minimum-stability": "beta",
"stability-flags": { "stability-flags": [],
"bit-badger/inspired-by-fsharp": 20
},
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {

View File

@ -104,7 +104,7 @@ class Feed
try { try {
$tryExisting = Find::firstByFields(Table::Item, $tryExisting = Find::firstByFields(Table::Item,
[Field::EQ('feed_id', $feedId), Field::EQ('item_guid', $item->guid)], Item::class); [Field::EQ('feed_id', $feedId), Field::EQ('item_guid', $item->guid)], Item::class);
if ($tryExisting->isDefined()) { if ($tryExisting->isSome()) {
$existing = $tryExisting->get(); $existing = $tryExisting->get();
if ($existing->published_on !== $item->publishedOn if ($existing->published_on !== $item->publishedOn
|| ($existing->updated_on !== ($item->updatedOn ?? ''))) { || ($existing->updated_on !== ($item->updatedOn ?? ''))) {
@ -180,16 +180,16 @@ class Feed
public static function refreshFeed(int $feedId, string $url): Result public static function refreshFeed(int $feedId, string $url): Result
{ {
$tryRetrieve = ParsedFeed::retrieve($url); $tryRetrieve = ParsedFeed::retrieve($url);
if (Result::isError($tryRetrieve)) return $tryRetrieve; if ($tryRetrieve->isError()) return $tryRetrieve;
$feed = $tryRetrieve->getOK(); $feed = $tryRetrieve->getOK();
try { try {
$feedDoc = Find::byId(Table::Feed, $feedId, self::class); $feedDoc = Find::byId(Table::Feed, $feedId, self::class);
if ($feedDoc->isEmpty()) return Result::Error('Could not derive date last checked for feed'); if ($feedDoc->isNone()) return Result::Error('Could not derive date last checked for feed');
$lastChecked = date_create_immutable($feedDoc->get()->checked_on ?? WWW_EPOCH); $lastChecked = date_create_immutable($feedDoc->get()->checked_on ?? WWW_EPOCH);
$itemUpdate = self::updateItems($feedId, $feed, $lastChecked); $itemUpdate = self::updateItems($feedId, $feed, $lastChecked);
if (Result::isError($itemUpdate)) return $itemUpdate; if ($itemUpdate->isError()) return $itemUpdate;
$patch = [ $patch = [
'title' => $feed->title, 'title' => $feed->title,
@ -214,7 +214,7 @@ class Feed
public static function add(string $url): Result public static function add(string $url): Result
{ {
$tryRetrieve = ParsedFeed::retrieve($url); $tryRetrieve = ParsedFeed::retrieve($url);
if (Result::isError($tryRetrieve)) return $tryRetrieve; if ($tryRetrieve->isError()) return $tryRetrieve;
$feed = $tryRetrieve->getOK(); $feed = $tryRetrieve->getOK();
try { try {
@ -226,11 +226,11 @@ class Feed
Document::insert(Table::Feed, self::fromParsed($feed)); Document::insert(Table::Feed, self::fromParsed($feed));
$tryDoc = Find::firstByFields(Table::Feed, $fields, self::class); $tryDoc = Find::firstByFields(Table::Feed, $fields, self::class);
if ($tryDoc->isEmpty()) return Result::Error('Could not retrieve inserted feed'); if ($tryDoc->isNone()) return Result::Error('Could not retrieve inserted feed');
$doc = $tryDoc->get(); $doc = $tryDoc->get();
$result = self::updateItems($doc->id, $feed, date_create_immutable(WWW_EPOCH)); $result = self::updateItems($doc->id, $feed, date_create_immutable(WWW_EPOCH));
return Result::isError($result) ? $result : Result::OK($doc->id); return $result->isError() ? $result : Result::OK($doc->id);
} catch (DocumentException $ex) { } catch (DocumentException $ex) {
return Result::Error("$ex"); return Result::Error("$ex");
} }
@ -280,8 +280,8 @@ class Feed
try { try {
self::retrieveAll($_SESSION[Key::UserId])->iter(function (Feed $feed) use (&$errors) { self::retrieveAll($_SESSION[Key::UserId])->iter(function (Feed $feed) use (&$errors) {
Result::mapError(function (string $err) use (&$errors) { $errors[] = $err; }, self::refreshFeed($feed->id, $feed->url)
self::refreshFeed($feed->id, $feed->url)); ->mapError(function (string $err) use (&$errors) { $errors[] = $err; });
}); });
} catch (DocumentException $ex) { } catch (DocumentException $ex) {
return Result::Error("$ex"); return Result::Error("$ex");
@ -299,8 +299,7 @@ class Feed
*/ */
public static function retrieveById(int $feedId): Option public static function retrieveById(int $feedId): Option
{ {
return Option::of( return Find::byId(Table::Feed, $feedId, static::class)
Find::byId(Table::Feed, $feedId, static::class) ->filter(fn($it) => $it->user_id === $_SESSION[Key::UserId]);
->filter(fn($it) => $it->user_id === $_SESSION[Key::UserId]));
} }
} }

View File

@ -8,6 +8,7 @@ declare(strict_types=1);
namespace FeedReaderCentral; namespace FeedReaderCentral;
use BitBadger\InspiredByFSharp\Option;
use BitBadger\PDODocument\{Configuration, Custom, DocumentException, DocumentList, Field, Parameters, Query}; use BitBadger\PDODocument\{Configuration, Custom, DocumentException, DocumentList, Field, Parameters, Query};
use BitBadger\PDODocument\Mapper\DocumentMapper; use BitBadger\PDODocument\Mapper\DocumentMapper;
@ -21,18 +22,8 @@ class ItemList
/** @var DocumentList<ItemWithFeed> The items matching the criteria, lazily iterable */ /** @var DocumentList<ItemWithFeed> The items matching the criteria, lazily iterable */
private DocumentList $dbList; private DocumentList $dbList;
/** @var string The error message generated by executing a query */ /** @var Option<string> The error message generated by executing a query */
public string $error = ''; private Option $error;
/**
* Is there an error condition associated with this list?
*
* @return bool True if there is an error condition associated with this list, false if not
*/
public function isError(): bool
{
return $this->error !== '';
}
/** @var bool Whether to render a link to the feed to which the item belongs */ /** @var bool Whether to render a link to the feed to which the item belongs */
public bool $linkFeed = false; public bool $linkFeed = false;
@ -54,7 +45,8 @@ class ItemList
private function __construct(public string $itemType, public string $returnURL = '', array $fields = [], private function __construct(public string $itemType, public string $returnURL = '', array $fields = [],
string $searchWhere = '') string $searchWhere = '')
{ {
$allFields = [Data::userIdField(Table::Feed), ...$fields]; $this->error = Option::None();
$allFields = [Data::userIdField(Table::Feed), ...$fields];
try { try {
$this->dbList = Custom::list( $this->dbList = Custom::list(
ItemWithFeed::SELECT_WITH_FEED . ' WHERE ' ItemWithFeed::SELECT_WITH_FEED . ' WHERE '
@ -62,7 +54,7 @@ class ItemList
. "$searchWhere ORDER BY coalesce(item.data->>'updated_on', item.data->>'published_on') DESC", . "$searchWhere ORDER BY coalesce(item.data->>'updated_on', item.data->>'published_on') DESC",
Parameters::addFields($allFields, []), new DocumentMapper(ItemWithFeed::class)); Parameters::addFields($allFields, []), new DocumentMapper(ItemWithFeed::class));
} catch (DocumentException $ex) { } catch (DocumentException $ex) {
$this->error = "$ex"; $this->error = Option::Some("$ex");
} }
} }
@ -152,8 +144,8 @@ class ItemList
*/ */
public function render(): void public function render(): void
{ {
if ($this->isError()) { if ($this->error->isSome()) {
echo "<p>Error retrieving list:<br>$this->error"; echo "<p>Error retrieving list:<br>{$this->error->get()}";
return; return;
} }
$return = $this->returnURL === '' ? '' : '&from=' . urlencode($this->returnURL); $return = $this->returnURL === '' ? '' : '&from=' . urlencode($this->returnURL);

View File

@ -68,8 +68,7 @@ class ItemWithFeed extends Item
public static function retrieveById(int $id): Option public static function retrieveById(int $id): Option
{ {
$fields = self::idAndUserFields($id); $fields = self::idAndUserFields($id);
return Option::of( return Custom::single(self::SELECT_WITH_FEED . ' WHERE ' . Query::whereByFields($fields),
Custom::single(self::SELECT_WITH_FEED . ' WHERE ' . Query::whereByFields($fields), Parameters::addFields($fields, []), new DocumentMapper(self::class));
Parameters::addFields($fields, []), new DocumentMapper(self::class)));
} }
} }

View File

@ -240,7 +240,7 @@ readonly class ParsedFeed
public static function retrieve(string $url): Result public static function retrieve(string $url): Result
{ {
$tryDoc = self::retrieveDocument($url); $tryDoc = self::retrieveDocument($url);
if (Result::isError($tryDoc)) return $tryDoc; if ($tryDoc->isError()) return $tryDoc;
$doc = $tryDoc->getOK(); $doc = $tryDoc->getOK();
if ($doc['code'] !== 200) { if ($doc['code'] !== 200) {
@ -250,7 +250,7 @@ readonly class ParsedFeed
$start = strtolower(strlen($doc['content']) >= 9 ? substr($doc['content'], 0, 9) : $doc['content']); $start = strtolower(strlen($doc['content']) >= 9 ? substr($doc['content'], 0, 9) : $doc['content']);
if ($start === '<!doctype' || str_starts_with($start, '<html')) { if ($start === '<!doctype' || str_starts_with($start, '<html')) {
$derivedURL = self::deriveFeedFromHTML($doc['content']); $derivedURL = self::deriveFeedFromHTML($doc['content']);
if (Result::isError($derivedURL)) return $derivedURL; if ($derivedURL->isError()) return $derivedURL;
$feedURL = $derivedURL->getOK(); $feedURL = $derivedURL->getOK();
if (!str_starts_with($feedURL, 'http')) { if (!str_starts_with($feedURL, 'http')) {
// Relative URL; feed should be retrieved in the context of the original URL // Relative URL; feed should be retrieved in the context of the original URL
@ -259,7 +259,7 @@ readonly class ParsedFeed
$feedURL = $original['scheme'] . '://' . $original['host'] . $port . $feedURL; $feedURL = $original['scheme'] . '://' . $original['host'] . $port . $feedURL;
} }
$tryDoc = self::retrieveDocument($feedURL); $tryDoc = self::retrieveDocument($feedURL);
if (Result::isError($tryDoc)) return $tryDoc; if ($tryDoc->isError()) return $tryDoc;
$doc = $tryDoc->getOK(); $doc = $tryDoc->getOK();
if ($doc['code'] !== 200) { if ($doc['code'] !== 200) {
return Result::Error("Derived feed URL $url returned HTTP Code {$doc['code']}: {$doc['content']}"); return Result::Error("Derived feed URL $url returned HTTP Code {$doc['code']}: {$doc['content']}");
@ -267,7 +267,7 @@ readonly class ParsedFeed
} }
$tryParse = self::parseFeed($doc['content']); $tryParse = self::parseFeed($doc['content']);
if (Result::isError($tryParse)) return $tryParse; if ($tryParse->isError()) return $tryParse;
$parsed = $tryParse->getOK(); $parsed = $tryParse->getOK();
$extract = $parsed->getElementsByTagNameNS(self::ATOM_NS, 'feed')->length > 0 $extract = $parsed->getElementsByTagNameNS(self::ATOM_NS, 'feed')->length > 0

View File

@ -60,10 +60,10 @@ readonly class Security
* *
* @param User $user The user information retrieved from the database * @param User $user The user information retrieved from the database
* @param string $password The password provided by the user * @param string $password The password provided by the user
* @param string|null $returnTo The URL to which the user should be redirected * @param Option<string> $returnTo The URL to which the user should be redirected
* @throws DocumentException if any is encountered * @throws DocumentException if any is encountered
*/ */
private static function verifyPassword(User $user, string $password, ?string $returnTo): void private static function verifyPassword(User $user, string $password, Option $returnTo): void
{ {
if (password_verify($password, $user->password)) { if (password_verify($password, $user->password)) {
if (password_needs_rehash($user->password, self::PasswordAlgorithm)) { if (password_needs_rehash($user->password, self::PasswordAlgorithm)) {
@ -71,7 +71,7 @@ readonly class Security
} }
$_SESSION[Key::UserId] = $user->id; $_SESSION[Key::UserId] = $user->id;
$_SESSION[Key::UserEmail] = $user->email; $_SESSION[Key::UserEmail] = $user->email;
frc_redirect($returnTo ?? '/'); frc_redirect($returnTo->getOrDefault('/'));
} }
} }
@ -80,10 +80,10 @@ readonly class Security
* *
* @param string $email The e-mail address for the user (cannot be the single-user mode user) * @param string $email The e-mail address for the user (cannot be the single-user mode user)
* @param string $password The password provided by the user * @param string $password The password provided by the user
* @param string|null $returnTo The URL to which the user should be redirected * @param Option<string> $returnTo The URL to which the user should be redirected
* @throws DocumentException If any is encountered * @throws DocumentException If any is encountered
*/ */
public static function logOnUser(string $email, string $password, ?string $returnTo): void public static function logOnUser(string $email, string $password, Option $returnTo): void
{ {
if (SECURITY_MODEL === self::SingleUserPasswordMode) { if (SECURITY_MODEL === self::SingleUserPasswordMode) {
$dbEmail = self::SingleUserEmail; $dbEmail = self::SingleUserEmail;
@ -94,7 +94,7 @@ readonly class Security
} }
$dbEmail = $email; $dbEmail = $email;
} }
Option::iter(fn(User $it) => self::verifyPassword($it, $password, $returnTo), User::findByEmail($dbEmail)); User::findByEmail($dbEmail)->iter(fn(User $it) => self::verifyPassword($it, $password, $returnTo));
add_error('Invalid credentials; log on unsuccessful'); add_error('Invalid credentials; log on unsuccessful');
} }
@ -118,12 +118,11 @@ readonly class Security
*/ */
private static function logOnSingleUser(): void private static function logOnSingleUser(): void
{ {
$user = User::findByEmail(self::SingleUserEmail); $user = User::findByEmail(self::SingleUserEmail)->getOrCall(function () {
if (Option::isNone($user)) {
User::add(self::SingleUserEmail, self::SingleUserPassword); User::add(self::SingleUserEmail, self::SingleUserPassword);
$user = User::findByEmail(self::SingleUserEmail); return User::findByEmail(self::SingleUserEmail)->get();
} });
self::verifyPassword($user->get(), self::SingleUserPassword, $_GET['returnTo']); self::verifyPassword($user, self::SingleUserPassword, $_GET['returnTo']);
} }
/** /**

View File

@ -35,7 +35,7 @@ class User
*/ */
public static function findByEmail(string $email): Option public static function findByEmail(string $email): Option
{ {
return Option::of(Find::firstByFields(Table::User, [Field::EQ('email', $email)], User::class)); return Find::firstByFields(Table::User, [Field::EQ('email', $email)], User::class);
} }
/** /**

View File

@ -19,20 +19,20 @@ include '../start.php';
FeedReaderCentral\Security::verifyUser(); FeedReaderCentral\Security::verifyUser();
$id = key_exists('id', $_GET) ? (int)$_GET['id'] : -1; $id = key_exists('id', $_GET) ? (int)$_GET['id'] : -1;
$item = Option::getOrCall(not_found(...), ItemWithFeed::retrieveById($id)); $item = ItemWithFeed::retrieveById($id)->getOrCall(not_found(...));
if (key_exists('action', $_GET)) { if (key_exists('action', $_GET)) {
Option::iter(function (int $flag) use ($id, &$item) { (match ($_GET['action']) {
'add' => Option::Some(1),
'remove' => Option::Some(0),
default => Option::None(),
})->iter(function (int $flag) use ($id, &$item) {
try { try {
Patch::byId(Table::Item, $id, ['is_bookmarked' => $flag]); Patch::byId(Table::Item, $id, ['is_bookmarked' => $flag]);
$item->is_bookmarked = $flag; $item->is_bookmarked = $flag;
} catch (DocumentException $ex) { } catch (DocumentException $ex) {
add_error("$ex"); add_error("$ex");
} }
}, match ($_GET['action']) {
'add' => Option::Some(1),
'remove' => Option::Some(0),
default => Option::None(),
}); });
} }

View File

@ -10,7 +10,7 @@
declare(strict_types=1); declare(strict_types=1);
use BitBadger\InspiredByFSharp\{Option, Result}; use BitBadger\InspiredByFSharp\Result;
use BitBadger\PDODocument\{Delete, DocumentException, Field}; use BitBadger\PDODocument\{Delete, DocumentException, Field};
use FeedReaderCentral\{Feed, Security, Table}; use FeedReaderCentral\{Feed, Security, Table};
@ -23,7 +23,7 @@ $feedId = key_exists('id', $_GET) ? (int)$_GET['id'] : -1;
switch ($_SERVER['REQUEST_METHOD']) { switch ($_SERVER['REQUEST_METHOD']) {
case 'DELETE': case 'DELETE':
try { try {
$feed = Option::getOrCall(not_found(...), Feed::retrieveById($feedId)); $feed = Feed::retrieveById($feedId)->getOrCall(not_found(...));
Delete::byFields(Table::Item, [Field::EQ('feed_id', $feed->id)]); Delete::byFields(Table::Item, [Field::EQ('feed_id', $feed->id)]);
Delete::byId(Table::Feed, $feed->id); Delete::byId(Table::Feed, $feed->id);
add_info('Feed &ldquo;' . htmlentities($feed->title) . '&rdquo; deleted successfully'); add_info('Feed &ldquo;' . htmlentities($feed->title) . '&rdquo; deleted successfully');
@ -40,11 +40,11 @@ switch ($_SERVER['REQUEST_METHOD']) {
} else { } else {
$feedId = (int)$_POST['id']; $feedId = (int)$_POST['id'];
$toEdit = Feed::retrieveById($feedId); $toEdit = Feed::retrieveById($feedId);
$result = Option::isSome($toEdit) $result = $toEdit->isSome()
? Feed::update($toEdit->get(), $_POST['url']) ? Feed::update($toEdit->get(), $_POST['url'])
: Result::Error("Feed $feedId not found"); : Result::Error("Feed $feedId not found");
} }
if (Result::isOK($result)) { if ($result->isOK()) {
add_info('Feed saved successfully'); add_info('Feed saved successfully');
frc_redirect('/feeds'); frc_redirect('/feeds');
} }
@ -63,7 +63,7 @@ if ($feedId == -1) {
$title = 'Edit RSS Feed'; $title = 'Edit RSS Feed';
$feed = $feedId == 'error' $feed = $feedId == 'error'
? new Feed(id: (int)$_POST['id'], url: $_POST['url'] ?? '') ? new Feed(id: (int)$_POST['id'], url: $_POST['url'] ?? '')
: Option::getOrCall(not_found(...), Feed::retrieveById((int)$feedId)); : Feed::retrieveById((int)$feedId)->getOrCall(not_found(...));
} }
page_head($title); ?> page_head($title); ?>

View File

@ -10,7 +10,6 @@
declare(strict_types=1); declare(strict_types=1);
use BitBadger\InspiredByFSharp\Option;
use FeedReaderCentral\{Feed, ItemList}; use FeedReaderCentral\{Feed, ItemList};
include '../../start.php'; include '../../start.php';
@ -18,7 +17,7 @@ include '../../start.php';
FeedReaderCentral\Security::verifyUser(); FeedReaderCentral\Security::verifyUser();
$id = key_exists('id', $_GET) ? (int)$_GET['id'] : -1; $id = key_exists('id', $_GET) ? (int)$_GET['id'] : -1;
$feed = Option::getOrCall(not_found(...), Feed::retrieveById($id)); $feed = Feed::retrieveById($id)->getOrCall(not_found(...));
$list = match (true) { $list = match (true) {
key_exists('unread', $_GET) => ItemList::unreadForFeed($feed->id), key_exists('unread', $_GET) => ItemList::unreadForFeed($feed->id),

View File

@ -30,7 +30,7 @@ $feeds->iter(function (Feed $feed) {
SELECT (SELECT COUNT(*) FROM $item WHERE data->>'feed_id' = :feed) AS total, 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_read' = 0) AS unread,
(SELECT COUNT(*) FROM $item WHERE data->>'feed_id' = :feed AND data->>'is_bookmarked' = 1) AS marked (SELECT COUNT(*) FROM $item WHERE data->>'feed_id' = :feed AND data->>'is_bookmarked' = 1) AS marked
SQL, [':feed' => $feed->id], new ArrayMapper())->getOrElse(['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>' echo '<p><strong>' . htmlentities($feed->title) . '</strong><br>'
. '<span class=meta><em>Last Updated ' . date_time($feed->updated_on) . ' &bull; ' . '<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; ' . 'As of ' . date_time($feed->checked_on) . '</em><br>' . hx_get("/feed/?id=$feed->id", 'Edit') . ' &bull; '

View File

@ -10,7 +10,6 @@
declare(strict_types=1); declare(strict_types=1);
use BitBadger\InspiredByFSharp\Result;
use FeedReaderCentral\{Feed, ItemList}; use FeedReaderCentral\{Feed, ItemList};
include '../start.php'; include '../start.php';
@ -19,7 +18,7 @@ FeedReaderCentral\Security::verifyUser();
if (key_exists('refresh', $_GET)) { if (key_exists('refresh', $_GET)) {
$refreshResult = Feed::refreshAll(); $refreshResult = Feed::refreshAll();
if (Result::isOK($refreshResult)) { if ($refreshResult->isOK()) {
add_info('All feeds refreshed successfully'); add_info('All feeds refreshed successfully');
} else { } else {
add_error(nl2br($refreshResult->getError())); add_error(nl2br($refreshResult->getError()));

View File

@ -10,7 +10,6 @@
declare(strict_types=1); declare(strict_types=1);
use BitBadger\InspiredByFSharp\Option;
use BitBadger\PDODocument\{Delete, DocumentException, Patch}; use BitBadger\PDODocument\{Delete, DocumentException, Patch};
use FeedReaderCentral\{ItemWithFeed, Table}; use FeedReaderCentral\{ItemWithFeed, Table};
@ -50,7 +49,7 @@ switch ($_SERVER['REQUEST_METHOD']) {
frc_redirect($from); frc_redirect($from);
} }
!$item = Option::getOrCall(not_found(...), ItemWithFeed::retrieveById($id)); $item = ItemWithFeed::retrieveById($id)->getOrCall(not_found(...));
try { try {
Patch::byId(Table::Item, $id, ['is_read' => 1]); Patch::byId(Table::Item, $id, ['is_read' => 1]);
} catch (DocumentException $ex) { } catch (DocumentException $ex) {

View File

@ -13,6 +13,7 @@ declare(strict_types=1);
include '../../start.php'; include '../../start.php';
use BitBadger\InspiredByFSharp\Option;
use FeedReaderCentral\{Key, Security}; use FeedReaderCentral\{Key, Security};
Security::verifyUser(redirectIfAnonymous: false); Security::verifyUser(redirectIfAnonymous: false);
@ -21,7 +22,7 @@ Security::verifyUser(redirectIfAnonymous: false);
if (key_exists(Key::UserId, $_SESSION)) frc_redirect('/'); if (key_exists(Key::UserId, $_SESSION)) frc_redirect('/');
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
Security::logOnUser($_POST['email'] ?? '', $_POST['password'], $_POST['returnTo'] ?? null); Security::logOnUser($_POST['email'] ?? '', $_POST['password'], Option::of($_POST['returnTo'] ?? null));
// If we're still here, something didn't work; preserve the returnTo parameter // If we're still here, something didn't work; preserve the returnTo parameter
$_GET['returnTo'] = $_POST['returnTo']; $_GET['returnTo'] = $_POST['returnTo'];
} }

View File

@ -10,7 +10,6 @@
declare(strict_types=1); declare(strict_types=1);
use BitBadger\InspiredByFSharp\Option;
use BitBadger\PDODocument\Configuration; use BitBadger\PDODocument\Configuration;
use FeedReaderCentral\{Key, Security, User}; use FeedReaderCentral\{Key, Security, User};

View File

@ -52,7 +52,7 @@ function json_column_exists(): bool
{ {
try { try {
$table = Custom::single("SELECT sql FROM sqlite_master WHERE tbl_name='frc_user'", [], new ArrayMapper()) $table = Custom::single("SELECT sql FROM sqlite_master WHERE tbl_name='frc_user'", [], new ArrayMapper())
->getOrElse(['sql' => '']); ->getOrDefault(['sql' => '']);
return $table && substr_compare(strtolower($table['sql']), 'data text not null', 0) >= 0; return $table && substr_compare(strtolower($table['sql']), 'data text not null', 0) >= 0;
} catch (DocumentException $ex) { } catch (DocumentException $ex) {
printfn("ERR $ex"); printfn("ERR $ex");

View File

@ -50,9 +50,9 @@ function refresh_all(): void
$userKey = "$feed->user_id"; $userKey = "$feed->user_id";
if (!key_exists($userKey, $users)) { if (!key_exists($userKey, $users)) {
$users[$userKey] = Find::byId(Table::User, $feed->user_id, User::class) $users[$userKey] = Find::byId(Table::User, $feed->user_id, User::class)
->getOrElse(new User(email: 'user-not-found')); ->getOrDefault(new User(email: 'user-not-found'));
} }
if (Result::isError($result)) { if ($result->isError()) {
printfn('ERR (%s) %s', $users[$userKey]->email, $feed->url); printfn('ERR (%s) %s', $users[$userKey]->email, $feed->url);
printfn(' %s', $result->getError()); printfn(' %s', $result->getError());
} else { } else {

View File

@ -104,7 +104,7 @@ function add_user(): void
try { try {
// Ensure there is not already a user with this e-mail address // Ensure there is not already a user with this e-mail address
$user = User::findByEmail($argv[2]); $user = User::findByEmail($argv[2]);
if (Option::isSome($user)) { if ($user->isSome()) {
printfn('A user with e-mail address "%s" already exists', $argv[2]); printfn('A user with e-mail address "%s" already exists', $argv[2]);
return; return;
} }
@ -138,7 +138,7 @@ function set_password(string $email, string $password): void
// Ensure this user exists // Ensure this user exists
$user = User::findByEmail($email); $user = User::findByEmail($email);
if (Option::isNone($user)) { if ($user->isNone()) {
printfn('No %s exists', $displayUser); printfn('No %s exists', $displayUser);
return; return;
} }
@ -165,7 +165,7 @@ function delete_user(string $email): void
// Get the user for the provided e-mail address // Get the user for the provided e-mail address
$tryUser = User::findByEmail($email); $tryUser = User::findByEmail($email);
if (Option::isNone($tryUser)) { if ($tryUser->isNone()) {
printfn('No %s exists', $displayUser); printfn('No %s exists', $displayUser);
return; return;
} }
@ -210,7 +210,7 @@ function migrate_single_user(): void
try { try {
$single = User::findByEmail(Security::SingleUserEmail); $single = User::findByEmail(Security::SingleUserEmail);
if (Option::isNone($single)) { if ($single->isNone()) {
printfn('There is no single-user mode user to be migrated'); printfn('There is no single-user mode user to be migrated');
return; return;
} }