Add table to doc util script

- Remove db parameter from several places
- Add constructors for document types
This commit is contained in:
2024-06-01 23:17:29 -04:00
parent 610ab67475
commit b88ad1f268
26 changed files with 306 additions and 262 deletions

View File

@@ -14,18 +14,8 @@ use SQLite3;
/**
* A centralized place for data access for the application
*/
class Data {
/**
* Obtain a new connection to the database
* @return SQLite3 A new connection to the database
*/
public static function getConnection(): SQLite3 {
$db = new SQLite3(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'data', DATABASE_NAME]));
$db->exec('PRAGMA foreign_keys = ON;');
return $db;
}
class Data
{
/**
* Create the search index and synchronization triggers for the item table
*
@@ -71,51 +61,17 @@ class Data {
{
$tables = Custom::array("SELECT name FROM sqlite_master WHERE type = 'table'", [], new StringMapper('name'));
$db = Configuration::dbConn();
// $tables = array();
// $tableQuery = $db->query("SELECT name FROM sqlite_master WHERE type = 'table'");
// while ($table = $tableQuery->fetchArray(SQLITE3_NUM)) $tables[] = $table[0];
if (!in_array(Table::USER, $tables)) {
Definition::ensureTable(Table::USER, $db);
Definition::ensureFieldIndex(Table::USER, 'email', ['email'], $db);
// $db->exec(<<<'SQL'
// CREATE TABLE frc_user (
// id INTEGER NOT NULL PRIMARY KEY,
// email TEXT NOT NULL,
// password TEXT NOT NULL)
// SQL);
// $db->exec('CREATE INDEX idx_user_email ON frc_user (email)');
}
if (!in_array(Table::FEED, $tables)) {
Definition::ensureTable(Table::FEED, $db);
Definition::ensureFieldIndex(Table::FEED, 'user', ['user_id'], $db);
// $db->exec(<<<'SQL'
// CREATE TABLE feed (
// id INTEGER NOT NULL PRIMARY KEY,
// user_id INTEGER NOT NULL,
// url TEXT NOT NULL,
// title TEXT,
// updated_on TEXT,
// checked_on TEXT,
// FOREIGN KEY (user_id) REFERENCES frc_user (id))
// SQL);
}
if (!in_array(Table::ITEM, $tables)) {
Definition::ensureTable(Table::ITEM, $db);
Definition::ensureFieldIndex(Table::ITEM, 'feed', ['feed_id', 'item_link'], $db);
// $db->exec(<<<'SQL'
// CREATE TABLE item (
// id INTEGER NOT NULL PRIMARY KEY,
// feed_id INTEGER NOT NULL,
// title TEXT NOT NULL,
// item_guid TEXT NOT NULL,
// item_link TEXT NOT NULL,
// published_on TEXT NOT NULL,
// updated_on TEXT,
// content TEXT NOT NULL,
// is_read BOOLEAN NOT NULL DEFAULT 0,
// is_bookmarked BOOLEAN NOT NULL DEFAULT 0,
// FOREIGN KEY (feed_id) REFERENCES feed (id))
// SQL);
self::createSearchIndex($db);
}
$db->close();
@@ -127,21 +83,12 @@ class Data {
* @param ?string $value The date/time to be parsed and formatted
* @return string|null The date/time in `DateTimeInterface::ATOM` format, or `null` if the input cannot be parsed
*/
public static function formatDate(?string $value): ?string {
public static function formatDate(?string $value): ?string
{
try {
return $value ? (new DateTimeImmutable($value))->format(DateTimeInterface::ATOM) : null;
} catch (Exception) {
return null;
}
}
/**
* Return the last SQLite error message as a result array
*
* @param SQLite3 $db The database connection on which the error has occurred
* @return string[] ['error' => message] for last SQLite error message
*/
public static function error(SQLite3 $db): array {
return ['error' => 'SQLite error: ' . $db->lastErrorMsg()];
}
}

View File

@@ -33,25 +33,19 @@ class Feed
/** @var int Purge items in number greater than the specified number of items to keep */
public const int PURGE_BY_COUNT = 3;
// ***** PROPERTIES *****
/** @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;
/**
* Constructor
*
* @param int $id The ID of the feed
* @param int $user_id The ID of the user to whom this subscription belongs
* @param string $url The URL of the feed
* @param string|null $title The title of this feed
* @param string|null $updated_on The date/time items in this feed were last updated
* @param string|null $checked_on The date/time this feed was last checked
*/
public function __construct(public int $id = 0, public int $user_id = 0, public string $url = '',
public ?string $title = null, public ?string $updated_on = null,
public ?string $checked_on = null) { }
// ***** STATIC FUNCTIONS *****

View File

@@ -14,35 +14,24 @@ use BitBadger\Documents\SQLite\Parameters;
*/
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;
/**
* Constructor
*
* @param int $id The ID of this item in the Feed Reader Central database
* @param int $feed_id The ID of the feed to which this item belongs
* @param string $title The title of this item
* @param string $item_guid The Globally Unique ID (GUID) for this item (an attribute in the feed XML)
* @param string $item_link The link to the item on its original site
* @param string $published_on The date/time this item was published
* @param string|null $updated_on The date/time this item was last updated
* @param string $content The content for this item
* @param int $is_read 1 if the item has been read, 0 if not
* @param int $is_bookmarked 1 if the item is bookmarked, 0 if not
*/
public function __construct(public int $id = 0, public int $feed_id = 0, public string $title = '',
public string $item_guid = '', public string $item_link = '',
public string $published_on = '', public ?string $updated_on = null,
public string $content = '', public int $is_read = 0, public int $is_bookmarked = 0) { }
/**
* Has the item been read?
@@ -73,16 +62,14 @@ class Item
*/
public static function fromFeedItem(int $feedId, ParsedItem $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;
return new static(
feed_id: $feedId,
title: $item->title,
item_guid: $item->guid,
item_link: $item->link,
published_on: $item->publishedOn,
updated_on: $item->updatedOn,
content: $item->content);
}
/**

View File

@@ -48,7 +48,7 @@ class ItemList {
{
$result = $query->execute();
if (!$result) {
$this->error = Data::error($db)['error'];
$this->error = 'SQLite error: ' . $db->lastErrorMsg();
} else {
$this->items = $result;
}

View File

@@ -4,13 +4,12 @@ namespace FeedReaderCentral;
use BitBadger\Documents\DocumentException;
use BitBadger\Documents\Field;
use BitBadger\Documents\SQLite\Patch;
use SQLite3;
/**
* Security functions
*/
class Security {
class Security
{
/** @var int Run as a single user requiring no password */
public const int SINGLE_USER = 0;
@@ -35,17 +34,16 @@ class Security {
* @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
* @throws DocumentException if any is encountered
*/
private static function verifyPassword(User $user, string $password, ?string $returnTo, SQLite3 $db): void
private static function verifyPassword(User $user, string $password, ?string $returnTo): 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);
Patch::byId(Table::USER, $user->id, ['password' => password_hash($password, self::PW_ALGORITHM)]);
}
$_SESSION[Key::USER_ID] = $user['id'];
$_SESSION[Key::USER_EMAIL] = $user['email'];
$_SESSION[Key::USER_ID] = $user->id;
$_SESSION[Key::USER_EMAIL] = $user->email;
frc_redirect($returnTo ?? '/');
}
}
@@ -56,10 +54,9 @@ class Security {
* @param string $email The e-mail address for the user (cannot be the single-user mode user)
* @param string $password The password provided by the user
* @param string|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
* @throws DocumentException If any is encountered
*/
public static function logOnUser(string $email, string $password, ?string $returnTo, SQLite3 $db): void {
public static function logOnUser(string $email, string $password, ?string $returnTo): void {
if (SECURITY_MODEL == self::SINGLE_USER_WITH_PASSWORD) {
$dbEmail = self::SINGLE_USER_EMAIL;
} else {
@@ -70,7 +67,7 @@ class Security {
$dbEmail = $email;
}
$user = User::findByEmail($dbEmail);
if ($user) self::verifyPassword($user, $password, $returnTo, $db);
if ($user) self::verifyPassword($user, $password, $returnTo);
add_error('Invalid credentials; log on unsuccessful');
}
@@ -79,40 +76,37 @@ class Security {
*
* @param string $email The e-mail address of the user whose password should be updated
* @param string $password The new password for this user
* @param SQLite3 $db The database connection to use in updating the password
* @throws DocumentException If any is encountered
*/
public static function updatePassword(string $email, string $password, SQLite3 $db): void {
public static function updatePassword(string $email, string $password): void {
Patch::byFields(Table::USER, [Field::EQ('email', $email)],
['password' => password_hash($password, self::PW_ALGORITHM)], $db);
['password' => password_hash($password, self::PW_ALGORITHM)]);
}
/**
* Log on the single user
*
* @param SQLite3 $db The data connection to use to retrieve the user
* @throws DocumentException If any is encountered
*/
private static function logOnSingleUser(SQLite3 $db): void {
private static function logOnSingleUser(): void {
$user = User::findByEmail(self::SINGLE_USER_EMAIL);
if (!$user) {
User::add(self::SINGLE_USER_EMAIL, self::SINGLE_USER_PASSWORD, $db);
User::add(self::SINGLE_USER_EMAIL, self::SINGLE_USER_PASSWORD);
$user = User::findByEmail(self::SINGLE_USER_EMAIL);
}
self::verifyPassword($user, self::SINGLE_USER_PASSWORD, $_GET['returnTo'], $db);
self::verifyPassword($user, self::SINGLE_USER_PASSWORD, $_GET['returnTo']);
}
/**
* Verify that user is logged on
*
* @param SQLite3 $db The data connection to use if required
* @param bool $redirectIfAnonymous Whether to redirect the request if there is no user logged on
* @throws DocumentException If any is encountered
*/
public static function verifyUser(SQLite3 $db, bool $redirectIfAnonymous = true): void {
public static function verifyUser(bool $redirectIfAnonymous = true): void {
if (key_exists(Key::USER_ID, $_SESSION)) return;
if (SECURITY_MODEL == self::SINGLE_USER) self::logOnSingleUser($db);
if (SECURITY_MODEL == self::SINGLE_USER) self::logOnSingleUser();
if (SECURITY_MODEL != self::SINGLE_USER_WITH_PASSWORD && SECURITY_MODEL != self::MULTI_USER) {
die('Unrecognized security model (' . SECURITY_MODEL . ')');

View File

@@ -3,8 +3,12 @@ namespace FeedReaderCentral;
use BitBadger\Documents\DocumentException;
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\Parameters;
use BitBadger\Documents\SQLite\Results;
use SQLite3;
/**
@@ -12,14 +16,14 @@ use SQLite3;
*/
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 = '';
/**
* Constructor
*
* @param int $id The ID of the user
* @param string $email The e-mail address for the user
* @param string $password The password for the user
*/
public function __construct(public int $id = 0, public string $email = '', public string $password = '') { }
/**
* Find a user by their e=mail address
@@ -38,15 +42,34 @@ class User
*
* @param string $email The e-mail address for the user
* @param string $password The user's password
* @param SQLite3 $db The data connection to use to add the user
* @throws DocumentException If any is encountered
*/
public static function add(string $email, string $password, SQLite3 $db): void
public static function add(string $email, string $password): void
{
$user = new User();
$user->email = $email;
$user->password = $password;
Document::insert(Table::USER, $user, $db);
Document::insert(Table::USER, new User(email: $email, password: $password));
}
/**
* Does this user have any bookmarked items?
* @return bool True if the user has any bookmarked items, false if not
* @throws DocumentException If any is encountered
*/
public static function hasBookmarks(): bool
{
$userField = Field::EQ('user_id', $_SESSION[Key::USER_ID], '@user');
$userField->qualifier = Table::FEED;
$bookField = Field::EQ('is_bookmarked', 1, '@book');
$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(...));
}
}