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
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain

74
composer.json

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

104
lib/AbstractException.php

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

104
lib/Conf.php

@ -3,64 +3,64 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync;
class Conf {
public $lang = "en";
public $dbDriver = Db\DriverSQLite3::class;
public $dbSQLite3File = BASE."newssync.db";
public $dbSQLite3Key = "";
public $dbSQLite3AutoUpd = true;
public $dbPostgreSQLHost = "localhost";
public $dbPostgreSQLUser = "newssync";
public $dbPostgreSQLPass = "";
public $dbPostgreSQLPort = 5432;
public $dbPostgreSQLDb = "newssync";
public $dbPostgreSQLSchema = "";
public $dbPostgreSQLAutoUpd = false;
public $dbMySQLHost = "localhost";
public $dbMySQLUser = "newssync";
public $dbMySQLPass = "";
public $dbMySQLPort = 3306;
public $dbMySQLDb = "newssync";
public $dbMySQLAutoUpd = false;
public $lang = "en";
public $userDriver = User\DriverInternal::class;
public $userAuthPreferHTTP = false;
public $userComposeNames = true;
public $dbDriver = Db\DriverSQLite3::class;
public $dbSQLite3File = BASE."newssync.db";
public $dbSQLite3Key = "";
public $dbSQLite3AutoUpd = true;
public $dbPostgreSQLHost = "localhost";
public $dbPostgreSQLUser = "newssync";
public $dbPostgreSQLPass = "";
public $dbPostgreSQLPort = 5432;
public $dbPostgreSQLDb = "newssync";
public $dbPostgreSQLSchema = "";
public $dbPostgreSQLAutoUpd = false;
public $dbMySQLHost = "localhost";
public $dbMySQLUser = "newssync";
public $dbMySQLPass = "";
public $dbMySQLPort = 3306;
public $dbMySQLDb = "newssync";
public $dbMySQLAutoUpd = false;
public $simplepieCache = BASE.".cache";
public $userDriver = User\DriverInternal::class;
public $userAuthPreferHTTP = false;
public $userComposeNames = true;
public $simplepieCache = BASE.".cache";
public function __construct(string $import_file = "") {
if($import_file != "") $this->importFile($import_file);
}
public function importFile(string $file): self {
if(!file_exists($file)) throw new Conf\Exception("fileMissing", $file);
if(!is_readable($file)) throw new Conf\Exception("fileUnreadable", $file);
try {
ob_start();
$arr = (@include $file);
} catch(\Throwable $e) {
$arr = null;
} finally {
ob_end_clean();
}
if(!is_array($arr)) throw new Conf\Exception("fileCorrupt", $file);
return $this->import($arr);
}
public function __construct(string $import_file = "") {
if($import_file != "") $this->importFile($import_file);
}
public function import(array $arr): self {
foreach($arr as $key => $value) {
$this->$key = $value;
}
return $this;
}
public function importFile(string $file): self {
if(!file_exists($file)) throw new Conf\Exception("fileMissing", $file);
if(!is_readable($file)) throw new Conf\Exception("fileUnreadable", $file);
try {
ob_start();
$arr = (@include $file);
} catch(\Throwable $e) {
$arr = null;
} finally {
ob_end_clean();
}
if(!is_array($arr)) throw new Conf\Exception("fileCorrupt", $file);
return $this->import($arr);
}
public function export(string $file = ""): string {
// TODO
}
public function import(array $arr): self {
foreach($arr as $key => $value) {
$this->$key = $value;
}
return $this;
}
public function __toString(): string {
return $this->export();
}
public function export(string $file = ""): string {
// TODO
}
public function __toString(): string {
return $this->export();
}
}

504
lib/Database.php

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

118
lib/Db/Common.php

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

18
lib/Db/CommonPDO.php

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

82
lib/Db/CommonSQLite3.php

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

50
lib/Db/Driver.php

@ -2,29 +2,29 @@
declare(strict_types=1);
namespace JKingWeb\NewsSync\Db;
interface Driver {
// returns an instance of a class implementing this interface. Implemented as a static method so that classes may return their PDO equivalents instead of themselves
static function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver;
// returns a human-friendly name for the driver (for display in installer, for example)
static function driverName(): string;
// returns the version of the scheme of the opened database; if uninitialized should return 0
function schemaVersion(): int;
// begin a real or synthetic transactions, with real or synthetic nesting
function begin(): bool;
// commit either the latest or all pending nested transactions; use of this method should assume a partial commit is a no-op
function commit(bool $all = false): bool;
// rollback either the latest or all pending nested transactions; use of this method should assume a partial rollback will not work
function rollback(bool $all = false): bool;
// attempt to advise other processes that they should not attempt to access the database; used during live upgrades
function lock(): bool;
function unlock(): bool;
function isLocked(): bool;
// attempt to perform an in-place upgrade of the database schema; this may be a no-op which always throws an exception
function update(int $to): bool;
// execute one or more unsanitized SQL queries and return an indication of success
function exec(string $query): bool;
// perform a single unsanitized query and return a result set
function query(string $query): Result;
// ready a prepared statement for later execution
function prepare(string $query, string ...$paramType): Statement;
interface Driver {
// returns an instance of a class implementing this interface. Implemented as a static method so that classes may return their PDO equivalents instead of themselves
static function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver;
// returns a human-friendly name for the driver (for display in installer, for example)
static function driverName(): string;
// returns the version of the scheme of the opened database; if uninitialized should return 0
function schemaVersion(): int;
// begin a real or synthetic transactions, with real or synthetic nesting
function begin(): bool;
// commit either the latest or all pending nested transactions; use of this method should assume a partial commit is a no-op
function commit(bool $all = false): bool;
// rollback either the latest or all pending nested transactions; use of this method should assume a partial rollback will not work
function rollback(bool $all = false): bool;
// attempt to advise other processes that they should not attempt to access the database; used during live upgrades
function lock(): bool;
function unlock(): bool;
function isLocked(): bool;
// attempt to perform an in-place upgrade of the database schema; this may be a no-op which always throws an exception
function update(int $to): bool;
// execute one or more unsanitized SQL queries and return an indication of success
function exec(string $query): bool;
// perform a single unsanitized query and return a result set
function query(string $query): Result;
// ready a prepared statement for later execution
function prepare(string $query, string ...$paramType): Statement;
}

98
lib/Db/DriverSQLite3.php

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

40
lib/Db/DriverSQLite3PDO.php

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

16
lib/Db/Result.php

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

96
lib/Db/ResultSQLite3.php

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

6
lib/Db/Statement.php

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

124
lib/Db/StatementSQLite3.php

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

6
lib/ExceptionFatal.php

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

294
lib/Lang.php

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

34
lib/Lang/Exception.php

@ -3,22 +3,22 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\Lang;
class Exception extends \JKingWeb\NewsSync\AbstractException {
static $test = false; // used during PHPUnit testing only
static $test = false; // used during PHPUnit testing only
function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
if(!self::$test) {
parent::__construct($msgID, $vars, $e);
} else {
$codeID = "Lang/Exception.$msgID";
if(!array_key_exists($codeID,self::CODES)) {
$code = -1;
$msg = "Exception.".str_replace("\\","/",parent::class).".uncoded";
$vars = $msgID;
} else {
$code = self::CODES[$codeID];
$msg = "Exception.".str_replace("\\","/",__CLASS__).".$msgID";
}
\Exception::__construct($msg, $code, $e);
}
}
function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
if(!self::$test) {
parent::__construct($msgID, $vars, $e);
} else {
$codeID = "Lang/Exception.$msgID";
if(!array_key_exists($codeID,self::CODES)) {
$code = -1;
$msg = "Exception.".str_replace("\\","/",parent::class).".uncoded";
$vars = $msgID;
} else {
$code = self::CODES[$codeID];
$msg = "Exception.".str_replace("\\","/",__CLASS__).".$msgID";
}
\Exception::__construct($msg, $code, $e);
}
}
}

18
lib/RuntimeData.php

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

474
lib/User.php

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

44
lib/User/Driver.php

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

68
lib/User/DriverInternal.php

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

132
lib/User/InternalFunctions.php

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

92
locale/en.php

@ -1,52 +1,52 @@
<?php
return [
'Exception.JKingWeb/NewsSync/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in Exception.php',
//this should not usually be encountered
'Exception.JKingWeb/NewsSync/Exception.unknown' => 'An unknown error has occurred',
'Exception.JKingWeb/NewsSync/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in Exception.php',
//this should not usually be encountered
'Exception.JKingWeb/NewsSync/Exception.unknown' => 'An unknown error has occurred',
'Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing' => 'Default language file "{0}" missing',
'Exception.JKingWeb/NewsSync/Lang/Exception.fileMissing' => 'Language file "{0}" is not available',
'Exception.JKingWeb/NewsSync/Lang/Exception.fileUnreadable' => 'Insufficient permissions to read language file "{0}"',
'Exception.JKingWeb/NewsSync/Lang/Exception.fileCorrupt' => 'Language file "{0}" is corrupt or does not conform to expected format',
'Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing' => 'Message string "{msgID}" missing from all loaded language files ({fileList})',
'Exception.JKingWeb/NewsSync/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})',
'Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing' => 'Default language file "{0}" missing',
'Exception.JKingWeb/NewsSync/Lang/Exception.fileMissing' => 'Language file "{0}" is not available',
'Exception.JKingWeb/NewsSync/Lang/Exception.fileUnreadable' => 'Insufficient permissions to read language file "{0}"',
'Exception.JKingWeb/NewsSync/Lang/Exception.fileCorrupt' => 'Language file "{0}" is corrupt or does not conform to expected format',
'Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing' => 'Message string "{msgID}" missing from all loaded language files ({fileList})',
'Exception.JKingWeb/NewsSync/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})',
'Exception.JKingWeb/NewsSync/Conf/Exception.fileMissing' => 'Configuration file "{0}" does not exist',
'Exception.JKingWeb/NewsSync/Conf/Exception.fileUnreadable' => 'Insufficient permissions to read configuration file "{0}"',
'Exception.JKingWeb/NewsSync/Conf/Exception.fileUncreatable' => 'Insufficient permissions to write new configuration file "{0}"',
'Exception.JKingWeb/NewsSync/Conf/Exception.fileUnwritable' => 'Insufficient permissions to overwrite configuration file "{0}"',
'Exception.JKingWeb/NewsSync/Conf/Exception.fileCorrupt' => 'Configuration file "{0}" is corrupt or does not conform to expected format',
'Exception.JKingWeb/NewsSync/Db/Exception.extMissing' => 'Required PHP extension for driver "{0}" not installed',
'Exception.JKingWeb/NewsSync/Db/Exception.fileMissing' => 'Database file "{0}" does not exist',
'Exception.JKingWeb/NewsSync/Db/Exception.fileUnreadable' => 'Insufficient permissions to open database file "{0}" for reading',
'Exception.JKingWeb/NewsSync/Db/Exception.fileUnwritable' => 'Insufficient permissions to open database file "{0}" for writing',
'Exception.JKingWeb/NewsSync/Db/Exception.fileUnusable' => 'Insufficient permissions to open database file "{0}" for reading or writing',
'Exception.JKingWeb/NewsSync/Db/Exception.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"',
'Exception.JKingWeb/NewsSync/Db/Exception.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database',
'Exception.JKingWeb/NewsSync/Db/Update/Exception.manual' =>
'{from_version, select,
0 {{driver_name} database is configured for manual updates and is not initialized; please populate the database with the base schema}
other {{driver_name} database is configured for manual updates; please update from schema version {current} to version {target}}
}',
'Exception.JKingWeb/NewsSync/Db/Update/Exception.manualOnly' =>
'{from_version, select,
0 {{driver_name} database must be updated manually and is not initialized; please populate the database with the base schema}
other {{driver_name} database must be updated manually; please update from schema version {current} to version {target}}
}',
'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileMissing' => 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} not being available',
'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileUnreadable' => 'Automatic updating of the {driver_name} database failed due to insufficient permissions to read instructions for updating from version {current}',
'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileUnusable' => 'Automatic updating of the {driver_name} database failed due to an error reading instructions for updating from version {current}',
'Exception.JKingWeb/NewsSync/Db/Update/Exception.tooNew' =>
'{difference, select,
0 {Automatic updating of the {driver_name} database failed because it is already up to date with the requested version, {target}}
other {Automatic updating of the {driver_name} database failed because its version, {current}, is newer than the requested version, {target}}
}',
'Exception.JKingWeb/NewsSync/Conf/Exception.fileMissing' => 'Configuration file "{0}" does not exist',
'Exception.JKingWeb/NewsSync/Conf/Exception.fileUnreadable' => 'Insufficient permissions to read configuration file "{0}"',
'Exception.JKingWeb/NewsSync/Conf/Exception.fileUncreatable' => 'Insufficient permissions to write new configuration file "{0}"',
'Exception.JKingWeb/NewsSync/Conf/Exception.fileUnwritable' => 'Insufficient permissions to overwrite configuration file "{0}"',
'Exception.JKingWeb/NewsSync/Conf/Exception.fileCorrupt' => 'Configuration file "{0}" is corrupt or does not conform to expected format',
'Exception.JKingWeb/NewsSync/User/Exception.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists',
'Exception.JKingWeb/NewsSync/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist',
'Exception.JKingWeb/NewsSync/User/Exception.authMissing' => 'Please log in to proceed',
'Exception.JKingWeb/NewsSync/User/Exception.authFailed' => 'Authentication failed',
'Exception.JKingWeb/NewsSync/User/ExceptionAuthz.notAuthorized' => 'Authenticated user is not authorized to perform the action "{action}" on behalf of {user}',
'Exception.JKingWeb/NewsSync/Db/Exception.extMissing' => 'Required PHP extension for driver "{0}" not installed',
'Exception.JKingWeb/NewsSync/Db/Exception.fileMissing' => 'Database file "{0}" does not exist',
'Exception.JKingWeb/NewsSync/Db/Exception.fileUnreadable' => 'Insufficient permissions to open database file "{0}" for reading',
'Exception.JKingWeb/NewsSync/Db/Exception.fileUnwritable' => 'Insufficient permissions to open database file "{0}" for writing',
'Exception.JKingWeb/NewsSync/Db/Exception.fileUnusable' => 'Insufficient permissions to open database file "{0}" for reading or writing',
'Exception.JKingWeb/NewsSync/Db/Exception.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"',
'Exception.JKingWeb/NewsSync/Db/Exception.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database',
'Exception.JKingWeb/NewsSync/Db/Update/Exception.manual' =>
'{from_version, select,
0 {{driver_name} database is configured for manual updates and is not initialized; please populate the database with the base schema}
other {{driver_name} database is configured for manual updates; please update from schema version {current} to version {target}}
}',
'Exception.JKingWeb/NewsSync/Db/Update/Exception.manualOnly' =>
'{from_version, select,
0 {{driver_name} database must be updated manually and is not initialized; please populate the database with the base schema}
other {{driver_name} database must be updated manually; please update from schema version {current} to version {target}}
}',
'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileMissing' => 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} not being available',
'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileUnreadable' => 'Automatic updating of the {driver_name} database failed due to insufficient permissions to read instructions for updating from version {current}',
'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileUnusable' => 'Automatic updating of the {driver_name} database failed due to an error reading instructions for updating from version {current}',
'Exception.JKingWeb/NewsSync/Db/Update/Exception.tooNew' =>
'{difference, select,
0 {Automatic updating of the {driver_name} database failed because it is already up to date with the requested version, {target}}
other {Automatic updating of the {driver_name} database failed because its version, {current}, is newer than the requested version, {target}}
}',
'Exception.JKingWeb/NewsSync/User/Exception.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists',
'Exception.JKingWeb/NewsSync/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist',
'Exception.JKingWeb/NewsSync/User/Exception.authMissing' => 'Please log in to proceed',
'Exception.JKingWeb/NewsSync/User/Exception.authFailed' => 'Authentication failed',
'Exception.JKingWeb/NewsSync/User/ExceptionAuthz.notAuthorized' => 'Authenticated user is not authorized to perform the action "{action}" on behalf of {user}',
];

138
sql/SQLite3/0.sql

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

142
tests/TestConf.php

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

88
tests/TestException.php

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

82
tests/TestLang.php

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

94
tests/TestLangErrors.php

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

76
tests/lib/Lang/Setup.php

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

22
tests/lib/Tools.php

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

28
tests/phpunit.xml

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

114
tests/testLangComplex.php

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