WIP on document conversion
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
<?php
|
||||
namespace FeedReaderCentral;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Exception;
|
||||
use SQLite3;
|
||||
|
||||
/**
|
||||
* A centralized place for data access for the application
|
||||
*/
|
||||
|
||||
71
src/lib/Domain/Feed.php
Normal file
71
src/lib/Domain/Feed.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
namespace FeedReaderCentral\Domain;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Exception;
|
||||
use FeedReaderCentral\Data;
|
||||
use FeedReaderCentral\Key;
|
||||
|
||||
/**
|
||||
* An RSS or Atom feed
|
||||
*/
|
||||
class Feed
|
||||
{
|
||||
/** @var int The ID of the feed */
|
||||
public int $id = 0;
|
||||
|
||||
/** @var int The ID of the user to whom this subscription belongs */
|
||||
public int $user_id = 0;
|
||||
|
||||
/** @var string The URL of the feed */
|
||||
public string $url = '';
|
||||
|
||||
/** @var string|null The title of this feed */
|
||||
public ?string $title = null;
|
||||
|
||||
/** @var string|null The date/time items in this feed were last updated */
|
||||
public ?string $updated_on = null;
|
||||
|
||||
/** @var string|null The date/time this feed was last checked */
|
||||
public ?string $checked_on = null;
|
||||
|
||||
/**
|
||||
* The date/time items in this feed were last updated
|
||||
*
|
||||
* @return DateTimeImmutable|null The updated date, or null if it is not set
|
||||
* @throws Exception If the date/time is an invalid format
|
||||
*/
|
||||
public function updatedOn(): ?DateTimeImmutable
|
||||
{
|
||||
return is_null($this->updated_on) ? null : new DateTimeImmutable($this->updated_on);
|
||||
}
|
||||
|
||||
/**
|
||||
* The date/time this feed was last checked
|
||||
*
|
||||
* @return DateTimeImmutable|null The last checked date, or null if it is not set
|
||||
* @throws Exception If the date/time is an invalid format
|
||||
*/
|
||||
public function checkedOn(): ?DateTimeImmutable
|
||||
{
|
||||
return is_null($this->checked_on) ? null : new DateTimeImmutable($this->checked_on);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document from the parsed feed
|
||||
*
|
||||
* @param \FeedReaderCentral\Feed $feed The parsed feed
|
||||
* @return static The document constructed from the parsed feed
|
||||
*/
|
||||
public static function fromParsed(\FeedReaderCentral\Feed $feed): static
|
||||
{
|
||||
$it = new static();
|
||||
$it->user_id = $_SESSION[Key::USER_ID];
|
||||
$it->url = $feed->url;
|
||||
$it->title = $feed->title;
|
||||
$it->updated_on = $feed->updatedOn;
|
||||
$it->checked_on = Data::formatDate('now');
|
||||
|
||||
return $it;
|
||||
}
|
||||
}
|
||||
78
src/lib/Domain/Item.php
Normal file
78
src/lib/Domain/Item.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
namespace FeedReaderCentral\Domain;
|
||||
|
||||
use FeedReaderCentral\FeedItem;
|
||||
|
||||
class Item
|
||||
{
|
||||
/** @var int The ID of this item in the Feed Reader Central database */
|
||||
public int $id = 0;
|
||||
|
||||
/** @var int The ID of the feed to which this item belongs */
|
||||
public int $feed_id = 0;
|
||||
|
||||
/** @var string The title of this item */
|
||||
public string $title = '';
|
||||
|
||||
/** @var string The Globally Unique ID (GUID) for this item (an attribute in the feed XML) */
|
||||
public string $item_guid = '';
|
||||
|
||||
/** @var string The link to the item on its original site */
|
||||
public string $item_link = '';
|
||||
|
||||
/** @var string The date/time this item was published */
|
||||
public string $published_on = '';
|
||||
|
||||
/** @var string|null The date/time this item was last updated */
|
||||
public ?string $updated_on = null;
|
||||
|
||||
/** @var string The content for this item */
|
||||
public string $content = '';
|
||||
|
||||
/** @var int 1 if the item has been read, 0 if not */
|
||||
public int $is_read = 0;
|
||||
|
||||
/** @var int 1 if the item is bookmarked, 0 if not */
|
||||
public int $is_bookmarked = 0;
|
||||
|
||||
/**
|
||||
* Has the item been read?
|
||||
*
|
||||
* @return bool True if the item has been read, false if not
|
||||
*/
|
||||
public function isRead(): bool
|
||||
{
|
||||
return $this->is_read <> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the item bookmarked?
|
||||
*
|
||||
* @return bool True if the item is bookmarked, false if not
|
||||
*/
|
||||
public function isBookmarked(): bool
|
||||
{
|
||||
return $this->is_bookmarked <> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an item document from a parsed feed item
|
||||
*
|
||||
* @param int $feedId The ID of the feed to which this item belongs
|
||||
* @param FeedItem $item The parsed feed item
|
||||
* @return static The item document
|
||||
*/
|
||||
public static function fromFeedItem(int $feedId, FeedItem $item): static
|
||||
{
|
||||
$it = new static();
|
||||
$it->feed_id = $feedId;
|
||||
$it->item_guid = $item->guid;
|
||||
$it->item_link = $item->link;
|
||||
$it->title = $item->title;
|
||||
$it->published_on = $item->publishedOn;
|
||||
$it->updated_on = $item->updatedOn;
|
||||
$it->content = $item->content;
|
||||
|
||||
return $it;
|
||||
}
|
||||
}
|
||||
17
src/lib/Domain/Table.php
Normal file
17
src/lib/Domain/Table.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace FeedReaderCentral\Domain;
|
||||
|
||||
/**
|
||||
* Constants to use when accessing tables
|
||||
*/
|
||||
class Table
|
||||
{
|
||||
/** @var string The user table */
|
||||
public const string USER = 'frc_user';
|
||||
|
||||
/** @var string The feed table */
|
||||
public const string FEED = 'feed';
|
||||
|
||||
/** @var string The item table */
|
||||
public const string ITEM = 'item';
|
||||
}
|
||||
17
src/lib/Domain/User.php
Normal file
17
src/lib/Domain/User.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace FeedReaderCentral\Domain;
|
||||
|
||||
/**
|
||||
* A user of Feed Reader Central
|
||||
*/
|
||||
class User
|
||||
{
|
||||
/** @var int The ID of the user */
|
||||
public int $id = 0;
|
||||
|
||||
/** @var string The e-mail address for the user */
|
||||
public string $email = '';
|
||||
|
||||
/** @var string The password for the user */
|
||||
public string $password = '';
|
||||
}
|
||||
191
src/lib/Feed.php
191
src/lib/Feed.php
@@ -1,4 +1,24 @@
|
||||
<?php
|
||||
namespace FeedReaderCentral;
|
||||
|
||||
use BitBadger\Documents\Field;
|
||||
use BitBadger\Documents\Query;
|
||||
use BitBadger\Documents\SQLite\Custom;
|
||||
use BitBadger\Documents\SQLite\Document;
|
||||
use BitBadger\Documents\SQLite\Find;
|
||||
use BitBadger\Documents\SQLite\Patch;
|
||||
use BitBadger\Documents\SQLite\Results;
|
||||
use DateTimeInterface;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMException;
|
||||
use DOMNode;
|
||||
use Exception;
|
||||
use FeedReaderCentral\Domain\Feed as FeedDocument;
|
||||
use FeedReaderCentral\Domain\Item;
|
||||
use FeedReaderCentral\Domain\Table;
|
||||
use SQLite3;
|
||||
use SQLite3Result;
|
||||
|
||||
/**
|
||||
* Feed retrieval, parsing, and manipulation
|
||||
@@ -110,7 +130,7 @@ class Feed {
|
||||
}
|
||||
}
|
||||
|
||||
$feed = new Feed();
|
||||
$feed = new static();
|
||||
$feed->title = self::rssValue($channel, 'title');
|
||||
$feed->url = $url;
|
||||
$feed->updatedOn = Data::formatDate($updatedOn);
|
||||
@@ -267,21 +287,23 @@ class Feed {
|
||||
* @return bool|SQLite3Result The result if the update is successful, false if it failed
|
||||
*/
|
||||
private static function updateItem(int $itemId, FeedItem $item, SQLite3 $db): bool|SQLite3Result {
|
||||
$query = $db->prepare(<<<'SQL'
|
||||
UPDATE item
|
||||
SET title = :title,
|
||||
published_on = :published,
|
||||
updated_on = :updated,
|
||||
content = :content,
|
||||
is_read = 0
|
||||
WHERE id = :id
|
||||
SQL);
|
||||
$query->bindValue(':title', $item->title);
|
||||
$query->bindValue(':published', $item->publishedOn);
|
||||
$query->bindValue(':updated', $item->updatedOn);
|
||||
$query->bindValue(':content', $item->content);
|
||||
$query->bindValue(':id', $itemId);
|
||||
return $query->execute();
|
||||
Patch::byId(Table::ITEM, $itemId, $item->patchFields(), $db);
|
||||
// $query = $db->prepare(<<<'SQL'
|
||||
// UPDATE item
|
||||
// SET title = :title,
|
||||
// published_on = :published,
|
||||
// updated_on = :updated,
|
||||
// content = :content,
|
||||
// is_read = 0
|
||||
// WHERE id = :id
|
||||
// SQL);
|
||||
// $query->bindValue(':title', $item->title);
|
||||
// $query->bindValue(':published', $item->publishedOn);
|
||||
// $query->bindValue(':updated', $item->updatedOn);
|
||||
// $query->bindValue(':content', $item->content);
|
||||
// $query->bindValue(':id', $itemId);
|
||||
// return $query->execute();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,21 +315,23 @@ class Feed {
|
||||
* @return bool|SQLite3Result The result if the update is successful, false if it failed
|
||||
*/
|
||||
private static function addItem(int $feedId, FeedItem $item, SQLite3 $db): bool|SQLite3Result {
|
||||
$query = $db->prepare(<<<'SQL'
|
||||
INSERT INTO item (
|
||||
feed_id, item_guid, item_link, title, published_on, updated_on, content
|
||||
) VALUES (
|
||||
:feed, :guid, :link, :title, :published, :updated, :content
|
||||
)
|
||||
SQL);
|
||||
$query->bindValue(':feed', $feedId);
|
||||
$query->bindValue(':guid', $item->guid);
|
||||
$query->bindValue(':link', $item->link);
|
||||
$query->bindValue(':title', $item->title);
|
||||
$query->bindValue(':published', $item->publishedOn);
|
||||
$query->bindValue(':updated', $item->updatedOn);
|
||||
$query->bindValue(':content', $item->content);
|
||||
return $query->execute();
|
||||
Document::insert(Table::ITEM, Item::fromFeedItem($feedId, $item), $db);
|
||||
// $query = $db->prepare(<<<'SQL'
|
||||
// INSERT INTO item (
|
||||
// feed_id, item_guid, item_link, title, published_on, updated_on, content
|
||||
// ) VALUES (
|
||||
// :feed, :guid, :link, :title, :published, :updated, :content
|
||||
// )
|
||||
// SQL);
|
||||
// $query->bindValue(':feed', $feedId);
|
||||
// $query->bindValue(':guid', $item->guid);
|
||||
// $query->bindValue(':link', $item->link);
|
||||
// $query->bindValue(':title', $item->title);
|
||||
// $query->bindValue(':published', $item->publishedOn);
|
||||
// $query->bindValue(':updated', $item->updatedOn);
|
||||
// $query->bindValue(':content', $item->content);
|
||||
// return $query->execute();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,6 +345,7 @@ class Feed {
|
||||
public static function updateItems(int $feedId, Feed $feed, DateTimeInterface $lastChecked, SQLite3 $db): array {
|
||||
$results =
|
||||
array_map(function ($item) use ($db, $feedId) {
|
||||
// TODO: convert this query
|
||||
$existsQuery = $db->prepare(
|
||||
'SELECT id, published_on, updated_on FROM item WHERE feed_id = :feed AND item_guid = :guid');
|
||||
$existsQuery->bindValue(':feed', $feedId);
|
||||
@@ -357,6 +382,7 @@ class Feed {
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: convert this query
|
||||
$sql = match (PURGE_TYPE) {
|
||||
self::PURGE_READ => 'AND is_read = 1',
|
||||
self::PURGE_BY_DAYS => 'AND date(coalesce(updated_on, published_on)) < date(:oldest)',
|
||||
@@ -401,21 +427,24 @@ class Feed {
|
||||
$itemUpdate = self::updateItems($feedId, $feed, $lastChecked, $db);
|
||||
if (key_exists('error', $itemUpdate)) return $itemUpdate;
|
||||
|
||||
$urlUpdate = $url == $feed->url ? '' : ', url = :url';
|
||||
$feedUpdate = $db->prepare(<<<SQL
|
||||
UPDATE feed
|
||||
SET title = :title,
|
||||
updated_on = :updated,
|
||||
checked_on = :checked
|
||||
$urlUpdate
|
||||
WHERE id = :id
|
||||
SQL);
|
||||
$feedUpdate->bindValue(':title', $feed->title);
|
||||
$feedUpdate->bindValue(':updated', $feed->updatedOn);
|
||||
$feedUpdate->bindValue(':checked', Data::formatDate('now'));
|
||||
$feedUpdate->bindValue(':id', $feedId);
|
||||
if ($urlUpdate != '') $feedUpdate->bindValue(':url', $feed->url);
|
||||
if (!$feedUpdate->execute()) return Data::error($db);
|
||||
$patch = ['title' => $feed->title, 'updated_on' => $feed->updatedOn, 'checked_on' => Data::formatDate('now')];
|
||||
if ($url == $feed->url) $patch['url'] = $feed->url;
|
||||
Patch::byId(Table::FEED, $feedId, $patch, $db);
|
||||
// $urlUpdate = $url == $feed->url ? '' : ', url = :url';
|
||||
// $feedUpdate = $db->prepare(<<<SQL
|
||||
// UPDATE feed
|
||||
// SET title = :title,
|
||||
// updated_on = :updated,
|
||||
// checked_on = :checked
|
||||
// $urlUpdate
|
||||
// WHERE id = :id
|
||||
// SQL);
|
||||
// $feedUpdate->bindValue(':title', $feed->title);
|
||||
// $feedUpdate->bindValue(':updated', $feed->updatedOn);
|
||||
// $feedUpdate->bindValue(':checked', Data::formatDate('now'));
|
||||
// $feedUpdate->bindValue(':id', $feedId);
|
||||
// if ($urlUpdate != '') $feedUpdate->bindValue(':url', $feed->url);
|
||||
// if (!$feedUpdate->execute()) return Data::error($db);
|
||||
|
||||
return PURGE_TYPE == self::PURGE_NONE ? ['ok' => true] : self::purgeItems($feedId, $db);
|
||||
}
|
||||
@@ -432,49 +461,55 @@ class Feed {
|
||||
|
||||
$feed = $feedExtract['ok'];
|
||||
|
||||
$existsQuery = $db->prepare('SELECT COUNT(*) FROM feed WHERE user_id = :user AND url = :url');
|
||||
$existsQuery->bindValue(':user', $_SESSION[Key::USER_ID]);
|
||||
$existsQuery->bindValue(':url', $feed->url);
|
||||
if (!($exists = $existsQuery->execute())) return Data::error($db);
|
||||
if ($exists->fetchArray(SQLITE3_NUM)[0] > 0) return ['error' => "Already subscribed to feed $feed->url"];
|
||||
$whereUserAndUrl = ' WHERE ' . Query::whereByField(Field::EQ('user_id', ''), '@user')
|
||||
. ' AND ' . Query::whereByField(Field::EQ('url', ''), '@url');
|
||||
$userAndUrlParams = ['@user' => $_SESSION[Key::USER_ID], '@url' => $feed->url];
|
||||
if (Custom::scalar('SELECT EXISTS (SELECT 1 FROM ' . Table::FEED . $whereUserAndUrl . ')', $userAndUrlParams,
|
||||
Results::toExists(...), $db)) {
|
||||
return ['error' => "Already subscribed to feed $feed->url"];
|
||||
}
|
||||
|
||||
$query = $db->prepare(<<<'SQL'
|
||||
INSERT INTO feed (
|
||||
user_id, url, title, updated_on, checked_on
|
||||
) VALUES (
|
||||
:user, :url, :title, :updated, :checked
|
||||
)
|
||||
SQL);
|
||||
$query->bindValue(':user', $_SESSION[Key::USER_ID]);
|
||||
$query->bindValue(':url', $feed->url);
|
||||
$query->bindValue(':title', $feed->title);
|
||||
$query->bindValue(':updated', $feed->updatedOn);
|
||||
$query->bindValue(':checked', Data::formatDate('now'));
|
||||
if (!$query->execute()) return Data::error($db);
|
||||
Document::insert(Table::FEED, FeedDocument::fromParsed($feed), $db);
|
||||
// $query = $db->prepare(<<<'SQL'
|
||||
// INSERT INTO feed (
|
||||
// user_id, url, title, updated_on, checked_on
|
||||
// ) VALUES (
|
||||
// :user, :url, :title, :updated, :checked
|
||||
// )
|
||||
// SQL);
|
||||
// $query->bindValue(':user', $_SESSION[Key::USER_ID]);
|
||||
// $query->bindValue(':url', $feed->url);
|
||||
// $query->bindValue(':title', $feed->title);
|
||||
// $query->bindValue(':updated', $feed->updatedOn);
|
||||
// $query->bindValue(':checked', Data::formatDate('now'));
|
||||
// if (!$query->execute()) return Data::error($db);
|
||||
$doc = Custom::single(Query::selectFromTable(Table::FEED) . $whereUserAndUrl, $userAndUrlParams,
|
||||
Results::fromData(...), Domain\Feed::class, $db);
|
||||
if (!$doc) return ['error' => 'Could not retrieve inserted feed'];
|
||||
|
||||
$feedId = $db->lastInsertRowID();
|
||||
$result = self::updateItems($feedId, $feed, date_create_immutable(WWW_EPOCH), $db);
|
||||
$result = self::updateItems($doc->id, $feed, date_create_immutable(WWW_EPOCH), $db);
|
||||
if (key_exists('error', $result)) return $result;
|
||||
|
||||
return ['ok' => $feedId];
|
||||
return ['ok' => $doc->id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an RSS feed
|
||||
*
|
||||
* @param array $existing The existing RSS feed
|
||||
* @param FeedDocument $existing The existing RSS feed
|
||||
* @param string $url The URL with which the existing feed should be modified
|
||||
* @param SQLite3 $db The database connection on which to execute the update
|
||||
* @return bool[]|string[] [ 'ok' => true ] if successful, [ 'error' => message ] if not
|
||||
*/
|
||||
public static function update(array $existing, string $url, SQLite3 $db): array {
|
||||
public static function update(FeedDocument $existing, string $url, SQLite3 $db): array {
|
||||
// TODO: convert this query (need to make Query\Patch::update visible)
|
||||
$query = $db->prepare('UPDATE feed SET url = :url WHERE id = :id AND user_id = :user');
|
||||
$query->bindValue(':url', $url);
|
||||
$query->bindValue(':id', $existing['id']);
|
||||
$query->bindValue(':id', $existing->id);
|
||||
$query->bindValue(':user', $_SESSION[Key::USER_ID]);
|
||||
if (!$query->execute()) return Data::error($db);
|
||||
|
||||
return self::refreshFeed($existing['id'], $url, $db);
|
||||
return self::refreshFeed($existing->id, $url, $db);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -520,12 +555,14 @@ class Feed {
|
||||
*
|
||||
* @param int $feedId The ID of the feed to retrieve
|
||||
* @param SQLite3 $db A database connection to use to retrieve the feed
|
||||
* @return array|bool The data for the feed if found, false if not found
|
||||
* @return FeedDocument|false The data for the feed if found, false if not found
|
||||
*/
|
||||
public static function retrieveById(int $feedId, SQLite3 $db): array|bool {
|
||||
$query = $db->prepare('SELECT * FROM feed WHERE id = :id AND user_id = :user');
|
||||
$query->bindValue(':id', $feedId);
|
||||
$query->bindValue(':user', $_SESSION[Key::USER_ID]);
|
||||
return ($result = $query->execute()) ? $result->fetchArray(SQLITE3_ASSOC) : false;
|
||||
public static function retrieveById(int $feedId, SQLite3 $db): FeedDocument|false {
|
||||
$doc = Find::byId(Table::FEED, $feedId, FeedDocument::class, $db);
|
||||
return $doc && $doc->user_id == $_SESSION[Key::USER_ID] ? $doc : false;
|
||||
// $query = $db->prepare('SELECT * FROM feed WHERE id = :id AND user_id = :user');
|
||||
// $query->bindValue(':id', $feedId);
|
||||
// $query->bindValue(':user', $_SESSION[Key::USER_ID]);
|
||||
// return ($result = $query->execute()) ? $result->fetchArray(SQLITE3_ASSOC) : false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php
|
||||
namespace FeedReaderCentral;
|
||||
|
||||
use DOMNode;
|
||||
|
||||
/**
|
||||
* Information for a feed item
|
||||
@@ -23,13 +26,30 @@ class FeedItem {
|
||||
/** @var string The content for the item */
|
||||
public string $content = '';
|
||||
|
||||
/**
|
||||
* Get the fields needed to update the item in the database
|
||||
*
|
||||
* @return array The fields needed tu update an item
|
||||
*/
|
||||
public function patchFields(): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->title,
|
||||
'published_on' => $this->publishedOn,
|
||||
'updated_on' => $this->updatedOn,
|
||||
'content' => $this->content,
|
||||
'is_read' => 0
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a feed item from an Atom feed's `<entry>` tag
|
||||
*
|
||||
* @param DOMNode $node The XML node from which a feed item should be constructed
|
||||
* @return FeedItem A feed item constructed from the given node
|
||||
* @return static A feed item constructed from the given node
|
||||
*/
|
||||
public static function fromAtom(DOMNode $node): FeedItem {
|
||||
public static function fromAtom(DOMNode $node): static
|
||||
{
|
||||
$guid = Feed::atomValue($node, 'id');
|
||||
$link = '';
|
||||
foreach ($node->getElementsByTagName('link') as $linkElt) {
|
||||
@@ -43,7 +63,7 @@ class FeedItem {
|
||||
}
|
||||
if ($link == '' && str_starts_with($guid, 'http')) $link = $guid;
|
||||
|
||||
$item = new FeedItem();
|
||||
$item = new static();
|
||||
$item->guid = $guid;
|
||||
$item->title = Feed::atomValue($node, 'title');
|
||||
$item->link = $link;
|
||||
@@ -58,14 +78,15 @@ class FeedItem {
|
||||
* Construct a feed item from an RSS feed's `<item>` tag
|
||||
*
|
||||
* @param DOMNode $node The XML node from which a feed item should be constructed
|
||||
* @return FeedItem A feed item constructed from the given node
|
||||
* @return static A feed item constructed from the given node
|
||||
*/
|
||||
public static function fromRSS(DOMNode $node): FeedItem {
|
||||
public static function fromRSS(DOMNode $node): static
|
||||
{
|
||||
$itemGuid = Feed::rssValue($node, 'guid');
|
||||
$updNodes = $node->getElementsByTagNameNS(Feed::ATOM_NS, 'updated');
|
||||
$encNodes = $node->getElementsByTagNameNS(Feed::CONTENT_NS, 'encoded');
|
||||
|
||||
$item = new FeedItem();
|
||||
$item = new static();
|
||||
$item->guid = $itemGuid == 'guid not found' ? Feed::rssValue($node, 'link') : $itemGuid;
|
||||
$item->title = Feed::rssValue($node, 'title');
|
||||
$item->link = Feed::rssValue($node, 'link');
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?php
|
||||
namespace FeedReaderCentral;
|
||||
|
||||
use SQLite3;
|
||||
use SQLite3Result;
|
||||
use SQLite3Stmt;
|
||||
|
||||
/**
|
||||
* A list of items to be displayed
|
||||
@@ -39,7 +44,8 @@ class ItemList {
|
||||
* @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
|
||||
*/
|
||||
private function __construct(SQLite3 $db, SQLite3Stmt $query, public string $itemType, public string $returnURL = '') {
|
||||
private function __construct(SQLite3 $db, SQLite3Stmt $query, public string $itemType, public string $returnURL = '')
|
||||
{
|
||||
$result = $query->execute();
|
||||
if (!$result) {
|
||||
$this->error = Data::error($db)['error'];
|
||||
@@ -56,7 +62,8 @@ class ItemList {
|
||||
* @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 {
|
||||
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,
|
||||
@@ -77,7 +84,8 @@ class ItemList {
|
||||
* @param SQLite3 $db The database connection to use to obtain items
|
||||
* @return static An item list with all bookmarked items
|
||||
*/
|
||||
public static function allBookmarked(SQLite3 $db): static {
|
||||
public static function allBookmarked(SQLite3 $db): static
|
||||
{
|
||||
$list = new static($db, self::makeQuery($db, ['item.is_bookmarked = 1']), 'Bookmarked', '/?bookmarked');
|
||||
$list->linkFeed = true;
|
||||
return $list;
|
||||
@@ -89,7 +97,8 @@ class ItemList {
|
||||
* @param SQLite3 $db The database connection to use to obtain items
|
||||
* @return static An item list with all unread items
|
||||
*/
|
||||
public static function allUnread(SQLite3 $db): static {
|
||||
public static function allUnread(SQLite3 $db): static
|
||||
{
|
||||
$list = new static($db, self::makeQuery($db, ['item.is_read = 0']), 'Unread');
|
||||
$list->linkFeed = true;
|
||||
return $list;
|
||||
@@ -102,7 +111,8 @@ class ItemList {
|
||||
* @param SQLite3 $db The database connection to use to obtain items
|
||||
* @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, SQLite3 $db): static
|
||||
{
|
||||
$list = new static($db, self::makeQuery($db, ['feed.id = :feed'], [[':feed', $feedId]]), '',
|
||||
"/feed/items?id=$feedId");
|
||||
$list->showIndicators = true;
|
||||
@@ -116,7 +126,8 @@ class ItemList {
|
||||
* @param SQLite3 $db The database connection to use to obtain items
|
||||
* @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, SQLite3 $db): static
|
||||
{
|
||||
return new static($db, self::makeQuery($db, ['feed.id = :feed', 'item.is_read = 0'], [[':feed', $feedId]]),
|
||||
'Unread', "/feed/items?id=$feedId&unread");
|
||||
}
|
||||
@@ -128,7 +139,8 @@ class ItemList {
|
||||
* @param SQLite3 $db The database connection to use to obtain items
|
||||
* @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, SQLite3 $db): static
|
||||
{
|
||||
return new static($db,
|
||||
self::makeQuery($db, ['feed.id = :feed', 'item.is_bookmarked = 1'], [[':feed', $feedId]]), 'Bookmarked',
|
||||
"/feed/items?id=$feedId&bookmarked");
|
||||
@@ -142,7 +154,8 @@ class ItemList {
|
||||
* @param SQLite3 $db The database connection to use to obtain items
|
||||
* @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, SQLite3 $db): static
|
||||
{
|
||||
$where = $isBookmarked ? ['item.is_bookmarked = 1'] : [];
|
||||
$where[] = 'item.id IN (SELECT ROWID FROM item_search WHERE content MATCH :search)';
|
||||
$list = new static($db, self::makeQuery($db, $where, [[':search', $search]]),
|
||||
@@ -156,7 +169,8 @@ class ItemList {
|
||||
/**
|
||||
* Render this item list
|
||||
*/
|
||||
public function render(): void {
|
||||
public function render(): void
|
||||
{
|
||||
if ($this->isError()) { ?>
|
||||
<p>Error retrieving list:<br><?=$this->error?><?php
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?php
|
||||
namespace FeedReaderCentral;
|
||||
|
||||
/**
|
||||
* Session and other keys used for array indexes
|
||||
*/
|
||||
class Key {
|
||||
|
||||
/** @var string The $_SESSION key for the current user's e-mail address */
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
<?php
|
||||
namespace FeedReaderCentral;
|
||||
|
||||
use BitBadger\Documents\Field;
|
||||
use BitBadger\Documents\SQLite\Document;
|
||||
use BitBadger\Documents\SQLite\Find;
|
||||
use BitBadger\Documents\SQLite\Patch;
|
||||
use FeedReaderCentral\Domain\Table;
|
||||
use FeedReaderCentral\Domain\User;
|
||||
use SQLite3;
|
||||
|
||||
/**
|
||||
* Security functions
|
||||
@@ -28,13 +37,14 @@ class Security {
|
||||
*
|
||||
* @param string $email The e-mail address of the user to retrieve
|
||||
* @param SQLite3 $db The data connection to use to retrieve the user
|
||||
* @return array|false The user information, or null if the user is not found
|
||||
* @return User|false The user information, or null if the user is not found
|
||||
*/
|
||||
public static function findUserByEmail(string $email, SQLite3 $db): array|false {
|
||||
$query = $db->prepare('SELECT * FROM frc_user WHERE email = :email');
|
||||
$query->bindValue(':email', $email);
|
||||
$result = $query->execute();
|
||||
return $result ? $result->fetchArray(SQLITE3_ASSOC) : false;
|
||||
public static function findUserByEmail(string $email, SQLite3 $db): User|false {
|
||||
return Find::firstByField(Table::USER, Field::EQ('email', $email), User::class, $db);
|
||||
// $query = $db->prepare('SELECT * FROM frc_user WHERE email = :email');
|
||||
// $query->bindValue(':email', $email);
|
||||
// $result = $query->execute();
|
||||
// return $result ? $result->fetchArray(SQLITE3_ASSOC) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,27 +55,32 @@ class Security {
|
||||
* @param SQLite3 $db The data connection to use to add the user
|
||||
*/
|
||||
public static function addUser(string $email, string $password, SQLite3 $db): void {
|
||||
$query = $db->prepare('INSERT INTO frc_user (email, password) VALUES (:email, :password)');
|
||||
$query->bindValue(':email', $email);
|
||||
$query->bindValue(':password', password_hash($password, self::PW_ALGORITHM));
|
||||
$query->execute();
|
||||
$user = new User();
|
||||
$user->email = $email;
|
||||
$user->password = $password;
|
||||
Document::insert(Table::USER, $user, $db);
|
||||
// $query = $db->prepare('INSERT INTO frc_user (email, password) VALUES (:email, :password)');
|
||||
// $query->bindValue(':email', $email);
|
||||
// $query->bindValue(':password', password_hash($password, self::PW_ALGORITHM));
|
||||
// $query->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a user's password
|
||||
*
|
||||
* @param array $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|null $returnTo The URL to which the user should be redirected
|
||||
* @param SQLite3 $db The database connection to use to verify the user's credentials
|
||||
*/
|
||||
private static function verifyPassword(array $user, string $password, ?string $returnTo, SQLite3 $db): void {
|
||||
if (password_verify($password, $user['password'])) {
|
||||
if (password_needs_rehash($user['password'], self::PW_ALGORITHM)) {
|
||||
$rehash = $db->prepare('UPDATE frc_user SET password = :hash WHERE id = :id');
|
||||
$rehash->bindValue(':hash', password_hash($password, self::PW_ALGORITHM));
|
||||
$rehash->bindValue(':id', $user['id']);
|
||||
$rehash->execute();
|
||||
private static function verifyPassword(User $user, string $password, ?string $returnTo, SQLite3 $db): void {
|
||||
if (password_verify($password, $user->password)) {
|
||||
if (password_needs_rehash($user->password, self::PW_ALGORITHM)) {
|
||||
Patch::byId(Table::USER, $user->id, ['password' => password_hash($password, self::PW_ALGORITHM)], $db);
|
||||
// $rehash = $db->prepare('UPDATE frc_user SET password = :hash WHERE id = :id');
|
||||
// $rehash->bindValue(':hash', password_hash($password, self::PW_ALGORITHM));
|
||||
// $rehash->bindValue(':id', $user['id']);
|
||||
// $rehash->execute();
|
||||
}
|
||||
$_SESSION[Key::USER_ID] = $user['id'];
|
||||
$_SESSION[Key::USER_EMAIL] = $user['email'];
|
||||
@@ -104,10 +119,12 @@ class Security {
|
||||
* @param SQLite3 $db The database connection to use in updating the password
|
||||
*/
|
||||
public static function updatePassword(string $email, string $password, SQLite3 $db): void {
|
||||
$query = $db->prepare('UPDATE frc_user SET password = :password WHERE email = :email');
|
||||
$query->bindValue(':password', password_hash($password, self::PW_ALGORITHM));
|
||||
$query->bindValue(':email', $email);
|
||||
$query->execute();
|
||||
Patch::byField(Table::USER, Field::EQ('email', $email),
|
||||
['password' => password_hash($password, self::PW_ALGORITHM)], $db);
|
||||
// $query = $db->prepare('UPDATE frc_user SET password = :password WHERE email = :email');
|
||||
// $query->bindValue(':password', password_hash($password, self::PW_ALGORITHM));
|
||||
// $query->bindValue(':email', $email);
|
||||
// $query->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user