Browse Source

Eliminated passing of RuntimeData instances

- RuntimeData has now been replaced by a single static Data class
- The Data class has a load() method which fills the same role as the constructor of RuntimeData
- The static Lang class is now an instantiable class and is a member of Data
- All tests have been adjusted and pass
- The Exception tests no longer require convoluted workarounds: a simple mock  for Data::$l suffices; Lang tests also use a mock to prevent loops now instead of using a workaround
microsub
J. King 7 years ago
parent
commit
f902346b6c
  1. 2
      autoload.php
  2. 3
      lib/AbstractException.php
  3. 18
      lib/Data.php
  4. 43
      lib/Database.php
  5. 2
      lib/Db/Driver.php
  6. 16
      lib/Db/SQLite3/Driver.php
  7. 116
      lib/Lang.php
  8. 18
      lib/Lang/Exception.php
  9. 5
      lib/REST.php
  10. 2
      lib/REST/AbstractHandler.php
  11. 2
      lib/REST/Handler.php
  12. 3
      lib/REST/NextCloudNews/Versions.php
  13. 16
      lib/RuntimeData.php
  14. 56
      lib/User.php
  15. 4
      lib/User/Driver.php
  16. 8
      lib/User/Internal/Driver.php
  17. 8
      lib/User/Internal/InternalFunctions.php
  18. 8
      tests/Conf/TestConf.php
  19. 34
      tests/Db/SQLite3/TestDbDriverSQLite3.php
  20. 10
      tests/Db/SQLite3/TestDbUpdateSQLite3.php
  21. 20
      tests/Exception/TestException.php
  22. 39
      tests/Lang/TestLang.php
  23. 36
      tests/Lang/TestLangErrors.php
  24. 67
      tests/Lang/testLangComplex.php
  25. 9
      tests/REST/NextCloudNews/TestNCNVersionDiscovery.php
  26. 119
      tests/User/TestAuthorization.php
  27. 15
      tests/User/TestUserInternalDriver.php
  28. 15
      tests/User/TestUserMockExternal.php
  29. 14
      tests/User/TestUserMockInternal.php
  30. 15
      tests/lib/Db/Tools.php
  31. 40
      tests/lib/Lang/Setup.php
  32. 13
      tests/lib/RuntimeData.php
  33. 15
      tests/lib/Tools.php
  34. 107
      tests/lib/User/CommonTests.php
  35. 30
      tests/lib/User/Database.php
  36. 13
      tests/lib/User/DriverExternalMock.php
  37. 8
      tests/lib/User/DriverInternalMock.php
  38. 1
      tests/lib/User/DriverSkeleton.php
  39. 7
      tests/phpunit.xml
  40. 19
      tests/test.php

2
autoload.php

@ -1,3 +1,3 @@
<?php <?php
require_once __DIR__.DIRECTORY_SEPARATOR."bootstrap.php"; require_once __DIR__.DIRECTORY_SEPARATOR."bootstrap.php";
$data = new RuntimeData(new Conf()); Data::load(new Conf());

3
lib/AbstractException.php

@ -6,7 +6,6 @@ abstract class AbstractException extends \Exception {
const CODES = [ const CODES = [
"Exception.uncoded" => -1, "Exception.uncoded" => -1,
"Exception.invalid" => 1, // this exception MUST NOT have a message string defined
"Exception.unknown" => 10000, "Exception.unknown" => 10000,
"Lang/Exception.defaultFileMissing" => 10101, "Lang/Exception.defaultFileMissing" => 10101,
"Lang/Exception.fileMissing" => 10102, "Lang/Exception.fileMissing" => 10102,
@ -79,7 +78,7 @@ abstract class AbstractException extends \Exception {
$code = self::CODES[$codeID]; $code = self::CODES[$codeID];
$msg = "Exception.".str_replace("\\", "/", $class).".$msgID"; $msg = "Exception.".str_replace("\\", "/", $class).".$msgID";
} }
$msg = Lang::msg($msg, $vars); $msg = Data::$l->msg($msg, $vars);
} }
parent::__construct($msg, $code, $e); parent::__construct($msg, $code, $e);
} }

18
lib/Data.php

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
class Data {
public static $l;
public static $conf;
public static $db;
public static $user;
static function load(Conf $conf) {
static::$l = new Lang();
static::$conf = $conf;
static::$l->set($conf->lang);
static::$db = new Database();
static::$user = new User();
}
}

43
lib/Database.php

@ -18,10 +18,9 @@ class Database {
return (string) preg_filter("[^0-9a-zA-Z_\.]", "", $name); return (string) preg_filter("[^0-9a-zA-Z_\.]", "", $name);
} }
public function __construct(RuntimeData $data) { public function __construct() {
$this->data = $data; $this->driver = $driver = Data::$conf->dbDriver;
$this->driver = $driver = $data->conf->dbDriver; $this->db = new $driver(INSTALL);
$this->db = new $driver($data, INSTALL);
$ver = $this->db->schemaVersion(); $ver = $this->db->schemaVersion();
if(!INSTALL && $ver < self::SCHEMA_VERSION) { if(!INSTALL && $ver < self::SCHEMA_VERSION) {
$this->db->schemaUpdate(self::SCHEMA_VERSION); $this->db->schemaUpdate(self::SCHEMA_VERSION);
@ -166,14 +165,14 @@ class Database {
} }
public function userExists(string $user): bool { public function userExists(string $user): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
return (bool) $this->db->prepare("SELECT count(*) from arsse_users where id is ?", "str")->run($user)->getValue(); return (bool) $this->db->prepare("SELECT count(*) from arsse_users where id is ?", "str")->run($user)->getValue();
} }
public function userAdd(string $user, string $password = null): string { public function userAdd(string $user, string $password = null): string {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if($this->userExists($user)) throw new User\Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]); if($this->userExists($user)) throw new User\Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]);
if($password===null) $password = (new PassGen)->length($this->data->conf->userTempPasswordLength)->get(); if($password===null) $password = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
$hash = ""; $hash = "";
if(strlen($password) > 0) $hash = password_hash($password, \PASSWORD_DEFAULT); if(strlen($password) > 0) $hash = password_hash($password, \PASSWORD_DEFAULT);
$this->db->prepare("INSERT INTO arsse_users(id,password) values(?,?)", "str", "str")->runArray([$user,$hash]); $this->db->prepare("INSERT INTO arsse_users(id,password) values(?,?)", "str", "str")->runArray([$user,$hash]);
@ -181,33 +180,33 @@ class Database {
} }
public function userRemove(string $user): bool { public function userRemove(string $user): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if($this->db->prepare("DELETE from arsse_users where id is ?", "str")->run($user)->changes() < 1) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); if($this->db->prepare("DELETE from arsse_users where id is ?", "str")->run($user)->changes() < 1) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return true; return true;
} }
public function userList(string $domain = null): array { public function userList(string $domain = null): array {
if($domain !== null) { if($domain !== null) {
if(!$this->data->user->authorize("@".$domain, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]); if(!Data::$user->authorize("@".$domain, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
$domain = str_replace(["\\","%","_"],["\\\\", "\\%", "\\_"], $domain); $domain = str_replace(["\\","%","_"],["\\\\", "\\%", "\\_"], $domain);
$domain = "%@".$domain; $domain = "%@".$domain;
return $this->db->prepare("SELECT id from arsse_users where id like ?", "str")->run($domain)->getAll(); return $this->db->prepare("SELECT id from arsse_users where id like ?", "str")->run($domain)->getAll();
} else { } else {
if(!$this->data->user->authorize("", __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]); if(!Data::$user->authorize("", __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]);
return $this->db->prepare("SELECT id from arsse_users")->run()->getAll(); return $this->db->prepare("SELECT id from arsse_users")->run()->getAll();
} }
} }
public function userPasswordGet(string $user): string { public function userPasswordGet(string $user): string {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return (string) $this->db->prepare("SELECT password from arsse_users where id is ?", "str")->run($user)->getValue(); return (string) $this->db->prepare("SELECT password from arsse_users where id is ?", "str")->run($user)->getValue();
} }
public function userPasswordSet(string $user, string $password = null): string { public function userPasswordSet(string $user, string $password = null): string {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
if($password===null) $password = (new PassGen)->length($this->data->conf->userTempPasswordLength)->get(); if($password===null) $password = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
$hash = ""; $hash = "";
if(strlen($password > 0)) $hash = password_hash($password, \PASSWORD_DEFAULT); if(strlen($password > 0)) $hash = password_hash($password, \PASSWORD_DEFAULT);
$this->db->prepare("UPDATE arsse_users set password = ? where id is ?", "str", "str")->run($hash, $user); $this->db->prepare("UPDATE arsse_users set password = ? where id is ?", "str", "str")->run($hash, $user);
@ -215,14 +214,14 @@ class Database {
} }
public function userPropertiesGet(string $user): array { public function userPropertiesGet(string $user): array {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
$prop = $this->db->prepare("SELECT name,rights from arsse_users where id is ?", "str")->run($user)->getRow(); $prop = $this->db->prepare("SELECT name,rights from arsse_users where id is ?", "str")->run($user)->getRow();
if(!$prop) return []; if(!$prop) return [];
return $prop; return $prop;
} }
public function userPropertiesSet(string $user, array &$properties): array { public function userPropertiesSet(string $user, array &$properties): array {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
$valid = [ // FIXME: add future properties $valid = [ // FIXME: add future properties
"name" => "str", "name" => "str",
]; ];
@ -237,12 +236,12 @@ class Database {
} }
public function userRightsGet(string $user): int { public function userRightsGet(string $user): int {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
return (int) $this->db->prepare("SELECT rights from arsse_users where id is ?", "str")->run($user)->getValue(); return (int) $this->db->prepare("SELECT rights from arsse_users where id is ?", "str")->run($user)->getValue();
} }
public function userRightsSet(string $user, int $rights): bool { public function userRightsSet(string $user, int $rights): bool {
if(!$this->data->user->authorize($user, __FUNCTION__, $rights)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__, $rights)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) return false; if(!$this->userExists($user)) return false;
$this->db->prepare("UPDATE arsse_users set rights = ? where id is ?", "int", "str")->run($rights, $user); $this->db->prepare("UPDATE arsse_users set rights = ? where id is ?", "int", "str")->run($rights, $user);
return true; return true;
@ -250,7 +249,7 @@ class Database {
public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = ""): int { public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = ""): int {
// If the user isn't authorized to perform this action then throw an exception. // If the user isn't authorized to perform this action then throw an exception.
if (!$this->data->user->authorize($user, __FUNCTION__)) { if (!Data::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
// If the user doesn't exist throw an exception. // If the user doesn't exist throw an exception.
@ -299,13 +298,13 @@ class Database {
} }
public function subscriptionRemove(string $user, int $id): bool { public function subscriptionRemove(string $user, int $id): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
return (bool) $this->db->prepare("DELETE from arsse_subscriptions where id is ?", "int")->run($id)->changes(); return (bool) $this->db->prepare("DELETE from arsse_subscriptions where id is ?", "int")->run($id)->changes();
} }
public function folderAdd(string $user, array $data): int { public function folderAdd(string $user, array $data): int {
// If the user isn't authorized to perform this action then throw an exception. // If the user isn't authorized to perform this action then throw an exception.
if (!$this->data->user->authorize($user, __FUNCTION__)) { if (!Data::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
// If the user doesn't exist throw an exception. // If the user doesn't exist throw an exception.
@ -345,7 +344,7 @@ class Database {
public function folderList(string $user, int $parent = null, bool $recursive = true): Db\Result { public function folderList(string $user, int $parent = null, bool $recursive = true): Db\Result {
// if the user isn't authorized to perform this action then throw an exception. // if the user isn't authorized to perform this action then throw an exception.
if (!$this->data->user->authorize($user, __FUNCTION__)) { if (!Data::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
// if the user doesn't exist throw an exception. // if the user doesn't exist throw an exception.
@ -487,7 +486,7 @@ class Database {
} }
public function folderRemove(string $user, int $id): bool { public function folderRemove(string $user, int $id): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
// common table expression to list all descendant folders of the target folder // common table expression to list all descendant folders of the target folder
$cte = "RECURSIVE folders(id) as (SELECT id from arsse_folders where owner is ? and id is ? union select arsse_folders.id from arsse_folders join folders on arsse_folders.parent=folders.id) "; $cte = "RECURSIVE folders(id) as (SELECT id from arsse_folders where owner is ? and id is ? union select arsse_folders.id from arsse_folders join folders on arsse_folders.parent=folders.id) ";
$changes = 0; $changes = 0;

2
lib/Db/Driver.php

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Db; namespace JKingWeb\Arsse\Db;
interface Driver { interface Driver {
function __construct(\JKingWeb\Arsse\RuntimeData $data, bool $install = false); function __construct(bool $install = false);
// returns a human-friendly name for the driver (for display in installer, for example) // returns a human-friendly name for the driver (for display in installer, for example)
static function driverName(): string; static function driverName(): string;
// returns the version of the scheme of the opened database; if uninitialized should return 0 // returns the version of the scheme of the opened database; if uninitialized should return 0

16
lib/Db/SQLite3/Driver.php

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3; namespace JKingWeb\Arsse\Db\SQLite3;
use JKingWeb\Arsse\Lang; use JKingWeb\Arsse\Data;
use JKingWeb\Arsse\Db\Exception; use JKingWeb\Arsse\Db\Exception;
use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout; use JKingWeb\Arsse\Db\ExceptionTimeout;
@ -15,16 +15,14 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
const SQLITE_MISMATCH = 20; const SQLITE_MISMATCH = 20;
protected $db; protected $db;
protected $data;
public function __construct(\JKingWeb\Arsse\RuntimeData $data, bool $install = false) { public function __construct(bool $install = false) {
// check to make sure required extension is loaded // check to make sure required extension is loaded
if(!class_exists("SQLite3")) throw new Exception("extMissing", self::driverName()); if(!class_exists("SQLite3")) throw new Exception("extMissing", self::driverName());
$this->data = $data; $file = Data::$conf->dbSQLite3File;
$file = $data->conf->dbSQLite3File;
// if the file exists (or we're initializing the database), try to open it // if the file exists (or we're initializing the database), try to open it
try { try {
$this->db = new \SQLite3($file, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, $data->conf->dbSQLite3Key); $this->db = new \SQLite3($file, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, Data::$conf->dbSQLite3Key);
} catch(\Throwable $e) { } catch(\Throwable $e) {
// if opening the database doesn't work, check various pre-conditions to find out what the problem might be // if opening the database doesn't work, check various pre-conditions to find out what the problem might be
if(!file_exists($file)) { if(!file_exists($file)) {
@ -57,7 +55,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
static public function driverName(): string { static public function driverName(): string {
return Lang::msg("Driver.Db.SQLite3.Name"); return Data::$l->msg("Driver.Db.SQLite3.Name");
} }
public function schemaVersion(): int { public function schemaVersion(): int {
@ -66,10 +64,10 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
public function schemaUpdate(int $to): bool { public function schemaUpdate(int $to): bool {
$ver = $this->schemaVersion(); $ver = $this->schemaVersion();
if(!$this->data->conf->dbSQLite3AutoUpd) throw new Exception("updateManual", ['version' => $ver, 'driver_name' => $this->driverName()]); if(!Data::$conf->dbSQLite3AutoUpd) throw new Exception("updateManual", ['version' => $ver, 'driver_name' => $this->driverName()]);
if($ver >= $to) throw new Exception("updateTooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]); if($ver >= $to) throw new Exception("updateTooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]);
$sep = \DIRECTORY_SEPARATOR; $sep = \DIRECTORY_SEPARATOR;
$path = $this->data->conf->dbSchemaBase.$sep."SQLite3".$sep; $path = Data::$conf->dbSchemaBase.$sep."SQLite3".$sep;
$this->lock(); $this->lock();
$this->begin(); $this->begin();
for($a = $ver; $a < $to; $a++) { for($a = $ver; $a < $to; $a++) {

116
lib/Lang.php

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse; namespace JKingWeb\Arsse;
use \Webmozart\Glob\Glob; use Webmozart\Glob\Glob;
class Lang { class Lang {
const DEFAULT = "en"; // fallback locale const DEFAULT = "en"; // fallback locale
@ -16,61 +16,67 @@ class Lang {
'Exception.JKingWeb/Arsse/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})', 'Exception.JKingWeb/Arsse/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})',
]; ];
static public $path = BASE."locale".DIRECTORY_SEPARATOR; // path to locale files; this is a public property to facilitate unit testing public $path; // path to locale files; this is a public property to facilitate unit testing
static protected $requirementsMet = false; // whether the Intl extension is loaded static protected $requirementsMet = false; // whether the Intl extension is loaded
static protected $synched = false; // whether the wanted locale is actually loaded (lazy loading is used by default) protected $synched = false; // whether the wanted locale is actually loaded (lazy loading is used by default)
static protected $wanted = self::DEFAULT; // the currently requested locale protected $wanted = self::DEFAULT; // the currently requested locale
static protected $locale = ""; // the currently loaded locale protected $locale = ""; // the currently loaded locale
static protected $loaded = []; // the cascade of loaded locale file names protected $loaded = []; // the cascade of loaded locale file names
static protected $strings = self::REQUIRED; // the loaded locale strings, merged protected $strings = self::REQUIRED; // the loaded locale strings, merged
protected function __construct() {} function __construct(string $path = BASE."locale".DIRECTORY_SEPARATOR) {
$this->path = $path;
}
static public function set(string $locale, bool $immediate = false): string { public function set(string $locale, bool $immediate = false): string {
// make sure the Intl extension is loaded // make sure the Intl extension is loaded
if(!self::$requirementsMet) self::checkRequirements(); if(!static::$requirementsMet) static::checkRequirements();
// if requesting the same locale as already wanted, just return (but load first if we've requested an immediate load) // if requesting the same locale as already wanted, just return (but load first if we've requested an immediate load)
if($locale==self::$wanted) { if($locale==$this->wanted) {
if($immediate && !self::$synched) self::load(); if($immediate && !$this->synched) $this->load();
return $locale; return $locale;
} }
// if we've requested a locale other than the null locale, fetch the list of available files and find the closest match e.g. en_ca_somedialect -> en_ca // if we've requested a locale other than the null locale, fetch the list of available files and find the closest match e.g. en_ca_somedialect -> en_ca
if($locale != "") { if($locale != "") {
$list = self::listFiles(); $list = $this->listFiles();
// if the default locale is unavailable, this is (for now) an error // if the default locale is unavailable, this is (for now) an error
if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT); if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT);
self::$wanted = self::match($locale, $list); $this->wanted = $this->match($locale, $list);
} else { } else {
self::$wanted = ""; $this->wanted = "";
} }
self::$synched = false; $this->synched = false;
// load right now if asked to, otherwise load later when actually required // load right now if asked to, otherwise load later when actually required
if($immediate) self::load(); if($immediate) $this->load();
return self::$wanted; return $this->wanted;
} }
static public function get(bool $loaded = false): string { public function get(bool $loaded = false): string {
// we can either return the wanted locale (default) or the currently loaded locale // we can either return the wanted locale (default) or the currently loaded locale
return $loaded ? self::$locale : self::$wanted; return $loaded ? $this->locale : $this->wanted;
}
public function dump(): array {
return $this->strings;
} }
static public function dump(): array { public function msg(string $msgID, $vars = null): string {
return self::$strings; return $this($msgID, $vars);
} }
static public function msg(string $msgID, $vars = null): string { public function __invoke(string $msgID, $vars = null): string {
// if we're trying to load the system default language and it fails, we have a chicken and egg problem, so we catch the exception and load no language file instead // if we're trying to load the system default language and it fails, we have a chicken and egg problem, so we catch the exception and load no language file instead
if(!self::$synched) try {self::load();} catch(Lang\Exception $e) { if(!$this->synched) try {$this->load();} catch(Lang\Exception $e) {
if(self::$wanted==self::DEFAULT) { if($this->wanted==self::DEFAULT) {
self::set("", true); $this->set("", true);
} else { } else {
throw $e; throw $e;
} }
} }
// if the requested message is not present in any of the currently loaded language files, throw an exception // if the requested message is not present in any of the currently loaded language files, throw an exception
// note that this is indicative of a programming error since the default locale should have all strings // note that this is indicative of a programming error since the default locale should have all strings
if(!array_key_exists($msgID, self::$strings)) throw new Lang\Exception("stringMissing", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]); if(!array_key_exists($msgID, $this->strings)) throw new Lang\Exception("stringMissing", ['msgID' => $msgID, 'fileList' => implode(", ",$this->loaded)]);
$msg = self::$strings[$msgID]; $msg = $this->strings[$msgID];
// variables fed to MessageFormatter must be contained in an array // variables fed to MessageFormatter must be contained in an array
if($vars===null) { if($vars===null) {
// even though strings not given parameters will not get formatted, we do not optimize this case away: we still want to catch invalid strings // even though strings not given parameters will not get formatted, we do not optimize this case away: we still want to catch invalid strings
@ -78,36 +84,36 @@ class Lang {
} else if(!is_array($vars)) { } else if(!is_array($vars)) {
$vars = [$vars]; $vars = [$vars];
} }
$msg = \MessageFormatter::formatMessage(self::$locale, $msg, $vars); $msg = \MessageFormatter::formatMessage($this->locale, $msg, $vars);
if($msg===false) throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]); if($msg===false) throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ",$this->loaded)]);
return $msg; return $msg;
} }
static public function list(string $locale = ""): array { public function list(string $locale = ""): array {
$out = []; $out = [];
$files = self::listFiles(); $files = $this->listFiles();
foreach($files as $tag) { foreach($files as $tag) {
$out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale); $out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale);
} }
return $out; return $out;
} }
static public function match(string $locale, array $list = null): string { public function match(string $locale, array $list = null): string {
if($list===null) $list = self::listFiles(); if($list===null) $list = $this->listFiles();
$default = (self::$locale=="") ? self::DEFAULT : self::$locale; $default = ($this->locale=="") ? self::DEFAULT : $this->locale;
return \Locale::lookup($list,$locale, true, $default); return \Locale::lookup($list,$locale, true, $default);
} }
static protected function checkRequirements(): bool { static protected function checkRequirements(): bool {
if(!extension_loaded("intl")) throw new ExceptionFatal("The \"Intl\" extension is required, but not loaded"); if(!extension_loaded("intl")) throw new ExceptionFatal("The \"Intl\" extension is required, but not loaded");
self::$requirementsMet = true; static::$requirementsMet = true;
return true; return true;
} }
static protected function listFiles(): array { protected function listFiles(): array {
$out = glob(self::$path."*.php"); $out = glob($this->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 // 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"); // FIXME: we should just mock glob() in tests instead and make this a dev dependency if(empty($out)) $out = Glob::glob($this->path."*.php"); // FIXME: we should just mock glob() in tests instead and make this a dev dependency
// trim the returned file paths to return just the language tag // trim the returned file paths to return just the language tag
$out = array_map(function($file) { $out = array_map(function($file) {
$file = str_replace(DIRECTORY_SEPARATOR, "/", $file); $file = str_replace(DIRECTORY_SEPARATOR, "/", $file);
@ -119,17 +125,17 @@ class Lang {
return $out; return $out;
} }
static protected function load(): bool { protected function load(): bool {
if(!self::$requirementsMet) self::checkRequirements(); if(!self::$requirementsMet) self::checkRequirements();
// if we've requested no locale (""), just load the fallback strings and return // if we've requested no locale (""), just load the fallback strings and return
if(self::$wanted=="") { if($this->wanted=="") {
self::$strings = self::REQUIRED; $this->strings = self::REQUIRED;
self::$locale = self::$wanted; $this->locale = $this->wanted;
self::$synched = true; $this->synched = true;
return true; return true;
} }
// decompose the requested locale from specific to general, building a list of files to load // decompose the requested locale from specific to general, building a list of files to load
$tags = \Locale::parseLocale(self::$wanted); $tags = \Locale::parseLocale($this->wanted);
$files = []; $files = [];
while(sizeof($tags) > 0) { while(sizeof($tags) > 0) {
$files[] = strtolower(\Locale::composeLocale($tags)); $files[] = strtolower(\Locale::composeLocale($tags));
@ -142,7 +148,7 @@ class Lang {
// 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") // 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 = []; $files = [];
foreach($loaded as $file) { foreach($loaded as $file) {
if($file==self::$locale) break; if($file==$this->locale) break;
$files[] = $file; $files[] = $file;
} }
// if we need to load all files, start with the fallback strings // if we need to load all files, start with the fallback strings
@ -151,17 +157,17 @@ class Lang {
$strings[] = self::REQUIRED; $strings[] = self::REQUIRED;
} else { } else {
// otherwise start with the strings we already have if we're going from e.g. "fr" to "fr_ca" // otherwise start with the strings we already have if we're going from e.g. "fr" to "fr_ca"
$strings[] = self::$strings; $strings[] = $this->strings;
} }
// read files in reverse order // read files in reverse order
$files = array_reverse($files); $files = array_reverse($files);
foreach($files as $file) { foreach($files as $file) {
if(!file_exists(self::$path."$file.php")) throw new Lang\Exception("fileMissing", $file); if(!file_exists($this->path."$file.php")) throw new Lang\Exception("fileMissing", $file);
if(!is_readable(self::$path."$file.php")) throw new Lang\Exception("fileUnreadable", $file); if(!is_readable($this->path."$file.php")) throw new Lang\Exception("fileUnreadable", $file);
try { try {
// we use output buffering in case the language file is corrupted // we use output buffering in case the language file is corrupted
ob_start(); ob_start();
$arr = (include self::$path."$file.php"); $arr = (include $this->path."$file.php");
} catch(\Throwable $e) { } catch(\Throwable $e) {
$arr = null; $arr = null;
} finally { } finally {
@ -171,10 +177,10 @@ class Lang {
$strings[] = $arr; $strings[] = $arr;
} }
// apply the results and return // apply the results and return
self::$strings = call_user_func_array("array_replace_recursive", $strings); $this->strings = call_user_func_array("array_replace_recursive", $strings);
self::$loaded = $loaded; $this->loaded = $loaded;
self::$locale = self::$wanted; $this->locale = $this->wanted;
self::$synched = true; $this->synched = true;
return true; return true;
} }
} }

18
lib/Lang/Exception.php

@ -3,22 +3,4 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Lang; namespace JKingWeb\Arsse\Lang;
class Exception extends \JKingWeb\Arsse\AbstractException { class Exception extends \JKingWeb\Arsse\AbstractException {
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);
}
}
} }

5
lib/REST.php

@ -26,8 +26,7 @@ class REST {
]; ];
protected $data; protected $data;
function __construct(RuntimeData $data) { function __construct() {
$this->data = $data;
} }
function dispatch(REST\Request $req = null): bool { function dispatch(REST\Request $req = null): bool {
@ -35,7 +34,7 @@ class REST {
$api = $this->apiMatch($url, $this->apis); $api = $this->apiMatch($url, $this->apis);
$req->url = substr($url,strlen($this->apis[$api]['strip'])); $req->url = substr($url,strlen($this->apis[$api]['strip']));
$class = $this->apis[$api]['class']; $class = $this->apis[$api]['class'];
$drv = new $class($this->data); $drv = new $class();
$drv->dispatch($req); $drv->dispatch($req);
return true; return true;
} }

2
lib/REST/AbstractHandler.php

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\REST; namespace JKingWeb\Arsse\REST;
abstract class AbstractHandler implements Handler { abstract class AbstractHandler implements Handler {
abstract function __construct(\JKingWeb\Arsse\RuntimeData $data); abstract function __construct();
abstract function dispatch(Request $req): Response; abstract function dispatch(Request $req): Response;
protected function parseURL(string $url): array { protected function parseURL(string $url): array {

2
lib/REST/Handler.php

@ -3,6 +3,6 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\REST; namespace JKingWeb\Arsse\REST;
interface Handler { interface Handler {
function __construct(\JKingWeb\Arsse\RuntimeData $data); function __construct();
function dispatch(Request $req): Response; function dispatch(Request $req): Response;
} }

3
lib/REST/NextCloudNews/Versions.php

@ -4,8 +4,7 @@ namespace JKingWeb\Arsse\REST\NextCloudNews;
use JKingWeb\Arsse\REST\Response; use JKingWeb\Arsse\REST\Response;
class Versions extends \JKingWeb\Arsse\REST\AbstractHandler { class Versions extends \JKingWeb\Arsse\REST\AbstractHandler {
function __construct(\JKingWeb\Arsse\RuntimeData $data) { function __construct() {
// runtime data is not needed; this method is deliberately empty
} }
function dispatch(\JKingWeb\Arsse\REST\Request $req): \JKingWeb\Arsse\REST\Response { function dispatch(\JKingWeb\Arsse\REST\Request $req): \JKingWeb\Arsse\REST\Response {

16
lib/RuntimeData.php

@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
class RuntimeData {
public $conf;
public $db;
public $user;
public function __construct(Conf $conf) {
$this->conf = $conf;
Lang::set($conf->lang);
$this->db = new Database($this);
$this->user = new User($this);
}
}

56
lib/User.php

@ -5,7 +5,6 @@ namespace JKingWeb\Arsse;
class User { class User {
public $id = null; public $id = null;
protected $data;
protected $u; protected $u;
protected $authz = true; protected $authz = true;
protected $authzSupported = 0; protected $authzSupported = 0;
@ -23,10 +22,9 @@ class User {
return $classes; return $classes;
} }
public function __construct(\JKingWeb\Arsse\RuntimeData $data) { public function __construct() {
$this->data = $data; $driver = Data::$conf->userDriver;
$driver = $data->conf->userDriver; $this->u = new $driver();
$this->u = new $driver($data);
$this->authzSupported = $this->u->driverFunctions("authorize"); $this->authzSupported = $this->u->driverFunctions("authorize");
} }
@ -42,9 +40,9 @@ class User {
// if we don't have a logged-in user, fetch credentials // if we don't have a logged-in user, fetch credentials
if($this->id===null) $this->credentials(); if($this->id===null) $this->credentials();
// if the affected user is the actor and the actor is not trying to grant themselves rights, accept the request // if the affected user is the actor and the actor is not trying to grant themselves rights, accept the request
if($affectedUser==$this->data->user->id && $action != "userRightsSet") return true; if($affectedUser==Data::$user->id && $action != "userRightsSet") return true;
// get properties of actor if not already available // get properties of actor if not already available
if(!sizeof($this->actor)) $this->actor = $this->propertiesGet($this->data->user->id); if(!sizeof($this->actor)) $this->actor = $this->propertiesGet(Data::$user->id);
$rights =& $this->actor["rights"]; $rights =& $this->actor["rights"];
// if actor is a global admin, accept the request // if actor is a global admin, accept the request
if($rights==User\Driver::RIGHTS_GLOBAL_ADMIN) return true; if($rights==User\Driver::RIGHTS_GLOBAL_ADMIN) return true;
@ -53,7 +51,7 @@ class User {
// if actor is not some other sort of admin, deny the request // if actor is not some other sort of admin, deny the request
if(!in_array($rights,[User\Driver::RIGHTS_GLOBAL_MANAGER,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN],true)) return false; if(!in_array($rights,[User\Driver::RIGHTS_GLOBAL_MANAGER,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN],true)) return false;
// if actor is a domain admin/manager and domains don't match, deny the request // if actor is a domain admin/manager and domains don't match, deny the request
if($this->data->conf->userComposeNames && $this->actor["domain"] && $rights != User\Driver::RIGHTS_GLOBAL_MANAGER) { if(Data::$conf->userComposeNames && $this->actor["domain"] && $rights != User\Driver::RIGHTS_GLOBAL_MANAGER) {
$test = "@".$this->actor["domain"]; $test = "@".$this->actor["domain"];
if(substr($affectedUser,-1*strlen($test)) != $test) return false; if(substr($affectedUser,-1*strlen($test)) != $test) return false;
} }
@ -79,7 +77,7 @@ class User {
} }
public function credentials(): array { public function credentials(): array {
if($this->data->conf->userAuthPreferHTTP) { if(Data::$conf->userAuthPreferHTTP) {
return $this->credentialsHTTP(); return $this->credentialsHTTP();
} else { } else {
return $this->credentialsForm(); return $this->credentialsForm();
@ -100,7 +98,7 @@ class User {
} else { } else {
$out = ["user" => "", "password" => ""]; $out = ["user" => "", "password" => ""];
} }
if($this->data->conf->userComposeNames && $out["user"] != "") { if(Data::$conf->userComposeNames && $out["user"] != "") {
$out["user"] = $this->composeName($out["user"]); $out["user"] = $this->composeName($out["user"]);
} }
$this->id = $out["user"]; $this->id = $out["user"];
@ -109,7 +107,7 @@ class User {
public function auth(string $user = null, string $password = null): bool { public function auth(string $user = null, string $password = null): bool {
if($user===null) { if($user===null) {
if($this->data->conf->userAuthPreferHTTP) return $this->authHTTP(); if(Data::$conf->userAuthPreferHTTP) return $this->authHTTP();
return $this->authForm(); return $this->authForm();
} else { } else {
$this->id = $user; $this->id = $user;
@ -117,7 +115,7 @@ class User {
switch($this->u->driverFunctions("auth")) { switch($this->u->driverFunctions("auth")) {
case User\Driver::FUNC_EXTERNAL: case User\Driver::FUNC_EXTERNAL:
$out = $this->u->auth($user, $password); $out = $this->u->auth($user, $password);
if($out && !$this->data->db->userExists($user)) $this->autoProvision($user, $password); if($out && !Data::$db->userExists($user)) $this->autoProvision($user, $password);
return $out; return $out;
case User\Driver::FUNC_INTERNAL: case User\Driver::FUNC_INTERNAL:
return $this->u->auth($user, $password); return $this->u->auth($user, $password);
@ -176,7 +174,7 @@ class User {
// we handle authorization checks for external drivers // we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]); if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$out = $this->u->userExists($user); $out = $this->u->userExists($user);
if($out && !$this->data->db->userExists($user)) $this->autoProvision($user, ""); if($out && !Data::$db->userExists($user)) $this->autoProvision($user, "");
return $out; return $out;
case User\Driver::FUNC_INTERNAL: case User\Driver::FUNC_INTERNAL:
// internal functions handle their own authorization // internal functions handle their own authorization
@ -195,7 +193,7 @@ class User {
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]); if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$newPassword = $this->u->userAdd($user, $password); $newPassword = $this->u->userAdd($user, $password);
// if there was no exception and we don't have the user in the internal database, add it // if there was no exception and we don't have the user in the internal database, add it
if(!$this->data->db->userExists($user)) $this->autoProvision($user, $newPassword); if(!Data::$db->userExists($user)) $this->autoProvision($user, $newPassword);
return $newPassword; return $newPassword;
case User\Driver::FUNC_INTERNAL: case User\Driver::FUNC_INTERNAL:
// internal functions handle their own authorization // internal functions handle their own authorization
@ -212,9 +210,9 @@ class User {
// we handle authorization checks for external drivers // we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]); if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$out = $this->u->userRemove($user); $out = $this->u->userRemove($user);
if($out && $this->data->db->userExists($user)) { if($out && Data::$db->userExists($user)) {
// if the user was removed and we have it in our data, remove it there // if the user was removed and we have it in our data, remove it there
if(!$this->data->db->userExists($user)) $this->data->db->userRemove($user); if(!Data::$db->userExists($user)) Data::$db->userRemove($user);
} }
return $out; return $out;
case User\Driver::FUNC_INTERNAL: case User\Driver::FUNC_INTERNAL:
@ -232,9 +230,9 @@ class User {
// we handle authorization checks for external drivers // we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]); if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$out = $this->u->userPasswordSet($user, $newPassword, $oldPassword); $out = $this->u->userPasswordSet($user, $newPassword, $oldPassword);
if($this->data->db->userExists($user)) { if(Data::$db->userExists($user)) {
// if the password change was successful and the user exists, set the internal password to the same value // if the password change was successful and the user exists, set the internal password to the same value
$this->data->db->userPasswordSet($user, $out); Data::$db->userPasswordSet($user, $out);
} else { } else {
// if the user does not exists in the internal database, create it // if the user does not exists in the internal database, create it
$this->autoProvision($user, $out); $this->autoProvision($user, $out);
@ -251,7 +249,7 @@ class User {
public function propertiesGet(string $user): array { public function propertiesGet(string $user): array {
// prepare default values // prepare default values
$domain = null; $domain = null;
if($this->data->conf->userComposeNames) $domain = substr($user,strrpos($user,"@")+1); if(Data::$conf->userComposeNames) $domain = substr($user,strrpos($user,"@")+1);
$init = [ $init = [
"id" => $user, "id" => $user,
"name" => $user, "name" => $user,
@ -267,7 +265,7 @@ class User {
// remove password if it is return (not exhaustive, but...) // remove password if it is return (not exhaustive, but...)
if(array_key_exists('password', $out)) unset($out['password']); if(array_key_exists('password', $out)) unset($out['password']);
// if the user does not exist in the internal database, add it // if the user does not exist in the internal database, add it
if(!$this->data->db->userExists($user)) $this->autoProvision($user, "", $out); if(!Data::$db->userExists($user)) $this->autoProvision($user, "", $out);
return $out; return $out;
case User\Driver::FUNC_INTERNAL: case User\Driver::FUNC_INTERNAL:
// internal functions handle their own authorization // internal functions handle their own authorization
@ -289,9 +287,9 @@ class User {
// we handle authorization checks for external drivers // we handle authorization checks for external drivers
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]); if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$out = $this->u->userPropertiesSet($user, $properties); $out = $this->u->userPropertiesSet($user, $properties);
if($this->data->db->userExists($user)) { if(Data::$db->userExists($user)) {
// if the property change was successful and the user exists, set the internal properties to the same values // if the property change was successful and the user exists, set the internal properties to the same values
$this->data->db->userPropertiesSet($user, $out); Data::$db->userPropertiesSet($user, $out);
} else { } else {
// if the user does not exists in the internal database, create it // if the user does not exists in the internal database, create it
$this->autoProvision($user, "", $out); $this->autoProvision($user, "", $out);
@ -313,7 +311,7 @@ class User {
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]); if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$out = $this->u->userRightsGet($user); $out = $this->u->userRightsGet($user);
// if the user does not exist in the internal database, add it // if the user does not exist in the internal database, add it
if(!$this->data->db->userExists($user)) $this->autoProvision($user, "", null, $out); if(!Data::$db->userExists($user)) $this->autoProvision($user, "", null, $out);
return $out; return $out;
case User\Driver::FUNC_INTERNAL: case User\Driver::FUNC_INTERNAL:
// internal functions handle their own authorization // internal functions handle their own authorization
@ -332,10 +330,10 @@ class User {
if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]); if(!$this->authorize($user, $func)) throw new User\ExceptionAuthz("notAuthorized", ["action" => $func, "user" => $user]);
$out = $this->u->userRightsSet($user, $level); $out = $this->u->userRightsSet($user, $level);
// if the user does not exist in the internal database, add it // if the user does not exist in the internal database, add it
if($out && $this->data->db->userExists($user)) { if($out && Data::$db->userExists($user)) {
$authz = $this->authorizationEnabled(); $authz = $this->authorizationEnabled();
$this->authorizationEnabled(false); $this->authorizationEnabled(false);
$this->data->db->userRightsSet($user, $level); Data::$db->userRightsSet($user, $level);
$this->authorizationEnabled($authz); $this->authorizationEnabled($authz);
} else if($out) { } else if($out) {
$this->autoProvision($user, "", null, $level); $this->autoProvision($user, "", null, $level);
@ -367,18 +365,18 @@ class User {
$authz = $this->authorizationEnabled(); $authz = $this->authorizationEnabled();
$this->authorizationEnabled(false); $this->authorizationEnabled(false);
// create the user // create the user
$out = $this->data->db->userAdd($user, $password); $out = Data::$db->userAdd($user, $password);
// set the user rights // set the user rights
$this->data->db->userRightsSet($user, $rights); Data::$db->userRightsSet($user, $rights);
// set the user properties... // set the user properties...
if($properties===null) { if($properties===null) {
// if nothing is provided but the driver uses an external function, try to get the current values from the external source // if nothing is provided but the driver uses an external function, try to get the current values from the external source
try { try {
if($this->u->driverFunctions("userPropertiesGet")==User\Driver::FUNC_EXTERNAL) $this->data->db->userPropertiesSet($user, $this->u->userPropertiesGet($user)); if($this->u->driverFunctions("userPropertiesGet")==User\Driver::FUNC_EXTERNAL) Data::$db->userPropertiesSet($user, $this->u->userPropertiesGet($user));
} catch(\Throwable $e) {} } catch(\Throwable $e) {}
} else { } else {
// otherwise if values are provided, use those // otherwise if values are provided, use those
$this->data->db->userPropertiesSet($user, $properties); Data::$db->userPropertiesSet($user, $properties);
} }
// re-enable authorization and return // re-enable authorization and return
$this->authorizationEnabled($authz); $this->authorizationEnabled($authz);

4
lib/User/Driver.php

@ -13,8 +13,8 @@ Interface Driver {
const RIGHTS_GLOBAL_MANAGER = 75; // able to act for any normal users on any domain; cannot elevate other users const RIGHTS_GLOBAL_MANAGER = 75; // able to act for any normal users on any domain; cannot elevate other users
const RIGHTS_GLOBAL_ADMIN = 100; // is completely unrestricted const RIGHTS_GLOBAL_ADMIN = 100; // is completely unrestricted
// returns an instance of a class implementing this interface. Implemented as a static method for consistency with database classes // returns an instance of a class implementing this interface.
function __construct(\JKingWeb\Arsse\RuntimeData $data); function __construct();
// returns a human-friendly name for the driver (for display in installer, for example) // returns a human-friendly name for the driver (for display in installer, for example)
static function driverName(): string; static function driverName(): string;
// returns an array (or single queried member of same) of methods defined by this interface and whether the class implements the internal function or a custom version // returns an array (or single queried member of same) of methods defined by this interface and whether the class implements the internal function or a custom version

8
lib/User/Internal/Driver.php

@ -1,13 +1,11 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\User\Internal; namespace JKingWeb\Arsse\User\Internal;
use JKingWeb\Arsse\Lang;
use JKingWeb\Arsse\User\Driver as Iface; use JKingWeb\Arsse\User\Driver as Iface;
final class Driver implements Iface { final class Driver implements Iface {
use InternalFunctions; use InternalFunctions;
protected $data;
protected $db; protected $db;
protected $functions = [ protected $functions = [
"auth" => Iface::FUNC_INTERNAL, "auth" => Iface::FUNC_INTERNAL,
@ -22,12 +20,8 @@ final class Driver implements Iface {
"userRightsSet" => Iface::FUNC_INTERNAL, "userRightsSet" => Iface::FUNC_INTERNAL,
]; ];
static public function create(\JKingWeb\Arsse\RuntimeData $data): Driver {
return new static($data);
}
static public function driverName(): string { static public function driverName(): string {
return Lang::msg("Driver.User.Internal.Name"); return Data::$l->msg("Driver.User.Internal.Name");
} }
public function driverFunctions(string $function = null) { public function driverFunctions(string $function = null) {

8
lib/User/Internal/InternalFunctions.php

@ -1,17 +1,17 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\User\Internal; namespace JKingWeb\Arsse\User\Internal;
use JKingWeb\Arsse\Data;
trait InternalFunctions { trait InternalFunctions {
protected $actor = []; protected $actor = [];
public function __construct(\JKingWeb\Arsse\RuntimeData $data) { public function __construct() {
$this->data = $data; $this->db = Data::$db;
$this->db = $this->data->db;
} }
function auth(string $user, string $password): bool { function auth(string $user, string $password): bool {
if(!$this->data->user->exists($user)) return false; if(!Data::$user->exists($user)) return false;
$hash = $this->db->userPasswordGet($user); $hash = $this->db->userPasswordGet($user);
if($password==="" && $hash==="") return true; if($password==="" && $hash==="") return true;
return password_verify($password, $hash); return password_verify($password, $hash);

8
tests/Conf/TestConf.php

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse; namespace JKingWeb\Arsse;
use \org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStream;
class TestConf extends \PHPUnit\Framework\TestCase { class TestConf extends \PHPUnit\Framework\TestCase {
@ -10,7 +10,8 @@ class TestConf extends \PHPUnit\Framework\TestCase {
static $vfs; static $vfs;
static $path; static $path;
static function setUpBeforeClass() { function setUp() {
$this->clearData();
self::$vfs = vfsStream::setup("root", null, [ self::$vfs = vfsStream::setup("root", null, [
'confGood' => '<?php return Array("lang" => "xx");', 'confGood' => '<?php return Array("lang" => "xx");',
'confNotArray' => '<?php return 0;', 'confNotArray' => '<?php return 0;',
@ -24,9 +25,10 @@ class TestConf extends \PHPUnit\Framework\TestCase {
chmod(self::$path."confUnreadable", 0000); chmod(self::$path."confUnreadable", 0000);
} }
static function tearDownAfterClass() { function tearDown() {
self::$path = null; self::$path = null;
self::$vfs = null; self::$vfs = null;
$this->clearData();
} }
function testLoadDefaultValues() { function testLoadDefaultValues() {

34
tests/Db/SQLite3/TestDbDriverSQLite3.php

@ -10,20 +10,22 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
protected $drv; protected $drv;
function setUp() { function setUp() {
$this->clearData();
$conf = new Conf(); $conf = new Conf();
$conf->dbDriver = Db\SQLite3\Driver::class; $conf->dbDriver = Db\SQLite3\Driver::class;
$conf->dbSQLite3File = tempnam(sys_get_temp_dir(), 'ook'); $conf->dbSQLite3File = tempnam(sys_get_temp_dir(), 'ook');
$this->data = new Test\RuntimeData($conf); Data::$conf = $conf;
$this->drv = new Db\SQLite3\Driver($this->data, true); $this->drv = new Db\SQLite3\Driver(true);
} }
function tearDown() { function tearDown() {
unset($this->drv); unset($this->drv);
unlink($this->data->conf->dbSQLite3File); unlink(Data::$conf->dbSQLite3File);
$this->clearData();
} }
function testFetchDriverName() { function testFetchDriverName() {
$class = $this->data->conf->dbDriver; $class = Data::$conf->dbDriver;
$this->assertTrue(strlen($class::driverName()) > 0); $this->assertTrue(strlen($class::driverName()) > 0);
} }
@ -38,12 +40,12 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testExecMultipleStatements() { function testExecMultipleStatements() {
$this->assertTrue($this->drv->exec("CREATE TABLE test(id integer primary key); INSERT INTO test(id) values(2112)")); $this->assertTrue($this->drv->exec("CREATE TABLE test(id integer primary key); INSERT INTO test(id) values(2112)"));
$ch = new \SQLite3($this->data->conf->dbSQLite3File); $ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->assertEquals(2112, $ch->querySingle("SELECT id from test")); $this->assertEquals(2112, $ch->querySingle("SELECT id from test"));
} }
function testExecTimeout() { function testExecTimeout() {
$ch = new \SQLite3($this->data->conf->dbSQLite3File); $ch = new \SQLite3(Data::$conf->dbSQLite3File);
$ch->exec("BEGIN EXCLUSIVE TRANSACTION"); $ch->exec("BEGIN EXCLUSIVE TRANSACTION");
$this->assertException("general", "Db", "ExceptionTimeout"); $this->assertException("general", "Db", "ExceptionTimeout");
$this->drv->exec("CREATE TABLE test(id integer primary key)"); $this->drv->exec("CREATE TABLE test(id integer primary key)");
@ -71,7 +73,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
} }
function testQueryTimeout() { function testQueryTimeout() {
$ch = new \SQLite3($this->data->conf->dbSQLite3File); $ch = new \SQLite3(Data::$conf->dbSQLite3File);
$ch->exec("BEGIN EXCLUSIVE TRANSACTION"); $ch->exec("BEGIN EXCLUSIVE TRANSACTION");
$this->assertException("general", "Db", "ExceptionTimeout"); $this->assertException("general", "Db", "ExceptionTimeout");
$this->drv->query("CREATE TABLE test(id integer primary key)"); $this->drv->query("CREATE TABLE test(id integer primary key)");
@ -102,7 +104,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testBeginTransaction() { function testBeginTransaction() {
$select = "SELECT count(*) FROM test"; $select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)"; $insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File); $ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)"); $this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin(); $this->drv->begin();
$this->drv->query($insert); $this->drv->query($insert);
@ -116,7 +118,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testCommitTransaction() { function testCommitTransaction() {
$select = "SELECT count(*) FROM test"; $select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)"; $insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File); $ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)"); $this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin(); $this->drv->begin();
$this->drv->query($insert); $this->drv->query($insert);
@ -130,7 +132,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testRollbackTransaction() { function testRollbackTransaction() {
$select = "SELECT count(*) FROM test"; $select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)"; $insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File); $ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)"); $this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin(); $this->drv->begin();
$this->drv->query($insert); $this->drv->query($insert);
@ -144,7 +146,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testBeginChainedTransactions() { function testBeginChainedTransactions() {
$select = "SELECT count(*) FROM test"; $select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)"; $insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File); $ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)"); $this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin(); $this->drv->begin();
$this->drv->query($insert); $this->drv->query($insert);
@ -159,7 +161,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testCommitChainedTransactions() { function testCommitChainedTransactions() {
$select = "SELECT count(*) FROM test"; $select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)"; $insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File); $ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)"); $this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin(); $this->drv->begin();
$this->drv->query($insert); $this->drv->query($insert);
@ -178,7 +180,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testRollbackChainedTransactions() { function testRollbackChainedTransactions() {
$select = "SELECT count(*) FROM test"; $select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)"; $insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File); $ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)"); $this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin(); $this->drv->begin();
$this->drv->query($insert); $this->drv->query($insert);
@ -199,7 +201,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testPartiallyRollbackChainedTransactions() { function testPartiallyRollbackChainedTransactions() {
$select = "SELECT count(*) FROM test"; $select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)"; $insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File); $ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)"); $this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin(); $this->drv->begin();
$this->drv->query($insert); $this->drv->query($insert);
@ -220,7 +222,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testFullyRollbackChainedTransactions() { function testFullyRollbackChainedTransactions() {
$select = "SELECT count(*) FROM test"; $select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)"; $insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File); $ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)"); $this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin(); $this->drv->begin();
$this->drv->query($insert); $this->drv->query($insert);
@ -238,7 +240,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase {
function testFullyCommitChainedTransactions() { function testFullyCommitChainedTransactions() {
$select = "SELECT count(*) FROM test"; $select = "SELECT count(*) FROM test";
$insert = "INSERT INTO test(id) values(null)"; $insert = "INSERT INTO test(id) values(null)";
$ch = new \SQLite3($this->data->conf->dbSQLite3File); $ch = new \SQLite3(Data::$conf->dbSQLite3File);
$this->drv->exec("CREATE TABLE test(id integer primary key)"); $this->drv->exec("CREATE TABLE test(id integer primary key)");
$this->drv->begin(); $this->drv->begin();
$this->drv->query($insert); $this->drv->query($insert);

10
tests/Db/SQLite3/TestDbUpdateSQLite3.php

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse; namespace JKingWeb\Arsse;
use \org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStream;
class TestDbUpdateSQLite3 extends \PHPUnit\Framework\TestCase { class TestDbUpdateSQLite3 extends \PHPUnit\Framework\TestCase {
@ -16,20 +16,22 @@ class TestDbUpdateSQLite3 extends \PHPUnit\Framework\TestCase {
const MINIMAL2 = "pragma user_version=2"; const MINIMAL2 = "pragma user_version=2";
function setUp() { function setUp() {
$this->clearData();
$this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]); $this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]);
$conf = new Conf(); $conf = new Conf();
$conf->dbDriver = Db\SQLite3\Driver::class; $conf->dbDriver = Db\SQLite3\Driver::class;
$conf->dbSchemaBase = $this->vfs->url(); $conf->dbSchemaBase = $this->vfs->url();
$this->base = $this->vfs->url()."/SQLite3/"; $this->base = $this->vfs->url()."/SQLite3/";
$conf->dbSQLite3File = ":memory:"; $conf->dbSQLite3File = ":memory:";
$this->data = new Test\RuntimeData($conf); Data::$conf = $conf;
$this->drv = new Db\SQLite3\Driver($this->data, true); $this->drv = new Db\SQLite3\Driver(true);
} }
function tearDown() { function tearDown() {
unset($this->drv); unset($this->drv);
unset($this->data); unset($this->data);
unset($this->vfs); unset($this->vfs);
$this->clearData();
} }
function testLoadMissingFile() { function testLoadMissingFile() {
@ -82,7 +84,7 @@ class TestDbUpdateSQLite3 extends \PHPUnit\Framework\TestCase {
} }
function testPerformActualUpdate() { function testPerformActualUpdate() {
$this->data->conf->dbSchemaBase = (new Conf())->dbSchemaBase; Data::$conf->dbSchemaBase = (new Conf())->dbSchemaBase;
$this->drv->schemaUpdate(Database::SCHEMA_VERSION); $this->drv->schemaUpdate(Database::SCHEMA_VERSION);
$this->assertEquals(Database::SCHEMA_VERSION, $this->drv->schemaVersion()); $this->assertEquals(Database::SCHEMA_VERSION, $this->drv->schemaVersion());
} }

20
tests/Exception/TestException.php

@ -6,12 +6,15 @@ namespace JKingWeb\Arsse;
class TestException extends \PHPUnit\Framework\TestCase { class TestException extends \PHPUnit\Framework\TestCase {
use Test\Tools; use Test\Tools;
static function setUpBeforeClass() { function setUp() {
Lang::set(""); $this->clearData(false);
$m = $this->getMockBuilder(Lang::class)->setMethods(['__invoke'])->getMock();
$m->expects($this->any())->method("__invoke")->with($this->anything(), $this->anything())->will($this->returnValue(""));
Data::$l = $m;
} }
static function tearDownAfterClass() { function tearDown() {
Lang::set(Lang::DEFAULT); $this->clearData(true);
} }
function testBaseClass() { function testBaseClass() {
@ -51,14 +54,6 @@ class TestException extends \PHPUnit\Framework\TestCase {
throw new Exception("testThisExceptionMessageDoesNotExist"); throw new Exception("testThisExceptionMessageDoesNotExist");
} }
/**
* @depends testBaseClass
*/
function testBaseClassWithMissingMessage() {
$this->assertException("stringMissing", "Lang");
throw new Exception("invalid");
}
/** /**
* @depends testBaseClassWithUnknownCode * @depends testBaseClassWithUnknownCode
*/ */
@ -66,5 +61,4 @@ class TestException extends \PHPUnit\Framework\TestCase {
$this->assertException("uncoded"); $this->assertException("uncoded");
throw new Lang\Exception("testThisExceptionMessageDoesNotExist"); throw new Lang\Exception("testThisExceptionMessageDoesNotExist");
} }
} }

39
tests/Lang/TestLang.php

@ -1,48 +1,47 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse; namespace JKingWeb\Arsse;
use \org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStream;
class TestLang extends \PHPUnit\Framework\TestCase { class TestLang extends \PHPUnit\Framework\TestCase {
use Test\Tools, Test\Lang\Setup; use Test\Tools, Test\Lang\Setup;
static $vfs; public $files;
static $path; public $path;
static $files; public $l;
static $defaultPath;
function testListLanguages() { function testListLanguages() {
$this->assertCount(sizeof(self::$files), Lang::list("en")); $this->assertCount(sizeof($this->files), $this->l->list("en"));
} }
/** /**
* @depends testListLanguages * @depends testListLanguages
*/ */
function testSetLanguage() { function testSetLanguage() {
$this->assertEquals("en", Lang::set("en")); $this->assertEquals("en", $this->l->set("en"));
$this->assertEquals("en_ca", Lang::set("en_ca")); $this->assertEquals("en_ca", $this->l->set("en_ca"));
$this->assertEquals("de", Lang::set("de_ch")); $this->assertEquals("de", $this->l->set("de_ch"));
$this->assertEquals("en", Lang::set("en_gb_hixie")); $this->assertEquals("en", $this->l->set("en_gb_hixie"));
$this->assertEquals("en_ca", Lang::set("en_ca_jking")); $this->assertEquals("en_ca", $this->l->set("en_ca_jking"));
$this->assertEquals("en", Lang::set("es")); $this->assertEquals("en", $this->l->set("es"));
$this->assertEquals("", Lang::set("")); $this->assertEquals("", $this->l->set(""));
} }
/** /**
* @depends testSetLanguage * @depends testSetLanguage
*/ */
function testLoadInternalStrings() { function testLoadInternalStrings() {
$this->assertEquals("", Lang::set("", true)); $this->assertEquals("", $this->l->set("", true));
$this->assertCount(sizeof(Lang::REQUIRED), Lang::dump()); $this->assertCount(sizeof(Lang::REQUIRED), $this->l->dump());
} }
/** /**
* @depends testLoadInternalStrings * @depends testLoadInternalStrings
*/ */
function testLoadDefaultLanguage() { function testLoadDefaultLanguage() {
$this->assertEquals(Lang::DEFAULT, Lang::set(Lang::DEFAULT, true)); $this->assertEquals(Lang::DEFAULT, $this->l->set(Lang::DEFAULT, true));
$str = Lang::dump(); $str = $this->l->dump();
$this->assertArrayHasKey('Exception.JKingWeb/Arsse/Exception.uncoded', $str); $this->assertArrayHasKey('Exception.JKingWeb/Arsse/Exception.uncoded', $str);
$this->assertArrayHasKey('Test.presentText', $str); $this->assertArrayHasKey('Test.presentText', $str);
} }
@ -51,9 +50,9 @@ class TestLang extends \PHPUnit\Framework\TestCase {
* @depends testLoadDefaultLanguage * @depends testLoadDefaultLanguage
*/ */
function testLoadSupplementaryLanguage() { function testLoadSupplementaryLanguage() {
Lang::set(Lang::DEFAULT, true); $this->l->set(Lang::DEFAULT, true);
$this->assertEquals("ja", Lang::set("ja", true)); $this->assertEquals("ja", $this->l->set("ja", true));
$str = Lang::dump(); $str = $this->l->dump();
$this->assertArrayHasKey('Exception.JKingWeb/Arsse/Exception.uncoded', $str); $this->assertArrayHasKey('Exception.JKingWeb/Arsse/Exception.uncoded', $str);
$this->assertArrayHasKey('Test.presentText', $str); $this->assertArrayHasKey('Test.presentText', $str);
$this->assertArrayHasKey('Test.absentText', $str); $this->assertArrayHasKey('Test.absentText', $str);

36
tests/Lang/TestLangErrors.php

@ -1,66 +1,64 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse; namespace JKingWeb\Arsse;
use \org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStream;
class TestLangErrors extends \PHPUnit\Framework\TestCase { class TestLangErrors extends \PHPUnit\Framework\TestCase {
use Test\Tools, Test\Lang\Setup; use Test\Tools, Test\Lang\Setup;
static $vfs; public $files;
static $path; public $path;
static $files; public $l;
static $defaultPath;
function setUp() { function setUpSeries() {
Lang::set("", true); $this->l->set("", true);
} }
function testLoadEmptyFile() { function testLoadEmptyFile() {
$this->assertException("fileCorrupt", "Lang"); $this->assertException("fileCorrupt", "Lang");
Lang::set("fr_ca", true); $this->l->set("fr_ca", true);
} }
function testLoadFileWhichDoesNotReturnAnArray() { function testLoadFileWhichDoesNotReturnAnArray() {
$this->assertException("fileCorrupt", "Lang"); $this->assertException("fileCorrupt", "Lang");
Lang::set("it", true); $this->l->set("it", true);
} }
function testLoadFileWhichIsNotPhp() { function testLoadFileWhichIsNotPhp() {
$this->assertException("fileCorrupt", "Lang"); $this->assertException("fileCorrupt", "Lang");
Lang::set("ko", true); $this->l->set("ko", true);
} }
function testLoadFileWhichIsCorrupt() { function testLoadFileWhichIsCorrupt() {
$this->assertException("fileCorrupt", "Lang"); $this->assertException("fileCorrupt", "Lang");
Lang::set("zh", true); $this->l->set("zh", true);
} }
function testLoadFileWithooutReadPermission() { function testLoadFileWithooutReadPermission() {
$this->assertException("fileUnreadable", "Lang"); $this->assertException("fileUnreadable", "Lang");
Lang::set("ru", true); $this->l->set("ru", true);
} }
function testLoadSubtagOfMissingLanguage() { function testLoadSubtagOfMissingLanguage() {
$this->assertException("fileMissing", "Lang"); $this->assertException("fileMissing", "Lang");
Lang::set("pt_br", true); $this->l->set("pt_br", true);
} }
function testFetchInvalidMessage() { function testFetchInvalidMessage() {
$this->assertException("stringInvalid", "Lang"); $this->assertException("stringInvalid", "Lang");
Lang::set("vi", true); $this->l->set("vi", true);
$txt = Lang::msg('Test.presentText'); $txt = $this->l->msg('Test.presentText');
} }
function testFetchMissingMessage() { function testFetchMissingMessage() {
$this->assertException("stringMissing", "Lang"); $this->assertException("stringMissing", "Lang");
$txt = Lang::msg('Test.absentText'); $txt = $this->l->msg('Test.absentText');
} }
function testLoadMissingDefaultLanguage() { function testLoadMissingDefaultLanguage() {
// this should be the last test of the series unlink($this->path.Lang::DEFAULT.".php");
unlink(self::$path.Lang::DEFAULT.".php");
$this->assertException("defaultFileMissing", "Lang"); $this->assertException("defaultFileMissing", "Lang");
Lang::set("fr", true); $this->l->set("fr", true);
} }
} }

67
tests/Lang/testLangComplex.php

@ -1,40 +1,39 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse; namespace JKingWeb\Arsse;
use \org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStream;
class TestLangComplex extends \PHPUnit\Framework\TestCase { class TestLangComplex extends \PHPUnit\Framework\TestCase {
use Test\Tools, Test\Lang\Setup; use Test\Tools, Test\Lang\Setup;
static $vfs; public $files;
static $path; public $path;
static $files; public $l;
static $defaultPath;
function setUp() { function setUpSeries() {
Lang::set(Lang::DEFAULT, true); $this->l->set(Lang::DEFAULT, true);
} }
function testLazyLoad() { function testLazyLoad() {
Lang::set("ja"); $this->l->set("ja");
$this->assertArrayNotHasKey('Test.absentText', Lang::dump()); $this->assertArrayNotHasKey('Test.absentText', $this->l->dump());
} }
/** /**
* @depends testLazyLoad * @depends testLazyLoad
*/ */
function testGetWantedAndLoadedLocale() { function testGetWantedAndLoadedLocale() {
Lang::set("en", true); $this->l->set("en", true);
Lang::set("ja"); $this->l->set("ja");
$this->assertEquals("ja", Lang::get()); $this->assertEquals("ja", $this->l->get());
$this->assertEquals("en", Lang::get(true)); $this->assertEquals("en", $this->l->get(true));
} }
function testLoadCascadeOfFiles() { function testLoadCascadeOfFiles() {
Lang::set("ja", true); $this->l->set("ja", true);
$this->assertEquals("de", Lang::set("de", true)); $this->assertEquals("de", $this->l->set("de", true));
$str = Lang::dump(); $str = $this->l->dump();
$this->assertArrayNotHasKey('Test.absentText', $str); $this->assertArrayNotHasKey('Test.absentText', $str);
$this->assertEquals('und der Stein der Weisen', $str['Test.presentText']); $this->assertEquals('und der Stein der Weisen', $str['Test.presentText']);
} }
@ -43,62 +42,62 @@ class TestLangComplex extends \PHPUnit\Framework\TestCase {
* @depends testLoadCascadeOfFiles * @depends testLoadCascadeOfFiles
*/ */
function testLoadSubtag() { function testLoadSubtag() {
$this->assertEquals("en_ca", Lang::set("en_ca", true)); $this->assertEquals("en_ca", $this->l->set("en_ca", true));
} }
function testFetchAMessage() { function testFetchAMessage() {
Lang::set("de", true); $this->l->set("de", true);
$this->assertEquals('und der Stein der Weisen', Lang::msg('Test.presentText')); $this->assertEquals('und der Stein der Weisen', $this->l->msg('Test.presentText'));
} }
/** /**
* @depends testFetchAMessage * @depends testFetchAMessage
*/ */
function testFetchAMessageWithMissingParameters() { function testFetchAMessageWithMissingParameters() {
Lang::set("en_ca", true); $this->l->set("en_ca", true);
$this->assertEquals('{0} and {1}', Lang::msg('Test.presentText')); $this->assertEquals('{0} and {1}', $this->l->msg('Test.presentText'));
} }
/** /**
* @depends testFetchAMessage * @depends testFetchAMessage
*/ */
function testFetchAMessageWithSingleNumericParameter() { function testFetchAMessageWithSingleNumericParameter() {
Lang::set("en_ca", true); $this->l->set("en_ca", true);
$this->assertEquals('Default language file "en" missing', Lang::msg('Exception.JKingWeb/Arsse/Lang/Exception.defaultFileMissing', Lang::DEFAULT)); $this->assertEquals('Default language file "en" missing', $this->l->msg('Exception.JKingWeb/Arsse/Lang/Exception.defaultFileMissing', Lang::DEFAULT));
} }
/** /**
* @depends testFetchAMessage * @depends testFetchAMessage
*/ */
function testFetchAMessageWithMultipleNumericParameters() { function testFetchAMessageWithMultipleNumericParameters() {
Lang::set("en_ca", true); $this->l->set("en_ca", true);
$this->assertEquals('Happy Rotter and the Philosopher\'s Stone', Lang::msg('Test.presentText', ['Happy Rotter', 'the Philosopher\'s Stone'])); $this->assertEquals('Happy Rotter and the Philosopher\'s Stone', $this->l->msg('Test.presentText', ['Happy Rotter', 'the Philosopher\'s Stone']));
} }
/** /**
* @depends testFetchAMessage * @depends testFetchAMessage
*/ */
function testFetchAMessageWithNamedParameters() { function testFetchAMessageWithNamedParameters() {
$this->assertEquals('Message string "Test.absentText" missing from all loaded language files (en)', Lang::msg('Exception.JKingWeb/Arsse/Lang/Exception.stringMissing', ['msgID' => 'Test.absentText', 'fileList' => 'en'])); $this->assertEquals('Message string "Test.absentText" missing from all loaded language files (en)', $this->l->msg('Exception.JKingWeb/Arsse/Lang/Exception.stringMissing', ['msgID' => 'Test.absentText', 'fileList' => 'en']));
} }
/** /**
* @depends testFetchAMessage * @depends testFetchAMessage
*/ */
function testReloadDefaultStrings() { function testReloadDefaultStrings() {
Lang::set("de", true); $this->l->set("de", true);
Lang::set("en", true); $this->l->set("en", true);
$this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText')); $this->assertEquals('and the Philosopher\'s Stone', $this->l->msg('Test.presentText'));
} }
/** /**
* @depends testFetchAMessage * @depends testFetchAMessage
*/ */
function testReloadGeneralTagAfterSubtag() { function testReloadGeneralTagAfterSubtag() {
Lang::set("en", true); $this->l->set("en", true);
Lang::set("en_us", true); $this->l->set("en_us", true);
$this->assertEquals('and the Sorcerer\'s Stone', Lang::msg('Test.presentText')); $this->assertEquals('and the Sorcerer\'s Stone', $this->l->msg('Test.presentText'));
Lang::set("en", true); $this->l->set("en", true);
$this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText')); $this->assertEquals('and the Philosopher\'s Stone', $this->l->msg('Test.presentText'));
} }
} }

9
tests/REST/NextCloudNews/TestNCNVersionDiscovery.php

@ -9,13 +9,12 @@ class TestNCNVersionDiscovery extends \PHPUnit\Framework\TestCase {
use Test\Tools; use Test\Tools;
function setUp() { function setUp() {
$conf = new Conf(); $this->clearData();
$this->data = new Test\RuntimeData($conf);
} }
function testFetchVersionList() { function testFetchVersionList() {
$exp = new Response(200, ['apiLevels' => ['v1-2']]); $exp = new Response(200, ['apiLevels' => ['v1-2']]);
$h = new Rest\NextCloudNews\Versions($this->data); $h = new Rest\NextCloudNews\Versions();
$req = new Request("GET", "/"); $req = new Request("GET", "/");
$res = $h->dispatch($req); $res = $h->dispatch($req);
$this->assertEquals($exp, $res); $this->assertEquals($exp, $res);
@ -29,7 +28,7 @@ class TestNCNVersionDiscovery extends \PHPUnit\Framework\TestCase {
function testUseIncorrectMethod() { function testUseIncorrectMethod() {
$exp = new Response(405); $exp = new Response(405);
$h = new Rest\NextCloudNews\Versions($this->data); $h = new Rest\NextCloudNews\Versions();
$req = new Request("POST", "/"); $req = new Request("POST", "/");
$res = $h->dispatch($req); $res = $h->dispatch($req);
$this->assertEquals($exp, $res); $this->assertEquals($exp, $res);
@ -37,7 +36,7 @@ class TestNCNVersionDiscovery extends \PHPUnit\Framework\TestCase {
function testUseIncorrectPath() { function testUseIncorrectPath() {
$exp = new Response(404); $exp = new Response(404);
$h = new Rest\NextCloudNews\Versions($this->data); $h = new Rest\NextCloudNews\Versions();
$req = new Request("GET", "/ook"); $req = new Request("GET", "/ook");
$res = $h->dispatch($req); $res = $h->dispatch($req);
$this->assertEquals($exp, $res); $this->assertEquals($exp, $res);

119
tests/User/TestAuthorization.php

@ -46,53 +46,58 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
protected $data; protected $data;
function setUp(string $drv = Test\User\DriverInternalMock::class, string $db = null) { function setUp(string $drv = Test\User\DriverInternalMock::class, string $db = null) {
$this->clearData();
$conf = new Conf(); $conf = new Conf();
$conf->userDriver = $drv; $conf->userDriver = $drv;
$conf->userAuthPreferHTTP = true; $conf->userAuthPreferHTTP = true;
$conf->userComposeNames = true; $conf->userComposeNames = true;
$this->data = new Test\RuntimeData($conf); Data::$conf = $conf;
if($db !== null) { if($db !== null) {
$this->data->db = new $db($this->data); Data::$db = new $db();
} }
$this->data->user = new User($this->data); Data::$user = new User();
$this->data->user->authorizationEnabled(false); Data::$user->authorizationEnabled(false);
foreach(self::USERS as $user => $level) { foreach(self::USERS as $user => $level) {
$this->data->user->add($user, ""); Data::$user->add($user, "");
$this->data->user->rightsSet($user, $level); Data::$user->rightsSet($user, $level);
} }
$this->data->user->authorizationEnabled(true); Data::$user->authorizationEnabled(true);
}
function tearDown() {
$this->clearData();
} }
function testSelfActionLogic() { function testSelfActionLogic() {
foreach(array_keys(self::USERS) as $user) { foreach(array_keys(self::USERS) as $user) {
$this->data->user->auth($user, ""); Data::$user->auth($user, "");
// users should be able to do basic actions for themselves // users should be able to do basic actions for themselves
$this->assertTrue($this->data->user->authorize($user, "userExists"), "User $user could not act for themselves."); $this->assertTrue(Data::$user->authorize($user, "userExists"), "User $user could not act for themselves.");
$this->assertTrue($this->data->user->authorize($user, "userRemove"), "User $user could not act for themselves."); $this->assertTrue(Data::$user->authorize($user, "userRemove"), "User $user could not act for themselves.");
} }
} }
function testRegularUserLogic() { function testRegularUserLogic() {
foreach(self::USERS as $actor => $rights) { foreach(self::USERS as $actor => $rights) {
if($rights != User\Driver::RIGHTS_NONE) continue; if($rights != User\Driver::RIGHTS_NONE) continue;
$this->data->user->auth($actor, ""); Data::$user->auth($actor, "");
foreach(array_keys(self::USERS) as $affected) { foreach(array_keys(self::USERS) as $affected) {
// regular users should only be able to act for themselves // regular users should only be able to act for themselves
if($actor==$affected) { if($actor==$affected) {
$this->assertTrue($this->data->user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue($this->data->user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else { } else {
$this->assertFalse($this->data->user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse($this->data->user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
} }
// they should never be able to set rights // they should never be able to set rights
foreach(self::LEVELS as $level) { foreach(self::LEVELS as $level) {
$this->assertFalse($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
} }
} }
// they should not be able to list users // they should not be able to list users
foreach(self::DOMAINS as $domain) { foreach(self::DOMAINS as $domain) {
$this->assertFalse($this->data->user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed."); $this->assertFalse(Data::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
} }
} }
} }
@ -101,36 +106,36 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
foreach(self::USERS as $actor => $actorRights) { foreach(self::USERS as $actor => $actorRights) {
if($actorRights != User\Driver::RIGHTS_DOMAIN_MANAGER) continue; if($actorRights != User\Driver::RIGHTS_DOMAIN_MANAGER) continue;
$actorDomain = substr($actor,strrpos($actor,"@")+1); $actorDomain = substr($actor,strrpos($actor,"@")+1);
$this->data->user->auth($actor, ""); Data::$user->auth($actor, "");
foreach(self::USERS as $affected => $affectedRights) { foreach(self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected,strrpos($affected,"@")+1); $affectedDomain = substr($affected,strrpos($affected,"@")+1);
// domain managers should be able to check any user on the same domain // domain managers should be able to check any user on the same domain
if($actorDomain==$affectedDomain) { if($actorDomain==$affectedDomain) {
$this->assertTrue($this->data->user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
} else { } else {
$this->assertFalse($this->data->user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
} }
// they should only be able to act for regular users on the same domain // they should only be able to act for regular users on the same domain
if($actor==$affected || ($actorDomain==$affectedDomain && $affectedRights==User\Driver::RIGHTS_NONE)) { if($actor==$affected || ($actorDomain==$affectedDomain && $affectedRights==User\Driver::RIGHTS_NONE)) {
$this->assertTrue($this->data->user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else { } else {
$this->assertFalse($this->data->user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
} }
// and they should only be able to set their own rights to regular user // and they should only be able to set their own rights to regular user
foreach(self::LEVELS as $level) { foreach(self::LEVELS as $level) {
if($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_DOMAIN_MANAGER])) { if($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_DOMAIN_MANAGER])) {
$this->assertTrue($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else { } else {
$this->assertFalse($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
} }
} }
} }
// they should also be able to list all users on their own domain // they should also be able to list all users on their own domain
foreach(self::DOMAINS as $domain) { foreach(self::DOMAINS as $domain) {
if($domain=="@".$actorDomain) { if($domain=="@".$actorDomain) {
$this->assertTrue($this->data->user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied."); $this->assertTrue(Data::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
} else { } else {
$this->assertFalse($this->data->user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed."); $this->assertFalse(Data::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
} }
} }
} }
@ -140,37 +145,37 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
foreach(self::USERS as $actor => $actorRights) { foreach(self::USERS as $actor => $actorRights) {
if($actorRights != User\Driver::RIGHTS_DOMAIN_ADMIN) continue; if($actorRights != User\Driver::RIGHTS_DOMAIN_ADMIN) continue;
$actorDomain = substr($actor,strrpos($actor,"@")+1); $actorDomain = substr($actor,strrpos($actor,"@")+1);
$this->data->user->auth($actor, ""); Data::$user->auth($actor, "");
$allowed = [User\Driver::RIGHTS_NONE,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN]; $allowed = [User\Driver::RIGHTS_NONE,User\Driver::RIGHTS_DOMAIN_MANAGER,User\Driver::RIGHTS_DOMAIN_ADMIN];
foreach(self::USERS as $affected => $affectedRights) { foreach(self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected,strrpos($affected,"@")+1); $affectedDomain = substr($affected,strrpos($affected,"@")+1);
// domain admins should be able to check any user on the same domain // domain admins should be able to check any user on the same domain
if($actorDomain==$affectedDomain) { if($actorDomain==$affectedDomain) {
$this->assertTrue($this->data->user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
} else { } else {
$this->assertFalse($this->data->user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
} }
// they should be able to act for any user on the same domain who is not a global manager or admin // they should be able to act for any user on the same domain who is not a global manager or admin
if($actorDomain==$affectedDomain && in_array($affectedRights, $allowed)) { if($actorDomain==$affectedDomain && in_array($affectedRights, $allowed)) {
$this->assertTrue($this->data->user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else { } else {
$this->assertFalse($this->data->user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
} }
// they should be able to set rights for any user on their domain who is not a global manager or admin, up to domain admin level // they should be able to set rights for any user on their domain who is not a global manager or admin, up to domain admin level
foreach(self::LEVELS as $level) { foreach(self::LEVELS as $level) {
if($actorDomain==$affectedDomain && in_array($affectedRights, $allowed) && in_array($level, $allowed)) { if($actorDomain==$affectedDomain && in_array($affectedRights, $allowed) && in_array($level, $allowed)) {
$this->assertTrue($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else { } else {
$this->assertFalse($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
} }
} }
} }
// they should also be able to list all users on their own domain // they should also be able to list all users on their own domain
foreach(self::DOMAINS as $domain) { foreach(self::DOMAINS as $domain) {
if($domain=="@".$actorDomain) { if($domain=="@".$actorDomain) {
$this->assertTrue($this->data->user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied."); $this->assertTrue(Data::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
} else { } else {
$this->assertFalse($this->data->user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed."); $this->assertFalse(Data::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
} }
} }
} }
@ -180,29 +185,29 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
foreach(self::USERS as $actor => $actorRights) { foreach(self::USERS as $actor => $actorRights) {
if($actorRights != User\Driver::RIGHTS_GLOBAL_MANAGER) continue; if($actorRights != User\Driver::RIGHTS_GLOBAL_MANAGER) continue;
$actorDomain = substr($actor,strrpos($actor,"@")+1); $actorDomain = substr($actor,strrpos($actor,"@")+1);
$this->data->user->auth($actor, ""); Data::$user->auth($actor, "");
foreach(self::USERS as $affected => $affectedRights) { foreach(self::USERS as $affected => $affectedRights) {
$affectedDomain = substr($affected,strrpos($affected,"@")+1); $affectedDomain = substr($affected,strrpos($affected,"@")+1);
// global managers should be able to check any user // global managers should be able to check any user
$this->assertTrue($this->data->user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
// they should only be able to act for regular users // they should only be able to act for regular users
if($actor==$affected || $affectedRights==User\Driver::RIGHTS_NONE) { if($actor==$affected || $affectedRights==User\Driver::RIGHTS_NONE) {
$this->assertTrue($this->data->user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else { } else {
$this->assertFalse($this->data->user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
} }
// and they should only be able to set their own rights to regular user // and they should only be able to set their own rights to regular user
foreach(self::LEVELS as $level) { foreach(self::LEVELS as $level) {
if($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_GLOBAL_MANAGER])) { if($actor==$affected && in_array($level, [User\Driver::RIGHTS_NONE, User\Driver::RIGHTS_GLOBAL_MANAGER])) {
$this->assertTrue($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else { } else {
$this->assertFalse($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
} }
} }
} }
// they should also be able to list all users // they should also be able to list all users
foreach(self::DOMAINS as $domain) { foreach(self::DOMAINS as $domain) {
$this->assertTrue($this->data->user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied."); $this->assertTrue(Data::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
} }
} }
} }
@ -210,17 +215,17 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
function testGlobalAdministratorLogic() { function testGlobalAdministratorLogic() {
foreach(self::USERS as $actor => $actorRights) { foreach(self::USERS as $actor => $actorRights) {
if($actorRights != User\Driver::RIGHTS_GLOBAL_ADMIN) continue; if($actorRights != User\Driver::RIGHTS_GLOBAL_ADMIN) continue;
$this->data->user->auth($actor, ""); Data::$user->auth($actor, "");
// global admins can do anything // global admins can do anything
foreach(self::USERS as $affected => $affectedRights) { foreach(self::USERS as $affected => $affectedRights) {
$this->assertTrue($this->data->user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue($this->data->user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
foreach(self::LEVELS as $level) { foreach(self::LEVELS as $level) {
$this->assertTrue($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} }
} }
foreach(self::DOMAINS as $domain) { foreach(self::DOMAINS as $domain) {
$this->assertTrue($this->data->user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied."); $this->assertTrue(Data::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
} }
} }
} }
@ -228,24 +233,24 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
function testInvalidLevelLogic() { function testInvalidLevelLogic() {
foreach(self::USERS as $actor => $rights) { foreach(self::USERS as $actor => $rights) {
if(in_array($rights, self::LEVELS)) continue; if(in_array($rights, self::LEVELS)) continue;
$this->data->user->auth($actor, ""); Data::$user->auth($actor, "");
foreach(array_keys(self::USERS) as $affected) { foreach(array_keys(self::USERS) as $affected) {
// users with unknown/invalid rights should be treated just like regular users and only be able to act for themselves // users with unknown/invalid rights should be treated just like regular users and only be able to act for themselves
if($actor==$affected) { if($actor==$affected) {
$this->assertTrue($this->data->user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue($this->data->user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied."); $this->assertTrue(Data::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else { } else {
$this->assertFalse($this->data->user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userExists"), "User $actor acted improperly for $affected, but the action was allowed.");
$this->assertFalse($this->data->user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
} }
// they should never be able to set rights // they should never be able to set rights
foreach(self::LEVELS as $level) { foreach(self::LEVELS as $level) {
$this->assertFalse($this->data->user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed."); $this->assertFalse(Data::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
} }
} }
// they should not be able to list users // they should not be able to list users
foreach(self::DOMAINS as $domain) { foreach(self::DOMAINS as $domain) {
$this->assertFalse($this->data->user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed."); $this->assertFalse(Data::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
} }
} }
} }
@ -264,10 +269,10 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
'list' => [], 'list' => [],
]; ];
// try first with a global admin (there should be no exception) // try first with a global admin (there should be no exception)
$this->data->user->auth("gadm@example.com", ""); Data::$user->auth("gadm@example.com", "");
$this->assertCount(0, $this->checkExceptions("user@example.org", $tests)); $this->assertCount(0, $this->checkExceptions("user@example.org", $tests));
// next try with a regular user acting on another user (everything should fail) // next try with a regular user acting on another user (everything should fail)
$this->data->user->auth("user@example.com", ""); Data::$user->auth("user@example.com", "");
$this->assertCount(sizeof($tests), $this->checkExceptions("user@example.org", $tests)); $this->assertCount(sizeof($tests), $this->checkExceptions("user@example.org", $tests));
} }
@ -286,7 +291,7 @@ class TestAuthorization extends \PHPUnit\Framework\TestCase {
// list method does not take an affected user, so do not unshift for that one // list method does not take an affected user, so do not unshift for that one
if($func != "list") array_unshift($args, $user); if($func != "list") array_unshift($args, $user);
try { try {
call_user_func_array(array($this->data->user, $func), $args); call_user_func_array(array(Data::$user, $func), $args);
} catch(User\ExceptionAuthz $e) { } catch(User\ExceptionAuthz $e) {
$err[] = $func; $err[] = $func;
} }

15
tests/User/TestUserInternalDriver.php

@ -9,18 +9,5 @@ class TestUserInternalDriver extends \PHPUnit\Framework\TestCase {
const USER1 = "john.doe@example.com"; const USER1 = "john.doe@example.com";
const USER2 = "jane.doe@example.com"; const USER2 = "jane.doe@example.com";
protected $data; public $drv = User\Internal\Driver::class;
function setUp() {
$drv = User\Internal\Driver::class;
$conf = new Conf();
$conf->userDriver = $drv;
$conf->userAuthPreferHTTP = true;
$this->data = new Test\RuntimeData($conf);
$this->data->db = new Test\User\Database($this->data);
$this->data->user = new User($this->data);
$this->data->user->authorizationEnabled(false);
$_SERVER['PHP_AUTH_USER'] = self::USER1;
$_SERVER['PHP_AUTH_PW'] = "secret";
}
} }

15
tests/User/TestUserMockExternal.php

@ -9,18 +9,5 @@ class TestUserMockExternal extends \PHPUnit\Framework\TestCase {
const USER1 = "john.doe@example.com"; const USER1 = "john.doe@example.com";
const USER2 = "jane.doe@example.com"; const USER2 = "jane.doe@example.com";
protected $data; public $drv = Test\User\DriverExternalMock::class;
function setUp() {
$drv = Test\User\DriverExternalMock::class;
$conf = new Conf();
$conf->userDriver = $drv;
$conf->userAuthPreferHTTP = true;
$this->data = new Test\RuntimeData($conf);
$this->data->db = new Test\User\Database($this->data);
$this->data->user = new User($this->data);
$this->data->user->authorizationEnabled(false);
$_SERVER['PHP_AUTH_USER'] = self::USER1;
$_SERVER['PHP_AUTH_PW'] = "secret";
}
} }

14
tests/User/TestUserMockInternal.php

@ -9,17 +9,9 @@ class TestUserMockInternal extends \PHPUnit\Framework\TestCase {
const USER1 = "john.doe@example.com"; const USER1 = "john.doe@example.com";
const USER2 = "jane.doe@example.com"; const USER2 = "jane.doe@example.com";
protected $data; public $drv = Test\User\DriverInternalMock::class;
function setUp() { function setUpSeries() {
$drv = Test\User\DriverInternalMock::class; Data::$db = null;
$conf = new Conf();
$conf->userDriver = $drv;
$conf->userAuthPreferHTTP = true;
$this->data = new Test\RuntimeData($conf);
$this->data->user = new User($this->data);
$this->data->user->authorizationEnabled(false);
$_SERVER['PHP_AUTH_USER'] = self::USER1;
$_SERVER['PHP_AUTH_PW'] = "secret";
} }
} }

15
tests/lib/Db/Tools.php

@ -3,13 +3,16 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Test\Db; namespace JKingWeb\Arsse\Test\Db;
trait Tools { trait Tools {
function prime(\JKingWeb\Arsse\Db\Driver $drv, array $data): bool { protected $drv;
function prime(array $data): bool {
$drv->begin(); $drv->begin();
foreach($data as $table => $info) { foreach($data as $table => $info) {
$cols = implode(",", array_keys($info['columns'])); $cols = implode(",", array_keys($info['columns']));
$bindings = array_values($info['columns']); $bindings = array_values($info['columns']);
$params = implode(",", array_fill(0, sizeof($info['columns']), "?")); $params = implode(",", array_fill(0, sizeof($info['columns']), "?"));
$s = $drv->prepareArray("INSERT INTO $table($cols) values($params)", $bindings); $s = $this->drv->prepareArray("INSERT INTO $table($cols) values($params)", $bindings);
foreach($info['rows'] as $row) { foreach($info['rows'] as $row) {
$this->assertEquals(1, $s->runArray($row)->changes()); $this->assertEquals(1, $s->runArray($row)->changes());
} }
@ -18,13 +21,17 @@ trait Tools {
return true; return true;
} }
function compare(\JKingWeb\Arsse\Db\Driver $drv, array $expected): bool { function compare(array $expected): bool {
foreach($expected as $table => $info) { foreach($expected as $table => $info) {
$cols = implode(",", array_keys($info['columns'])); $cols = implode(",", array_keys($info['columns']));
foreach($drv->prepare("SELECT $cols from $table")->run() as $num => $row) { foreach($this->drv->prepare("SELECT $cols from $table")->run() as $num => $row) {
$row = array_values($row); $row = array_values($row);
$assertSame($expected[$table]['rows'][$num], $row, "Row $num of table $table does not match expectation."); $assertSame($expected[$table]['rows'][$num], $row, "Row $num of table $table does not match expectation.");
} }
} }
} }
function setUp() {
}
} }

40
tests/lib/Lang/Setup.php

@ -1,16 +1,16 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\Test\Lang; namespace JKingWeb\Arsse\Test\Lang;
use \org\bovigo\vfs\vfsStream, \JKingWeb\Arsse\Lang; use org\bovigo\vfs\vfsStream;
use JKingWeb\Arsse\Lang;
use JKingWeb\Arsse\Data;
trait Setup { trait Setup {
static function setUpBeforeClass() { function setUp() {
// this is required to keep from having exceptions in Lang::msg() in turn calling Lang::msg() and looping
\JKingWeb\Arsse\Lang\Exception::$test = true;
// test files // test files
self::$files = [ $this->files = [
'en.php' => '<?php return ["Test.presentText" => "and the Philosopher\'s Stone"];', 'en.php' => '<?php return ["Test.presentText" => "and the Philosopher\'s Stone"];',
'en_ca.php' => '<?php return ["Test.presentText" => "{0} and {1}"];', 'en_ca.php' => '<?php return ["Test.presentText" => "{0} and {1}"];',
'en_us.php' => '<?php return ["Test.presentText" => "and the Sorcerer\'s Stone"];', 'en_us.php' => '<?php return ["Test.presentText" => "and the Sorcerer\'s Stone"];',
@ -28,22 +28,24 @@ trait Setup {
// unreadable file // unreadable file
'ru.php' => '', 'ru.php' => '',
]; ];
self::$vfs = vfsStream::setup("langtest", 0777, self::$files); $vfs = vfsStream::setup("langtest", 0777, $this->files);
self::$path = self::$vfs->url()."/"; $this->path = $vfs->url()."/";
// set up a file without read access // set up a file without read access
chmod(self::$path."ru.php", 0000); chmod($this->path."ru.php", 0000);
// make the Lang class use the vfs files // make the test Lang class use the vfs files
self::$defaultPath = Lang::$path; $this->l = new Lang($this->path);
Lang::$path = self::$path; // create a mock Lang object to keep exceptions from creating loops
$this->clearData(false);
$m = $this->getMockBuilder(Lang::class)->setMethods(['__invoke'])->getMock();
$m->expects($this->any())->method("__invoke")->with($this->anything(), $this->anything())->will($this->returnValue(""));
Data::$l = $m;
// call the additional setup method if it exists
if(method_exists($this, "setUpSeries")) $this->setUpSeries();
} }
static function tearDownAfterClass() { function tearDown() {
\JKingWeb\Arsse\Lang\Exception::$test = false; $this->clearData(true);
Lang::$path = self::$defaultPath; // call the additional teardiwn method if it exists
self::$path = null; if(method_exists($this, "tearDownSeries")) $this->tearDownSeries();
self::$vfs = null;
self::$files = null;
Lang::set("", true);
Lang::set(Lang::DEFAULT);
} }
} }

13
tests/lib/RuntimeData.php

@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\Test;
class RuntimeData extends \JKingWeb\Arsse\RuntimeData {
public $conf;
public $db;
public $user;
public function __construct(\JKingWeb\Arsse\Conf $conf = null) {
$this->conf = $conf;
}
}

15
tests/lib/Tools.php

@ -1,7 +1,8 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\Test; namespace JKingWeb\Arsse\Test;
use \JKingWeb\Arsse\Exception; use JKingWeb\Arsse\Exception;
use JKingWeb\Arsse\Data;
trait Tools { trait Tools {
function assertException(string $msg, string $prefix = "", string $type = "Exception") { function assertException(string $msg, string $prefix = "", string $type = "Exception") {
@ -15,4 +16,16 @@ trait Tools {
$this->expectException($class); $this->expectException($class);
$this->expectExceptionCode($code); $this->expectExceptionCode($code);
} }
function clearData(bool $loadLang = true): bool {
$r = new \ReflectionClass(\JKingWeb\Arsse\Data::class);
$props = array_keys($r->getStaticProperties());
foreach($props as $prop) {
Data::$$prop = null;
}
if($loadLang) {
Data::$l = new \JKingWeb\Arsse\Lang();
}
return true;
}
} }

107
tests/lib/User/CommonTests.php

@ -1,80 +1,105 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\Test\User; namespace JKingWeb\Arsse\Test\User;
use JKingWeb\Arsse\Data;
use JKingWeb\Arsse\Conf;
use JKingWeb\Arsse\User;
use JKingWeb\Arsse\User\Driver; use JKingWeb\Arsse\User\Driver;
trait CommonTests { trait CommonTests {
function setUp() {
$this->clearData();
$conf = new Conf();
$conf->userDriver = $this->drv;
$conf->userAuthPreferHTTP = true;
Data::$conf = $conf;
Data::$db = new Database();
Data::$user = new User();
Data::$user->authorizationEnabled(false);
$_SERVER['PHP_AUTH_USER'] = self::USER1;
$_SERVER['PHP_AUTH_PW'] = "secret";
// call the additional setup method if it exists
if(method_exists($this, "setUpSeries")) $this->setUpSeries();
}
function tearDown() {
$this->clearData();
// call the additional teardiwn method if it exists
if(method_exists($this, "tearDownSeries")) $this->tearDownSeries();
}
function testListUsers() { function testListUsers() {
$this->assertCount(0,$this->data->user->list()); $this->assertCount(0,Data::$user->list());
} }
function testCheckIfAUserDoesNotExist() { function testCheckIfAUserDoesNotExist() {
$this->assertFalse($this->data->user->exists(self::USER1)); $this->assertFalse(Data::$user->exists(self::USER1));
} }
function testAddAUser() { function testAddAUser() {
$this->data->user->add(self::USER1, ""); Data::$user->add(self::USER1, "");
$this->assertCount(1,$this->data->user->list()); $this->assertCount(1,Data::$user->list());
} }
function testCheckIfAUserDoesExist() { function testCheckIfAUserDoesExist() {
$this->data->user->add(self::USER1, ""); Data::$user->add(self::USER1, "");
$this->assertTrue($this->data->user->exists(self::USER1)); $this->assertTrue(Data::$user->exists(self::USER1));
} }
function testAddADuplicateUser() { function testAddADuplicateUser() {
$this->data->user->add(self::USER1, ""); Data::$user->add(self::USER1, "");
$this->assertException("alreadyExists", "User"); $this->assertException("alreadyExists", "User");
$this->data->user->add(self::USER1, ""); Data::$user->add(self::USER1, "");
} }
function testAddMultipleUsers() { function testAddMultipleUsers() {
$this->data->user->add(self::USER1, ""); Data::$user->add(self::USER1, "");
$this->data->user->add(self::USER2, ""); Data::$user->add(self::USER2, "");
$this->assertCount(2,$this->data->user->list()); $this->assertCount(2,Data::$user->list());
} }
function testRemoveAUser() { function testRemoveAUser() {
$this->data->user->add(self::USER1, ""); Data::$user->add(self::USER1, "");
$this->assertCount(1,$this->data->user->list()); $this->assertCount(1,Data::$user->list());
$this->data->user->remove(self::USER1); Data::$user->remove(self::USER1);
$this->assertCount(0,$this->data->user->list()); $this->assertCount(0,Data::$user->list());
} }
function testRemoveAMissingUser() { function testRemoveAMissingUser() {
$this->assertException("doesNotExist", "User"); $this->assertException("doesNotExist", "User");
$this->data->user->remove(self::USER1); Data::$user->remove(self::USER1);
} }
function testAuthenticateAUser() { function testAuthenticateAUser() {
$_SERVER['PHP_AUTH_USER'] = self::USER1; $_SERVER['PHP_AUTH_USER'] = self::USER1;
$_SERVER['PHP_AUTH_PW'] = "secret"; $_SERVER['PHP_AUTH_PW'] = "secret";
$this->data->user->add(self::USER1, "secret"); Data::$user->add(self::USER1, "secret");
$this->data->user->add(self::USER2, ""); Data::$user->add(self::USER2, "");
$this->assertTrue($this->data->user->auth()); $this->assertTrue(Data::$user->auth());
$this->assertTrue($this->data->user->auth(self::USER1, "secret")); $this->assertTrue(Data::$user->auth(self::USER1, "secret"));
$this->assertFalse($this->data->user->auth(self::USER1, "superman")); $this->assertFalse(Data::$user->auth(self::USER1, "superman"));
$this->assertTrue($this->data->user->auth(self::USER2, "")); $this->assertTrue(Data::$user->auth(self::USER2, ""));
} }
function testChangeAPassword() { function testChangeAPassword() {
$this->data->user->add(self::USER1, "secret"); Data::$user->add(self::USER1, "secret");
$this->assertEquals("superman", $this->data->user->passwordSet(self::USER1, "superman")); $this->assertEquals("superman", Data::$user->passwordSet(self::USER1, "superman"));
$this->assertTrue($this->data->user->auth(self::USER1, "superman")); $this->assertTrue(Data::$user->auth(self::USER1, "superman"));
$this->assertFalse($this->data->user->auth(self::USER1, "secret")); $this->assertFalse(Data::$user->auth(self::USER1, "secret"));
$this->assertEquals("", $this->data->user->passwordSet(self::USER1, "")); $this->assertEquals("", Data::$user->passwordSet(self::USER1, ""));
$this->assertTrue($this->data->user->auth(self::USER1, "")); $this->assertTrue(Data::$user->auth(self::USER1, ""));
$this->assertEquals($this->data->conf->userTempPasswordLength, strlen($this->data->user->passwordSet(self::USER1))); $this->assertEquals(Data::$conf->userTempPasswordLength, strlen(Data::$user->passwordSet(self::USER1)));
} }
function testChangeAPasswordForAMissingUser() { function testChangeAPasswordForAMissingUser() {
$this->assertException("doesNotExist", "User"); $this->assertException("doesNotExist", "User");
$this->data->user->passwordSet(self::USER1, "superman"); Data::$user->passwordSet(self::USER1, "superman");
} }
function testGetThePropertiesOfAUser() { function testGetThePropertiesOfAUser() {
$this->data->user->add(self::USER1, "secret"); Data::$user->add(self::USER1, "secret");
$p = $this->data->user->propertiesGet(self::USER1); $p = Data::$user->propertiesGet(self::USER1);
$this->assertArrayHasKey('id', $p); $this->assertArrayHasKey('id', $p);
$this->assertArrayHasKey('name', $p); $this->assertArrayHasKey('name', $p);
$this->assertArrayHasKey('domain', $p); $this->assertArrayHasKey('domain', $p);
@ -97,22 +122,22 @@ trait CommonTests {
'domain' => 'example.com', 'domain' => 'example.com',
'rights' => Driver::RIGHTS_NONE, 'rights' => Driver::RIGHTS_NONE,
]; ];
$this->data->user->add(self::USER1, "secret"); Data::$user->add(self::USER1, "secret");
$this->data->user->propertiesSet(self::USER1, $pSet); Data::$user->propertiesSet(self::USER1, $pSet);
$p = $this->data->user->propertiesGet(self::USER1); $p = Data::$user->propertiesGet(self::USER1);
$this->assertArraySubset($pGet, $p); $this->assertArraySubset($pGet, $p);
$this->assertArrayNotHasKey('password', $p); $this->assertArrayNotHasKey('password', $p);
$this->assertFalse($this->data->user->auth(self::USER1, "superman")); $this->assertFalse(Data::$user->auth(self::USER1, "superman"));
} }
function testGetTheRightsOfAUser() { function testGetTheRightsOfAUser() {
$this->data->user->add(self::USER1, ""); Data::$user->add(self::USER1, "");
$this->assertEquals(Driver::RIGHTS_NONE, $this->data->user->rightsGet(self::USER1)); $this->assertEquals(Driver::RIGHTS_NONE, Data::$user->rightsGet(self::USER1));
} }
function testSetTheRightsOfAUser() { function testSetTheRightsOfAUser() {
$this->data->user->add(self::USER1, ""); Data::$user->add(self::USER1, "");
$this->data->user->rightsSet(self::USER1, Driver::RIGHTS_GLOBAL_ADMIN); Data::$user->rightsSet(self::USER1, Driver::RIGHTS_GLOBAL_ADMIN);
$this->assertEquals(Driver::RIGHTS_GLOBAL_ADMIN, $this->data->user->rightsGet(self::USER1)); $this->assertEquals(Driver::RIGHTS_GLOBAL_ADMIN, Data::$user->rightsGet(self::USER1));
} }
} }

30
tests/lib/User/Database.php

@ -1,6 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\Test\User; namespace JKingWeb\Arsse\Test\User;
use JKingWeb\Arsse\Data;
use JKingWeb\Arsse\User\Driver; use JKingWeb\Arsse\User\Driver;
use JKingWeb\Arsse\User\Exception; use JKingWeb\Arsse\User\Exception;
use JKingWeb\Arsse\User\ExceptionAuthz; use JKingWeb\Arsse\User\ExceptionAuthz;
@ -10,48 +11,47 @@ class Database extends DriverSkeleton {
public $db = []; public $db = [];
public function __construct(\JKingWeb\Arsse\RuntimeData $data) { public function __construct() {
$this->data = $data;
} }
function userExists(string $user): bool { function userExists(string $user): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
return parent::userExists($user); return parent::userExists($user);
} }
function userAdd(string $user, string $password = null): string { function userAdd(string $user, string $password = null): string {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if($this->userExists($user)) throw new Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]); if($this->userExists($user)) throw new Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]);
if($password===null) $password = (new PassGen)->length($this->data->conf->userTempPasswordLength)->get(); if($password===null) $password = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
return parent::userAdd($user, $password); return parent::userAdd($user, $password);
} }
function userRemove(string $user): bool { function userRemove(string $user): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return parent::userRemove($user); return parent::userRemove($user);
} }
function userList(string $domain = null): array { function userList(string $domain = null): array {
if($domain===null) { if($domain===null) {
if(!$this->data->user->authorize("", __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]); if(!Data::$user->authorize("", __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]);
return parent::userList(); return parent::userList();
} else { } else {
$suffix = '@'.$domain; $suffix = '@'.$domain;
if(!$this->data->user->authorize($suffix, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]); if(!Data::$user->authorize($suffix, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
return parent::userList($domain); return parent::userList($domain);
} }
} }
function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): string { function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): string {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
if($newPassword===null) $newPassword = (new PassGen)->length($this->data->conf->userTempPasswordLength)->get(); if($newPassword===null) $newPassword = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
return parent::userPasswordSet($user, $newPassword); return parent::userPasswordSet($user, $newPassword);
} }
function userPropertiesGet(string $user): array { function userPropertiesGet(string $user): array {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
$out = parent::userPropertiesGet($user); $out = parent::userPropertiesGet($user);
unset($out['password']); unset($out['password']);
@ -59,20 +59,20 @@ class Database extends DriverSkeleton {
} }
function userPropertiesSet(string $user, array $properties): array { function userPropertiesSet(string $user, array $properties): array {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
parent::userPropertiesSet($user, $properties); parent::userPropertiesSet($user, $properties);
return $this->userPropertiesGet($user); return $this->userPropertiesGet($user);
} }
function userRightsGet(string $user): int { function userRightsGet(string $user): int {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return parent::userRightsGet($user); return parent::userRightsGet($user);
} }
function userRightsSet(string $user, int $level): bool { function userRightsSet(string $user, int $level): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return parent::userRightsSet($user, $level); return parent::userRightsSet($user, $level);
} }
@ -80,7 +80,7 @@ class Database extends DriverSkeleton {
// specific to mock database // specific to mock database
function userPasswordGet(string $user): string { function userPasswordGet(string $user): string {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); if(!Data::$user->authorize($user, __FUNCTION__)) throw new ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return $this->db[$user]['password']; return $this->db[$user]['password'];
} }

13
tests/lib/User/DriverExternalMock.php

@ -1,6 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\Test\User; namespace JKingWeb\Arsse\Test\User;
use JKingWeb\Arsse\Data;
use JKingWeb\Arsse\User\Driver; use JKingWeb\Arsse\User\Driver;
use JKingWeb\Arsse\User\Exception; use JKingWeb\Arsse\User\Exception;
use PasswordGenerator\Generator as PassGen; use PasswordGenerator\Generator as PassGen;
@ -8,7 +9,6 @@ use PasswordGenerator\Generator as PassGen;
class DriverExternalMock extends DriverSkeleton implements Driver { class DriverExternalMock extends DriverSkeleton implements Driver {
public $db = []; public $db = [];
protected $data;
protected $functions = [ protected $functions = [
"auth" => Driver::FUNC_EXTERNAL, "auth" => Driver::FUNC_EXTERNAL,
"userList" => Driver::FUNC_EXTERNAL, "userList" => Driver::FUNC_EXTERNAL,
@ -22,10 +22,6 @@ class DriverExternalMock extends DriverSkeleton implements Driver {
"userRightsSet" => Driver::FUNC_EXTERNAL, "userRightsSet" => Driver::FUNC_EXTERNAL,
]; ];
static public function create(\JKingWeb\Arsse\RuntimeData $data): Driver {
return new static($data);
}
static public function driverName(): string { static public function driverName(): string {
return "Mock External Driver"; return "Mock External Driver";
} }
@ -39,8 +35,7 @@ class DriverExternalMock extends DriverSkeleton implements Driver {
} }
} }
public function __construct(\JKingWeb\Arsse\RuntimeData $data) { public function __construct() {
$this->data = $data;
} }
function auth(string $user, string $password): bool { function auth(string $user, string $password): bool {
@ -56,7 +51,7 @@ class DriverExternalMock extends DriverSkeleton implements Driver {
function userAdd(string $user, string $password = null): string { function userAdd(string $user, string $password = null): string {
if($this->userExists($user)) throw new Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]); if($this->userExists($user)) throw new Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]);
if($password===null) $password = (new PassGen)->length($this->data->conf->userTempPasswordLength)->get(); if($password===null) $password = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
return parent::userAdd($user, $password); return parent::userAdd($user, $password);
} }
@ -75,7 +70,7 @@ class DriverExternalMock extends DriverSkeleton implements Driver {
function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): string { function userPasswordSet(string $user, string $newPassword = null, string $oldPassword = null): string {
if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); if(!$this->userExists($user)) throw new Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
if($newPassword===null) $newPassword = (new PassGen)->length($this->data->conf->userTempPasswordLength)->get(); if($newPassword===null) $newPassword = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
return parent::userPasswordSet($user, $newPassword); return parent::userPasswordSet($user, $newPassword);
} }

8
tests/lib/User/DriverInternalMock.php

@ -6,7 +6,6 @@ use JKingWeb\Arsse\User\Driver;
class DriverInternalMock extends Database implements Driver { class DriverInternalMock extends Database implements Driver {
public $db = []; public $db = [];
protected $data;
protected $functions = [ protected $functions = [
"auth" => Driver::FUNC_INTERNAL, "auth" => Driver::FUNC_INTERNAL,
"userList" => Driver::FUNC_INTERNAL, "userList" => Driver::FUNC_INTERNAL,
@ -20,10 +19,6 @@ class DriverInternalMock extends Database implements Driver {
"userRightsSet" => Driver::FUNC_INTERNAL, "userRightsSet" => Driver::FUNC_INTERNAL,
]; ];
static public function create(\JKingWeb\Arsse\RuntimeData $data): Driver {
return new static($data);
}
static public function driverName(): string { static public function driverName(): string {
return "Mock Internal Driver"; return "Mock Internal Driver";
} }
@ -37,8 +32,7 @@ class DriverInternalMock extends Database implements Driver {
} }
} }
public function __construct(\JKingWeb\Arsse\RuntimeData $data) { public function __construct() {
$this->data = $data;
} }
function auth(string $user, string $password): bool { function auth(string $user, string $password): bool {

1
tests/lib/User/DriverSkeleton.php

@ -10,7 +10,6 @@ use PasswordGenerator\Generator as PassGen;
abstract class DriverSkeleton { abstract class DriverSkeleton {
protected $db = []; protected $db = [];
protected $data;
function userExists(string $user): bool { function userExists(string $user): bool {
return array_key_exists($user, $this->db); return array_key_exists($user, $this->db);

7
tests/phpunit.xml

@ -10,10 +10,13 @@
beStrictAboutTestSize="true" beStrictAboutTestSize="true"
stopOnError="true"> stopOnError="true">
<testsuite name="Localization and exceptions"> <testsuite name="Exceptions">
<file>Exception/TestException.php</file>
</testsuite>
<testsuite name="Localization">
<file>Lang/TestLang.php</file> <file>Lang/TestLang.php</file>
<file>Lang/TestLangComplex.php</file> <file>Lang/TestLangComplex.php</file>
<file>Lang/TestException.php</file>
<file>Lang/TestLangErrors.php</file> <file>Lang/TestLangErrors.php</file>
</testsuite> </testsuite>

19
tests/test.php

@ -3,6 +3,7 @@ namespace JKingWeb\Arsse;
const INSTALL = true; const INSTALL = true;
require_once "../bootstrap.php"; require_once "../bootstrap.php";
$user = "john.doe@example.com"; $user = "john.doe@example.com";
$pass = "secret"; $pass = "secret";
$_SERVER['PHP_AUTH_USER'] = $user; $_SERVER['PHP_AUTH_USER'] = $user;
@ -10,13 +11,13 @@ $_SERVER['PHP_AUTH_PW'] = $pass;
$conf = new Conf(); $conf = new Conf();
$conf->dbSQLite3File = ":memory:"; $conf->dbSQLite3File = ":memory:";
$conf->userAuthPreferHTTP = true; $conf->userAuthPreferHTTP = true;
$data = new RuntimeData($conf); Data::load($conf);
$data->db->schemaUpdate(); Data::$db->schemaUpdate();
$data->user->add($user, $pass); Data::$user->add($user, $pass);
$data->user->auth(); Data::$user->auth();
$data->user->authorizationEnabled(false); Data::$user->authorizationEnabled(false);
$data->user->rightsSet($user, User\Driver::RIGHTS_GLOBAL_ADMIN); Data::$user->rightsSet($user, User\Driver::RIGHTS_GLOBAL_ADMIN);
$data->user->authorizationEnabled(true); Data::$user->authorizationEnabled(true);
$data->db->folderAdd($user, ['name' => 'ook']); Data::$db->folderAdd($user, ['name' => 'ook']);
$data->db->subscriptionAdd($user, "http://www.tbray.org/ongoing/ongoing.atom"); Data::$db->subscriptionAdd($user, "http://www.tbray.org/ongoing/ongoing.atom");
Loading…
Cancel
Save