From c5fac33398b8540def16c53c6b6e7a523b3f8df7 Mon Sep 17 00:00:00 2001 From: Dustin Wilson Date: Thu, 16 Feb 2017 14:29:42 -0600 Subject: [PATCH] Converted all hard tabs to soft tabs --- .gitattributes | 10 +- composer.json | 74 ++--- lib/AbstractException.php | 104 +++---- lib/Conf.php | 104 +++---- lib/Database.php | 504 ++++++++++++++++----------------- lib/Db/Common.php | 118 ++++---- lib/Db/CommonPDO.php | 18 +- lib/Db/CommonSQLite3.php | 82 +++--- lib/Db/Driver.php | 50 ++-- lib/Db/DriverSQLite3.php | 98 +++---- lib/Db/DriverSQLite3PDO.php | 40 +-- lib/Db/Result.php | 16 +- lib/Db/ResultSQLite3.php | 96 +++---- lib/Db/Statement.php | 6 +- lib/Db/StatementSQLite3.php | 124 ++++---- lib/ExceptionFatal.php | 6 +- lib/Lang.php | 294 +++++++++---------- lib/Lang/Exception.php | 34 +-- lib/RuntimeData.php | 18 +- lib/User.php | 474 +++++++++++++++---------------- lib/User/Driver.php | 44 +-- lib/User/DriverInternal.php | 68 ++--- lib/User/InternalFunctions.php | 132 ++++----- locale/en.php | 92 +++--- sql/SQLite3/0.sql | 138 ++++----- tests/TestConf.php | 142 +++++----- tests/TestException.php | 88 +++--- tests/TestLang.php | 82 +++--- tests/TestLangErrors.php | 94 +++--- tests/lib/Lang/Setup.php | 76 ++--- tests/lib/Tools.php | 22 +- tests/phpunit.xml | 28 +- tests/testLangComplex.php | 114 ++++---- 33 files changed, 1695 insertions(+), 1695 deletions(-) diff --git a/.gitattributes b/.gitattributes index 412eeda..2431c40 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,13 +10,13 @@ *.dbproj merge=union # Standard to msysgit -*.doc diff=astextplain -*.DOC diff=astextplain +*.doc diff=astextplain +*.DOC diff=astextplain *.docx diff=astextplain *.DOCX diff=astextplain *.dot diff=astextplain *.DOT diff=astextplain *.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/composer.json b/composer.json index 4bd6718..ead7a5b 100644 --- a/composer.json +++ b/composer.json @@ -1,40 +1,40 @@ { - "name": "jkingweb/arsse", - "type": "library", - "description": "TODO", - "keywords": ["rss"], - "license": "MIT", - "authors": [ - { - "name": "J. King", - "email": "jking@jkingweb.ca", - "homepage": "https://jkingweb.ca/" - }, - { - "name": "Dustin Wilson", - "email": "dustin@dustinwilson.com", - "homepage": "https://dustinwilson.com/" - } + "name": "jkingweb/arsse", + "type": "library", + "description": "TODO", + "keywords": ["rss"], + "license": "MIT", + "authors": [ + { + "name": "J. King", + "email": "jking@jkingweb.ca", + "homepage": "https://jkingweb.ca/" + }, + { + "name": "Dustin Wilson", + "email": "dustin@dustinwilson.com", + "homepage": "https://dustinwilson.com/" + } - ], - "require": { - "php": "^7.0.0", - "jkingweb/druuid": "^3.0.0", - "phpseclib/phpseclib": "^2.0.4", - "webmozart/glob": "^4.1.0", - "fguillot/picoFeed": ">=0.1.31" - }, - "require-dev": { - "mikey179/vfsStream": "^1.6.4" - }, - "autoload": { - "psr-4": { - "JKingWeb\\NewsSync\\": "lib/" - } - }, - "autoload-dev": { - "psr-4": { - "JKingWeb\\NewsSync\\Test\\": "tests/lib/" - } - } + ], + "require": { + "php": "^7.0.0", + "jkingweb/druuid": "^3.0.0", + "phpseclib/phpseclib": "^2.0.4", + "webmozart/glob": "^4.1.0", + "fguillot/picoFeed": ">=0.1.31" + }, + "require-dev": { + "mikey179/vfsStream": "^1.6.4" + }, + "autoload": { + "psr-4": { + "JKingWeb\\NewsSync\\": "lib/" + } + }, + "autoload-dev": { + "psr-4": { + "JKingWeb\\NewsSync\\Test\\": "tests/lib/" + } + } } \ No newline at end of file diff --git a/lib/AbstractException.php b/lib/AbstractException.php index b580774..f98d199 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -4,57 +4,57 @@ namespace JKingWeb\NewsSync; abstract class AbstractException extends \Exception { - const CODES = [ - "Exception.uncoded" => -1, - "Exception.invalid" => 1, // this exception MUST NOT have a message string defined - "Exception.unknown" => 10000, - "Lang/Exception.defaultFileMissing" => 10101, - "Lang/Exception.fileMissing" => 10102, - "Lang/Exception.fileUnreadable" => 10103, - "Lang/Exception.fileCorrupt" => 10104, - "Lang/Exception.stringMissing" => 10105, - "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/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, - "Conf/Exception.fileMissing" => 10302, - "Conf/Exception.fileUnusable" => 10303, - "Conf/Exception.fileUnreadable" => 10304, - "Conf/Exception.fileUnwritable" => 10305, - "Conf/Exception.fileUncreatable" => 10306, - "Conf/Exception.fileCorrupt" => 10307, - "User/Exception.functionNotImplemented" => 10401, - "User/Exception.doesNotExist" => 10402, - "User/Exception.alreadyExists" => 10403, - "User/Exception.authMissing" => 10411, - "User/Exception.authFailed" => 10412, - "User/Exception.notAuthorized" => 10421, - ]; + const CODES = [ + "Exception.uncoded" => -1, + "Exception.invalid" => 1, // this exception MUST NOT have a message string defined + "Exception.unknown" => 10000, + "Lang/Exception.defaultFileMissing" => 10101, + "Lang/Exception.fileMissing" => 10102, + "Lang/Exception.fileUnreadable" => 10103, + "Lang/Exception.fileCorrupt" => 10104, + "Lang/Exception.stringMissing" => 10105, + "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/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, + "Conf/Exception.fileMissing" => 10302, + "Conf/Exception.fileUnusable" => 10303, + "Conf/Exception.fileUnreadable" => 10304, + "Conf/Exception.fileUnwritable" => 10305, + "Conf/Exception.fileUncreatable" => 10306, + "Conf/Exception.fileCorrupt" => 10307, + "User/Exception.functionNotImplemented" => 10401, + "User/Exception.doesNotExist" => 10402, + "User/Exception.alreadyExists" => 10403, + "User/Exception.authMissing" => 10411, + "User/Exception.authFailed" => 10412, + "User/Exception.notAuthorized" => 10421, + ]; - public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) { - if($msgID=="") { - $msg = "Exception.unknown"; - $code = 10000; - } else { - $class = get_called_class(); - $codeID = str_replace("\\", "/", str_replace(NS_BASE, "", $class)).".$msgID"; - if(!array_key_exists($codeID, self::CODES)) { - throw new Exception("uncoded"); - } else { - $code = self::CODES[$codeID]; - $msg = "Exception.".str_replace("\\", "/", $class).".$msgID"; - } - $msg = Lang::msg($msg, $vars); - } - parent::__construct($msg, $code, $e); - } + public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) { + if($msgID=="") { + $msg = "Exception.unknown"; + $code = 10000; + } else { + $class = get_called_class(); + $codeID = str_replace("\\", "/", str_replace(NS_BASE, "", $class)).".$msgID"; + if(!array_key_exists($codeID, self::CODES)) { + throw new Exception("uncoded"); + } else { + $code = self::CODES[$codeID]; + $msg = "Exception.".str_replace("\\", "/", $class).".$msgID"; + } + $msg = Lang::msg($msg, $vars); + } + parent::__construct($msg, $code, $e); + } } \ No newline at end of file diff --git a/lib/Conf.php b/lib/Conf.php index c9d9194..9ed9899 100644 --- a/lib/Conf.php +++ b/lib/Conf.php @@ -3,64 +3,64 @@ declare(strict_types=1); namespace JKingWeb\NewsSync; class Conf { - public $lang = "en"; - - public $dbDriver = Db\DriverSQLite3::class; - public $dbSQLite3File = BASE."newssync.db"; - public $dbSQLite3Key = ""; - public $dbSQLite3AutoUpd = true; - public $dbPostgreSQLHost = "localhost"; - public $dbPostgreSQLUser = "newssync"; - public $dbPostgreSQLPass = ""; - public $dbPostgreSQLPort = 5432; - public $dbPostgreSQLDb = "newssync"; - public $dbPostgreSQLSchema = ""; - public $dbPostgreSQLAutoUpd = false; - public $dbMySQLHost = "localhost"; - public $dbMySQLUser = "newssync"; - public $dbMySQLPass = ""; - public $dbMySQLPort = 3306; - public $dbMySQLDb = "newssync"; - public $dbMySQLAutoUpd = false; + public $lang = "en"; - public $userDriver = User\DriverInternal::class; - public $userAuthPreferHTTP = false; - public $userComposeNames = true; + public $dbDriver = Db\DriverSQLite3::class; + public $dbSQLite3File = BASE."newssync.db"; + public $dbSQLite3Key = ""; + public $dbSQLite3AutoUpd = true; + public $dbPostgreSQLHost = "localhost"; + public $dbPostgreSQLUser = "newssync"; + public $dbPostgreSQLPass = ""; + public $dbPostgreSQLPort = 5432; + public $dbPostgreSQLDb = "newssync"; + public $dbPostgreSQLSchema = ""; + public $dbPostgreSQLAutoUpd = false; + public $dbMySQLHost = "localhost"; + public $dbMySQLUser = "newssync"; + public $dbMySQLPass = ""; + public $dbMySQLPort = 3306; + public $dbMySQLDb = "newssync"; + public $dbMySQLAutoUpd = false; - public $simplepieCache = BASE.".cache"; + public $userDriver = User\DriverInternal::class; + public $userAuthPreferHTTP = false; + public $userComposeNames = true; + public $simplepieCache = BASE.".cache"; - public function __construct(string $import_file = "") { - if($import_file != "") $this->importFile($import_file); - } - public function importFile(string $file): self { - if(!file_exists($file)) throw new Conf\Exception("fileMissing", $file); - if(!is_readable($file)) throw new Conf\Exception("fileUnreadable", $file); - try { - ob_start(); - $arr = (@include $file); - } catch(\Throwable $e) { - $arr = null; - } finally { - ob_end_clean(); - } - if(!is_array($arr)) throw new Conf\Exception("fileCorrupt", $file); - return $this->import($arr); - } + public function __construct(string $import_file = "") { + if($import_file != "") $this->importFile($import_file); + } - public function import(array $arr): self { - foreach($arr as $key => $value) { - $this->$key = $value; - } - return $this; - } + public function importFile(string $file): self { + if(!file_exists($file)) throw new Conf\Exception("fileMissing", $file); + if(!is_readable($file)) throw new Conf\Exception("fileUnreadable", $file); + try { + ob_start(); + $arr = (@include $file); + } catch(\Throwable $e) { + $arr = null; + } finally { + ob_end_clean(); + } + if(!is_array($arr)) throw new Conf\Exception("fileCorrupt", $file); + return $this->import($arr); + } - public function export(string $file = ""): string { - // TODO - } + public function import(array $arr): self { + foreach($arr as $key => $value) { + $this->$key = $value; + } + return $this; + } - public function __toString(): string { - return $this->export(); - } + public function export(string $file = ""): string { + // TODO + } + + public function __toString(): string { + return $this->export(); + } } \ No newline at end of file diff --git a/lib/Database.php b/lib/Database.php index 59ff2a4..ef924aa 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -3,275 +3,275 @@ declare(strict_types=1); namespace JKingWeb\NewsSync; class Database { - const SCHEMA_VERSION = 1; - const FORMAT_TS = "Y-m-d h:i:s"; - const FORMAT_DATE = "Y-m-d"; - const FORMAT_TIME = "h:i:s"; - - protected $data; - public $db; + const SCHEMA_VERSION = 1; + const FORMAT_TS = "Y-m-d h:i:s"; + const FORMAT_DATE = "Y-m-d"; + const FORMAT_TIME = "h:i:s"; + + protected $data; + public $db; - protected function cleanName(string $name): string { - return (string) preg_filter("[^0-9a-zA-Z_\.]", "", $name); - } + protected function cleanName(string $name): string { + return (string) preg_filter("[^0-9a-zA-Z_\.]", "", $name); + } - public function __construct(RuntimeData $data) { - $this->data = $data; - $driver = $data->conf->dbDriver; - $this->db = $driver::create($data, INSTALL); - $ver = $this->db->schemaVersion(); - if(!INSTALL && $ver < self::SCHEMA_VERSION) { - $this->db->update(self::SCHEMA_VERSION); - } - } + public function __construct(RuntimeData $data) { + $this->data = $data; + $driver = $data->conf->dbDriver; + $this->db = $driver::create($data, INSTALL); + $ver = $this->db->schemaVersion(); + if(!INSTALL && $ver < self::SCHEMA_VERSION) { + $this->db->update(self::SCHEMA_VERSION); + } + } - 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; - } + 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->db->schemaVersion(); - } + public function schemaVersion(): int { + return $this->db->schemaVersion(); + } - public function dbUpdate(): bool { - if($this->db->schemaVersion() < self::SCHEMA_VERSION) return $this->db->update(self::SCHEMA_VERSION); - return false; - } + public function dbUpdate(): bool { + if($this->db->schemaVersion() < self::SCHEMA_VERSION) return $this->db->update(self::SCHEMA_VERSION); + return false; + } - public function settingGet(string $key) { - $row = $this->db->prepare("SELECT value, type from newssync_settings where key = ?", "str")->run($key)->get(); - if(!$row) return null; - switch($row['type']) { - case "int": return (int) $row['value']; - case "numeric": return (float) $row['value']; - case "text": return $row['value']; - case "json": return json_decode($row['value']); - case "timestamp": return date_create_from_format("!".self::FORMAT_TS, $row['value'], new DateTimeZone("UTC")); - case "date": return date_create_from_format("!".self::FORMAT_DATE, $row['value'], new DateTimeZone("UTC")); - case "time": return date_create_from_format("!".self::FORMAT_TIME, $row['value'], new DateTimeZone("UTC")); - case "bool": return (bool) $row['value']; - case "null": return null; - default: return $row['value']; - } - } + public function settingGet(string $key) { + $row = $this->db->prepare("SELECT value, type from newssync_settings where key = ?", "str")->run($key)->get(); + if(!$row) return null; + switch($row['type']) { + case "int": return (int) $row['value']; + case "numeric": return (float) $row['value']; + case "text": return $row['value']; + case "json": return json_decode($row['value']); + case "timestamp": return date_create_from_format("!".self::FORMAT_TS, $row['value'], new DateTimeZone("UTC")); + case "date": return date_create_from_format("!".self::FORMAT_DATE, $row['value'], new DateTimeZone("UTC")); + case "time": return date_create_from_format("!".self::FORMAT_TIME, $row['value'], new DateTimeZone("UTC")); + case "bool": return (bool) $row['value']; + case "null": return null; + default: return $row['value']; + } + } - public function settingSet(string $key, $in, string $type = null): bool { - if(!$type) { - switch(gettype($in)) { - case "boolean": $type = "bool"; break; - case "integer": $type = "int"; break; - case "double": $type = "numeric"; break; - case "string": - case "array": $type = "json"; break; - case "resource": - case "unknown type": - case "NULL": $type = "null"; break; - case "object": - if($in instanceof DateTimeInterface) { - $type = "timestamp"; - } else { - $type = "text"; - } - break; - default: $type = 'null'; break; - } - } - $type = strtolower($type); - switch($type) { - case "integer": - $type = "int"; - case "int": - $value =& $in; - break; - case "float": - case "double": - case "real": - $type = "numeric"; - case "numeric": - $value =& $in; - break; - case "str": - case "string": - $type = "text"; - case "text": - $value =& $in; - break; - case "json": - if(is_array($in) || is_object($in)) { - $value = json_encode($in); - } else { - $value =& $in; - } - break; - case "datetime": - $type = "timestamp"; - case "timestamp": - if($in instanceof DateTimeInterface) { - $value = gmdate(self::FORMAT_TS, $in->format("U")); - } else if(is_numeric($in)) { - $value = gmdate(self::FORMAT_TS, $in); - } else { - $value = gmdate(self::FORMAT_TS, gmstrftime($in)); - } - break; - case "date": - if($in instanceof DateTimeInterface) { - $value = gmdate(self::FORMAT_DATE, $in->format("U")); - } else if(is_numeric($in)) { - $value = gmdate(self::FORMAT_DATE, $in); - } else { - $value = gmdate(self::FORMAT_DATE, gmstrftime($in)); - } - break; - case "time": - if($in instanceof DateTimeInterface) { - $value = gmdate(self::FORMAT_TIME, $in->format("U")); - } else if(is_numeric($in)) { - $value = gmdate(self::FORMAT_TIME, $in); - } else { - $value = gmdate(self::FORMAT_TIME, gmstrftime($in)); - } - break; - case "boolean": - case "bit": - $type = "bool"; - case "bool": - $value = (int) $in; - break; - case "null": - $value = null; - break; - default: - $type = "text"; - $value =& $in; - break; - } - $this->db->prepare("REPLACE INTO newssync_settings(key,value,type) values(?,?,?)", "str", (($type=="null") ? "null" : "str"), "str")->run($key, $value, "text"); - } + public function settingSet(string $key, $in, string $type = null): bool { + if(!$type) { + switch(gettype($in)) { + case "boolean": $type = "bool"; break; + case "integer": $type = "int"; break; + case "double": $type = "numeric"; break; + case "string": + case "array": $type = "json"; break; + case "resource": + case "unknown type": + case "NULL": $type = "null"; break; + case "object": + if($in instanceof DateTimeInterface) { + $type = "timestamp"; + } else { + $type = "text"; + } + break; + default: $type = 'null'; break; + } + } + $type = strtolower($type); + switch($type) { + case "integer": + $type = "int"; + case "int": + $value =& $in; + break; + case "float": + case "double": + case "real": + $type = "numeric"; + case "numeric": + $value =& $in; + break; + case "str": + case "string": + $type = "text"; + case "text": + $value =& $in; + break; + case "json": + if(is_array($in) || is_object($in)) { + $value = json_encode($in); + } else { + $value =& $in; + } + break; + case "datetime": + $type = "timestamp"; + case "timestamp": + if($in instanceof DateTimeInterface) { + $value = gmdate(self::FORMAT_TS, $in->format("U")); + } else if(is_numeric($in)) { + $value = gmdate(self::FORMAT_TS, $in); + } else { + $value = gmdate(self::FORMAT_TS, gmstrftime($in)); + } + break; + case "date": + if($in instanceof DateTimeInterface) { + $value = gmdate(self::FORMAT_DATE, $in->format("U")); + } else if(is_numeric($in)) { + $value = gmdate(self::FORMAT_DATE, $in); + } else { + $value = gmdate(self::FORMAT_DATE, gmstrftime($in)); + } + break; + case "time": + if($in instanceof DateTimeInterface) { + $value = gmdate(self::FORMAT_TIME, $in->format("U")); + } else if(is_numeric($in)) { + $value = gmdate(self::FORMAT_TIME, $in); + } else { + $value = gmdate(self::FORMAT_TIME, gmstrftime($in)); + } + break; + case "boolean": + case "bit": + $type = "bool"; + case "bool": + $value = (int) $in; + break; + case "null": + $value = null; + break; + default: + $type = "text"; + $value =& $in; + break; + } + $this->db->prepare("REPLACE INTO newssync_settings(key,value,type) values(?,?,?)", "str", (($type=="null") ? "null" : "str"), "str")->run($key, $value, "text"); + } - public function settingRemove(string $key): bool { - $this->db->prepare("DELETE from newssync_settings where key = ?", "str")->run($key); - return true; - } + public function settingRemove(string $key): bool { + $this->db->prepare("DELETE from newssync_settings where key = ?", "str")->run($key); + return true; + } - public function userExists(string $user): bool { - if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - return (bool) $this->db->prepare("SELECT count(*) from newssync_users where id is ?", "str")->run($user)->getSingle(); - } + public function userExists(string $user): bool { + if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + return (bool) $this->db->prepare("SELECT count(*) from newssync_users where id is ?", "str")->run($user)->getSingle(); + } - public function userAdd(string $user, string $password = null): bool { - if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - if($this->userExists($user)) return false; - if(strlen($password) > 0) $password = password_hash($password, \PASSWORD_DEFAULT); - $this->db->prepare("INSERT INTO newssync_users(id,password) values(?,?)", "str", "str", "str")->run($user,$password,$admin); - return true; - } + public function userAdd(string $user, string $password = null): bool { + if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + if($this->userExists($user)) return false; + if(strlen($password) > 0) $password = password_hash($password, \PASSWORD_DEFAULT); + $this->db->prepare("INSERT INTO newssync_users(id,password) values(?,?)", "str", "str", "str")->run($user,$password,$admin); + return true; + } - public function userRemove(string $user): bool { - if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - $this->db->prepare("DELETE from newssync_users where id is ?", "str")->run($user); - return true; - } + public function userRemove(string $user): bool { + if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + $this->db->prepare("DELETE from newssync_users where id is ?", "str")->run($user); + return true; + } - public function userList(string $domain = null): array { - if($domain !== null) { - if(!$this->data->user->authorize("@".$domain, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]); - $domain = str_replace(["\\","%","_"],["\\\\", "\\%", "\\_"], $domain); - $domain = "%@".$domain; - $set = $this->db->prepare("SELECT id from newssync_users where id like ?", "str")->run($domain); - } else { - if(!$this->data->user->authorize("", __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "all users"]); - $set = $this->db->prepare("SELECT id from newssync_users")->run(); - } - $out = []; - foreach($set as $row) { - $out[] = $row["id"]; - } - return $out; - } - - public function userPasswordGet(string $user): string { - if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - if(!$this->userExists($user)) return ""; - return (string) $this->db->prepare("SELECT password from newssync_users where id is ?", "str")->run($user)->getSingle(); - } - - public function userPasswordSet(string $user, string $password = null): bool { - if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - if(!$this->userExists($user)) return false; - if(strlen($password > 0)) $password = password_hash($password); - $this->db->prepare("UPDATE newssync_users set password = ? where id is ?", "str", "str")->run($password, $user); - return true; - } + public function userList(string $domain = null): array { + if($domain !== null) { + if(!$this->data->user->authorize("@".$domain, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]); + $domain = str_replace(["\\","%","_"],["\\\\", "\\%", "\\_"], $domain); + $domain = "%@".$domain; + $set = $this->db->prepare("SELECT id from newssync_users where id like ?", "str")->run($domain); + } else { + if(!$this->data->user->authorize("", __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "all users"]); + $set = $this->db->prepare("SELECT id from newssync_users")->run(); + } + $out = []; + foreach($set as $row) { + $out[] = $row["id"]; + } + return $out; + } + + public function userPasswordGet(string $user): string { + if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + if(!$this->userExists($user)) return ""; + return (string) $this->db->prepare("SELECT password from newssync_users where id is ?", "str")->run($user)->getSingle(); + } + + public function userPasswordSet(string $user, string $password = null): bool { + if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + if(!$this->userExists($user)) return false; + if(strlen($password > 0)) $password = password_hash($password); + $this->db->prepare("UPDATE newssync_users set password = ? where id is ?", "str", "str")->run($password, $user); + return true; + } - public function userPropertiesGet(string $user): array { - if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - $prop = $this->db->prepare("SELECT name,rights from newssync_users where id is ?", "str")->run($user)->get(); - if(!$prop) return []; - return $prop; - } + public function userPropertiesGet(string $user): array { + if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + $prop = $this->db->prepare("SELECT name,rights from newssync_users where id is ?", "str")->run($user)->get(); + if(!$prop) return []; + return $prop; + } - public function userPropertiesSet(string $user, array &$properties): array { - if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - $valid = [ // FIXME: add future properties - "name" => "str", - ]; - if(!$this->userExists($user)) return []; - $this->db->begin(); - foreach($valid as $prop => $type) { - if(!array_key_exists($prop, $properties)) continue; - $this->db->prepare("UPDATE newssync_users set $prop = ? where id is ?", $type, "str")->run($properties[$prop], $user); - } - $this->db->commit(); - return $this->userPropertiesGet($user); - } + public function userPropertiesSet(string $user, array &$properties): array { + if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + $valid = [ // FIXME: add future properties + "name" => "str", + ]; + if(!$this->userExists($user)) return []; + $this->db->begin(); + foreach($valid as $prop => $type) { + if(!array_key_exists($prop, $properties)) continue; + $this->db->prepare("UPDATE newssync_users set $prop = ? where id is ?", $type, "str")->run($properties[$prop], $user); + } + $this->db->commit(); + return $this->userPropertiesGet($user); + } - public function userRightsGet(string $user): int { - if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - return (int) $this->db->prepare("SELECT rights from newssync_users where id is ?", "str")->run($user)->getSingle(); - } + public function userRightsGet(string $user): int { + if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + return (int) $this->db->prepare("SELECT rights from newssync_users where id is ?", "str")->run($user)->getSingle(); + } - public function userRightsSet(string $user, int $rights): bool { - if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - if(!$this->userExists($user)) return false; - $this->db->prepare("UPDATE newssync_users set rights = ? where id is ?", "int", "str")->run($rights, $user); - return true; - } + public function userRightsSet(string $user, int $rights): bool { + if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + if(!$this->userExists($user)) return false; + $this->db->prepare("UPDATE newssync_users set rights = ? where id is ?", "int", "str")->run($rights, $user); + return true; + } - public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = ""): int { - if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["user" => $user, "action" => __FUNCTION__]); - $this->db->begin(); - $qFeed = $this->db->prepare("SELECT id from newssync_feeds where url is ? and username is ? and password is ?", "str", "str", "str"); - $feed = $qFeed->run($url, $fetchUser, $fetchPassword)->getSingle(); - if($feed===null) { - $this->db->prepare("INSERT INTO newssync_feeds(url,username,password) values(?,?,?)", "str", "str", "str")->run($url, $fetchUser, $fetchPassword); - $feed = $qFeed->run($url, $fetchUser, $fetchPassword)->getSingle(); - } - $this->db->prepare("INSERT INTO newssync_subscriptions(owner,feed) values(?,?)", "str", "int")->run($user,$feed); - $sub = $this->db->prepare("SELECT id from newssync_subscriptions where owner is ? and feed is ?", "str", "int")->run($user, $feed)->getSingle(); - $this->db->commit(); - return $sub; - } + public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = ""): int { + if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["user" => $user, "action" => __FUNCTION__]); + $this->db->begin(); + $qFeed = $this->db->prepare("SELECT id from newssync_feeds where url is ? and username is ? and password is ?", "str", "str", "str"); + $feed = $qFeed->run($url, $fetchUser, $fetchPassword)->getSingle(); + if($feed===null) { + $this->db->prepare("INSERT INTO newssync_feeds(url,username,password) values(?,?,?)", "str", "str", "str")->run($url, $fetchUser, $fetchPassword); + $feed = $qFeed->run($url, $fetchUser, $fetchPassword)->getSingle(); + } + $this->db->prepare("INSERT INTO newssync_subscriptions(owner,feed) values(?,?)", "str", "int")->run($user,$feed); + $sub = $this->db->prepare("SELECT id from newssync_subscriptions where owner is ? and feed is ?", "str", "int")->run($user, $feed)->getSingle(); + $this->db->commit(); + return $sub; + } - public function subscriptionRemove(int $id): bool { - $this->db->begin(); - $user = $this->db->prepare("SELECT owner from newssync_subscriptions where id is ?", "int")->run($id)->getSingle(); - if($user===null) return false; - if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); - return (bool) $this->db->prepare("DELETE from newssync_subscriptions where id is ?", "int")->run($id)->changes(); - } + public function subscriptionRemove(int $id): bool { + $this->db->begin(); + $user = $this->db->prepare("SELECT owner from newssync_subscriptions where id is ?", "int")->run($id)->getSingle(); + if($user===null) return false; + if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + return (bool) $this->db->prepare("DELETE from newssync_subscriptions where id is ?", "int")->run($id)->changes(); + } } \ No newline at end of file diff --git a/lib/Db/Common.php b/lib/Db/Common.php index ceb64eb..27a1926 100644 --- a/lib/Db/Common.php +++ b/lib/Db/Common.php @@ -4,70 +4,70 @@ namespace JKingWeb\NewsSync\Db; use JKingWeb\DrUUID\UUID as UUID; Trait Common { - protected $transDepth = 0; - - public function schemaVersion(): int { - try { - return $this->data->db->settingGet("schema_version"); - } catch(\Throwable $e) { - return 0; - } - } - - public function begin(): bool { - $this->exec("SAVEPOINT newssync_".($this->transDepth)); - $this->transDepth += 1; - return true; - } + protected $transDepth = 0; + + public function schemaVersion(): int { + try { + return $this->data->db->settingGet("schema_version"); + } catch(\Throwable $e) { + return 0; + } + } + + public function begin(): bool { + $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 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)); - // rollback to savepoint does not collpase the savepoint - $this->commit(); - $this->transDepth -= 1; - if($this->transDepth==0) $this->exec("ROLLBACK TRANSACTION"); - } else { - $this->exec("ROLLBACK 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)); + // rollback to savepoint does not collpase the savepoint + $this->commit(); + $this->transDepth -= 1; + if($this->transDepth==0) $this->exec("ROLLBACK TRANSACTION"); + } else { + $this->exec("ROLLBACK TRANSACTION"); + $this->transDepth = 0; + } + return true; + } - public function lock(): bool { - if($this->schemaVersion() < 1) return true; - if($this->isLocked()) return false; - $uuid = UUID::mintStr(); - if(!$this->data->db->settingSet("lock", $uuid)) return false; - sleep(1); - if($this->data->db->settingGet("lock") != $uuid) return false; - return true; - } + public function lock(): bool { + if($this->schemaVersion() < 1) return true; + if($this->isLocked()) return false; + $uuid = UUID::mintStr(); + if(!$this->data->db->settingSet("lock", $uuid)) return false; + sleep(1); + if($this->data->db->settingGet("lock") != $uuid) return false; + return true; + } - public function unlock(): bool { - return $this->data->db->settingRemove("lock"); - } + public function unlock(): bool { + return $this->data->db->settingRemove("lock"); + } - public function isLocked(): bool { - if($this->schemaVersion() < 1) return false; - return ($this->query("SELECT count(*) from newssync_settings where key = 'lock'")->getSingle() > 0); - } + public function isLocked(): bool { + if($this->schemaVersion() < 1) return false; + return ($this->query("SELECT count(*) from newssync_settings where key = 'lock'")->getSingle() > 0); + } - public function prepare(string $query, string ...$paramType): Statement { - return $this->prepareArray($query, $paramType); - } + public function prepare(string $query, string ...$paramType): Statement { + return $this->prepareArray($query, $paramType); + } } \ No newline at end of file diff --git a/lib/Db/CommonPDO.php b/lib/Db/CommonPDO.php index 308d918..4489c9a 100644 --- a/lib/Db/CommonPDO.php +++ b/lib/Db/CommonPDO.php @@ -3,15 +3,15 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\Db; Trait CommonPDO { - public function query(string $query): Result { - return new ResultPDO($this->db->query($query)); - } + public function query(string $query): Result { + return new ResultPDO($this->db->query($query)); + } - public function prepareArray(string $query, array $paramTypes): Statement { - return new StatementPDO($query, $paramTypes); - } + 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); - } + public function prepare(string $query, string ...$paramType): Statement { + return $this->prepareArray($query, $paramType); + } } \ No newline at end of file diff --git a/lib/Db/CommonSQLite3.php b/lib/Db/CommonSQLite3.php index 66ad858..ab5ceea 100644 --- a/lib/Db/CommonSQLite3.php +++ b/lib/Db/CommonSQLite3.php @@ -3,48 +3,48 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\Db; Trait CommonSQLite3 { - - static public function driverName(): string { - return "SQLite 3"; - } + + static public function driverName(): string { + return "SQLite 3"; + } - public function schemaVersion(): int { - return $this->query("PRAGMA user_version")->getSingle(); - } + public function schemaVersion(): int { + return $this->query("PRAGMA user_version")->getSingle(); + } - public function update(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()]); - $sep = \DIRECTORY_SEPARATOR; - $path = \JKingWeb\NewsSync\BASE."sql".$sep."SQLite3".$sep; - $this->lock(); - $this->begin(); - for($a = $ver; $a < $to; $a++) { - $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()]); - $sql = @file_get_contents($file); - if($sql===false) throw new Update\Exception("fileUnusable", ['file' => $file, 'driver_name' => $this->driverName()]); - $this->exec($sql); - } catch(\Throwable $e) { - // undo any partial changes from the failed update - $this->rollback(); - // commit any successful updates if updating by more than one version - $this->commit(true); - // throw the error received - throw $e; - } - $this->commit(); - } - $this->unlock(); - $this->commit(); - return true; - } + public function update(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()]); + $sep = \DIRECTORY_SEPARATOR; + $path = \JKingWeb\NewsSync\BASE."sql".$sep."SQLite3".$sep; + $this->lock(); + $this->begin(); + for($a = $ver; $a < $to; $a++) { + $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()]); + $sql = @file_get_contents($file); + if($sql===false) throw new Update\Exception("fileUnusable", ['file' => $file, 'driver_name' => $this->driverName()]); + $this->exec($sql); + } catch(\Throwable $e) { + // undo any partial changes from the failed update + $this->rollback(); + // commit any successful updates if updating by more than one version + $this->commit(true); + // throw the error received + throw $e; + } + $this->commit(); + } + $this->unlock(); + $this->commit(); + return true; + } - public function exec(string $query): bool { - return (bool) $this->db->exec($query); - } + public function exec(string $query): bool { + return (bool) $this->db->exec($query); + } } \ No newline at end of file diff --git a/lib/Db/Driver.php b/lib/Db/Driver.php index 7ec5c5c..de05cc6 100644 --- a/lib/Db/Driver.php +++ b/lib/Db/Driver.php @@ -2,29 +2,29 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\Db; -interface Driver { - // returns an instance of a class implementing this interface. Implemented as a static method so that classes may return their PDO equivalents instead of themselves - static function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver; - // returns a human-friendly name for the driver (for display in installer, for example) - static function driverName(): string; - // returns the version of the scheme of the opened database; if uninitialized should return 0 - function schemaVersion(): int; - // begin a real or synthetic transactions, with real or synthetic nesting - function begin(): bool; - // commit either the latest or all pending nested transactions; use of this method should assume a partial commit is a no-op - function commit(bool $all = false): bool; - // rollback either the latest or all pending nested transactions; use of this method should assume a partial rollback will not work - function rollback(bool $all = false): bool; - // attempt to advise other processes that they should not attempt to access the database; used during live upgrades - function lock(): bool; - function unlock(): bool; - function isLocked(): bool; - // attempt to perform an in-place upgrade of the database schema; this may be a no-op which always throws an exception - function update(int $to): bool; - // execute one or more unsanitized SQL queries and return an indication of success - function exec(string $query): bool; - // perform a single unsanitized query and return a result set - function query(string $query): Result; - // ready a prepared statement for later execution - function prepare(string $query, string ...$paramType): Statement; +interface Driver { + // returns an instance of a class implementing this interface. Implemented as a static method so that classes may return their PDO equivalents instead of themselves + static function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver; + // returns a human-friendly name for the driver (for display in installer, for example) + static function driverName(): string; + // returns the version of the scheme of the opened database; if uninitialized should return 0 + function schemaVersion(): int; + // begin a real or synthetic transactions, with real or synthetic nesting + function begin(): bool; + // commit either the latest or all pending nested transactions; use of this method should assume a partial commit is a no-op + function commit(bool $all = false): bool; + // rollback either the latest or all pending nested transactions; use of this method should assume a partial rollback will not work + function rollback(bool $all = false): bool; + // attempt to advise other processes that they should not attempt to access the database; used during live upgrades + function lock(): bool; + function unlock(): bool; + function isLocked(): bool; + // attempt to perform an in-place upgrade of the database schema; this may be a no-op which always throws an exception + function update(int $to): bool; + // execute one or more unsanitized SQL queries and return an indication of success + function exec(string $query): bool; + // perform a single unsanitized query and return a result set + function query(string $query): Result; + // ready a prepared statement for later execution + function prepare(string $query, string ...$paramType): Statement; } \ No newline at end of file diff --git a/lib/Db/DriverSQLite3.php b/lib/Db/DriverSQLite3.php index 7cff026..7ca9307 100644 --- a/lib/Db/DriverSQLite3.php +++ b/lib/Db/DriverSQLite3.php @@ -3,57 +3,57 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\Db; class DriverSQLite3 implements Driver { - use Common, CommonSQLite3 { - CommonSQLite3::schemaVersion insteadof Common; - } - - protected $db; - protected $data; - - private function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) { - $this->data = $data; - $file = $data->conf->dbSQLite3File; - // if the file exists (or we're initializing the database), try to open it and set initial options - try { - $this->db = new \SQLite3($file, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, $data->conf->dbSQLite3Key); - $this->db->enableExceptions(true); - $this->exec("PRAGMA journal_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 - 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); - } - } + use Common, CommonSQLite3 { + CommonSQLite3::schemaVersion insteadof Common; + } + + protected $db; + protected $data; + + private function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) { + $this->data = $data; + $file = $data->conf->dbSQLite3File; + // if the file exists (or we're initializing the database), try to open it and set initial options + try { + $this->db = new \SQLite3($file, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, $data->conf->dbSQLite3Key); + $this->db->enableExceptions(true); + $this->exec("PRAGMA journal_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 + 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); - } + public function __destruct() { + $this->db->close(); + unset($this->db); + } - static public function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver { - // check to make sure required extensions are loaded - if(class_exists("SQLite3")) { - return new self($data, $install); - } else if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) { - return new DriverSQLite3PDO($data, $install); - } else { - throw new Exception("extMissing", self::driverName()); - } - } + static public function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver { + // check to make sure required extensions are loaded + if(class_exists("SQLite3")) { + return new self($data, $install); + } else if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) { + return new DriverSQLite3PDO($data, $install); + } else { + throw new Exception("extMissing", self::driverName()); + } + } - public function query(string $query): Result { - return new ResultSQLite3($this->db->query($query), $this->db->changes()); - } + public function query(string $query): Result { + return new ResultSQLite3($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): Statement { + return new StatementSQLite3($this->db, $this->db->prepare($query), $paramTypes); + } } \ No newline at end of file diff --git a/lib/Db/DriverSQLite3PDO.php b/lib/Db/DriverSQLite3PDO.php index 166b059..72d0252 100644 --- a/lib/Db/DriverSQLite3PDO.php +++ b/lib/Db/DriverSQLite3PDO.php @@ -3,26 +3,26 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\Db; class DriverSQLite3 implements Driver { - use CommonPDO, CommonSQLite3; - - protected $db; - - private function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) { - // FIXME: stub - } + use CommonPDO, CommonSQLite3; + + protected $db; + + private function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) { + // FIXME: stub + } - public function __destruct() { - // FIXME: stub - } + public function __destruct() { + // FIXME: stub + } - static public function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver { - // check to make sure required extensions are loaded - if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) { - return new self($data, $install); - } else if(class_exists("SQLite3")) { - return new DriverSQLite3($data, $install); - } else { - throw new Exception("extMissing", self::driverName()); - } - } + static public function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver { + // check to make sure required extensions are loaded + if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) { + return new self($data, $install); + } else if(class_exists("SQLite3")) { + return new DriverSQLite3($data, $install); + } else { + throw new Exception("extMissing", self::driverName()); + } + } } \ No newline at end of file diff --git a/lib/Db/Result.php b/lib/Db/Result.php index 78ad90a..ea34b39 100644 --- a/lib/Db/Result.php +++ b/lib/Db/Result.php @@ -3,13 +3,13 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\Db; interface Result extends \Iterator { - function current(); - function key(); - function next(); - function rewind(); - function valid(); + function current(); + function key(); + function next(); + function rewind(); + function valid(); - function get(); - function getSingle(); - function changes(); + function get(); + function getSingle(); + function changes(); } \ No newline at end of file diff --git a/lib/Db/ResultSQLite3.php b/lib/Db/ResultSQLite3.php index 1040049..af7202f 100644 --- a/lib/Db/ResultSQLite3.php +++ b/lib/Db/ResultSQLite3.php @@ -3,62 +3,62 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\Db; class ResultSQLite3 implements Result { - protected $st; - protected $set; - protected $pos = 0; - protected $cur = null; - protected $rows = 0; + protected $st; + protected $set; + protected $pos = 0; + protected $cur = null; + protected $rows = 0; - public function __construct($result, $changes, $statement = null) { - $this->st = $statement; //keeps the statement from being destroyed, invalidating the result set - $this->set = $result; - $this->rows = $changes; - } + public function __construct($result, $changes, $statement = null) { + $this->st = $statement; //keeps the statement from being destroyed, invalidating the result set + $this->set = $result; + $this->rows = $changes; + } - public function __destruct() { - $this->set->finalize(); - unset($this->set); - } + public function __destruct() { + $this->set->finalize(); + unset($this->set); + } - public function valid() { - $this->cur = $this->set->fetchArray(\SQLITE3_ASSOC); - return ($this->cur !== false); - } + public function valid() { + $this->cur = $this->set->fetchArray(\SQLITE3_ASSOC); + return ($this->cur !== false); + } - public function next() { - $this->cur = null; - $this->pos += 1; - } + public function next() { + $this->cur = null; + $this->pos += 1; + } - public function current() { - return $this->cur; - } + public function current() { + return $this->cur; + } - public function key() { - return $this->pos; - } + public function key() { + return $this->pos; + } - public function rewind() { - $this->pos = 0; - $this->cur = null; - $this->set->reset(); - } + public function rewind() { + $this->pos = 0; + $this->cur = null; + $this->set->reset(); + } - public function getSingle() { - $this->next(); - if($this->valid()) { - $keys = array_keys($this->cur); - return $this->cur[array_shift($keys)]; - } - return null; - } + public function getSingle() { + $this->next(); + if($this->valid()) { + $keys = array_keys($this->cur); + return $this->cur[array_shift($keys)]; + } + return null; + } - public function get() { - $this->next(); - return ($this->valid() ? $this->cur : null); - } + public function get() { + $this->next(); + return ($this->valid() ? $this->cur : null); + } - public function changes() { - return $this->rows; - } + public function changes() { + return $this->rows; + } } \ No newline at end of file diff --git a/lib/Db/Statement.php b/lib/Db/Statement.php index 4dee6d9..a127a89 100644 --- a/lib/Db/Statement.php +++ b/lib/Db/Statement.php @@ -3,7 +3,7 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\Db; interface Statement { - function __invoke(&...$values); // alias of run() - function run(&...$values): Result; - function runArray(array &$values): Result; + function __invoke(&...$values); // alias of run() + function run(&...$values): Result; + function runArray(array &$values): Result; } \ No newline at end of file diff --git a/lib/Db/StatementSQLite3.php b/lib/Db/StatementSQLite3.php index e0b235c..bc87c7b 100644 --- a/lib/Db/StatementSQLite3.php +++ b/lib/Db/StatementSQLite3.php @@ -3,71 +3,71 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\Db; class StatementSQLite3 implements Statement { - protected $db; - protected $st; - protected $types; + protected $db; + protected $st; + protected $types; - public function __construct($db, $st, array $bindings = null) { - $this->db = $db; - $this->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 "date": - case "time": - case "datetime": - case "timestamp": - $this->types[] = \SQLITE3_TEXT; break; - case "blob": - case "bin": - case "binary": - $this->types[] = \SQLITE3_BLOB; break; - case "text": - case "string": - case "str": - $this->types[] = \SQLITE3_TEXT; break; - case "bool": - case "boolean": - case "bit": - $this->types[] = \SQLITE3_INTEGER; break; - default: - $this->types[] = \SQLITE3_TEXT; break; - } - } - } + public function __construct($db, $st, array $bindings = null) { + $this->db = $db; + $this->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 "date": + case "time": + case "datetime": + case "timestamp": + $this->types[] = \SQLITE3_TEXT; break; + case "blob": + case "bin": + case "binary": + $this->types[] = \SQLITE3_BLOB; break; + case "text": + case "string": + case "str": + $this->types[] = \SQLITE3_TEXT; break; + case "bool": + case "boolean": + case "bit": + $this->types[] = \SQLITE3_INTEGER; break; + default: + $this->types[] = \SQLITE3_TEXT; break; + } + } + } - public function __destruct() { - $this->st->close(); - unset($this->st); - } + public function __destruct() { + $this->st->close(); + unset($this->st); + } - public function __invoke(&...$values) { - return $this->runArray($values); - } + public function __invoke(&...$values) { + return $this->runArray($values); + } - public function run(&...$values): Result { - 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; - } - $this->st->bindParam($a+1, $values[$a], $type); - } - return new ResultSQLite3($this->st->execute(), $this->db->changes(), $this); - } + 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; + } + $this->st->bindParam($a+1, $values[$a], $type); + } + return new ResultSQLite3($this->st->execute(), $this->db->changes(), $this); + } } \ No newline at end of file diff --git a/lib/ExceptionFatal.php b/lib/ExceptionFatal.php index fcb7a96..af12d35 100644 --- a/lib/ExceptionFatal.php +++ b/lib/ExceptionFatal.php @@ -3,7 +3,7 @@ declare(strict_types=1); namespace JKingWeb\NewsSync; class ExceptionFatal extends AbstractException { - public function __construct($msg = "", $code = 0, $e = null) { - \Exception::__construct($msg, $code, $e); - } + public function __construct($msg = "", $code = 0, $e = null) { + \Exception::__construct($msg, $code, $e); + } } \ No newline at end of file diff --git a/lib/Lang.php b/lib/Lang.php index d557866..f157221 100644 --- a/lib/Lang.php +++ b/lib/Lang.php @@ -4,161 +4,161 @@ namespace JKingWeb\NewsSync; use \Webmozart\Glob\Glob; class Lang { - const DEFAULT = "en"; - const REQUIRED = [ - 'Exception.JKingWeb/NewsSync/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in Exception.php', - 'Exception.JKingWeb/NewsSync/Exception.unknown' => 'An unknown error has occurred', - 'Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing' => 'Default language file "{0}" missing', - 'Exception.JKingWeb/NewsSync/Lang/Exception.fileMissing' => 'Language file "{0}" is not available', - 'Exception.JKingWeb/NewsSync/Lang/Exception.fileUnreadable' => 'Insufficient permissions to read language file "{0}"', - 'Exception.JKingWeb/NewsSync/Lang/Exception.fileCorrupt' => 'Language file "{0}" is corrupt or does not conform to expected format', - 'Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing' => 'Message string "{msgID}" missing from all loaded language files ({fileList})', - 'Exception.JKingWeb/NewsSync/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})', - ]; + const DEFAULT = "en"; + const REQUIRED = [ + 'Exception.JKingWeb/NewsSync/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in Exception.php', + 'Exception.JKingWeb/NewsSync/Exception.unknown' => 'An unknown error has occurred', + 'Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing' => 'Default language file "{0}" missing', + 'Exception.JKingWeb/NewsSync/Lang/Exception.fileMissing' => 'Language file "{0}" is not available', + 'Exception.JKingWeb/NewsSync/Lang/Exception.fileUnreadable' => 'Insufficient permissions to read language file "{0}"', + 'Exception.JKingWeb/NewsSync/Lang/Exception.fileCorrupt' => 'Language file "{0}" is corrupt or does not conform to expected format', + 'Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing' => 'Message string "{msgID}" missing from all loaded language files ({fileList})', + 'Exception.JKingWeb/NewsSync/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})', + ]; - static public $path = BASE."locale".DIRECTORY_SEPARATOR; - static protected $requirementsMet = false; - static protected $synched = false; - static protected $wanted = self::DEFAULT; - static protected $locale = ""; - static protected $loaded = []; - static protected $strings = self::REQUIRED; + static public $path = BASE."locale".DIRECTORY_SEPARATOR; + static protected $requirementsMet = false; + static protected $synched = false; + static protected $wanted = self::DEFAULT; + static protected $locale = ""; + static protected $loaded = []; + static protected $strings = self::REQUIRED; - protected function __construct() {} + protected function __construct() {} - static public function set(string $locale, bool $immediate = false): string { - if(!self::$requirementsMet) self::checkRequirements(); - if($locale==self::$wanted) return $locale; - if($locale != "") { - $list = self::listFiles(); - if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT); - self::$wanted = self::match($locale, $list); - } else { - self::$wanted = ""; - } - self::$synched = false; - if($immediate) self::load(); - return self::$wanted; - } + static public function set(string $locale, bool $immediate = false): string { + if(!self::$requirementsMet) self::checkRequirements(); + if($locale==self::$wanted) return $locale; + if($locale != "") { + $list = self::listFiles(); + if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT); + self::$wanted = self::match($locale, $list); + } else { + self::$wanted = ""; + } + self::$synched = false; + if($immediate) self::load(); + return self::$wanted; + } - static public function get(): string { - return (self::$locale=="") ? self::DEFAULT : self::$locale; - } + static public function get(): string { + return (self::$locale=="") ? self::DEFAULT : self::$locale; + } - static public function dump(): array { - return self::$strings; - } + static public function dump(): array { + return self::$strings; + } - static public function msg(string $msgID, $vars = null): string { - // if we're trying to load the system default language and it fails, we have a chicken and egg problem, so we catch the exception and load no language file instead - if(!self::$synched) try {self::load();} catch(Lang\Exception $e) { - if(self::$wanted==self::DEFAULT) { - self::set("", true); - } else { - throw $e; - } - } - if(!array_key_exists($msgID, self::$strings)) throw new Lang\Exception("stringMissing", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]); - // variables fed to MessageFormatter must be contained in array - $msg = self::$strings[$msgID]; - if($vars===null) { - return $msg; - } else if(!is_array($vars)) { - $vars = [$vars]; - } - $msg = \MessageFormatter::formatMessage(self::$locale, $msg, $vars); - if($msg===false) throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]); - return $msg; - } + static public function msg(string $msgID, $vars = null): string { + // if we're trying to load the system default language and it fails, we have a chicken and egg problem, so we catch the exception and load no language file instead + if(!self::$synched) try {self::load();} catch(Lang\Exception $e) { + if(self::$wanted==self::DEFAULT) { + self::set("", true); + } else { + throw $e; + } + } + if(!array_key_exists($msgID, self::$strings)) throw new Lang\Exception("stringMissing", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]); + // variables fed to MessageFormatter must be contained in array + $msg = self::$strings[$msgID]; + if($vars===null) { + return $msg; + } else if(!is_array($vars)) { + $vars = [$vars]; + } + $msg = \MessageFormatter::formatMessage(self::$locale, $msg, $vars); + if($msg===false) throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]); + return $msg; + } - static public function list(string $locale = ""): array { - $out = []; - $files = self::listFiles(); - foreach($files as $tag) { - $out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale); - } - return $out; - } + static public function list(string $locale = ""): array { + $out = []; + $files = self::listFiles(); + foreach($files as $tag) { + $out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale); + } + return $out; + } - static public function match(string $locale, array $list = null): string { - if($list===null) $list = self::listFiles(); - $default = (self::$locale=="") ? self::DEFAULT : self::$locale; - return \Locale::lookup($list,$locale, true, $default); - } + static public function match(string $locale, array $list = null): string { + if($list===null) $list = self::listFiles(); + $default = (self::$locale=="") ? self::DEFAULT : self::$locale; + return \Locale::lookup($list,$locale, true, $default); + } - static protected function checkRequirements(): bool { - if(!extension_loaded("intl")) throw new ExceptionFatal("The \"Intl\" extension is required, but not loaded"); - self::$requirementsMet = true; - return true; - } - - static protected function listFiles(): array { - $out = glob(self::$path."*.php"); - // built-in glob doesn't work with vfsStream (and this other glob doesn't seem to work with Windows paths), so we try both - if(empty($out)) $out = Glob::glob(self::$path."*.php"); - $out = array_map(function($file) { - $file = str_replace(DIRECTORY_SEPARATOR, "/", $file); - $file = substr($file, strrpos($file, "/")+1); - return strtolower(substr($file,0,strrpos($file,"."))); - },$out); - natsort($out); - return $out; - } + static protected function checkRequirements(): bool { + if(!extension_loaded("intl")) throw new ExceptionFatal("The \"Intl\" extension is required, but not loaded"); + self::$requirementsMet = true; + return true; + } - static protected function load(): bool { - if(!self::$requirementsMet) self::checkRequirements(); - // if we've requested no locale (""), just load the fallback strings and return - if(self::$wanted=="") { - self::$strings = self::REQUIRED; - self::$locale = self::$wanted; - self::$synched = true; - return true; - } - // decompose the requested locale from specific to general, building a list of files to load - $tags = \Locale::parseLocale(self::$wanted); - $files = []; - while(sizeof($tags) > 0) { - $files[] = strtolower(\Locale::composeLocale($tags)); - $tag = array_pop($tags); - } - // include the default locale as the base if the most general locale requested is not the default - if($tag != self::DEFAULT) $files[] = self::DEFAULT; - // save the list of files to be loaded for later reference - $loaded = $files; - // reduce the list of files to be loaded to the minimum necessary (e.g. if we go from "fr" to "fr_ca", we don't need to load "fr" or "en") - $files = []; - foreach($loaded as $file) { - if($file==self::$locale) break; - $files[] = $file; - } - // if we need to load all files, start with the fallback strings - $strings = []; - if($files==$loaded) { - $strings[] = self::REQUIRED; - } else { - // otherwise start with the strings we already have if we're going from e.g. "fr" to "fr_ca" - $strings[] = self::$strings; - } - // read files in reverse order - $files = array_reverse($files); - foreach($files as $file) { - if(!file_exists(self::$path."$file.php")) throw new Lang\Exception("fileMissing", $file); - if(!is_readable(self::$path."$file.php")) throw new Lang\Exception("fileUnreadable", $file); - try { - ob_start(); - $arr = (include self::$path."$file.php"); - } catch(\Throwable $e) { - $arr = null; - } finally { - ob_end_clean(); - } - if(!is_array($arr)) throw new Lang\Exception("fileCorrupt", $file); - $strings[] = $arr; - } - // apply the results and return - self::$strings = call_user_func_array("array_replace_recursive", $strings); - self::$loaded = $loaded; - self::$locale = self::$wanted; - return true; - } + static protected function listFiles(): array { + $out = glob(self::$path."*.php"); + // built-in glob doesn't work with vfsStream (and this other glob doesn't seem to work with Windows paths), so we try both + if(empty($out)) $out = Glob::glob(self::$path."*.php"); + $out = array_map(function($file) { + $file = str_replace(DIRECTORY_SEPARATOR, "/", $file); + $file = substr($file, strrpos($file, "/")+1); + return strtolower(substr($file,0,strrpos($file,"."))); + },$out); + natsort($out); + return $out; + } + + static protected function load(): bool { + if(!self::$requirementsMet) self::checkRequirements(); + // if we've requested no locale (""), just load the fallback strings and return + if(self::$wanted=="") { + self::$strings = self::REQUIRED; + self::$locale = self::$wanted; + self::$synched = true; + return true; + } + // decompose the requested locale from specific to general, building a list of files to load + $tags = \Locale::parseLocale(self::$wanted); + $files = []; + while(sizeof($tags) > 0) { + $files[] = strtolower(\Locale::composeLocale($tags)); + $tag = array_pop($tags); + } + // include the default locale as the base if the most general locale requested is not the default + if($tag != self::DEFAULT) $files[] = self::DEFAULT; + // save the list of files to be loaded for later reference + $loaded = $files; + // reduce the list of files to be loaded to the minimum necessary (e.g. if we go from "fr" to "fr_ca", we don't need to load "fr" or "en") + $files = []; + foreach($loaded as $file) { + if($file==self::$locale) break; + $files[] = $file; + } + // if we need to load all files, start with the fallback strings + $strings = []; + if($files==$loaded) { + $strings[] = self::REQUIRED; + } else { + // otherwise start with the strings we already have if we're going from e.g. "fr" to "fr_ca" + $strings[] = self::$strings; + } + // read files in reverse order + $files = array_reverse($files); + foreach($files as $file) { + if(!file_exists(self::$path."$file.php")) throw new Lang\Exception("fileMissing", $file); + if(!is_readable(self::$path."$file.php")) throw new Lang\Exception("fileUnreadable", $file); + try { + ob_start(); + $arr = (include self::$path."$file.php"); + } catch(\Throwable $e) { + $arr = null; + } finally { + ob_end_clean(); + } + if(!is_array($arr)) throw new Lang\Exception("fileCorrupt", $file); + $strings[] = $arr; + } + // apply the results and return + self::$strings = call_user_func_array("array_replace_recursive", $strings); + self::$loaded = $loaded; + self::$locale = self::$wanted; + return true; + } } \ No newline at end of file diff --git a/lib/Lang/Exception.php b/lib/Lang/Exception.php index e280322..8db223a 100644 --- a/lib/Lang/Exception.php +++ b/lib/Lang/Exception.php @@ -3,22 +3,22 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\Lang; class Exception extends \JKingWeb\NewsSync\AbstractException { - static $test = false; // used during PHPUnit testing only + static $test = false; // used during PHPUnit testing only - function __construct(string $msgID = "", $vars = null, \Throwable $e = null) { - if(!self::$test) { - parent::__construct($msgID, $vars, $e); - } else { - $codeID = "Lang/Exception.$msgID"; - if(!array_key_exists($codeID,self::CODES)) { - $code = -1; - $msg = "Exception.".str_replace("\\","/",parent::class).".uncoded"; - $vars = $msgID; - } else { - $code = self::CODES[$codeID]; - $msg = "Exception.".str_replace("\\","/",__CLASS__).".$msgID"; - } - \Exception::__construct($msg, $code, $e); - } - } + function __construct(string $msgID = "", $vars = null, \Throwable $e = null) { + if(!self::$test) { + parent::__construct($msgID, $vars, $e); + } else { + $codeID = "Lang/Exception.$msgID"; + if(!array_key_exists($codeID,self::CODES)) { + $code = -1; + $msg = "Exception.".str_replace("\\","/",parent::class).".uncoded"; + $vars = $msgID; + } else { + $code = self::CODES[$codeID]; + $msg = "Exception.".str_replace("\\","/",__CLASS__).".$msgID"; + } + \Exception::__construct($msg, $code, $e); + } + } } \ No newline at end of file diff --git a/lib/RuntimeData.php b/lib/RuntimeData.php index 54ca06f..42ac9b9 100644 --- a/lib/RuntimeData.php +++ b/lib/RuntimeData.php @@ -3,14 +3,14 @@ declare(strict_types=1); namespace JKingWeb\NewsSync; class RuntimeData { - public $conf; - public $db; - public $auth; + public $conf; + public $db; + public $auth; - public function __construct(Conf $conf) { - $this->conf = $conf; - Lang::set($conf->lang); - $this->db = new Database($this); - $this->user = new User($this); - } + public function __construct(Conf $conf) { + $this->conf = $conf; + Lang::set($conf->lang); + $this->db = new Database($this); + $this->user = new User($this); + } } \ No newline at end of file diff --git a/lib/User.php b/lib/User.php index ad7cc36..8730012 100644 --- a/lib/User.php +++ b/lib/User.php @@ -3,241 +3,241 @@ declare(strict_types=1); namespace JKingWeb\NewsSync; class User { - public $id = null; - - protected $data; - protected $u; - protected $authz = true; - protected $existSupported = 0; - protected $authzSupported = 0; - - static public function listDrivers(): array { - $sep = \DIRECTORY_SEPARATOR; - $path = __DIR__.$sep."User".$sep; - $classes = []; - foreach(glob($path."Driver?*.php") as $file) { - $name = basename($file, ".php"); - $name = NS_BASE."Db\\$name"; - if(class_exists($name)) { - $classes[$name] = $name::driverName(); - } - } - return $classes; - } - - public function __construct(\JKingWeb\NewsSync\RuntimeData $data) { - $this->data = $data; - $driver = $data->conf->userDriver; - $this->u = $driver::create($data); - $this->existSupported = $this->u->driverFunctions("userExists"); - $this->authzSupported = $this->u->driverFunctions("authorize"); - } - - public function __toString() { - if($this->id===null) $this->credentials(); - return (string) $this->id; - } - - public function credentials(): array { - if($this->data->conf->userAuthPreferHTTP) { - return $this->credentialsHTTP(); - } else { - return $this->credentialsForm(); - } - } - - public function credentialsForm(): array { - // FIXME: stub - $this->id = "john.doe@example.com"; - return ["user" => "john.doe@example.com", "password" => "secret"]; - } - - public function credentialsHTTP(): array { - if($_SERVER['PHP_AUTH_USER']) { - $out = ["user" => $_SERVER['PHP_AUTH_USER'], "password" => $_SERVER['PHP_AUTH_PW']]; - } else if($_SERVER['REMOTE_USER']) { - $out = ["user" => $_SERVER['REMOTE_USER'], "password" => ""]; - } else { - $out = ["user" => "", "password" => ""]; - } - if($this->data->conf->userComposeNames && $out["user"] != "") { - $out["user"] = $this->composeName($out["user"]); - } - $this->id = $out["user"]; - return $out; - } - - public function auth(string $user = null, string $password = null): bool { - if($user===null) { - if($this->data->conf->userAuthPreferHTTP) return $this->authHTTP(); - return $this->authForm(); - } else { - if($this->u->auth($user, $password)) { - $this->authPostProcess($user, $password); - return true; - } - return false; - } - } - - public function authForm(): bool { - $cred = $this->credentialsForm(); - if(!$cred["user"]) return $this->challengeForm(); - if(!$this->u->auth($cred["user"], $cred["password"])) return $this->challengeForm(); - $this->authPostProcess($cred["user"], $cred["password"]); - return true; - } - - public function authHTTP(): bool { - $cred = $this->credentialsHTTP(); - if(!$cred["user"]) return $this->challengeHTTP(); - if(!$this->u->auth($cred["user"], $cred["password"])) return $this->challengeHTTP(); - $this->authPostProcess($cred["user"], $cred["password"]); - return true; - } - - public function driverFunctions(string $function = null) { - return $this->u->driverFunctions($function); - } - - public function list(string $domain = null): array { - if($this->u->driverFunctions("userList")==User\Driver::FUNC_EXTERNAL) { - if($domain===null) { - if(!$this->data->user->authorize("@".$domain, "userList")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userList", "user" => $domain]); - } else { - if(!$this->data->user->authorize("", "userList")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userList", "user" => "all users"]); - } - return $this->u->userList($domain); - } else { - return $this->data->db->userList($domain); - } - } - - public function authorize(string $affectedUser, string $action, int $promoteLevel = 0): bool { - if(!$this->authz) return true; - if($this->id===null) $this->credentials(); - if($this->authzSupported) return $this->u->authorize($affectedUser, $action, $promoteLevel); - // if the driver does not implement authorization, only allow operation for the current user (this means no new users can be added) - if($affectedUser==$this->id && $action != "userRightsSet") return true; - return false; - } - - public function authorizationEnabled(bool $setting = null): bool { - if($setting===null) return $this->authz; - $this->authz = $setting; - return $setting; - } - - public function exists(string $user): bool { - if($this->u->driverFunctions("userExists") != User\Driver::FUNC_INTERNAL) { - if(!$this->data->user->authorize($user, "userExists")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userExists", "user" => $user]); - } - if(!$this->existSupported) return true; - $out = $this->u->userExists($user); - if($out && $this->existSupported==User\Driver::FUNC_EXTERNAL && !$this->data->db->userExist($user)) { - try {$this->data->db->userAdd($user);} catch(\Throwable $e) {} - } - return $out; - } - - public function add($user, $password = null): bool { - if($this->u->driverFunctions("userAdd") != User\Driver::FUNC_INTERNAL) { - if(!$this->data->user->authorize($user, "userAdd")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userAdd", "user" => $user]); - } - if($this->exists($user)) return false; - $out = $this->u->userAdd($user, $password); - if($out && $this->u->driverFunctions("userAdd") != User\Driver::FUNC_INTERNAL) { - try { - if(!$this->data->db->userExists($user)) $this->data->db->userAdd($user, $password); - } catch(\Throwable $e) {} - } - return $out; - } - - public function remove(string $user): bool { - if($this->u->driverFunctions("userRemove") != User\Driver::FUNC_INTERNAL) { - if(!$this->data->user->authorize($user, "userRemove")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRemove", "user" => $user]); - } - if(!$this->exists($user)) return false; - $out = $this->u->userRemove($user); - if($out && $this->u->driverFunctions("userRemove") != User\Driver::FUNC_INTERNAL) { - try { - if($this->data->db->userExists($user)) $this->data->db->userRemove($user); - } catch(\Throwable $e) {} - } - return $out; - } - - public function passwordSet(string $user, string $password): bool { - if($this->u->driverFunctions("userPasswordSet") != User\Driver::FUNC_INTERNAL) { - if(!$this->data->user->authorize($user, "userPasswordSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPasswordSet", "user" => $user]); - } - if(!$this->exists($user)) return false; - return $this->u->userPasswordSet($user, $password); - } - - public function propertiesGet(string $user): array { - if($this->u->driverFunctions("userPropertiesGet") != User\Driver::FUNC_INTERNAL) { - if(!$this->data->user->authorize($user, "userPropertiesGet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPropertiesGet", "user" => $user]); - } - if(!$this->exists($user)) return false; - $domain = null; - if($this->data->conf->userComposeNames) $domain = substr($user,strrpos($user,"@")+1); - $init = [ - "id" => $user, - "name" => $user, - "rights" => User\Driver::RIGHTS_NONE, - "domain" => $domain - ]; - if($this->u->driverFunctions("userPropertiesGet") != User\Driver::FUNC_NOT_IMPLEMENTED) { - return array_merge($init, $this->u->userPropertiesGet($user)); - } - return $init; - } - - public function propertiesSet(string $user, array $properties): array { - if($this->u->driverFunctions("userPropertiesSet") != User\Driver::FUNC_INTERNAL) { - if(!$this->data->user->authorize($user, "userPropertiesSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPropertiesSet", "user" => $user]); - } - if(!$this->exists($user)) throw new User\Exception("doesNotExist", ["user" => $user, "action" => "userPropertiesSet"]); - return $this->u->userPropertiesSet($user, $properties); - } - - public function rightsGet(string $user): int { - if($this->u->driverFunctions("userRightsGet") != User\Driver::FUNC_INTERNAL) { - if(!$this->data->user->authorize($user, "userRightsGet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRightsGet", "user" => $user]); - } - // we do not throw an exception here if the user does not exist, because it makes no material difference - if(!$this->exists($user)) return User\Driver::RIGHTS_NONE; - return $this->u->userRightsGet($user); - } - - public function rightsSet(string $user, int $level): bool { - if($this->u->driverFunctions("userRightsSet") != User\Driver::FUNC_INTERNAL) { - if(!$this->data->user->authorize($user, "userRightsSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRightsSet", "user" => $user]); - } - if(!$this->exists($user)) return false; - return $this->u->userRightsSet($user, $level); - } - - // FIXME: stubs - public function challenge(): bool {throw new User\Exception("authFailed");} - public function challengeForm(): bool {throw new User\Exception("authFailed");} - public function challengeHTTP(): bool {throw new User\Exception("authFailed");} - - protected function composeName(string $user): string { - if(preg_match("/.+?@[^@]+$/",$user)) { - return $user; - } else { - return $user."@".$_SERVER['HTTP_HOST']; - } - } - - protected function authPostprocess(string $user, string $password): bool { - if($this->u->driverFunctions("auth") != User\Driver::FUNC_INTERNAL && !$this->data->db->userExists($user)) { - if($password=="") $password = null; - try {$this->data->db->userAdd($user, $password);} catch(\Throwable $e) {} - } - return true; - } + public $id = null; + + protected $data; + protected $u; + protected $authz = true; + protected $existSupported = 0; + protected $authzSupported = 0; + + static public function listDrivers(): array { + $sep = \DIRECTORY_SEPARATOR; + $path = __DIR__.$sep."User".$sep; + $classes = []; + foreach(glob($path."Driver?*.php") as $file) { + $name = basename($file, ".php"); + $name = NS_BASE."Db\\$name"; + if(class_exists($name)) { + $classes[$name] = $name::driverName(); + } + } + return $classes; + } + + public function __construct(\JKingWeb\NewsSync\RuntimeData $data) { + $this->data = $data; + $driver = $data->conf->userDriver; + $this->u = $driver::create($data); + $this->existSupported = $this->u->driverFunctions("userExists"); + $this->authzSupported = $this->u->driverFunctions("authorize"); + } + + public function __toString() { + if($this->id===null) $this->credentials(); + return (string) $this->id; + } + + public function credentials(): array { + if($this->data->conf->userAuthPreferHTTP) { + return $this->credentialsHTTP(); + } else { + return $this->credentialsForm(); + } + } + + public function credentialsForm(): array { + // FIXME: stub + $this->id = "john.doe@example.com"; + return ["user" => "john.doe@example.com", "password" => "secret"]; + } + + public function credentialsHTTP(): array { + if($_SERVER['PHP_AUTH_USER']) { + $out = ["user" => $_SERVER['PHP_AUTH_USER'], "password" => $_SERVER['PHP_AUTH_PW']]; + } else if($_SERVER['REMOTE_USER']) { + $out = ["user" => $_SERVER['REMOTE_USER'], "password" => ""]; + } else { + $out = ["user" => "", "password" => ""]; + } + if($this->data->conf->userComposeNames && $out["user"] != "") { + $out["user"] = $this->composeName($out["user"]); + } + $this->id = $out["user"]; + return $out; + } + + public function auth(string $user = null, string $password = null): bool { + if($user===null) { + if($this->data->conf->userAuthPreferHTTP) return $this->authHTTP(); + return $this->authForm(); + } else { + if($this->u->auth($user, $password)) { + $this->authPostProcess($user, $password); + return true; + } + return false; + } + } + + public function authForm(): bool { + $cred = $this->credentialsForm(); + if(!$cred["user"]) return $this->challengeForm(); + if(!$this->u->auth($cred["user"], $cred["password"])) return $this->challengeForm(); + $this->authPostProcess($cred["user"], $cred["password"]); + return true; + } + + public function authHTTP(): bool { + $cred = $this->credentialsHTTP(); + if(!$cred["user"]) return $this->challengeHTTP(); + if(!$this->u->auth($cred["user"], $cred["password"])) return $this->challengeHTTP(); + $this->authPostProcess($cred["user"], $cred["password"]); + return true; + } + + public function driverFunctions(string $function = null) { + return $this->u->driverFunctions($function); + } + + public function list(string $domain = null): array { + if($this->u->driverFunctions("userList")==User\Driver::FUNC_EXTERNAL) { + if($domain===null) { + if(!$this->data->user->authorize("@".$domain, "userList")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userList", "user" => $domain]); + } else { + if(!$this->data->user->authorize("", "userList")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userList", "user" => "all users"]); + } + return $this->u->userList($domain); + } else { + return $this->data->db->userList($domain); + } + } + + public function authorize(string $affectedUser, string $action, int $promoteLevel = 0): bool { + if(!$this->authz) return true; + if($this->id===null) $this->credentials(); + if($this->authzSupported) return $this->u->authorize($affectedUser, $action, $promoteLevel); + // if the driver does not implement authorization, only allow operation for the current user (this means no new users can be added) + if($affectedUser==$this->id && $action != "userRightsSet") return true; + return false; + } + + public function authorizationEnabled(bool $setting = null): bool { + if($setting===null) return $this->authz; + $this->authz = $setting; + return $setting; + } + + public function exists(string $user): bool { + if($this->u->driverFunctions("userExists") != User\Driver::FUNC_INTERNAL) { + if(!$this->data->user->authorize($user, "userExists")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userExists", "user" => $user]); + } + if(!$this->existSupported) return true; + $out = $this->u->userExists($user); + if($out && $this->existSupported==User\Driver::FUNC_EXTERNAL && !$this->data->db->userExist($user)) { + try {$this->data->db->userAdd($user);} catch(\Throwable $e) {} + } + return $out; + } + + public function add($user, $password = null): bool { + if($this->u->driverFunctions("userAdd") != User\Driver::FUNC_INTERNAL) { + if(!$this->data->user->authorize($user, "userAdd")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userAdd", "user" => $user]); + } + if($this->exists($user)) return false; + $out = $this->u->userAdd($user, $password); + if($out && $this->u->driverFunctions("userAdd") != User\Driver::FUNC_INTERNAL) { + try { + if(!$this->data->db->userExists($user)) $this->data->db->userAdd($user, $password); + } catch(\Throwable $e) {} + } + return $out; + } + + public function remove(string $user): bool { + if($this->u->driverFunctions("userRemove") != User\Driver::FUNC_INTERNAL) { + if(!$this->data->user->authorize($user, "userRemove")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRemove", "user" => $user]); + } + if(!$this->exists($user)) return false; + $out = $this->u->userRemove($user); + if($out && $this->u->driverFunctions("userRemove") != User\Driver::FUNC_INTERNAL) { + try { + if($this->data->db->userExists($user)) $this->data->db->userRemove($user); + } catch(\Throwable $e) {} + } + return $out; + } + + public function passwordSet(string $user, string $password): bool { + if($this->u->driverFunctions("userPasswordSet") != User\Driver::FUNC_INTERNAL) { + if(!$this->data->user->authorize($user, "userPasswordSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPasswordSet", "user" => $user]); + } + if(!$this->exists($user)) return false; + return $this->u->userPasswordSet($user, $password); + } + + public function propertiesGet(string $user): array { + if($this->u->driverFunctions("userPropertiesGet") != User\Driver::FUNC_INTERNAL) { + if(!$this->data->user->authorize($user, "userPropertiesGet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPropertiesGet", "user" => $user]); + } + if(!$this->exists($user)) return false; + $domain = null; + if($this->data->conf->userComposeNames) $domain = substr($user,strrpos($user,"@")+1); + $init = [ + "id" => $user, + "name" => $user, + "rights" => User\Driver::RIGHTS_NONE, + "domain" => $domain + ]; + if($this->u->driverFunctions("userPropertiesGet") != User\Driver::FUNC_NOT_IMPLEMENTED) { + return array_merge($init, $this->u->userPropertiesGet($user)); + } + return $init; + } + + public function propertiesSet(string $user, array $properties): array { + if($this->u->driverFunctions("userPropertiesSet") != User\Driver::FUNC_INTERNAL) { + if(!$this->data->user->authorize($user, "userPropertiesSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPropertiesSet", "user" => $user]); + } + if(!$this->exists($user)) throw new User\Exception("doesNotExist", ["user" => $user, "action" => "userPropertiesSet"]); + return $this->u->userPropertiesSet($user, $properties); + } + + public function rightsGet(string $user): int { + if($this->u->driverFunctions("userRightsGet") != User\Driver::FUNC_INTERNAL) { + if(!$this->data->user->authorize($user, "userRightsGet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRightsGet", "user" => $user]); + } + // we do not throw an exception here if the user does not exist, because it makes no material difference + if(!$this->exists($user)) return User\Driver::RIGHTS_NONE; + return $this->u->userRightsGet($user); + } + + public function rightsSet(string $user, int $level): bool { + if($this->u->driverFunctions("userRightsSet") != User\Driver::FUNC_INTERNAL) { + if(!$this->data->user->authorize($user, "userRightsSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRightsSet", "user" => $user]); + } + if(!$this->exists($user)) return false; + return $this->u->userRightsSet($user, $level); + } + + // FIXME: stubs + public function challenge(): bool {throw new User\Exception("authFailed");} + public function challengeForm(): bool {throw new User\Exception("authFailed");} + public function challengeHTTP(): bool {throw new User\Exception("authFailed");} + + protected function composeName(string $user): string { + if(preg_match("/.+?@[^@]+$/",$user)) { + return $user; + } else { + return $user."@".$_SERVER['HTTP_HOST']; + } + } + + protected function authPostprocess(string $user, string $password): bool { + if($this->u->driverFunctions("auth") != User\Driver::FUNC_INTERNAL && !$this->data->db->userExists($user)) { + if($password=="") $password = null; + try {$this->data->db->userAdd($user, $password);} catch(\Throwable $e) {} + } + return true; + } } \ No newline at end of file diff --git a/lib/User/Driver.php b/lib/User/Driver.php index 245a395..eed2e25 100644 --- a/lib/User/Driver.php +++ b/lib/User/Driver.php @@ -3,28 +3,28 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\User; Interface Driver { - const FUNC_NOT_IMPLEMENTED = 0; - const FUNC_INTERNAL = 1; - const FUNC_EXTERNAL = 2; + const FUNC_NOT_IMPLEMENTED = 0; + const FUNC_INTERNAL = 1; + const FUNC_EXTERNAL = 2; - const RIGHTS_NONE = 0; - const RIGHTS_DOMAIN_MANAGER = 25; - const RIGHTS_DOMAIN_ADMIN = 50; - const RIGHTS_GLOBAL_MANAGER = 75; - const RIGHTS_GLOBAL_ADMIN = 100; + const RIGHTS_NONE = 0; + const RIGHTS_DOMAIN_MANAGER = 25; + const RIGHTS_DOMAIN_ADMIN = 50; + const RIGHTS_GLOBAL_MANAGER = 75; + const RIGHTS_GLOBAL_ADMIN = 100; - static function create(\JKingWeb\NewsSync\RuntimeData $data): Driver; - static function driverName(): string; - function driverFunctions(string $function = null); - function auth(string $user, string $password): bool; - function authorize(string $affectedUser, string $action): bool; - function userExists(string $user): bool; - function userAdd(string $user, string $password = null): bool; - function userRemove(string $user): bool; - function userList(string $domain = null): array; - function userPasswordSet(string $user, string $newPassword, string $oldPassword): bool; - function userPropertiesGet(string $user): array; - function userPropertiesSet(string $user, array $properties): array; - function userRightsGet(string $user): int; - function userRightsSet(string $user, int $level): bool; + static function create(\JKingWeb\NewsSync\RuntimeData $data): Driver; + static function driverName(): string; + function driverFunctions(string $function = null); + function auth(string $user, string $password): bool; + function authorize(string $affectedUser, string $action): bool; + function userExists(string $user): bool; + function userAdd(string $user, string $password = null): bool; + function userRemove(string $user): bool; + function userList(string $domain = null): array; + function userPasswordSet(string $user, string $newPassword, string $oldPassword): bool; + function userPropertiesGet(string $user): array; + function userPropertiesSet(string $user, array $properties): array; + function userRightsGet(string $user): int; + function userRightsSet(string $user, int $level): bool; } \ No newline at end of file diff --git a/lib/User/DriverInternal.php b/lib/User/DriverInternal.php index fbbe22e..7e85cf3 100644 --- a/lib/User/DriverInternal.php +++ b/lib/User/DriverInternal.php @@ -3,43 +3,43 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\User; class DriverInternal implements Driver { - use InternalFunctions; + use InternalFunctions; - protected $data; - protected $db; - protected $functions = [ - "auth" => Driver::FUNC_INTERNAL, - "authorize" => 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, - ]; + protected $data; + protected $db; + protected $functions = [ + "auth" => Driver::FUNC_INTERNAL, + "authorize" => 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 create(\JKingWeb\NewsSync\RuntimeData $data): Driver { + return new static($data); + } - public function __construct(\JKingWeb\NewsSync\RuntimeData $data) { - $this->data = $data; - $this->db = $this->data->db; - } + public function __construct(\JKingWeb\NewsSync\RuntimeData $data) { + $this->data = $data; + $this->db = $this->data->db; + } - static public function driverName(): string { - return "Internal"; - } + static public function driverName(): string { + return "Internal"; + } - 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; - } - } + 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; + } + } } \ No newline at end of file diff --git a/lib/User/InternalFunctions.php b/lib/User/InternalFunctions.php index 5912b77..43fc285 100644 --- a/lib/User/InternalFunctions.php +++ b/lib/User/InternalFunctions.php @@ -2,78 +2,78 @@ declare(strict_types=1); namespace JKingWeb\NewsSync\User; -trait InternalFunctions { - protected $actor = []; - - function auth(string $user, string $password): bool { - if(!$this->data->user->exists($user)) return false; - $hash = $this->db->userPasswordGet($user); - if(!$hash) return false; - return password_verify($password, $hash); - } +trait InternalFunctions { + protected $actor = []; + + function auth(string $user, string $password): bool { + if(!$this->data->user->exists($user)) return false; + $hash = $this->db->userPasswordGet($user); + if(!$hash) return false; + return password_verify($password, $hash); + } - function authorize(string $affectedUser, string $action, int $newRightsLevel = 0): bool { - // if the affected user is the actor and the actor is not trying to grant themselves rights, accept the request - if($affectedUser==$this->data->user->id && $action != "userRightsSet") return true; - // get properties of actor if not already available - if(!sizeof($this->actor)) $this->actor = $this->data->user->propertiesGet($this->data->user->id); - $rights =& $this->actor["rights"]; - // if actor is a global admin, accept the request - if($rights==self::RIGHTS_GLOBAL_ADMIN) return true; - // if actor is a common user, deny the request - if($rights==self::RIGHTS_NONE) return false; - // if actor is not some other sort of admin, deny the request - if(!in_array($rights,[self::RIGHTS_GLOBAL_MANAGER,self::RIGHTS_DOMAIN_MANAGER,self::RIGHTS_DOMAIN_ADMIN],true)) return false; - // if actor is a domain admin/manager and domains don't match, deny the request - if($this->data->conf->userComposeNames && $this->actor["domain"] && $rights != self::RIGHTS_GLOBAL_MANAGER) { - $test = "@".$this->actor["domain"]; - if(substr($affectedUser,-1*strlen($test)) != $test) return false; - } - // certain actions shouldn't check affected user's rights - if(in_array($action, ["userRightsGet","userExists","userList"], true)) return true; - if($action=="userRightsSet") { - // setting rights above your own (or equal to your own, for managers) is not allowed - if($newRightsLevel > $rights || ($rights != self::RIGHTS_DOMAIN_ADMIN && $newRightsLevel==$rights)) return false; - } - $affectedRights = $this->data->user->rightsGet($affectedUser); - // acting for users with rights greater than your own (or equal, for managers) is not allowed - if($affectedRights > $rights || ($rights != self::RIGHTS_DOMAIN_ADMIN && $affectedRights==$rights)) return false; - return true; - } + function authorize(string $affectedUser, string $action, int $newRightsLevel = 0): bool { + // if the affected user is the actor and the actor is not trying to grant themselves rights, accept the request + if($affectedUser==$this->data->user->id && $action != "userRightsSet") return true; + // get properties of actor if not already available + if(!sizeof($this->actor)) $this->actor = $this->data->user->propertiesGet($this->data->user->id); + $rights =& $this->actor["rights"]; + // if actor is a global admin, accept the request + if($rights==self::RIGHTS_GLOBAL_ADMIN) return true; + // if actor is a common user, deny the request + if($rights==self::RIGHTS_NONE) return false; + // if actor is not some other sort of admin, deny the request + if(!in_array($rights,[self::RIGHTS_GLOBAL_MANAGER,self::RIGHTS_DOMAIN_MANAGER,self::RIGHTS_DOMAIN_ADMIN],true)) return false; + // if actor is a domain admin/manager and domains don't match, deny the request + if($this->data->conf->userComposeNames && $this->actor["domain"] && $rights != self::RIGHTS_GLOBAL_MANAGER) { + $test = "@".$this->actor["domain"]; + if(substr($affectedUser,-1*strlen($test)) != $test) return false; + } + // certain actions shouldn't check affected user's rights + if(in_array($action, ["userRightsGet","userExists","userList"], true)) return true; + if($action=="userRightsSet") { + // setting rights above your own (or equal to your own, for managers) is not allowed + if($newRightsLevel > $rights || ($rights != self::RIGHTS_DOMAIN_ADMIN && $newRightsLevel==$rights)) return false; + } + $affectedRights = $this->data->user->rightsGet($affectedUser); + // acting for users with rights greater than your own (or equal, for managers) is not allowed + if($affectedRights > $rights || ($rights != self::RIGHTS_DOMAIN_ADMIN && $affectedRights==$rights)) return false; + return true; + } - function userExists(string $user): bool { - return $this->db->userExists($user); - } + function userExists(string $user): bool { + return $this->db->userExists($user); + } - function userAdd(string $user, string $password = null): bool { - return $this->db->userAdd($user, $password); - } + function userAdd(string $user, string $password = null): bool { + return $this->db->userAdd($user, $password); + } - function userRemove(string $user): bool { - return $this->db->userRemove($user); - } + function userRemove(string $user): bool { + return $this->db->userRemove($user); + } - function userList(string $domain = null): array { - return $this->db->userList($domain); - } - - function userPasswordSet(string $user, string $newPassword, string $oldPassword): bool { - return $this->db->userPasswordSet($user, $newPassword); - } + function userList(string $domain = null): array { + return $this->db->userList($domain); + } + + function userPasswordSet(string $user, string $newPassword, string $oldPassword): bool { + return $this->db->userPasswordSet($user, $newPassword); + } - function userPropertiesGet(string $user): array { - return $this->db->userPropertiesGet($user); - } + function userPropertiesGet(string $user): array { + return $this->db->userPropertiesGet($user); + } - function userPropertiesSet(string $user, array $properties): array { - return $this->db->userPropertiesSet($user, $properties); - } + function userPropertiesSet(string $user, array $properties): array { + return $this->db->userPropertiesSet($user, $properties); + } - function userRightsGet(string $user): int { - return $this->db->userRightsGet($user); - } - - function userRightsSet(string $user, int $level): bool { - return $this->db->userRightsSet($user, $level); - } + function userRightsGet(string $user): int { + return $this->db->userRightsGet($user); + } + + function userRightsSet(string $user, int $level): bool { + return $this->db->userRightsSet($user, $level); + } } \ No newline at end of file diff --git a/locale/en.php b/locale/en.php index 163392f..e6eb3b7 100644 --- a/locale/en.php +++ b/locale/en.php @@ -1,52 +1,52 @@ 'The specified exception symbol {0} has no code specified in Exception.php', - //this should not usually be encountered - 'Exception.JKingWeb/NewsSync/Exception.unknown' => 'An unknown error has occurred', + 'Exception.JKingWeb/NewsSync/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in Exception.php', + //this should not usually be encountered + 'Exception.JKingWeb/NewsSync/Exception.unknown' => 'An unknown error has occurred', - 'Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing' => 'Default language file "{0}" missing', - 'Exception.JKingWeb/NewsSync/Lang/Exception.fileMissing' => 'Language file "{0}" is not available', - 'Exception.JKingWeb/NewsSync/Lang/Exception.fileUnreadable' => 'Insufficient permissions to read language file "{0}"', - 'Exception.JKingWeb/NewsSync/Lang/Exception.fileCorrupt' => 'Language file "{0}" is corrupt or does not conform to expected format', - 'Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing' => 'Message string "{msgID}" missing from all loaded language files ({fileList})', - 'Exception.JKingWeb/NewsSync/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})', + 'Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing' => 'Default language file "{0}" missing', + 'Exception.JKingWeb/NewsSync/Lang/Exception.fileMissing' => 'Language file "{0}" is not available', + 'Exception.JKingWeb/NewsSync/Lang/Exception.fileUnreadable' => 'Insufficient permissions to read language file "{0}"', + 'Exception.JKingWeb/NewsSync/Lang/Exception.fileCorrupt' => 'Language file "{0}" is corrupt or does not conform to expected format', + 'Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing' => 'Message string "{msgID}" missing from all loaded language files ({fileList})', + 'Exception.JKingWeb/NewsSync/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})', - 'Exception.JKingWeb/NewsSync/Conf/Exception.fileMissing' => 'Configuration file "{0}" does not exist', - 'Exception.JKingWeb/NewsSync/Conf/Exception.fileUnreadable' => 'Insufficient permissions to read configuration file "{0}"', - 'Exception.JKingWeb/NewsSync/Conf/Exception.fileUncreatable' => 'Insufficient permissions to write new configuration file "{0}"', - 'Exception.JKingWeb/NewsSync/Conf/Exception.fileUnwritable' => '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/Update/Exception.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' => - '{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' => - '{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}} - }', + 'Exception.JKingWeb/NewsSync/Conf/Exception.fileMissing' => 'Configuration file "{0}" does not exist', + 'Exception.JKingWeb/NewsSync/Conf/Exception.fileUnreadable' => 'Insufficient permissions to read configuration file "{0}"', + 'Exception.JKingWeb/NewsSync/Conf/Exception.fileUncreatable' => 'Insufficient permissions to write new configuration file "{0}"', + 'Exception.JKingWeb/NewsSync/Conf/Exception.fileUnwritable' => '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/User/Exception.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists', - 'Exception.JKingWeb/NewsSync/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist', - 'Exception.JKingWeb/NewsSync/User/Exception.authMissing' => 'Please log in to proceed', - 'Exception.JKingWeb/NewsSync/User/Exception.authFailed' => 'Authentication failed', - 'Exception.JKingWeb/NewsSync/User/ExceptionAuthz.notAuthorized' => 'Authenticated user is not authorized to perform the action "{action}" on behalf of {user}', + '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/Update/Exception.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' => + '{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' => + '{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}} + }', + + 'Exception.JKingWeb/NewsSync/User/Exception.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists', + 'Exception.JKingWeb/NewsSync/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist', + 'Exception.JKingWeb/NewsSync/User/Exception.authMissing' => 'Please log in to proceed', + 'Exception.JKingWeb/NewsSync/User/Exception.authFailed' => 'Authentication failed', + 'Exception.JKingWeb/NewsSync/User/ExceptionAuthz.notAuthorized' => 'Authenticated user is not authorized to perform the action "{action}" on behalf of {user}', ]; \ No newline at end of file diff --git a/sql/SQLite3/0.sql b/sql/SQLite3/0.sql index 9611792..59c38bb 100644 --- a/sql/SQLite3/0.sql +++ b/sql/SQLite3/0.sql @@ -1,110 +1,110 @@ -- 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, -- time at which the feed last actually changed - etag TEXT, -- HTTP ETag hash used for cache validation, changes each time the content changes - 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 not null default '', -- HTTP authentication username - password TEXT not null default '', -- HTTP authentication password (this is stored in plain text) - unique(url,username,password) -- a URL with particular credentials should only appear once + 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, -- time at which the feed last actually changed + etag TEXT, -- HTTP ETag hash used for cache validation, changes each time the content changes + 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 not null default '', -- HTTP authentication username + password TEXT not null default '', -- HTTP authentication password (this is stored in plain text) + unique(url,username,password) -- a URL with particular credentials should only appear once ); -- entries in newsfeeds create table newssync_articles( - id integer primary key not null, -- sequence number - feed integer not null references newssync_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 + id integer primary key not null, -- sequence number + feed integer not null references newssync_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 ); -- enclosures associated with articles create table newssync_enclosures( - article integer not null references newssync_articles(id) on delete cascade, - url TEXT, - type varchar(255) + article integer not null references newssync_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 not null references newssync_articles(id) on delete cascade, - name TEXT + 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' -- + 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 + 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 + 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 + 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, - article integer not null references newssync_articles(id) on delete cascade, - read boolean not null default 0, - starred boolean not null default 0, - modified datetime not null default CURRENT_TIMESTAMP + id integer primary key not null, + article integer not null references newssync_articles(id) on delete cascade, + read boolean not null default 0, + starred boolean not null default 0, + modified datetime not null default CURRENT_TIMESTAMP ); -- user labels associated with newsfeed entries create table newssync_labels( - sub_article integer not null references newssync_subscription_articles(id) on delete cascade, -- - owner TEXT not null references newssync_users(id) on delete cascade on update cascade, - name TEXT + sub_article integer not null references newssync_subscription_articles(id) on delete cascade, -- + owner TEXT not null references newssync_users(id) on delete cascade on update cascade, + name TEXT ); create index newssync_label_names on newssync_labels(name); diff --git a/tests/TestConf.php b/tests/TestConf.php index fbbacf6..e7fcd82 100644 --- a/tests/TestConf.php +++ b/tests/TestConf.php @@ -5,101 +5,101 @@ use \org\bovigo\vfs\vfsStream; class TestConf extends \PHPUnit\Framework\TestCase { - use Test\Tools; - - static $vfs; - static $path; + use Test\Tools; + + static $vfs; + static $path; - static function setUpBeforeClass() { - self::$vfs = vfsStream::setup("root", null, [ - 'confGood' => ' "xx");', - 'confNotArray' => ' ' 'DEAD BEEF', - 'confEmpty' => '', - 'confUnreadable' => '', - ]); - self::$path = self::$vfs->url()."/"; - // set up a file without read access - chmod(self::$path."confUnreadable", 0000); - } + static function setUpBeforeClass() { + self::$vfs = vfsStream::setup("root", null, [ + 'confGood' => ' "xx");', + 'confNotArray' => ' ' 'DEAD BEEF', + 'confEmpty' => '', + 'confUnreadable' => '', + ]); + self::$path = self::$vfs->url()."/"; + // set up a file without read access + chmod(self::$path."confUnreadable", 0000); + } - static function tearDownAfterClass() { - self::$path = null; - self::$vfs = null; - } - - function testLoadDefaultValues() { - $this->assertInstanceOf(Conf::class, new Conf()); - } + static function tearDownAfterClass() { + self::$path = null; + self::$vfs = null; + } + + function testLoadDefaultValues() { + $this->assertInstanceOf(Conf::class, new Conf()); + } - /** + /** * @depends testLoadDefaultValues */ - function testImportFromArray() { - $arr = ['lang' => "xx"]; - $conf = new Conf(); - $conf->import($arr); - $this->assertEquals("xx", $conf->lang); - } + function testImportFromArray() { + $arr = ['lang' => "xx"]; + $conf = new Conf(); + $conf->import($arr); + $this->assertEquals("xx", $conf->lang); + } - /** + /** * @depends testImportFromArray */ - function testImportFromFile() { - $conf = new Conf(); - $conf->importFile(self::$path."confGood"); - $this->assertEquals("xx", $conf->lang); - $conf = new Conf(self::$path."confGood"); - $this->assertEquals("xx", $conf->lang); - } + function testImportFromFile() { + $conf = new Conf(); + $conf->importFile(self::$path."confGood"); + $this->assertEquals("xx", $conf->lang); + $conf = new Conf(self::$path."confGood"); + $this->assertEquals("xx", $conf->lang); + } - /** + /** * @depends testImportFromFile */ - function testImportFromMissingFile() { - $this->assertException("fileMissing", "Conf"); - $conf = new Conf(self::$path."confMissing"); - } + function testImportFromMissingFile() { + $this->assertException("fileMissing", "Conf"); + $conf = new Conf(self::$path."confMissing"); + } - /** + /** * @depends testImportFromFile */ - function testImportFromEmptyFile() { - $this->assertException("fileCorrupt", "Conf"); - $conf = new Conf(self::$path."confEmpty"); - } + function testImportFromEmptyFile() { + $this->assertException("fileCorrupt", "Conf"); + $conf = new Conf(self::$path."confEmpty"); + } - /** + /** * @depends testImportFromFile */ - function testImportFromFileWithoutReadPermission() { - $this->assertException("fileUnreadable", "Conf"); - $conf = new Conf(self::$path."confUnreadable"); - } + function testImportFromFileWithoutReadPermission() { + $this->assertException("fileUnreadable", "Conf"); + $conf = new Conf(self::$path."confUnreadable"); + } - /** + /** * @depends testImportFromFile */ - function testImportFromFileWhichIsNotAnArray() { - $this->assertException("fileCorrupt", "Conf"); - $conf = new Conf(self::$path."confNotArray"); - } + function testImportFromFileWhichIsNotAnArray() { + $this->assertException("fileCorrupt", "Conf"); + $conf = new Conf(self::$path."confNotArray"); + } - /** + /** * @depends testImportFromFile */ - function testImportFromFileWhichIsNotPhp() { - $this->assertException("fileCorrupt", "Conf"); - // this should not print the output of the non-PHP file - $conf = new Conf(self::$path."confNotPHP"); - } + function testImportFromFileWhichIsNotPhp() { + $this->assertException("fileCorrupt", "Conf"); + // this should not print the output of the non-PHP file + $conf = new Conf(self::$path."confNotPHP"); + } - /** + /** * @depends testImportFromFile */ - function testImportFromCorruptFile() { - $this->assertException("fileCorrupt", "Conf"); - $conf = new Conf(self::$path."confCorrupt"); - } + function testImportFromCorruptFile() { + $this->assertException("fileCorrupt", "Conf"); + $conf = new Conf(self::$path."confCorrupt"); + } } diff --git a/tests/TestException.php b/tests/TestException.php index 902f328..7281d37 100644 --- a/tests/TestException.php +++ b/tests/TestException.php @@ -4,67 +4,67 @@ namespace JKingWeb\NewsSync; class TestException extends \PHPUnit\Framework\TestCase { - use Test\Tools; + use Test\Tools; - static function setUpBeforeClass() { - Lang::set(""); - } + static function setUpBeforeClass() { + Lang::set(""); + } - static function tearDownAfterClass() { - Lang::set(Lang::DEFAULT); - } - - function testBaseClass() { - $this->assertException("unknown"); - throw new Exception("unknown"); - } + static function tearDownAfterClass() { + Lang::set(Lang::DEFAULT); + } + + function testBaseClass() { + $this->assertException("unknown"); + throw new Exception("unknown"); + } - /** + /** * @depends testBaseClass */ - function testBaseClassWithoutMessage() { - $this->assertException("unknown"); - throw new Exception(); - } - - /** + function testBaseClassWithoutMessage() { + $this->assertException("unknown"); + throw new Exception(); + } + + /** * @depends testBaseClass */ - function testDerivedClass() { - $this->assertException("fileMissing", "Lang"); - throw new Lang\Exception("fileMissing"); - } + function testDerivedClass() { + $this->assertException("fileMissing", "Lang"); + throw new Lang\Exception("fileMissing"); + } - /** + /** * @depends testDerivedClass */ - function testDerivedClassWithMessageParameters() { - $this->assertException("fileMissing", "Lang"); - throw new Lang\Exception("fileMissing", "en"); - } + function testDerivedClassWithMessageParameters() { + $this->assertException("fileMissing", "Lang"); + throw new Lang\Exception("fileMissing", "en"); + } - /** + /** * @depends testBaseClass */ - function testBaseClassWithUnknownCode() { - $this->assertException("uncoded"); - throw new Exception("testThisExceptionMessageDoesNotExist"); - } + function testBaseClassWithUnknownCode() { + $this->assertException("uncoded"); + throw new Exception("testThisExceptionMessageDoesNotExist"); + } - /** + /** * @depends testBaseClass */ - function testBaseClassWithMissingMessage() { - $this->assertException("stringMissing", "Lang"); - throw new Exception("invalid"); - } + function testBaseClassWithMissingMessage() { + $this->assertException("stringMissing", "Lang"); + throw new Exception("invalid"); + } - /** + /** * @depends testBaseClassWithUnknownCode */ - function testDerivedClassWithMissingMessage() { - $this->assertException("uncoded"); - throw new Lang\Exception("testThisExceptionMessageDoesNotExist"); - } - + function testDerivedClassWithMissingMessage() { + $this->assertException("uncoded"); + throw new Lang\Exception("testThisExceptionMessageDoesNotExist"); + } + } diff --git a/tests/TestLang.php b/tests/TestLang.php index 9b81f53..cf53a9e 100644 --- a/tests/TestLang.php +++ b/tests/TestLang.php @@ -5,58 +5,58 @@ use \org\bovigo\vfs\vfsStream; class TestLang extends \PHPUnit\Framework\TestCase { - use Test\Tools, Test\Lang\Setup; + use Test\Tools, Test\Lang\Setup; - static $vfs; - static $path; - static $files; - static $defaultPath; + static $vfs; + static $path; + static $files; + static $defaultPath; - function testListLanguages() { - $this->assertCount(sizeof(self::$files), Lang::list("en")); - } + function testListLanguages() { + $this->assertCount(sizeof(self::$files), Lang::list("en")); + } - /** + /** * @depends testListLanguages */ - function testSetLanguage() { - $this->assertEquals("en", Lang::set("en")); - $this->assertEquals("en_ca", Lang::set("en_ca")); - $this->assertEquals("de", Lang::set("de_ch")); - $this->assertEquals("en", Lang::set("en_gb_hixie")); - $this->assertEquals("en_ca", Lang::set("en_ca_jking")); - $this->assertEquals("en", Lang::set("es")); - $this->assertEquals("", Lang::set("")); - } - - /** + function testSetLanguage() { + $this->assertEquals("en", Lang::set("en")); + $this->assertEquals("en_ca", Lang::set("en_ca")); + $this->assertEquals("de", Lang::set("de_ch")); + $this->assertEquals("en", Lang::set("en_gb_hixie")); + $this->assertEquals("en_ca", Lang::set("en_ca_jking")); + $this->assertEquals("en", Lang::set("es")); + $this->assertEquals("", Lang::set("")); + } + + /** * @depends testSetLanguage */ - function testLoadInternalStrings() { - $this->assertEquals("", Lang::set("", true)); - $this->assertCount(sizeof(Lang::REQUIRED), Lang::dump()); - } + function testLoadInternalStrings() { + $this->assertEquals("", Lang::set("", true)); + $this->assertCount(sizeof(Lang::REQUIRED), Lang::dump()); + } - /** + /** * @depends testLoadInternalStrings */ - function testLoadDefaultLanguage() { - $this->assertEquals(Lang::DEFAULT, Lang::set(Lang::DEFAULT, true)); - $str = Lang::dump(); - $this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str); - $this->assertArrayHasKey('Test.presentText', $str); - } - - /** + function testLoadDefaultLanguage() { + $this->assertEquals(Lang::DEFAULT, Lang::set(Lang::DEFAULT, true)); + $str = Lang::dump(); + $this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str); + $this->assertArrayHasKey('Test.presentText', $str); + } + + /** * @depends testLoadDefaultLanguage */ - function testLoadSupplementaryLanguage() { - Lang::set(Lang::DEFAULT, true); - $this->assertEquals("ja", Lang::set("ja", true)); - $str = Lang::dump(); - $this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str); - $this->assertArrayHasKey('Test.presentText', $str); - $this->assertArrayHasKey('Test.absentText', $str); - } + function testLoadSupplementaryLanguage() { + Lang::set(Lang::DEFAULT, true); + $this->assertEquals("ja", Lang::set("ja", true)); + $str = Lang::dump(); + $this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str); + $this->assertArrayHasKey('Test.presentText', $str); + $this->assertArrayHasKey('Test.absentText', $str); + } } \ No newline at end of file diff --git a/tests/TestLangErrors.php b/tests/TestLangErrors.php index 0bc93c8..14075e7 100644 --- a/tests/TestLangErrors.php +++ b/tests/TestLangErrors.php @@ -5,51 +5,51 @@ use \org\bovigo\vfs\vfsStream; class TestLangErrors extends \PHPUnit\Framework\TestCase { - use Test\Tools, Test\Lang\Setup; - - static $vfs; - static $path; - static $files; - static $defaultPath; - - function setUp() { - Lang::set("", true); - } - - function testLoadEmptyFile() { - $this->assertException("fileCorrupt", "Lang"); - Lang::set("fr_ca", true); - } - - function testLoadFileWhichDoesNotReturnAnArray() { - $this->assertException("fileCorrupt", "Lang"); - Lang::set("it", true); - } - - function testLoadFileWhichIsNotPhp() { - $this->assertException("fileCorrupt", "Lang"); - Lang::set("ko", true); - } - - function testLoadFileWhichIsCorrupt() { - $this->assertException("fileCorrupt", "Lang"); - Lang::set("zh", true); - } - - function testLoadFileWithooutReadPermission() { - $this->assertException("fileUnreadable", "Lang"); - Lang::set("ru", true); - } - - function testLoadSubtagOfMissingLanguage() { - $this->assertException("fileMissing", "Lang"); - Lang::set("pt_br", true); - } - - function testLoadMissingDefaultLanguage() { - // this should be the last test of the series - unlink(self::$path.Lang::DEFAULT.".php"); - $this->assertException("defaultFileMissing", "Lang"); - Lang::set("fr", true); - } + use Test\Tools, Test\Lang\Setup; + + static $vfs; + static $path; + static $files; + static $defaultPath; + + function setUp() { + Lang::set("", true); + } + + function testLoadEmptyFile() { + $this->assertException("fileCorrupt", "Lang"); + Lang::set("fr_ca", true); + } + + function testLoadFileWhichDoesNotReturnAnArray() { + $this->assertException("fileCorrupt", "Lang"); + Lang::set("it", true); + } + + function testLoadFileWhichIsNotPhp() { + $this->assertException("fileCorrupt", "Lang"); + Lang::set("ko", true); + } + + function testLoadFileWhichIsCorrupt() { + $this->assertException("fileCorrupt", "Lang"); + Lang::set("zh", true); + } + + function testLoadFileWithooutReadPermission() { + $this->assertException("fileUnreadable", "Lang"); + Lang::set("ru", true); + } + + function testLoadSubtagOfMissingLanguage() { + $this->assertException("fileMissing", "Lang"); + Lang::set("pt_br", true); + } + + function testLoadMissingDefaultLanguage() { + // this should be the last test of the series + unlink(self::$path.Lang::DEFAULT.".php"); + $this->assertException("defaultFileMissing", "Lang"); + Lang::set("fr", true); + } } \ No newline at end of file diff --git a/tests/lib/Lang/Setup.php b/tests/lib/Lang/Setup.php index 05fc316..8d1bcc5 100644 --- a/tests/lib/Lang/Setup.php +++ b/tests/lib/Lang/Setup.php @@ -6,43 +6,43 @@ use \org\bovigo\vfs\vfsStream, \JKingWeb\NewsSync\Lang; trait Setup { - static function setUpBeforeClass() { - // this is required to keep from having exceptions in Lang::msg() in turn calling Lang::msg() and looping - \JKingWeb\NewsSync\Lang\Exception::$test = true; - // test files - self::$files = [ - 'en.php' => ' "and the Philosopher\'s Stone"];', - 'en_ca.php' => ' "{0} and {1}"];', - 'en_us.php' => ' "and the Sorcerer\'s Stone"];', - 'fr.php' => ' "à l\'école des sorciers"];', - 'ja.php' => ' "賢者の石"];', - 'de.php' => ' "und der Stein der Weisen"];', - 'pt_br.php' => ' "e a Pedra Filosofal"];', - 'vi.php' => ' ' ' 'DEAD BEEF', - 'fr_ca.php' => '', - // unreadable file - 'ru.php' => '', - ]; - self::$vfs = vfsStream::setup("langtest", 0777, self::$files); - self::$path = self::$vfs->url()."/"; - // set up a file without read access - chmod(self::$path."ru.php", 0000); - // make the Lang class use the vfs files - self::$defaultPath = Lang::$path; - Lang::$path = self::$path; - } + static function setUpBeforeClass() { + // this is required to keep from having exceptions in Lang::msg() in turn calling Lang::msg() and looping + \JKingWeb\NewsSync\Lang\Exception::$test = true; + // test files + self::$files = [ + 'en.php' => ' "and the Philosopher\'s Stone"];', + 'en_ca.php' => ' "{0} and {1}"];', + 'en_us.php' => ' "and the Sorcerer\'s Stone"];', + 'fr.php' => ' "à l\'école des sorciers"];', + 'ja.php' => ' "賢者の石"];', + 'de.php' => ' "und der Stein der Weisen"];', + 'pt_br.php' => ' "e a Pedra Filosofal"];', + 'vi.php' => ' ' ' 'DEAD BEEF', + 'fr_ca.php' => '', + // unreadable file + 'ru.php' => '', + ]; + self::$vfs = vfsStream::setup("langtest", 0777, self::$files); + self::$path = self::$vfs->url()."/"; + // set up a file without read access + chmod(self::$path."ru.php", 0000); + // make the Lang class use the vfs files + self::$defaultPath = Lang::$path; + Lang::$path = self::$path; + } - static function tearDownAfterClass() { - \JKingWeb\NewsSync\Lang\Exception::$test = false; - Lang::$path = self::$defaultPath; - self::$path = null; - self::$vfs = null; - self::$files = null; - Lang::set("", true); - Lang::set(Lang::DEFAULT); - } + static function tearDownAfterClass() { + \JKingWeb\NewsSync\Lang\Exception::$test = false; + Lang::$path = self::$defaultPath; + self::$path = null; + self::$vfs = null; + self::$files = null; + Lang::set("", true); + Lang::set(Lang::DEFAULT); + } } \ No newline at end of file diff --git a/tests/lib/Tools.php b/tests/lib/Tools.php index ba2b353..8de83e1 100644 --- a/tests/lib/Tools.php +++ b/tests/lib/Tools.php @@ -4,15 +4,15 @@ namespace JKingWeb\NewsSync\Test; use \JKingWeb\NewsSync\Exception; trait Tools { - function assertException(string $msg, string $prefix = "", string $type = "Exception") { - $class = \JKingWeb\NewsSync\NS_BASE . ($prefix !== "" ? str_replace("/", "\\", $prefix) . "\\" : "") . $type; - $msgID = ($prefix !== "" ? $prefix . "/" : "") . $type. ".$msg"; - if(array_key_exists($msgID, Exception::CODES)) { - $code = Exception::CODES[$msgID]; - } else { - $code = 0; - } - $this->expectException($class); - $this->expectExceptionCode($code); - } + function assertException(string $msg, string $prefix = "", string $type = "Exception") { + $class = \JKingWeb\NewsSync\NS_BASE . ($prefix !== "" ? str_replace("/", "\\", $prefix) . "\\" : "") . $type; + $msgID = ($prefix !== "" ? $prefix . "/" : "") . $type. ".$msg"; + if(array_key_exists($msgID, Exception::CODES)) { + $code = Exception::CODES[$msgID]; + } else { + $code = 0; + } + $this->expectException($class); + $this->expectExceptionCode($code); + } } \ No newline at end of file diff --git a/tests/phpunit.xml b/tests/phpunit.xml index efe3398..a7167ed 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,23 +1,23 @@ + colors="true" + bootstrap="../bootstrap.php" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + beStrictAboutTestsThatDoNotTestAnything="true" + beStrictAboutOutputDuringTests="true" + beStrictAboutTestSize="true" + stopOnError="true"> - TestLang.php - TestLangComplex.php - TestException.php - TestLangErrors.php + TestLang.php + TestLangComplex.php + TestException.php + TestLangErrors.php - TestConf.php + TestConf.php \ No newline at end of file diff --git a/tests/testLangComplex.php b/tests/testLangComplex.php index 6ab5436..60c55c9 100644 --- a/tests/testLangComplex.php +++ b/tests/testLangComplex.php @@ -5,82 +5,82 @@ use \org\bovigo\vfs\vfsStream; class TestLangComplex extends \PHPUnit\Framework\TestCase { - use Test\Tools, Test\Lang\Setup; + use Test\Tools, Test\Lang\Setup; - static $vfs; - static $path; - static $files; - static $defaultPath; + static $vfs; + static $path; + static $files; + static $defaultPath; - function setUp() { - Lang::set(Lang::DEFAULT, true); - } + function setUp() { + Lang::set(Lang::DEFAULT, true); + } - function testLazyLoad() { - Lang::set("ja"); - $this->assertArrayNotHasKey('Test.absentText', Lang::dump()); - } - - function testLoadCascadeOfFiles() { - Lang::set("ja", true); - $this->assertEquals("de", Lang::set("de", true)); - $str = Lang::dump(); - $this->assertArrayNotHasKey('Test.absentText', $str); - $this->assertEquals('und der Stein der Weisen', $str['Test.presentText']); - } + function testLazyLoad() { + Lang::set("ja"); + $this->assertArrayNotHasKey('Test.absentText', Lang::dump()); + } + + function testLoadCascadeOfFiles() { + Lang::set("ja", true); + $this->assertEquals("de", Lang::set("de", true)); + $str = Lang::dump(); + $this->assertArrayNotHasKey('Test.absentText', $str); + $this->assertEquals('und der Stein der Weisen', $str['Test.presentText']); + } - /** + /** * @depends testLoadCascadeOfFiles */ - function testLoadSubtag() { - $this->assertEquals("en_ca", Lang::set("en_ca", true)); - } - - function testFetchAMessage() { - Lang::set("de", true); - $this->assertEquals('und der Stein der Weisen', Lang::msg('Test.presentText')); - } + function testLoadSubtag() { + $this->assertEquals("en_ca", Lang::set("en_ca", true)); + } + + function testFetchAMessage() { + Lang::set("de", true); + $this->assertEquals('und der Stein der Weisen', Lang::msg('Test.presentText')); + } - /** + /** * @depends testFetchAMessage */ - function testFetchAMessageWithSingleNumericParameter() { - Lang::set("en_ca", true); - $this->assertEquals('Default language file "en" missing', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing', Lang::DEFAULT)); - } + function testFetchAMessageWithSingleNumericParameter() { + Lang::set("en_ca", true); + $this->assertEquals('Default language file "en" missing', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing', Lang::DEFAULT)); + } - /** + /** * @depends testFetchAMessage */ - function testFetchAMessageWithMultipleNumericParameters() { - Lang::set("en_ca", true); - $this->assertEquals('Happy Rotter and the Philosopher\'s Stone', Lang::msg('Test.presentText', ['Happy Rotter', 'the Philosopher\'s Stone'])); - } + function testFetchAMessageWithMultipleNumericParameters() { + Lang::set("en_ca", true); + $this->assertEquals('Happy Rotter and the Philosopher\'s Stone', Lang::msg('Test.presentText', ['Happy Rotter', 'the Philosopher\'s Stone'])); + } - /** + /** * @depends testFetchAMessage */ - function testFetchAMessageWithNamedParameters() { - $this->assertEquals('Message string "Test.absentText" missing from all loaded language files (en)', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing', ['msgID' => 'Test.absentText', 'fileList' => 'en'])); - } + function testFetchAMessageWithNamedParameters() { + $this->assertEquals('Message string "Test.absentText" missing from all loaded language files (en)', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing', ['msgID' => 'Test.absentText', 'fileList' => 'en'])); + } - /** + /** * @depends testFetchAMessage */ - function testReloadDefaultStrings() { - Lang::set("de", true); - Lang::set("en", true); - $this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText')); - } + function testReloadDefaultStrings() { + Lang::set("de", true); + Lang::set("en", true); + $this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText')); + } - /** + /** * @depends testFetchAMessage */ - function testReloadGeneralTagAfterSubtag() { - Lang::set("en", true); - Lang::set("en_us", true); - $this->assertEquals('and the Sorcerer\'s Stone', Lang::msg('Test.presentText')); - Lang::set("en", true); - $this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText')); - } + function testReloadGeneralTagAfterSubtag() { + Lang::set("en", true); + Lang::set("en_us", true); + $this->assertEquals('and the Sorcerer\'s Stone', Lang::msg('Test.presentText')); + Lang::set("en", true); + $this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText')); + } } \ No newline at end of file