/** Class for loading, saving, and querying configuration
*
*
* The Conf class serves both as a means of importing and querying configuration information, as well as a source for default parameters when a configuration file does not specify a value.
* All public properties are configuration parameters that may be set by the server administrator. */
class Conf {
@ -57,50 +57,50 @@ class Conf {
public $purgeFeeds = "PT24H";
/** @var string When to delete an unstarred article in the database after it has been marked read by all users, as an ISO 8601 duration (default: 7 days; empty string for never)
/** @var string When to delete an unstarred article in the database regardless of its read state, as an ISO 8601 duration (default: 21 days; empty string for never)
* @param string $import_file Optional file to read configuration data from
* @see self::importFile() */
public function __construct(string $import_file = "") {
if($import_file != "") {
if($import_file != "") {
$this->importFile($import_file);
}
}
/** Layers configuration data from a file into an existing object
/** Layers configuration data from a file into an existing object
*
* The file must be a PHP script which return an array with keys that match the properties of the Conf class. Malformed files will throw an exception; unknown keys are silently ignored. Files may be imported is succession, though this is not currently used.
* @param string $file Full path and file name for the file to import */
public function importFile(string $file): self {
if(!file_exists($file)) {
if(!file_exists($file)) {
throw new Conf\Exception("fileMissing", $file);
} elseif(!is_readable($file)) {
} elseif(!is_readable($file)) {
throw new Conf\Exception("fileUnreadable", $file);
}
try {
ob_start();
$arr = (@include $file);
} catch(\Throwable $e) {
} catch(\Throwable $e) {
$arr = null;
} finally {
ob_end_clean();
}
if(!is_array($arr)) {
if(!is_array($arr)) {
throw new Conf\Exception("fileCorrupt", $file);
}
return $this->import($arr);
}
/** Layers configuration data from an associative array into an existing object
/** Layers configuration data from an associative array into an existing object
*
* The input array must have keys that match the properties of the Conf class; unknown keys are silently ignored. Arrays may be imported is succession, though this is not currently used.
* @param mixed[] $arr Array of configuration parameters to export */
public function import(array $arr): self {
foreach($arr as $key => $value) {
foreach($arr as $key => $value) {
$this->$key = $value;
}
return $this;
@ -112,13 +112,13 @@ class Conf {
$ref = new self;
$out = [];
$conf = new \ReflectionObject($this);
foreach($conf->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
foreach($conf->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
$name = $prop->name;
// add the property to the output if the value is scalar and either:
// check if a folder by the same name already exists, because nulls are wonky in SQL
// FIXME: How should folder name be compared? Should a Unicode normalization be applied before comparison and insertion?
if($this->db->prepare("SELECT count(*) from arsse_folders where owner is ? and parent is ? and name is ?", "str", "int", "str")->run($user, $parent, $data['name'])->getValue() > 0) {
if($this->db->prepare("SELECT count(*) from arsse_folders where owner is ? and parent is ? and name is ?", "str", "int", "str")->run($user, $parent, $data['name'])->getValue() > 0) {
throw new Db\ExceptionInput("constraintViolation"); // FIXME: There needs to be a practical message here
}
// actually perform the insert (!)
@ -256,17 +257,17 @@ class Database {
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.
// check to make sure the target folder name/location would not create a duplicate (we must do this check because null is not distinct in SQL)
$existing = $this->db->prepare("SELECT id from arsse_folders where owner is ? and parent is ? and name is ?", "str", "int", "str")->run($user, $data['parent'], $data['name'])->getValue();
if(!is_null($existing) && $existing != $id) {
if(!is_null($existing) && $existing != $id) {
throw new Db\ExceptionInput("constraintViolation"); // FIXME: There needs to be a practical message here
}
$valid = [
@ -327,32 +328,32 @@ class Database {
}
protected function folderValidateId(string $user, int $id = null, int $parent = null, bool $subject = false): array {
if(is_null($id)) {
if(is_null($id)) {
// if no ID is specified this is a no-op, unless a parent is specified, which is always a circular dependence (the root cannot be moved)
// if we're moving a folder to a new parent, check that the parent is valid
if(!is_null($parent)) {
if(!is_null($parent)) {
// make sure both that the parent exists, and that the parent is not either the folder itself or one of its children (a circular dependence)
$p = $this->db->prepare(
"WITH 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) ".
"SELECT id,(id not in (select id from folders)) as valid from arsse_folders where owner is ? and id is ?",
"str", "int", "str", "int"
)->run($user, $id, $user, $parent)->getRow();
if(!$p) {
if(!$p) {
// if the parent doesn't exist or doesn't below to the user, throw an exception
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// check to see if the feed exists
$feedID = $this->db->prepare("SELECT id from arsse_feeds where url is ? and username is ? and password is ?", "str", "str", "str")->run($url, $fetchUser, $fetchPassword)->getValue();
if(is_null($feedID)) {
if(is_null($feedID)) {
// if the feed doesn't exist add it to the database; we do this unconditionally so as to lock SQLite databases for as little time as possible
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// create a complex query
@ -415,11 +416,11 @@ class Database {
$q->setCTE("user(user)", "SELECT ?", "str", $user); // the subject user; this way we only have to pass it to prepare() once
// topmost folders belonging to the user
$q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders join user on owner is user where parent is null union select id,top from arsse_folders join topmost on parent=f_id");
if(!is_null($id)) {
if(!is_null($id)) {
// this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder
// if an ID is specified, add a suitable WHERE condition and bindings
$q->setWhere("arsse_subscriptions.id is ?", "int", $id);
} elseif(!is_null($folder)) {
} elseif(!is_null($folder)) {
// if a folder is specified, make sure it exists
$this->folderValidateId($user, $folder);
// if it does exist, add a common table expression to list it and its children so that we select from the entire subtree
@ -431,50 +432,50 @@ class Database {
}
public function subscriptionRemove(string $user, int $id): bool {
$feeds = $this->db->query("SELECT id from arsse_feeds where next_fetch <= CURRENT_TIMESTAMP")->getAll();
return array_column($feeds,'id');
return array_column($feeds,'id');
}
public function feedUpdate(int $feedID, bool $throwError = false): bool {
$tr = $this->db->begin();
// check to make sure the feed exists
$f = $this->db->prepare("SELECT url, username, password, modified, etag, err_count, scrape FROM arsse_feeds where id is ?", "int")->run($feedID)->getRow();
// if the feed hasn't changed, just compute the next fetch time and record it
$this->db->prepare("UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ? WHERE id is ?", 'datetime', 'int')->run($feed->nextFetch, $feedID);
$tr->commit();
@ -528,38 +529,38 @@ class Database {
} catch (Feed\Exception $e) {
// update the database with the resultant error and the next fetch time, incrementing the error count
$this->db->prepare(
"UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ?, err_count = err_count + 1, err_msg = ? WHERE id is ?",
"UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ?, err_count = err_count + 1, err_msg = ? WHERE id is ?",
$qInsertEnclosure = $this->db->prepare("INSERT INTO arsse_enclosures(article,url,type) values(?,?,?)", 'int', 'str', 'str');
$qInsertCategory = $this->db->prepare("INSERT INTO arsse_categories(article,name) values(?,?)", 'int', 'str');
$qInsertEdition = $this->db->prepare("INSERT INTO arsse_editions(article) values(?)", 'int');
}
if(sizeof($feed->newItems)) {
if(sizeof($feed->newItems)) {
$qInsertArticle = $this->db->prepare(
"INSERT INTO arsse_articles(url,title,author,published,edited,guid,content,url_title_hash,url_content_hash,title_content_hash,feed) values(?,?,?,?,?,?,?,?,?,?,?)",
// lastly update the feed database itself with updated information.
$this->db->prepare(
"UPDATE arsse_feeds SET url = ?, title = ?, favicon = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ? WHERE id is ?",
"UPDATE arsse_feeds SET url = ?, title = ?, favicon = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ? WHERE id is ?",
'str', 'str', 'str', 'str', 'datetime', 'str', 'datetime', 'int', 'int'
)->run(
$feed->data->feedUrl,
@ -633,7 +634,7 @@ class Database {
$this->db->query("UPDATE arsse_feeds set orphaned = CURRENT_TIMESTAMP where orphaned is null and not exists(SELECT id from arsse_subscriptions where feed is arsse_feeds.id)");
// finally delete feeds that have been orphaned longer than the retention period
$limit = Date::normalize("now");
if(Arsse::$conf->purgeFeeds) {
if(Arsse::$conf->purgeFeeds) {
// if there is a retention period specified, compute it; otherwise feed are deleted immediatelty
public function feedMatchLatest(int $feedID, int $count): Db\Result {
return $this->db->prepare(
"SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? ORDER BY modified desc, id desc limit ?",
"SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? ORDER BY modified desc, id desc limit ?",
"SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? and (guid in($cId) or url_title_hash in($cHashUT) or url_content_hash in($cHashUC) or title_content_hash in($cHashTC))",
"SELECT id, edited, guid, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? and (guid in($cId) or url_title_hash in($cHashUT) or url_content_hash in($cHashUC) or title_content_hash in($cHashTC))",
// add a basic CTE that will join in only the requested subscription
$q->setCTE("subscribed_feeds(id,sub)", "SELECT ?,?", ["int","int"], [$id,$context->subscription], "join subscribed_feeds on feed is subscribed_feeds.id");
} elseif($context->folder()) {
} elseif($context->folder()) {
// if a folder is specified, make sure it exists
$this->folderValidateId($user, $context->folder);
// if it does exist, add a common table expression to list it and its children so that we select from the entire subtree
@ -818,18 +819,18 @@ class Database {
// otherwise add a CTE for all the user's subscriptions
$q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user is owner", [], [], "join subscribed_feeds on feed is subscribed_feeds.id");
}
if($context->edition()) {
if($context->edition()) {
// if an edition is specified, filter for its previously identified article
$q->setWhere("arsse_articles.id is ?", "int", $edition['article']);
} elseif($context->article()) {
} elseif($context->article()) {
// if an article is specified, filter for it (it has already been validated above)
$q->setWhere("arsse_articles.id is ?", "int", $context->article);
}
if($context->editions()) {
if($context->editions()) {
// if multiple specific editions have been requested, prepare a CTE to list them and their articles
if(!$context->editions) {
if(!$context->editions) {
throw new Db\ExceptionInput("tooShort", ['field' => "editions", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element
} elseif(sizeof($context->editions) > 50) {
} elseif(sizeof($context->editions) > 50) {
throw new Db\ExceptionInput("tooLong", ['field' => "editions", 'action' => __FUNCTION__, 'max' => 50]); // must not have more than 50 array elements
"SELECT id,(select max(id) from arsse_editions where article is arsse_articles.id) as edition from arsse_articles where arsse_articles.id in ($inParams)",
$inTypes,
$context->articles
@ -858,17 +859,17 @@ class Database {
$q->setCTE("requested_articles(id,edition)", "SELECT 'empty','table' where 1 is 0");
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
return $this->db->prepare("SELECT count(*) from arsse_marks where starred is 1 and subscription in (select id from arsse_subscriptions where owner is ?)", "str")->run($user)->getValue();
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
if(!$context) {
if(!$context) {
$context = new Context;
}
$q = new Query("SELECT max(arsse_editions.id) from arsse_editions left join arsse_articles on article is arsse_articles.id left join arsse_feeds on arsse_articles.feed is arsse_feeds.id");
if($context->subscription()) {
if($context->subscription()) {
// if a subscription is specified, make sure it exists
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
protected static $requirementsMet = false; // whether the Intl extension is loaded
protected $synched = false; // whether the wanted locale is actually loaded (lazy loading is used by default)
protected $wanted = self::DEFAULT; // the currently requested locale
protected $locale = ""; // the currently loaded locale
protected $loaded = []; // the cascade of loaded locale file names
protected $strings = self::REQUIRED; // the loaded locale strings, merged
function __construct(string $path = BASE."locale".DIRECTORY_SEPARATOR) {
public function __construct(string $path = BASE."locale".DIRECTORY_SEPARATOR) {
$this->path = $path;
}
public function set(string $locale, bool $immediate = false): string {
// make sure the Intl extension is loaded
if(!static::$requirementsMet) {
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($locale==$this->wanted) {
if($immediate && !$this->synched) {
if($locale==$this->wanted) {
if($immediate && !$this->synched) {
$this->load();
}
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($locale != "") {
if($locale != "") {
$list = $this->listFiles();
// if the default locale is unavailable, this is (for now) an error
if(!in_array(self::DEFAULT, $list)) {
if(!in_array(self::DEFAULT, $list)) {
throw new Lang\Exception("defaultFileMissing", self::DEFAULT);
}
$this->wanted = $this->match($locale, $list);
@ -52,7 +52,7 @@ class Lang {
}
$this->synched = false;
// load right now if asked to, otherwise load later when actually required
if($immediate) {
if($immediate) {
$this->load();
}
return $this->wanted;
@ -73,29 +73,33 @@ class Lang {
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
return new Response(405, "", "", ["Allow: ".$e->getMessage()]);
}
if(!method_exists($this, $func)) {
if(!method_exists($this, $func)) {
return new Response(501);
}
// dispatch
try {
return $this->$func($req->paths, $data);
} catch(Exception $e) {
} catch(Exception $e) {
// if there was a REST exception return 400
return new Response(400);
} catch(AbstractException $e) {
} catch(AbstractException $e) {
// if there was any other Arsse exception return 500
return new Response(500);
}
@ -133,27 +134,27 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// the first path element is the overall scope of the request
$scope = $url[0];
// any URL components which are only digits should be replaced with "0", for easier comparison (integer segments are IDs, and we don't care about the specific ID)
for($a = 0; $a <sizeof($url);$a++){
if($this->validateInt($url[$a])) {
for($a = 0; $a <sizeof($url);$a++){
if($this->validateInt($url[$a])) {
$url[$a] = "0";
}
}
// normalize the HTTP method to uppercase
$method = strtoupper($method);
// if the scope is not supported, return 501
if(!array_key_exists($scope, $choices)) {
if(!array_key_exists($scope, $choices)) {
throw new Exception501();
}
// we now evaluate the supplied URL against every supported path for the selected scope
// the URL is evaluated as an array so as to avoid decoded escapes turning invalid URLs into valid ones
foreach($choices[$scope] as $path => $funcs) {
foreach($choices[$scope] as $path => $funcs) {
// add the scope to the path to match against and split it
const RIGHTS_GLOBAL_ADMIN = 100; // is completely unrestricted
// returns an instance of a class implementing this interface.
function __construct();
public function __construct();
// returns a human-friendly name for the driver (for display in installer, for example)
static function driverName(): string;
public 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
function driverFunctions(string $function = null);
public function driverFunctions(string $function = null);
// authenticates a user against their name and password
function auth(string $user, string $password): bool;
public function auth(string $user, string $password): bool;
// checks whether a user exists
function userExists(string $user): bool;
public function userExists(string $user): bool;
// adds a user
function userAdd(string $user, string $password = null): string;
public function userAdd(string $user, string $password = null): string;
// removes a user
function userRemove(string $user): bool;
public function userRemove(string $user): bool;
// lists all users
function userList(string $domain = null): array;
public function userList(string $domain = null): array;
// sets a user's password; if the driver does not require the old password, it may be ignored
@ -467,13 +468,13 @@ class TestNCNV1_2 extends Test\AbstractTest {
];
// set up the necessary mocks
Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.com/news.atom")->thenReturn(2112)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call
Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.org/news.atom")->thenReturn(42)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call
Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.org/news.atom")->thenReturn(42)->thenThrow(new ExceptionInput("constraintViolation")); // error on the second call
Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id, (new Context)->subscription(2112))->thenReturn(0);
Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id, (new Context)->subscription(42))->thenReturn(4758915);
Phake::when(Arsse::$db)->editionLatest(Arsse::$user->id, (new Context)->subscription(42))->thenReturn(4758915);
Phake::when(Arsse::$db)->subscriptionPropertiesSet(Arsse::$user->id, 2112, ['folder' => 3])->thenThrow(new ExceptionInput("idMissing")); // folder ID 3 does not exist
Phake::when(Arsse::$db)->subscriptionAdd(Arsse::$user->id, "http://example.net/news.atom")->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.net/news.atom", new \PicoFeed\Client\InvalidUrlException()));
// add the subscriptions
@ -494,7 +495,7 @@ class TestNCNV1_2 extends Test\AbstractTest {
// users should be able to do basic actions for themselves
$this->assertTrue(Arsse::$user->authorize($user, "userExists"), "User $user could not act for themselves.");
@ -84,15 +85,15 @@ class TestAuthorization extends Test\AbstractTest {
}
}
function testRegularUserLogic() {
foreach(self::USERS as $actor => $rights) {
if($rights != User\Driver::RIGHTS_NONE) {
public function testRegularUserLogic() {
foreach(self::USERS as $actor => $rights) {
if($rights != User\Driver::RIGHTS_NONE) {
continue;
}
Arsse::$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
if($actor==$affected) {
if($actor==$affected) {
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
@ -100,41 +101,41 @@ class TestAuthorization extends Test\AbstractTest {
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should never be able to set rights
foreach(self::LEVELS as $level) {
foreach(self::LEVELS as $level) {
$this->assertFalse(Arsse::$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
foreach(self::DOMAINS as $domain) {
foreach(self::DOMAINS as $domain) {
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
@ -142,8 +143,8 @@ class TestAuthorization extends Test\AbstractTest {
}
}
// they should also be able to list all users on their own domain
foreach(self::DOMAINS as $domain) {
if($domain=="@".$actorDomain) {
foreach(self::DOMAINS as $domain) {
if($domain=="@".$actorDomain) {
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
@ -152,31 +153,31 @@ class TestAuthorization extends Test\AbstractTest {
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
@ -184,8 +185,8 @@ class TestAuthorization extends Test\AbstractTest {
}
}
// they should also be able to list all users on their own domain
foreach(self::DOMAINS as $domain) {
if($domain=="@".$actorDomain) {
foreach(self::DOMAINS as $domain) {
if($domain=="@".$actorDomain) {
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
@ -194,26 +195,26 @@ class TestAuthorization extends Test\AbstractTest {
$this->assertTrue(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted properly for $affected settings rights level $level, but the action was denied.");
} else {
$this->assertFalse(Arsse::$user->authorize($affected, "userRightsSet", $level), "User $actor acted improperly for $affected settings rights level $level, but the action was allowed.");
@ -221,41 +222,41 @@ class TestAuthorization extends Test\AbstractTest {
}
}
// they should also be able to list all users
foreach(self::DOMAINS as $domain) {
foreach(self::DOMAINS as $domain) {
$this->assertTrue(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
foreach(self::USERS as $affected => $affectedRights) {
foreach(self::USERS as $affected => $affectedRights) {
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Arsse::$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(Arsse::$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(Arsse::$user->authorize($domain, "userList"), "User $actor properly checked user list for domain '$domain', but the action was denied.");
}
}
}
function testInvalidLevelLogic() {
foreach(self::USERS as $actor => $rights) {
if(in_array($rights, self::LEVELS)) {
public function testInvalidLevelLogic() {
foreach(self::USERS as $actor => $rights) {
if(in_array($rights, self::LEVELS)) {
continue;
}
Arsse::$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
if($actor==$affected) {
if($actor==$affected) {
$this->assertTrue(Arsse::$user->authorize($affected, "userExists"), "User $actor acted properly for $affected, but the action was denied.");
$this->assertTrue(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted properly for $affected, but the action was denied.");
} else {
@ -263,18 +264,18 @@ class TestAuthorization extends Test\AbstractTest {
$this->assertFalse(Arsse::$user->authorize($affected, "userRemove"), "User $actor acted improperly for $affected, but the action was allowed.");
}
// they should never be able to set rights
foreach(self::LEVELS as $level) {
foreach(self::LEVELS as $level) {
$this->assertFalse(Arsse::$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
foreach(self::DOMAINS as $domain) {
foreach(self::DOMAINS as $domain) {
$this->assertFalse(Arsse::$user->authorize($domain, "userList"), "User $actor improperly checked user list for domain '$domain', but the action was allowed.");
}
}
}
function testInternalExceptionLogic() {
public function testInternalExceptionLogic() {
$tests = [
// methods of User class to test, with parameters besides affected user
'exists' => [],
@ -295,7 +296,7 @@ class TestAuthorization extends Test\AbstractTest {