From 68422390dae37a7b4968ff8c7771b849e5c4ed2f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 10 Feb 2021 11:24:01 -0500 Subject: [PATCH] Implement CLI for user metadata --- CHANGELOG | 1 + lib/CLI.php | 112 ++++++++++++++++++++++++++++++++++-- lib/User.php | 4 +- tests/cases/CLI/TestCLI.php | 61 ++++++++++++++++++++ tests/lib/AbstractTest.php | 1 + 5 files changed, 172 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0e7cdc67..781625de 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ New features: - Support for the Miniflux protocol (see manual for details) - Support for API level 15 of Tiny Tiny RSS - Support for feed icons in Fever +- Command-line functionality for managing user metadata Bug fixes: - Use icons specified in Atom feeds when available diff --git a/lib/CLI.php b/lib/CLI.php index bc96f468..d40f683e 100644 --- a/lib/CLI.php +++ b/lib/CLI.php @@ -17,8 +17,11 @@ Usage: arsse.php feed refresh arsse.php conf save-defaults [] arsse.php user [list] - arsse.php user add [] + arsse.php user add [] [--admin] arsse.php user remove + arsse.php user show + arsse.php user set + arsse.php user unset arsse.php user set-pass [] [--oldpass=] [--fever] arsse.php user unset-pass @@ -63,11 +66,13 @@ Commands: Prints a list of all existing users, one per line. - user add [] + user add [] [--admin] Adds the user specified by , with the provided password . If no password is specified, a random password will be - generated and printed to standard output. + generated and printed to standard output. The --admin option will make + the user an administrator, which allows them to manage users via the + Miniflux protocol, among other things. user remove @@ -76,6 +81,22 @@ Commands: which the user was subscribed will be retained and refreshed until the configured retention time elapses. + user show + + Displays the metadata of a user in a basic tabular format. See below for + details on the various properties displayed. + + user set + + Sets a user's metadata proprty to the supplied value. See below for + details on the various properties available. + + user unset + + Sets a user's metadata proprty to its default value. See below for + details on the various properties available. What the default value + for a property evaluates to depends on which protocol is used. + user set-pass [] Changes 's password to . If no password is specified, @@ -128,6 +149,65 @@ Commands: The --flat option can be used to omit folders from the export. Some OPML implementations may not support folders, or arbitrary nesting; this option may be used when planning to import into such software. + +User metadata: + + User metadata is primary used by the Miniflux protocol, and most + properties have identical or similar names to those used by Miniflux. + Properties may also affect other protocols, or conversely may have no + effect even when using the Miniflux protocol; this is noted below when + appropriate. + + Booleans accept any of the values true/false, 1/0, yes/no, on/off. + + The following metadata properties exist for each user: + + num + Integer. The numeric identifier of the user. This is assigned at user + creation and is read-only. + admin + Boolean. Whether the user is an administrator. Administrators may + manage other users via the Miniflux protocol, and also may trigger + feed updates manually via the Nextcloud News protocol. + lang + String. The preferred language of the user, as a BCP 47 language tag + e.g. "en-ca". Note that since The Arsse currently only includes + English text it is not used by The Arsse itself, but clients may + use this metadata in protocols which expose it. + tz + String. The time zone of the user, as a tzdata identifier e.g. + "America/Los_Angeles". + root_folder_name + String. The name of the root folder, in protocols which allow it to + be renamed. + sort_asc + Boolean. Whether the user prefers ascending sort order for articles. + Descending order is usually the default, but explicitly setting this + property false will also make a preference for descending order + explicit. + theme + String. The user's preferred theme. This is not used by The Arsse + itself, but clients may use this metadata in protocols which expose + it. + page_size + Integer. The user's preferred pge size when listing articles. This is + not used by The Arsse itself, but clients may use this metadata in + protocols which expose it. + shortcuts + Boolean. Whether to enable keyboard shortcuts. This is not used by + The Arsse itself, but clients may use this metadata in protocols which + expose it. + gestures + Boolean. Whether to enable touch gestures. This is not used by + The Arsse itself, but clients may use this metadata in protocols which + expose it. + reading_time + Boolean. Whether to calculate and display the estimated reading time + for articles. Currently The Arsse does not calculate reading time, so + changing this will likely have no effect. + stylesheet + String. A user CSS stylesheet. This is not used by The Arsse itself, + but clients may use this metadata in protocols which expose it. USAGE_TEXT; protected function usage($prog): string { @@ -215,10 +295,14 @@ USAGE_TEXT; } protected function userManage($args): int { - $cmd = $this->command(["add", "remove", "set-pass", "unset-pass", "list", "auth"], $args); + $cmd = $this->command(["add", "remove", "show", "set", "unset", "set-pass", "unset-pass", "list", "auth"], $args); switch ($cmd) { case "add": - return $this->userAddOrSetPassword("add", $args[""], $args[""]); + $out = $this->userAddOrSetPassword("add", $args[""], $args[""]); + if ($args['--admin']) { + Arsse::$user->propertiesSet($args[""], ['admin' => true]); + } + return $out; case "set-pass": if ($args['--fever']) { $passwd = Arsse::$obj->get(Fever::class)->register($args[""], $args[""]); @@ -239,6 +323,12 @@ USAGE_TEXT; return 0; case "remove": return (int) !Arsse::$user->remove($args[""]); + case "show": + return $this->userShowProperties($args[""]); + case "set": + return (int) !Arsse::$user->propertiesSet($args[""], [$args[""] => $args[""]]); + case "unset": + return (int) !Arsse::$user->propertiesSet($args[""], [$args[""] => null]); case "auth": return $this->userAuthenticate($args[""], $args[""], $args["--fever"]); case "list": @@ -275,4 +365,16 @@ USAGE_TEXT; return 1; } } + + protected function userShowProperties(string $user): int { + $data = Arsse::$user->propertiesGet($user); + $len = array_reduce(array_keys($data), function($carry, $item) { + return max($carry, strlen($item)); + }, 0) + 2; + foreach ($data as $k => $v) { + echo str_pad($k, $len, " "); + echo var_export($v, true).\PHP_EOL; + } + return 0; + } } diff --git a/lib/User.php b/lib/User.php index 04748962..4bf8e36b 100644 --- a/lib/User.php +++ b/lib/User.php @@ -18,14 +18,14 @@ class User { 'admin' => V::T_BOOL, 'lang' => V::T_STRING, 'tz' => V::T_STRING, + 'root_folder_name' => V::T_STRING, 'sort_asc' => V::T_BOOL, 'theme' => V::T_STRING, 'page_size' => V::T_INT, // greater than zero 'shortcuts' => V::T_BOOL, 'gestures' => V::T_BOOL, - 'stylesheet' => V::T_STRING, 'reading_time' => V::T_BOOL, - 'root_folder_name' => V::T_STRING, + 'stylesheet' => V::T_STRING, ]; public const PROPERTIES_LARGE = ["stylesheet"]; diff --git a/tests/cases/CLI/TestCLI.php b/tests/cases/CLI/TestCLI.php index 30237757..dfb4d12e 100644 --- a/tests/cases/CLI/TestCLI.php +++ b/tests/cases/CLI/TestCLI.php @@ -156,6 +156,15 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { ]; } + public function testAddAUserAsAdministrator(): void { + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("add")->willReturn("random password"); + Arsse::$user->method("propertiesSet")->willReturn([]); + Arsse::$user->expects($this->exactly(1))->method("add")->with("jane.doe@example.com", null); + Arsse::$user->expects($this->exactly(1))->method("propertiesSet")->with("jane.doe@example.com", ['admin' => true]); + $this->assertConsole($this->cli, "arsse.php user add jane.doe@example.com --admin", 0, "random password"); + } + /** @dataProvider provideUserAuthentication */ public function testAuthenticateAUser(string $cmd, int $exitStatus, string $output): void { // FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead @@ -357,4 +366,56 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { ["arsse.php import jane.doe@example.com bad.opml --replace --flat", 10603, "bad.opml", "jane.doe@example.com", true, true], ]; } + + public function testShowMetadataOfAUser(): void { + $data = [ + 'num' => 42, + 'admin' => false, + 'lang' => "en-ca", + 'tz' => "America/Toronto", + 'root_folder_name' => null, + 'sort_asc' => true, + 'theme' => null, + 'page_size' => 50, + 'shortcuts' => true, + 'gestures' => null, + 'reading_time' => false, + 'stylesheet' => "body {color:gray}", + ]; + $exp = implode(\PHP_EOL, [ + "num 42", + "admin false", + "lang 'en-ca'", + "tz 'America/Toronto'", + "root_folder_name NULL", + "sort_asc true", + "theme NULL", + "page_size 50", + "shortcuts true", + "gestures NULL", + "reading_time false", + "stylesheet 'body {color:gray}'", + ]); + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("propertiesGet")->willReturn($data); + Arsse::$user->expects($this->once())->method("propertiesGet")->with("john.doe@example.com", true); + $this->assertConsole($this->cli, "arsse.php user show john.doe@example.com", 0, $exp); + } + + /** @dataProvider provideMetadataChanges */ + public function testSetMetadataOfAUser(string $cmd, string $user, array $in, array $out, int $exp): void { + Arsse::$user = $this->createMock(User::class); + Arsse::$user->method("propertiesSet")->willReturn($out); + Arsse::$user->expects($this->once())->method("propertiesSet")->with($user, $in); + $this->assertConsole($this->cli, $cmd, $exp, ""); + } + + public function provideMetadataChanges(): iterable { + return [ + ["arsse.php user set john admin true", "john", ['admin' => "true"], ['admin' => "true"], 0], + ["arsse.php user set john bogus 1", "john", ['bogus' => "1"], [], 1], + ["arsse.php user unset john admin", "john", ['admin' => null], ['admin' => null], 0], + ["arsse.php user unset john bogus", "john", ['bogus' => null], [], 1], + ]; + } } diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index e096ca1e..f807e6bb 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -88,6 +88,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { }, $params, array_keys($params))); } $url = URL::queryAppend($url, (string) $params); + $params = null; } $q = parse_url($url, \PHP_URL_QUERY); if (strlen($q ?? "")) {