diff --git a/.gitignore b/.gitignore index aaf5ea9..ff87fff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .idea vendor -src/data/*.db +src/data/*.db* src/user-config.php diff --git a/src/composer.lock b/src/composer.lock index 515e447..cf66c9b 100644 --- a/src/composer.lock +++ b/src/composer.lock @@ -12,7 +12,10 @@ "source": { "type": "git", "url": "https://git.bitbadger.solutions/bit-badger/documents-common", - "reference": "36e66969a20ba7e20d6a68e810181522c6724d9f" + "reference": "9dd8059e80423c67c79c62ff27e92f920d5f041b" + }, + "require": { + "netresearch/jsonmapper": "^4" }, "require-dev": { "phpunit/phpunit": "^11" @@ -24,7 +27,7 @@ "BitBadger\\Documents\\Query\\": "./Query" } }, - "time": "2024-06-03T01:55:12+00:00" + "time": "2024-06-03T15:26:37+00:00" }, { "name": "bit-badger/documents-sqlite", @@ -32,20 +35,80 @@ "source": { "type": "git", "url": "https://git.bitbadger.solutions/bit-badger/documents-sqlite", - "reference": "3761397ea5e286f5f3858bb1a5266adee6d92a25" + "reference": "1439742c40ee8b8accd8d70f2ce94df458c6bc42" }, "require": { "bit-badger/documents-common": "dev-conversion", "ext-sqlite3": "*" }, + "require-dev": { + "phpunit/phpunit": "^11" + }, "type": "library", "autoload": { "psr-4": { - "BitBadger\\Documents\\SQLite\\": "./", - "BitBadger\\Documents\\SQLite\\Query\\": "./Query" + "BitBadger\\Documents\\SQLite\\": "./src", + "BitBadger\\Documents\\SQLite\\Query\\": "./src/Query" } }, - "time": "2024-06-03T01:56:39+00:00" + "autoload-dev": { + "psr-4": { + "Test\\Unit\\": "./tests/unit", + "Test\\Integration\\": "./tests/integration" + } + }, + "time": "2024-06-03T22:16:08+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v4.4.1", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/132c75c7dd83e45353ebb9c6c9f591952995bbf0", + "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1" + }, + "time": "2024-01-31T06:18:54+00:00" } ], "packages-dev": [], diff --git a/src/lib/Data.php b/src/lib/Data.php index 83cdac7..2558888 100644 --- a/src/lib/Data.php +++ b/src/lib/Data.php @@ -76,6 +76,8 @@ class Data Definition::ensureFieldIndex(Table::ITEM, 'feed', ['feed_id', 'item_link'], $db); self::createSearchIndex($db); } + $journalMode = Custom::scalar("PRAGMA journal_mode", [], fn($it) => $it[0]); + if ($journalMode <> 'wal') Custom::nonQuery("PRAGMA journal_mode = 'wal'", []); $db->close(); } diff --git a/src/lib/ItemList.php b/src/lib/ItemList.php index 405e1fb..49175e5 100644 --- a/src/lib/ItemList.php +++ b/src/lib/ItemList.php @@ -10,6 +10,7 @@ use BitBadger\Documents\JsonMapper; use BitBadger\Documents\Query; use BitBadger\Documents\SQLite\Custom; use BitBadger\Documents\SQLite\Parameters; +use Iterator; /** * A list of items to be displayed @@ -140,7 +141,7 @@ class ItemList if ($isBookmarked) $fields[] = Data::bookmarkField(Table::ITEM); $list = new static('Matching' . ($isBookmarked ? ' Bookmarked' : ''), "/search?search=$search&items=" . ($isBookmarked ? 'bookmarked' : 'all'), $fields, - ' ' . Table::ITEM . ".data->>'" . Configuration::idField() . "' IN " + ' AND ' . Table::ITEM . ".data->>'" . Configuration::idField() . "' IN " . '(SELECT ROWID FROM item_search WHERE content MATCH @search)'); $list->showIndicators = true; $list->displayFeed = true; @@ -159,8 +160,7 @@ class ItemList $return = $this->returnURL == '' ? '' : '&from=' . urlencode($this->returnURL); $hasItems = false; echo '
'; - iterator_apply($this->dbList->items(), function (ItemWithFeed $it) use (&$hasItems, $return) - { + foreach ($this->dbList->items() as $it) { $hasItems = true; echo '

' . hx_get("/item?id=$it->id$return", strip_tags($it->title)) . '
'; if ($this->showIndicators) { @@ -176,7 +176,7 @@ class ItemList echo ' • ' . htmlentities($it->feed->title); } echo ''; - }); + } if (!$hasItems) { echo '

There are no ' . strtolower($this->itemType) . ' items'; } diff --git a/src/lib/ItemWithFeed.php b/src/lib/ItemWithFeed.php index 46ea813..6f3d962 100644 --- a/src/lib/ItemWithFeed.php +++ b/src/lib/ItemWithFeed.php @@ -22,7 +22,7 @@ class ItemWithFeed extends Item /** @var string The `SELECT` clause to add the feed as a property to the item's document */ public const string SELECT_WITH_FEED = - 'SELECT json_set(' . Table::ITEM . ".data, '$.feed', " . Table::FEED . '.data) AS data FROM ' + 'SELECT json_set(' . Table::ITEM . ".data, '$.feed', json(" . Table::FEED . '.data)) AS data FROM ' . self::FROM_WITH_JOIN; /** @var Feed The feed to which this item belongs */ diff --git a/src/lib/Security.php b/src/lib/Security.php index a5bf524..25bd600 100644 --- a/src/lib/Security.php +++ b/src/lib/Security.php @@ -68,7 +68,6 @@ class Security $dbEmail = $email; } $user = User::findByEmail($dbEmail); - var_dump($user); if ($user) self::verifyPassword($user, $password, $returnTo); add_error('Invalid credentials; log on unsuccessful'); } diff --git a/src/public/bookmark.php b/src/public/bookmark.php index d03039e..cd35b32 100644 --- a/src/public/bookmark.php +++ b/src/public/bookmark.php @@ -15,7 +15,7 @@ include '../start.php'; FeedReaderCentral\Security::verifyUser(); -$id = $_GET['id']; +$id = key_exists('id', $_GET) ? (int)$_GET['id'] : -1; if (!$item = ItemWithFeed::retrieveById($id)) not_found(); diff --git a/src/public/feed/items.php b/src/public/feed/items.php index 2373286..a688cf2 100644 --- a/src/public/feed/items.php +++ b/src/public/feed/items.php @@ -13,7 +13,8 @@ include '../../start.php'; FeedReaderCentral\Security::verifyUser(); -if (!($feed = Feed::retrieveById($_GET['id']))) not_found(); +$id = key_exists('id', $_GET) ? (int)$_GET['id'] : -1; +if (!($feed = Feed::retrieveById($id))) not_found(); $list = match (true) { key_exists('unread', $_GET) => ItemList::unreadForFeed($feed->id), diff --git a/src/public/feeds.php b/src/public/feeds.php index b29ae51..d139857 100644 --- a/src/public/feeds.php +++ b/src/public/feeds.php @@ -22,13 +22,13 @@ FeedReaderCentral\Security::verifyUser(); // TODO: adapt query when document list is done $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)); + $field->appendParameter([]), new JsonMapper(Feed::class)); page_head('Your Feeds'); ?>

Your Feeds

items(), function (Feed $feed) { + foreach ($feeds->items() as /** @var Feed $feed */ $feed) { $item = Table::ITEM; $counts = Custom::single(<<>'feed_id' = @feed) AS total, @@ -47,6 +47,6 @@ page_head('Your Feeds'); ?> 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 + } ?>
(int)$_POST['id'], + key_exists('id', $_GET) => (int)$_GET['id'], + default => -1 +}; + 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($_POST['id'])) { - Patch::byId(Table::ITEM, $_POST['id'], ['is_read' => 0]); + if (ItemWithFeed::existsById($id)) { + Patch::byId(Table::ITEM, $id, ['is_read' => 0]); } frc_redirect($_POST['from']); } catch (DocumentException $ex) { @@ -32,8 +38,8 @@ $from = $_GET['from'] ?? '/'; if ($_SERVER['REQUEST_METHOD'] == 'DELETE') { try { - if (ItemWithFeed::existsById($_GET['id'])) { - Delete::byId(Table::ITEM, $_GET['id']); + if (ItemWithFeed::existsById($id)) { + Delete::byId(Table::ITEM, $id); } } catch (DocumentException $ex) { add_error("$ex"); @@ -41,10 +47,9 @@ if ($_SERVER['REQUEST_METHOD'] == 'DELETE') { frc_redirect($from); } -if (!$item = ItemWithFeed::retrieveById($_GET['id'])) not_found(); - +if (!$item = ItemWithFeed::retrieveById($id)) not_found(); try { - Patch::byId(Table::ITEM, $_GET['id'], ['is_read' => 1]); + Patch::byId(Table::ITEM, $id, ['is_read' => 1]); } catch (DocumentException $ex) { add_error("$ex"); } diff --git a/src/start.php b/src/start.php index da78944..336cf32 100644 --- a/src/start.php +++ b/src/start.php @@ -12,6 +12,8 @@ session_start([ 'cookie_httponly' => true, 'cookie_samesite' => 'Strict']); +//const DOC_DEBUG_SQL = true; + /** * Add a message to be displayed at the top of the page * diff --git a/src/util/db-update.php b/src/util/db-update.php index a296e55..361e043 100644 --- a/src/util/db-update.php +++ b/src/util/db-update.php @@ -5,6 +5,7 @@ use BitBadger\Documents\DocumentException; use BitBadger\Documents\SQLite\Configuration; use BitBadger\Documents\SQLite\Custom; use BitBadger\Documents\SQLite\Document; +use BitBadger\Documents\SQLite\Results; use FeedReaderCentral\Data; use FeedReaderCentral\Feed; use FeedReaderCentral\Item; @@ -73,11 +74,15 @@ function run_update(): void } try { - printfn('Removing search index...'); - Custom::nonQuery('DROP TRIGGER item_ai', [], $db); - Custom::nonQuery('DROP TRIGGER item_au', [], $db); - Custom::nonQuery('DROP TRIGGER item_ad', [], $db); - Custom::nonQuery('DROP TABLE item_search', [], $db); + $searchExists = Custom::scalar("SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE name = 'item_ai')", [], + Results::toExists(...)); + if ($searchExists) { + printfn('Removing search index...'); + Custom::nonQuery('DROP TRIGGER item_ai', [], $db); + Custom::nonQuery('DROP TRIGGER item_au', [], $db); + Custom::nonQuery('DROP TRIGGER item_ad', [], $db); + Custom::nonQuery('DROP TABLE item_search', [], $db); + } printfn('Moving old tables...'); Custom::nonQuery('ALTER TABLE item RENAME TO old_item', [], $db); Custom::nonQuery('ALTER TABLE feed RENAME TO old_feed', [], $db);