Documents and Documentation (beta 1) #23

Merged
danieljsummers merged 16 commits from doc-experiment into main 2024-06-12 02:07:36 +00:00
26 changed files with 306 additions and 262 deletions
Showing only changes of commit b88ad1f268 - Show all commits

View File

@ -5,7 +5,7 @@
use BitBadger\Documents\SQLite\Configuration; use BitBadger\Documents\SQLite\Configuration;
use FeedReaderCentral\Data; use FeedReaderCentral\Data;
const FRC_VERSION = '1.0.0-alpha7'; const FRC_VERSION = '1.0.0-beta1';
/** /**
* Drop .0 or .0.0 from the end of the version to format it for display * Drop .0 or .0.0 from the end of the version to format it for display
@ -24,15 +24,6 @@ function display_version(): string {
} }
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/vendor/autoload.php';
//spl_autoload_register(function ($class) {
// $file = implode(DIRECTORY_SEPARATOR, [__DIR__, 'lib', "$class.php"]);
// if (file_exists($file)) {
// require $file;
// return true;
// }
// return false;
//});
require 'user-config.php'; require 'user-config.php';
Configuration::useDbFileName(implode(DIRECTORY_SEPARATOR, [__DIR__, 'data', DATABASE_NAME])); Configuration::useDbFileName(implode(DIRECTORY_SEPARATOR, [__DIR__, 'data', DATABASE_NAME]));

20
src/composer.lock generated
View File

@ -12,10 +12,16 @@
"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": "30d3ad0621485d0797f2483424a6199ab0021c97" "reference": "4aecbfe3e8030fe7ddc0391ee715d6766cbb9c6e"
}, },
"type": "library", "type": "library",
"time": "2024-06-01T02:26:15+00:00" "autoload": {
"psr-4": {
"BitBadger\\Documents\\": "./",
"BitBadger\\Documents\\Query\\": "./Query"
}
},
"time": "2024-06-02T02:11:21+00:00"
}, },
{ {
"name": "bit-badger/documents-sqlite", "name": "bit-badger/documents-sqlite",
@ -23,14 +29,20 @@
"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": "009ea77b7510fd13936ec4927e2390bccd5d5e70" "reference": "ac34dbf481287526b6d044fd7699568e0ee92805"
}, },
"require": { "require": {
"bit-badger/documents-common": "dev-conversion", "bit-badger/documents-common": "dev-conversion",
"ext-sqlite3": "*" "ext-sqlite3": "*"
}, },
"type": "library", "type": "library",
"time": "2024-06-01T02:28:46+00:00" "autoload": {
"psr-4": {
"BitBadger\\Documents\\SQLite\\": "./",
"BitBadger\\Documents\\SQLite\\Query\\": "./Query"
}
},
"time": "2024-06-02T02:18:12+00:00"
} }
], ],
"packages-dev": [], "packages-dev": [],

View File

@ -14,18 +14,8 @@ use SQLite3;
/** /**
* A centralized place for data access for the application * A centralized place for data access for the application
*/ */
class Data { 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;
}
/** /**
* Create the search index and synchronization triggers for the item table * 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')); $tables = Custom::array("SELECT name FROM sqlite_master WHERE type = 'table'", [], new StringMapper('name'));
$db = Configuration::dbConn(); $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)) { if (!in_array(Table::USER, $tables)) {
Definition::ensureTable(Table::USER, $db); Definition::ensureTable(Table::USER, $db);
Definition::ensureFieldIndex(Table::USER, 'email', ['email'], $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)) { if (!in_array(Table::FEED, $tables)) {
Definition::ensureTable(Table::FEED, $db); Definition::ensureTable(Table::FEED, $db);
Definition::ensureFieldIndex(Table::FEED, 'user', ['user_id'], $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)) { if (!in_array(Table::ITEM, $tables)) {
Definition::ensureTable(Table::ITEM, $db); Definition::ensureTable(Table::ITEM, $db);
Definition::ensureFieldIndex(Table::ITEM, 'feed', ['feed_id', 'item_link'], $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); self::createSearchIndex($db);
} }
$db->close(); $db->close();
@ -127,21 +83,12 @@ class Data {
* @param ?string $value The date/time to be parsed and formatted * @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 * @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 { try {
return $value ? (new DateTimeImmutable($value))->format(DateTimeInterface::ATOM) : null; return $value ? (new DateTimeImmutable($value))->format(DateTimeInterface::ATOM) : null;
} catch (Exception) { } catch (Exception) {
return null; 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 */ /** @var int Purge items in number greater than the specified number of items to keep */
public const int PURGE_BY_COUNT = 3; public const int PURGE_BY_COUNT = 3;
// ***** PROPERTIES ***** /**
* Constructor
/** @var int The ID of the feed */ *
public int $id = 0; * @param int $id The ID of the feed
* @param int $user_id The ID of the user to whom this subscription belongs
/** @var int The ID of the user to whom this subscription belongs */ * @param string $url The URL of the feed
public int $user_id = 0; * @param string|null $title The title of this feed
* @param string|null $updated_on The date/time items in this feed were last updated
/** @var string The URL of the feed */ * @param string|null $checked_on The date/time this feed was last checked
public string $url = ''; */
public function __construct(public int $id = 0, public int $user_id = 0, public string $url = '',
/** @var string|null The title of this feed */ public ?string $title = null, public ?string $updated_on = null,
public ?string $title = null; public ?string $checked_on = 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;
// ***** STATIC FUNCTIONS ***** // ***** STATIC FUNCTIONS *****

View File

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

View File

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

View File

@ -4,13 +4,12 @@ namespace FeedReaderCentral;
use BitBadger\Documents\DocumentException; use BitBadger\Documents\DocumentException;
use BitBadger\Documents\Field; use BitBadger\Documents\Field;
use BitBadger\Documents\SQLite\Patch; use BitBadger\Documents\SQLite\Patch;
use SQLite3;
/** /**
* Security functions * Security functions
*/ */
class Security { class Security
{
/** @var int Run as a single user requiring no password */ /** @var int Run as a single user requiring no password */
public const int SINGLE_USER = 0; public const int SINGLE_USER = 0;
@ -35,17 +34,16 @@ class Security {
* @param User $user The user information retrieved from the database * @param User $user The user information retrieved from the database
* @param string $password The password provided by the user * @param string $password The password provided by the user
* @param string|null $returnTo The URL to which the user should be redirected * @param 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 * @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_verify($password, $user->password)) {
if (password_needs_rehash($user->password, self::PW_ALGORITHM)) { 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_ID] = $user->id;
$_SESSION[Key::USER_EMAIL] = $user['email']; $_SESSION[Key::USER_EMAIL] = $user->email;
frc_redirect($returnTo ?? '/'); 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 $email The e-mail address for the user (cannot be the single-user mode user)
* @param string $password The password provided by the user * @param string $password The password provided by the user
* @param string|null $returnTo The URL to which the user should be redirected * @param 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 * @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) { if (SECURITY_MODEL == self::SINGLE_USER_WITH_PASSWORD) {
$dbEmail = self::SINGLE_USER_EMAIL; $dbEmail = self::SINGLE_USER_EMAIL;
} else { } else {
@ -70,7 +67,7 @@ class Security {
$dbEmail = $email; $dbEmail = $email;
} }
$user = User::findByEmail($dbEmail); $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'); 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 $email The e-mail address of the user whose password should be updated
* @param string $password The new password for this user * @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 * @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)], 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 * Log on the single user
* *
* @param SQLite3 $db The data connection to use to retrieve the user
* @throws DocumentException If any is encountered * @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); $user = User::findByEmail(self::SINGLE_USER_EMAIL);
if (!$user) { 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); $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 * 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 * @param bool $redirectIfAnonymous Whether to redirect the request if there is no user logged on
* @throws DocumentException If any is encountered * @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 (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) { if (SECURITY_MODEL != self::SINGLE_USER_WITH_PASSWORD && SECURITY_MODEL != self::MULTI_USER) {
die('Unrecognized security model (' . SECURITY_MODEL . ')'); die('Unrecognized security model (' . SECURITY_MODEL . ')');

View File

@ -3,8 +3,12 @@ namespace FeedReaderCentral;
use BitBadger\Documents\DocumentException; use BitBadger\Documents\DocumentException;
use BitBadger\Documents\Field; use BitBadger\Documents\Field;
use BitBadger\Documents\Query;
use BitBadger\Documents\SQLite\Custom;
use BitBadger\Documents\SQLite\Document; use BitBadger\Documents\SQLite\Document;
use BitBadger\Documents\SQLite\Find; use BitBadger\Documents\SQLite\Find;
use BitBadger\Documents\SQLite\Parameters;
use BitBadger\Documents\SQLite\Results;
use SQLite3; use SQLite3;
/** /**
@ -12,14 +16,14 @@ use SQLite3;
*/ */
class User class User
{ {
/** @var int The ID of the user */ /**
public int $id = 0; * Constructor
*
/** @var string The e-mail address for the user */ * @param int $id The ID of the user
public string $email = ''; * @param string $email The e-mail address for the user
* @param string $password The password for the user
/** @var string The password for the user */ */
public string $password = ''; public function __construct(public int $id = 0, public string $email = '', public string $password = '') { }
/** /**
* Find a user by their e=mail address * 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 $email The e-mail address for the user
* @param string $password The user's password * @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 * @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(); Document::insert(Table::USER, new User(email: $email, password: $password));
$user->email = $email;
$user->password = $password;
Document::insert(Table::USER, $user, $db);
} }
/**
* 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(...));
}
} }

View File

@ -7,9 +7,9 @@
*/ */
use BitBadger\Documents\DocumentException; use BitBadger\Documents\DocumentException;
use BitBadger\Documents\SQLite\Configuration;
use BitBadger\Documents\SQLite\Find; use BitBadger\Documents\SQLite\Find;
use BitBadger\Documents\SQLite\Patch; use BitBadger\Documents\SQLite\Patch;
use FeedReaderCentral\Data;
use FeedReaderCentral\Item; use FeedReaderCentral\Item;
use FeedReaderCentral\Key; use FeedReaderCentral\Key;
use FeedReaderCentral\Security; use FeedReaderCentral\Security;
@ -17,12 +17,12 @@ use FeedReaderCentral\Table;
include '../start.php'; include '../start.php';
$db = Data::getConnection(); Security::verifyUser();
Security::verifyUser($db);
$id = $_GET['id']; $id = $_GET['id'];
// TODO: adapt query once "by fields" is available // TODO: adapt query once "by fields" is available
$db = Configuration::dbConn();
$existsQuery = $db->prepare( $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'); '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(':id', $id);

View File

@ -1,12 +1,8 @@
<?php <?php
use FeedReaderCentral\Data;
use FeedReaderCentral\Security;
include '../../start.php'; include '../../start.php';
$db = Data::getConnection(); FeedReaderCentral\Security::verifyUser(redirectIfAnonymous: false);
Security::verifyUser($db, redirectIfAnonymous: false);
page_head('Feeds | Documentation'); ?> page_head('Feeds | Documentation'); ?>
<h1>Feeds</h1> <h1>Feeds</h1>
@ -75,4 +71,3 @@ php-cli util/refresh.php all</pre>
add <code>nice -n 1</code> (with a trailing space) before the path to the script. add <code>nice -n 1</code> (with a trailing space) before the path to the script.
</article><?php </article><?php
page_foot(); page_foot();
$db->close();

View File

@ -1,12 +1,8 @@
<?php <?php
use FeedReaderCentral\Data;
use FeedReaderCentral\Security;
include '../../start.php'; include '../../start.php';
$db = Data::getConnection(); FeedReaderCentral\Security::verifyUser(redirectIfAnonymous: false);
Security::verifyUser($db, redirectIfAnonymous: false);
page_head('Documentation'); ?> page_head('Documentation'); ?>
<h1>Documentation Home</h1> <h1>Documentation Home</h1>
@ -35,4 +31,3 @@ page_head('Documentation'); ?>
that can be performed via its command line interface. that can be performed via its command line interface.
</article><?php </article><?php
page_foot(); page_foot();
$db->close();

View File

@ -1,12 +1,8 @@
<?php <?php
use FeedReaderCentral\Data;
use FeedReaderCentral\Security;
include '../../start.php'; include '../../start.php';
$db = Data::getConnection(); FeedReaderCentral\Security::verifyUser(redirectIfAnonymous: false);
Security::verifyUser($db, redirectIfAnonymous: false);
page_head('Items | Documentation'); ?> page_head('Items | Documentation'); ?>
<h1>Items</h1> <h1>Items</h1>
@ -70,4 +66,3 @@ page_head('Items | Documentation'); ?>
others; if the items seem to move around in the list after a refresh, this is likely the cause. others; if the items seem to move around in the list after a refresh, this is likely the cause.
</article><?php </article><?php
page_foot(); page_foot();
$db->close();

View File

@ -1,12 +1,8 @@
<?php <?php
use FeedReaderCentral\Data;
use FeedReaderCentral\Security;
include '../../start.php'; include '../../start.php';
$db = Data::getConnection(); FeedReaderCentral\Security::verifyUser(redirectIfAnonymous: false);
Security::verifyUser($db, redirectIfAnonymous: false);
page_head('Security Modes | Documentation'); ?> page_head('Security Modes | Documentation'); ?>
<h1>Configuring Security Modes</h1> <h1>Configuring Security Modes</h1>
@ -63,4 +59,3 @@ page_head('Security Modes | Documentation'); ?>
<p><code>php-cli util/user.php reset-single-password</code> <p><code>php-cli util/user.php reset-single-password</code>
</article><?php </article><?php
page_foot(); page_foot();
$db->close();

View File

@ -1,12 +1,8 @@
<?php <?php
use FeedReaderCentral\Data;
use FeedReaderCentral\Security;
include '../../start.php'; include '../../start.php';
$db = Data::getConnection(); FeedReaderCentral\Security::verifyUser(redirectIfAnonymous: false);
Security::verifyUser($db, redirectIfAnonymous: false);
page_head('About the CLI | Documentation'); ?> page_head('About the CLI | Documentation'); ?>
<h1>About the CLI</h1> <h1>About the CLI</h1>
@ -25,4 +21,3 @@ page_head('About the CLI | Documentation'); ?>
<p><code>php-cli util/some-process.php command option1 option2</code> <p><code>php-cli util/some-process.php command option1 option2</code>
</article><?php </article><?php
page_foot(); page_foot();
$db->close();

View File

@ -7,24 +7,23 @@
use BitBadger\Documents\DocumentException; use BitBadger\Documents\DocumentException;
use BitBadger\Documents\Field; use BitBadger\Documents\Field;
use BitBadger\Documents\SQLite\Configuration;
use BitBadger\Documents\SQLite\Delete; use BitBadger\Documents\SQLite\Delete;
use FeedReaderCentral\Data;
use FeedReaderCentral\Feed; use FeedReaderCentral\Feed;
use FeedReaderCentral\Security; use FeedReaderCentral\Security;
use FeedReaderCentral\Table; use FeedReaderCentral\Table;
include '../../start.php'; include '../../start.php';
$db = Data::getConnection(); Security::verifyUser();
Security::verifyUser($db);
$feedId = $_GET['id'] ?? ''; $feedId = $_GET['id'] ?? '';
if ($_SERVER['REQUEST_METHOD'] == 'DELETE') { if ($_SERVER['REQUEST_METHOD'] == 'DELETE') {
try { try {
if (!($feed = Feed::retrieveById($feedId))) not_found(); if (!($feed = Feed::retrieveById($feedId))) not_found();
Delete::byFields(Table::ITEM, [Field::EQ('feed_id', $feed->id)], $db); Delete::byFields(Table::ITEM, [Field::EQ('feed_id', $feed->id)]);
Delete::byId(Table::FEED, $feed->id, $db); Delete::byId(Table::FEED, $feed->id);
add_info('Feed &ldquo;' . htmlentities($feed->title) . '&rdquo; deleted successfully'); add_info('Feed &ldquo;' . htmlentities($feed->title) . '&rdquo; deleted successfully');
$db->close(); $db->close();
frc_redirect('/feeds'); frc_redirect('/feeds');
@ -33,6 +32,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'DELETE') {
} }
} }
$db = Configuration::dbConn();
if ($_SERVER['REQUEST_METHOD'] == 'POST') { if ($_SERVER['REQUEST_METHOD'] == 'POST') {
try { try {
$isNew = $_POST['id'] == 'new'; $isNew = $_POST['id'] == 'new';

View File

@ -5,18 +5,18 @@
* Lists items in a given feed (all, unread, or bookmarked) * Lists items in a given feed (all, unread, or bookmarked)
*/ */
use FeedReaderCentral\Data; use BitBadger\Documents\SQLite\Configuration;
use FeedReaderCentral\Feed; use FeedReaderCentral\Feed;
use FeedReaderCentral\ItemList; use FeedReaderCentral\ItemList;
use FeedReaderCentral\Security; use FeedReaderCentral\Security;
include '../../start.php'; include '../../start.php';
$db = Data::getConnection(); Security::verifyUser();
Security::verifyUser($db);
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, $db),
key_exists('bookmarked', $_GET) => ItemList::bookmarkedForFeed($feed->id, $db), key_exists('bookmarked', $_GET) => ItemList::bookmarkedForFeed($feed->id, $db),

View File

@ -10,7 +10,6 @@ use BitBadger\Documents\Field;
use BitBadger\Documents\JsonMapper; use BitBadger\Documents\JsonMapper;
use BitBadger\Documents\Query; use BitBadger\Documents\Query;
use BitBadger\Documents\SQLite\Custom; use BitBadger\Documents\SQLite\Custom;
use FeedReaderCentral\Data;
use FeedReaderCentral\Feed; use FeedReaderCentral\Feed;
use FeedReaderCentral\Key; use FeedReaderCentral\Key;
use FeedReaderCentral\Security; use FeedReaderCentral\Security;
@ -18,8 +17,7 @@ use FeedReaderCentral\Table;
include '../start.php'; include '../start.php';
$db = Data::getConnection(); Security::verifyUser();
Security::verifyUser($db);
// 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');
@ -52,4 +50,3 @@ page_head('Your Feeds'); ?>
}); ?> }); ?>
</article><?php </article><?php
page_foot(); page_foot();
$db->close();

View File

@ -5,16 +5,16 @@
* Displays a list of unread or bookmarked items for the current user * Displays a list of unread or bookmarked items for the current user
*/ */
use FeedReaderCentral\Data; use BitBadger\Documents\SQLite\Configuration;
use FeedReaderCentral\Feed; use FeedReaderCentral\Feed;
use FeedReaderCentral\ItemList; use FeedReaderCentral\ItemList;
use FeedReaderCentral\Security; use FeedReaderCentral\Security;
include '../start.php'; include '../start.php';
$db = Data::getConnection(); Security::verifyUser();
Security::verifyUser($db);
$db = Configuration::dbConn();
if (key_exists('refresh', $_GET)) { if (key_exists('refresh', $_GET)) {
$refreshResult = Feed::refreshAll($db); $refreshResult = Feed::refreshAll($db);
if (key_exists('ok', $refreshResult)) { if (key_exists('ok', $refreshResult)) {

View File

@ -7,9 +7,9 @@
*/ */
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\Data;
use FeedReaderCentral\Item; use FeedReaderCentral\Item;
use FeedReaderCentral\Key; use FeedReaderCentral\Key;
use FeedReaderCentral\Security; use FeedReaderCentral\Security;
@ -17,16 +17,14 @@ use FeedReaderCentral\Table;
include '../start.php'; include '../start.php';
$db = Data::getConnection(); Security::verifyUser();
Security::verifyUser($db);
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 (Item::retrieveByIdForUser($_POST['id'])) {
Patch::byId(Table::ITEM, $_POST['id'], ['is_read' => 0], $db); Patch::byId(Table::ITEM, $_POST['id'], ['is_read' => 0]);
} }
$db->close();
frc_redirect($_POST['from']); frc_redirect($_POST['from']);
} catch (DocumentException $ex) { } catch (DocumentException $ex) {
add_error("$ex"); add_error("$ex");
@ -38,16 +36,16 @@ $from = $_GET['from'] ?? '/';
if ($_SERVER['REQUEST_METHOD'] == 'DELETE') { if ($_SERVER['REQUEST_METHOD'] == 'DELETE') {
try { try {
if (Item::retrieveByIdForUser($_GET['id'])) { if (Item::retrieveByIdForUser($_GET['id'])) {
Delete::byId(Table::ITEM, $_GET['id'], $db); Delete::byId(Table::ITEM, $_GET['id']);
} }
} catch (DocumentException $ex) { } catch (DocumentException $ex) {
add_error("$ex"); add_error("$ex");
} }
$db->close();
frc_redirect($from); frc_redirect($from);
} }
// TODO: convert this query // TODO: convert this query
$db = Configuration::dbConn();
$query = $db->prepare(<<<'SQL' $query = $db->prepare(<<<'SQL'
SELECT item.title AS item_title, item.item_link, item.published_on, item.updated_on, item.content, SELECT item.title AS item_title, item.item_link, item.published_on, item.updated_on, item.content,
feed.title AS feed_title feed.title AS feed_title

View File

@ -6,18 +6,18 @@
* Search for items across all feeds * Search for items across all feeds
*/ */
use FeedReaderCentral\Data; use BitBadger\Documents\SQLite\Configuration;
use FeedReaderCentral\ItemList; use FeedReaderCentral\ItemList;
use FeedReaderCentral\Security; use FeedReaderCentral\Security;
include '../start.php'; include '../start.php';
$db = Data::getConnection(); Security::verifyUser();
Security::verifyUser($db);
$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 = ItemList::matchingSearch($search, $items == 'bookmarked', $db);
} }

View File

@ -1,18 +1,16 @@
<?php <?php
include '../../start.php'; include '../../start.php';
use FeedReaderCentral\Data;
use FeedReaderCentral\Key; use FeedReaderCentral\Key;
use FeedReaderCentral\Security; use FeedReaderCentral\Security;
$db = Data::getConnection(); Security::verifyUser(redirectIfAnonymous: false);
Security::verifyUser($db, redirectIfAnonymous: false);
// Users already logged on have no need of this page // Users already logged on have no need of this page
if (key_exists(Key::USER_ID, $_SESSION)) frc_redirect('/'); if (key_exists(Key::USER_ID, $_SESSION)) frc_redirect('/');
if ($_SERVER['REQUEST_METHOD'] == 'POST') { if ($_SERVER['REQUEST_METHOD'] == 'POST') {
Security::logOnUser($_POST['email'] ?? '', $_POST['password'], $_POST['returnTo'] ?? null, $db); Security::logOnUser($_POST['email'] ?? '', $_POST['password'], $_POST['returnTo'] ?? null);
// If we're still here, something didn't work; preserve the returnTo parameter // If we're still here, something didn't work; preserve the returnTo parameter
$_GET['returnTo'] = $_POST['returnTo']; $_GET['returnTo'] = $_POST['returnTo'];
} }
@ -41,4 +39,3 @@ page_head('Log On'); ?>
</form> </form>
</article><?php </article><?php
page_foot(); page_foot();
$db->close();

View File

@ -1,8 +1,15 @@
<?php <?php
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\Data;
use FeedReaderCentral\Key; use FeedReaderCentral\Key;
use FeedReaderCentral\Security; use FeedReaderCentral\Security;
use FeedReaderCentral\Table;
use FeedReaderCentral\User;
require 'app-config.php'; require 'app-config.php';
@ -69,28 +76,14 @@ function title_bar(): void
<div><a href=/ class=title>Feed Reader Central</a><span class=version><?=$version?></span></div> <div><a href=/ class=title>Feed Reader Central</a><span class=version><?=$version?></span></div>
<nav><?php <nav><?php
if (key_exists(Key::USER_ID, $_SESSION)) { if (key_exists(Key::USER_ID, $_SESSION)) {
$db = Data::getConnection();
try {
$bookQuery = $db->prepare(<<<'SQL'
SELECT EXISTS(
SELECT 1
FROM item INNER JOIN feed ON item.feed_id = feed.id
WHERE feed.user_id = :id AND item.is_bookmarked = 1)
SQL);
$bookQuery->bindValue(':id', $_SESSION[Key::USER_ID]);
$bookResult = $bookQuery->execute();
$hasBookmarks = $bookResult && $bookResult->fetchArray(SQLITE3_NUM)[0];
nav_link(hx_get('/feeds', 'Feeds'), true); nav_link(hx_get('/feeds', 'Feeds'), true);
if ($hasBookmarks) nav_link(hx_get('/?bookmarked', 'Bookmarked')); if (User::hasBookmarks()) nav_link(hx_get('/?bookmarked', 'Bookmarked'));
nav_link(hx_get('/search', 'Search')); nav_link(hx_get('/search', 'Search'));
nav_link(hx_get('/docs/', 'Docs')); nav_link(hx_get('/docs/', 'Docs'));
nav_link('<a href=/user/log-off>Log Off</a>'); nav_link('<a href=/user/log-off>Log Off</a>');
if ($_SESSION[Key::USER_EMAIL] != Security::SINGLE_USER_EMAIL) { if ($_SESSION[Key::USER_EMAIL] != Security::SINGLE_USER_EMAIL) {
nav_link($_SESSION[Key::USER_EMAIL]); nav_link($_SESSION[Key::USER_EMAIL]);
} }
} finally {
$db->close();
}
} else { } else {
nav_link(hx_get('/user/log-on', 'Log On'), true); nav_link(hx_get('/user/log-on', 'Log On'), true);
nav_link(hx_get('/docs/', 'Docs')); nav_link(hx_get('/docs/', 'Docs'));

120
src/util/db-update.php Normal file
View File

@ -0,0 +1,120 @@
<?php
use BitBadger\Documents\ArrayMapper;
use BitBadger\Documents\DocumentException;
use BitBadger\Documents\SQLite\Configuration;
use BitBadger\Documents\SQLite\Custom;
use BitBadger\Documents\SQLite\Document;
use FeedReaderCentral\Data;
use FeedReaderCentral\Feed;
use FeedReaderCentral\Item;
use FeedReaderCentral\Table;
use FeedReaderCentral\User;
require __DIR__ . '/../cli-start.php';
cli_title('DATABASE UPDATE');
if ($argc < 2) display_help();
switch ($argv[1]) {
case 'check':
check_status();
break;
case 'run':
run_update();
break;
default:
printfn('Unrecognized option "%s"', $argv[1]);
display_help();
}
/**
* Display the options for this utility and exit
*/
function display_help(): never
{
printfn('Options:');
printfn(' - check');
printfn(' Check to see if the configured database has been updated');
printfn(' - run');
printfn(' Run the beta1 database storage update');
exit(0);
}
function json_column_exists(): bool
{
try {
$table = Custom::single("SELECT sql FROM sqlite_master WHERE tbl_name='frc_user'", [], new ArrayMapper());
return $table && substr_compare(strtolower($table['sql']), 'data text not null', 0) >= 0;
} catch (DocumentException $ex) {
printfn("ERR $ex");
return false;
}
}
function check_status(): void
{
if (json_column_exists()) {
printfn('The database has already been updated');
} else {
printfn('The database has yet to be updated');
}
}
function run_update(): void
{
try {
$db = Configuration::dbConn();
} catch (DocumentException $ex) {
printfn("ERR: Cannot obtain a connection to the database\n $ex");
return;
}
try {
printfn('Removing search index...');
Custom::nonQuery('DROP TRIGGER item_ai', [], $db);
Custom::nonQuery('DROP TRIGGER item_au', [], $db);
Custom::nonQuery('DROP TRIGGER item_ad', [], $db);
Custom::nonQuery('DROP TABLE item_search', [], $db);
printfn('Moving old tables...');
Custom::nonQuery('ALTER TABLE item RENAME TO old_item', [], $db);
Custom::nonQuery('ALTER TABLE feed RENAME TO old_feed', [], $db);
Custom::nonQuery('ALTER TABLE frc_user RENAME TO old_user', [], $db);
printfn('Creating new tables...');
Data::ensureDb();
printfn('Migrating users...');
$users = $db->query('SELECT * FROM old_user');
if (!$users) throw new DocumentException('Could not retrieve users');
while ($user = $users->fetchArray(SQLITE3_ASSOC)) {
Document::insert(Table::USER, new User($user['id'], $user['email'], $user['password']), $db);
}
printfn('Migrating feeds...');
$feeds = $db->query('SELECT * FROM old_feed');
if (!$feeds) throw new DocumentException('Could not retrieve feeds');
while ($feed = $feeds->fetchArray(SQLITE3_ASSOC)) {
Document::insert(Table::FEED,
new Feed($feed['id'], $feed['user_id'], $feed['url'], $feed['title'], $feed['updated_on'],
$feed['checked_on']), $db);
}
printfn('Migrating items...');
$items = $db->query('SELECT * FROM old_item');
if (!$items) throw new DocumentException('Could not retrieve items');
while ($item = $items->fetchArray(SQLITE3_ASSOC)) {
Document::insert(Table::ITEM,
new Item($item['id'], $item['feed_id'], $item['title'], $item['item_guid'], $item['item_link'],
$item['published_on'], $item['updated_on'], $item['content'], $item['is_read'],
$item['is_bookmarked']), $db);
}
printfn('Dropping old tables...');
Custom::nonQuery('DROP TABLE old_item', [], $db);
Custom::nonQuery('DROP TABLE old_feed', [], $db);
Custom::nonQuery('DROP TABLE old_user', [], $db);
printfn(PHP_EOL. 'Migration complete!');
} catch (DocumentException $ex) {
printfn("ERR $ex");
} finally {
$db->close();
}
}

View File

@ -1,6 +1,7 @@
<?php <?php
use BitBadger\Documents\DocumentException; use BitBadger\Documents\DocumentException;
use BitBadger\Documents\SQLite\Configuration;
use BitBadger\Documents\SQLite\Find; use BitBadger\Documents\SQLite\Find;
use FeedReaderCentral\Data; use FeedReaderCentral\Data;
use FeedReaderCentral\Feed; use FeedReaderCentral\Feed;
@ -36,7 +37,12 @@ function display_help(): never
function refresh_all(): void function refresh_all(): void
{ {
$db = Data::getConnection(); try {
$db = Configuration::dbConn();
} catch (DocumentException $ex) {
printfn("ERR: Cannot obtain a connection to the database\n $ex");
return;
}
try { try {
$users = []; $users = [];

View File

@ -1,6 +1,7 @@
<?php <?php
use BitBadger\Documents\DocumentException; use BitBadger\Documents\DocumentException;
use BitBadger\Documents\SQLite\Configuration;
use BitBadger\Documents\SQLite\Custom; use BitBadger\Documents\SQLite\Custom;
use BitBadger\Documents\SQLite\Results; use BitBadger\Documents\SQLite\Results;
use FeedReaderCentral\Data; use FeedReaderCentral\Data;
@ -37,7 +38,12 @@ function display_help(): never
*/ */
function rebuild_index(): void function rebuild_index(): void
{ {
$db = Data::getConnection(); try {
$db = Configuration::dbConn();
} catch (DocumentException $ex) {
printfn("ERR: Cannot obtain a connection to the database\n $ex");
return;
}
try { try {
$hasIndex = Custom::scalar("SELECT COUNT(*) FROM sqlite_master WHERE name = 'item_ai'", [], $hasIndex = Custom::scalar("SELECT COUNT(*) FROM sqlite_master WHERE name = 'item_ai'", [],

View File

@ -2,6 +2,7 @@
use BitBadger\Documents\DocumentException; use BitBadger\Documents\DocumentException;
use BitBadger\Documents\Field; use BitBadger\Documents\Field;
use BitBadger\Documents\SQLite\Configuration;
use BitBadger\Documents\SQLite\Count; use BitBadger\Documents\SQLite\Count;
use BitBadger\Documents\SQLite\Delete; use BitBadger\Documents\SQLite\Delete;
use BitBadger\Documents\SQLite\Patch; use BitBadger\Documents\SQLite\Patch;
@ -96,8 +97,6 @@ function add_user(): void
{ {
global $argv; global $argv;
$db = Data::getConnection();
try { try {
// Ensure there is not already a user with this e-mail address // Ensure there is not already a user with this e-mail address
$user = User::findByEmail($argv[2]); $user = User::findByEmail($argv[2]);
@ -106,13 +105,11 @@ function add_user(): void
return; return;
} }
User::add($argv[2], $argv[3], $db); User::add($argv[2], $argv[3]);
printfn('User "%s" with password "%s" added successfully', $argv[2], $argv[3]); printfn('User "%s" with password "%s" added successfully', $argv[2], $argv[3]);
} catch (DocumentException $ex) { } catch (DocumentException $ex) {
printfn("$ex"); printfn("$ex");
} finally {
$db->close();
} }
} }
@ -132,7 +129,6 @@ function display_user(string $email): string
*/ */
function set_password(string $email, string $password): void function set_password(string $email, string $password): void
{ {
$db = Data::getConnection();
try { try {
$displayUser = display_user($email); $displayUser = display_user($email);
@ -143,15 +139,13 @@ function set_password(string $email, string $password): void
return; return;
} }
Security::updatePassword($email, $password, $db); Security::updatePassword($email, $password);
$msg = $email == Security::SINGLE_USER_EMAIL && $password == Security::SINGLE_USER_PASSWORD $msg = $email == Security::SINGLE_USER_EMAIL && $password == Security::SINGLE_USER_PASSWORD
? 'reset' : sprintf('set to "%s"', $password); ? 'reset' : sprintf('set to "%s"', $password);
printfn('%s password %s successfully', init_cap($displayUser), $msg); printfn('%s password %s successfully', init_cap($displayUser), $msg);
} catch (DocumentException $ex) { } catch (DocumentException $ex) {
printfn("$ex"); printfn("$ex");
} finally {
$db->close();
} }
} }
@ -162,7 +156,12 @@ function set_password(string $email, string $password): void
*/ */
function delete_user(string $email): void function delete_user(string $email): void
{ {
$db = Data::getConnection(); try {
$db = Configuration::dbConn();
} catch (DocumentException $ex) {
printfn("ERR: Cannot obtain a connection to the database\n $ex");
return;
}
try { try {
$displayUser = display_user($email); $displayUser = display_user($email);
@ -214,7 +213,12 @@ function migrate_single_user(): void
{ {
global $argv; global $argv;
$db = Data::getConnection(); try {
$db = Configuration::dbConn();
} catch (DocumentException $ex) {
printfn("ERR: Cannot obtain a connection to the database\n $ex");
return;
}
try { try {
if (!$single = User::findByEmail(Security::SINGLE_USER_EMAIL)) { if (!$single = User::findByEmail(Security::SINGLE_USER_EMAIL)) {