mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 21:22:40 +00:00
Partial API documentation for the Database class
This commit is contained in:
parent
b1282b6f6a
commit
d3a385beef
1 changed files with 197 additions and 0 deletions
197
lib/Database.php
197
lib/Database.php
|
@ -14,8 +14,11 @@ use JKingWeb\Arsse\Misc\Date;
|
||||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
use JKingWeb\Arsse\Misc\ValueInfo;
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
|
/** The version number of the latest schema the interface is aware of */
|
||||||
const SCHEMA_VERSION = 4;
|
const SCHEMA_VERSION = 4;
|
||||||
|
/** The maximum number of articles to mark in one query without chunking */
|
||||||
const LIMIT_ARTICLES = 50;
|
const LIMIT_ARTICLES = 50;
|
||||||
|
/** A map database driver short-names and their associated class names */
|
||||||
const DRIVER_NAMES = [
|
const DRIVER_NAMES = [
|
||||||
'sqlite3' => \JKingWeb\Arsse\Db\SQLite3\Driver::class,
|
'sqlite3' => \JKingWeb\Arsse\Db\SQLite3\Driver::class,
|
||||||
'postgresql' => \JKingWeb\Arsse\Db\PostgreSQL\Driver::class,
|
'postgresql' => \JKingWeb\Arsse\Db\PostgreSQL\Driver::class,
|
||||||
|
@ -25,6 +28,10 @@ class Database {
|
||||||
/** @var Db\Driver */
|
/** @var Db\Driver */
|
||||||
public $db;
|
public $db;
|
||||||
|
|
||||||
|
/** Constructs the database interface
|
||||||
|
*
|
||||||
|
* @param boolean $initialize Whether to attempt to upgrade the databse schema when constructing
|
||||||
|
*/
|
||||||
public function __construct($initialize = true) {
|
public function __construct($initialize = true) {
|
||||||
$driver = Arsse::$conf->dbDriver;
|
$driver = Arsse::$conf->dbDriver;
|
||||||
$this->db = $driver::create();
|
$this->db = $driver::create();
|
||||||
|
@ -34,10 +41,14 @@ class Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the bare name of the calling context's calling method, when __FUNCTION__ is not appropriate */
|
||||||
protected function caller(): string {
|
protected function caller(): string {
|
||||||
return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function'];
|
return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lists the available database drivers, as an associative array with
|
||||||
|
* fully-qualified class names as keys, and human-readable descriptions as values
|
||||||
|
*/
|
||||||
public static function driverList(): array {
|
public static function driverList(): array {
|
||||||
$sep = \DIRECTORY_SEPARATOR;
|
$sep = \DIRECTORY_SEPARATOR;
|
||||||
$path = __DIR__.$sep."Db".$sep;
|
$path = __DIR__.$sep."Db".$sep;
|
||||||
|
@ -50,10 +61,12 @@ class Database {
|
||||||
return $classes;
|
return $classes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the current (actual) schema version of the database; compared against self::SCHEMA_VERSION to know when an upgrade is required */
|
||||||
public function driverSchemaVersion(): int {
|
public function driverSchemaVersion(): int {
|
||||||
return $this->db->schemaVersion();
|
return $this->db->schemaVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Attempts to update the database schema. If it is already up to date, false is returned */
|
||||||
public function driverSchemaUpdate(): bool {
|
public function driverSchemaUpdate(): bool {
|
||||||
if ($this->db->schemaVersion() < self::SCHEMA_VERSION) {
|
if ($this->db->schemaVersion() < self::SCHEMA_VERSION) {
|
||||||
return $this->db->schemaUpdate(self::SCHEMA_VERSION);
|
return $this->db->schemaUpdate(self::SCHEMA_VERSION);
|
||||||
|
@ -61,10 +74,18 @@ class Database {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns whether the database's character set is Unicode */
|
||||||
public function driverCharsetAcceptable(): bool {
|
public function driverCharsetAcceptable(): bool {
|
||||||
return $this->db->charsetAcceptable();
|
return $this->db->charsetAcceptable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Computes the column and value text of an SQL "SET" clause, validating arbitrary input against a whitelist
|
||||||
|
*
|
||||||
|
* Returns an indexed array containing the clause text, an array of types, and another array of values
|
||||||
|
*
|
||||||
|
* @param array $props An associative array containing untrusted data; keys are column names
|
||||||
|
* @param array $valid An associative array containing a whitelist: keys are column names, and values are strings representing data types
|
||||||
|
*/
|
||||||
protected function generateSet(array $props, array $valid): array {
|
protected function generateSet(array $props, array $valid): array {
|
||||||
$out = [
|
$out = [
|
||||||
[], // query clause
|
[], // query clause
|
||||||
|
@ -83,6 +104,13 @@ class Database {
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Conputes the contents of an SQL "IN()" clause, producing one parameter placeholder for each input value
|
||||||
|
*
|
||||||
|
* Returns an indexed array containing the clause text, and an array of types
|
||||||
|
*
|
||||||
|
* @param array $values Arbitrary values
|
||||||
|
* @param string $type A single data type applied to each value
|
||||||
|
*/
|
||||||
protected function generateIn(array $values, string $type): array {
|
protected function generateIn(array $values, string $type): array {
|
||||||
$out = [
|
$out = [
|
||||||
"", // query clause
|
"", // query clause
|
||||||
|
@ -100,14 +128,17 @@ class Database {
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a Transaction object, which is rolled back unless explicitly committed */
|
||||||
public function begin(): Db\Transaction {
|
public function begin(): Db\Transaction {
|
||||||
return $this->db->begin();
|
return $this->db->begin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Retrieve a value from the metadata table. If the key is not set null is returned */
|
||||||
public function metaGet(string $key) {
|
public function metaGet(string $key) {
|
||||||
return $this->db->prepare("SELECT value from arsse_meta where \"key\" = ?", "str")->run($key)->getValue();
|
return $this->db->prepare("SELECT value from arsse_meta where \"key\" = ?", "str")->run($key)->getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sets the given key in the metadata table to the given value. If the key already exists it is silently overwritten */
|
||||||
public function metaSet(string $key, $value, string $type = "str"): bool {
|
public function metaSet(string $key, $value, string $type = "str"): bool {
|
||||||
$out = $this->db->prepare("UPDATE arsse_meta set value = ? where \"key\" = ?", $type, "str")->run($value, $key)->changes();
|
$out = $this->db->prepare("UPDATE arsse_meta set value = ? where \"key\" = ?", $type, "str")->run($value, $key)->changes();
|
||||||
if (!$out) {
|
if (!$out) {
|
||||||
|
@ -116,10 +147,12 @@ class Database {
|
||||||
return (bool) $out;
|
return (bool) $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Unsets the given key in the metadata table. Returns false if the key does not exist */
|
||||||
public function metaRemove(string $key): bool {
|
public function metaRemove(string $key): bool {
|
||||||
return (bool) $this->db->prepare("DELETE from arsse_meta where \"key\" = ?", "str")->run($key)->changes();
|
return (bool) $this->db->prepare("DELETE from arsse_meta where \"key\" = ?", "str")->run($key)->changes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns whether the specified user exists in the database */
|
||||||
public function userExists(string $user): bool {
|
public function userExists(string $user): bool {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -127,6 +160,11 @@ class Database {
|
||||||
return (bool) $this->db->prepare("SELECT count(*) from arsse_users where id = ?", "str")->run($user)->getValue();
|
return (bool) $this->db->prepare("SELECT count(*) from arsse_users where id = ?", "str")->run($user)->getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Adds a user to the database
|
||||||
|
*
|
||||||
|
* @param string $user The user to add
|
||||||
|
* @param string $passwordThe user's password in cleartext. It will be stored hashed
|
||||||
|
*/
|
||||||
public function userAdd(string $user, string $password): bool {
|
public function userAdd(string $user, string $password): bool {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -138,6 +176,7 @@ class Database {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Removes a user from the database */
|
||||||
public function userRemove(string $user): bool {
|
public function userRemove(string $user): bool {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -148,6 +187,7 @@ class Database {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a flat, indexed array of all users in the database */
|
||||||
public function userList(): array {
|
public function userList(): array {
|
||||||
$out = [];
|
$out = [];
|
||||||
if (!Arsse::$user->authorize("", __FUNCTION__)) {
|
if (!Arsse::$user->authorize("", __FUNCTION__)) {
|
||||||
|
@ -159,6 +199,7 @@ class Database {
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Retrieves the hashed password of a user */
|
||||||
public function userPasswordGet(string $user): string {
|
public function userPasswordGet(string $user): string {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -168,6 +209,11 @@ class Database {
|
||||||
return (string) $this->db->prepare("SELECT password from arsse_users where id = ?", "str")->run($user)->getValue();
|
return (string) $this->db->prepare("SELECT password from arsse_users where id = ?", "str")->run($user)->getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sets the password of an existing user
|
||||||
|
*
|
||||||
|
* @param string $user The user for whom to set the password
|
||||||
|
* @param string $password The new password, in cleartext. The password will be stored hashed
|
||||||
|
*/
|
||||||
public function userPasswordSet(string $user, string $password): bool {
|
public function userPasswordSet(string $user, string $password): bool {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -179,6 +225,7 @@ class Database {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates a new session for the given user and returns the session identifier */
|
||||||
public function sessionCreate(string $user): string {
|
public function sessionCreate(string $user): string {
|
||||||
// If the user isn't authorized to perform this action then throw an exception.
|
// If the user isn't authorized to perform this action then throw an exception.
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
|
@ -193,6 +240,14 @@ class Database {
|
||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Explicitly removes a session from the database
|
||||||
|
*
|
||||||
|
* Sessions may also be invalidated as they expire, and then be automatically pruned.
|
||||||
|
* This function can be used to explicitly invalidate a session after a user logs out
|
||||||
|
*
|
||||||
|
* @param string $user The user who owns the session to be destroyed
|
||||||
|
* @param string $id The identifier of the session to destroy
|
||||||
|
*/
|
||||||
public function sessionDestroy(string $user, string $id): bool {
|
public function sessionDestroy(string $user, string $id): bool {
|
||||||
// If the user isn't authorized to perform this action then throw an exception.
|
// If the user isn't authorized to perform this action then throw an exception.
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
|
@ -202,6 +257,10 @@ class Database {
|
||||||
return (bool) $this->db->prepare("DELETE FROM arsse_sessions where id = ? and \"user\" = ?", "str", "str")->run($id, $user)->changes();
|
return (bool) $this->db->prepare("DELETE FROM arsse_sessions where id = ? and \"user\" = ?", "str", "str")->run($id, $user)->changes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resumes a session, returning available session data
|
||||||
|
*
|
||||||
|
* This also has the side effect of refreshing the session if it is near its timeout
|
||||||
|
*/
|
||||||
public function sessionResume(string $id): array {
|
public function sessionResume(string $id): array {
|
||||||
$maxAge = Date::sub(Arsse::$conf->userSessionLifetime);
|
$maxAge = Date::sub(Arsse::$conf->userSessionLifetime);
|
||||||
$out = $this->db->prepare("SELECT id,created,expires,\"user\" from arsse_sessions where id = ? and expires > CURRENT_TIMESTAMP and created > ?", "str", "datetime")->run($id, $maxAge)->getRow();
|
$out = $this->db->prepare("SELECT id,created,expires,\"user\" from arsse_sessions where id = ? and expires > CURRENT_TIMESTAMP and created > ?", "str", "datetime")->run($id, $maxAge)->getRow();
|
||||||
|
@ -217,11 +276,13 @@ class Database {
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Deletes expires sessions from the database, returning the number of deleted sessions */
|
||||||
public function sessionCleanup(): int {
|
public function sessionCleanup(): int {
|
||||||
$maxAge = Date::sub(Arsse::$conf->userSessionLifetime);
|
$maxAge = Date::sub(Arsse::$conf->userSessionLifetime);
|
||||||
return $this->db->prepare("DELETE FROM arsse_sessions where expires < CURRENT_TIMESTAMP or created < ?", "datetime")->run($maxAge)->changes();
|
return $this->db->prepare("DELETE FROM arsse_sessions where expires < CURRENT_TIMESTAMP or created < ?", "datetime")->run($maxAge)->changes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Checks if a given future timeout is less than half the session timeout interval */
|
||||||
protected function sessionExpiringSoon(\DateTimeInterface $expiry): bool {
|
protected function sessionExpiringSoon(\DateTimeInterface $expiry): bool {
|
||||||
// calculate half the session timeout as a number of seconds
|
// calculate half the session timeout as a number of seconds
|
||||||
$now = time();
|
$now = time();
|
||||||
|
@ -231,6 +292,18 @@ class Database {
|
||||||
return (($now + $diff) >= $expiry->getTimestamp());
|
return (($now + $diff) >= $expiry->getTimestamp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Adds a folder for containing newsfeed subscriptions, returning an integer identifying the created folder
|
||||||
|
*
|
||||||
|
* The $data array may contain the following keys:
|
||||||
|
*
|
||||||
|
* - "name": A folder name, which must be a non-empty string not composed solely of whitespace; this key is required
|
||||||
|
* - "parent": An integer (or null) identifying a parent folder; this key is optional
|
||||||
|
*
|
||||||
|
* If a folder with the same name and parent already exists, this is an error
|
||||||
|
*
|
||||||
|
* @param string $user The user who will own the folder
|
||||||
|
* @param array $data An associative array defining the folder
|
||||||
|
*/
|
||||||
public function folderAdd(string $user, array $data): int {
|
public function folderAdd(string $user, array $data): int {
|
||||||
// If the user isn't authorized to perform this action then throw an exception.
|
// If the user isn't authorized to perform this action then throw an exception.
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
|
@ -245,6 +318,20 @@ class Database {
|
||||||
return $this->db->prepare("INSERT INTO arsse_folders(owner,parent,name) values(?,?,?)", "str", "int", "str")->run($user, $parent, $name)->lastId();
|
return $this->db->prepare("INSERT INTO arsse_folders(owner,parent,name) values(?,?,?)", "str", "int", "str")->run($user, $parent, $name)->lastId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a result set listing a user's folders
|
||||||
|
*
|
||||||
|
* Each record in the result set contains:
|
||||||
|
*
|
||||||
|
* - "id": The folder identifier, an integer
|
||||||
|
* - "name": The folder's name, a string
|
||||||
|
* - "parent": The integer identifier of the folder's parent, or null
|
||||||
|
* - "children": The number of child folders contained in the given folder
|
||||||
|
* - "feeds": The number of newsfeed subscriptions contained in the given folder, not including subscriptions in descendent folders
|
||||||
|
*
|
||||||
|
* @param string $uer The user whose folders are to be listed
|
||||||
|
* @param integer|null $parent Restricts the list to the descendents of the specified folder identifier
|
||||||
|
* @param boolean $recursive Whether to list all descendents, or only direct children
|
||||||
|
*/
|
||||||
public function folderList(string $user, $parent = null, bool $recursive = true): Db\Result {
|
public function folderList(string $user, $parent = null, bool $recursive = true): Db\Result {
|
||||||
// if the user isn't authorized to perform this action then throw an exception.
|
// if the user isn't authorized to perform this action then throw an exception.
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
|
@ -270,6 +357,13 @@ class Database {
|
||||||
return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());
|
return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Deletes a folder from the database
|
||||||
|
*
|
||||||
|
* Any descendent folders are also deleted, as are all newsfeed subscriptions contained in the deleted folder tree
|
||||||
|
*
|
||||||
|
* @param string $user The user to whom the folder to be deleted belongs
|
||||||
|
* @param integer $id The identifier of the folder to delete
|
||||||
|
*/
|
||||||
public function folderRemove(string $user, $id): bool {
|
public function folderRemove(string $user, $id): bool {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -284,6 +378,7 @@ class Database {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the identifier, name, and parent of the given folder as an associative array */
|
||||||
public function folderPropertiesGet(string $user, $id): array {
|
public function folderPropertiesGet(string $user, $id): array {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -298,6 +393,19 @@ class Database {
|
||||||
return $props;
|
return $props;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Modifies the properties of a folder
|
||||||
|
*
|
||||||
|
* The $data array must contain one or more of the following keys:
|
||||||
|
*
|
||||||
|
* - "name": A new folder name, which must be a non-empty string not composed solely of whitespace
|
||||||
|
* - "parent": An integer (or null) identifying a parent folder
|
||||||
|
*
|
||||||
|
* If a folder with the new name and parent combination already exists, this is an error; it is also an error to move a folder to itself or one of its descendents
|
||||||
|
*
|
||||||
|
* @param string $user The user who owns the folder to be modified
|
||||||
|
* @param integer $id The identifier of the folder to be modified
|
||||||
|
* @param array $data An associative array of properties to modify. Anything not specified will remain unchanged
|
||||||
|
*/
|
||||||
public function folderPropertiesSet(string $user, $id, array $data): bool {
|
public function folderPropertiesSet(string $user, $id, array $data): bool {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -334,6 +442,14 @@ class Database {
|
||||||
return (bool) $this->db->prepare("UPDATE arsse_folders set $setClause, modified = CURRENT_TIMESTAMP where owner = ? and id = ?", $setTypes, "str", "int")->run($setValues, $user, $id)->changes();
|
return (bool) $this->db->prepare("UPDATE arsse_folders set $setClause, modified = CURRENT_TIMESTAMP where owner = ? and id = ?", $setTypes, "str", "int")->run($setValues, $user, $id)->changes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ensures the specified folder exists and raises an exception otherwise
|
||||||
|
*
|
||||||
|
* Returns an associative array containing the id, name, and parent of the folder if it exists
|
||||||
|
*
|
||||||
|
* @param string $user The user who owns the folder to be validated
|
||||||
|
* @param integer|null $id The identifier of the folder to validate; null or zero represent the implied root folder
|
||||||
|
* @param boolean $subject Whether the folder is the subject rather than the object of the operation being performed; this only affects the semantics of the error message if validation fails
|
||||||
|
*/
|
||||||
protected function folderValidateId(string $user, $id = null, bool $subject = false): array {
|
protected function folderValidateId(string $user, $id = null, bool $subject = false): array {
|
||||||
// if the specified ID is not a non-negative integer (or null), this will always fail
|
// if the specified ID is not a non-negative integer (or null), this will always fail
|
||||||
if (!ValueInfo::id($id, true)) {
|
if (!ValueInfo::id($id, true)) {
|
||||||
|
@ -351,6 +467,7 @@ class Database {
|
||||||
return $f;
|
return $f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ensures an operation to rename and/or move a folder does not result in a conflict or circular dependence, and raises an exception otherwise */
|
||||||
protected function folderValidateMove(string $user, $id = null, $parent = null, string $name = null) {
|
protected function folderValidateMove(string $user, $id = null, $parent = null, string $name = null) {
|
||||||
$errData = ["action" => $this->caller(), "field" => "parent", 'id' => $parent];
|
$errData = ["action" => $this->caller(), "field" => "parent", 'id' => $parent];
|
||||||
if (!$id) {
|
if (!$id) {
|
||||||
|
@ -403,6 +520,12 @@ class Database {
|
||||||
return $parent;
|
return $parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ensures a prospective folder name is valid, and optionally ensure it is not a duplicate if renamed
|
||||||
|
*
|
||||||
|
* @param string $name The name to check
|
||||||
|
* @param boolean $checkDuplicates Whether to also check if the new name would cause a collision
|
||||||
|
* @param integer|null $parent The parent folder context in which to check for duplication
|
||||||
|
*/
|
||||||
protected function folderValidateName($name, bool $checkDuplicates = false, $parent = null): bool {
|
protected function folderValidateName($name, bool $checkDuplicates = false, $parent = null): bool {
|
||||||
$info = ValueInfo::str($name);
|
$info = ValueInfo::str($name);
|
||||||
if ($info & (ValueInfo::NULL | ValueInfo::EMPTY)) {
|
if ($info & (ValueInfo::NULL | ValueInfo::EMPTY)) {
|
||||||
|
@ -424,6 +547,14 @@ class Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Adds a subscription to a newsfeed, and returns the numeric identifier of the added subscription
|
||||||
|
*
|
||||||
|
* @param string $user The user which will own the subscription
|
||||||
|
* @param string $url The URL of the newsfeed or discovery source
|
||||||
|
* @param string $fetchUser The user name required to access the newsfeed, if applicable
|
||||||
|
* @param string $fetchPassword The password required to fetch the newsfeed, if applicable; this will be stored in cleartext
|
||||||
|
* @param boolean $discovery Whether to perform newsfeed discovery if $url points to an HTML document
|
||||||
|
*/
|
||||||
public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = "", bool $discover = true): int {
|
public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = "", bool $discover = true): int {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -452,6 +583,13 @@ class Database {
|
||||||
return $this->db->prepare('INSERT INTO arsse_subscriptions(owner,feed) values(?,?)', 'str', 'int')->run($user, $feedID)->lastId();
|
return $this->db->prepare('INSERT INTO arsse_subscriptions(owner,feed) values(?,?)', 'str', 'int')->run($user, $feedID)->lastId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lists a user's subscriptions, returning various data
|
||||||
|
*
|
||||||
|
* @param string $user The user whose subscriptions are to be listed
|
||||||
|
* @param integer|null $folder The identifier of the folder under which to list subscriptions; by default the root folder is used
|
||||||
|
* @param boolean $recursive Whether to list subscriptions of descendent folders as well as the selected folder
|
||||||
|
* @param integer|null $id The numeric identifier of a particular subscription; used internally by subscriptionPropertiesGet
|
||||||
|
*/
|
||||||
public function subscriptionList(string $user, $folder = null, bool $recursive = true, int $id = null): Db\Result {
|
public function subscriptionList(string $user, $folder = null, bool $recursive = true, int $id = null): Db\Result {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -494,6 +632,7 @@ class Database {
|
||||||
return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());
|
return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the number of subscriptions in a folder, counting recursively */
|
||||||
public function subscriptionCount(string $user, $folder = null): int {
|
public function subscriptionCount(string $user, $folder = null): int {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -512,6 +651,13 @@ class Database {
|
||||||
return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue();
|
return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Deletes a subscription from the database
|
||||||
|
*
|
||||||
|
* This has the side effect of deleting all marks the user has set on articles
|
||||||
|
* belonging to the newsfeed, but may not delete the articles themselves, as
|
||||||
|
* other users may also be subscribed to the same newsfeed. There is also a
|
||||||
|
* configurable retention period for newsfeeds
|
||||||
|
*/
|
||||||
public function subscriptionRemove(string $user, $id): bool {
|
public function subscriptionRemove(string $user, $id): bool {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -526,6 +672,24 @@ class Database {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Retrieves data about a particular subscription, as an associative array with the following keys:
|
||||||
|
*
|
||||||
|
* - "id": The numeric identifier of the subscription
|
||||||
|
* - "feed": The numeric identifier of the underlying newsfeed
|
||||||
|
* - "url": The URL of the newsfeed, after discovery and HTTP redirects
|
||||||
|
* - "title": The title of the newsfeed
|
||||||
|
* - "favicon": The URL of an icon representing the newsfeed or its source
|
||||||
|
* - "source": The URL of the source of the newsfeed i.e. its parent Web site
|
||||||
|
* - "folder": The numeric identifier (or null) of the subscription's folder
|
||||||
|
* - "top_folder": The numeric identifier (or null) of the top-level folder for the subscription
|
||||||
|
* - "pinned": Whether the subscription is pinned
|
||||||
|
* - "err_count": The count of times attempting to refresh the newsfeed has resulted in an error since the last successful retrieval
|
||||||
|
* - "err_msg": The error message of the last unsuccessful retrieval
|
||||||
|
* - "order_type": Whether articles should be sorted in reverse cronological order (2), chronological order (1), or the default (0)
|
||||||
|
* - "added": The date and time at which the subscription was added
|
||||||
|
* - "updated": The date and time at which the newsfeed was last updated (not when it was last refreshed)
|
||||||
|
* - "unread": The number of unread articles associated with the subscription
|
||||||
|
*/
|
||||||
public function subscriptionPropertiesGet(string $user, $id): array {
|
public function subscriptionPropertiesGet(string $user, $id): array {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -540,6 +704,19 @@ class Database {
|
||||||
return $sub;
|
return $sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Modifies the properties of a subscription
|
||||||
|
*
|
||||||
|
* The $data array must contain one or more of the following keys:
|
||||||
|
*
|
||||||
|
* - "title": The title of the newsfeed
|
||||||
|
* - "folder": The numeric identifier (or null) of the subscription's folder
|
||||||
|
* - "pinned": Whether the subscription is pinned
|
||||||
|
* - "order_type": Whether articles should be sorted in reverse cronological order (2), chronological order (1), or the default (0)
|
||||||
|
*
|
||||||
|
* @param string $user The user whose subscription is to be modified
|
||||||
|
* @param integer|null $id the numeric identifier of the subscription to modfify
|
||||||
|
* @param array $data An associative array of properties to modify; any keys not specified will be left unchanged
|
||||||
|
*/
|
||||||
public function subscriptionPropertiesSet(string $user, $id, array $data): bool {
|
public function subscriptionPropertiesSet(string $user, $id, array $data): bool {
|
||||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
|
@ -580,6 +757,18 @@ class Database {
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Retrieves the URL of the icon for a subscription.
|
||||||
|
*
|
||||||
|
* Note that while the $user parameter is optional, it
|
||||||
|
* is NOT recommended to omit it, as this can lead to
|
||||||
|
* leaks of private information. The parameter is only
|
||||||
|
* optional because this is required for Tiny Tiny RSS,
|
||||||
|
* the original implementation of which leaks private
|
||||||
|
* information due to a design flaw.
|
||||||
|
*
|
||||||
|
* @param integer $id The numeric identifier of the subscription
|
||||||
|
* @param string|null $user The user who owns the subscription being queried
|
||||||
|
*/
|
||||||
public function subscriptionFavicon(int $id, string $user = null): string {
|
public function subscriptionFavicon(int $id, string $user = null): string {
|
||||||
$q = new Query("SELECT favicon from arsse_feeds join arsse_subscriptions on feed = arsse_feeds.id");
|
$q = new Query("SELECT favicon from arsse_feeds join arsse_subscriptions on feed = arsse_feeds.id");
|
||||||
$q->setWhere("arsse_subscriptions.id = ?", "int", $id);
|
$q->setWhere("arsse_subscriptions.id = ?", "int", $id);
|
||||||
|
@ -592,6 +781,14 @@ class Database {
|
||||||
return (string) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue();
|
return (string) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ensures the specified subscription exists and raises an exception otherwise
|
||||||
|
*
|
||||||
|
* Returns an associative array containing the id of the subscription and the id of the underlying newsfeed
|
||||||
|
*
|
||||||
|
* @param string $user The user who owns the subscription to be validated
|
||||||
|
* @param integer|null $id The identifier of the subscription to validate
|
||||||
|
* @param boolean $subject Whether the subscription is the subject rather than the object of the operation being performed; this only affects the semantics of the error message if validation fails
|
||||||
|
*/
|
||||||
protected function subscriptionValidateId(string $user, $id, bool $subject = false): array {
|
protected function subscriptionValidateId(string $user, $id, bool $subject = false): array {
|
||||||
if (!ValueInfo::id($id)) {
|
if (!ValueInfo::id($id)) {
|
||||||
throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "feed", 'type' => "int > 0"]);
|
throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "feed", 'type' => "int > 0"]);
|
||||||
|
|
Loading…
Reference in a new issue