From 6ffe942f99a4a3a306914b59749439736b7861ff Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 5 Oct 2016 22:08:43 -0400 Subject: [PATCH] SQLite3 database driver in working condition PDO stub for now; other drivers to come --- .gitignore | 2 +- schema.sql | 130 +++++++++--------- .../Auth/{AuthInterface.php => Driver.php} | 2 +- .../JKingWeb/NewsSync/Auth/DriverInternal.php | 2 +- vendor/JKingWeb/NewsSync/Conf.php | 2 +- vendor/JKingWeb/NewsSync/Database.php | 23 +++- vendor/JKingWeb/NewsSync/Db/Common.php | 47 +++++++ vendor/JKingWeb/NewsSync/Db/CommonPDO.php | 17 +++ vendor/JKingWeb/NewsSync/Db/CommonSQLite3.php | 18 +++ vendor/JKingWeb/NewsSync/Db/Driver.php | 15 ++ .../JKingWeb/NewsSync/Db/DriverInterface.php | 7 - vendor/JKingWeb/NewsSync/Db/DriverSQLite3.php | 81 +++++++---- .../JKingWeb/NewsSync/Db/DriverSQLite3PDO.php | 28 ++++ vendor/JKingWeb/NewsSync/Db/Result.php | 9 ++ vendor/JKingWeb/NewsSync/Db/ResultSQLite3.php | 30 ++++ vendor/JKingWeb/NewsSync/Db/Statement.php | 9 ++ .../JKingWeb/NewsSync/Db/StatementSQLite3.php | 62 +++++++++ vendor/JKingWeb/NewsSync/Exception.php | 2 +- vendor/JKingWeb/NewsSync/RuntimeData.php | 6 +- 19 files changed, 380 insertions(+), 112 deletions(-) rename vendor/JKingWeb/NewsSync/Auth/{AuthInterface.php => Driver.php} (88%) create mode 100644 vendor/JKingWeb/NewsSync/Db/Common.php create mode 100644 vendor/JKingWeb/NewsSync/Db/CommonPDO.php create mode 100644 vendor/JKingWeb/NewsSync/Db/CommonSQLite3.php create mode 100644 vendor/JKingWeb/NewsSync/Db/Driver.php delete mode 100644 vendor/JKingWeb/NewsSync/Db/DriverInterface.php create mode 100644 vendor/JKingWeb/NewsSync/Db/DriverSQLite3PDO.php create mode 100644 vendor/JKingWeb/NewsSync/Db/Result.php create mode 100644 vendor/JKingWeb/NewsSync/Db/ResultSQLite3.php create mode 100644 vendor/JKingWeb/NewsSync/Db/Statement.php create mode 100644 vendor/JKingWeb/NewsSync/Db/StatementSQLite3.php diff --git a/.gitignore b/.gitignore index 51fc87aa..8362fb1c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ vendor/simplepie/* #temp files cache/* test.php -newssync.db* +/db # Windows image file caches Thumbs.db diff --git a/schema.sql b/schema.sql index 8cab2e61..0c9e51f7 100644 --- a/schema.sql +++ b/schema.sql @@ -1,109 +1,111 @@ begin; +create table main.newssync_settings( + key varchar(255) primary key not null, -- + value varchar(255), -- + type varchar(255) not null check( + type in('numeric','text','timestamp', 'date', 'time', 'bool') + ) -- +); +insert into main.newssync_settings values('schema_version',1,'int'); + -- 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_type TEXT, -- avatar image's MIME content type - avatar_data BLOB, -- avatar image's binary data - admin boolean not null default 0 -- whether the user is an administrator +create table main.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_type TEXT, -- avatar image's MIME content type + avatar_data BLOB, -- avatar image's binary data + admin boolean not null default 0 -- whether the user is an administrator ); -- TT-RSS categories and ownCloud folders -create table newssync_categories( - id integer primary key not null, -- sequence number - owner TEXT references 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 +create table main.newssync_categories( + id integer primary key not null, -- sequence number + owner TEXT not null references 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 ); -- newsfeeds, deduplicated -create table newssync_feeds( - id integer primary key not null, -- sequence number - url TEXT not null, -- URL of feed - title TEXT, -- default title of feed - favicon TEXT, -- URL of favicon - source TEXT, -- URL of site to which the feed belongs - updated datetime, -- time at which the feed was last fetched - modified datetime not null default CURRENT_TIMESTAMP, -- - err_count integer not null default 0, -- count of successive times update resulted in error since last successful update - err_msg TEXT, -- last error message - username TEXT, -- HTTP authentication username - password TEXT, -- HTTP authentication password (this is stored in plain text) - unique(url,username,password) -- a URL with particular credentials should only appear once +create table feeds.newssync_feeds( + id integer primary key not null, -- sequence number + url TEXT not null, -- URL of feed + title TEXT, -- default title of feed + favicon TEXT, -- URL of favicon + source TEXT, -- URL of site to which the feed belongs + updated datetime, -- time at which the feed was last fetched + modified datetime not null default CURRENT_TIMESTAMP, -- + err_count integer not null default 0, -- count of successive times update resulted in error since last successful update + err_msg TEXT, -- last error message + username TEXT, -- HTTP authentication username + password TEXT, -- HTTP authentication password (this is stored in plain text) + unique(url,username,password) -- a URL with particular credentials should only appear once ); -- users' subscriptions to newsfeeds, with settings -create table newssync_subscriptions( +create table main.newssync_subscriptions( id integer primary key not null, -- sequence number - owner TEXT references users(id) on delete cascade on update cascade, -- owner of subscription - feed integer references feeds(id) on delete cascade, -- feed for the subscription + owner TEXT not null references users(id) on delete cascade on update cascade, -- owner of subscription + feed integer not null references 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 categories(id) on delete set null, -- TT-RSS category (nestable); the first-level category (which acts as ownCloud folder) is joined in when needed + category integer not null references 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 ); -- entries in newsfeeds -create table newssync_articles( - id integer primary key not null, -- sequence number - feed integer references feeds(id) on delete cascade, -- feed for the subscription - url TEXT not null, -- URL of article - title TEXT, -- article title - author TEXT, -- author's name - published datetime, -- time of original publication - edited datetime, -- time of last edit - guid TEXT, -- GUID - content TEXT, -- content, as (X)HTML - modified datetime not null default CURRENT_TIMESTAMP, -- date when article properties were last modified - hash varchar(64) not null, -- ownCloud hash - fingerprint varchar(64) not null, -- ownCloud fingerprint - enclosures_hash varchar(64), -- hash of enclosures, if any; since enclosures are not uniquely identified, we need to know when they change - tags_hash varchar(64) -- hash of RSS/Atom categories included in article; since these categories are not uniquely identified, we need to know when they change +create table feeds.newssync_articles( + id integer primary key not null, -- sequence number + feed integer not null references feeds(id) on delete cascade, -- feed for the subscription + url TEXT not null, -- URL of article + title TEXT, -- article title + author TEXT, -- author's name + published datetime, -- time of original publication + edited datetime, -- time of last edit + guid TEXT, -- GUID + content TEXT, -- content, as (X)HTML + modified datetime not null default CURRENT_TIMESTAMP, -- date when article properties were last modified + hash varchar(64) not null, -- ownCloud hash + fingerprint varchar(64) not null, -- ownCloud fingerprint + enclosures_hash varchar(64), -- hash of enclosures, if any; since enclosures are not uniquely identified, we need to know when they change + tags_hash varchar(64) -- hash of RSS/Atom categories included in article; since these categories are not uniquely identified, we need to know when they change ); -- users' actions on newsfeed entries -create table newssync_subscription_articles( +create table main.newssync_subscription_articles( id integer primary key not null, - article integer references articles(id) on delete cascade, + article integer not null references articles(id) on delete cascade, read boolean not null default 0, starred boolean not null default 0, modified datetime not null default CURRENT_TIMESTAMP ); -- enclosures associated with articles -create table newssync_enclosures( - article integer references articles(id) on delete cascade, +create table main.newssync_enclosures( + article integer not null references articles(id) on delete cascade, url TEXT, type varchar(255) ); -- author labels ("categories" in RSS/Atom parlance) associated with newsfeed entries -create table newssync_tags( - article integer references articles(id) on delete cascade, +create table main.newssync_tags( + article integer not null references articles(id) on delete cascade, name TEXT ); -- user labels associated with newsfeed entries -create table newssync_labels( - sub_article integer references subscription_articles(id) on delete cascade, - owner TEXT references users(id) on delete cascade on update cascade, +create table main.newssync_labels( + sub_article integer not null references subscription_articles(id) on delete cascade, + owner TEXT not null references users(id) on delete cascade on update cascade, name TEXT ); -create index newssync_label_names on newssync_labels(name); - -create table newssync_settings( - key varchar(255) primary key not null, - value varchar(255), - type varchar(255) not null -); -insert into newssync_settings values('schema_version',0,'int'); +create index main.newssync_label_names on newssync_labels(name); commit; \ No newline at end of file diff --git a/vendor/JKingWeb/NewsSync/Auth/AuthInterface.php b/vendor/JKingWeb/NewsSync/Auth/Driver.php similarity index 88% rename from vendor/JKingWeb/NewsSync/Auth/AuthInterface.php rename to vendor/JKingWeb/NewsSync/Auth/Driver.php index 59928c87..1a676f7e 100644 --- a/vendor/JKingWeb/NewsSync/Auth/AuthInterface.php +++ b/vendor/JKingWeb/NewsSync/Auth/Driver.php @@ -2,7 +2,7 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\Auth; -Interface AuthInterface { +Interface Driver { public function __construct($conf, $db); public function auth(): bool; public function authHTTP(): bool; diff --git a/vendor/JKingWeb/NewsSync/Auth/DriverInternal.php b/vendor/JKingWeb/NewsSync/Auth/DriverInternal.php index 62f06bec..050b51ec 100644 --- a/vendor/JKingWeb/NewsSync/Auth/DriverInternal.php +++ b/vendor/JKingWeb/NewsSync/Auth/DriverInternal.php @@ -2,7 +2,7 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\Auth; -class Internal implements AuthInterface { +class Internal implements Driver { protected $conf; protected $db; diff --git a/vendor/JKingWeb/NewsSync/Conf.php b/vendor/JKingWeb/NewsSync/Conf.php index a781c33a..b5ab0ec3 100644 --- a/vendor/JKingWeb/NewsSync/Conf.php +++ b/vendor/JKingWeb/NewsSync/Conf.php @@ -6,7 +6,7 @@ class Conf { public $lang = "en"; public $dbClass = NS_BASE."Db\\DriverSQLite3"; - public $dbSQLite3File = BASE."newssync.db"; + public $dbSQLite3Path = BASE."db"; public $dbSQLite3Key = ""; public $dbPostgreSQLHost = "localhost"; public $dbPostgreSQLUser = "newssync"; diff --git a/vendor/JKingWeb/NewsSync/Database.php b/vendor/JKingWeb/NewsSync/Database.php index 7b897ede..2acc73e7 100644 --- a/vendor/JKingWeb/NewsSync/Database.php +++ b/vendor/JKingWeb/NewsSync/Database.php @@ -1,4 +1,5 @@ dbClass; - $this->drv = new $driver($conf); + $this->drv = $driver::create($conf); } - static public function listDrivers() { - + static public function listDrivers(): array { + $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(); + } + } + } + return $classes; + } + + public function schemaVersion(): int { + return $this->drv->schemaVersion(); } } \ No newline at end of file diff --git a/vendor/JKingWeb/NewsSync/Db/Common.php b/vendor/JKingWeb/NewsSync/Db/Common.php new file mode 100644 index 00000000..ad44a941 --- /dev/null +++ b/vendor/JKingWeb/NewsSync/Db/Common.php @@ -0,0 +1,47 @@ +transDepth==0) { + $this->exec("BEGIN TRANSACTION"); + } else{ + $this->exec("SAVEPOINT newssync_".$this->transDepth); + } + $this->transDepth += 1; + return true; + } + + public function commit(bool $all = false): bool { + if($this->transDepth==0) return false; + if(!$all) { + $this->exec("RELEASE SAVEPOINT newssync_".$this->transDepth-1); + $this->transDepth -= 1; + } else { + $this->exec("COMMIT TRANSACTION"); + $this->transDepth = 0; + } + return true; + } + + public function rollback(bool $all = false): bool { + if($this->transDepth==0) return false; + if(!$all) { + $this->exec("ROLLBACK TRANSACTION TO SAVEPOINT newssync_".$this->transDepth-1); + $this->transDepth -= 1; + if($this->transDepth==0) $this->exec("ROLLBACK TRANSACTION"); + } else { + $this->exec("ROLLBACK TRANSACTION"); + $this->transDepth = 0; + } + return true; + } + + public function prepare(string $query, string ...$paramType): Statement { + return $this->prepareArray($query, $paramType); + } + +} \ No newline at end of file diff --git a/vendor/JKingWeb/NewsSync/Db/CommonPDO.php b/vendor/JKingWeb/NewsSync/Db/CommonPDO.php new file mode 100644 index 00000000..5e48f2c9 --- /dev/null +++ b/vendor/JKingWeb/NewsSync/Db/CommonPDO.php @@ -0,0 +1,17 @@ +db->query($query)); + } + + public function prepareArray(string $query, array $paramTypes): Statement { + return new StatementPDO($query, $paramTypes); + } + + public function prepare(string $query, string ...$paramType): Statement { + return $this->prepareArray($query, $paramType); + } +} \ No newline at end of file diff --git a/vendor/JKingWeb/NewsSync/Db/CommonSQLite3.php b/vendor/JKingWeb/NewsSync/Db/CommonSQLite3.php new file mode 100644 index 00000000..a6d1f063 --- /dev/null +++ b/vendor/JKingWeb/NewsSync/Db/CommonSQLite3.php @@ -0,0 +1,18 @@ +unsafeQuery("PRAGMA $schema.user_version")->getSingle(); + } + + public function exec(string $query): bool { + return (bool) $this->db->exec($query); + } +} \ No newline at end of file diff --git a/vendor/JKingWeb/NewsSync/Db/Driver.php b/vendor/JKingWeb/NewsSync/Db/Driver.php new file mode 100644 index 00000000..c1be5fd2 --- /dev/null +++ b/vendor/JKingWeb/NewsSync/Db/Driver.php @@ -0,0 +1,15 @@ +dbSQLite3Path; + $sep = \DIRECTORY_SEPARATOR; + if(substr($path,-(strlen($sep))) != $sep) $path .= $sep; + $mainfile = $path."newssync-main.db"; + $feedfile = $path."newssync-feeds.db"; + // if the files exists (or we're initializing the database), try to open it and set initial options + try { + $this->db = new \SQLite3($mainfile, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, $conf->dbSQLite3Key); + $this->db->enableExceptions(true); + $attach = "'".$this->db->escapeString($feedfile)."'"; + $this->exec("ATTACH DATABASE $attach AS feeds"); + $this->exec("PRAGMA main.jounral_mode = wal"); + $this->exec("PRAGMA feeds.jounral_mode = wal"); + $this->exec("PRAGMA foreign_keys = yes"); + } catch(\Throwable $e) { + // if opening the database doesn't work, check various pre-conditions to find out what the problem might be + foreach([$mainfile, $mainfile."-wal", $mainfile."-shm", $feedfile, $feedfile."-wal", $feedfile."-shm"] as $file) { + if(!file_exists($file)) { + if($install && !is_writable(dirname($file))) throw new Exception("fileUncreatable", dirname($file)); + throw new Exception("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); + } + // otherwise the database is probably corrupt + throw new Exception("fileCorrupt", $mainfile); + } + } + + public function __destruct() { + $this->db->close(); + unset($this->db); + } + + static public function create(\JKingWeb\NewsSync\Conf $conf, bool $install = false): Driver { // check to make sure required extensions are loaded if(class_exists("SQLite3")) { - $this->pdo = false; + return new self($conf, $install); } else if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) { - $this->pdo = true; + return new DriverSQLite3PDO($conf, $install); } else { throw new Exception("extMissing", self::driverName()); } - // if the file exists (or we're initializing the database), try to open it and set initial options - if((!$install && file_exists($conf->dbSQLite3File)) || $install) { - try { - $this->db = ($this->PDO) ? (new \SQLite3($conf->dbSQLite3File, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $conf->dbSQLite3Key)) : (new PDO("sqlite:".$conf->dbSQLite3File)); - //FIXME: add foreign key enforcement, WAL mode - } catch(\Throwable $e) { - // if opening the database doesn't work, check various pre-conditions to find out what the problem might be - foreach([$conf->dbSQLite3File, $conf->dbSQLite3File."-wal", $conf->dbSQLite3File."-shm"] as $file) { - if(!file_exists($file)) { - if($install && !is_writable(dirname($file))) throw new Exception("fileUncreatable", dirname($file)); - throw new Exception("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); - } - // otherwise the database is probably corrupt - throw new Exception("fileCorrupt", $conf->dbSQLite3File); - } - } else { - throw new Exception("fileMissing", $conf->dbSQLite3File); - } } - static public function driverName(): string { - return "SQLite3"; + public function unsafeQuery(string $query): Result { + return new ResultSQLite3($this->db->query($query)); + } + + public function prepareArray(string $query, array $paramTypes): Statement { + return new StatementSQLite3($query, $paramTypes); } } \ No newline at end of file diff --git a/vendor/JKingWeb/NewsSync/Db/DriverSQLite3PDO.php b/vendor/JKingWeb/NewsSync/Db/DriverSQLite3PDO.php new file mode 100644 index 00000000..08a78953 --- /dev/null +++ b/vendor/JKingWeb/NewsSync/Db/DriverSQLite3PDO.php @@ -0,0 +1,28 @@ +set = $resultObj; + } + + public function __destruct() { + $this->set->finalize(); + unset($this->set); + } + + public function __invoke() { + return $this->get(); + } + + public function get() { + return $this->set->fetchArray(\SQLITE3_ASSOC); + } + + public function getSingle() { + $res = $this->get(); + if($res===FALSE) return null; + return array_shift($res); + } +} \ No newline at end of file diff --git a/vendor/JKingWeb/NewsSync/Db/Statement.php b/vendor/JKingWeb/NewsSync/Db/Statement.php new file mode 100644 index 00000000..da2c3ec8 --- /dev/null +++ b/vendor/JKingWeb/NewsSync/Db/Statement.php @@ -0,0 +1,9 @@ +st = $st; + $this->types = []; + foreach($bindings as $binding) { + switch(trim(strtolower($binding))) { + case "int": + case "integer": + $this->types[] = \SQLITE3_INTEGER; break; + case "float": + case "double": + case "real": + case "numeric": + $this->types[] = \SQLITE3_FLOAT; break; + case "blob": + case "bin": + case "binary": + $this->types[] = \SQLITE3_BLOB; break; + case "text": + case "string": + case "str": + $this->types[] = \SQLITE3_TEXT; break; + default: + $this->types[] = \SQLITE3_TEXT; break; + } + } + } + + public function __destruct() { + $this->st->close(); + unset($this->st); + } + + public function __invoke(&...$values) { + return $this->runArray($values); + } + + public function run(&...$values): Result { + return $this->runArray($values); + } + + public function runArray(array &$values = null): Result { + $this->st->clear(); + $l = sizeof($values); + for($a = 0; $a < $l; $a++) { + if($values[$a]===null) { + $type = \SQLITE3_NULL; + } else { + $type = (array_key_exists($a,$this->types)) ? $this->types[$a] : \SQLITE3_TEXT; + } + $st->bindParam($a+1, $values[$a], $type); + } + return new ResultSQLite3($st->execute()); + } +} \ No newline at end of file diff --git a/vendor/JKingWeb/NewsSync/Exception.php b/vendor/JKingWeb/NewsSync/Exception.php index 9bdc4dc0..6fecba0f 100644 --- a/vendor/JKingWeb/NewsSync/Exception.php +++ b/vendor/JKingWeb/NewsSync/Exception.php @@ -13,7 +13,7 @@ class Exception extends \Exception { "Lang/Exception.stringMissing" => 10105, ]; - public function __construct(string $msgID = "", $vars = null, Throwable $e = null) { + public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) { if($msgID=="") { $msg = ""; $code = 0; diff --git a/vendor/JKingWeb/NewsSync/RuntimeData.php b/vendor/JKingWeb/NewsSync/RuntimeData.php index 3a019c1e..bd51604b 100644 --- a/vendor/JKingWeb/NewsSync/RuntimeData.php +++ b/vendor/JKingWeb/NewsSync/RuntimeData.php @@ -3,9 +3,9 @@ declare(strict_types=1); namespace JKingWeb\NewsSync; class RuntimeData { - protected $conf; - protected $db; - protected $auth; + public $conf; + public $db; + public $auth; public function __construct(Conf $conf) { $this->conf = $conf;