- 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
if($this->db->prepare("DELETE from arsse_users where id is ?", "str")->run($user)->changes() <1)thrownewUser\Exception("doesNotExist",["action"=> __FUNCTION__, "user" => $user]);
if($this->db->prepare("DELETE from arsse_users where id is ?", "str")->run($user)->changes() <1)thrownewUser\Exception("doesNotExist",["action"=> __FUNCTION__, "user" => $user]);
return true;
return true;
}
}
public function userList(string $domain = null): array {
public function userList(string $domain = null): array {
// 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) ";
// 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
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
@ -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 {
$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 {
$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 {
$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 {
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)