First cut of item-with-feed and list impl
- Add strict types to all files - Convert many queries to document commands
This commit is contained in:
parent
b88ad1f268
commit
93dd8e880f
|
@ -1,4 +1,4 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
/** The current Feed Reader Central version */
|
/** The current Feed Reader Central version */
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
require 'app-config.php';
|
require 'app-config.php';
|
||||||
|
|
||||||
if (php_sapi_name() != 'cli') {
|
if (php_sapi_name() != 'cli') {
|
||||||
|
|
11
src/composer.lock
generated
11
src/composer.lock
generated
|
@ -12,7 +12,10 @@
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.bitbadger.solutions/bit-badger/documents-common",
|
"url": "https://git.bitbadger.solutions/bit-badger/documents-common",
|
||||||
"reference": "4aecbfe3e8030fe7ddc0391ee715d6766cbb9c6e"
|
"reference": "36e66969a20ba7e20d6a68e810181522c6724d9f"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^11"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
@ -21,7 +24,7 @@
|
||||||
"BitBadger\\Documents\\Query\\": "./Query"
|
"BitBadger\\Documents\\Query\\": "./Query"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"time": "2024-06-02T02:11:21+00:00"
|
"time": "2024-06-03T01:55:12+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "bit-badger/documents-sqlite",
|
"name": "bit-badger/documents-sqlite",
|
||||||
|
@ -29,7 +32,7 @@
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.bitbadger.solutions/bit-badger/documents-sqlite",
|
"url": "https://git.bitbadger.solutions/bit-badger/documents-sqlite",
|
||||||
"reference": "ac34dbf481287526b6d044fd7699568e0ee92805"
|
"reference": "3761397ea5e286f5f3858bb1a5266adee6d92a25"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"bit-badger/documents-common": "dev-conversion",
|
"bit-badger/documents-common": "dev-conversion",
|
||||||
|
@ -42,7 +45,7 @@
|
||||||
"BitBadger\\Documents\\SQLite\\Query\\": "./Query"
|
"BitBadger\\Documents\\SQLite\\Query\\": "./Query"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"time": "2024-06-02T02:18:12+00:00"
|
"time": "2024-06-03T01:56:39+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
namespace FeedReaderCentral;
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
use BitBadger\Documents\DocumentException;
|
use BitBadger\Documents\DocumentException;
|
||||||
|
use BitBadger\Documents\Field;
|
||||||
use BitBadger\Documents\SQLite\Configuration;
|
use BitBadger\Documents\SQLite\Configuration;
|
||||||
use BitBadger\Documents\SQLite\Custom;
|
use BitBadger\Documents\SQLite\Custom;
|
||||||
use BitBadger\Documents\SQLite\Definition;
|
use BitBadger\Documents\SQLite\Definition;
|
||||||
|
@ -77,6 +79,59 @@ class Data
|
||||||
$db->close();
|
$db->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a JSON field comparison to find bookmarked items
|
||||||
|
*
|
||||||
|
* @param string $qualifier The table qualifier to include (optional; defaults to no qualifier)
|
||||||
|
* @return Field A field that will find bookmarked items
|
||||||
|
*/
|
||||||
|
public static function bookmarkField(string $qualifier = ''): Field
|
||||||
|
{
|
||||||
|
$bookField = Field::EQ('is_bookmarked', 1, '@book');
|
||||||
|
$bookField->qualifier = $qualifier;
|
||||||
|
return $bookField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a JSON field comparison to find items for a given feed
|
||||||
|
*
|
||||||
|
* @param int $feedId The ID of the feed for which items should be retrieved
|
||||||
|
* @param string $qualifier The table qualifier to include (optional; defaults to no qualifier)
|
||||||
|
* @return Field A field to find items for the give feed
|
||||||
|
*/
|
||||||
|
public static function feedField(int $feedId, string $qualifier = ''): Field
|
||||||
|
{
|
||||||
|
$feedField = Field::EQ(Configuration::idField(), $feedId, '@feed');
|
||||||
|
$feedField->qualifier = $qualifier;
|
||||||
|
return $feedField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a JSON field comparison to find unread items
|
||||||
|
*
|
||||||
|
* @param string $qualifier The table qualifier to include (optional; defaults to no qualifier)
|
||||||
|
* @return Field A field to find unread items
|
||||||
|
*/
|
||||||
|
public static function unreadField(string $qualifier = ''): Field
|
||||||
|
{
|
||||||
|
$readField = Field::EQ('is_read', 0, '@read');
|
||||||
|
$readField->qualifier = $qualifier;
|
||||||
|
return $readField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a JSON field comparison to find items belonging to feeds to which the given user is subscribed
|
||||||
|
*
|
||||||
|
* @param string $qualifier The table qualifier to include (optional; defaults to no qualifier)
|
||||||
|
* @return Field A field to find feeds belonging to the given user
|
||||||
|
*/
|
||||||
|
public static function userIdField(string $qualifier = ''): Field
|
||||||
|
{
|
||||||
|
$userField = Field::EQ('user_id', $_SESSION[Key::USER_ID], '@user');
|
||||||
|
$userField->qualifier = $qualifier;
|
||||||
|
return $userField;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse/format a date/time from a string
|
* Parse/format a date/time from a string
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
namespace FeedReaderCentral;
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
use BitBadger\Documents\DocumentException;
|
use BitBadger\Documents\DocumentException;
|
||||||
|
@ -10,6 +11,7 @@ use BitBadger\Documents\SQLite\Custom;
|
||||||
use BitBadger\Documents\SQLite\Document;
|
use BitBadger\Documents\SQLite\Document;
|
||||||
use BitBadger\Documents\SQLite\Exists;
|
use BitBadger\Documents\SQLite\Exists;
|
||||||
use BitBadger\Documents\SQLite\Find;
|
use BitBadger\Documents\SQLite\Find;
|
||||||
|
use BitBadger\Documents\SQLite\Parameters;
|
||||||
use BitBadger\Documents\SQLite\Patch;
|
use BitBadger\Documents\SQLite\Patch;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use SQLite3;
|
use SQLite3;
|
||||||
|
@ -137,7 +139,7 @@ class Feed
|
||||||
SQL;
|
SQL;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Custom::nonQuery($sql, array_merge(array_map($it -> $it->asParameter(), $fields)), $db);
|
Custom::nonQuery($sql, Parameters::addFields($fields, []), $db);
|
||||||
return ['ok' => true];
|
return ['ok' => true];
|
||||||
} catch (DocumentException $ex) {
|
} catch (DocumentException $ex) {
|
||||||
return ['error' => "$ex"];
|
return ['error' => "$ex"];
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
namespace FeedReaderCentral;
|
|
||||||
|
|
||||||
use BitBadger\Documents\DocumentException;
|
namespace FeedReaderCentral;
|
||||||
use BitBadger\Documents\Field;
|
|
||||||
use BitBadger\Documents\JsonMapper;
|
|
||||||
use BitBadger\Documents\Query;
|
|
||||||
use BitBadger\Documents\SQLite\Configuration;
|
|
||||||
use BitBadger\Documents\SQLite\Custom;
|
|
||||||
use BitBadger\Documents\SQLite\Parameters;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An item from a feed
|
* An item from a feed
|
||||||
|
@ -71,27 +64,4 @@ class Item
|
||||||
updated_on: $item->updatedOn,
|
updated_on: $item->updatedOn,
|
||||||
content: $item->content);
|
content: $item->content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve an item by its ID, ensuring that its owner matches the current user
|
|
||||||
*
|
|
||||||
* @param int $id The ID of the item to retrieve
|
|
||||||
* @return Item|false The item if it exists and is owned by the current user, false if not
|
|
||||||
* @throws DocumentException If any is encountered
|
|
||||||
*/
|
|
||||||
public static function retrieveByIdForUser(int $id): Item|false
|
|
||||||
{
|
|
||||||
$idField = Field::EQ(Configuration::idField(), $id, '@id');
|
|
||||||
$idField->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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace FeedReaderCentral;
|
|
||||||
|
|
||||||
use BitBadger\Documents\JsonMapper;
|
|
||||||
use BitBadger\Documents\Mapper;
|
|
||||||
use FeedReaderCentral\Domain\Feed;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A combined item and feed (used for lists)
|
|
||||||
*/
|
|
||||||
class ItemAndFeed
|
|
||||||
{
|
|
||||||
/** @var Item The item to be manipulated */
|
|
||||||
public Item $item;
|
|
||||||
|
|
||||||
/** @var Feed The feed to which the item belongs */
|
|
||||||
public Feed $feed;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a mapper for this item
|
|
||||||
* @return Mapper<ItemAndFeed> A mapper to deserialize this from the query
|
|
||||||
*/
|
|
||||||
public static function mapper(): Mapper
|
|
||||||
{
|
|
||||||
return new class implements Mapper {
|
|
||||||
public function map(array $result): ItemAndFeed
|
|
||||||
{
|
|
||||||
$it = new ItemAndFeed();
|
|
||||||
$it->item = (new JsonMapper(Item::class, 'item_data'))->map($result);
|
|
||||||
$it->feed = (new JsonMapper(Feed::class, 'feed_data'))->map($result);
|
|
||||||
return $it;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the `SELECT` and `FROM` clauses for the query to retrieve this item
|
|
||||||
*
|
|
||||||
* @return string The `SELECT` and `FROM` clauses to retrieve these items
|
|
||||||
*/
|
|
||||||
public static function selectFrom(): string
|
|
||||||
{
|
|
||||||
$item = Table::ITEM;
|
|
||||||
$feed = Table::FEED;
|
|
||||||
return <<<SQL
|
|
||||||
SELECT $item.data AS item_data, $feed.data AS feed_data
|
|
||||||
FROM $item INNER JOIN $feed ON $item.data->>'feed_id' = $feed.data->>'id'
|
|
||||||
SQL;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +1,25 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
namespace FeedReaderCentral;
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
use SQLite3;
|
use BitBadger\Documents\Configuration;
|
||||||
use SQLite3Result;
|
use BitBadger\Documents\DocumentException;
|
||||||
use SQLite3Stmt;
|
use BitBadger\Documents\DocumentList;
|
||||||
|
use BitBadger\Documents\Field;
|
||||||
|
use BitBadger\Documents\JsonMapper;
|
||||||
|
use BitBadger\Documents\Query;
|
||||||
|
use BitBadger\Documents\SQLite\Custom;
|
||||||
|
use BitBadger\Documents\SQLite\Parameters;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of items to be displayed
|
* A list of items to be displayed
|
||||||
*
|
*
|
||||||
* This is a wrapper for retrieval and display of arbitrary lists of items based on a SQLite result.
|
* This is a wrapper for retrieval and display of arbitrary lists of items based on a SQLite result.
|
||||||
*/
|
*/
|
||||||
class ItemList {
|
class ItemList
|
||||||
|
{
|
||||||
/** @var SQLite3Result The list of items to be displayed */
|
/** @var DocumentList<ItemWithFeed> The items matching the criteria, lazily iterable */
|
||||||
private SQLite3Result $items;
|
private DocumentList $dbList;
|
||||||
|
|
||||||
/** @var string The error message generated by executing a query */
|
/** @var string The error message generated by executing a query */
|
||||||
public string $error = '';
|
public string $error = '';
|
||||||
|
@ -23,7 +29,8 @@ class ItemList {
|
||||||
*
|
*
|
||||||
* @return bool True if there is an error condition associated with this list, false if not
|
* @return bool True if there is an error condition associated with this list, false if not
|
||||||
*/
|
*/
|
||||||
public function isError(): bool {
|
public function isError(): bool
|
||||||
|
{
|
||||||
return $this->error != '';
|
return $this->error != '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,54 +46,34 @@ class ItemList {
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* 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 $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
|
* @param string $returnURL The URL to which the item page should return once the item has been viewed
|
||||||
|
* @param array|Field[] $fields The fields to use to restrict the results
|
||||||
|
* @param string $searchWhere Additional WHERE clause to use for searching
|
||||||
*/
|
*/
|
||||||
private function __construct(SQLite3 $db, SQLite3Stmt $query, public string $itemType, public string $returnURL = '')
|
private function __construct(public string $itemType, public string $returnURL = '', array $fields = [],
|
||||||
|
string $searchWhere = '')
|
||||||
{
|
{
|
||||||
$result = $query->execute();
|
$allFields = [Data::userIdField(Table::FEED), ...$fields];
|
||||||
if (!$result) {
|
try {
|
||||||
$this->error = 'SQLite error: ' . $db->lastErrorMsg();
|
$this->dbList = Custom::list(
|
||||||
} else {
|
ItemWithFeed::SELECT_WITH_FEED . ' WHERE '
|
||||||
$this->items = $result;
|
. Query::whereByFields(array_filter($allFields, fn($it) => $it->paramName <> '@search'))
|
||||||
|
. $searchWhere,
|
||||||
|
Parameters::addFields($allFields, []), new JsonMapper(ItemWithFeed::class));
|
||||||
|
} catch (DocumentException $ex) {
|
||||||
|
$this->error = "$ex";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = <<<SQL
|
|
||||||
SELECT item.id, item.feed_id, item.title AS item_title, coalesce(item.updated_on, item.published_on) AS as_of,
|
|
||||||
item.is_read, item.is_bookmarked, feed.title AS feed_title
|
|
||||||
FROM item INNER JOIN feed ON feed.id = item.feed_id
|
|
||||||
WHERE feed.user_id = :userId $where
|
|
||||||
ORDER BY coalesce(item.updated_on, item.published_on) DESC
|
|
||||||
SQL;
|
|
||||||
$query = $db->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
|
* 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
|
* @return static An item list with all bookmarked items
|
||||||
*/
|
*/
|
||||||
public static function allBookmarked(SQLite3 $db): static
|
public static function allBookmarked(): static
|
||||||
{
|
{
|
||||||
$list = new static($db, self::makeQuery($db, ['item.is_bookmarked = 1']), 'Bookmarked', '/?bookmarked');
|
$list = new static('Bookmarked', '/?bookmarked', [Data::bookmarkField(Table::ITEM)]);
|
||||||
$list->linkFeed = true;
|
$list->linkFeed = true;
|
||||||
return $list;
|
return $list;
|
||||||
}
|
}
|
||||||
|
@ -94,12 +81,11 @@ class ItemList {
|
||||||
/**
|
/**
|
||||||
* Create an item list with all the current user's unread items
|
* 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
|
* @return static An item list with all unread items
|
||||||
*/
|
*/
|
||||||
public static function allUnread(SQLite3 $db): static
|
public static function allUnread(): static
|
||||||
{
|
{
|
||||||
$list = new static($db, self::makeQuery($db, ['item.is_read = 0']), 'Unread');
|
$list = new static('Unread', fields: [Data::unreadField(Table::ITEM)]);
|
||||||
$list->linkFeed = true;
|
$list->linkFeed = true;
|
||||||
return $list;
|
return $list;
|
||||||
}
|
}
|
||||||
|
@ -108,13 +94,11 @@ class ItemList {
|
||||||
* Create an item list with all items for the given feed
|
* 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 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
|
* @return static An item list with all items for the given feed
|
||||||
*/
|
*/
|
||||||
public static function allForFeed(int $feedId, SQLite3 $db): static
|
public static function allForFeed(int $feedId): static
|
||||||
{
|
{
|
||||||
$list = new static($db, self::makeQuery($db, ['feed.id = :feed'], [[':feed', $feedId]]), '',
|
$list = new static('', "/feed/items?id=$feedId", [Data::feedField($feedId, Table::FEED)]);
|
||||||
"/feed/items?id=$feedId");
|
|
||||||
$list->showIndicators = true;
|
$list->showIndicators = true;
|
||||||
return $list;
|
return $list;
|
||||||
}
|
}
|
||||||
|
@ -123,27 +107,24 @@ class ItemList {
|
||||||
* Create an item list with unread items for the given feed
|
* 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 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
|
* @return static An item list with unread items for the given feed
|
||||||
*/
|
*/
|
||||||
public static function unreadForFeed(int $feedId, SQLite3 $db): static
|
public static function unreadForFeed(int $feedId): static
|
||||||
{
|
{
|
||||||
return new static($db, self::makeQuery($db, ['feed.id = :feed', 'item.is_read = 0'], [[':feed', $feedId]]),
|
return new static('Unread', "/feed/items?id=$feedId&unread",
|
||||||
'Unread', "/feed/items?id=$feedId&unread");
|
[Data::feedField($feedId, Table::FEED), Data::unreadField(Table::ITEM)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an item list with bookmarked items for the given feed
|
* 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 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
|
* @return static An item list with bookmarked items for the given feed
|
||||||
*/
|
*/
|
||||||
public static function bookmarkedForFeed(int $feedId, SQLite3 $db): static
|
public static function bookmarkedForFeed(int $feedId): static
|
||||||
{
|
{
|
||||||
return new static($db,
|
return new static('Bookmarked', "/feed/items?id=$feedId&bookmarked",
|
||||||
self::makeQuery($db, ['feed.id = :feed', 'item.is_bookmarked = 1'], [[':feed', $feedId]]), 'Bookmarked',
|
[Data::feedField($feedId, Table::FEED), Data::bookmarkField(Table::ITEM)]);
|
||||||
"/feed/items?id=$feedId&bookmarked");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -151,16 +132,16 @@ class ItemList {
|
||||||
*
|
*
|
||||||
* @param string $search The item search terms / query
|
* @param string $search The item search terms / query
|
||||||
* @param bool $isBookmarked Whether to restrict the search to bookmarked items
|
* @param bool $isBookmarked Whether to restrict the search to bookmarked items
|
||||||
* @param SQLite3 $db The database connection to use to obtain items
|
|
||||||
* @return static An item list match the given search terms
|
* @return static An item list match the given search terms
|
||||||
*/
|
*/
|
||||||
public static function matchingSearch(string $search, bool $isBookmarked, SQLite3 $db): static
|
public static function matchingSearch(string $search, bool $isBookmarked): static
|
||||||
{
|
{
|
||||||
$where = $isBookmarked ? ['item.is_bookmarked = 1'] : [];
|
$fields = [Field::EQ('content', $search, '@search')];
|
||||||
$where[] = 'item.id IN (SELECT ROWID FROM item_search WHERE content MATCH :search)';
|
if ($isBookmarked) $fields[] = Data::bookmarkField(Table::ITEM);
|
||||||
$list = new static($db, self::makeQuery($db, $where, [[':search', $search]]),
|
$list = new static('Matching' . ($isBookmarked ? ' Bookmarked' : ''),
|
||||||
'Matching' . ($isBookmarked ? ' Bookmarked' : ''),
|
"/search?search=$search&items=" . ($isBookmarked ? 'bookmarked' : 'all'), $fields,
|
||||||
"/search?search=$search&items=" . ($isBookmarked ? 'bookmarked' : 'all'));
|
' ' . Table::ITEM . ".data->>'" . Configuration::idField() . "' IN "
|
||||||
|
. '(SELECT ROWID FROM item_search WHERE content MATCH @search)');
|
||||||
$list->showIndicators = true;
|
$list->showIndicators = true;
|
||||||
$list->displayFeed = true;
|
$list->displayFeed = true;
|
||||||
return $list;
|
return $list;
|
||||||
|
@ -171,34 +152,33 @@ class ItemList {
|
||||||
*/
|
*/
|
||||||
public function render(): void
|
public function render(): void
|
||||||
{
|
{
|
||||||
if ($this->isError()) { ?>
|
if ($this->isError()) {
|
||||||
<p>Error retrieving list:<br><?=$this->error?><?php
|
echo "<p>Error retrieving list:<br>$this->error";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$item = $this->items->fetchArray(SQLITE3_ASSOC);
|
|
||||||
$return = $this->returnURL == '' ? '' : '&from=' . urlencode($this->returnURL);
|
$return = $this->returnURL == '' ? '' : '&from=' . urlencode($this->returnURL);
|
||||||
|
$hasItems = false;
|
||||||
echo '<article>';
|
echo '<article>';
|
||||||
if ($item) {
|
iterator_apply($this->dbList->items(), function (ItemWithFeed $it) use (&$hasItems, $return)
|
||||||
while ($item) { ?>
|
{
|
||||||
<p><?=hx_get("/item?id={$item['id']}$return", strip_tags($item['item_title']))?><br>
|
$hasItems = true;
|
||||||
<small><?php
|
echo '<p>' . hx_get("/item?id=$it->id$return", strip_tags($it->title)) . '<br><small>';
|
||||||
if ($this->showIndicators) {
|
if ($this->showIndicators) {
|
||||||
if (!$item['is_read']) echo '<strong>Unread</strong> ';
|
if (!$it->isRead()) echo '<strong>Unread</strong> ';
|
||||||
if ($item['is_bookmarked']) echo '<strong>Bookmarked</strong> ';
|
if ($it->isBookmarked()) echo '<strong>Bookmarked</strong> ';
|
||||||
}
|
}
|
||||||
echo '<em>' . date_time($item['as_of']) . '</em>';
|
echo '<em>' . date_time($it->updated_on ?? $it->published_on) . '</em>';
|
||||||
if ($this->linkFeed) {
|
if ($this->linkFeed) {
|
||||||
echo ' • ' .
|
echo ' • ' .
|
||||||
hx_get("/feed/items?id={$item['feed_id']}&" . strtolower($this->itemType),
|
hx_get("/feed/items?id={$it->feed->id}&" . strtolower($this->itemType),
|
||||||
htmlentities($item['feed_title']));
|
htmlentities($it->feed->title));
|
||||||
} elseif ($this->displayFeed) {
|
} elseif ($this->displayFeed) {
|
||||||
echo ' • ' . htmlentities($item['feed_title']);
|
echo ' • ' . htmlentities($it->feed->title);
|
||||||
} ?>
|
|
||||||
</small><?php
|
|
||||||
$item = $this->items->fetchArray(SQLITE3_ASSOC);
|
|
||||||
}
|
}
|
||||||
} else { ?>
|
echo '</small>';
|
||||||
<p><em>There are no <?=strtolower($this->itemType)?> items</em><?php
|
});
|
||||||
|
if (!$hasItems) {
|
||||||
|
echo '<p><em>There are no ' . strtolower($this->itemType) . ' items</em>';
|
||||||
}
|
}
|
||||||
echo '</article>';
|
echo '</article>';
|
||||||
}
|
}
|
||||||
|
|
73
src/lib/ItemWithFeed.php
Normal file
73
src/lib/ItemWithFeed.php
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
|
use BitBadger\Documents\DocumentException;
|
||||||
|
use BitBadger\Documents\Field;
|
||||||
|
use BitBadger\Documents\JsonMapper;
|
||||||
|
use BitBadger\Documents\Query;
|
||||||
|
use BitBadger\Documents\SQLite\Configuration;
|
||||||
|
use BitBadger\Documents\SQLite\Custom;
|
||||||
|
use BitBadger\Documents\SQLite\Parameters;
|
||||||
|
use BitBadger\Documents\SQLite\Results;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A combined item and feed (used for lists)
|
||||||
|
*/
|
||||||
|
class ItemWithFeed extends Item
|
||||||
|
{
|
||||||
|
/** @var string The body of the `FROM` clause to join item and feed */
|
||||||
|
public const string FROM_WITH_JOIN = Table::ITEM . ' INNER JOIN ' . Table::FEED
|
||||||
|
. ' ON ' . Table::ITEM . ".data->>'feed_id' = " . Table::FEED . ".data->>'id'";
|
||||||
|
|
||||||
|
/** @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 '
|
||||||
|
. self::FROM_WITH_JOIN;
|
||||||
|
|
||||||
|
/** @var Feed The feed to which this item belongs */
|
||||||
|
public Feed $feed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create JSON comparison fields to retrieve items while also checking the owning user
|
||||||
|
*
|
||||||
|
* @param int $id The ID of the item being retrieved
|
||||||
|
* @return array|Field[] The fields for item ID and user ID
|
||||||
|
*/
|
||||||
|
private static function idAndUserFields(int $id): array
|
||||||
|
{
|
||||||
|
$idField = Field::EQ(Configuration::idField(), $id, '@id');
|
||||||
|
$idField->qualifier = Table::ITEM;
|
||||||
|
$userField = Field::EQ('user_id', $_SESSION[Key::USER_ID], '@user');
|
||||||
|
$userField->qualifier = Table::FEED;
|
||||||
|
return [$idField, $userField];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check an item's existence via its ID
|
||||||
|
*
|
||||||
|
* @param int $id The ID of the item whose existence should be checked
|
||||||
|
* @return bool True if the item exists for the current user, false if not
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function existsById(int $id): bool
|
||||||
|
{
|
||||||
|
$fields = self::idAndUserFields($id);
|
||||||
|
return Custom::scalar(Query\Exists::query(self::FROM_WITH_JOIN, Query::whereByFields($fields)),
|
||||||
|
Parameters::addFields($fields, []), Results::toExists(...));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve an item via its ID
|
||||||
|
*
|
||||||
|
* @param int $id The ID of the item to be retrieved
|
||||||
|
* @return ItemWithFeed|false The item if it is found, false if not
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function retrieveById(int $id): ItemWithFeed|false
|
||||||
|
{
|
||||||
|
$fields = self::idAndUserFields($id);
|
||||||
|
return Custom::single(self::SELECT_WITH_FEED . ' WHERE ' . Query::whereByFields($fields),
|
||||||
|
Parameters::addFields($fields, []), new JsonMapper(self::class));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
namespace FeedReaderCentral;
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
namespace FeedReaderCentral;
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
namespace FeedReaderCentral;
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
use DOMNode;
|
use DOMNode;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
namespace FeedReaderCentral;
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
use BitBadger\Documents\DocumentException;
|
use BitBadger\Documents\DocumentException;
|
||||||
|
@ -67,6 +68,7 @@ class Security
|
||||||
$dbEmail = $email;
|
$dbEmail = $email;
|
||||||
}
|
}
|
||||||
$user = User::findByEmail($dbEmail);
|
$user = User::findByEmail($dbEmail);
|
||||||
|
var_dump($user);
|
||||||
if ($user) self::verifyPassword($user, $password, $returnTo);
|
if ($user) self::verifyPassword($user, $password, $returnTo);
|
||||||
add_error('Invalid credentials; log on unsuccessful');
|
add_error('Invalid credentials; log on unsuccessful');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
namespace FeedReaderCentral;
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
namespace FeedReaderCentral;
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
use BitBadger\Documents\DocumentException;
|
use BitBadger\Documents\DocumentException;
|
||||||
|
@ -9,7 +10,6 @@ use BitBadger\Documents\SQLite\Document;
|
||||||
use BitBadger\Documents\SQLite\Find;
|
use BitBadger\Documents\SQLite\Find;
|
||||||
use BitBadger\Documents\SQLite\Parameters;
|
use BitBadger\Documents\SQLite\Parameters;
|
||||||
use BitBadger\Documents\SQLite\Results;
|
use BitBadger\Documents\SQLite\Results;
|
||||||
use SQLite3;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A user of Feed Reader Central
|
* A user of Feed Reader Central
|
||||||
|
@ -51,25 +51,14 @@ class User
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does this user have any bookmarked items?
|
* Does this user have any bookmarked items?
|
||||||
|
*
|
||||||
* @return bool True if the user has any bookmarked items, false if not
|
* @return bool True if the user has any bookmarked items, false if not
|
||||||
* @throws DocumentException If any is encountered
|
* @throws DocumentException If any is encountered
|
||||||
*/
|
*/
|
||||||
public static function hasBookmarks(): bool
|
public static function hasBookmarks(): bool
|
||||||
{
|
{
|
||||||
$userField = Field::EQ('user_id', $_SESSION[Key::USER_ID], '@user');
|
$fields = [Data::userIdField(Table::FEED), Data::bookmarkField(Table::ITEM)];
|
||||||
$userField->qualifier = Table::FEED;
|
return Custom::scalar(Query\Exists::query(ItemWithFeed::FROM_WITH_JOIN, Query::whereByFields($fields)),
|
||||||
$bookField = Field::EQ('is_bookmarked', 1, '@book');
|
Parameters::addFields($fields, []), Results::toExists(...));
|
||||||
$bookField->qualifier = Table::ITEM;
|
|
||||||
$fields = [$userField, $bookField];
|
|
||||||
|
|
||||||
$item = Table::ITEM;
|
|
||||||
$feed = Table::FEED;
|
|
||||||
$where = Query::whereByFields($fields);
|
|
||||||
return Custom::scalar(<<<SQL
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM $item INNER JOIN $feed ON $item.data->>'feed_id' = $feed.data->>'id'
|
|
||||||
WHERE $where)
|
|
||||||
SQL, Parameters::addFields($fields, []), Results::toExists(...));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bookmark Partial Handler
|
* Bookmark Partial Handler
|
||||||
|
@ -7,30 +7,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use BitBadger\Documents\DocumentException;
|
use BitBadger\Documents\DocumentException;
|
||||||
use BitBadger\Documents\SQLite\Configuration;
|
|
||||||
use BitBadger\Documents\SQLite\Find;
|
|
||||||
use BitBadger\Documents\SQLite\Patch;
|
use BitBadger\Documents\SQLite\Patch;
|
||||||
use FeedReaderCentral\Item;
|
use FeedReaderCentral\ItemWithFeed;
|
||||||
use FeedReaderCentral\Key;
|
|
||||||
use FeedReaderCentral\Security;
|
|
||||||
use FeedReaderCentral\Table;
|
use FeedReaderCentral\Table;
|
||||||
|
|
||||||
include '../start.php';
|
include '../start.php';
|
||||||
|
|
||||||
Security::verifyUser();
|
FeedReaderCentral\Security::verifyUser();
|
||||||
|
|
||||||
$id = $_GET['id'];
|
$id = $_GET['id'];
|
||||||
|
|
||||||
// TODO: adapt query once "by fields" is available
|
if (!$item = ItemWithFeed::retrieveById($id)) not_found();
|
||||||
$db = Configuration::dbConn();
|
|
||||||
$existsQuery = $db->prepare(
|
|
||||||
'SELECT item.id FROM item INNER JOIN feed ON feed.id = item.feed_id WHERE item.id = :id AND feed.user_id = :user');
|
|
||||||
$existsQuery->bindValue(':id', $id);
|
|
||||||
$existsQuery->bindValue(':user', $_SESSION[Key::USER_ID]);
|
|
||||||
$existsResult = $existsQuery->execute();
|
|
||||||
$exists = $existsResult ? $existsResult->fetchArray(SQLITE3_ASSOC) : false;
|
|
||||||
|
|
||||||
if (!$exists) not_found();
|
|
||||||
|
|
||||||
if (key_exists('action', $_GET)) {
|
if (key_exists('action', $_GET)) {
|
||||||
$flag = match ($_GET['action']) {
|
$flag = match ($_GET['action']) {
|
||||||
|
@ -40,15 +27,14 @@ if (key_exists('action', $_GET)) {
|
||||||
};
|
};
|
||||||
if (isset($flag)) {
|
if (isset($flag)) {
|
||||||
try {
|
try {
|
||||||
Patch::byId(Table::ITEM, $id, ['is_bookmarked' => $flag], $db);
|
Patch::byId(Table::ITEM, $id, ['is_bookmarked' => $flag]);
|
||||||
|
$item->is_bookmarked = $flag;
|
||||||
} catch (DocumentException $ex) {
|
} catch (DocumentException $ex) {
|
||||||
add_error("$ex");
|
add_error("$ex");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$item = Find::byId(Table::ITEM, $id, Item::class)) not_found();
|
|
||||||
|
|
||||||
$action = $item->isBookmarked() ? 'remove' : 'add';
|
$action = $item->isBookmarked() ? 'remove' : 'add';
|
||||||
$icon = $item->isBookmarked() ? 'added' : 'add'; ?>
|
$icon = $item->isBookmarked() ? 'added' : 'add'; ?>
|
||||||
<button class="bookmark <?=$action?>" type=button role=button hx-patch="/bookmark?id=<?=$id?>&action=<?=$action?>"
|
<button class="bookmark <?=$action?>" type=button role=button hx-patch="/bookmark?id=<?=$id?>&action=<?=$action?>"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
include '../../start.php';
|
include '../../start.php';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
include '../../start.php';
|
include '../../start.php';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
include '../../start.php';
|
include '../../start.php';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
include '../../start.php';
|
include '../../start.php';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
include '../../start.php';
|
include '../../start.php';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add/Edit/Delete Feed Page
|
* Add/Edit/Delete Feed Page
|
||||||
*
|
*
|
||||||
|
@ -25,7 +26,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'DELETE') {
|
||||||
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 “' . htmlentities($feed->title) . '” deleted successfully');
|
add_info('Feed “' . htmlentities($feed->title) . '” deleted successfully');
|
||||||
$db->close();
|
|
||||||
frc_redirect('/feeds');
|
frc_redirect('/feeds');
|
||||||
} catch (DocumentException $ex) {
|
} catch (DocumentException $ex) {
|
||||||
add_error("$ex");
|
add_error("$ex");
|
||||||
|
|
|
@ -1,26 +1,24 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feed Item List Page
|
* Feed Item List Page
|
||||||
*
|
*
|
||||||
* Lists items in a given feed (all, unread, or bookmarked)
|
* Lists items in a given feed (all, unread, or bookmarked)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use BitBadger\Documents\SQLite\Configuration;
|
|
||||||
use FeedReaderCentral\Feed;
|
use FeedReaderCentral\Feed;
|
||||||
use FeedReaderCentral\ItemList;
|
use FeedReaderCentral\ItemList;
|
||||||
use FeedReaderCentral\Security;
|
|
||||||
|
|
||||||
include '../../start.php';
|
include '../../start.php';
|
||||||
|
|
||||||
Security::verifyUser();
|
FeedReaderCentral\Security::verifyUser();
|
||||||
|
|
||||||
if (!($feed = Feed::retrieveById($_GET['id']))) not_found();
|
if (!($feed = Feed::retrieveById($_GET['id']))) not_found();
|
||||||
|
|
||||||
$db = Configuration::dbConn();
|
|
||||||
$list = match (true) {
|
$list = match (true) {
|
||||||
key_exists('unread', $_GET) => ItemList::unreadForFeed($feed->id, $db),
|
key_exists('unread', $_GET) => ItemList::unreadForFeed($feed->id),
|
||||||
key_exists('bookmarked', $_GET) => ItemList::bookmarkedForFeed($feed->id, $db),
|
key_exists('bookmarked', $_GET) => ItemList::bookmarkedForFeed($feed->id),
|
||||||
default => ItemList::allForFeed($feed->id, $db)
|
default => ItemList::allForFeed($feed->id)
|
||||||
};
|
};
|
||||||
|
|
||||||
page_head(($list->itemType != '' ? "$list->itemType Items | " : '') . strip_tags($feed['title']));
|
page_head(($list->itemType != '' ? "$list->itemType Items | " : '') . strip_tags($feed['title']));
|
||||||
|
@ -32,4 +30,3 @@ if ($list->itemType == '') {
|
||||||
}
|
}
|
||||||
$list->render();
|
$list->render();
|
||||||
page_foot();
|
page_foot();
|
||||||
$db->close();
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feed Maintenance Page
|
* Feed Maintenance Page
|
||||||
*
|
*
|
||||||
|
@ -12,12 +13,11 @@ use BitBadger\Documents\Query;
|
||||||
use BitBadger\Documents\SQLite\Custom;
|
use BitBadger\Documents\SQLite\Custom;
|
||||||
use FeedReaderCentral\Feed;
|
use FeedReaderCentral\Feed;
|
||||||
use FeedReaderCentral\Key;
|
use FeedReaderCentral\Key;
|
||||||
use FeedReaderCentral\Security;
|
|
||||||
use FeedReaderCentral\Table;
|
use FeedReaderCentral\Table;
|
||||||
|
|
||||||
include '../start.php';
|
include '../start.php';
|
||||||
|
|
||||||
Security::verifyUser();
|
FeedReaderCentral\Security::verifyUser();
|
||||||
|
|
||||||
// TODO: adapt query when document list is done
|
// TODO: adapt query when document list is done
|
||||||
$field = Field::EQ('user_id', $_SESSION[Key::USER_ID], '@user');
|
$field = Field::EQ('user_id', $_SESSION[Key::USER_ID], '@user');
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Home Page
|
* Home Page
|
||||||
*
|
*
|
||||||
|
@ -8,15 +9,15 @@
|
||||||
use BitBadger\Documents\SQLite\Configuration;
|
use BitBadger\Documents\SQLite\Configuration;
|
||||||
use FeedReaderCentral\Feed;
|
use FeedReaderCentral\Feed;
|
||||||
use FeedReaderCentral\ItemList;
|
use FeedReaderCentral\ItemList;
|
||||||
use FeedReaderCentral\Security;
|
|
||||||
|
|
||||||
include '../start.php';
|
include '../start.php';
|
||||||
|
|
||||||
Security::verifyUser();
|
FeedReaderCentral\Security::verifyUser();
|
||||||
|
|
||||||
$db = Configuration::dbConn();
|
|
||||||
if (key_exists('refresh', $_GET)) {
|
if (key_exists('refresh', $_GET)) {
|
||||||
|
$db = Configuration::dbConn();
|
||||||
$refreshResult = Feed::refreshAll($db);
|
$refreshResult = Feed::refreshAll($db);
|
||||||
|
$db->close();
|
||||||
if (key_exists('ok', $refreshResult)) {
|
if (key_exists('ok', $refreshResult)) {
|
||||||
add_info('All feeds refreshed successfully');
|
add_info('All feeds refreshed successfully');
|
||||||
} else {
|
} else {
|
||||||
|
@ -25,8 +26,8 @@ if (key_exists('refresh', $_GET)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$list = match (true) {
|
$list = match (true) {
|
||||||
key_exists('bookmarked', $_GET) => ItemList::allBookmarked($db),
|
key_exists('bookmarked', $_GET) => ItemList::allBookmarked(),
|
||||||
default => ItemList::allUnread($db)
|
default => ItemList::allUnread()
|
||||||
};
|
};
|
||||||
$title = "Your $list->itemType Items";
|
$title = "Your $list->itemType Items";
|
||||||
|
|
||||||
|
@ -39,4 +40,3 @@ if ($list->itemType == 'Unread') {
|
||||||
echo '</h1>';
|
echo '</h1>';
|
||||||
$list->render();
|
$list->render();
|
||||||
page_foot();
|
page_foot();
|
||||||
$db->close();
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item View Page
|
* Item View Page
|
||||||
|
@ -7,22 +7,19 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use BitBadger\Documents\DocumentException;
|
use BitBadger\Documents\DocumentException;
|
||||||
use BitBadger\Documents\SQLite\Configuration;
|
|
||||||
use BitBadger\Documents\SQLite\Delete;
|
use BitBadger\Documents\SQLite\Delete;
|
||||||
use BitBadger\Documents\SQLite\Patch;
|
use BitBadger\Documents\SQLite\Patch;
|
||||||
use FeedReaderCentral\Item;
|
use FeedReaderCentral\ItemWithFeed;
|
||||||
use FeedReaderCentral\Key;
|
|
||||||
use FeedReaderCentral\Security;
|
|
||||||
use FeedReaderCentral\Table;
|
use FeedReaderCentral\Table;
|
||||||
|
|
||||||
include '../start.php';
|
include '../start.php';
|
||||||
|
|
||||||
Security::verifyUser();
|
FeedReaderCentral\Security::verifyUser();
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
try {
|
try {
|
||||||
// "Keep as New" button sends a POST request to reset the is_read flag before going back to the item list
|
// "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'])) {
|
if (ItemWithFeed::existsById($_POST['id'])) {
|
||||||
Patch::byId(Table::ITEM, $_POST['id'], ['is_read' => 0]);
|
Patch::byId(Table::ITEM, $_POST['id'], ['is_read' => 0]);
|
||||||
}
|
}
|
||||||
frc_redirect($_POST['from']);
|
frc_redirect($_POST['from']);
|
||||||
|
@ -35,7 +32,7 @@ $from = $_GET['from'] ?? '/';
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'DELETE') {
|
if ($_SERVER['REQUEST_METHOD'] == 'DELETE') {
|
||||||
try {
|
try {
|
||||||
if (Item::retrieveByIdForUser($_GET['id'])) {
|
if (ItemWithFeed::existsById($_GET['id'])) {
|
||||||
Delete::byId(Table::ITEM, $_GET['id']);
|
Delete::byId(Table::ITEM, $_GET['id']);
|
||||||
}
|
}
|
||||||
} catch (DocumentException $ex) {
|
} catch (DocumentException $ex) {
|
||||||
|
@ -44,43 +41,29 @@ if ($_SERVER['REQUEST_METHOD'] == 'DELETE') {
|
||||||
frc_redirect($from);
|
frc_redirect($from);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: convert this query
|
if (!$item = ItemWithFeed::retrieveById($_GET['id'])) not_found();
|
||||||
$db = Configuration::dbConn();
|
|
||||||
$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
|
|
||||||
FROM item INNER JOIN feed ON feed.id = item.feed_id
|
|
||||||
WHERE item.id = :id
|
|
||||||
AND feed.user_id = :user
|
|
||||||
SQL);
|
|
||||||
$query->bindValue(':id', $_GET['id']);
|
|
||||||
$query->bindValue(':user', $_SESSION[Key::USER_ID]);
|
|
||||||
$result = $query->execute();
|
|
||||||
$item = $result ? $result->fetchArray(SQLITE3_ASSOC) : false;
|
|
||||||
|
|
||||||
if ($item) {
|
try {
|
||||||
try {
|
Patch::byId(Table::ITEM, $_GET['id'], ['is_read' => 1]);
|
||||||
Patch::byId(Table::ITEM, $_GET['id'], ['is_read' => 1], $db);
|
} catch (DocumentException $ex) {
|
||||||
} catch (DocumentException $ex) {
|
|
||||||
add_error("$ex");
|
add_error("$ex");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$published = date_time($item['published_on']);
|
$published = date_time($item->published_on);
|
||||||
$updated = isset($item['updated_on']) ? date_time($item['updated_on']) : null;
|
$updated = isset($item->updated_on) ? date_time($item->updated_on) : null;
|
||||||
|
|
||||||
page_head(htmlentities("{$item['item_title']} | {$item['feed_title']}")); ?>
|
page_head(htmlentities("$item->title | {$item->feed->title}")); ?>
|
||||||
<h1 class=item_heading>
|
<h1 class=item_heading>
|
||||||
<span class=bookmark hx-get="/bookmark?id=<?=$_GET['id']?>" hx-trigger=load hx-target=this hx-swap=outerHTML
|
<span class=bookmark hx-get="/bookmark?id=<?=$_GET['id']?>" hx-trigger=load hx-target=this hx-swap=outerHTML
|
||||||
hx-push-url=false></span>
|
hx-push-url=false></span>
|
||||||
<a href="<?=$item['item_link']?>" target=_blank rel=noopener><?=strip_tags($item['item_title'])?></a><br>
|
<a href="<?=$item->item_link?>" target=_blank rel=noopener><?=strip_tags($item->title)?></a><br>
|
||||||
</h1>
|
</h1>
|
||||||
<div class=item_published>
|
<div class=item_published>
|
||||||
From <strong><?=htmlentities($item['feed_title'])?></strong><br>
|
From <strong><?=htmlentities($item->feed->title)?></strong><br>
|
||||||
Published <?=date_time($item['published_on'])?><?=$updated && $updated != $published ? " (Updated $updated)" : ''?>
|
Published <?=date_time($item->published_on)?><?=$updated && $updated != $published ? " (Updated $updated)" : ''?>
|
||||||
</div>
|
</div>
|
||||||
<article>
|
<article>
|
||||||
<div class=item_content><?=str_replace('<a ', '<a target=_blank rel=noopener ', $item['content'])?></div>
|
<div class=item_content><?=str_replace('<a ', '<a target=_blank rel=noopener ', $item->content)?></div>
|
||||||
<form class=action_buttons action=/item method=POST hx-post=/item>
|
<form class=action_buttons action=/item method=POST hx-post=/item>
|
||||||
<input type=hidden name=id value=<?=$_GET['id']?>>
|
<input type=hidden name=id value=<?=$_GET['id']?>>
|
||||||
<input type=hidden name=from value="<?=$from?>">
|
<input type=hidden name=from value="<?=$from?>">
|
||||||
|
@ -90,4 +73,3 @@ page_head(htmlentities("{$item['item_title']} | {$item['feed_title']}")); ?>
|
||||||
</form>
|
</form>
|
||||||
</article><?php
|
</article><?php
|
||||||
page_foot();
|
page_foot();
|
||||||
$db->close();
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item Search Page
|
* Item Search Page
|
||||||
|
@ -6,20 +6,15 @@
|
||||||
* Search for items across all feeds
|
* Search for items across all feeds
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use BitBadger\Documents\SQLite\Configuration;
|
|
||||||
use FeedReaderCentral\ItemList;
|
|
||||||
use FeedReaderCentral\Security;
|
|
||||||
|
|
||||||
include '../start.php';
|
include '../start.php';
|
||||||
|
|
||||||
Security::verifyUser();
|
FeedReaderCentral\Security::verifyUser();
|
||||||
|
|
||||||
$search = $_GET['search'] ?? '';
|
$search = $_GET['search'] ?? '';
|
||||||
$items = $_GET['items'] ?? 'all';
|
$items = $_GET['items'] ?? 'all';
|
||||||
|
|
||||||
$db = Configuration::dbConn();
|
|
||||||
if ($search != '') {
|
if ($search != '') {
|
||||||
$list = ItemList::matchingSearch($search, $items == 'bookmarked', $db);
|
$list = FeedReaderCentral\ItemList::matchingSearch($search, $items == 'bookmarked');
|
||||||
}
|
}
|
||||||
|
|
||||||
page_head('Item Search'); ?>
|
page_head('Item Search'); ?>
|
||||||
|
@ -46,4 +41,3 @@ page_head('Item Search'); ?>
|
||||||
} ?>
|
} ?>
|
||||||
</article><?php
|
</article><?php
|
||||||
page_foot();
|
page_foot();
|
||||||
$db->close();
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User Log Off Page
|
* User Log Off Page
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
include '../../start.php';
|
include '../../start.php';
|
||||||
|
|
||||||
use FeedReaderCentral\Key;
|
use FeedReaderCentral\Key;
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
use BitBadger\Documents\Field;
|
|
||||||
use BitBadger\Documents\Query;
|
|
||||||
use BitBadger\Documents\SQLite\Custom;
|
|
||||||
use BitBadger\Documents\SQLite\Parameters;
|
|
||||||
use BitBadger\Documents\SQLite\Results;
|
|
||||||
use FeedReaderCentral\Data;
|
|
||||||
use FeedReaderCentral\Key;
|
use FeedReaderCentral\Key;
|
||||||
use FeedReaderCentral\Security;
|
use FeedReaderCentral\Security;
|
||||||
use FeedReaderCentral\Table;
|
|
||||||
use FeedReaderCentral\User;
|
use FeedReaderCentral\User;
|
||||||
|
|
||||||
require 'app-config.php';
|
require 'app-config.php';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* USER CONFIGURATION ITEMS
|
* USER CONFIGURATION ITEMS
|
||||||
*
|
*
|
||||||
|
@ -8,6 +9,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use FeedReaderCentral\Feed;
|
use FeedReaderCentral\Feed;
|
||||||
|
use FeedReaderCentral\Security;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Which security model should the application use? Options are:
|
* Which security model should the application use? Options are:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
use BitBadger\Documents\ArrayMapper;
|
use BitBadger\Documents\ArrayMapper;
|
||||||
use BitBadger\Documents\DocumentException;
|
use BitBadger\Documents\DocumentException;
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
use BitBadger\Documents\DocumentException;
|
use BitBadger\Documents\DocumentException;
|
||||||
use BitBadger\Documents\SQLite\Configuration;
|
use BitBadger\Documents\SQLite\Configuration;
|
||||||
use BitBadger\Documents\SQLite\Find;
|
use BitBadger\Documents\SQLite\Find;
|
||||||
use FeedReaderCentral\Data;
|
|
||||||
use FeedReaderCentral\Feed;
|
use FeedReaderCentral\Feed;
|
||||||
use FeedReaderCentral\Table;
|
use FeedReaderCentral\Table;
|
||||||
use FeedReaderCentral\User;
|
use FeedReaderCentral\User;
|
||||||
|
@ -46,7 +45,7 @@ function refresh_all(): void
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$users = [];
|
$users = [];
|
||||||
iterator_apply(Feed::retrieveAll()->items(), function (Feed $feed) use ($db, $users) {
|
iterator_apply(Feed::retrieveAll()->items(), function (Feed $feed) use ($db, &$users) {
|
||||||
$result = Feed::refreshFeed($feed->id, $feed->url, $db);
|
$result = Feed::refreshFeed($feed->id, $feed->url, $db);
|
||||||
$userKey = "$feed->user_id";
|
$userKey = "$feed->user_id";
|
||||||
if (!key_exists($userKey, $users)) $users[$userKey] = Find::byId(Table::USER, $feed->user_id, User::class);
|
if (!key_exists($userKey, $users)) $users[$userKey] = Find::byId(Table::USER, $feed->user_id, User::class);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
use BitBadger\Documents\DocumentException;
|
use BitBadger\Documents\DocumentException;
|
||||||
use BitBadger\Documents\SQLite\Configuration;
|
use BitBadger\Documents\SQLite\Configuration;
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
<?php
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
use BitBadger\Documents\DocumentException;
|
use BitBadger\Documents\DocumentException;
|
||||||
use BitBadger\Documents\Field;
|
use BitBadger\Documents\Field;
|
||||||
|
use BitBadger\Documents\Query;
|
||||||
use BitBadger\Documents\SQLite\Configuration;
|
use BitBadger\Documents\SQLite\Configuration;
|
||||||
use BitBadger\Documents\SQLite\Count;
|
use BitBadger\Documents\SQLite\Count;
|
||||||
|
use BitBadger\Documents\SQLite\Custom;
|
||||||
use BitBadger\Documents\SQLite\Delete;
|
use BitBadger\Documents\SQLite\Delete;
|
||||||
|
use BitBadger\Documents\SQLite\Parameters;
|
||||||
use BitBadger\Documents\SQLite\Patch;
|
use BitBadger\Documents\SQLite\Patch;
|
||||||
use FeedReaderCentral\Data;
|
|
||||||
use FeedReaderCentral\Security;
|
use FeedReaderCentral\Security;
|
||||||
use FeedReaderCentral\Table;
|
use FeedReaderCentral\Table;
|
||||||
use FeedReaderCentral\User;
|
use FeedReaderCentral\User;
|
||||||
|
@ -187,12 +189,11 @@ function delete_user(string $email): void
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: convert query
|
$fields = [Field::EQ('user_id', $user->id, '@user')];
|
||||||
$itemDelete = $db->prepare('DELETE FROM item WHERE feed_id IN (SELECT id FROM feed WHERE user_id = :user)');
|
Custom::nonQuery(
|
||||||
$itemDelete->bindValue(':user', $user->id);
|
'DELETE FROM ' . Table::ITEM . " WHERE data->>'feed_id' IN (SELECT data->>'id' FROM " . Table::FEED
|
||||||
$itemDelete->execute();
|
. ' WHERE ' . Query::whereByFields($fields) . ')', Parameters::addFields($fields, []), $db);
|
||||||
|
Delete::byFields(Table::FEED, $fields, $db);
|
||||||
Delete::byFields(Table::FEED, [Field::EQ('user_id', $user->id)], $db);
|
|
||||||
Delete::byId(Table::USER, $user->id, $db);
|
Delete::byId(Table::USER, $user->id, $db);
|
||||||
|
|
||||||
printfn('%s deleted successfully', init_cap($displayUser));
|
printfn('%s deleted successfully', init_cap($displayUser));
|
||||||
|
|
Loading…
Reference in New Issue
Block a user