Documents and Documentation (beta 1) #23
8
src/composer.lock
generated
8
src/composer.lock
generated
|
@ -12,10 +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": "60bf3a7d97f06d49db3cacb9a6a84b129a83daa6"
|
"reference": "30d3ad0621485d0797f2483424a6199ab0021c97"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"time": "2024-05-31T16:06:59+00:00"
|
"time": "2024-06-01T02:26:15+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "bit-badger/documents-sqlite",
|
"name": "bit-badger/documents-sqlite",
|
||||||
|
@ -23,14 +23,14 @@
|
||||||
"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": "9378a62e7ac190ef4bbffdd4330bf83bbe39def0"
|
"reference": "009ea77b7510fd13936ec4927e2390bccd5d5e70"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"bit-badger/documents-common": "dev-conversion",
|
"bit-badger/documents-common": "dev-conversion",
|
||||||
"ext-sqlite3": "*"
|
"ext-sqlite3": "*"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"time": "2024-05-31T16:07:51+00:00"
|
"time": "2024-06-01T02:28:46+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
|
|
|
@ -9,7 +9,6 @@ use BitBadger\Documents\StringMapper;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use Exception;
|
use Exception;
|
||||||
use FeedReaderCentral\Domain\Table;
|
|
||||||
use SQLite3;
|
use SQLite3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
<?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 = '';
|
|
||||||
}
|
|
|
@ -12,8 +12,6 @@ use BitBadger\Documents\SQLite\Exists;
|
||||||
use BitBadger\Documents\SQLite\Find;
|
use BitBadger\Documents\SQLite\Find;
|
||||||
use BitBadger\Documents\SQLite\Patch;
|
use BitBadger\Documents\SQLite\Patch;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use FeedReaderCentral\Domain\Item;
|
|
||||||
use FeedReaderCentral\Domain\Table;
|
|
||||||
use SQLite3;
|
use SQLite3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
namespace FeedReaderCentral\Domain;
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
use FeedReaderCentral\ParsedItem;
|
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An item from a feed
|
* An item from a feed
|
||||||
|
@ -78,4 +84,27 @@ class Item
|
||||||
|
|
||||||
return $it;
|
return $it;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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));
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -5,8 +5,6 @@ namespace FeedReaderCentral;
|
||||||
use BitBadger\Documents\JsonMapper;
|
use BitBadger\Documents\JsonMapper;
|
||||||
use BitBadger\Documents\Mapper;
|
use BitBadger\Documents\Mapper;
|
||||||
use FeedReaderCentral\Domain\Feed;
|
use FeedReaderCentral\Domain\Feed;
|
||||||
use FeedReaderCentral\Domain\Item;
|
|
||||||
use FeedReaderCentral\Domain\Table;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A combined item and feed (used for lists)
|
* A combined item and feed (used for lists)
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
namespace FeedReaderCentral;
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
|
use BitBadger\Documents\DocumentException;
|
||||||
use BitBadger\Documents\Field;
|
use BitBadger\Documents\Field;
|
||||||
use BitBadger\Documents\SQLite\Document;
|
|
||||||
use BitBadger\Documents\SQLite\Find;
|
|
||||||
use BitBadger\Documents\SQLite\Patch;
|
use BitBadger\Documents\SQLite\Patch;
|
||||||
use FeedReaderCentral\Domain\Table;
|
|
||||||
use FeedReaderCentral\Domain\User;
|
|
||||||
use SQLite3;
|
use SQLite3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,39 +29,6 @@ class Security {
|
||||||
/** @var string The password algorithm to use for our passwords */
|
/** @var string The password algorithm to use for our passwords */
|
||||||
public const string PW_ALGORITHM = PASSWORD_DEFAULT;
|
public const string PW_ALGORITHM = PASSWORD_DEFAULT;
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a user by their ID
|
|
||||||
*
|
|
||||||
* @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 User|false The user information, or null if the user is not found
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a 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
|
|
||||||
*/
|
|
||||||
public static function addUser(string $email, string $password, SQLite3 $db): void {
|
|
||||||
$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
|
* Verify a user's password
|
||||||
*
|
*
|
||||||
|
@ -72,15 +36,13 @@ class Security {
|
||||||
* @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
|
* @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, SQLite3 $db): 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)], $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_ID] = $user['id'];
|
||||||
$_SESSION[Key::USER_EMAIL] = $user['email'];
|
$_SESSION[Key::USER_EMAIL] = $user['email'];
|
||||||
|
@ -95,6 +57,7 @@ class Security {
|
||||||
* @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
|
* @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, SQLite3 $db): void {
|
||||||
if (SECURITY_MODEL == self::SINGLE_USER_WITH_PASSWORD) {
|
if (SECURITY_MODEL == self::SINGLE_USER_WITH_PASSWORD) {
|
||||||
|
@ -106,7 +69,7 @@ class Security {
|
||||||
}
|
}
|
||||||
$dbEmail = $email;
|
$dbEmail = $email;
|
||||||
}
|
}
|
||||||
$user = self::findUserByEmail($dbEmail, $db);
|
$user = User::findByEmail($dbEmail);
|
||||||
if ($user) self::verifyPassword($user, $password, $returnTo, $db);
|
if ($user) self::verifyPassword($user, $password, $returnTo, $db);
|
||||||
add_error('Invalid credentials; log on unsuccessful');
|
add_error('Invalid credentials; log on unsuccessful');
|
||||||
}
|
}
|
||||||
|
@ -117,26 +80,24 @@ 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
|
* @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, SQLite3 $db): void {
|
||||||
Patch::byField(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)], $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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log on the single user
|
* Log on the single user
|
||||||
*
|
*
|
||||||
* @param SQLite3 $db The data connection to use to retrieve the 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(SQLite3 $db): void {
|
||||||
$user = self::findUserByEmail(self::SINGLE_USER_EMAIL, $db);
|
$user = User::findByEmail(self::SINGLE_USER_EMAIL);
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
self::addUser(self::SINGLE_USER_EMAIL, self::SINGLE_USER_PASSWORD, $db);
|
User::add(self::SINGLE_USER_EMAIL, self::SINGLE_USER_PASSWORD, $db);
|
||||||
$user = self::findUserByEmail(self::SINGLE_USER_EMAIL, $db);
|
$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'], $db);
|
||||||
}
|
}
|
||||||
|
@ -146,6 +107,7 @@ class Security {
|
||||||
*
|
*
|
||||||
* @param SQLite3 $db The data connection to use if required
|
* @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
|
||||||
*/
|
*/
|
||||||
public static function verifyUser(SQLite3 $db, bool $redirectIfAnonymous = true): void {
|
public static function verifyUser(SQLite3 $db, bool $redirectIfAnonymous = true): void {
|
||||||
if (key_exists(Key::USER_ID, $_SESSION)) return;
|
if (key_exists(Key::USER_ID, $_SESSION)) return;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
namespace FeedReaderCentral\Domain;
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constants to use when accessing tables
|
* Constants to use when accessing tables
|
52
src/lib/User.php
Normal file
52
src/lib/User.php
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
namespace FeedReaderCentral;
|
||||||
|
|
||||||
|
use BitBadger\Documents\DocumentException;
|
||||||
|
use BitBadger\Documents\Field;
|
||||||
|
use BitBadger\Documents\SQLite\Document;
|
||||||
|
use BitBadger\Documents\SQLite\Find;
|
||||||
|
use SQLite3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a user by their e=mail address
|
||||||
|
*
|
||||||
|
* @param string $email The e-mail address of the user to retrieve
|
||||||
|
* @return User|false The user information, or null if the user is not found
|
||||||
|
* @throws DocumentException If any is encountered
|
||||||
|
*/
|
||||||
|
public static function findByEmail(string $email): User|false
|
||||||
|
{
|
||||||
|
return Find::firstByFields(Table::USER, [Field::EQ('email', $email)], User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a 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
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->email = $email;
|
||||||
|
$user->password = $password;
|
||||||
|
Document::insert(Table::USER, $user, $db);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,10 +10,10 @@ use BitBadger\Documents\DocumentException;
|
||||||
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\Data;
|
||||||
use FeedReaderCentral\Domain\Item;
|
use FeedReaderCentral\Item;
|
||||||
use FeedReaderCentral\Domain\Table;
|
|
||||||
use FeedReaderCentral\Key;
|
use FeedReaderCentral\Key;
|
||||||
use FeedReaderCentral\Security;
|
use FeedReaderCentral\Security;
|
||||||
|
use FeedReaderCentral\Table;
|
||||||
|
|
||||||
include '../start.php';
|
include '../start.php';
|
||||||
|
|
||||||
|
@ -33,11 +33,11 @@ $exists = $existsResult ? $existsResult->fetchArray(SQLITE3_ASSOC) : false;
|
||||||
if (!$exists) not_found();
|
if (!$exists) not_found();
|
||||||
|
|
||||||
if (key_exists('action', $_GET)) {
|
if (key_exists('action', $_GET)) {
|
||||||
if ($_GET['action'] == 'add') {
|
$flag = match ($_GET['action']) {
|
||||||
$flag = 1;
|
'add' => 1,
|
||||||
} elseif ($_GET['action'] == 'remove') {
|
'remove' => 0,
|
||||||
$flag = 0;
|
default => null
|
||||||
}
|
};
|
||||||
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], $db);
|
||||||
|
|
|
@ -9,9 +9,9 @@ use BitBadger\Documents\DocumentException;
|
||||||
use BitBadger\Documents\Field;
|
use BitBadger\Documents\Field;
|
||||||
use BitBadger\Documents\SQLite\Delete;
|
use BitBadger\Documents\SQLite\Delete;
|
||||||
use FeedReaderCentral\Data;
|
use FeedReaderCentral\Data;
|
||||||
use FeedReaderCentral\Domain\Table;
|
|
||||||
use FeedReaderCentral\Feed;
|
use FeedReaderCentral\Feed;
|
||||||
use FeedReaderCentral\Security;
|
use FeedReaderCentral\Security;
|
||||||
|
use FeedReaderCentral\Table;
|
||||||
|
|
||||||
include '../../start.php';
|
include '../../start.php';
|
||||||
|
|
||||||
|
@ -58,11 +58,14 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
|
|
||||||
if ($feedId == 'new') {
|
if ($feedId == 'new') {
|
||||||
$title = 'Add RSS Feed';
|
$title = 'Add RSS Feed';
|
||||||
$feed = ['id' => $_GET['id'], 'url' => ''];
|
$feed = new Feed();
|
||||||
|
$feed->id = $_GET['id'];
|
||||||
} else {
|
} else {
|
||||||
$title = 'Edit RSS Feed';
|
$title = 'Edit RSS Feed';
|
||||||
if ($feedId == 'error') {
|
if ($feedId == 'error') {
|
||||||
$feed = ['id' => $_POST['id'] ?? '', 'url' => $_POST['url'] ?? ''];
|
$feed = new Feed();
|
||||||
|
$feed->id = $_POST['id'] ?? '';
|
||||||
|
$feed->url = $_POST['url'] ?? '';
|
||||||
} elseif (!($feed = Feed::retrieveById((int)$feedId))) not_found();
|
} elseif (!($feed = Feed::retrieveById((int)$feedId))) not_found();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,10 +73,10 @@ page_head($title); ?>
|
||||||
<h1><?=$title?></h1>
|
<h1><?=$title?></h1>
|
||||||
<article>
|
<article>
|
||||||
<form method=POST action=/feed/ hx-post=/feed/>
|
<form method=POST action=/feed/ hx-post=/feed/>
|
||||||
<input type=hidden name=id value=<?=$feed['id']?>>
|
<input type=hidden name=id value=<?=$feed->id?>>
|
||||||
<label>
|
<label>
|
||||||
Feed URL
|
Feed URL
|
||||||
<input type=url name=url required autofocus value="<?=$feed['url']?>">
|
<input type=url name=url required autofocus value="<?=$feed->url?>">
|
||||||
</label>
|
</label>
|
||||||
<span class=break></span>
|
<span class=break></span>
|
||||||
<button type=submit>Save</button>
|
<button type=submit>Save</button>
|
||||||
|
|
|
@ -5,9 +5,16 @@
|
||||||
* List feeds and provide links for maintenance actions
|
* List feeds and provide links for maintenance actions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use BitBadger\Documents\ArrayMapper;
|
||||||
|
use BitBadger\Documents\Field;
|
||||||
|
use BitBadger\Documents\JsonMapper;
|
||||||
|
use BitBadger\Documents\Query;
|
||||||
|
use BitBadger\Documents\SQLite\Custom;
|
||||||
use FeedReaderCentral\Data;
|
use FeedReaderCentral\Data;
|
||||||
|
use FeedReaderCentral\Feed;
|
||||||
use FeedReaderCentral\Key;
|
use FeedReaderCentral\Key;
|
||||||
use FeedReaderCentral\Security;
|
use FeedReaderCentral\Security;
|
||||||
|
use FeedReaderCentral\Table;
|
||||||
|
|
||||||
include '../start.php';
|
include '../start.php';
|
||||||
|
|
||||||
|
@ -15,42 +22,34 @@ $db = Data::getConnection();
|
||||||
Security::verifyUser($db);
|
Security::verifyUser($db);
|
||||||
|
|
||||||
// TODO: adapt query when document list is done
|
// TODO: adapt query when document list is done
|
||||||
$feedQuery = $db->prepare('SELECT * FROM feed WHERE user_id = :user ORDER BY lower(title)');
|
$field = Field::EQ('user_id', $_SESSION[Key::USER_ID], '@user');
|
||||||
$feedQuery->bindValue(':user', $_SESSION[Key::USER_ID]);
|
$feeds = Custom::list(Query\Find::byFields(Table::FEED, [$field]) . " ORDER BY lower(data->>'title')",
|
||||||
if (!($feedResult = $feedQuery->execute())) {
|
$field->toParameter(), new JsonMapper(Feed::class));
|
||||||
add_error(Data::error($db)['error']);
|
|
||||||
}
|
|
||||||
|
|
||||||
page_head('Your Feeds'); ?>
|
page_head('Your Feeds'); ?>
|
||||||
<h1>Your Feeds</h1>
|
<h1>Your Feeds</h1>
|
||||||
<article>
|
<article>
|
||||||
<p class=action_buttons><?=hx_get('/feed/?id=new', 'Add Feed')?></p><?php
|
<p class=action_buttons><?=hx_get('/feed/?id=new', 'Add Feed')?></p><?php
|
||||||
if ($feedResult) {
|
iterator_apply($feeds->items(), function (Feed $feed) {
|
||||||
while ($feed = $feedResult->fetchArray(SQLITE3_ASSOC)) {
|
$item = Table::ITEM;
|
||||||
$feedId = $feed['id'];
|
$counts = Custom::single(<<<SQL
|
||||||
$countQuery = $db->prepare(<<<'SQL'
|
SELECT (SELECT COUNT(*) FROM $item WHERE data->>'feed_id' = @feed) AS total,
|
||||||
SELECT (SELECT COUNT(*) FROM item WHERE feed_id = :feed) AS total,
|
(SELECT COUNT(*) FROM $item WHERE data->>'feed_id' = @feed AND data->>'is_read' = '0') AS unread,
|
||||||
(SELECT COUNT(*) FROM item WHERE feed_id = :feed AND is_read = 0) AS unread,
|
(SELECT COUNT(*) FROM $item WHERE data->>'feed_id' = @feed AND data->>'is_bookmarked' = '1') AS marked
|
||||||
(SELECT COUNT(*) FROM item WHERE feed_id = :feed AND is_bookmarked = 1) AS marked
|
SQL, ['@feed' => $feed->id], new ArrayMapper()) ?? ['total' => 0, 'unread' => 0, 'marked' => 0]; ?>
|
||||||
SQL);
|
<p><strong><?=htmlentities($feed->title)?></strong><br>
|
||||||
$countQuery->bindValue(':feed', $feed['id']);
|
<span class=meta><em>Last Updated <?=date_time($feed->updated_on)?> •
|
||||||
$countResult = $countQuery->execute();
|
As of <?=date_time($feed->checked_on)?></em><br>
|
||||||
$counts = $countResult
|
<?=hx_get("/feed/?id=$feed->id", 'Edit')?> • Read
|
||||||
? $countResult->fetchArray(SQLITE3_ASSOC) : ['total' => 0, 'unread' => 0, 'marked' => 0]; ?>
|
<?=$counts['unread'] > 0 ? hx_get("/feed/items?id=$feed->id&unread", 'Unread') : 'Unread'?>
|
||||||
<p><strong><?=htmlentities($feed['title'])?></strong><br>
|
|
||||||
<span class=meta><em>Last Updated <?=date_time($feed['updated_on'])?> •
|
|
||||||
As of <?=date_time($feed['checked_on'])?></em><br>
|
|
||||||
<?=hx_get("/feed/?id=$feedId", 'Edit')?> • Read
|
|
||||||
<?=$counts['unread'] > 0 ? hx_get("/feed/items?id=$feedId&unread", 'Unread') : 'Unread'?>
|
|
||||||
(<?=$counts['unread']?>) |
|
(<?=$counts['unread']?>) |
|
||||||
<?=$counts['total'] > 0 ? hx_get("/feed/items?id=$feedId", 'All') : 'All'?> (<?=$counts['total']?>) |
|
<?=$counts['total'] > 0 ? hx_get("/feed/items?id=$feed->id", 'All') : 'All'?> (<?=$counts['total']?>) |
|
||||||
<?=$counts['marked'] > 0 ? hx_get("/feed/items?id=$feedId&bookmarked", 'Bookmarked') : 'Bookmarked'?>
|
<?=$counts['marked'] > 0 ? hx_get("/feed/items?id=$feed->id&bookmarked", 'Bookmarked') : 'Bookmarked'?>
|
||||||
(<?=$counts['marked']?>) •
|
(<?=$counts['marked']?>) •
|
||||||
<a href=/feed/?id=<?=$feedId?> hx-delete=/feed/?id=<?=$feedId?>
|
<a href=/feed/?id=<?=$feed->id?> hx-delete=/feed/?id=<?=$feed->id?>
|
||||||
hx-confirm="Are you sure you want to delete “<?=htmlentities($feed['title'], ENT_QUOTES)?>”? This will remove the feed and all its items, including unread and bookmarked.">Delete</a>
|
hx-confirm="Are you sure you want to delete “<?=htmlspecialchars($feed->title)?>”? This will remove the feed and all its items, including unread and bookmarked.">Delete</a>
|
||||||
</span><?php
|
</span><?php
|
||||||
}
|
}); ?>
|
||||||
} ?>
|
|
||||||
</article><?php
|
</article><?php
|
||||||
page_foot();
|
page_foot();
|
||||||
$db->close();
|
$db->close();
|
||||||
|
|
|
@ -6,11 +6,14 @@
|
||||||
* Retrieves and displays an item from a feed belonging to the current user
|
* Retrieves and displays an item from a feed belonging to the current user
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use BitBadger\Documents\DocumentException;
|
||||||
|
use BitBadger\Documents\SQLite\Delete;
|
||||||
use BitBadger\Documents\SQLite\Patch;
|
use BitBadger\Documents\SQLite\Patch;
|
||||||
use FeedReaderCentral\Data;
|
use FeedReaderCentral\Data;
|
||||||
use FeedReaderCentral\Domain\Table;
|
use FeedReaderCentral\Item;
|
||||||
use FeedReaderCentral\Key;
|
use FeedReaderCentral\Key;
|
||||||
use FeedReaderCentral\Security;
|
use FeedReaderCentral\Security;
|
||||||
|
use FeedReaderCentral\Table;
|
||||||
|
|
||||||
include '../start.php';
|
include '../start.php';
|
||||||
|
|
||||||
|
@ -18,47 +21,33 @@ $db = Data::getConnection();
|
||||||
Security::verifyUser($db);
|
Security::verifyUser($db);
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
// "Keep as New" button sends a POST request to reset the is_read flag before going back to the list of unread items
|
try {
|
||||||
$isValidQuery = $db->prepare(<<<'SQL'
|
// "Keep as New" button sends a POST request to reset the is_read flag before going back to the item list
|
||||||
SELECT COUNT(*)
|
if (Item::retrieveByIdForUser($_POST['id'])) {
|
||||||
FROM item INNER JOIN feed ON feed.id = item.feed_id
|
|
||||||
WHERE item.id = :id AND feed.user_id = :user
|
|
||||||
SQL);
|
|
||||||
$isValidQuery->bindValue(':id', $_POST['id']);
|
|
||||||
$isValidQuery->bindValue(':user', $_SESSION[Key::USER_ID]);
|
|
||||||
$isValidResult = $isValidQuery->execute();
|
|
||||||
if ($isValidResult && $isValidResult->fetchArray(SQLITE3_NUM)[0] == 1) {
|
|
||||||
Patch::byId(Table::ITEM, $_POST['id'], ['is_read' => 0], $db);
|
Patch::byId(Table::ITEM, $_POST['id'], ['is_read' => 0], $db);
|
||||||
// $keepUnread = $db->prepare('UPDATE item SET is_read = 0 WHERE id = :id');
|
|
||||||
// $keepUnread->bindValue(':id', $_POST['id']);
|
|
||||||
// $keepUnread->execute();
|
|
||||||
}
|
}
|
||||||
$db->close();
|
$db->close();
|
||||||
frc_redirect($_POST['from']);
|
frc_redirect($_POST['from']);
|
||||||
|
} catch (DocumentException $ex) {
|
||||||
|
add_error("$ex");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$from = $_GET['from'] ?? '/';
|
$from = $_GET['from'] ?? '/';
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] == 'DELETE') {
|
if ($_SERVER['REQUEST_METHOD'] == 'DELETE') {
|
||||||
$deleteQuery = $db->prepare(<<<'SQL'
|
try {
|
||||||
DELETE FROM item
|
if (Item::retrieveByIdForUser($_GET['id'])) {
|
||||||
WHERE id IN (
|
Delete::byId(Table::ITEM, $_GET['id'], $db);
|
||||||
SELECT item.id
|
}
|
||||||
FROM item INNER JOIN feed ON feed.id = item.feed_id
|
} catch (DocumentException $ex) {
|
||||||
WHERE item.id = :id
|
add_error("$ex");
|
||||||
AND feed.user_id = :user)
|
|
||||||
SQL);
|
|
||||||
$deleteQuery->bindValue(':id', $_GET['id']);
|
|
||||||
$deleteQuery->bindValue(':user', $_SESSION[Key::USER_ID]);
|
|
||||||
if ($deleteQuery->execute()) {
|
|
||||||
add_info('Item deleted');
|
|
||||||
} else {
|
|
||||||
add_error(Data::error($db)['error']);
|
|
||||||
}
|
}
|
||||||
$db->close();
|
$db->close();
|
||||||
frc_redirect($from);
|
frc_redirect($from);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: convert this query
|
||||||
$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
|
||||||
|
@ -72,9 +61,11 @@ $result = $query->execute();
|
||||||
$item = $result ? $result->fetchArray(SQLITE3_ASSOC) : false;
|
$item = $result ? $result->fetchArray(SQLITE3_ASSOC) : false;
|
||||||
|
|
||||||
if ($item) {
|
if ($item) {
|
||||||
$markRead = $db->prepare('UPDATE item SET is_read = 1 WHERE id = :id');
|
try {
|
||||||
$markRead->bindValue(':id', $_GET['id']);
|
Patch::byId(Table::ITEM, $_GET['id'], ['is_read' => 1], $db);
|
||||||
$markRead->execute();
|
} catch (DocumentException $ex) {
|
||||||
|
add_error("$ex");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$published = date_time($item['published_on']);
|
$published = date_time($item['published_on']);
|
||||||
|
|
|
@ -3,10 +3,9 @@
|
||||||
use BitBadger\Documents\DocumentException;
|
use BitBadger\Documents\DocumentException;
|
||||||
use BitBadger\Documents\SQLite\Find;
|
use BitBadger\Documents\SQLite\Find;
|
||||||
use FeedReaderCentral\Data;
|
use FeedReaderCentral\Data;
|
||||||
use FeedReaderCentral\Domain\Feed as FeedDocument;
|
|
||||||
use FeedReaderCentral\Domain\Table;
|
|
||||||
use FeedReaderCentral\Domain\User;
|
|
||||||
use FeedReaderCentral\Feed;
|
use FeedReaderCentral\Feed;
|
||||||
|
use FeedReaderCentral\Table;
|
||||||
|
use FeedReaderCentral\User;
|
||||||
|
|
||||||
require __DIR__ . '/../cli-start.php';
|
require __DIR__ . '/../cli-start.php';
|
||||||
|
|
||||||
|
@ -41,7 +40,7 @@ function refresh_all(): void
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$users = [];
|
$users = [];
|
||||||
iterator_apply(Feed::retrieveAll()->items(), function (FeedDocument $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,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use BitBadger\Documents\DocumentException;
|
||||||
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;
|
||||||
|
@ -47,8 +48,9 @@ function rebuild_index(): void
|
||||||
}
|
}
|
||||||
printfn('Rebuilding search index...');
|
printfn('Rebuilding search index...');
|
||||||
Custom::nonQuery("INSERT INTO item_search (item_search) VALUES ('rebuild')", [], $db);
|
Custom::nonQuery("INSERT INTO item_search (item_search) VALUES ('rebuild')", [], $db);
|
||||||
// $db->exec("INSERT INTO item_search (item_search) VALUES ('rebuild')");
|
|
||||||
printfn(PHP_EOL . 'Search index rebuilt');
|
printfn(PHP_EOL . 'Search index rebuilt');
|
||||||
|
} catch (DocumentException $ex) {
|
||||||
|
printfn("$ex");
|
||||||
} finally {
|
} finally {
|
||||||
$db->close();
|
$db->close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,9 @@ 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;
|
||||||
use FeedReaderCentral\Data;
|
use FeedReaderCentral\Data;
|
||||||
use FeedReaderCentral\Domain\Table;
|
|
||||||
use FeedReaderCentral\Security;
|
use FeedReaderCentral\Security;
|
||||||
|
use FeedReaderCentral\Table;
|
||||||
|
use FeedReaderCentral\User;
|
||||||
|
|
||||||
require __DIR__ . '/../cli-start.php';
|
require __DIR__ . '/../cli-start.php';
|
||||||
|
|
||||||
|
@ -99,15 +100,17 @@ function add_user(): void
|
||||||
|
|
||||||
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 = Security::findUserByEmail($argv[2], $db);
|
$user = User::findByEmail($argv[2]);
|
||||||
if ($user) {
|
if ($user) {
|
||||||
printfn('A user with e-mail address "%s" already exists', $argv[2]);
|
printfn('A user with e-mail address "%s" already exists', $argv[2]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Security::addUser($argv[2], $argv[3], $db);
|
User::add($argv[2], $argv[3], $db);
|
||||||
|
|
||||||
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) {
|
||||||
|
printfn("$ex");
|
||||||
} finally {
|
} finally {
|
||||||
$db->close();
|
$db->close();
|
||||||
}
|
}
|
||||||
|
@ -134,7 +137,7 @@ function set_password(string $email, string $password): void
|
||||||
$displayUser = display_user($email);
|
$displayUser = display_user($email);
|
||||||
|
|
||||||
// Ensure this user exists
|
// Ensure this user exists
|
||||||
$user = Security::findUserByEmail($email, $db);
|
$user = User::findByEmail($email);
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
printfn('No %s exists', $displayUser);
|
printfn('No %s exists', $displayUser);
|
||||||
return;
|
return;
|
||||||
|
@ -145,6 +148,8 @@ function set_password(string $email, string $password): void
|
||||||
$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) {
|
||||||
|
printfn("$ex");
|
||||||
} finally {
|
} finally {
|
||||||
$db->close();
|
$db->close();
|
||||||
}
|
}
|
||||||
|
@ -163,7 +168,7 @@ function delete_user(string $email): void
|
||||||
$displayUser = display_user($email);
|
$displayUser = display_user($email);
|
||||||
|
|
||||||
// Get the user for the provided e-mail address
|
// Get the user for the provided e-mail address
|
||||||
$user = Security::findUserByEmail($email, $db);
|
$user = User::findByEmail($email);
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
printfn('No %s exists', $displayUser);
|
printfn('No %s exists', $displayUser);
|
||||||
return;
|
return;
|
||||||
|
@ -195,6 +200,8 @@ function delete_user(string $email): void
|
||||||
} catch (DocumentException $ex) {
|
} catch (DocumentException $ex) {
|
||||||
printfn("$ex");
|
printfn("$ex");
|
||||||
}
|
}
|
||||||
|
} catch (DocumentException $ex) {
|
||||||
|
printfn("$ex");
|
||||||
} finally {
|
} finally {
|
||||||
$db->close();
|
$db->close();
|
||||||
}
|
}
|
||||||
|
@ -210,7 +217,7 @@ function migrate_single_user(): void
|
||||||
$db = Data::getConnection();
|
$db = Data::getConnection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!$single = Security::findUserByEmail(Security::SINGLE_USER_EMAIL, $db)) {
|
if (!$single = User::findByEmail(Security::SINGLE_USER_EMAIL)) {
|
||||||
printfn('There is no single-user mode user to be migrated');
|
printfn('There is no single-user mode user to be migrated');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user