Browse Source

Converted all hard tabs to soft tabs

microsub
Dustin Wilson 7 years ago
parent
commit
c5fac33398
  1. 10
      .gitattributes
  2. 74
      composer.json
  3. 104
      lib/AbstractException.php
  4. 104
      lib/Conf.php
  5. 504
      lib/Database.php
  6. 118
      lib/Db/Common.php
  7. 18
      lib/Db/CommonPDO.php
  8. 82
      lib/Db/CommonSQLite3.php
  9. 50
      lib/Db/Driver.php
  10. 98
      lib/Db/DriverSQLite3.php
  11. 40
      lib/Db/DriverSQLite3PDO.php
  12. 16
      lib/Db/Result.php
  13. 96
      lib/Db/ResultSQLite3.php
  14. 6
      lib/Db/Statement.php
  15. 124
      lib/Db/StatementSQLite3.php
  16. 6
      lib/ExceptionFatal.php
  17. 294
      lib/Lang.php
  18. 34
      lib/Lang/Exception.php
  19. 18
      lib/RuntimeData.php
  20. 474
      lib/User.php
  21. 44
      lib/User/Driver.php
  22. 68
      lib/User/DriverInternal.php
  23. 132
      lib/User/InternalFunctions.php
  24. 92
      locale/en.php
  25. 138
      sql/SQLite3/0.sql
  26. 142
      tests/TestConf.php
  27. 88
      tests/TestException.php
  28. 82
      tests/TestLang.php
  29. 94
      tests/TestLangErrors.php
  30. 76
      tests/lib/Lang/Setup.php
  31. 22
      tests/lib/Tools.php
  32. 28
      tests/phpunit.xml
  33. 114
      tests/testLangComplex.php

10
.gitattributes

@ -10,13 +10,13 @@
*.dbproj merge=union *.dbproj merge=union
# Standard to msysgit # Standard to msysgit
*.doc diff=astextplain *.doc diff=astextplain
*.DOC diff=astextplain *.DOC diff=astextplain
*.docx diff=astextplain *.docx diff=astextplain
*.DOCX diff=astextplain *.DOCX diff=astextplain
*.dot diff=astextplain *.dot diff=astextplain
*.DOT diff=astextplain *.DOT diff=astextplain
*.pdf diff=astextplain *.pdf diff=astextplain
*.PDF diff=astextplain *.PDF diff=astextplain
*.rtf diff=astextplain *.rtf diff=astextplain
*.RTF diff=astextplain *.RTF diff=astextplain

74
composer.json

@ -1,40 +1,40 @@
{ {
"name": "jkingweb/arsse", "name": "jkingweb/arsse",
"type": "library", "type": "library",
"description": "TODO", "description": "TODO",
"keywords": ["rss"], "keywords": ["rss"],
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
{ {
"name": "J. King", "name": "J. King",
"email": "jking@jkingweb.ca", "email": "jking@jkingweb.ca",
"homepage": "https://jkingweb.ca/" "homepage": "https://jkingweb.ca/"
}, },
{ {
"name": "Dustin Wilson", "name": "Dustin Wilson",
"email": "dustin@dustinwilson.com", "email": "dustin@dustinwilson.com",
"homepage": "https://dustinwilson.com/" "homepage": "https://dustinwilson.com/"
} }
], ],
"require": { "require": {
"php": "^7.0.0", "php": "^7.0.0",
"jkingweb/druuid": "^3.0.0", "jkingweb/druuid": "^3.0.0",
"phpseclib/phpseclib": "^2.0.4", "phpseclib/phpseclib": "^2.0.4",
"webmozart/glob": "^4.1.0", "webmozart/glob": "^4.1.0",
"fguillot/picoFeed": ">=0.1.31" "fguillot/picoFeed": ">=0.1.31"
}, },
"require-dev": { "require-dev": {
"mikey179/vfsStream": "^1.6.4" "mikey179/vfsStream": "^1.6.4"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"JKingWeb\\NewsSync\\": "lib/" "JKingWeb\\NewsSync\\": "lib/"
} }
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"JKingWeb\\NewsSync\\Test\\": "tests/lib/" "JKingWeb\\NewsSync\\Test\\": "tests/lib/"
} }
} }
} }

104
lib/AbstractException.php

@ -4,57 +4,57 @@ namespace JKingWeb\NewsSync;
abstract class AbstractException extends \Exception { abstract class AbstractException extends \Exception {
const CODES = [ const CODES = [
"Exception.uncoded" => -1, "Exception.uncoded" => -1,
"Exception.invalid" => 1, // this exception MUST NOT have a message string defined "Exception.invalid" => 1, // this exception MUST NOT have a message string defined
"Exception.unknown" => 10000, "Exception.unknown" => 10000,
"Lang/Exception.defaultFileMissing" => 10101, "Lang/Exception.defaultFileMissing" => 10101,
"Lang/Exception.fileMissing" => 10102, "Lang/Exception.fileMissing" => 10102,
"Lang/Exception.fileUnreadable" => 10103, "Lang/Exception.fileUnreadable" => 10103,
"Lang/Exception.fileCorrupt" => 10104, "Lang/Exception.fileCorrupt" => 10104,
"Lang/Exception.stringMissing" => 10105, "Lang/Exception.stringMissing" => 10105,
"Db/Exception.extMissing" => 10201, "Db/Exception.extMissing" => 10201,
"Db/Exception.fileMissing" => 10202, "Db/Exception.fileMissing" => 10202,
"Db/Exception.fileUnusable" => 10203, "Db/Exception.fileUnusable" => 10203,
"Db/Exception.fileUnreadable" => 10204, "Db/Exception.fileUnreadable" => 10204,
"Db/Exception.fileUnwritable" => 10205, "Db/Exception.fileUnwritable" => 10205,
"Db/Exception.fileUncreatable" => 10206, "Db/Exception.fileUncreatable" => 10206,
"Db/Exception.fileCorrupt" => 10207, "Db/Exception.fileCorrupt" => 10207,
"Db/Update/Exception.tooNew" => 10211, "Db/Update/Exception.tooNew" => 10211,
"Db/Update/Exception.fileMissing" => 10212, "Db/Update/Exception.fileMissing" => 10212,
"Db/Update/Exception.fileUnusable" => 10213, "Db/Update/Exception.fileUnusable" => 10213,
"Db/Update/Exception.fileUnreadable" => 10214, "Db/Update/Exception.fileUnreadable" => 10214,
"Db/Update/Exception.manual" => 10215, "Db/Update/Exception.manual" => 10215,
"Db/Update/Exception.manualOnly" => 10216, "Db/Update/Exception.manualOnly" => 10216,
"Conf/Exception.fileMissing" => 10302, "Conf/Exception.fileMissing" => 10302,
"Conf/Exception.fileUnusable" => 10303, "Conf/Exception.fileUnusable" => 10303,
"Conf/Exception.fileUnreadable" => 10304, "Conf/Exception.fileUnreadable" => 10304,
"Conf/Exception.fileUnwritable" => 10305, "Conf/Exception.fileUnwritable" => 10305,
"Conf/Exception.fileUncreatable" => 10306, "Conf/Exception.fileUncreatable" => 10306,
"Conf/Exception.fileCorrupt" => 10307, "Conf/Exception.fileCorrupt" => 10307,
"User/Exception.functionNotImplemented" => 10401, "User/Exception.functionNotImplemented" => 10401,
"User/Exception.doesNotExist" => 10402, "User/Exception.doesNotExist" => 10402,
"User/Exception.alreadyExists" => 10403, "User/Exception.alreadyExists" => 10403,
"User/Exception.authMissing" => 10411, "User/Exception.authMissing" => 10411,
"User/Exception.authFailed" => 10412, "User/Exception.authFailed" => 10412,
"User/Exception.notAuthorized" => 10421, "User/Exception.notAuthorized" => 10421,
]; ];
public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) { public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
if($msgID=="") { if($msgID=="") {
$msg = "Exception.unknown"; $msg = "Exception.unknown";
$code = 10000; $code = 10000;
} else { } else {
$class = get_called_class(); $class = get_called_class();
$codeID = str_replace("\\", "/", str_replace(NS_BASE, "", $class)).".$msgID"; $codeID = str_replace("\\", "/", str_replace(NS_BASE, "", $class)).".$msgID";
if(!array_key_exists($codeID, self::CODES)) { if(!array_key_exists($codeID, self::CODES)) {
throw new Exception("uncoded"); throw new Exception("uncoded");
} else { } else {
$code = self::CODES[$codeID]; $code = self::CODES[$codeID];
$msg = "Exception.".str_replace("\\", "/", $class).".$msgID"; $msg = "Exception.".str_replace("\\", "/", $class).".$msgID";
} }
$msg = Lang::msg($msg, $vars); $msg = Lang::msg($msg, $vars);
} }
parent::__construct($msg, $code, $e); parent::__construct($msg, $code, $e);
} }
} }

104
lib/Conf.php

@ -3,64 +3,64 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync; namespace JKingWeb\NewsSync;
class Conf { class Conf {
public $lang = "en"; 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 $userDriver = User\DriverInternal::class; public $dbDriver = Db\DriverSQLite3::class;
public $userAuthPreferHTTP = false; public $dbSQLite3File = BASE."newssync.db";
public $userComposeNames = true; 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 { public function __construct(string $import_file = "") {
if(!file_exists($file)) throw new Conf\Exception("fileMissing", $file); if($import_file != "") $this->importFile($import_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 import(array $arr): self { public function importFile(string $file): self {
foreach($arr as $key => $value) { if(!file_exists($file)) throw new Conf\Exception("fileMissing", $file);
$this->$key = $value; if(!is_readable($file)) throw new Conf\Exception("fileUnreadable", $file);
} try {
return $this; 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 { public function import(array $arr): self {
// TODO foreach($arr as $key => $value) {
} $this->$key = $value;
}
return $this;
}
public function __toString(): string { public function export(string $file = ""): string {
return $this->export(); // TODO
} }
public function __toString(): string {
return $this->export();
}
} }

504
lib/Database.php

@ -3,275 +3,275 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync; namespace JKingWeb\NewsSync;
class Database { class Database {
const SCHEMA_VERSION = 1; const SCHEMA_VERSION = 1;
const FORMAT_TS = "Y-m-d h:i:s"; const FORMAT_TS = "Y-m-d h:i:s";
const FORMAT_DATE = "Y-m-d"; const FORMAT_DATE = "Y-m-d";
const FORMAT_TIME = "h:i:s"; const FORMAT_TIME = "h:i:s";
protected $data; protected $data;
public $db; public $db;
protected function cleanName(string $name): string { protected function cleanName(string $name): string {
return (string) preg_filter("[^0-9a-zA-Z_\.]", "", $name); return (string) preg_filter("[^0-9a-zA-Z_\.]", "", $name);
} }
public function __construct(RuntimeData $data) { public function __construct(RuntimeData $data) {
$this->data = $data; $this->data = $data;
$driver = $data->conf->dbDriver; $driver = $data->conf->dbDriver;
$this->db = $driver::create($data, INSTALL); $this->db = $driver::create($data, INSTALL);
$ver = $this->db->schemaVersion(); $ver = $this->db->schemaVersion();
if(!INSTALL && $ver < self::SCHEMA_VERSION) { if(!INSTALL && $ver < self::SCHEMA_VERSION) {
$this->db->update(self::SCHEMA_VERSION); $this->db->update(self::SCHEMA_VERSION);
} }
} }
static public function listDrivers(): array { static public function listDrivers(): array {
$sep = \DIRECTORY_SEPARATOR; $sep = \DIRECTORY_SEPARATOR;
$path = __DIR__.$sep."Db".$sep; $path = __DIR__.$sep."Db".$sep;
$classes = []; $classes = [];
foreach(glob($path."Driver?*.php") as $file) { foreach(glob($path."Driver?*.php") as $file) {
$name = basename($file, ".php"); $name = basename($file, ".php");
if(substr($name,-3) != "PDO") { if(substr($name,-3) != "PDO") {
$name = NS_BASE."Db\\$name"; $name = NS_BASE."Db\\$name";
if(class_exists($name)) { if(class_exists($name)) {
$classes[$name] = $name::driverName(); $classes[$name] = $name::driverName();
} }
} }
} }
return $classes; return $classes;
} }
public function schemaVersion(): int { public function schemaVersion(): int {
return $this->db->schemaVersion(); return $this->db->schemaVersion();
} }
public function dbUpdate(): bool { public function dbUpdate(): bool {
if($this->db->schemaVersion() < self::SCHEMA_VERSION) return $this->db->update(self::SCHEMA_VERSION); if($this->db->schemaVersion() < self::SCHEMA_VERSION) return $this->db->update(self::SCHEMA_VERSION);
return false; return false;
} }
public function settingGet(string $key) { public function settingGet(string $key) {
$row = $this->db->prepare("SELECT value, type from newssync_settings where key = ?", "str")->run($key)->get(); $row = $this->db->prepare("SELECT value, type from newssync_settings where key = ?", "str")->run($key)->get();
if(!$row) return null; if(!$row) return null;
switch($row['type']) { switch($row['type']) {
case "int": return (int) $row['value']; case "int": return (int) $row['value'];
case "numeric": return (float) $row['value']; case "numeric": return (float) $row['value'];
case "text": return $row['value']; case "text": return $row['value'];
case "json": return json_decode($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 "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 "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 "time": return date_create_from_format("!".self::FORMAT_TIME, $row['value'], new DateTimeZone("UTC"));
case "bool": return (bool) $row['value']; case "bool": return (bool) $row['value'];
case "null": return null; case "null": return null;
default: return $row['value']; default: return $row['value'];
} }
} }
public function settingSet(string $key, $in, string $type = null): bool { public function settingSet(string $key, $in, string $type = null): bool {
if(!$type) { if(!$type) {
switch(gettype($in)) { switch(gettype($in)) {
case "boolean": $type = "bool"; break; case "boolean": $type = "bool"; break;
case "integer": $type = "int"; break; case "integer": $type = "int"; break;
case "double": $type = "numeric"; break; case "double": $type = "numeric"; break;
case "string": case "string":
case "array": $type = "json"; break; case "array": $type = "json"; break;
case "resource": case "resource":
case "unknown type": case "unknown type":
case "NULL": $type = "null"; break; case "NULL": $type = "null"; break;
case "object": case "object":
if($in instanceof DateTimeInterface) { if($in instanceof DateTimeInterface) {
$type = "timestamp"; $type = "timestamp";
} else { } else {
$type = "text"; $type = "text";
} }
break; break;
default: $type = 'null'; break; default: $type = 'null'; break;
} }
} }
$type = strtolower($type); $type = strtolower($type);
switch($type) { switch($type) {
case "integer": case "integer":
$type = "int"; $type = "int";
case "int": case "int":
$value =& $in; $value =& $in;
break; break;
case "float": case "float":
case "double": case "double":
case "real": case "real":
$type = "numeric"; $type = "numeric";
case "numeric": case "numeric":
$value =& $in; $value =& $in;
break; break;
case "str": case "str":
case "string": case "string":
$type = "text"; $type = "text";
case "text": case "text":
$value =& $in; $value =& $in;
break; break;
case "json": case "json":
if(is_array($in) || is_object($in)) { if(is_array($in) || is_object($in)) {
$value = json_encode($in); $value = json_encode($in);
} else { } else {
$value =& $in; $value =& $in;
} }
break; break;
case "datetime": case "datetime":
$type = "timestamp"; $type = "timestamp";
case "timestamp": case "timestamp":
if($in instanceof DateTimeInterface) { if($in instanceof DateTimeInterface) {
$value = gmdate(self::FORMAT_TS, $in->format("U")); $value = gmdate(self::FORMAT_TS, $in->format("U"));
} else if(is_numeric($in)) { } else if(is_numeric($in)) {
$value = gmdate(self::FORMAT_TS, $in); $value = gmdate(self::FORMAT_TS, $in);
} else { } else {
$value = gmdate(self::FORMAT_TS, gmstrftime($in)); $value = gmdate(self::FORMAT_TS, gmstrftime($in));
} }
break; break;
case "date": case "date":
if($in instanceof DateTimeInterface) { if($in instanceof DateTimeInterface) {
$value = gmdate(self::FORMAT_DATE, $in->format("U")); $value = gmdate(self::FORMAT_DATE, $in->format("U"));
} else if(is_numeric($in)) { } else if(is_numeric($in)) {
$value = gmdate(self::FORMAT_DATE, $in); $value = gmdate(self::FORMAT_DATE, $in);
} else { } else {
$value = gmdate(self::FORMAT_DATE, gmstrftime($in)); $value = gmdate(self::FORMAT_DATE, gmstrftime($in));
} }
break; break;
case "time": case "time":
if($in instanceof DateTimeInterface) { if($in instanceof DateTimeInterface) {
$value = gmdate(self::FORMAT_TIME, $in->format("U")); $value = gmdate(self::FORMAT_TIME, $in->format("U"));
} else if(is_numeric($in)) { } else if(is_numeric($in)) {
$value = gmdate(self::FORMAT_TIME, $in); $value = gmdate(self::FORMAT_TIME, $in);
} else { } else {
$value = gmdate(self::FORMAT_TIME, gmstrftime($in)); $value = gmdate(self::FORMAT_TIME, gmstrftime($in));
} }
break; break;
case "boolean": case "boolean":
case "bit": case "bit":
$type = "bool"; $type = "bool";
case "bool": case "bool":
$value = (int) $in; $value = (int) $in;
break; break;
case "null": case "null":
$value = null; $value = null;
break; break;
default: default:
$type = "text"; $type = "text";
$value =& $in; $value =& $in;
break; break;
} }
$this->db->prepare("REPLACE INTO newssync_settings(key,value,type) values(?,?,?)", "str", (($type=="null") ? "null" : "str"), "str")->run($key, $value, "text"); $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 { public function settingRemove(string $key): bool {
$this->db->prepare("DELETE from newssync_settings where key = ?", "str")->run($key); $this->db->prepare("DELETE from newssync_settings where key = ?", "str")->run($key);
return true; return true;
} }
public function userExists(string $user): bool { public function userExists(string $user): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); 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(); 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 { 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->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if($this->userExists($user)) return false; if($this->userExists($user)) return false;
if(strlen($password) > 0) $password = password_hash($password, \PASSWORD_DEFAULT); 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); $this->db->prepare("INSERT INTO newssync_users(id,password) values(?,?)", "str", "str", "str")->run($user,$password,$admin);
return true; return true;
} }
public function userRemove(string $user): bool { public function userRemove(string $user): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); 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); $this->db->prepare("DELETE from newssync_users where id is ?", "str")->run($user);
return true; return true;
} }
public function userList(string $domain = null): array { public function userList(string $domain = null): array {
if($domain !== null) { if($domain !== null) {
if(!$this->data->user->authorize("@".$domain, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]); if(!$this->data->user->authorize("@".$domain, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
$domain = str_replace(["\\","%","_"],["\\\\", "\\%", "\\_"], $domain); $domain = str_replace(["\\","%","_"],["\\\\", "\\%", "\\_"], $domain);
$domain = "%@".$domain; $domain = "%@".$domain;
$set = $this->db->prepare("SELECT id from newssync_users where id like ?", "str")->run($domain); $set = $this->db->prepare("SELECT id from newssync_users where id like ?", "str")->run($domain);
} else { } else {
if(!$this->data->user->authorize("", __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "all users"]); 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(); $set = $this->db->prepare("SELECT id from newssync_users")->run();
} }
$out = []; $out = [];
foreach($set as $row) { foreach($set as $row) {
$out[] = $row["id"]; $out[] = $row["id"];
} }
return $out; return $out;
} }
public function userPasswordGet(string $user): string { public function userPasswordGet(string $user): string {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) return ""; if(!$this->userExists($user)) return "";
return (string) $this->db->prepare("SELECT password from newssync_users where id is ?", "str")->run($user)->getSingle(); 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 { 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->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) return false; if(!$this->userExists($user)) return false;
if(strlen($password > 0)) $password = password_hash($password); if(strlen($password > 0)) $password = password_hash($password);
$this->db->prepare("UPDATE newssync_users set password = ? where id is ?", "str", "str")->run($password, $user); $this->db->prepare("UPDATE newssync_users set password = ? where id is ?", "str", "str")->run($password, $user);
return true; return true;
} }
public function userPropertiesGet(string $user): array { public function userPropertiesGet(string $user): array {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); 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(); $prop = $this->db->prepare("SELECT name,rights from newssync_users where id is ?", "str")->run($user)->get();
if(!$prop) return []; if(!$prop) return [];
return $prop; return $prop;
} }
public function userPropertiesSet(string $user, array &$properties): array { public function userPropertiesSet(string $user, array &$properties): array {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
$valid = [ // FIXME: add future properties $valid = [ // FIXME: add future properties
"name" => "str", "name" => "str",
]; ];
if(!$this->userExists($user)) return []; if(!$this->userExists($user)) return [];
$this->db->begin(); $this->db->begin();
foreach($valid as $prop => $type) { foreach($valid as $prop => $type) {
if(!array_key_exists($prop, $properties)) continue; 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->prepare("UPDATE newssync_users set $prop = ? where id is ?", $type, "str")->run($properties[$prop], $user);
} }
$this->db->commit(); $this->db->commit();
return $this->userPropertiesGet($user); return $this->userPropertiesGet($user);
} }
public function userRightsGet(string $user): int { public function userRightsGet(string $user): int {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); 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(); 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 { 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->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) return false; if(!$this->userExists($user)) return false;
$this->db->prepare("UPDATE newssync_users set rights = ? where id is ?", "int", "str")->run($rights, $user); $this->db->prepare("UPDATE newssync_users set rights = ? where id is ?", "int", "str")->run($rights, $user);
return true; return true;
} }
public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = ""): int { 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->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__]); if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["user" => $user, "action" => __FUNCTION__]);
$this->db->begin(); $this->db->begin();
$qFeed = $this->db->prepare("SELECT id from newssync_feeds where url is ? and username is ? and password is ?", "str", "str", "str"); $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(); $feed = $qFeed->run($url, $fetchUser, $fetchPassword)->getSingle();
if($feed===null) { if($feed===null) {
$this->db->prepare("INSERT INTO newssync_feeds(url,username,password) values(?,?,?)", "str", "str", "str")->run($url, $fetchUser, $fetchPassword); $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(); $feed = $qFeed->run($url, $fetchUser, $fetchPassword)->getSingle();
} }
$this->db->prepare("INSERT INTO newssync_subscriptions(owner,feed) values(?,?)", "str", "int")->run($user,$feed); $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(); $sub = $this->db->prepare("SELECT id from newssync_subscriptions where owner is ? and feed is ?", "str", "int")->run($user, $feed)->getSingle();
$this->db->commit(); $this->db->commit();
return $sub; return $sub;
} }
public function subscriptionRemove(int $id): bool { public function subscriptionRemove(int $id): bool {
$this->db->begin(); $this->db->begin();
$user = $this->db->prepare("SELECT owner from newssync_subscriptions where id is ?", "int")->run($id)->getSingle(); $user = $this->db->prepare("SELECT owner from newssync_subscriptions where id is ?", "int")->run($id)->getSingle();
if($user===null) return false; if($user===null) return false;
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); 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(); return (bool) $this->db->prepare("DELETE from newssync_subscriptions where id is ?", "int")->run($id)->changes();
} }
} }

118
lib/Db/Common.php

@ -4,70 +4,70 @@ namespace JKingWeb\NewsSync\Db;
use JKingWeb\DrUUID\UUID as UUID; use JKingWeb\DrUUID\UUID as UUID;
Trait Common { Trait Common {
protected $transDepth = 0; protected $transDepth = 0;
public function schemaVersion(): int { public function schemaVersion(): int {
try { try {
return $this->data->db->settingGet("schema_version"); return $this->data->db->settingGet("schema_version");
} catch(\Throwable $e) { } catch(\Throwable $e) {
return 0; return 0;
} }
} }
public function begin(): bool { public function begin(): bool {
$this->exec("SAVEPOINT newssync_".($this->transDepth)); $this->exec("SAVEPOINT newssync_".($this->transDepth));
$this->transDepth += 1; $this->transDepth += 1;
return true; return true;
} }
public function commit(bool $all = false): bool { public function commit(bool $all = false): bool {
if($this->transDepth==0) return false; if($this->transDepth==0) return false;
if(!$all) { if(!$all) {
$this->exec("RELEASE SAVEPOINT newssync_".($this->transDepth - 1)); $this->exec("RELEASE SAVEPOINT newssync_".($this->transDepth - 1));
$this->transDepth -= 1; $this->transDepth -= 1;
} else { } else {
$this->exec("COMMIT TRANSACTION"); $this->exec("COMMIT TRANSACTION");
$this->transDepth = 0; $this->transDepth = 0;
} }
return true; return true;
} }
public function rollback(bool $all = false): bool { public function rollback(bool $all = false): bool {
if($this->transDepth==0) return false; if($this->transDepth==0) return false;
if(!$all) { if(!$all) {
$this->exec("ROLLBACK TRANSACTION TO SAVEPOINT newssync_".($this->transDepth - 1)); $this->exec("ROLLBACK TRANSACTION TO SAVEPOINT newssync_".($this->transDepth - 1));
// rollback to savepoint does not collpase the savepoint // rollback to savepoint does not collpase the savepoint
$this->commit(); $this->commit();
$this->transDepth -= 1; $this->transDepth -= 1;
if($this->transDepth==0) $this->exec("ROLLBACK TRANSACTION"); if($this->transDepth==0) $this->exec("ROLLBACK TRANSACTION");
} else { } else {
$this->exec("ROLLBACK TRANSACTION"); $this->exec("ROLLBACK TRANSACTION");
$this->transDepth = 0; $this->transDepth = 0;
} }
return true; return true;
} }
public function lock(): bool { public function lock(): bool {
if($this->schemaVersion() < 1) return true; if($this->schemaVersion() < 1) return true;
if($this->isLocked()) return false; if($this->isLocked()) return false;
$uuid = UUID::mintStr(); $uuid = UUID::mintStr();
if(!$this->data->db->settingSet("lock", $uuid)) return false; if(!$this->data->db->settingSet("lock", $uuid)) return false;
sleep(1); sleep(1);
if($this->data->db->settingGet("lock") != $uuid) return false; if($this->data->db->settingGet("lock") != $uuid) return false;
return true; return true;
} }
public function unlock(): bool { public function unlock(): bool {
return $this->data->db->settingRemove("lock"); return $this->data->db->settingRemove("lock");
} }
public function isLocked(): bool { public function isLocked(): bool {
if($this->schemaVersion() < 1) return false; if($this->schemaVersion() < 1) return false;
return ($this->query("SELECT count(*) from newssync_settings where key = 'lock'")->getSingle() > 0); return ($this->query("SELECT count(*) from newssync_settings where key = 'lock'")->getSingle() > 0);
} }
public function prepare(string $query, string ...$paramType): Statement { public function prepare(string $query, string ...$paramType): Statement {
return $this->prepareArray($query, $paramType); return $this->prepareArray($query, $paramType);
} }
} }

18
lib/Db/CommonPDO.php

@ -3,15 +3,15 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\Db; namespace JKingWeb\NewsSync\Db;
Trait CommonPDO { Trait CommonPDO {
public function query(string $query): Result { public function query(string $query): Result {
return new ResultPDO($this->db->query($query)); return new ResultPDO($this->db->query($query));
} }
public function prepareArray(string $query, array $paramTypes): Statement { public function prepareArray(string $query, array $paramTypes): Statement {
return new StatementPDO($query, $paramTypes); return new StatementPDO($query, $paramTypes);
} }
public function prepare(string $query, string ...$paramType): Statement { public function prepare(string $query, string ...$paramType): Statement {
return $this->prepareArray($query, $paramType); return $this->prepareArray($query, $paramType);
} }
} }

82
lib/Db/CommonSQLite3.php

@ -3,48 +3,48 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\Db; namespace JKingWeb\NewsSync\Db;
Trait CommonSQLite3 { Trait CommonSQLite3 {
static public function driverName(): string { static public function driverName(): string {
return "SQLite 3"; return "SQLite 3";
} }
public function schemaVersion(): int { public function schemaVersion(): int {
return $this->query("PRAGMA user_version")->getSingle(); return $this->query("PRAGMA user_version")->getSingle();
} }
public function update(int $to): bool { public function update(int $to): bool {
$ver = $this->schemaVersion(); $ver = $this->schemaVersion();
if(!$this->data->conf->dbSQLite3AutoUpd) throw new Update\Exception("manual", ['version' => $ver, 'driver_name' => $this->driverName()]); if(!$this->data->conf->dbSQLite3AutoUpd) throw new Update\Exception("manual", ['version' => $ver, 'driver_name' => $this->driverName()]);
if($ver >= $to) throw new Update\Exception("tooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]); if($ver >= $to) throw new Update\Exception("tooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]);
$sep = \DIRECTORY_SEPARATOR; $sep = \DIRECTORY_SEPARATOR;
$path = \JKingWeb\NewsSync\BASE."sql".$sep."SQLite3".$sep; $path = \JKingWeb\NewsSync\BASE."sql".$sep."SQLite3".$sep;
$this->lock(); $this->lock();
$this->begin(); $this->begin();
for($a = $ver; $a < $to; $a++) { for($a = $ver; $a < $to; $a++) {
$this->begin(); $this->begin();
try { try {
$file = $path.$a.".sql"; $file = $path.$a.".sql";
if(!file_exists($file)) throw new Update\Exception("fileMissing", ['file' => $file, 'driver_name' => $this->driverName()]); if(!file_exists($file)) throw new Update\Exception("fileMissing", ['file' => $file, 'driver_name' => $this->driverName()]);
if(!is_readable($file)) throw new Update\Exception("fileUnreadable", ['file' => $file, 'driver_name' => $this->driverName()]); if(!is_readable($file)) throw new Update\Exception("fileUnreadable", ['file' => $file, 'driver_name' => $this->driverName()]);
$sql = @file_get_contents($file); $sql = @file_get_contents($file);
if($sql===false) throw new Update\Exception("fileUnusable", ['file' => $file, 'driver_name' => $this->driverName()]); if($sql===false) throw new Update\Exception("fileUnusable", ['file' => $file, 'driver_name' => $this->driverName()]);
$this->exec($sql); $this->exec($sql);
} catch(\Throwable $e) { } catch(\Throwable $e) {
// undo any partial changes from the failed update // undo any partial changes from the failed update
$this->rollback(); $this->rollback();
// commit any successful updates if updating by more than one version // commit any successful updates if updating by more than one version
$this->commit(true); $this->commit(true);
// throw the error received // throw the error received
throw $e; throw $e;
} }
$this->commit(); $this->commit();
} }
$this->unlock(); $this->unlock();
$this->commit(); $this->commit();
return true; return true;
} }
public function exec(string $query): bool { public function exec(string $query): bool {
return (bool) $this->db->exec($query); return (bool) $this->db->exec($query);
} }
} }

50
lib/Db/Driver.php

@ -2,29 +2,29 @@
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\NewsSync\Db; namespace JKingWeb\NewsSync\Db;
interface Driver { 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 // 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; 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) // returns a human-friendly name for the driver (for display in installer, for example)
static function driverName(): string; static function driverName(): string;
// returns the version of the scheme of the opened database; if uninitialized should return 0 // returns the version of the scheme of the opened database; if uninitialized should return 0
function schemaVersion(): int; function schemaVersion(): int;
// begin a real or synthetic transactions, with real or synthetic nesting // begin a real or synthetic transactions, with real or synthetic nesting
function begin(): bool; 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 // 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; 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 // 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; function rollback(bool $all = false): bool;
// attempt to advise other processes that they should not attempt to access the database; used during live upgrades // attempt to advise other processes that they should not attempt to access the database; used during live upgrades
function lock(): bool; function lock(): bool;
function unlock(): bool; function unlock(): bool;
function isLocked(): 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 // 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; function update(int $to): bool;
// execute one or more unsanitized SQL queries and return an indication of success // execute one or more unsanitized SQL queries and return an indication of success
function exec(string $query): bool; function exec(string $query): bool;
// perform a single unsanitized query and return a result set // perform a single unsanitized query and return a result set
function query(string $query): Result; function query(string $query): Result;
// ready a prepared statement for later execution // ready a prepared statement for later execution
function prepare(string $query, string ...$paramType): Statement; function prepare(string $query, string ...$paramType): Statement;
} }

98
lib/Db/DriverSQLite3.php

@ -3,57 +3,57 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\Db; namespace JKingWeb\NewsSync\Db;
class DriverSQLite3 implements Driver { class DriverSQLite3 implements Driver {
use Common, CommonSQLite3 { use Common, CommonSQLite3 {
CommonSQLite3::schemaVersion insteadof Common; CommonSQLite3::schemaVersion insteadof Common;
} }
protected $db; protected $db;
protected $data; protected $data;
private function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) { private function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) {
$this->data = $data; $this->data = $data;
$file = $data->conf->dbSQLite3File; $file = $data->conf->dbSQLite3File;
// if the file exists (or we're initializing the database), try to open it and set initial options // if the file exists (or we're initializing the database), try to open it and set initial options
try { try {
$this->db = new \SQLite3($file, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, $data->conf->dbSQLite3Key); $this->db = new \SQLite3($file, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, $data->conf->dbSQLite3Key);
$this->db->enableExceptions(true); $this->db->enableExceptions(true);
$this->exec("PRAGMA journal_mode = wal"); $this->exec("PRAGMA journal_mode = wal");
$this->exec("PRAGMA foreign_keys = yes"); $this->exec("PRAGMA foreign_keys = yes");
} catch(\Throwable $e) { } catch(\Throwable $e) {
// if opening the database doesn't work, check various pre-conditions to find out what the problem might be // if opening the database doesn't work, check various pre-conditions to find out what the problem might be
if(!file_exists($file)) { if(!file_exists($file)) {
if($install && !is_writable(dirname($file))) throw new Exception("fileUncreatable", dirname($file)); if($install && !is_writable(dirname($file))) throw new Exception("fileUncreatable", dirname($file));
throw new Exception("fileMissing", $file); throw new Exception("fileMissing", $file);
} }
if(!is_readable($file) && !is_writable($file)) throw new Exception("fileUnusable", $file); if(!is_readable($file) && !is_writable($file)) throw new Exception("fileUnusable", $file);
if(!is_readable($file)) throw new Exception("fileUnreadable", $file); if(!is_readable($file)) throw new Exception("fileUnreadable", $file);
if(!is_writable($file)) throw new Exception("fileUnwritable", $file); if(!is_writable($file)) throw new Exception("fileUnwritable", $file);
// otherwise the database is probably corrupt // otherwise the database is probably corrupt
throw new Exception("fileCorrupt", $mainfile); throw new Exception("fileCorrupt", $mainfile);
} }
} }
public function __destruct() { public function __destruct() {
$this->db->close(); $this->db->close();
unset($this->db); unset($this->db);
} }
static public function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver { static public function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver {
// check to make sure required extensions are loaded // check to make sure required extensions are loaded
if(class_exists("SQLite3")) { if(class_exists("SQLite3")) {
return new self($data, $install); return new self($data, $install);
} else if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) { } else if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) {
return new DriverSQLite3PDO($data, $install); return new DriverSQLite3PDO($data, $install);
} else { } else {
throw new Exception("extMissing", self::driverName()); throw new Exception("extMissing", self::driverName());
} }
} }
public function query(string $query): Result { public function query(string $query): Result {
return new ResultSQLite3($this->db->query($query), $this->db->changes()); return new ResultSQLite3($this->db->query($query), $this->db->changes());
} }
public function prepareArray(string $query, array $paramTypes): Statement { public function prepareArray(string $query, array $paramTypes): Statement {
return new StatementSQLite3($this->db, $this->db->prepare($query), $paramTypes); return new StatementSQLite3($this->db, $this->db->prepare($query), $paramTypes);
} }
} }

40
lib/Db/DriverSQLite3PDO.php

@ -3,26 +3,26 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\Db; namespace JKingWeb\NewsSync\Db;
class DriverSQLite3 implements Driver { class DriverSQLite3 implements Driver {
use CommonPDO, CommonSQLite3; use CommonPDO, CommonSQLite3;
protected $db; protected $db;
private function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) { private function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) {
// FIXME: stub // FIXME: stub
} }
public function __destruct() { public function __destruct() {
// FIXME: stub // FIXME: stub
} }
static public function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver { static public function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver {
// check to make sure required extensions are loaded // check to make sure required extensions are loaded
if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) { if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) {
return new self($data, $install); return new self($data, $install);
} else if(class_exists("SQLite3")) { } else if(class_exists("SQLite3")) {
return new DriverSQLite3($data, $install); return new DriverSQLite3($data, $install);
} else { } else {
throw new Exception("extMissing", self::driverName()); throw new Exception("extMissing", self::driverName());
} }
} }
} }

16
lib/Db/Result.php

@ -3,13 +3,13 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\Db; namespace JKingWeb\NewsSync\Db;
interface Result extends \Iterator { interface Result extends \Iterator {
function current(); function current();
function key(); function key();
function next(); function next();
function rewind(); function rewind();
function valid(); function valid();
function get(); function get();
function getSingle(); function getSingle();
function changes(); function changes();
} }

96
lib/Db/ResultSQLite3.php

@ -3,62 +3,62 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\Db; namespace JKingWeb\NewsSync\Db;
class ResultSQLite3 implements Result { class ResultSQLite3 implements Result {
protected $st; protected $st;
protected $set; protected $set;
protected $pos = 0; protected $pos = 0;
protected $cur = null; protected $cur = null;
protected $rows = 0; protected $rows = 0;
public function __construct($result, $changes, $statement = null) { public function __construct($result, $changes, $statement = null) {
$this->st = $statement; //keeps the statement from being destroyed, invalidating the result set $this->st = $statement; //keeps the statement from being destroyed, invalidating the result set
$this->set = $result; $this->set = $result;
$this->rows = $changes; $this->rows = $changes;
} }
public function __destruct() { public function __destruct() {
$this->set->finalize(); $this->set->finalize();
unset($this->set); unset($this->set);
} }
public function valid() { public function valid() {
$this->cur = $this->set->fetchArray(\SQLITE3_ASSOC); $this->cur = $this->set->fetchArray(\SQLITE3_ASSOC);
return ($this->cur !== false); return ($this->cur !== false);
} }
public function next() { public function next() {
$this->cur = null; $this->cur = null;
$this->pos += 1; $this->pos += 1;
} }
public function current() { public function current() {
return $this->cur; return $this->cur;
} }
public function key() { public function key() {
return $this->pos; return $this->pos;
} }
public function rewind() { public function rewind() {
$this->pos = 0; $this->pos = 0;
$this->cur = null; $this->cur = null;
$this->set->reset(); $this->set->reset();
} }
public function getSingle() { public function getSingle() {
$this->next(); $this->next();
if($this->valid()) { if($this->valid()) {
$keys = array_keys($this->cur); $keys = array_keys($this->cur);
return $this->cur[array_shift($keys)]; return $this->cur[array_shift($keys)];
} }
return null; return null;
} }
public function get() { public function get() {
$this->next(); $this->next();
return ($this->valid() ? $this->cur : null); return ($this->valid() ? $this->cur : null);
} }
public function changes() { public function changes() {
return $this->rows; return $this->rows;
} }
} }

6
lib/Db/Statement.php

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\Db; namespace JKingWeb\NewsSync\Db;
interface Statement { interface Statement {
function __invoke(&...$values); // alias of run() function __invoke(&...$values); // alias of run()
function run(&...$values): Result; function run(&...$values): Result;
function runArray(array &$values): Result; function runArray(array &$values): Result;
} }

124
lib/Db/StatementSQLite3.php

@ -3,71 +3,71 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\Db; namespace JKingWeb\NewsSync\Db;
class StatementSQLite3 implements Statement { class StatementSQLite3 implements Statement {
protected $db; protected $db;
protected $st; protected $st;
protected $types; protected $types;
public function __construct($db, $st, array $bindings = null) { public function __construct($db, $st, array $bindings = null) {
$this->db = $db; $this->db = $db;
$this->st = $st; $this->st = $st;
$this->types = []; $this->types = [];
foreach($bindings as $binding) { foreach($bindings as $binding) {
switch(trim(strtolower($binding))) { switch(trim(strtolower($binding))) {
case "int": case "int":
case "integer": case "integer":
$this->types[] = \SQLITE3_INTEGER; break; $this->types[] = \SQLITE3_INTEGER; break;
case "float": case "float":
case "double": case "double":
case "real": case "real":
case "numeric": case "numeric":
$this->types[] = \SQLITE3_FLOAT; break; $this->types[] = \SQLITE3_FLOAT; break;
case "date": case "date":
case "time": case "time":
case "datetime": case "datetime":
case "timestamp": case "timestamp":
$this->types[] = \SQLITE3_TEXT; break; $this->types[] = \SQLITE3_TEXT; break;
case "blob": case "blob":
case "bin": case "bin":
case "binary": case "binary":
$this->types[] = \SQLITE3_BLOB; break; $this->types[] = \SQLITE3_BLOB; break;
case "text": case "text":
case "string": case "string":
case "str": case "str":
$this->types[] = \SQLITE3_TEXT; break; $this->types[] = \SQLITE3_TEXT; break;
case "bool": case "bool":
case "boolean": case "boolean":
case "bit": case "bit":
$this->types[] = \SQLITE3_INTEGER; break; $this->types[] = \SQLITE3_INTEGER; break;
default: default:
$this->types[] = \SQLITE3_TEXT; break; $this->types[] = \SQLITE3_TEXT; break;
} }
} }
} }
public function __destruct() { public function __destruct() {
$this->st->close(); $this->st->close();
unset($this->st); unset($this->st);
} }
public function __invoke(&...$values) { public function __invoke(&...$values) {
return $this->runArray($values); return $this->runArray($values);
} }
public function run(&...$values): Result { public function run(&...$values): Result {
return $this->runArray($values); return $this->runArray($values);
} }
public function runArray(array &$values = null): Result { public function runArray(array &$values = null): Result {
$this->st->clear(); $this->st->clear();
$l = sizeof($values); $l = sizeof($values);
for($a = 0; $a < $l; $a++) { for($a = 0; $a < $l; $a++) {
if($values[$a]===null) { if($values[$a]===null) {
$type = \SQLITE3_NULL; $type = \SQLITE3_NULL;
} else { } else {
$type = (array_key_exists($a,$this->types)) ? $this->types[$a] : \SQLITE3_TEXT; $type = (array_key_exists($a,$this->types)) ? $this->types[$a] : \SQLITE3_TEXT;
} }
$this->st->bindParam($a+1, $values[$a], $type); $this->st->bindParam($a+1, $values[$a], $type);
} }
return new ResultSQLite3($this->st->execute(), $this->db->changes(), $this); return new ResultSQLite3($this->st->execute(), $this->db->changes(), $this);
} }
} }

6
lib/ExceptionFatal.php

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync; namespace JKingWeb\NewsSync;
class ExceptionFatal extends AbstractException { class ExceptionFatal extends AbstractException {
public function __construct($msg = "", $code = 0, $e = null) { public function __construct($msg = "", $code = 0, $e = null) {
\Exception::__construct($msg, $code, $e); \Exception::__construct($msg, $code, $e);
} }
} }

294
lib/Lang.php

@ -4,161 +4,161 @@ namespace JKingWeb\NewsSync;
use \Webmozart\Glob\Glob; use \Webmozart\Glob\Glob;
class Lang { class Lang {
const DEFAULT = "en"; const DEFAULT = "en";
const REQUIRED = [ const REQUIRED = [
'Exception.JKingWeb/NewsSync/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in Exception.php', '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/Exception.unknown' => 'An unknown error has occurred',
'Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing' => 'Default language file "{0}" missing', '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.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.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.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.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.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})',
]; ];
static public $path = BASE."locale".DIRECTORY_SEPARATOR; static public $path = BASE."locale".DIRECTORY_SEPARATOR;
static protected $requirementsMet = false; static protected $requirementsMet = false;
static protected $synched = false; static protected $synched = false;
static protected $wanted = self::DEFAULT; static protected $wanted = self::DEFAULT;
static protected $locale = ""; static protected $locale = "";
static protected $loaded = []; static protected $loaded = [];
static protected $strings = self::REQUIRED; static protected $strings = self::REQUIRED;
protected function __construct() {} protected function __construct() {}
static public function set(string $locale, bool $immediate = false): string { static public function set(string $locale, bool $immediate = false): string {
if(!self::$requirementsMet) self::checkRequirements(); if(!self::$requirementsMet) self::checkRequirements();
if($locale==self::$wanted) return $locale; if($locale==self::$wanted) return $locale;
if($locale != "") { if($locale != "") {
$list = self::listFiles(); $list = self::listFiles();
if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT); if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT);
self::$wanted = self::match($locale, $list); self::$wanted = self::match($locale, $list);
} else { } else {
self::$wanted = ""; self::$wanted = "";
} }
self::$synched = false; self::$synched = false;
if($immediate) self::load(); if($immediate) self::load();
return self::$wanted; return self::$wanted;
} }
static public function get(): string { static public function get(): string {
return (self::$locale=="") ? self::DEFAULT : self::$locale; return (self::$locale=="") ? self::DEFAULT : self::$locale;
} }
static public function dump(): array { static public function dump(): array {
return self::$strings; return self::$strings;
} }
static public function msg(string $msgID, $vars = null): string { 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 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::$synched) try {self::load();} catch(Lang\Exception $e) {
if(self::$wanted==self::DEFAULT) { if(self::$wanted==self::DEFAULT) {
self::set("", true); self::set("", true);
} else { } else {
throw $e; throw $e;
} }
} }
if(!array_key_exists($msgID, self::$strings)) throw new Lang\Exception("stringMissing", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]); 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 // variables fed to MessageFormatter must be contained in array
$msg = self::$strings[$msgID]; $msg = self::$strings[$msgID];
if($vars===null) { if($vars===null) {
return $msg; return $msg;
} else if(!is_array($vars)) { } else if(!is_array($vars)) {
$vars = [$vars]; $vars = [$vars];
} }
$msg = \MessageFormatter::formatMessage(self::$locale, $msg, $vars); $msg = \MessageFormatter::formatMessage(self::$locale, $msg, $vars);
if($msg===false) throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]); if($msg===false) throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]);
return $msg; return $msg;
} }
static public function list(string $locale = ""): array { static public function list(string $locale = ""): array {
$out = []; $out = [];
$files = self::listFiles(); $files = self::listFiles();
foreach($files as $tag) { foreach($files as $tag) {
$out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale); $out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale);
} }
return $out; return $out;
} }
static public function match(string $locale, array $list = null): string { static public function match(string $locale, array $list = null): string {
if($list===null) $list = self::listFiles(); if($list===null) $list = self::listFiles();
$default = (self::$locale=="") ? self::DEFAULT : self::$locale; $default = (self::$locale=="") ? self::DEFAULT : self::$locale;
return \Locale::lookup($list,$locale, true, $default); return \Locale::lookup($list,$locale, true, $default);
} }
static protected function checkRequirements(): bool { static protected function checkRequirements(): bool {
if(!extension_loaded("intl")) throw new ExceptionFatal("The \"Intl\" extension is required, but not loaded"); if(!extension_loaded("intl")) throw new ExceptionFatal("The \"Intl\" extension is required, but not loaded");
self::$requirementsMet = true; self::$requirementsMet = true;
return 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 load(): bool { static protected function listFiles(): array {
if(!self::$requirementsMet) self::checkRequirements(); $out = glob(self::$path."*.php");
// if we've requested no locale (""), just load the fallback strings and return // 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(self::$wanted=="") { if(empty($out)) $out = Glob::glob(self::$path."*.php");
self::$strings = self::REQUIRED; $out = array_map(function($file) {
self::$locale = self::$wanted; $file = str_replace(DIRECTORY_SEPARATOR, "/", $file);
self::$synched = true; $file = substr($file, strrpos($file, "/")+1);
return true; return strtolower(substr($file,0,strrpos($file,".")));
} },$out);
// decompose the requested locale from specific to general, building a list of files to load natsort($out);
$tags = \Locale::parseLocale(self::$wanted); return $out;
$files = []; }
while(sizeof($tags) > 0) {
$files[] = strtolower(\Locale::composeLocale($tags)); static protected function load(): bool {
$tag = array_pop($tags); if(!self::$requirementsMet) self::checkRequirements();
} // if we've requested no locale (""), just load the fallback strings and return
// include the default locale as the base if the most general locale requested is not the default if(self::$wanted=="") {
if($tag != self::DEFAULT) $files[] = self::DEFAULT; self::$strings = self::REQUIRED;
// save the list of files to be loaded for later reference self::$locale = self::$wanted;
$loaded = $files; self::$synched = true;
// 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") return true;
$files = []; }
foreach($loaded as $file) { // decompose the requested locale from specific to general, building a list of files to load
if($file==self::$locale) break; $tags = \Locale::parseLocale(self::$wanted);
$files[] = $file; $files = [];
} while(sizeof($tags) > 0) {
// if we need to load all files, start with the fallback strings $files[] = strtolower(\Locale::composeLocale($tags));
$strings = []; $tag = array_pop($tags);
if($files==$loaded) { }
$strings[] = self::REQUIRED; // include the default locale as the base if the most general locale requested is not the default
} else { if($tag != self::DEFAULT) $files[] = self::DEFAULT;
// otherwise start with the strings we already have if we're going from e.g. "fr" to "fr_ca" // save the list of files to be loaded for later reference
$strings[] = self::$strings; $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")
// read files in reverse order $files = [];
$files = array_reverse($files); foreach($loaded as $file) {
foreach($files as $file) { if($file==self::$locale) break;
if(!file_exists(self::$path."$file.php")) throw new Lang\Exception("fileMissing", $file); $files[] = $file;
if(!is_readable(self::$path."$file.php")) throw new Lang\Exception("fileUnreadable", $file); }
try { // if we need to load all files, start with the fallback strings
ob_start(); $strings = [];
$arr = (include self::$path."$file.php"); if($files==$loaded) {
} catch(\Throwable $e) { $strings[] = self::REQUIRED;
$arr = null; } else {
} finally { // otherwise start with the strings we already have if we're going from e.g. "fr" to "fr_ca"
ob_end_clean(); $strings[] = self::$strings;
} }
if(!is_array($arr)) throw new Lang\Exception("fileCorrupt", $file); // read files in reverse order
$strings[] = $arr; $files = array_reverse($files);
} foreach($files as $file) {
// apply the results and return if(!file_exists(self::$path."$file.php")) throw new Lang\Exception("fileMissing", $file);
self::$strings = call_user_func_array("array_replace_recursive", $strings); if(!is_readable(self::$path."$file.php")) throw new Lang\Exception("fileUnreadable", $file);
self::$loaded = $loaded; try {
self::$locale = self::$wanted; ob_start();
return true; $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;
}
} }

34
lib/Lang/Exception.php

@ -3,22 +3,22 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\Lang; namespace JKingWeb\NewsSync\Lang;
class Exception extends \JKingWeb\NewsSync\AbstractException { 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) { function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
if(!self::$test) { if(!self::$test) {
parent::__construct($msgID, $vars, $e); parent::__construct($msgID, $vars, $e);
} else { } else {
$codeID = "Lang/Exception.$msgID"; $codeID = "Lang/Exception.$msgID";
if(!array_key_exists($codeID,self::CODES)) { if(!array_key_exists($codeID,self::CODES)) {
$code = -1; $code = -1;
$msg = "Exception.".str_replace("\\","/",parent::class).".uncoded"; $msg = "Exception.".str_replace("\\","/",parent::class).".uncoded";
$vars = $msgID; $vars = $msgID;
} else { } else {
$code = self::CODES[$codeID]; $code = self::CODES[$codeID];
$msg = "Exception.".str_replace("\\","/",__CLASS__).".$msgID"; $msg = "Exception.".str_replace("\\","/",__CLASS__).".$msgID";
} }
\Exception::__construct($msg, $code, $e); \Exception::__construct($msg, $code, $e);
} }
} }
} }

18
lib/RuntimeData.php

@ -3,14 +3,14 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync; namespace JKingWeb\NewsSync;
class RuntimeData { class RuntimeData {
public $conf; public $conf;
public $db; public $db;
public $auth; public $auth;
public function __construct(Conf $conf) { public function __construct(Conf $conf) {
$this->conf = $conf; $this->conf = $conf;
Lang::set($conf->lang); Lang::set($conf->lang);
$this->db = new Database($this); $this->db = new Database($this);
$this->user = new User($this); $this->user = new User($this);
} }
} }

474
lib/User.php

@ -3,241 +3,241 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync; namespace JKingWeb\NewsSync;
class User { class User {
public $id = null; public $id = null;
protected $data; protected $data;
protected $u; protected $u;
protected $authz = true; protected $authz = true;
protected $existSupported = 0; protected $existSupported = 0;
protected $authzSupported = 0; protected $authzSupported = 0;
static public function listDrivers(): array { static public function listDrivers(): array {
$sep = \DIRECTORY_SEPARATOR; $sep = \DIRECTORY_SEPARATOR;
$path = __DIR__.$sep."User".$sep; $path = __DIR__.$sep."User".$sep;
$classes = []; $classes = [];
foreach(glob($path."Driver?*.php") as $file) { foreach(glob($path."Driver?*.php") as $file) {
$name = basename($file, ".php"); $name = basename($file, ".php");
$name = NS_BASE."Db\\$name"; $name = NS_BASE."Db\\$name";
if(class_exists($name)) { if(class_exists($name)) {
$classes[$name] = $name::driverName(); $classes[$name] = $name::driverName();
} }
} }
return $classes; return $classes;
} }
public function __construct(\JKingWeb\NewsSync\RuntimeData $data) { public function __construct(\JKingWeb\NewsSync\RuntimeData $data) {
$this->data = $data; $this->data = $data;
$driver = $data->conf->userDriver; $driver = $data->conf->userDriver;
$this->u = $driver::create($data); $this->u = $driver::create($data);
$this->existSupported = $this->u->driverFunctions("userExists"); $this->existSupported = $this->u->driverFunctions("userExists");
$this->authzSupported = $this->u->driverFunctions("authorize"); $this->authzSupported = $this->u->driverFunctions("authorize");
} }
public function __toString() { public function __toString() {
if($this->id===null) $this->credentials(); if($this->id===null) $this->credentials();
return (string) $this->id; return (string) $this->id;
} }
public function credentials(): array { public function credentials(): array {
if($this->data->conf->userAuthPreferHTTP) { if($this->data->conf->userAuthPreferHTTP) {
return $this->credentialsHTTP(); return $this->credentialsHTTP();
} else { } else {
return $this->credentialsForm(); return $this->credentialsForm();
} }
} }
public function credentialsForm(): array { public function credentialsForm(): array {
// FIXME: stub // FIXME: stub
$this->id = "john.doe@example.com"; $this->id = "john.doe@example.com";
return ["user" => "john.doe@example.com", "password" => "secret"]; return ["user" => "john.doe@example.com", "password" => "secret"];
} }
public function credentialsHTTP(): array { public function credentialsHTTP(): array {
if($_SERVER['PHP_AUTH_USER']) { if($_SERVER['PHP_AUTH_USER']) {
$out = ["user" => $_SERVER['PHP_AUTH_USER'], "password" => $_SERVER['PHP_AUTH_PW']]; $out = ["user" => $_SERVER['PHP_AUTH_USER'], "password" => $_SERVER['PHP_AUTH_PW']];
} else if($_SERVER['REMOTE_USER']) { } else if($_SERVER['REMOTE_USER']) {
$out = ["user" => $_SERVER['REMOTE_USER'], "password" => ""]; $out = ["user" => $_SERVER['REMOTE_USER'], "password" => ""];
} else { } else {
$out = ["user" => "", "password" => ""]; $out = ["user" => "", "password" => ""];
} }
if($this->data->conf->userComposeNames && $out["user"] != "") { if($this->data->conf->userComposeNames && $out["user"] != "") {
$out["user"] = $this->composeName($out["user"]); $out["user"] = $this->composeName($out["user"]);
} }
$this->id = $out["user"]; $this->id = $out["user"];
return $out; return $out;
} }
public function auth(string $user = null, string $password = null): bool { public function auth(string $user = null, string $password = null): bool {
if($user===null) { if($user===null) {
if($this->data->conf->userAuthPreferHTTP) return $this->authHTTP(); if($this->data->conf->userAuthPreferHTTP) return $this->authHTTP();
return $this->authForm(); return $this->authForm();
} else { } else {
if($this->u->auth($user, $password)) { if($this->u->auth($user, $password)) {
$this->authPostProcess($user, $password); $this->authPostProcess($user, $password);
return true; return true;
} }
return false; return false;
} }
} }
public function authForm(): bool { public function authForm(): bool {
$cred = $this->credentialsForm(); $cred = $this->credentialsForm();
if(!$cred["user"]) return $this->challengeForm(); if(!$cred["user"]) return $this->challengeForm();
if(!$this->u->auth($cred["user"], $cred["password"])) return $this->challengeForm(); if(!$this->u->auth($cred["user"], $cred["password"])) return $this->challengeForm();
$this->authPostProcess($cred["user"], $cred["password"]); $this->authPostProcess($cred["user"], $cred["password"]);
return true; return true;
} }
public function authHTTP(): bool { public function authHTTP(): bool {
$cred = $this->credentialsHTTP(); $cred = $this->credentialsHTTP();
if(!$cred["user"]) return $this->challengeHTTP(); if(!$cred["user"]) return $this->challengeHTTP();
if(!$this->u->auth($cred["user"], $cred["password"])) return $this->challengeHTTP(); if(!$this->u->auth($cred["user"], $cred["password"])) return $this->challengeHTTP();
$this->authPostProcess($cred["user"], $cred["password"]); $this->authPostProcess($cred["user"], $cred["password"]);
return true; return true;
} }
public function driverFunctions(string $function = null) { public function driverFunctions(string $function = null) {
return $this->u->driverFunctions($function); return $this->u->driverFunctions($function);
} }
public function list(string $domain = null): array { public function list(string $domain = null): array {
if($this->u->driverFunctions("userList")==User\Driver::FUNC_EXTERNAL) { if($this->u->driverFunctions("userList")==User\Driver::FUNC_EXTERNAL) {
if($domain===null) { if($domain===null) {
if(!$this->data->user->authorize("@".$domain, "userList")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userList", "user" => $domain]); if(!$this->data->user->authorize("@".$domain, "userList")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userList", "user" => $domain]);
} else { } else {
if(!$this->data->user->authorize("", "userList")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userList", "user" => "all users"]); if(!$this->data->user->authorize("", "userList")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userList", "user" => "all users"]);
} }
return $this->u->userList($domain); return $this->u->userList($domain);
} else { } else {
return $this->data->db->userList($domain); return $this->data->db->userList($domain);
} }
} }
public function authorize(string $affectedUser, string $action, int $promoteLevel = 0): bool { public function authorize(string $affectedUser, string $action, int $promoteLevel = 0): bool {
if(!$this->authz) return true; if(!$this->authz) return true;
if($this->id===null) $this->credentials(); if($this->id===null) $this->credentials();
if($this->authzSupported) return $this->u->authorize($affectedUser, $action, $promoteLevel); 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 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; if($affectedUser==$this->id && $action != "userRightsSet") return true;
return false; return false;
} }
public function authorizationEnabled(bool $setting = null): bool { public function authorizationEnabled(bool $setting = null): bool {
if($setting===null) return $this->authz; if($setting===null) return $this->authz;
$this->authz = $setting; $this->authz = $setting;
return $setting; return $setting;
} }
public function exists(string $user): bool { public function exists(string $user): bool {
if($this->u->driverFunctions("userExists") != User\Driver::FUNC_INTERNAL) { 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->data->user->authorize($user, "userExists")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userExists", "user" => $user]);
} }
if(!$this->existSupported) return true; if(!$this->existSupported) return true;
$out = $this->u->userExists($user); $out = $this->u->userExists($user);
if($out && $this->existSupported==User\Driver::FUNC_EXTERNAL && !$this->data->db->userExist($user)) { if($out && $this->existSupported==User\Driver::FUNC_EXTERNAL && !$this->data->db->userExist($user)) {
try {$this->data->db->userAdd($user);} catch(\Throwable $e) {} try {$this->data->db->userAdd($user);} catch(\Throwable $e) {}
} }
return $out; return $out;
} }
public function add($user, $password = null): bool { public function add($user, $password = null): bool {
if($this->u->driverFunctions("userAdd") != User\Driver::FUNC_INTERNAL) { 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->data->user->authorize($user, "userAdd")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userAdd", "user" => $user]);
} }
if($this->exists($user)) return false; if($this->exists($user)) return false;
$out = $this->u->userAdd($user, $password); $out = $this->u->userAdd($user, $password);
if($out && $this->u->driverFunctions("userAdd") != User\Driver::FUNC_INTERNAL) { if($out && $this->u->driverFunctions("userAdd") != User\Driver::FUNC_INTERNAL) {
try { try {
if(!$this->data->db->userExists($user)) $this->data->db->userAdd($user, $password); if(!$this->data->db->userExists($user)) $this->data->db->userAdd($user, $password);
} catch(\Throwable $e) {} } catch(\Throwable $e) {}
} }
return $out; return $out;
} }
public function remove(string $user): bool { public function remove(string $user): bool {
if($this->u->driverFunctions("userRemove") != User\Driver::FUNC_INTERNAL) { 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->data->user->authorize($user, "userRemove")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRemove", "user" => $user]);
} }
if(!$this->exists($user)) return false; if(!$this->exists($user)) return false;
$out = $this->u->userRemove($user); $out = $this->u->userRemove($user);
if($out && $this->u->driverFunctions("userRemove") != User\Driver::FUNC_INTERNAL) { if($out && $this->u->driverFunctions("userRemove") != User\Driver::FUNC_INTERNAL) {
try { try {
if($this->data->db->userExists($user)) $this->data->db->userRemove($user); if($this->data->db->userExists($user)) $this->data->db->userRemove($user);
} catch(\Throwable $e) {} } catch(\Throwable $e) {}
} }
return $out; return $out;
} }
public function passwordSet(string $user, string $password): bool { public function passwordSet(string $user, string $password): bool {
if($this->u->driverFunctions("userPasswordSet") != User\Driver::FUNC_INTERNAL) { 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->data->user->authorize($user, "userPasswordSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPasswordSet", "user" => $user]);
} }
if(!$this->exists($user)) return false; if(!$this->exists($user)) return false;
return $this->u->userPasswordSet($user, $password); return $this->u->userPasswordSet($user, $password);
} }
public function propertiesGet(string $user): array { public function propertiesGet(string $user): array {
if($this->u->driverFunctions("userPropertiesGet") != User\Driver::FUNC_INTERNAL) { 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->data->user->authorize($user, "userPropertiesGet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPropertiesGet", "user" => $user]);
} }
if(!$this->exists($user)) return false; if(!$this->exists($user)) return false;
$domain = null; $domain = null;
if($this->data->conf->userComposeNames) $domain = substr($user,strrpos($user,"@")+1); if($this->data->conf->userComposeNames) $domain = substr($user,strrpos($user,"@")+1);
$init = [ $init = [
"id" => $user, "id" => $user,
"name" => $user, "name" => $user,
"rights" => User\Driver::RIGHTS_NONE, "rights" => User\Driver::RIGHTS_NONE,
"domain" => $domain "domain" => $domain
]; ];
if($this->u->driverFunctions("userPropertiesGet") != User\Driver::FUNC_NOT_IMPLEMENTED) { if($this->u->driverFunctions("userPropertiesGet") != User\Driver::FUNC_NOT_IMPLEMENTED) {
return array_merge($init, $this->u->userPropertiesGet($user)); return array_merge($init, $this->u->userPropertiesGet($user));
} }
return $init; return $init;
} }
public function propertiesSet(string $user, array $properties): array { public function propertiesSet(string $user, array $properties): array {
if($this->u->driverFunctions("userPropertiesSet") != User\Driver::FUNC_INTERNAL) { 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->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"]); if(!$this->exists($user)) throw new User\Exception("doesNotExist", ["user" => $user, "action" => "userPropertiesSet"]);
return $this->u->userPropertiesSet($user, $properties); return $this->u->userPropertiesSet($user, $properties);
} }
public function rightsGet(string $user): int { public function rightsGet(string $user): int {
if($this->u->driverFunctions("userRightsGet") != User\Driver::FUNC_INTERNAL) { 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]); 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 // 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; if(!$this->exists($user)) return User\Driver::RIGHTS_NONE;
return $this->u->userRightsGet($user); return $this->u->userRightsGet($user);
} }
public function rightsSet(string $user, int $level): bool { public function rightsSet(string $user, int $level): bool {
if($this->u->driverFunctions("userRightsSet") != User\Driver::FUNC_INTERNAL) { 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->data->user->authorize($user, "userRightsSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRightsSet", "user" => $user]);
} }
if(!$this->exists($user)) return false; if(!$this->exists($user)) return false;
return $this->u->userRightsSet($user, $level); return $this->u->userRightsSet($user, $level);
} }
// FIXME: stubs // FIXME: stubs
public function challenge(): bool {throw new User\Exception("authFailed");} public function challenge(): bool {throw new User\Exception("authFailed");}
public function challengeForm(): bool {throw new User\Exception("authFailed");} public function challengeForm(): bool {throw new User\Exception("authFailed");}
public function challengeHTTP(): bool {throw new User\Exception("authFailed");} public function challengeHTTP(): bool {throw new User\Exception("authFailed");}
protected function composeName(string $user): string { protected function composeName(string $user): string {
if(preg_match("/.+?@[^@]+$/",$user)) { if(preg_match("/.+?@[^@]+$/",$user)) {
return $user; return $user;
} else { } else {
return $user."@".$_SERVER['HTTP_HOST']; return $user."@".$_SERVER['HTTP_HOST'];
} }
} }
protected function authPostprocess(string $user, string $password): bool { protected function authPostprocess(string $user, string $password): bool {
if($this->u->driverFunctions("auth") != User\Driver::FUNC_INTERNAL && !$this->data->db->userExists($user)) { if($this->u->driverFunctions("auth") != User\Driver::FUNC_INTERNAL && !$this->data->db->userExists($user)) {
if($password=="") $password = null; if($password=="") $password = null;
try {$this->data->db->userAdd($user, $password);} catch(\Throwable $e) {} try {$this->data->db->userAdd($user, $password);} catch(\Throwable $e) {}
} }
return true; return true;
} }
} }

44
lib/User/Driver.php

@ -3,28 +3,28 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\User; namespace JKingWeb\NewsSync\User;
Interface Driver { Interface Driver {
const FUNC_NOT_IMPLEMENTED = 0; const FUNC_NOT_IMPLEMENTED = 0;
const FUNC_INTERNAL = 1; const FUNC_INTERNAL = 1;
const FUNC_EXTERNAL = 2; const FUNC_EXTERNAL = 2;
const RIGHTS_NONE = 0; const RIGHTS_NONE = 0;
const RIGHTS_DOMAIN_MANAGER = 25; const RIGHTS_DOMAIN_MANAGER = 25;
const RIGHTS_DOMAIN_ADMIN = 50; const RIGHTS_DOMAIN_ADMIN = 50;
const RIGHTS_GLOBAL_MANAGER = 75; const RIGHTS_GLOBAL_MANAGER = 75;
const RIGHTS_GLOBAL_ADMIN = 100; const RIGHTS_GLOBAL_ADMIN = 100;
static function create(\JKingWeb\NewsSync\RuntimeData $data): Driver; static function create(\JKingWeb\NewsSync\RuntimeData $data): Driver;
static function driverName(): string; static function driverName(): string;
function driverFunctions(string $function = null); function driverFunctions(string $function = null);
function auth(string $user, string $password): bool; function auth(string $user, string $password): bool;
function authorize(string $affectedUser, string $action): bool; function authorize(string $affectedUser, string $action): bool;
function userExists(string $user): bool; function userExists(string $user): bool;
function userAdd(string $user, string $password = null): bool; function userAdd(string $user, string $password = null): bool;
function userRemove(string $user): bool; function userRemove(string $user): bool;
function userList(string $domain = null): array; function userList(string $domain = null): array;
function userPasswordSet(string $user, string $newPassword, string $oldPassword): bool; function userPasswordSet(string $user, string $newPassword, string $oldPassword): bool;
function userPropertiesGet(string $user): array; function userPropertiesGet(string $user): array;
function userPropertiesSet(string $user, array $properties): array; function userPropertiesSet(string $user, array $properties): array;
function userRightsGet(string $user): int; function userRightsGet(string $user): int;
function userRightsSet(string $user, int $level): bool; function userRightsSet(string $user, int $level): bool;
} }

68
lib/User/DriverInternal.php

@ -3,43 +3,43 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\User; namespace JKingWeb\NewsSync\User;
class DriverInternal implements Driver { class DriverInternal implements Driver {
use InternalFunctions; use InternalFunctions;
protected $data; protected $data;
protected $db; protected $db;
protected $functions = [ protected $functions = [
"auth" => Driver::FUNC_INTERNAL, "auth" => Driver::FUNC_INTERNAL,
"authorize" => Driver::FUNC_INTERNAL, "authorize" => Driver::FUNC_INTERNAL,
"userList" => Driver::FUNC_INTERNAL, "userList" => Driver::FUNC_INTERNAL,
"userExists" => Driver::FUNC_INTERNAL, "userExists" => Driver::FUNC_INTERNAL,
"userAdd" => Driver::FUNC_INTERNAL, "userAdd" => Driver::FUNC_INTERNAL,
"userRemove" => Driver::FUNC_INTERNAL, "userRemove" => Driver::FUNC_INTERNAL,
"userPasswordSet" => Driver::FUNC_INTERNAL, "userPasswordSet" => Driver::FUNC_INTERNAL,
"userPropertiesGet" => Driver::FUNC_INTERNAL, "userPropertiesGet" => Driver::FUNC_INTERNAL,
"userPropertiesSet" => Driver::FUNC_INTERNAL, "userPropertiesSet" => Driver::FUNC_INTERNAL,
"userRightsGet" => Driver::FUNC_INTERNAL, "userRightsGet" => Driver::FUNC_INTERNAL,
"userRightsSet" => Driver::FUNC_INTERNAL, "userRightsSet" => Driver::FUNC_INTERNAL,
]; ];
static public function create(\JKingWeb\NewsSync\RuntimeData $data): Driver { static public function create(\JKingWeb\NewsSync\RuntimeData $data): Driver {
return new static($data); return new static($data);
} }
public function __construct(\JKingWeb\NewsSync\RuntimeData $data) { public function __construct(\JKingWeb\NewsSync\RuntimeData $data) {
$this->data = $data; $this->data = $data;
$this->db = $this->data->db; $this->db = $this->data->db;
} }
static public function driverName(): string { static public function driverName(): string {
return "Internal"; return "Internal";
} }
public function driverFunctions(string $function = null) { public function driverFunctions(string $function = null) {
if($function===null) return $this->functions; if($function===null) return $this->functions;
if(array_key_exists($function, $this->functions)) { if(array_key_exists($function, $this->functions)) {
return $this->functions[$function]; return $this->functions[$function];
} else { } else {
return Driver::FUNC_NOT_IMPLEMENTED; return Driver::FUNC_NOT_IMPLEMENTED;
} }
} }
} }

132
lib/User/InternalFunctions.php

@ -2,78 +2,78 @@
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\NewsSync\User; namespace JKingWeb\NewsSync\User;
trait InternalFunctions { trait InternalFunctions {
protected $actor = []; protected $actor = [];
function auth(string $user, string $password): bool { function auth(string $user, string $password): bool {
if(!$this->data->user->exists($user)) return false; if(!$this->data->user->exists($user)) return false;
$hash = $this->db->userPasswordGet($user); $hash = $this->db->userPasswordGet($user);
if(!$hash) return false; if(!$hash) return false;
return password_verify($password, $hash); return password_verify($password, $hash);
} }
function authorize(string $affectedUser, string $action, int $newRightsLevel = 0): bool { 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 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; if($affectedUser==$this->data->user->id && $action != "userRightsSet") return true;
// get properties of actor if not already available // get properties of actor if not already available
if(!sizeof($this->actor)) $this->actor = $this->data->user->propertiesGet($this->data->user->id); if(!sizeof($this->actor)) $this->actor = $this->data->user->propertiesGet($this->data->user->id);
$rights =& $this->actor["rights"]; $rights =& $this->actor["rights"];
// if actor is a global admin, accept the request // if actor is a global admin, accept the request
if($rights==self::RIGHTS_GLOBAL_ADMIN) return true; if($rights==self::RIGHTS_GLOBAL_ADMIN) return true;
// if actor is a common user, deny the request // if actor is a common user, deny the request
if($rights==self::RIGHTS_NONE) return false; if($rights==self::RIGHTS_NONE) return false;
// if actor is not some other sort of admin, deny the request // 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(!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 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) { if($this->data->conf->userComposeNames && $this->actor["domain"] && $rights != self::RIGHTS_GLOBAL_MANAGER) {
$test = "@".$this->actor["domain"]; $test = "@".$this->actor["domain"];
if(substr($affectedUser,-1*strlen($test)) != $test) return false; if(substr($affectedUser,-1*strlen($test)) != $test) return false;
} }
// certain actions shouldn't check affected user's rights // certain actions shouldn't check affected user's rights
if(in_array($action, ["userRightsGet","userExists","userList"], true)) return true; if(in_array($action, ["userRightsGet","userExists","userList"], true)) return true;
if($action=="userRightsSet") { if($action=="userRightsSet") {
// setting rights above your own (or equal to your own, for managers) is not allowed // 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; if($newRightsLevel > $rights || ($rights != self::RIGHTS_DOMAIN_ADMIN && $newRightsLevel==$rights)) return false;
} }
$affectedRights = $this->data->user->rightsGet($affectedUser); $affectedRights = $this->data->user->rightsGet($affectedUser);
// acting for users with rights greater than your own (or equal, for managers) is not allowed // 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; if($affectedRights > $rights || ($rights != self::RIGHTS_DOMAIN_ADMIN && $affectedRights==$rights)) return false;
return true; return true;
} }
function userExists(string $user): bool { function userExists(string $user): bool {
return $this->db->userExists($user); return $this->db->userExists($user);
} }
function userAdd(string $user, string $password = null): bool { function userAdd(string $user, string $password = null): bool {
return $this->db->userAdd($user, $password); return $this->db->userAdd($user, $password);
} }
function userRemove(string $user): bool { function userRemove(string $user): bool {
return $this->db->userRemove($user); return $this->db->userRemove($user);
} }
function userList(string $domain = null): array { function userList(string $domain = null): array {
return $this->db->userList($domain); return $this->db->userList($domain);
} }
function userPasswordSet(string $user, string $newPassword, string $oldPassword): bool { function userPasswordSet(string $user, string $newPassword, string $oldPassword): bool {
return $this->db->userPasswordSet($user, $newPassword); return $this->db->userPasswordSet($user, $newPassword);
} }
function userPropertiesGet(string $user): array { function userPropertiesGet(string $user): array {
return $this->db->userPropertiesGet($user); return $this->db->userPropertiesGet($user);
} }
function userPropertiesSet(string $user, array $properties): array { function userPropertiesSet(string $user, array $properties): array {
return $this->db->userPropertiesSet($user, $properties); return $this->db->userPropertiesSet($user, $properties);
} }
function userRightsGet(string $user): int { function userRightsGet(string $user): int {
return $this->db->userRightsGet($user); return $this->db->userRightsGet($user);
} }
function userRightsSet(string $user, int $level): bool { function userRightsSet(string $user, int $level): bool {
return $this->db->userRightsSet($user, $level); return $this->db->userRightsSet($user, $level);
} }
} }

92
locale/en.php

@ -1,52 +1,52 @@
<?php <?php
return [ return [
'Exception.JKingWeb/NewsSync/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in Exception.php', 'Exception.JKingWeb/NewsSync/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in Exception.php',
//this should not usually be encountered //this should not usually be encountered
'Exception.JKingWeb/NewsSync/Exception.unknown' => 'An unknown error has occurred', '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.defaultFileMissing' => 'Default language file "{0}" missing',
'Exception.JKingWeb/NewsSync/Lang/Exception.fileMissing' => 'Language file "{0}" is not available', '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.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.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.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.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.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.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.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.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/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/User/Exception.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists', 'Exception.JKingWeb/NewsSync/Db/Exception.extMissing' => 'Required PHP extension for driver "{0}" not installed',
'Exception.JKingWeb/NewsSync/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist', 'Exception.JKingWeb/NewsSync/Db/Exception.fileMissing' => 'Database file "{0}" does not exist',
'Exception.JKingWeb/NewsSync/User/Exception.authMissing' => 'Please log in to proceed', 'Exception.JKingWeb/NewsSync/Db/Exception.fileUnreadable' => 'Insufficient permissions to open database file "{0}" for reading',
'Exception.JKingWeb/NewsSync/User/Exception.authFailed' => 'Authentication failed', 'Exception.JKingWeb/NewsSync/Db/Exception.fileUnwritable' => 'Insufficient permissions to open database file "{0}" for writing',
'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.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}',
]; ];

138
sql/SQLite3/0.sql

@ -1,110 +1,110 @@
-- newsfeeds, deduplicated -- newsfeeds, deduplicated
create table newssync_feeds( create table newssync_feeds(
id integer primary key not null, -- sequence number id integer primary key not null, -- sequence number
url TEXT not null, -- URL of feed url TEXT not null, -- URL of feed
title TEXT, -- default title of feed title TEXT, -- default title of feed
favicon TEXT, -- URL of favicon favicon TEXT, -- URL of favicon
source TEXT, -- URL of site to which the feed belongs source TEXT, -- URL of site to which the feed belongs
updated datetime, -- time at which the feed was last fetched updated datetime, -- time at which the feed was last fetched
modified datetime, -- time at which the feed last actually changed 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 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_count integer not null default 0, -- count of successive times update resulted in error since last successful update
err_msg TEXT, -- last error message err_msg TEXT, -- last error message
username TEXT not null default '', -- HTTP authentication username username TEXT not null default '', -- HTTP authentication username
password TEXT not null default '', -- HTTP authentication password (this is stored in plain text) 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 unique(url,username,password) -- a URL with particular credentials should only appear once
); );
-- entries in newsfeeds -- entries in newsfeeds
create table newssync_articles( create table newssync_articles(
id integer primary key not null, -- sequence number id integer primary key not null, -- sequence number
feed integer not null references newssync_feeds(id) on delete cascade, -- feed for the subscription feed integer not null references newssync_feeds(id) on delete cascade, -- feed for the subscription
url TEXT not null, -- URL of article url TEXT not null, -- URL of article
title TEXT, -- article title title TEXT, -- article title
author TEXT, -- author's name author TEXT, -- author's name
published datetime, -- time of original publication published datetime, -- time of original publication
edited datetime, -- time of last edit edited datetime, -- time of last edit
guid TEXT, -- GUID guid TEXT, -- GUID
content TEXT, -- content, as (X)HTML content TEXT, -- content, as (X)HTML
modified datetime not null default CURRENT_TIMESTAMP, -- date when article properties were last modified modified datetime not null default CURRENT_TIMESTAMP, -- date when article properties were last modified
hash varchar(64) not null, -- ownCloud hash hash varchar(64) not null, -- ownCloud hash
fingerprint varchar(64) not null, -- ownCloud fingerprint 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 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 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 -- enclosures associated with articles
create table newssync_enclosures( create table newssync_enclosures(
article integer not null references newssync_articles(id) on delete cascade, article integer not null references newssync_articles(id) on delete cascade,
url TEXT, url TEXT,
type varchar(255) type varchar(255)
); );
-- author labels ("categories" in RSS/Atom parlance) associated with newsfeed entries -- author labels ("categories" in RSS/Atom parlance) associated with newsfeed entries
create table newssync_tags( create table newssync_tags(
article integer not null references newssync_articles(id) on delete cascade, article integer not null references newssync_articles(id) on delete cascade,
name TEXT name TEXT
); );
-- settings -- settings
create table newssync_settings( create table newssync_settings(
key varchar(255) primary key not null, -- key varchar(255) primary key not null, --
value varchar(255), -- value varchar(255), --
type varchar(255) not null check( type varchar(255) not null check(
type in('int','numeric','text','timestamp','date','time','bool','null','json') type in('int','numeric','text','timestamp','date','time','bool','null','json')
) default 'text' -- ) default 'text' --
); );
-- users -- users
create table newssync_users( create table newssync_users(
id TEXT primary key not null, -- user id id TEXT primary key not null, -- user id
password TEXT, -- password, salted and hashed; if using external authentication this would be blank password TEXT, -- password, salted and hashed; if using external authentication this would be blank
name TEXT, -- display name name TEXT, -- display name
avatar_url TEXT, -- external URL to avatar avatar_url TEXT, -- external URL to avatar
avatar_type TEXT, -- internal avatar image's MIME content type avatar_type TEXT, -- internal avatar image's MIME content type
avatar_data BLOB, -- internal avatar image's binary data avatar_data BLOB, -- internal avatar image's binary data
rights integer not null default 0 -- any administrative rights the user may have rights integer not null default 0 -- any administrative rights the user may have
); );
-- TT-RSS categories and ownCloud folders -- TT-RSS categories and ownCloud folders
create table newssync_categories( create table newssync_categories(
id integer primary key not null, -- sequence number 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 owner TEXT not null references newssync_users(id) on delete cascade on update cascade, -- owner of category
parent integer, -- parent category id parent integer, -- parent category id
folder integer not null, -- first-level category (ownCloud folder) folder integer not null, -- first-level category (ownCloud folder)
name TEXT not null, -- category name name TEXT not null, -- category name
modified datetime not null default CURRENT_TIMESTAMP, -- 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 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 -- users' subscriptions to newsfeeds, with settings
create table newssync_subscriptions( create table newssync_subscriptions(
id integer primary key not null, -- sequence number 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 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 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 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 modified datetime not null default CURRENT_TIMESTAMP, -- date at which subscription properties were last modified
title TEXT, -- user-supplied title title TEXT, -- user-supplied title
order_type int not null default 0, -- ownCloud sort order order_type int not null default 0, -- ownCloud sort order
pinned boolean not null default 0, -- whether feed is pinned (always sorts at top) 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 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 unique(owner,feed) -- a given feed should only appear once for a given owner
); );
-- users' actions on newsfeed entries -- users' actions on newsfeed entries
create table newssync_subscription_articles( create table newssync_subscription_articles(
id integer primary key not null, id integer primary key not null,
article integer not null references newssync_articles(id) on delete cascade, article integer not null references newssync_articles(id) on delete cascade,
read boolean not null default 0, read boolean not null default 0,
starred boolean not null default 0, starred boolean not null default 0,
modified datetime not null default CURRENT_TIMESTAMP modified datetime not null default CURRENT_TIMESTAMP
); );
-- user labels associated with newsfeed entries -- user labels associated with newsfeed entries
create table newssync_labels( create table newssync_labels(
sub_article integer not null references newssync_subscription_articles(id) on delete cascade, -- 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, owner TEXT not null references newssync_users(id) on delete cascade on update cascade,
name TEXT name TEXT
); );
create index newssync_label_names on newssync_labels(name); create index newssync_label_names on newssync_labels(name);

142
tests/TestConf.php

@ -5,101 +5,101 @@ use \org\bovigo\vfs\vfsStream;
class TestConf extends \PHPUnit\Framework\TestCase { class TestConf extends \PHPUnit\Framework\TestCase {
use Test\Tools; use Test\Tools;
static $vfs; static $vfs;
static $path; static $path;
static function setUpBeforeClass() { static function setUpBeforeClass() {
self::$vfs = vfsStream::setup("root", null, [ self::$vfs = vfsStream::setup("root", null, [
'confGood' => '<?php return Array("lang" => "xx");', 'confGood' => '<?php return Array("lang" => "xx");',
'confNotArray' => '<?php return 0;', 'confNotArray' => '<?php return 0;',
'confCorrupt' => '<?php return 0', 'confCorrupt' => '<?php return 0',
'confNotPHP' => 'DEAD BEEF', 'confNotPHP' => 'DEAD BEEF',
'confEmpty' => '', 'confEmpty' => '',
'confUnreadable' => '', 'confUnreadable' => '',
]); ]);
self::$path = self::$vfs->url()."/"; self::$path = self::$vfs->url()."/";
// set up a file without read access // set up a file without read access
chmod(self::$path."confUnreadable", 0000); chmod(self::$path."confUnreadable", 0000);
} }
static function tearDownAfterClass() { static function tearDownAfterClass() {
self::$path = null; self::$path = null;
self::$vfs = null; self::$vfs = null;
} }
function testLoadDefaultValues() { function testLoadDefaultValues() {
$this->assertInstanceOf(Conf::class, new Conf()); $this->assertInstanceOf(Conf::class, new Conf());
} }
/** /**
* @depends testLoadDefaultValues * @depends testLoadDefaultValues
*/ */
function testImportFromArray() { function testImportFromArray() {
$arr = ['lang' => "xx"]; $arr = ['lang' => "xx"];
$conf = new Conf(); $conf = new Conf();
$conf->import($arr); $conf->import($arr);
$this->assertEquals("xx", $conf->lang); $this->assertEquals("xx", $conf->lang);
} }
/** /**
* @depends testImportFromArray * @depends testImportFromArray
*/ */
function testImportFromFile() { function testImportFromFile() {
$conf = new Conf(); $conf = new Conf();
$conf->importFile(self::$path."confGood"); $conf->importFile(self::$path."confGood");
$this->assertEquals("xx", $conf->lang); $this->assertEquals("xx", $conf->lang);
$conf = new Conf(self::$path."confGood"); $conf = new Conf(self::$path."confGood");
$this->assertEquals("xx", $conf->lang); $this->assertEquals("xx", $conf->lang);
} }
/** /**
* @depends testImportFromFile * @depends testImportFromFile
*/ */
function testImportFromMissingFile() { function testImportFromMissingFile() {
$this->assertException("fileMissing", "Conf"); $this->assertException("fileMissing", "Conf");
$conf = new Conf(self::$path."confMissing"); $conf = new Conf(self::$path."confMissing");
} }
/** /**
* @depends testImportFromFile * @depends testImportFromFile
*/ */
function testImportFromEmptyFile() { function testImportFromEmptyFile() {
$this->assertException("fileCorrupt", "Conf"); $this->assertException("fileCorrupt", "Conf");
$conf = new Conf(self::$path."confEmpty"); $conf = new Conf(self::$path."confEmpty");
} }
/** /**
* @depends testImportFromFile * @depends testImportFromFile
*/ */
function testImportFromFileWithoutReadPermission() { function testImportFromFileWithoutReadPermission() {
$this->assertException("fileUnreadable", "Conf"); $this->assertException("fileUnreadable", "Conf");
$conf = new Conf(self::$path."confUnreadable"); $conf = new Conf(self::$path."confUnreadable");
} }
/** /**
* @depends testImportFromFile * @depends testImportFromFile
*/ */
function testImportFromFileWhichIsNotAnArray() { function testImportFromFileWhichIsNotAnArray() {
$this->assertException("fileCorrupt", "Conf"); $this->assertException("fileCorrupt", "Conf");
$conf = new Conf(self::$path."confNotArray"); $conf = new Conf(self::$path."confNotArray");
} }
/** /**
* @depends testImportFromFile * @depends testImportFromFile
*/ */
function testImportFromFileWhichIsNotPhp() { function testImportFromFileWhichIsNotPhp() {
$this->assertException("fileCorrupt", "Conf"); $this->assertException("fileCorrupt", "Conf");
// this should not print the output of the non-PHP file // this should not print the output of the non-PHP file
$conf = new Conf(self::$path."confNotPHP"); $conf = new Conf(self::$path."confNotPHP");
} }
/** /**
* @depends testImportFromFile * @depends testImportFromFile
*/ */
function testImportFromCorruptFile() { function testImportFromCorruptFile() {
$this->assertException("fileCorrupt", "Conf"); $this->assertException("fileCorrupt", "Conf");
$conf = new Conf(self::$path."confCorrupt"); $conf = new Conf(self::$path."confCorrupt");
} }
} }

88
tests/TestException.php

@ -4,67 +4,67 @@ namespace JKingWeb\NewsSync;
class TestException extends \PHPUnit\Framework\TestCase { class TestException extends \PHPUnit\Framework\TestCase {
use Test\Tools; use Test\Tools;
static function setUpBeforeClass() { static function setUpBeforeClass() {
Lang::set(""); Lang::set("");
} }
static function tearDownAfterClass() { static function tearDownAfterClass() {
Lang::set(Lang::DEFAULT); Lang::set(Lang::DEFAULT);
} }
function testBaseClass() { function testBaseClass() {
$this->assertException("unknown"); $this->assertException("unknown");
throw new Exception("unknown"); throw new Exception("unknown");
} }
/** /**
* @depends testBaseClass * @depends testBaseClass
*/ */
function testBaseClassWithoutMessage() { function testBaseClassWithoutMessage() {
$this->assertException("unknown"); $this->assertException("unknown");
throw new Exception(); throw new Exception();
} }
/** /**
* @depends testBaseClass * @depends testBaseClass
*/ */
function testDerivedClass() { function testDerivedClass() {
$this->assertException("fileMissing", "Lang"); $this->assertException("fileMissing", "Lang");
throw new Lang\Exception("fileMissing"); throw new Lang\Exception("fileMissing");
} }
/** /**
* @depends testDerivedClass * @depends testDerivedClass
*/ */
function testDerivedClassWithMessageParameters() { function testDerivedClassWithMessageParameters() {
$this->assertException("fileMissing", "Lang"); $this->assertException("fileMissing", "Lang");
throw new Lang\Exception("fileMissing", "en"); throw new Lang\Exception("fileMissing", "en");
} }
/** /**
* @depends testBaseClass * @depends testBaseClass
*/ */
function testBaseClassWithUnknownCode() { function testBaseClassWithUnknownCode() {
$this->assertException("uncoded"); $this->assertException("uncoded");
throw new Exception("testThisExceptionMessageDoesNotExist"); throw new Exception("testThisExceptionMessageDoesNotExist");
} }
/** /**
* @depends testBaseClass * @depends testBaseClass
*/ */
function testBaseClassWithMissingMessage() { function testBaseClassWithMissingMessage() {
$this->assertException("stringMissing", "Lang"); $this->assertException("stringMissing", "Lang");
throw new Exception("invalid"); throw new Exception("invalid");
} }
/** /**
* @depends testBaseClassWithUnknownCode * @depends testBaseClassWithUnknownCode
*/ */
function testDerivedClassWithMissingMessage() { function testDerivedClassWithMissingMessage() {
$this->assertException("uncoded"); $this->assertException("uncoded");
throw new Lang\Exception("testThisExceptionMessageDoesNotExist"); throw new Lang\Exception("testThisExceptionMessageDoesNotExist");
} }
} }

82
tests/TestLang.php

@ -5,58 +5,58 @@ use \org\bovigo\vfs\vfsStream;
class TestLang extends \PHPUnit\Framework\TestCase { class TestLang extends \PHPUnit\Framework\TestCase {
use Test\Tools, Test\Lang\Setup; use Test\Tools, Test\Lang\Setup;
static $vfs; static $vfs;
static $path; static $path;
static $files; static $files;
static $defaultPath; static $defaultPath;
function testListLanguages() { function testListLanguages() {
$this->assertCount(sizeof(self::$files), Lang::list("en")); $this->assertCount(sizeof(self::$files), Lang::list("en"));
} }
/** /**
* @depends testListLanguages * @depends testListLanguages
*/ */
function testSetLanguage() { function testSetLanguage() {
$this->assertEquals("en", Lang::set("en")); $this->assertEquals("en", Lang::set("en"));
$this->assertEquals("en_ca", Lang::set("en_ca")); $this->assertEquals("en_ca", Lang::set("en_ca"));
$this->assertEquals("de", Lang::set("de_ch")); $this->assertEquals("de", Lang::set("de_ch"));
$this->assertEquals("en", Lang::set("en_gb_hixie")); $this->assertEquals("en", Lang::set("en_gb_hixie"));
$this->assertEquals("en_ca", Lang::set("en_ca_jking")); $this->assertEquals("en_ca", Lang::set("en_ca_jking"));
$this->assertEquals("en", Lang::set("es")); $this->assertEquals("en", Lang::set("es"));
$this->assertEquals("", Lang::set("")); $this->assertEquals("", Lang::set(""));
} }
/** /**
* @depends testSetLanguage * @depends testSetLanguage
*/ */
function testLoadInternalStrings() { function testLoadInternalStrings() {
$this->assertEquals("", Lang::set("", true)); $this->assertEquals("", Lang::set("", true));
$this->assertCount(sizeof(Lang::REQUIRED), Lang::dump()); $this->assertCount(sizeof(Lang::REQUIRED), Lang::dump());
} }
/** /**
* @depends testLoadInternalStrings * @depends testLoadInternalStrings
*/ */
function testLoadDefaultLanguage() { function testLoadDefaultLanguage() {
$this->assertEquals(Lang::DEFAULT, Lang::set(Lang::DEFAULT, true)); $this->assertEquals(Lang::DEFAULT, Lang::set(Lang::DEFAULT, true));
$str = Lang::dump(); $str = Lang::dump();
$this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str); $this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str);
$this->assertArrayHasKey('Test.presentText', $str); $this->assertArrayHasKey('Test.presentText', $str);
} }
/** /**
* @depends testLoadDefaultLanguage * @depends testLoadDefaultLanguage
*/ */
function testLoadSupplementaryLanguage() { function testLoadSupplementaryLanguage() {
Lang::set(Lang::DEFAULT, true); Lang::set(Lang::DEFAULT, true);
$this->assertEquals("ja", Lang::set("ja", true)); $this->assertEquals("ja", Lang::set("ja", true));
$str = Lang::dump(); $str = Lang::dump();
$this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str); $this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str);
$this->assertArrayHasKey('Test.presentText', $str); $this->assertArrayHasKey('Test.presentText', $str);
$this->assertArrayHasKey('Test.absentText', $str); $this->assertArrayHasKey('Test.absentText', $str);
} }
} }

94
tests/TestLangErrors.php

@ -5,51 +5,51 @@ use \org\bovigo\vfs\vfsStream;
class TestLangErrors extends \PHPUnit\Framework\TestCase { class TestLangErrors extends \PHPUnit\Framework\TestCase {
use Test\Tools, Test\Lang\Setup; use Test\Tools, Test\Lang\Setup;
static $vfs; static $vfs;
static $path; static $path;
static $files; static $files;
static $defaultPath; static $defaultPath;
function setUp() { function setUp() {
Lang::set("", true); Lang::set("", true);
} }
function testLoadEmptyFile() { function testLoadEmptyFile() {
$this->assertException("fileCorrupt", "Lang"); $this->assertException("fileCorrupt", "Lang");
Lang::set("fr_ca", true); Lang::set("fr_ca", true);
} }
function testLoadFileWhichDoesNotReturnAnArray() { function testLoadFileWhichDoesNotReturnAnArray() {
$this->assertException("fileCorrupt", "Lang"); $this->assertException("fileCorrupt", "Lang");
Lang::set("it", true); Lang::set("it", true);
} }
function testLoadFileWhichIsNotPhp() { function testLoadFileWhichIsNotPhp() {
$this->assertException("fileCorrupt", "Lang"); $this->assertException("fileCorrupt", "Lang");
Lang::set("ko", true); Lang::set("ko", true);
} }
function testLoadFileWhichIsCorrupt() { function testLoadFileWhichIsCorrupt() {
$this->assertException("fileCorrupt", "Lang"); $this->assertException("fileCorrupt", "Lang");
Lang::set("zh", true); Lang::set("zh", true);
} }
function testLoadFileWithooutReadPermission() { function testLoadFileWithooutReadPermission() {
$this->assertException("fileUnreadable", "Lang"); $this->assertException("fileUnreadable", "Lang");
Lang::set("ru", true); Lang::set("ru", true);
} }
function testLoadSubtagOfMissingLanguage() { function testLoadSubtagOfMissingLanguage() {
$this->assertException("fileMissing", "Lang"); $this->assertException("fileMissing", "Lang");
Lang::set("pt_br", true); Lang::set("pt_br", true);
} }
function testLoadMissingDefaultLanguage() { function testLoadMissingDefaultLanguage() {
// this should be the last test of the series // this should be the last test of the series
unlink(self::$path.Lang::DEFAULT.".php"); unlink(self::$path.Lang::DEFAULT.".php");
$this->assertException("defaultFileMissing", "Lang"); $this->assertException("defaultFileMissing", "Lang");
Lang::set("fr", true); Lang::set("fr", true);
} }
} }

76
tests/lib/Lang/Setup.php

@ -6,43 +6,43 @@ use \org\bovigo\vfs\vfsStream, \JKingWeb\NewsSync\Lang;
trait Setup { trait Setup {
static function setUpBeforeClass() { static function setUpBeforeClass() {
// this is required to keep from having exceptions in Lang::msg() in turn calling Lang::msg() and looping // this is required to keep from having exceptions in Lang::msg() in turn calling Lang::msg() and looping
\JKingWeb\NewsSync\Lang\Exception::$test = true; \JKingWeb\NewsSync\Lang\Exception::$test = true;
// test files // test files
self::$files = [ self::$files = [
'en.php' => '<?php return ["Test.presentText" => "and the Philosopher\'s Stone"];', 'en.php' => '<?php return ["Test.presentText" => "and the Philosopher\'s Stone"];',
'en_ca.php' => '<?php return ["Test.presentText" => "{0} and {1}"];', 'en_ca.php' => '<?php return ["Test.presentText" => "{0} and {1}"];',
'en_us.php' => '<?php return ["Test.presentText" => "and the Sorcerer\'s Stone"];', 'en_us.php' => '<?php return ["Test.presentText" => "and the Sorcerer\'s Stone"];',
'fr.php' => '<?php return ["Test.presentText" => "à l\'école des sorciers"];', 'fr.php' => '<?php return ["Test.presentText" => "à l\'école des sorciers"];',
'ja.php' => '<?php return ["Test.absentText" => "賢者の石"];', 'ja.php' => '<?php return ["Test.absentText" => "賢者の石"];',
'de.php' => '<?php return ["Test.presentText" => "und der Stein der Weisen"];', 'de.php' => '<?php return ["Test.presentText" => "und der Stein der Weisen"];',
'pt_br.php' => '<?php return ["Test.presentText" => "e a Pedra Filosofal"];', 'pt_br.php' => '<?php return ["Test.presentText" => "e a Pedra Filosofal"];',
'vi.php' => '<?php return [];', 'vi.php' => '<?php return [];',
// corrupt files // corrupt files
'it.php' => '<?php return 0;', 'it.php' => '<?php return 0;',
'zh.php' => '<?php return 0', 'zh.php' => '<?php return 0',
'ko.php' => 'DEAD BEEF', 'ko.php' => 'DEAD BEEF',
'fr_ca.php' => '', 'fr_ca.php' => '',
// unreadable file // unreadable file
'ru.php' => '', 'ru.php' => '',
]; ];
self::$vfs = vfsStream::setup("langtest", 0777, self::$files); self::$vfs = vfsStream::setup("langtest", 0777, self::$files);
self::$path = self::$vfs->url()."/"; self::$path = self::$vfs->url()."/";
// set up a file without read access // set up a file without read access
chmod(self::$path."ru.php", 0000); chmod(self::$path."ru.php", 0000);
// make the Lang class use the vfs files // make the Lang class use the vfs files
self::$defaultPath = Lang::$path; self::$defaultPath = Lang::$path;
Lang::$path = self::$path; Lang::$path = self::$path;
} }
static function tearDownAfterClass() { static function tearDownAfterClass() {
\JKingWeb\NewsSync\Lang\Exception::$test = false; \JKingWeb\NewsSync\Lang\Exception::$test = false;
Lang::$path = self::$defaultPath; Lang::$path = self::$defaultPath;
self::$path = null; self::$path = null;
self::$vfs = null; self::$vfs = null;
self::$files = null; self::$files = null;
Lang::set("", true); Lang::set("", true);
Lang::set(Lang::DEFAULT); Lang::set(Lang::DEFAULT);
} }
} }

22
tests/lib/Tools.php

@ -4,15 +4,15 @@ namespace JKingWeb\NewsSync\Test;
use \JKingWeb\NewsSync\Exception; use \JKingWeb\NewsSync\Exception;
trait Tools { trait Tools {
function assertException(string $msg, string $prefix = "", string $type = "Exception") { function assertException(string $msg, string $prefix = "", string $type = "Exception") {
$class = \JKingWeb\NewsSync\NS_BASE . ($prefix !== "" ? str_replace("/", "\\", $prefix) . "\\" : "") . $type; $class = \JKingWeb\NewsSync\NS_BASE . ($prefix !== "" ? str_replace("/", "\\", $prefix) . "\\" : "") . $type;
$msgID = ($prefix !== "" ? $prefix . "/" : "") . $type. ".$msg"; $msgID = ($prefix !== "" ? $prefix . "/" : "") . $type. ".$msg";
if(array_key_exists($msgID, Exception::CODES)) { if(array_key_exists($msgID, Exception::CODES)) {
$code = Exception::CODES[$msgID]; $code = Exception::CODES[$msgID];
} else { } else {
$code = 0; $code = 0;
} }
$this->expectException($class); $this->expectException($class);
$this->expectExceptionCode($code); $this->expectExceptionCode($code);
} }
} }

28
tests/phpunit.xml

@ -1,23 +1,23 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<phpunit <phpunit
colors="true" colors="true"
bootstrap="../bootstrap.php" bootstrap="../bootstrap.php"
convertErrorsToExceptions="true" convertErrorsToExceptions="true"
convertNoticesToExceptions="true" convertNoticesToExceptions="true"
convertWarningsToExceptions="true" convertWarningsToExceptions="true"
beStrictAboutTestsThatDoNotTestAnything="true" beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutOutputDuringTests="true" beStrictAboutOutputDuringTests="true"
beStrictAboutTestSize="true" beStrictAboutTestSize="true"
stopOnError="true"> stopOnError="true">
<testsuite name="Localization and exceptions"> <testsuite name="Localization and exceptions">
<file>TestLang.php</file> <file>TestLang.php</file>
<file>TestLangComplex.php</file> <file>TestLangComplex.php</file>
<file>TestException.php</file> <file>TestException.php</file>
<file>TestLangErrors.php</file> <file>TestLangErrors.php</file>
</testsuite> </testsuite>
<testsuite name="Configuration loading and saving"> <testsuite name="Configuration loading and saving">
<file>TestConf.php</file> <file>TestConf.php</file>
</testsuite> </testsuite>
</phpunit> </phpunit>

114
tests/testLangComplex.php

@ -5,82 +5,82 @@ use \org\bovigo\vfs\vfsStream;
class TestLangComplex extends \PHPUnit\Framework\TestCase { class TestLangComplex extends \PHPUnit\Framework\TestCase {
use Test\Tools, Test\Lang\Setup; use Test\Tools, Test\Lang\Setup;
static $vfs; static $vfs;
static $path; static $path;
static $files; static $files;
static $defaultPath; static $defaultPath;
function setUp() { function setUp() {
Lang::set(Lang::DEFAULT, true); Lang::set(Lang::DEFAULT, true);
} }
function testLazyLoad() { function testLazyLoad() {
Lang::set("ja"); Lang::set("ja");
$this->assertArrayNotHasKey('Test.absentText', Lang::dump()); $this->assertArrayNotHasKey('Test.absentText', Lang::dump());
} }
function testLoadCascadeOfFiles() { function testLoadCascadeOfFiles() {
Lang::set("ja", true); Lang::set("ja", true);
$this->assertEquals("de", Lang::set("de", true)); $this->assertEquals("de", Lang::set("de", true));
$str = Lang::dump(); $str = Lang::dump();
$this->assertArrayNotHasKey('Test.absentText', $str); $this->assertArrayNotHasKey('Test.absentText', $str);
$this->assertEquals('und der Stein der Weisen', $str['Test.presentText']); $this->assertEquals('und der Stein der Weisen', $str['Test.presentText']);
} }
/** /**
* @depends testLoadCascadeOfFiles * @depends testLoadCascadeOfFiles
*/ */
function testLoadSubtag() { function testLoadSubtag() {
$this->assertEquals("en_ca", Lang::set("en_ca", true)); $this->assertEquals("en_ca", Lang::set("en_ca", true));
} }
function testFetchAMessage() { function testFetchAMessage() {
Lang::set("de", true); Lang::set("de", true);
$this->assertEquals('und der Stein der Weisen', Lang::msg('Test.presentText')); $this->assertEquals('und der Stein der Weisen', Lang::msg('Test.presentText'));
} }
/** /**
* @depends testFetchAMessage * @depends testFetchAMessage
*/ */
function testFetchAMessageWithSingleNumericParameter() { function testFetchAMessageWithSingleNumericParameter() {
Lang::set("en_ca", true); Lang::set("en_ca", true);
$this->assertEquals('Default language file "en" missing', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing', Lang::DEFAULT)); $this->assertEquals('Default language file "en" missing', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing', Lang::DEFAULT));
} }
/** /**
* @depends testFetchAMessage * @depends testFetchAMessage
*/ */
function testFetchAMessageWithMultipleNumericParameters() { function testFetchAMessageWithMultipleNumericParameters() {
Lang::set("en_ca", true); Lang::set("en_ca", true);
$this->assertEquals('Happy Rotter and the Philosopher\'s Stone', Lang::msg('Test.presentText', ['Happy Rotter', 'the Philosopher\'s Stone'])); $this->assertEquals('Happy Rotter and the Philosopher\'s Stone', Lang::msg('Test.presentText', ['Happy Rotter', 'the Philosopher\'s Stone']));
} }
/** /**
* @depends testFetchAMessage * @depends testFetchAMessage
*/ */
function testFetchAMessageWithNamedParameters() { 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'])); $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 * @depends testFetchAMessage
*/ */
function testReloadDefaultStrings() { function testReloadDefaultStrings() {
Lang::set("de", true); Lang::set("de", true);
Lang::set("en", true); Lang::set("en", true);
$this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText')); $this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText'));
} }
/** /**
* @depends testFetchAMessage * @depends testFetchAMessage
*/ */
function testReloadGeneralTagAfterSubtag() { function testReloadGeneralTagAfterSubtag() {
Lang::set("en", true); Lang::set("en", true);
Lang::set("en_us", true); Lang::set("en_us", true);
$this->assertEquals('and the Sorcerer\'s Stone', Lang::msg('Test.presentText')); $this->assertEquals('and the Sorcerer\'s Stone', Lang::msg('Test.presentText'));
Lang::set("en", true); Lang::set("en", true);
$this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText')); $this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText'));
} }
} }
Loading…
Cancel
Save