diff --git a/bootstrap.php b/bootstrap.php index fc11202..23d3854 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -8,4 +8,5 @@ const NS_BASE = __NAMESPACE__."\\"; if(!defined(NS_BASE."INSTALL")) define(NS_BASE."INSTALL", false); require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php"; -ignore_user_abort(true); \ No newline at end of file +ignore_user_abort(true); +iconv_set_encoding("internal_encoding", "UTF-8"); \ No newline at end of file diff --git a/composer.json b/composer.json index 1bdf628..87393f7 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,8 @@ ], "require": { "php": "^7.0.0", + "ext-intl": "*", + "ext-iconv": "*", "jkingweb/druuid": "^3.0.0", "phpseclib/phpseclib": "^2.0.4", "webmozart/glob": "^4.1.0", diff --git a/composer.lock b/composer.lock index 54d1b21..069334d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "264437f06f643a1413d45660c2a32124", + "content-hash": "8260cf555776b4ffaef7fca3ca891311", "packages": [ { "name": "fguillot/picofeed", @@ -479,7 +479,9 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.0.0" + "php": "^7.0.0", + "ext-intl": "*", + "ext-iconv": "*" }, "platform-dev": [] } diff --git a/lib/AbstractException.php b/lib/AbstractException.php index 2811f72..bb2dfb4 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -14,22 +14,22 @@ abstract class AbstractException extends \Exception { "Lang/Exception.fileCorrupt" => 10104, "Lang/Exception.stringMissing" => 10105, "Lang/Exception.stringInvalid" => 10106, - "Db/Exception.extMissing" => 10201, - "Db/Exception.fileMissing" => 10202, - "Db/Exception.fileUnusable" => 10203, - "Db/Exception.fileUnreadable" => 10204, - "Db/Exception.fileUnwritable" => 10205, - "Db/Exception.fileUncreatable" => 10206, - "Db/Exception.fileCorrupt" => 10207, + "Db/ExceptionStartup.extMissing" => 10201, + "Db/ExceptionStartup.fileMissing" => 10202, + "Db/ExceptionStartup.fileUnusable" => 10203, + "Db/ExceptionStartup.fileUnreadable" => 10204, + "Db/ExceptionStartup.fileUnwritable" => 10205, + "Db/ExceptionStartup.fileUncreatable" => 10206, + "Db/ExceptionStartup.fileCorrupt" => 10207, "Db/Exception.paramTypeInvalid" => 10401, "Db/Exception.paramTypeUnknown" => 10402, "Db/Exception.paramTypeMissing" => 10403, - "Db/Update/Exception.tooNew" => 10211, - "Db/Update/Exception.fileMissing" => 10212, - "Db/Update/Exception.fileUnusable" => 10213, - "Db/Update/Exception.fileUnreadable" => 10214, - "Db/Update/Exception.manual" => 10215, - "Db/Update/Exception.manualOnly" => 10216, + "Db/ExceptionUpdate.tooNew" => 10211, + "Db/ExceptionUpdate.fileMissing" => 10212, + "Db/ExceptionUpdate.fileUnusable" => 10213, + "Db/ExceptionUpdate.fileUnreadable" => 10214, + "Db/ExceptionUpdate.manual" => 10215, + "Db/ExceptionUpdate.manualOnly" => 10216, "Conf/Exception.fileMissing" => 10302, "Conf/Exception.fileUnusable" => 10303, "Conf/Exception.fileUnreadable" => 10304, diff --git a/lib/Conf.php b/lib/Conf.php index 0c286eb..c373519 100644 --- a/lib/Conf.php +++ b/lib/Conf.php @@ -5,7 +5,7 @@ namespace JKingWeb\NewsSync; class Conf { public $lang = "en"; - public $dbDriver = Db\DriverSQLite3::class; + public $dbDriver = Db\SQLite3\Driver::class; public $dbSQLite3File = BASE."newssync.db"; public $dbSQLite3Key = ""; public $dbSQLite3AutoUpd = true; @@ -23,7 +23,7 @@ class Conf { public $dbMySQLDb = "newssync"; public $dbMySQLAutoUpd = false; - public $userDriver = User\DriverInternal::class; + public $userDriver = User\Internal\Driver::class; public $userAuthPreferHTTP = false; public $userComposeNames = true; public $userTempPasswordLength = 20; diff --git a/lib/Database.php b/lib/Database.php index ed6b9aa..006567e 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -34,14 +34,10 @@ class Database { $sep = \DIRECTORY_SEPARATOR; $path = __DIR__.$sep."Db".$sep; $classes = []; - foreach(glob($path."Driver?*.php") as $file) { - $name = basename($file, ".php"); - if(substr($name,-3) != "PDO") { - $name = NS_BASE."Db\\$name"; - if(class_exists($name)) { - $classes[$name] = $name::driverName(); - } - } + foreach(glob($path."*".$sep."Driver.php") as $file) { + $name = basename(dirname($file)); + $class = NS_BASE."Db\\$name\\Driver"; + $classes[$class] = $class::driverName(); } return $classes; } @@ -328,4 +324,38 @@ class Database { return (bool) $this->db->prepare("DELETE from newssync_subscriptions where id is ?", "int")->run($id)->changes(); } + public function folderAdd(string $user, array $data): int { + // If the user isn't authorized to perform this action then throw an exception. + if (!$this->data->user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } + // If the user doesn't exist throw an exception. + if (!$this->userExists($user)) { + throw new User\Exception("doesNotExist", ["user" => $user, "action" => __FUNCTION__]); + } + // if the desired folder name is missing or invalid, throw an exception + if(!array_key_exists("name", $data)) { + throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "name"]); + } else if(!strlen(trim($data['name']))) { + throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "name"]); + } else if(iconv_strlen($data['name']) > 100) { + throw new Db\ExceptionInput("tooLong", ["action" => __FUNCTION__, "field" => "name", 'max' => 100]); + } + // normalize folder's parent, if there is one + $parent = array_key_exists("parent", $data) ? (int) $data['parent'] : 0; + if($parent===0) { + // if no parent is specified, do nothing + $parent = null; + $root = null; + } else { + // if a parent is specified, make sure it exists and belongs to the user; get its root (first-level) folder if it's a nested folder + $p = $this->db->prepare("SELECT id,root from newssync_folders where owner is ? and id is ?", "str", "int")->run($user, $parent)->get(); + if($p===null) { + throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "parent", 'id' => $parent]); + } else { + // if the parent does not have a root specified (because it is a first-level folder) use the parent ID as the root ID + $root = $p['root']===null ? $parent : $p['root']; + } + } + } } \ No newline at end of file diff --git a/lib/Db/ExceptionInput.php b/lib/Db/ExceptionInput.php new file mode 100644 index 0000000..7aca7a2 --- /dev/null +++ b/lib/Db/ExceptionInput.php @@ -0,0 +1,6 @@ +data = $data; $file = $data->conf->dbSQLite3File; // if the file exists (or we're initializing the database), try to open it and set initial options @@ -20,14 +32,14 @@ class DriverSQLite3 extends AbstractDriver { } catch(\Throwable $e) { // if opening the database doesn't work, check various pre-conditions to find out what the problem might be if(!file_exists($file)) { - if($install && !is_writable(dirname($file))) throw new Exception("fileUncreatable", dirname($file)); - throw new Exception("fileMissing", $file); + if($install && !is_writable(dirname($file))) throw new ExceptionStartup("fileUncreatable", dirname($file)); + throw new ExceptionStartup("fileMissing", $file); } - if(!is_readable($file) && !is_writable($file)) throw new Exception("fileUnusable", $file); - if(!is_readable($file)) throw new Exception("fileUnreadable", $file); - if(!is_writable($file)) throw new Exception("fileUnwritable", $file); + if(!is_readable($file) && !is_writable($file)) throw new ExceptionStartup("fileUnusable", $file); + if(!is_readable($file)) throw new ExceptionStartup("fileUnreadable", $file); + if(!is_writable($file)) throw new ExceptionStartup("fileUnwritable", $file); // otherwise the database is probably corrupt - throw new Exception("fileCorrupt", $mainfile); + throw new ExceptionStartup("fileCorrupt", $mainfile); } } @@ -38,18 +50,17 @@ class DriverSQLite3 extends AbstractDriver { static public function driverName(): string { - $name = str_replace(Driver::class, "", static::class); - return Lang::msg("Driver.Db.$name.Name"); + return Lang::msg("Driver.Db.SQLite3.Name"); } public function schemaVersion(): int { - return $this->query("PRAGMA user_version")->getSingle(); + return $this->query("PRAGMA user_version")->getValue(); } public function schemaUpdate(int $to): bool { $ver = $this->schemaVersion(); - if(!$this->data->conf->dbSQLite3AutoUpd) throw new Update\Exception("manual", ['version' => $ver, 'driver_name' => $this->driverName()]); - if($ver >= $to) throw new Update\Exception("tooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]); + if(!$this->data->conf->dbSQLite3AutoUpd) throw new ExceptionUpdate("manual", ['version' => $ver, 'driver_name' => $this->driverName()]); + if($ver >= $to) throw new ExceptionUpdate("tooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]); $sep = \DIRECTORY_SEPARATOR; $path = \JKingWeb\NewsSync\BASE."sql".$sep."SQLite3".$sep; $this->lock(); @@ -58,10 +69,10 @@ class DriverSQLite3 extends AbstractDriver { $this->begin(); try { $file = $path.$a.".sql"; - if(!file_exists($file)) throw new Update\Exception("fileMissing", ['file' => $file, 'driver_name' => $this->driverName()]); - if(!is_readable($file)) throw new Update\Exception("fileUnreadable", ['file' => $file, 'driver_name' => $this->driverName()]); + if(!file_exists($file)) throw new ExceptionUpdate("fileMissing", ['file' => $file, 'driver_name' => $this->driverName()]); + if(!is_readable($file)) throw new ExceptionUpdate("fileUnreadable", ['file' => $file, 'driver_name' => $this->driverName()]); $sql = @file_get_contents($file); - if($sql===false) throw new Update\Exception("fileUnusable", ['file' => $file, 'driver_name' => $this->driverName()]); + if($sql===false) throw new ExceptionUpdate("fileUnusable", ['file' => $file, 'driver_name' => $this->driverName()]); $this->exec($sql); } catch(\Throwable $e) { // undo any partial changes from the failed update @@ -69,6 +80,7 @@ class DriverSQLite3 extends AbstractDriver { // commit any successful updates if updating by more than one version $this->commit(true); // throw the error received + // FIXME: This should create the relevant type of SQL exception throw $e; } $this->commit(); @@ -82,11 +94,11 @@ class DriverSQLite3 extends AbstractDriver { return (bool) $this->db->exec($query); } - public function query(string $query): Result { - return new ResultSQLite3($this->db->query($query), $this->db->changes()); + public function query(string $query): \JKingWeb\NewsSync\Db\Result { + return new Result($this->db->query($query), $this->db->changes()); } - public function prepareArray(string $query, array $paramTypes): Statement { - return new StatementSQLite3($this->db, $this->db->prepare($query), $paramTypes); + public function prepareArray(string $query, array $paramTypes): \JKingWeb\NewsSync\Db\Statement { + return new Statement($this->db, $this->db->prepare($query), $paramTypes); } } \ No newline at end of file diff --git a/lib/Db/ResultSQLite3.php b/lib/Db/SQLite3/Result.php similarity index 92% rename from lib/Db/ResultSQLite3.php rename to lib/Db/SQLite3/Result.php index 2bcc0b8..36d18e7 100644 --- a/lib/Db/ResultSQLite3.php +++ b/lib/Db/SQLite3/Result.php @@ -1,8 +1,8 @@ st = $statement; //keeps the statement from being destroyed, invalidating the result set $this->set = $result; $this->rows = $changes; diff --git a/lib/Db/StatementSQLite3.php b/lib/Db/SQLite3/Statement.php similarity index 86% rename from lib/Db/StatementSQLite3.php rename to lib/Db/SQLite3/Statement.php index 0052c21..fda5675 100644 --- a/lib/Db/StatementSQLite3.php +++ b/lib/Db/SQLite3/Statement.php @@ -1,8 +1,9 @@ \SQLITE3_NULL, "integer" => \SQLITE3_INTEGER, @@ -38,7 +39,7 @@ class StatementSQLite3 extends AbstractStatement { ])[$part]; } - public function runArray(array $values = null): Result { + public function runArray(array $values = null): \JKingWeb\NewsSync\Db\Result { $this->st->clear(); $l = sizeof($values); for($a = 0; $a < $l; $a++) { @@ -58,6 +59,6 @@ class StatementSQLite3 extends AbstractStatement { // perform binding $this->st->bindValue($a+1, $values[$a], $type); } - return new ResultSQLite3($this->st->execute(), $this->db->changes(), $this); + return new Result($this->st->execute(), $this->db->changes(), $this); } } \ No newline at end of file diff --git a/lib/Db/Update/Exception.php b/lib/Db/Update/Exception.php deleted file mode 100644 index d8c0519..0000000 --- a/lib/Db/Update/Exception.php +++ /dev/null @@ -1,6 +0,0 @@ - Driver::FUNC_INTERNAL, - "userList" => Driver::FUNC_INTERNAL, - "userExists" => Driver::FUNC_INTERNAL, - "userAdd" => Driver::FUNC_INTERNAL, - "userRemove" => Driver::FUNC_INTERNAL, - "userPasswordSet" => Driver::FUNC_INTERNAL, - "userPropertiesGet" => Driver::FUNC_INTERNAL, - "userPropertiesSet" => Driver::FUNC_INTERNAL, - "userRightsGet" => Driver::FUNC_INTERNAL, - "userRightsSet" => Driver::FUNC_INTERNAL, - ]; - - static public function create(\JKingWeb\NewsSync\RuntimeData $data): Driver { - return new static($data); - } - - static public function driverName(): string { - $name = str_replace(Driver::class, "", static::class); - return Lang::msg("Driver.User.$name.Name"); - } - - public function driverFunctions(string $function = null) { - if($function===null) return $this->functions; - if(array_key_exists($function, $this->functions)) { - return $this->functions[$function]; - } else { - return Driver::FUNC_NOT_IMPLEMENTED; - } - } - - // see InternalFunctions.php for bulk of methods -} \ No newline at end of file diff --git a/lib/User/Internal/Driver.php b/lib/User/Internal/Driver.php new file mode 100644 index 0000000..7526edf --- /dev/null +++ b/lib/User/Internal/Driver.php @@ -0,0 +1,43 @@ + Iface::FUNC_INTERNAL, + "userList" => Iface::FUNC_INTERNAL, + "userExists" => Iface::FUNC_INTERNAL, + "userAdd" => Iface::FUNC_INTERNAL, + "userRemove" => Iface::FUNC_INTERNAL, + "userPasswordSet" => Iface::FUNC_INTERNAL, + "userPropertiesGet" => Iface::FUNC_INTERNAL, + "userPropertiesSet" => Iface::FUNC_INTERNAL, + "userRightsGet" => Iface::FUNC_INTERNAL, + "userRightsSet" => Iface::FUNC_INTERNAL, + ]; + + static public function create(\JKingWeb\NewsSync\RuntimeData $data): Driver { + return new static($data); + } + + static public function driverName(): string { + return Lang::msg("Driver.User.Internal.Name"); + } + + public function driverFunctions(string $function = null) { + if($function===null) return $this->functions; + if(array_key_exists($function, $this->functions)) { + return $this->functions[$function]; + } else { + return Iface::FUNC_NOT_IMPLEMENTED; + } + } + + // see InternalFunctions.php for bulk of methods +} \ No newline at end of file diff --git a/lib/User/InternalFunctions.php b/lib/User/Internal/InternalFunctions.php similarity index 97% rename from lib/User/InternalFunctions.php rename to lib/User/Internal/InternalFunctions.php index 6ea37cd..06cda34 100644 --- a/lib/User/InternalFunctions.php +++ b/lib/User/Internal/InternalFunctions.php @@ -1,6 +1,6 @@ 'Insufficient permissions to overwrite configuration file "{0}"', 'Exception.JKingWeb/NewsSync/Conf/Exception.fileCorrupt' => 'Configuration file "{0}" is corrupt or does not conform to expected format', - 'Exception.JKingWeb/NewsSync/Db/Exception.extMissing' => 'Required PHP extension for driver "{0}" not installed', - 'Exception.JKingWeb/NewsSync/Db/Exception.fileMissing' => 'Database file "{0}" does not exist', - 'Exception.JKingWeb/NewsSync/Db/Exception.fileUnreadable' => 'Insufficient permissions to open database file "{0}" for reading', - 'Exception.JKingWeb/NewsSync/Db/Exception.fileUnwritable' => 'Insufficient permissions to open database file "{0}" for writing', - 'Exception.JKingWeb/NewsSync/Db/Exception.fileUnusable' => 'Insufficient permissions to open database file "{0}" for reading or writing', - 'Exception.JKingWeb/NewsSync/Db/Exception.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"', - 'Exception.JKingWeb/NewsSync/Db/Exception.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database', + 'Exception.JKingWeb/NewsSync/Db/ExceptionStartup.extMissing' => 'Required PHP extension for driver "{0}" not installed', + 'Exception.JKingWeb/NewsSync/Db/ExceptionStartup.fileMissing' => 'Database file "{0}" does not exist', + 'Exception.JKingWeb/NewsSync/Db/ExceptionStartup.fileUnreadable' => 'Insufficient permissions to open database file "{0}" for reading', + 'Exception.JKingWeb/NewsSync/Db/ExceptionStartup.fileUnwritable' => 'Insufficient permissions to open database file "{0}" for writing', + 'Exception.JKingWeb/NewsSync/Db/ExceptionStartup.fileUnusable' => 'Insufficient permissions to open database file "{0}" for reading or writing', + 'Exception.JKingWeb/NewsSync/Db/ExceptionStartup.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"', + 'Exception.JKingWeb/NewsSync/Db/ExceptionStartup.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database', 'Exception.JKingWeb/NewsSync/Db/Exception.paramTypeInvalid' => 'Prepared statement parameter type "{0}" is invalid', 'Exception.JKingWeb/NewsSync/Db/Exception.paramTypeUnknown' => 'Prepared statement parameter type "{0}" is valid, but not implemented', 'Exception.JKingWeb/NewsSync/Db/Exception.paramTypeMissing' => 'Prepared statement parameter type for parameter #{0} was not specified', - 'Exception.JKingWeb/NewsSync/Db/Update/Exception.manual' => + 'Exception.JKingWeb/NewsSync/Db/ExceptionUpdate.manual' => '{from_version, select, 0 {{driver_name} database is configured for manual updates and is not initialized; please populate the database with the base schema} other {{driver_name} database is configured for manual updates; please update from schema version {current} to version {target}} }', - 'Exception.JKingWeb/NewsSync/Db/Update/Exception.manualOnly' => + 'Exception.JKingWeb/NewsSync/Db/ExceptionUpdate.manualOnly' => '{from_version, select, 0 {{driver_name} database must be updated manually and is not initialized; please populate the database with the base schema} other {{driver_name} database must be updated manually; please update from schema version {current} to version {target}} }', - 'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileMissing' => 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} not being available', - 'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileUnreadable' => 'Automatic updating of the {driver_name} database failed due to insufficient permissions to read instructions for updating from version {current}', - 'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileUnusable' => 'Automatic updating of the {driver_name} database failed due to an error reading instructions for updating from version {current}', - 'Exception.JKingWeb/NewsSync/Db/Update/Exception.tooNew' => + 'Exception.JKingWeb/NewsSync/Db/ExceptionUpdate.fileMissing' => 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} not being available', + 'Exception.JKingWeb/NewsSync/Db/ExceptionUpdate.fileUnreadable' => 'Automatic updating of the {driver_name} database failed due to insufficient permissions to read instructions for updating from version {current}', + 'Exception.JKingWeb/NewsSync/Db/ExceptionUpdate.fileUnusable' => 'Automatic updating of the {driver_name} database failed due to an error reading instructions for updating from version {current}', + 'Exception.JKingWeb/NewsSync/Db/ExceptionUpdate.tooNew' => '{difference, select, 0 {Automatic updating of the {driver_name} database failed because it is already up to date with the requested version, {target}} other {Automatic updating of the {driver_name} database failed because its version, {current}, is newer than the requested version, {target}} diff --git a/sql/SQLite3/0.sql b/sql/SQLite3/0.sql index 59c38bb..c286bf0 100644 --- a/sql/SQLite3/0.sql +++ b/sql/SQLite3/0.sql @@ -1,3 +1,23 @@ +-- settings +create table newssync_settings( + key varchar(255) primary key not null, -- setting key + value varchar(255), -- setting value, serialized as a string + type varchar(255) not null check( + type in('int','numeric','text','timestamp','date','time','bool','null','json') + ) default 'text' -- the deserialized type of the value +); + +-- users +create table newssync_users( + id TEXT primary key not null, -- user id + password TEXT, -- password, salted and hashed; if using external authentication this would be blank + name TEXT, -- display name + avatar_url TEXT, -- external URL to avatar + avatar_type TEXT, -- internal avatar image's MIME content type + avatar_data BLOB, -- internal avatar image's binary data + rights integer not null default 0 -- any administrative rights the user may have +); + -- newsfeeds, deduplicated create table newssync_feeds( id integer primary key not null, -- sequence number @@ -15,6 +35,31 @@ create table newssync_feeds( unique(url,username,password) -- a URL with particular credentials should only appear once ); +-- users' subscriptions to newsfeeds, with settings +create table newssync_subscriptions( + id integer primary key not null, -- sequence number + owner TEXT not null references newssync_users(id) on delete cascade on update cascade, -- owner of subscription + feed integer not null references newssync_feeds(id) on delete cascade, -- feed for the subscription + added datetime not null default CURRENT_TIMESTAMP, -- time at which feed was added + modified datetime not null default CURRENT_TIMESTAMP, -- date at which subscription properties were last modified + title TEXT, -- user-supplied title + order_type int not null default 0, -- ownCloud sort order + pinned boolean not null default 0, -- whether feed is pinned (always sorts at top) + folder integer references newssync_folders(id) on delete set null, -- TT-RSS category (nestable); the first-level category (which acts as ownCloud folder) is joined in when needed + unique(owner,feed) -- a given feed should only appear once for a given owner +); + +-- TT-RSS categories and ownCloud folders +create table newssync_folders( + id integer primary key not null, -- sequence number + owner TEXT not null references newssync_users(id) on delete cascade on update cascade, -- owner of folder + parent integer not null default 0, -- parent folder id + root integer not null default 0, -- first-level folder (ownCloud folder) + name TEXT not null, -- folder name + modified datetime not null default CURRENT_TIMESTAMP, -- + unique(owner,name,parent) -- cannot have multiple folders with the same name under the same parent for the same owner +); + -- entries in newsfeeds create table newssync_articles( id integer primary key not null, -- sequence number @@ -40,57 +85,6 @@ create table newssync_enclosures( type varchar(255) ); --- author labels ("categories" in RSS/Atom parlance) associated with newsfeed entries -create table newssync_tags( - article integer not null references newssync_articles(id) on delete cascade, - name TEXT -); - --- settings -create table newssync_settings( - key varchar(255) primary key not null, -- - value varchar(255), -- - type varchar(255) not null check( - type in('int','numeric','text','timestamp','date','time','bool','null','json') - ) default 'text' -- -); - --- users -create table newssync_users( - id TEXT primary key not null, -- user id - password TEXT, -- password, salted and hashed; if using external authentication this would be blank - name TEXT, -- display name - avatar_url TEXT, -- external URL to avatar - avatar_type TEXT, -- internal avatar image's MIME content type - avatar_data BLOB, -- internal avatar image's binary data - rights integer not null default 0 -- any administrative rights the user may have -); - --- TT-RSS categories and ownCloud folders -create table newssync_categories( - id integer primary key not null, -- sequence number - owner TEXT not null references newssync_users(id) on delete cascade on update cascade, -- owner of category - parent integer, -- parent category id - folder integer not null, -- first-level category (ownCloud folder) - name TEXT not null, -- category name - modified datetime not null default CURRENT_TIMESTAMP, -- - unique(owner,name,parent) -- cannot have multiple categories with the same name under the same parent for the same owner -); - --- users' subscriptions to newsfeeds, with settings -create table newssync_subscriptions( - id integer primary key not null, -- sequence number - owner TEXT not null references newssync_users(id) on delete cascade on update cascade, -- owner of subscription - feed integer not null references newssync_feeds(id) on delete cascade, -- feed for the subscription - added datetime not null default CURRENT_TIMESTAMP, -- time at which feed was added - modified datetime not null default CURRENT_TIMESTAMP, -- date at which subscription properties were last modified - title TEXT, -- user-supplied title - order_type int not null default 0, -- ownCloud sort order - pinned boolean not null default 0, -- whether feed is pinned (always sorts at top) - category integer references newssync_categories(id) on delete set null, -- TT-RSS category (nestable); the first-level category (which acts as ownCloud folder) is joined in when needed - unique(owner,feed) -- a given feed should only appear once for a given owner -); - -- users' actions on newsfeed entries create table newssync_subscription_articles( id integer primary key not null, @@ -108,6 +102,12 @@ create table newssync_labels( ); create index newssync_label_names on newssync_labels(name); +-- author labels ("categories" in RSS/Atom parlance) associated with newsfeed entries +create table newssync_tags( + article integer not null references newssync_articles(id) on delete cascade, + name TEXT +); + -- set version marker pragma user_version = 1; insert into newssync_settings values('schema_version',1,'int'); \ No newline at end of file diff --git a/tests/Db/SQLite3/TestDbResultSQLite3.php b/tests/Db/SQLite3/TestDbResultSQLite3.php index b16cd45..8954a6b 100644 --- a/tests/Db/SQLite3/TestDbResultSQLite3.php +++ b/tests/Db/SQLite3/TestDbResultSQLite3.php @@ -21,20 +21,20 @@ class TestDbResultSQLite3 extends \PHPUnit\Framework\TestCase { function testConstructResult() { $set = $this->c->query("SELECT 1"); - $this->assertInstanceOf(Db\ResultSQLite3::class, new Db\ResultSQLite3($set)); + $this->assertInstanceOf(Db\Result::class, new Db\SQLite3\Result($set)); } function testGetChangeCount() { $this->c->query("CREATE TABLE test(col)"); $set = $this->c->query("INSERT INTO test(col) values(1)"); $rows = $this->c->changes(); - $this->assertEquals($rows, (new Db\ResultSQLite3($set,$rows))->changes()); + $this->assertEquals($rows, (new Db\SQLite3\Result($set,$rows))->changes()); } function testIterateOverResults() { $set = $this->c->query("SELECT 1 as col union select 2 as col union select 3 as col"); $rows = []; - foreach(new Db\ResultSQLite3($set) as $row) { + foreach(new Db\SQLite3\Result($set) as $row) { $rows[] = $row['col']; } $this->assertEquals([1,2,3], $rows); @@ -43,7 +43,7 @@ class TestDbResultSQLite3 extends \PHPUnit\Framework\TestCase { function testIterateOverResultsTwice() { $set = $this->c->query("SELECT 1 as col union select 2 as col union select 3 as col"); $rows = []; - $test = new Db\ResultSQLite3($set); + $test = new Db\SQLite3\Result($set); foreach($test as $row) { $rows[] = $row['col']; } @@ -55,7 +55,7 @@ class TestDbResultSQLite3 extends \PHPUnit\Framework\TestCase { function testGetSingleValues() { $set = $this->c->query("SELECT 1867 as year union select 1970 as year union select 2112 as year"); - $test = new Db\ResultSQLite3($set); + $test = new Db\SQLite3\Result($set); $this->assertEquals(1867, $test->getValue()); $this->assertEquals(1970, $test->getValue()); $this->assertEquals(2112, $test->getValue()); @@ -64,7 +64,7 @@ class TestDbResultSQLite3 extends \PHPUnit\Framework\TestCase { function testGetFirstValuesOnly() { $set = $this->c->query("SELECT 1867 as year, 19 as century union select 1970 as year, 20 as century union select 2112 as year, 22 as century"); - $test = new Db\ResultSQLite3($set); + $test = new Db\SQLite3\Result($set); $this->assertEquals(1867, $test->getValue()); $this->assertEquals(1970, $test->getValue()); $this->assertEquals(2112, $test->getValue()); @@ -77,7 +77,7 @@ class TestDbResultSQLite3 extends \PHPUnit\Framework\TestCase { ['album' => '2112', 'track' => '2112'], ['album' => 'Clockwork Angels', 'track' => 'The Wreckers'], ]; - $test = new Db\ResultSQLite3($set); + $test = new Db\SQLite3\Result($set); $this->assertEquals($rows[0], $test->get()); $this->assertEquals($rows[1], $test->get()); $this->assertSame(null, $test->get()); diff --git a/tests/Db/SQLite3/TestDbStatementSQLite3.php b/tests/Db/SQLite3/TestDbStatementSQLite3.php index 8e48dd9..ab05585 100644 --- a/tests/Db/SQLite3/TestDbStatementSQLite3.php +++ b/tests/Db/SQLite3/TestDbStatementSQLite3.php @@ -8,7 +8,7 @@ class TestDbStatementSQLite3 extends \PHPUnit\Framework\TestCase { use Test\Tools, Test\Db\BindingTests; protected $c; - static protected $imp = Db\StatementSQLite3::class; + static protected $imp = Db\SQLite3\Statement::class; function setUp() { date_default_timezone_set("UTC"); @@ -36,7 +36,7 @@ class TestDbStatementSQLite3 extends \PHPUnit\Framework\TestCase { function testConstructStatement() { $nativeStatement = $this->c->prepare("SELECT ? as value"); - $this->assertInstanceOf(Db\StatementSQLite3::class, new Db\StatementSQLite3($this->c, $nativeStatement)); + $this->assertInstanceOf(Statement::class, new Db\SQLite3\Statement($this->c, $nativeStatement)); } function testBindMissingValue() { diff --git a/tests/User/TestUserInternalDriver.php b/tests/User/TestUserInternalDriver.php index e47bd7f..a0b2d05 100644 --- a/tests/User/TestUserInternalDriver.php +++ b/tests/User/TestUserInternalDriver.php @@ -12,7 +12,7 @@ class TestUserInternalDriver extends \PHPUnit\Framework\TestCase { protected $data; function setUp() { - $drv = User\DriverInternal::class; + $drv = User\Internal\Driver::class; $conf = new Conf(); $conf->userDriver = $drv; $conf->userAuthPreferHTTP = true;