aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli
diff options
context:
space:
mode:
authorKasimir Cash <kasimir.cash@outlook.com>2024-02-28 12:23:28 +0000
committerGitHub <noreply@github.com>2024-02-28 13:23:28 +0100
commit4b29e666b06762b4b36438c9370c38bc43121f78 (patch)
tree2b92dcbc5879aa7de8eeec81ccde208f572b3cf0 /cli
parent5de794ee0fbbce2fdf0af3787b9b89299be8698e (diff)
downloadfreshrss-4b29e666b06762b4b36438c9370c38bc43121f78.tar.gz
freshrss-4b29e666b06762b4b36438c9370c38bc43121f78.zip
Command Line Parser Concept (#6099)
* Adds logic for validation * Adds validation to do-install * Adds help to do-install * Adds validation & help to reconfigure * Adds validation to check.translation * Adds validation to manipulate.translation * Small fixes to help texts * Refactors language option validation * Adds default options to validation * Fixes validation with regex * Refactors readAs functions * Updates to new regex validation format * Fixes typing around default values * Adds file extension validation * Restandardises validation & parsing typing around array of strings * Adds NotOneOf validation * Adds ArrayOfString read as * Refactors existing validation * Adds validation throughout cli * Removes unused file * Adds new CL parser with goal of wrapping CLI behaviour * Hides parsing and validation * Rewites CL parser to make better use of classes * Rolls out new parser across CL * Fixes error during unknown option check * Fixes misnamed property calls * Seperates validations into more appropriate locations * Adds common boolean forms to validation * Moves CommandLineParser and Option classes into their own files * Fixes error when validating Int type * Rewrites appendTypedValues -> appendTypedValidValues now filters invalid values from output * Renames -> for clarity * Adds some docs clarifying option defaults and value taking behaviour * Refactors getUsageMessage for readability * Minor formatting changes * Adds tests for CommandLineParser * Adds more tests * Adds minor fixs * Reconfigure now correctly updates config * More fixes to reconfigure * Fixes required files for CommandLineParserTest * Use .php extension for PHP file * PHPStan ignore instead of wrong typing * Refactors to support php 7.4 * Moves away from dynamic properties by adding 'Definintions' to all commands * Renames target to definition for clarity * Stops null from being returned as a valid value in a certain edge case * Adds PHPStan ignore instead of incorrect typing * Refactors tests to take account of new typing solution * Marks file as executable * Draft CLI rework * Finish rewrite as object-oriented * Fix PHPStan ignore and make more strongly typed * Rename class Option to CliOption * Light renaming + anonymous classes --------- Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Diffstat (limited to 'cli')
-rw-r--r--cli/CliOption.php104
-rw-r--r--cli/CliOptionsParser.php247
-rwxr-xr-x[-rw-r--r--]cli/_cli.php119
-rw-r--r--cli/_update-or-create-user.php71
-rwxr-xr-xcli/actualize-user.php28
-rwxr-xr-xcli/check.translation.php53
-rwxr-xr-xcli/create-user.php87
-rwxr-xr-xcli/db-optimize.php25
-rwxr-xr-xcli/delete-user.php36
-rwxr-xr-xcli/do-install.php182
-rwxr-xr-xcli/export-opml-for-user.php25
-rwxr-xr-xcli/export-sqlite-for-user.php33
-rwxr-xr-xcli/export-zip-for-user.php30
-rwxr-xr-xcli/import-for-user.php33
-rwxr-xr-xcli/import-sqlite-for-user.php38
-rwxr-xr-xcli/manipulate.translation.php103
-rwxr-xr-xcli/reconfigure.php207
-rwxr-xr-xcli/update-user.php75
-rwxr-xr-xcli/user-info.php51
19 files changed, 907 insertions, 640 deletions
diff --git a/cli/CliOption.php b/cli/CliOption.php
new file mode 100644
index 000000000..d0eace311
--- /dev/null
+++ b/cli/CliOption.php
@@ -0,0 +1,104 @@
+<?php
+declare(strict_types=1);
+
+final class CliOption {
+ public const VALUE_NONE = 'none';
+ public const VALUE_REQUIRED = 'required';
+ public const VALUE_OPTIONAL = 'optional';
+
+ private string $longAlias;
+ private ?string $shortAlias;
+ private string $valueTaken = self::VALUE_REQUIRED;
+ /** @var array{type:string,isArray:bool} $types */
+ private array $types = ['type' => 'string', 'isArray' => false];
+ private string $optionalValueDefault = '';
+ private ?string $deprecatedAlias = null;
+
+ public function __construct(string $longAlias, ?string $shortAlias = null) {
+ $this->longAlias = $longAlias;
+ $this->shortAlias = $shortAlias;
+ }
+
+ /** Sets this option to be treated as a flag. */
+ public function withValueNone(): self {
+ $this->valueTaken = static::VALUE_NONE;
+ return $this;
+ }
+
+ /** Sets this option to always require a value when used. */
+ public function withValueRequired(): self {
+ $this->valueTaken = static::VALUE_REQUIRED;
+ return $this;
+ }
+
+ /**
+ * Sets this option to accept both values and flag behavior.
+ * @param string $optionalValueDefault When this option is used as a flag it receives this value as input.
+ */
+ public function withValueOptional(string $optionalValueDefault = ''): self {
+ $this->valueTaken = static::VALUE_OPTIONAL;
+ $this->optionalValueDefault = $optionalValueDefault;
+ return $this;
+ }
+
+ public function typeOfString(): self {
+ $this->types = ['type' => 'string', 'isArray' => false];
+ return $this;
+ }
+
+ public function typeOfInt(): self {
+ $this->types = ['type' => 'int', 'isArray' => false];
+ return $this;
+ }
+
+ public function typeOfBool(): self {
+ $this->types = ['type' => 'bool', 'isArray' => false];
+ return $this;
+ }
+
+ public function typeOfArrayOfString(): self {
+ $this->types = ['type' => 'string', 'isArray' => true];
+ return $this;
+ }
+
+ public function deprecatedAs(string $deprecated): self {
+ $this->deprecatedAlias = $deprecated;
+ return $this;
+ }
+
+ public function getValueTaken(): string {
+ return $this->valueTaken;
+ }
+
+ public function getOptionalValueDefault(): string {
+ return $this->optionalValueDefault;
+ }
+
+ public function getDeprecatedAlias(): ?string {
+ return $this->deprecatedAlias;
+ }
+
+ public function getLongAlias(): string {
+ return $this->longAlias;
+ }
+
+ public function getShortAlias(): ?string {
+ return $this->shortAlias;
+ }
+
+ /** @return array{type:string,isArray:bool} */
+ public function getTypes(): array {
+ return $this->types;
+ }
+
+ /** @return string[] */
+ public function getAliases(): array {
+ $aliases = [
+ $this->longAlias,
+ $this->shortAlias,
+ $this->deprecatedAlias,
+ ];
+
+ return array_filter($aliases);
+ }
+}
diff --git a/cli/CliOptionsParser.php b/cli/CliOptionsParser.php
new file mode 100644
index 000000000..be325bd91
--- /dev/null
+++ b/cli/CliOptionsParser.php
@@ -0,0 +1,247 @@
+<?php
+declare(strict_types=1);
+
+abstract class CliOptionsParser {
+ /** @var array<string,CliOption> */
+ private array $options = [];
+ /** @var array<string,array{defaultInput:?string[],required:?bool,aliasUsed:?string,values:?string[]}> */
+ private array $inputs = [];
+ /** @var array<string,string> $errors */
+ public array $errors = [];
+ public string $usage = '';
+
+ public function __construct() {
+ global $argv;
+
+ $this->usage = $this->getUsageMessage($argv[0]);
+
+ $this->parseInput();
+ $this->appendUnknownAliases($argv);
+ $this->appendInvalidValues();
+ $this->appendTypedValidValues();
+ }
+
+ private function parseInput(): void {
+ $getoptInputs = $this->getGetoptInputs();
+ $this->getoptOutputTransformer(getopt($getoptInputs['short'], $getoptInputs['long']));
+ $this->checkForDeprecatedAliasUse();
+ }
+
+ /** Adds an option that produces an error message if not set. */
+ protected function addRequiredOption(string $name, CliOption $option): void {
+ $this->inputs[$name] = [
+ 'defaultInput' => null,
+ 'required' => true,
+ 'aliasUsed' => null,
+ 'values' => null,
+ ];
+ $this->options[$name] = $option;
+ }
+
+ /**
+ * Adds an optional option.
+ * @param string $defaultInput If not null this value is received as input in all cases where no
+ * user input is present. e.g. set this if you want an option to always return a value.
+ */
+ protected function addOption(string $name, CliOption $option, string $defaultInput = null): void {
+ $this->inputs[$name] = [
+ 'defaultInput' => is_string($defaultInput) ? [$defaultInput] : $defaultInput,
+ 'required' => null,
+ 'aliasUsed' => null,
+ 'values' => null,
+ ];
+ $this->options[$name] = $option;
+ }
+
+ private function appendInvalidValues(): void {
+ foreach ($this->options as $name => $option) {
+ if ($this->inputs[$name]['required'] && $this->inputs[$name]['values'] === null) {
+ $this->errors[$name] = 'invalid input: ' . $option->getLongAlias() . ' cannot be empty';
+ }
+ }
+
+ foreach ($this->inputs as $name => $input) {
+ foreach ($input['values'] ?? $input['defaultInput'] ?? [] as $value) {
+ switch ($this->options[$name]->getTypes()['type']) {
+ case 'int':
+ if (!ctype_digit($value)) {
+ $this->errors[$name] = 'invalid input: ' . $input['aliasUsed'] . ' must be an integer';
+ }
+ break;
+ case 'bool':
+ if (filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === null) {
+ $this->errors[$name] = 'invalid input: ' . $input['aliasUsed'] . ' must be a boolean';
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ private function appendTypedValidValues(): void {
+ foreach ($this->inputs as $name => $input) {
+ $values = $input['values'] ?? $input['defaultInput'] ?? null;
+ $types = $this->options[$name]->getTypes();
+ if ($values) {
+ $validValues = [];
+ $typedValues = [];
+
+ switch ($types['type']) {
+ case 'string':
+ $typedValues = $values;
+ break;
+ case 'int':
+ $validValues = array_filter($values, static fn($value) => ctype_digit($value));
+ $typedValues = array_map(static fn($value) => (int) $value, $validValues);
+ break;
+ case 'bool':
+ $validValues = array_filter($values, static fn($value) => filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) !== null);
+ $typedValues = array_map(static fn($value) => (bool) filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE), $validValues);
+ break;
+ }
+
+ if (!empty($typedValues)) {
+ // @phpstan-ignore-next-line (change to `@phpstan-ignore property.dynamicName` when upgrading to PHPStan 1.11+)
+ $this->$name = $types['isArray'] ? $typedValues : array_pop($typedValues);
+ }
+ }
+ }
+ }
+
+ /** @param array<string,string|false>|false $getoptOutput */
+ private function getoptOutputTransformer($getoptOutput): void {
+ $getoptOutput = is_array($getoptOutput) ? $getoptOutput : [];
+
+ foreach ($getoptOutput as $alias => $value) {
+ foreach ($this->options as $name => $data) {
+ if (in_array($alias, $data->getAliases(), true)) {
+ $this->inputs[$name]['aliasUsed'] = $alias;
+ $this->inputs[$name]['values'] = $value === false
+ ? [$data->getOptionalValueDefault()]
+ : (is_array($value)
+ ? $value
+ : [$value]);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param array<string> $userInputs
+ * @return array<string>
+ */
+ private function getAliasesUsed(array $userInputs, string $regex): array {
+ $foundAliases = [];
+
+ foreach ($userInputs as $input) {
+ preg_match($regex, $input, $matches);
+
+ if(!empty($matches['short'])) {
+ $foundAliases = array_merge($foundAliases, str_split($matches['short']));
+ }
+ if(!empty($matches['long'])) {
+ $foundAliases[] = $matches['long'];
+ }
+ }
+
+ return $foundAliases;
+ }
+
+ /**
+ * @param array<string> $input List of user command-line inputs.
+ */
+ private function appendUnknownAliases(array $input): void {
+ $valid = [];
+ foreach ($this->options as $option) {
+ $valid = array_merge($valid, $option->getAliases());
+ }
+
+ $sanitizeInput = $this->getAliasesUsed($input, $this->makeInputRegex());
+ $unknownAliases = array_diff($sanitizeInput, $valid);
+ if (empty($unknownAliases)) {
+ return;
+ }
+
+ foreach ($unknownAliases as $unknownAlias) {
+ $this->errors[$unknownAlias] = 'unknown option: ' . $unknownAlias;
+ }
+ }
+
+ /**
+ * Checks for presence of deprecated aliases.
+ * @return bool Returns TRUE and generates a deprecation warning if deprecated aliases are present, FALSE otherwise.
+ */
+ private function checkForDeprecatedAliasUse(): bool {
+ $deprecated = [];
+ $replacements = [];
+
+ foreach ($this->inputs as $name => $data) {
+ if ($data['aliasUsed'] !== null && $data['aliasUsed'] === $this->options[$name]->getDeprecatedAlias()) {
+ $deprecated[] = $this->options[$name]->getDeprecatedAlias();
+ $replacements[] = $this->options[$name]->getLongAlias();
+ }
+ }
+
+ if (empty($deprecated)) {
+ return false;
+ }
+
+ fwrite(STDERR, "FreshRSS deprecation warning: the CLI option(s): " . implode(', ', $deprecated) .
+ " are deprecated and will be removed in a future release. Use: " . implode(', ', $replacements) .
+ " instead\n");
+ return true;
+ }
+
+ /** @return array{long:array<string>,short:string}*/
+ private function getGetoptInputs(): array {
+ $getoptNotation = [
+ 'none' => '',
+ 'required' => ':',
+ 'optional' => '::',
+ ];
+
+ $long = [];
+ $short = '';
+
+ foreach ($this->options as $option) {
+ $long[] = $option->getLongAlias() . $getoptNotation[$option->getValueTaken()];
+ $long[] = $option->getDeprecatedAlias() ? $option->getDeprecatedAlias() . $getoptNotation[$option->getValueTaken()] : '';
+ $short .= $option->getShortAlias() ? $option->getShortAlias() . $getoptNotation[$option->getValueTaken()] : '';
+ }
+
+ return [
+ 'long' => array_filter($long),
+ 'short' => $short
+ ];
+ }
+
+ private function getUsageMessage(string $command): string {
+ $required = ['Usage: ' . basename($command)];
+ $optional = [];
+
+ foreach ($this->options as $name => $option) {
+ $shortAlias = $option->getShortAlias() ? '-' . $option->getShortAlias() . ' ' : '';
+ $longAlias = '--' . $option->getLongAlias() . ($option->getValueTaken() === 'required' ? '=<' . strtolower($name) . '>' : '');
+ if ($this->inputs[$name]['required']) {
+ $required[] = $shortAlias . $longAlias;
+ } else {
+ $optional[] = '[' . $shortAlias . $longAlias . ']';
+ }
+ }
+
+ return implode(' ', $required) . ' ' . implode(' ', $optional);
+ }
+
+ private function makeInputRegex() : string {
+ $shortWithValues = '';
+ foreach ($this->options as $option) {
+ if (($option->getValueTaken() === 'required' || $option->getValueTaken() === 'optional') && $option->getShortAlias()) {
+ $shortWithValues .= $option->getShortAlias();
+ }
+ }
+
+ return $shortWithValues === ''
+ ? "/^--(?'long'[^=]+)|^-(?<short>\w+)/"
+ : "/^--(?'long'[^=]+)|^-(?<short>(?(?=\w*[$shortWithValues])[^$shortWithValues]*[$shortWithValues]|\w+))/";
+ }
+}
diff --git a/cli/_cli.php b/cli/_cli.php
index c51dd69a3..9d9d9c32d 100644..100755
--- a/cli/_cli.php
+++ b/cli/_cli.php
@@ -6,11 +6,12 @@ if (php_sapi_name() !== 'cli') {
}
const EXIT_CODE_ALREADY_EXISTS = 3;
-const REGEX_INPUT_OPTIONS = '/^-{2}|^-{1}/';
require(__DIR__ . '/../constants.php');
require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
require(LIB_PATH . '/lib_install.php');
+require_once(__DIR__ . '/CliOption.php');
+require_once(__DIR__ . '/CliOptionsParser.php');
Minz_Session::init('FreshRSS', true);
FreshRSS_Context::initSystem();
@@ -73,119 +74,3 @@ function performRequirementCheck(string $databaseType): void {
fail($message);
}
}
-
-/**
- * Parses parameters used with FreshRSS' CLI commands.
- * @param array{'long':array<string,string>,'short':array<string,string>,'deprecated':array<string,string>} $parameters
- * Matrix of 'long': map of long option names as keys and their respective getopt() notations as values,
- * 'short': map of short option names as values and their equivalent long options as keys, 'deprecated': map of
- * replacement option names as keys and their respective deprecated option names as values.
- * @return array{'valid':array<string,string>,'invalid':array<string>} Matrix of 'valid': map of of all known
- * option names used and their respective values and 'invalid': list of all unknown options used.
- */
-function parseCliParams(array $parameters): array {
- global $argv;
- $longOptions = [];
- $shortOptions = '';
-
- foreach ($parameters['long'] as $name => $getopt_note) {
- $longOptions[] = $name . $getopt_note;
- }
- foreach ($parameters['deprecated'] as $name => $deprecatedName) {
- $longOptions[] = $deprecatedName . $parameters['long'][$name];
- }
- foreach ($parameters['short'] as $name => $shortName) {
- $shortOptions .= $shortName . $parameters['long'][$name];
- }
-
- $options = getopt($shortOptions, $longOptions);
-
- $valid = is_array($options) ? $options : [];
-
- array_walk($valid, static fn(&$option) => $option = $option === false ? '' : $option);
-
- /** @var array<string,string> $valid */
- checkForDeprecatedOptions(array_keys($valid), $parameters['deprecated']);
-
- $valid = replaceOptions($valid, $parameters['short']);
- $valid = replaceOptions($valid, $parameters['deprecated']);
-
- $invalid = findInvalidOptions(
- $argv,
- array_merge(array_keys($parameters['long']), array_values($parameters['short']), array_values($parameters['deprecated']))
- );
-
- return [
- 'valid' => $valid,
- 'invalid' => $invalid
- ];
-}
-
-/**
- * @param array<string> $options
- * @return array<string>
- */
-function getOptions(array $options, string $regex): array {
- $longOptions = array_filter($options, static function (string $a) use ($regex) {
- return preg_match($regex, $a) === 1;
- });
- return array_map(static function (string $a) use ($regex) {
- return preg_replace($regex, '', $a) ?? '';
- }, $longOptions);
-}
-
-/**
- * Checks for presence of unknown options.
- * @param array<string> $input List of command line arguments to check for validity.
- * @param array<string> $params List of valid options to check against.
- * @return array<string> Returns a list all unknown options found.
- */
-function findInvalidOptions(array $input, array $params): array {
- $sanitizeInput = getOptions($input, REGEX_INPUT_OPTIONS);
- $unknownOptions = array_diff($sanitizeInput, $params);
-
- if (0 === count($unknownOptions)) {
- return [];
- }
-
- fwrite(STDERR, sprintf("FreshRSS error: unknown options: %s\n", implode (', ', $unknownOptions)));
- return $unknownOptions;
-}
-
-/**
- * Checks for presence of deprecated options.
- * @param array<string> $optionNames Command line option names to check for deprecation.
- * @param array<string,string> $params Map of replacement options as keys and their respective deprecated
- * options as values.
- * @return bool Returns TRUE and generates a deprecation warning if deprecated options are present, FALSE otherwise.
- */
-function checkForDeprecatedOptions(array $optionNames, array $params): bool {
- $deprecatedOptions = array_intersect($optionNames, $params);
- $replacements = array_map(static fn($option) => array_search($option, $params, true), $deprecatedOptions);
-
- if (0 === count($deprecatedOptions)) {
- return false;
- }
-
- fwrite(STDERR, "FreshRSS deprecation warning: the CLI option(s): " . implode(', ', $deprecatedOptions) .
- " are deprecated and will be removed in a future release. Use: "
- . implode(', ', $replacements) . " instead\n");
- return true;
-}
-
-/**
- * Switches items in a list to their provided replacements.
- * @param array<string,string> $options Map with items to check for replacement as keys.
- * @param array<string,string> $replacements Map of replacement items as keys and the item they replace as their values.
- * @return array<string,string> Returns $options with replacements.
- */
-function replaceOptions(array $options, array $replacements): array {
- $updatedOptions = [];
-
- foreach ($options as $name => $value) {
- $replacement = array_search($name, $replacements, true);
- $updatedOptions[$replacement ? $replacement : $name] = $value;
- }
-
- return $updatedOptions;
-}
diff --git a/cli/_update-or-create-user.php b/cli/_update-or-create-user.php
deleted file mode 100644
index 1cc08bd25..000000000
--- a/cli/_update-or-create-user.php
+++ /dev/null
@@ -1,71 +0,0 @@
-<?php
-declare(strict_types=1);
-require(__DIR__ . '/_cli.php');
-
-performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
-
-$parameters = [
- 'long' => [
- 'user' => ':',
- 'password' => ':',
- 'api-password' => ':',
- 'language' => ':',
- 'email' => ':',
- 'token' => ':',
- 'purge-after-months' => ':',
- 'feed-min-articles-default' => ':',
- 'feed-ttl-default' => ':',
- 'since-hours-posts-per-rss' => ':',
- 'max-posts-per-rss' => ':',
- ],
- 'short' => [],
- 'deprecated' => [
- 'api-password' => 'api_password',
- 'purge-after-months' => 'purge_after_months',
- 'feed-min-articles-default' => 'feed_min_articles_default',
- 'feed-ttl-default' => 'feed_ttl_default',
- 'since-hours-posts-per-rss' => 'since_hours_posts_per_rss',
- 'max-posts-per-rss' => 'max_posts_per_rss',
- ],
-];
-
-if (!isset($isUpdate)) {
- $isUpdate = false;
-} elseif (!$isUpdate) {
- $parameters['long']['no-default-feeds'] = ''; //Only for creating new users
- $parameters['deprecated']['no-default-feeds'] = 'no_default_feeds';
-}
-
-$GLOBALS['options'] = parseCliParams($parameters);
-
-if (!empty($options['invalid']) || empty($options['valid']['user'])) {
- fail('Usage: ' . basename($_SERVER['SCRIPT_FILENAME']) .
- " --user username ( --password 'password' --api-password 'api_password'" .
- " --language en --email user@example.net --token 'longRandomString'" .
- ($isUpdate ? '' : ' --no-default-feeds') .
- " --purge-after-months 3 --feed-min-articles-default 50 --feed-ttl-default 3600" .
- " --since-hours-posts-per-rss 168 --max-posts-per-rss 400 )");
-}
-
-function strParam(string $name): ?string {
- global $options;
- return isset($options['valid'][$name]) ? strval($options['valid'][$name]) : null;
-}
-
-function intParam(string $name): ?int {
- global $options;
- return isset($options['valid'][$name]) && ctype_digit($options['valid'][$name]) ? intval($options['valid'][$name]) : null;
-}
-
-$values = array(
- 'language' => strParam('language'),
- 'mail_login' => strParam('email'),
- 'token' => strParam('token'),
- 'old_entries' => intParam('purge-after-months'), //TODO: Update with new mechanism
- 'keep_history_default' => intParam('feed-min-articles-default'), //TODO: Update with new mechanism
- 'ttl_default' => intParam('feed-ttl-default'),
- 'since_hours_posts_per_rss' => intParam('since-hours-posts-per-rss'),
- 'max_posts_per_rss' => intParam('max-posts-per-rss'),
- );
-
-$values = array_filter($values);
diff --git a/cli/actualize-user.php b/cli/actualize-user.php
index 03af5f5c8..c07cf6d0e 100755
--- a/cli/actualize-user.php
+++ b/cli/actualize-user.php
@@ -5,21 +5,23 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
-$parameters = [
- 'long' => [
- 'user' => ':'
- ],
- 'short' => [],
- 'deprecated' => [],
-];
-
-$options = parseCliParams($parameters);
-
-if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
- fail('Usage: ' . basename(__FILE__) . " --user username");
+$cliOptions = new class extends CliOptionsParser {
+ public string $user;
+
+ public function __construct() {
+ $this->addRequiredOption('user', (new CliOption('user')));
+ parent::__construct();
+ }
+};
+
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
-$username = cliInitUser($options['valid']['user']);
+$username = cliInitUser($cliOptions->user);
+
+Minz_ExtensionManager::callHookVoid('freshrss_user_maintenance');
+
fwrite(STDERR, 'FreshRSS actualizing user “' . $username . "”…\n");
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
diff --git a/cli/check.translation.php b/cli/check.translation.php
index 10a346dee..0da415d85 100755
--- a/cli/check.translation.php
+++ b/cli/check.translation.php
@@ -8,38 +8,39 @@ require_once __DIR__ . '/i18n/I18nFile.php';
require_once __DIR__ . '/i18n/I18nUsageValidator.php';
require_once __DIR__ . '/../constants.php';
-$i18nFile = new I18nFile();
-$i18nData = new I18nData($i18nFile->load());
+$cliOptions = new class extends CliOptionsParser {
+ /** @var array<int,string> $language */
+ public array $language;
+ public string $displayResult;
+ public string $help;
+ public string $displayReport;
+
+ public function __construct() {
+ $this->addOption('language', (new CliOption('language', 'l'))->typeOfArrayOfString());
+ $this->addOption('displayResult', (new CliOption('display-result', 'd'))->withValueNone());
+ $this->addOption('help', (new CliOption('help', 'h'))->withValueNone());
+ $this->addOption('displayReport', (new CliOption('display-report', 'r'))->withValueNone());
+ parent::__construct();
+ }
+};
-$parameters = [
- 'long' => [
- 'display-result' => '',
- 'help' => '',
- 'language' => ':',
- 'display-report' => '',
- ],
- 'short' => [
- 'display-result' => 'd',
- 'help' => 'h',
- 'language' => 'l',
- 'display-report' => 'r',
- ],
- 'deprecated' => [],
-];
-
-$options = parseCliParams($parameters);
-
-if (!empty($options['invalid']) || array_key_exists('help', $options['valid'])) {
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
+}
+if (isset($cliOptions->help)) {
checkHelp();
}
-if (array_key_exists('language', $options['valid'])) {
- $languages = [$options['valid']['language']];
+$i18nFile = new I18nFile();
+$i18nData = new I18nData($i18nFile->load());
+
+if (isset($cliOptions->language)) {
+ $languages = $cliOptions->language;
} else {
$languages = $i18nData->getAvailableLanguages();
}
-$displayResults = array_key_exists('display-result', $options['valid']);
-$displayReport = array_key_exists('display-report', $options['valid']);
+$displayResults = isset($cliOptions->displayResult);
+$displayReport = isset($cliOptions->displayReport);
$isValidated = true;
$result = [];
@@ -122,5 +123,5 @@ DESCRIPTION
-r, --display-report display completion report.
HELP;
- exit;
+ exit();
}
diff --git a/cli/create-user.php b/cli/create-user.php
index 53e0335bc..61bbc3563 100755
--- a/cli/create-user.php
+++ b/cli/create-user.php
@@ -1,38 +1,93 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
+require(__DIR__ . '/_cli.php');
-$isUpdate = false;
-require(__DIR__ . '/_update-or-create-user.php');
+$cliOptions = new class extends CliOptionsParser {
+ public string $user;
+ public string $password;
+ public string $apiPassword;
+ public string $language;
+ public string $email;
+ public string $token;
+ public int $purgeAfterMonths;
+ public int $feedMinArticles;
+ public int $feedTtl;
+ public int $sinceHoursPostsPerRss;
+ public int $maxPostsPerRss;
+ public bool $noDefaultFeeds;
-$username = $GLOBALS['options']['valid']['user'];
-if (!FreshRSS_user_Controller::checkUsername($username)) {
- fail('FreshRSS error: invalid username “' . $username .
- '”! Must be matching ' . FreshRSS_user_Controller::USERNAME_PATTERN);
-}
+ public function __construct() {
+ $this->addRequiredOption('user', (new CliOption('user')));
+ $this->addOption('password', (new CliOption('password')));
+ $this->addOption('apiPassword', (new CliOption('api-password'))->deprecatedAs('api_password'));
+ $this->addOption('language', (new CliOption('language')));
+ $this->addOption('email', (new CliOption('email')));
+ $this->addOption('token', (new CliOption('token')));
+ $this->addOption(
+ 'purgeAfterMonths',
+ (new CliOption('purge-after-months'))->typeOfInt()->deprecatedAs('purge_after_months')
+ );
+ $this->addOption(
+ 'feedMinArticles',
+ (new CliOption('feed-min-articles-default'))->typeOfInt()->deprecatedAs('feed_min_articles_default')
+ );
+ $this->addOption(
+ 'feedTtl',
+ (new CliOption('feed-ttl-default'))->typeOfInt()->deprecatedAs('feed_ttl_default')
+ );
+ $this->addOption(
+ 'sinceHoursPostsPerRss',
+ (new CliOption('since-hours-posts-per-rss'))->typeOfInt()->deprecatedAs('since_hours_posts_per_rss')
+ );
+ $this->addOption(
+ 'maxPostsPerRss',
+ (new CliOption('max-posts-per-rss'))->typeOfInt()->deprecatedAs('max_posts_per_rss')
+ );
+ $this->addOption(
+ 'noDefaultFeeds',
+ (new CliOption('no-default-feeds'))->withValueNone()->deprecatedAs('no_default_feeds')
+ );
+ parent::__construct();
+ }
+};
-$usernames = listUsers();
-if (preg_grep("/^$username$/i", $usernames)) {
- fail('FreshRSS warning: username already exists “' . $username . '”', EXIT_CODE_ALREADY_EXISTS);
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
+$username = $cliOptions->user;
+
echo 'FreshRSS creating user “', $username, "”…\n";
+$values = [
+ 'language' => $cliOptions->language ?? null,
+ 'mail_login' => $cliOptions->email ?? null,
+ 'token' => $cliOptions->token ?? null,
+ 'old_entries' => $cliOptions->purgeAfterMonths ?? null,
+ 'keep_history_default' => $cliOptions->feedMinArticles ?? null,
+ 'ttl_default' => $cliOptions->feedTtl ?? null,
+ 'since_hours_posts_per_rss' => $cliOptions->sinceHoursPostsPerRss ?? null,
+ 'max_posts_per_rss' => $cliOptions->maxPostsPerRss ?? null,
+];
+
+$values = array_filter($values);
+
$ok = FreshRSS_user_Controller::createUser(
$username,
- empty($options['valid']['email']) ? '' : $options['valid']['email'],
- empty($options['valid']['password']) ? '' : $options['valid']['password'],
- $GLOBALS['values'],
- !isset($options['valid']['no-default-feeds'])
+ isset($cliOptions->email) ? $cliOptions->email : null,
+ $cliOptions->password ?? '',
+ $values,
+ !isset($cliOptions->noDefaultFeeds)
);
if (!$ok) {
fail('FreshRSS could not create user!');
}
-if (!empty($options['valid']['api-password'])) {
+if (isset($cliOptions->apiPassword)) {
$username = cliInitUser($username);
- $error = FreshRSS_api_Controller::updatePassword($options['valid']['api-password']);
+ $error = FreshRSS_api_Controller::updatePassword($cliOptions->apiPassword);
if ($error !== false) {
fail($error);
}
diff --git a/cli/db-optimize.php b/cli/db-optimize.php
index d553b64d9..f6d3884b9 100755
--- a/cli/db-optimize.php
+++ b/cli/db-optimize.php
@@ -5,21 +5,20 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
-$parameters = [
- 'long' => [
- 'user' => ':',
- ],
- 'short' => [],
- 'deprecated' => [],
-];
-
-$options = parseCliParams($parameters);
-
-if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
- fail('Usage: ' . basename(__FILE__) . " --user username");
+$cliOptions = new class extends CliOptionsParser {
+ public string $user;
+
+ public function __construct() {
+ $this->addRequiredOption('user', (new CliOption('user')));
+ parent::__construct();
+ }
+};
+
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
-$username = cliInitUser($options['valid']['user']);
+$username = cliInitUser($cliOptions->user);
echo 'FreshRSS optimizing database for user “', $username, "”…\n";
diff --git a/cli/delete-user.php b/cli/delete-user.php
index b4f042847..18efa4253 100755
--- a/cli/delete-user.php
+++ b/cli/delete-user.php
@@ -5,29 +5,27 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
-$parameters = [
- 'long' => [
- 'user' => ':',
- ],
- 'short' => [],
- 'deprecated' => [],
-];
-
-$options = parseCliParams($parameters);
-
-if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
- fail('Usage: ' . basename(__FILE__) . " --user username");
+$cliOptions = new class extends CliOptionsParser {
+ public string $user;
+
+ public function __construct() {
+ $this->addRequiredOption('user', (new CliOption('user')));
+ parent::__construct();
+ }
+};
+
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
-$username = $options['valid']['user'];
+
+$username = $cliOptions->user;
+
if (!FreshRSS_user_Controller::checkUsername($username)) {
- fail('FreshRSS error: invalid username “' . $username . '”');
+ fail('FreshRSS error: invalid username: ' . $username . "\n");
}
-
-$usernames = listUsers();
-if (!preg_grep("/^$username$/i", $usernames)) {
- fail('FreshRSS error: username not found “' . $username . '”');
+if (!FreshRSS_user_Controller::userExists($username)) {
+ fail('FreshRSS error: user not found: ' . $username . "\n");
}
-
if (strcasecmp($username, FreshRSS_Context::systemConf()->default_user) === 0) {
fail('FreshRSS error: default user must not be deleted: “' . $username . '”');
}
diff --git a/cli/do-install.php b/cli/do-install.php
index 2d73c2e98..77acc58ed 100755
--- a/cli/do-install.php
+++ b/cli/do-install.php
@@ -7,74 +7,91 @@ if (file_exists(DATA_PATH . '/applied_migrations.txt')) {
fail('FreshRSS seems to be already installed!' . "\n" . 'Please use `./cli/reconfigure.php` instead.', EXIT_CODE_ALREADY_EXISTS);
}
-$parameters = [
- 'long' => [
- 'environment' => ':',
- 'base-url' => ':',
- 'language' => ':',
- 'title' => ':',
- 'default-user' => ':',
- 'allow-anonymous' => '',
- 'allow-anonymous-refresh' => '',
- 'auth-type' => ':',
- 'api-enabled' => '',
- 'allow-robots' => '',
- 'disable-update' => '',
- 'db-type' => ':',
- 'db-host' => ':',
- 'db-user' => ':',
- 'db-password' => ':',
- 'db-base' => ':',
- 'db-prefix' => '::',
- ],
- 'short' => [],
- 'deprecated' => [
- 'base-url' => 'base_url',
- 'default-user' => 'default_user',
- 'allow-anonymous' => 'allow_anonymous',
- 'allow-anonymous-refresh' => 'allow_anonymous_refresh',
- 'auth-type' => 'auth_type',
- 'api-enabled' => 'api_enabled',
- 'allow-robots' => 'allow_robots',
- 'disable-update' => 'disable_update',
- ],
-];
-
-$configParams = [
- 'environment' => 'environment',
- 'base-url' => 'base_url',
- 'language' => 'language',
- 'title' => 'title',
- 'default-user' => 'default_user',
- 'allow-anonymous' => 'allow_anonymous',
- 'allow-anonymous-refresh' => 'allow_anonymous_refresh',
- 'auth-type' => 'auth_type',
- 'api-enabled' => 'api_enabled',
- 'allow-robots' => 'allow_robots',
- 'disable-update' => 'disable_update',
-];
-
-$dBconfigParams = [
- 'db-type' => 'type',
- 'db-host' => 'host',
- 'db-user' => 'user',
- 'db-password' => 'password',
- 'db-base' => 'base',
- 'db-prefix' => 'prefix',
-];
-
-$options = parseCliParams($parameters);
+$cliOptions = new class extends CliOptionsParser {
+ public string $defaultUser;
+ public string $environment;
+ public string $baseUrl;
+ public string $language;
+ public string $title;
+ public bool $allowAnonymous;
+ public bool $allowAnonymousRefresh;
+ public string $authType;
+ public bool $apiEnabled;
+ public bool $allowRobots;
+ public bool $disableUpdate;
+ public string $dbType;
+ public string $dbHost;
+ public string $dbUser;
+ public string $dbPassword;
+ public string $dbBase;
+ public string $dbPrefix;
+
+ public function __construct() {
+ $this->addRequiredOption('defaultUser', (new CliOption('default-user'))->deprecatedAs('default_user'));
+ $this->addOption('environment', (new CliOption('environment')));
+ $this->addOption('baseUrl', (new CliOption('base-url'))->deprecatedAs('base_url'));
+ $this->addOption('language', (new CliOption('language')));
+ $this->addOption('title', (new CliOption('title')));
+ $this->addOption(
+ 'allowAnonymous',
+ (new CliOption('allow-anonymous'))->withValueOptional('true')->deprecatedAs('allow_anonymous')->typeOfBool()
+ );
+ $this->addOption(
+ 'allowAnonymousRefresh',
+ (new CliOption('allow-anonymous-refresh'))->withValueOptional('true')->deprecatedAs('allow_anonymous_refresh')->typeOfBool()
+ );
+ $this->addOption('authType', (new CliOption('auth-type'))->deprecatedAs('auth_type'));
+ $this->addOption(
+ 'apiEnabled',
+ (new CliOption('api-enabled'))->withValueOptional('true')->deprecatedAs('api_enabled')->typeOfBool()
+ );
+ $this->addOption(
+ 'allowRobots',
+ (new CliOption('allow-robots'))->withValueOptional('true')->deprecatedAs('allow_robots')->typeOfBool()
+ );
+ $this->addOption(
+ 'disableUpdate',
+ (new CliOption('disable-update'))->withValueOptional('true')->deprecatedAs('disable_update')->typeOfBool()
+ );
+ $this->addOption('dbType', (new CliOption('db-type')));
+ $this->addOption('dbHost', (new CliOption('db-host')));
+ $this->addOption('dbUser', (new CliOption('db-user')));
+ $this->addOption('dbPassword', (new CliOption('db-password')));
+ $this->addOption('dbBase', (new CliOption('db-base')));
+ $this->addOption('dbPrefix', (new CliOption('db-prefix'))->withValueOptional());
+ parent::__construct();
+ }
+};
-if (!empty($options['invalid']) || empty($options['valid']['default-user']) || !is_string($options['valid']['default-user'])) {
- fail('Usage: ' . basename(__FILE__) . " --default-user admin ( --auth-type form" .
- " --environment production --base-url https://rss.example.net --allow-robots" .
- " --language en --title FreshRSS --allow-anonymous --allow-anonymous-refresh --api-enabled" .
- " --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123" .
- " --db-base freshrss --db-prefix freshrss_ --disable-update )");
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
fwrite(STDERR, 'FreshRSS install…' . "\n");
+$values = [
+ 'default_user' => $cliOptions->defaultUser ?? null,
+ 'environment' => $cliOptions->environment ?? null,
+ 'base_url' => $cliOptions->baseUrl ?? null,
+ 'language' => $cliOptions->language ?? null,
+ 'title' => $cliOptions->title ?? null,
+ 'allow_anonymous' => $cliOptions->allowAnonymous ?? null,
+ 'allow_anonymous_refresh' => $cliOptions->allowAnonymousRefresh ?? null,
+ 'auth_type' => $cliOptions->authType ?? null,
+ 'api_enabled' => $cliOptions->apiEnabled ?? null,
+ 'allow_robots' => $cliOptions->allowRobots ?? null,
+ 'disable_update' => $cliOptions->disableUpdate ?? null,
+];
+
+$dbValues = [
+ 'type' => $cliOptions->dbType ?? null,
+ 'host' => $cliOptions->dbHost ?? null,
+ 'user' => $cliOptions->dbUser ?? null,
+ 'password' => $cliOptions->dbPassword ?? null,
+ 'base' => $cliOptions->dbBase ?? null,
+ 'prefix' => $cliOptions->dbPrefix ?? null,
+];
+
$config = array(
'salt' => generateSalt(),
'db' => FreshRSS_Context::systemConf()->db,
@@ -88,10 +105,26 @@ if (file_exists($customConfigPath)) {
}
}
-foreach ($configParams as $param => $configParam) {
- if (isset($options['valid'][$param])) {
- $isFlag = $parameters['long'][$param] === '';
- $config[$configParam] = $isFlag ? true : $options['valid'][$param];
+foreach ($values as $name => $value) {
+ if ($value !== null) {
+ switch ($name) {
+ case 'default_user':
+ if (!FreshRSS_user_Controller::checkUsername($value)) {
+ fail('FreshRSS invalid default username! default_user must be ASCII alphanumeric');
+ }
+ break;
+ case 'environment':
+ if (!in_array($value, ['development', 'production', 'silent'], true)) {
+ fail('FreshRSS invalid environment! environment must be one of { development, production, silent }');
+ }
+ break;
+ case 'auth_type':
+ if (!in_array($value, ['form', 'http_auth', 'none'], true)) {
+ fail('FreshRSS invalid authentication method! auth_type must be one of { form, http_auth, none }');
+ }
+ break;
+ }
+ $config[$name] = $value;
}
}
@@ -99,23 +132,10 @@ if ((!empty($config['base_url'])) && is_string($config['base_url']) && Minz_Requ
$config['pubsubhubbub_enabled'] = true;
}
-foreach ($dBconfigParams as $dBparam => $configDbParam) {
- if (isset($options['valid'][$dBparam])) {
- $config['db'][$configDbParam] = $options['valid'][$dBparam];
- }
-}
+$config['db'] = array_merge($config['db'], array_filter($dbValues));
performRequirementCheck($config['db']['type']);
-if (!FreshRSS_user_Controller::checkUsername($options['valid']['default-user'])) {
- fail('FreshRSS error: invalid default username “' . $options['valid']['default-user']
- . '”! Must be matching ' . FreshRSS_user_Controller::USERNAME_PATTERN);
-}
-
-if (isset($options['valid']['auth-type']) && !in_array($options['valid']['auth-type'], ['form', 'http_auth', 'none'], true)) {
- fail('FreshRSS invalid authentication method (auth-type must be one of { form, http_auth, none })');
-}
-
if (file_put_contents(join_path(DATA_PATH, 'config.php'),
"<?php\n return " . var_export($config, true) . ";\n") === false) {
fail('FreshRSS could not write configuration file!: ' . join_path(DATA_PATH, 'config.php'));
diff --git a/cli/export-opml-for-user.php b/cli/export-opml-for-user.php
index 7c59c65ef..4866a848f 100755
--- a/cli/export-opml-for-user.php
+++ b/cli/export-opml-for-user.php
@@ -5,21 +5,20 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
-$parameters = [
- 'long' => [
- 'user' => ':',
- ],
- 'short' => [],
- 'deprecated' => [],
-];
-
-$options = parseCliParams($parameters);
-
-if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
- fail('Usage: ' . basename(__FILE__) . " --user username > /path/to/file.opml.xml");
+$cliOptions = new class extends CliOptionsParser {
+ public string $user;
+
+ public function __construct() {
+ $this->addRequiredOption('user', (new CliOption('user')));
+ parent::__construct();
+ }
+};
+
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
-$username = cliInitUser($options['valid']['user']);
+$username = cliInitUser($cliOptions->user);
fwrite(STDERR, 'FreshRSS exporting OPML for user “' . $username . "”…\n");
diff --git a/cli/export-sqlite-for-user.php b/cli/export-sqlite-for-user.php
index e67896df9..98e05da22 100755
--- a/cli/export-sqlite-for-user.php
+++ b/cli/export-sqlite-for-user.php
@@ -5,26 +5,23 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
-$parameters = [
- 'long' => [
- 'user' => ':',
- 'filename' => ':',
- ],
- 'short' => [],
- 'deprecated' => [],
-];
-
-$options = parseCliParams($parameters);
-
-if (!empty($options['invalid'])
- || empty($options['valid']['user']) || empty($options['valid']['filename'])
- || !is_string($options['valid']['user']) || !is_string($options['valid']['filename'])
-) {
- fail('Usage: ' . basename(__FILE__) . ' --user username --filename /path/to/db.sqlite');
+$cliOptions = new class extends CliOptionsParser {
+ public string $user;
+ public string $filename;
+
+ public function __construct() {
+ $this->addRequiredOption('user', (new CliOption('user')));
+ $this->addRequiredOption('filename', (new CliOption('filename')));
+ parent::__construct();
+ }
+};
+
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
-$username = cliInitUser($options['valid']['user']);
-$filename = $options['valid']['filename'];
+$username = cliInitUser($cliOptions->user);
+$filename = $cliOptions->filename;
if (pathinfo($filename, PATHINFO_EXTENSION) !== 'sqlite') {
fail('Only *.sqlite files are supported!');
diff --git a/cli/export-zip-for-user.php b/cli/export-zip-for-user.php
index d818096e4..304acc392 100755
--- a/cli/export-zip-for-user.php
+++ b/cli/export-zip-for-user.php
@@ -5,31 +5,31 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
-$parameters = [
- 'long' => [
- 'user' => ':',
- 'max-feed-entries' => ':',
- ],
- 'short' => [],
- 'deprecated' => [],
-];
-
-$options = parseCliParams($parameters);
-
-if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
- fail('Usage: ' . basename(__FILE__) . " --user username ( --max-feed-entries 100 ) > /path/to/file.zip");
+$cliOptions = new class extends CliOptionsParser {
+ public string $user;
+ public int $maxFeedEntries;
+
+ public function __construct() {
+ $this->addRequiredOption('user', (new CliOption('user')));
+ $this->addOption('maxFeedEntries', (new CliOption('max-feed-entries'))->typeOfInt(), '100');
+ parent::__construct();
+ }
+};
+
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
if (!extension_loaded('zip')) {
fail('FreshRSS error: Lacking php-zip extension!');
}
-$username = cliInitUser($options['valid']['user']);
+$username = cliInitUser($cliOptions->user);
fwrite(STDERR, 'FreshRSS exporting ZIP for user “' . $username . "”…\n");
$export_service = new FreshRSS_Export_Service($username);
-$number_entries = empty($options['valid']['max-feed-entries']) ? 100 : intval($options['valid']['max-feed-entries']);
+$number_entries = $cliOptions->maxFeedEntries;
$exported_files = [];
// First, we generate the OPML file
diff --git a/cli/import-for-user.php b/cli/import-for-user.php
index 6969a8946..4c4db8405 100755
--- a/cli/import-for-user.php
+++ b/cli/import-for-user.php
@@ -5,27 +5,24 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
-$parameters = [
- 'long' => [
- 'user' => ':',
- 'filename' => ':',
- ],
- 'short' => [],
- 'deprecated' => [],
-];
-
-$options = parseCliParams($parameters);
-
-if (!empty($options['invalid'])
- || empty($options['valid']['user']) || empty($options['valid']['filename'])
- || !is_string($options['valid']['user']) || !is_string($options['valid']['filename'])
-) {
- fail('Usage: ' . basename(__FILE__) . " --user username --filename /path/to/file.ext");
+$cliOptions = new class extends CliOptionsParser {
+ public string $user;
+ public string $filename;
+
+ public function __construct() {
+ $this->addRequiredOption('user', (new CliOption('user')));
+ $this->addRequiredOption('filename', (new CliOption('filename')));
+ parent::__construct();
+ }
+};
+
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
-$username = cliInitUser($options['valid']['user']);
+$username = cliInitUser($cliOptions->user);
+$filename = $cliOptions->filename;
-$filename = $options['valid']['filename'];
if (!is_readable($filename)) {
fail('FreshRSS error: file is not readable “' . $filename . '”');
}
diff --git a/cli/import-sqlite-for-user.php b/cli/import-sqlite-for-user.php
index 29b7c1b0c..45ecb3597 100755
--- a/cli/import-sqlite-for-user.php
+++ b/cli/import-sqlite-for-user.php
@@ -5,27 +5,25 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
-$parameters = [
- 'long' => [
- 'user' => ':',
- 'filename' => ':',
- 'force-overwrite' => '',
- ],
- 'short' => [],
- 'deprecated' => [],
-];
-
-$options = parseCliParams($parameters);
-
-if (!empty($options['invalid'])
- || empty($options['valid']['user']) || empty($options['valid']['filename'])
- || !is_string($options['valid']['user']) || !is_string($options['valid']['filename'])
-) {
- fail('Usage: ' . basename(__FILE__) . ' --user username --force-overwrite --filename /path/to/db.sqlite');
+$cliOptions = new class extends CliOptionsParser {
+ public string $user;
+ public string $filename;
+ public string $forceOverwrite;
+
+ public function __construct() {
+ $this->addRequiredOption('user', (new CliOption('user')));
+ $this->addRequiredOption('filename', (new CliOption('filename')));
+ $this->addOption('forceOverwrite', (new CliOption('force-overwrite'))->withValueNone());
+ parent::__construct();
+ }
+};
+
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
-$username = cliInitUser($options['valid']['user']);
-$filename = $options['valid']['filename'];
+$username = cliInitUser($cliOptions->user);
+$filename = $cliOptions->filename;
if (pathinfo($filename, PATHINFO_EXTENSION) !== 'sqlite') {
fail('Only *.sqlite files are supported!');
@@ -34,7 +32,7 @@ if (pathinfo($filename, PATHINFO_EXTENSION) !== 'sqlite') {
echo 'FreshRSS importing database from SQLite for user “', $username, "”…\n";
$databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
-$clearFirst = array_key_exists('force-overwrite', $options['valid']);
+$clearFirst = isset($cliOptions->forceOverwrite);
$ok = $databaseDAO->dbCopy($filename, FreshRSS_DatabaseDAO::SQLITE_IMPORT, $clearFirst);
if (!$ok) {
echo 'If you would like to clear the user database first, use the option --force-overwrite', "\n";
diff --git a/cli/manipulate.translation.php b/cli/manipulate.translation.php
index 358a3ec33..740f3c4b7 100755
--- a/cli/manipulate.translation.php
+++ b/cli/manipulate.translation.php
@@ -6,70 +6,65 @@ require_once __DIR__ . '/i18n/I18nData.php';
require_once __DIR__ . '/i18n/I18nFile.php';
require_once __DIR__ . '/../constants.php';
-$parameters = [
- 'long' => [
- 'action' => ':',
- 'help' => '',
- 'key' => ':',
- 'language' => ':',
- 'origin-language' => ':',
- 'revert' => '',
- 'value' => ':',
- ],
- 'short' => [
- 'action' => 'a',
- 'help' => 'h',
- 'key' => 'k',
- 'language' => 'l',
- 'origin-language' => 'o',
- 'revert' => 'r',
- 'value' => 'v',
- ],
- 'deprecated' => [],
-];
-
-$options = parseCliParams($parameters);
-
-if (!empty($options['invalid']) || array_key_exists('help', $options['valid'])) {
- manipulateHelp();
- exit();
+$cliOptions = new class extends CliOptionsParser {
+ public string $action;
+ public string $key;
+ public string $value;
+ public string $language;
+ public string $originLanguage;
+ public string $revert;
+ public string $help;
+
+ public function __construct() {
+ $this->addRequiredOption('action', (new CliOption('action', 'a')));
+ $this->addOption('key', (new CliOption('key', 'k')));
+ $this->addOption('value', (new CliOption('value', 'v')));
+ $this->addOption('language', (new CliOption('language', 'l')));
+ $this->addOption('originLanguage', (new CliOption('origin-language', 'o')));
+ $this->addOption('revert', (new CliOption('revert', 'r'))->withValueNone());
+ $this->addOption('help', (new CliOption('help', 'h'))->withValueNone());
+ parent::__construct();
+ }
+};
+
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
-
-if (!array_key_exists('action', $options['valid'])) {
- error('You need to specify the action to perform.');
+if (isset($cliOptions->help)) {
+ manipulateHelp();
}
$data = new I18nFile();
$i18nData = new I18nData($data->load());
-switch ($options['valid']['action']) {
+switch ($cliOptions->action) {
case 'add' :
- if (array_key_exists('key', $options['valid']) && array_key_exists('value', $options['valid']) && array_key_exists('language', $options['valid'])) {
- $i18nData->addValue($options['valid']['key'], $options['valid']['value'], $options['valid']['language']);
- } elseif (array_key_exists('key', $options['valid']) && array_key_exists('value', $options['valid'])) {
- $i18nData->addKey($options['valid']['key'], $options['valid']['value']);
- } elseif (array_key_exists('language', $options['valid'])) {
+ if (isset($cliOptions->key) && isset($cliOptions->value) && isset($cliOptions->language)) {
+ $i18nData->addValue($cliOptions->key, $cliOptions->value, $cliOptions->language);
+ } elseif (isset($cliOptions->key) && isset($cliOptions->value)) {
+ $i18nData->addKey($cliOptions->key, $cliOptions->value);
+ } elseif (isset($cliOptions->language)) {
$reference = null;
- if (array_key_exists('origin-language', $options['valid'])) {
- $reference = $options['valid']['origin-language'];
+ if (isset($cliOptions->originLanguage)) {
+ $reference = $cliOptions->originLanguage;
}
- $i18nData->addLanguage($options['valid']['language'], $reference);
+ $i18nData->addLanguage($cliOptions->language, $reference);
} else {
error('You need to specify a valid set of options.');
exit;
}
break;
case 'delete' :
- if (array_key_exists('key', $options['valid'])) {
- $i18nData->removeKey($options['valid']['key']);
+ if (isset($cliOptions->key)) {
+ $i18nData->removeKey($cliOptions->key);
} else {
error('You need to specify the key to delete.');
exit;
}
break;
case 'exist':
- if (array_key_exists('key', $options['valid'])) {
- $key = $options['valid']['key'];
+ if (isset($cliOptions->key)) {
+ $key = $cliOptions->key;
if ($i18nData->isKnown($key)) {
echo "The '{$key}' key is known.\n\n";
} else {
@@ -83,16 +78,16 @@ switch ($options['valid']['action']) {
case 'format' :
break;
case 'ignore' :
- if (array_key_exists('language', $options['valid']) && array_key_exists('key', $options['valid'])) {
- $i18nData->ignore($options['valid']['key'], $options['valid']['language'], array_key_exists('revert', $options['valid']));
+ if (isset($cliOptions->language) && isset($cliOptions->key)) {
+ $i18nData->ignore($cliOptions->key, $cliOptions->language, isset($cliOptions->revert));
} else {
error('You need to specify a valid set of options.');
exit;
}
break;
case 'ignore_unmodified' :
- if (array_key_exists('language', $options['valid'])) {
- $i18nData->ignore_unmodified($options['valid']['language'], array_key_exists('revert', $options['valid']));
+ if (isset($cliOptions->language)) {
+ $i18nData->ignore_unmodified($cliOptions->language, isset($cliOptions->revert));
} else {
error('You need to specify a valid set of options.');
exit;
@@ -122,6 +117,7 @@ ERROR;
*/
function manipulateHelp(): void {
$file = str_replace(__DIR__ . '/', '', __FILE__);
+
echo <<<HELP
NAME
$file
@@ -144,17 +140,17 @@ DESCRIPTION
select the origin language (only for add language action)
EXAMPLES
-Example 1: add a language. It adds a new language by duplicating the referential.
+Example 1: add a language. Adds a new language by duplicating the reference language.
php $file -a add -l my_lang
php $file -a add -l my_lang -o ref_lang
-Example 2: add a new key. It adds the key for all supported languages.
+Example 2: add a new key. Adds a key to all supported languages.
php $file -a add -k my_key -v my_value
-Example 3: add a new value. It adds a new value for the selected key in the selected language.
+Example 3: add a new value. Sets a new value for the selected key in the selected language.
php $file -a add -k my_key -v my_value -l my_lang
-Example 4: delete a key. It deletes the selected key from all supported languages.
+Example 4: delete a key. Deletes the selected key from all supported languages.
php $file -a delete -k my_key
Example 5: format i18n files.
@@ -170,11 +166,12 @@ Example 8: ignore all unmodified keys. Adds IGNORE comments to all unmodified ke
php $file -a ignore_unmodified -l my_lang
Example 9: revert ignore on all unmodified keys. Removes IGNORE comments from all unmodified keys in the selected language.
- Warning: will also revert individually added unmodified keys.
+ Warning: will also revert individually added IGNOREs on unmodified keys.
php $file -a ignore_unmodified -r -l my_lang
Example 10: check if a key exist.
- php $file -a exist -k my_key\n\n
+ php $file -a exist -k my_key
HELP;
+ exit();
}
diff --git a/cli/reconfigure.php b/cli/reconfigure.php
index fde16c921..853e05297 100755
--- a/cli/reconfigure.php
+++ b/cli/reconfigure.php
@@ -3,133 +3,120 @@
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
-$parameters = [
- 'long' => [
- 'environment' => ':',
- 'base-url' => ':',
- 'language' => ':',
- 'title' => ':',
- 'default-user' => ':',
- 'allow-anonymous' => '',
- 'allow-anonymous-refresh' => '',
- 'auth-type' => ':',
- 'api-enabled' => '',
- 'allow-robots' => '',
- 'disable-update' => '',
- 'db-type' => ':',
- 'db-host' => ':',
- 'db-user' => ':',
- 'db-password' => ':',
- 'db-base' => ':',
- 'db-prefix' => '::',
- ],
- 'short' => [],
- 'deprecated' => [
- 'base-url' => 'base_url',
- 'default-user' => 'default_user',
- 'allow-anonymous' => 'allow_anonymous',
- 'allow-anonymous-refresh' => 'allow_anonymous_refresh',
- 'auth-type' => 'auth_type',
- 'api-enabled' => 'api_enabled',
- 'allow-robots' => 'allow_robots',
- 'disable-update' => 'disable_update',
- ],
-];
-
-$configParams = [
- 'environment',
- 'base-url',
- 'language',
- 'title',
- 'default-user',
- 'allow-anonymous',
- 'allow-anonymous-refresh',
- 'auth-type',
- 'api-enabled',
- 'allow-robots',
- 'disable-update',
-];
+$cliOptions = new class extends CliOptionsParser {
+ public string $defaultUser;
+ public string $environment;
+ public string $baseUrl;
+ public string $language;
+ public string $title;
+ public bool $allowAnonymous;
+ public bool $allowAnonymousRefresh;
+ public string $authType;
+ public bool $apiEnabled;
+ public bool $allowRobots;
+ public bool $disableUpdate;
+ public string $dbType;
+ public string $dbHost;
+ public string $dbUser;
+ public string $dbPassword;
+ public string $dbBase;
+ public string $dbPrefix;
-$dBconfigParams = [
- 'db-type' => 'type',
- 'db-host' => 'host',
- 'db-user' => 'user',
- 'db-password' => 'password',
- 'db-base' => 'base',
- 'db-prefix' => 'prefix',
-];
-
-$options = parseCliParams($parameters);
+ public function __construct() {
+ $this->addOption('defaultUser', (new CliOption('default-user'))->deprecatedAs('default_user'));
+ $this->addOption('environment', (new CliOption('environment')));
+ $this->addOption('baseUrl', (new CliOption('base-url'))->deprecatedAs('base_url'));
+ $this->addOption('language', (new CliOption('language')));
+ $this->addOption('title', (new CliOption('title')));
+ $this->addOption(
+ 'allowAnonymous',
+ (new CliOption('allow-anonymous'))->withValueOptional('true')->deprecatedAs('allow_anonymous')->typeOfBool()
+ );
+ $this->addOption(
+ 'allowAnonymousRefresh',
+ (new CliOption('allow-anonymous-refresh'))->withValueOptional('true')->deprecatedAs('allow_anonymous_refresh')->typeOfBool()
+ );
+ $this->addOption('authType', (new CliOption('auth-type'))->deprecatedAs('auth_type'));
+ $this->addOption(
+ 'apiEnabled',
+ (new CliOption('api-enabled'))->withValueOptional('true')->deprecatedAs('api_enabled')->typeOfBool()
+ );
+ $this->addOption(
+ 'allowRobots',
+ (new CliOption('allow-robots'))->withValueOptional('true')->deprecatedAs('allow_robots')->typeOfBool()
+ );
+ $this->addOption(
+ 'disableUpdate',
+ (new CliOption('disable-update'))->withValueOptional('true')->deprecatedAs('disable_update')->typeOfBool()
+ );
+ $this->addOption('dbType', (new CliOption('db-type')));
+ $this->addOption('dbHost', (new CliOption('db-host')));
+ $this->addOption('dbUser', (new CliOption('db-user')));
+ $this->addOption('dbPassword', (new CliOption('db-password')));
+ $this->addOption('dbBase', (new CliOption('db-base')));
+ $this->addOption('dbPrefix', (new CliOption('db-prefix'))->withValueOptional());
+ parent::__construct();
+ }
+};
-if (!empty($options['invalid'])) {
- fail('Usage: ' . basename(__FILE__) . " --default-user admin ( --auth-type form" .
- " --environment production --base-url https://rss.example.net --allow-robots" .
- " --language en --title FreshRSS --allow-anonymous --allow-anonymous-refresh --api-enabled" .
- " --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123" .
- " --db-base freshrss --db-prefix freshrss_ --disable-update )");
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
fwrite(STDERR, 'Reconfiguring FreshRSS…' . "\n");
-foreach ($configParams as $param) {
- if (isset($options['valid'][$param])) {
- switch ($param) {
- case 'allow-anonymous-refresh':
- FreshRSS_Context::systemConf()->allow_anonymous_refresh = true;
- break;
- case 'allow-anonymous':
- FreshRSS_Context::systemConf()->allow_anonymous = true;
- break;
- case 'allow-robots':
- FreshRSS_Context::systemConf()->allow_robots = true;
- break;
- case 'api-enabled':
- FreshRSS_Context::systemConf()->api_enabled = true;
- break;
- case 'auth-type':
- if (in_array($options['valid'][$param], ['form', 'http_auth', 'none'], true)) {
- FreshRSS_Context::systemConf()->auth_type = $options['valid'][$param];
- } else {
- fail('FreshRSS invalid authentication method! auth_type must be one of { form, http_auth, none }');
- }
- break;
- case 'base-url':
- FreshRSS_Context::systemConf()->base_url = (string) $options['valid'][$param];
- break;
- case 'default-user':
- if (FreshRSS_user_Controller::checkUsername((string) $options['valid'][$param])) {
- FreshRSS_Context::systemConf()->default_user = (string) $options['valid'][$param];
- } else {
+$values = [
+ 'default_user' => $cliOptions->defaultUser ?? null,
+ 'environment' => $cliOptions->environment ?? null,
+ 'base_url' => $cliOptions->baseUrl ?? null,
+ 'language' => $cliOptions->language ?? null,
+ 'title' => $cliOptions->title ?? null,
+ 'allow_anonymous' => $cliOptions->allowAnonymous ?? null,
+ 'allow_anonymous_refresh' => $cliOptions->allowAnonymousRefresh ?? null,
+ 'auth_type' => $cliOptions->authType ?? null,
+ 'api_enabled' => $cliOptions->apiEnabled ?? null,
+ 'allow_robots' => $cliOptions->allowRobots ?? null,
+ 'disable_update' => $cliOptions->disableUpdate ?? null,
+];
+
+$dbValues = [
+ 'type' => $cliOptions->dbType ?? null,
+ 'host' => $cliOptions->dbHost ?? null,
+ 'user' => $cliOptions->dbUser ?? null,
+ 'password' => $cliOptions->dbPassword ?? null,
+ 'base' => $cliOptions->dbBase ?? null,
+ 'prefix' => $cliOptions->dbPrefix ?? null,
+];
+
+$systemConf = FreshRSS_Context::systemConf();
+foreach ($values as $name => $value) {
+ if ($value !== null) {
+ switch ($name) {
+ case 'default_user':
+ if (!FreshRSS_user_Controller::checkUsername($value)) {
fail('FreshRSS invalid default username! default_user must be ASCII alphanumeric');
}
break;
- case 'disable-update':
- FreshRSS_Context::systemConf()->disable_update = true;
- break;
case 'environment':
- if (in_array($options['valid'][$param], ['development', 'production', 'silent'], true)) {
- FreshRSS_Context::systemConf()->environment = $options['valid'][$param];
- } else {
+ if (!in_array($value, ['development', 'production', 'silent'], true)) {
fail('FreshRSS invalid environment! environment must be one of { development, production, silent }');
}
break;
- case 'language':
- FreshRSS_Context::systemConf()->language = (string) $options['valid'][$param];
- break;
- case 'title':
- FreshRSS_Context::systemConf()->title = (string) $options['valid'][$param];
+ case 'auth_type':
+ if (!in_array($value, ['form', 'http_auth', 'none'], true)) {
+ fail('FreshRSS invalid authentication method! auth_type must be one of { form, http_auth, none }');
+ }
break;
}
+ // @phpstan-ignore-next-line (change to `@phpstan-ignore property.dynamicName` when upgrading to PHPStan 1.11+)
+ $systemConf->$name = $value;
}
}
-$db = FreshRSS_Context::systemConf()->db;
-foreach ($dBconfigParams as $dBparam => $configDbParam) {
- if (isset($options['valid'][$dBparam])) {
- $db[$configDbParam] = $options['valid'][$dBparam];
- }
-}
-/** @var array{'type':string,'host':string,'user':string,'password':string,'base':string,'prefix':string,
- * 'connection_uri_params':string,'pdo_options':array<int,int|string|bool>} $db */
+
+$db = array_merge(FreshRSS_Context::systemConf()->db, array_filter($dbValues));
+
+performRequirementCheck($db['type']);
+
FreshRSS_Context::systemConf()->db = $db;
FreshRSS_Context::systemConf()->save();
diff --git a/cli/update-user.php b/cli/update-user.php
index 9bb3ea7ff..ff0177997 100755
--- a/cli/update-user.php
+++ b/cli/update-user.php
@@ -1,26 +1,85 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
+require(__DIR__ . '/_cli.php');
-$isUpdate = true;
-require(__DIR__ . '/_update-or-create-user.php');
+$cliOptions = new class extends CliOptionsParser {
+ public string $user;
+ public string $password;
+ public string $apiPassword;
+ public string $language;
+ public string $email;
+ public string $token;
+ public int $purgeAfterMonths;
+ public int $feedMinArticles;
+ public int $feedTtl;
+ public int $sinceHoursPostsPerRss;
+ public int $maxPostsPerRss;
-$username = cliInitUser($GLOBALS['options']['valid']['user']);
+ public function __construct() {
+ $this->addRequiredOption('user', (new CliOption('user')));
+ $this->addOption('password', (new CliOption('password')));
+ $this->addOption('apiPassword', (new CliOption('api-password'))->deprecatedAs('api_password'));
+ $this->addOption('language', (new CliOption('language')));
+ $this->addOption('email', (new CliOption('email')));
+ $this->addOption('token', (new CliOption('token')));
+ $this->addOption(
+ 'purgeAfterMonths',
+ (new CliOption('purge-after-months'))->typeOfInt()->deprecatedAs('purge_after_months')
+ );
+ $this->addOption(
+ 'feedMinArticles',
+ (new CliOption('feed-min-articles-default'))->typeOfInt()->deprecatedAs('feed_min_articles_default')
+ );
+ $this->addOption(
+ 'feedTtl',
+ (new CliOption('feed-ttl-default'))->typeOfInt()->deprecatedAs('feed_ttl_default')
+ );
+ $this->addOption(
+ 'sinceHoursPostsPerRss',
+ (new CliOption('since-hours-posts-per-rss'))->typeOfInt()->deprecatedAs('since_hours_posts_per_rss')
+ );
+ $this->addOption(
+ 'maxPostsPerRss',
+ (new CliOption('max-posts-per-rss'))->typeOfInt()->deprecatedAs('max_posts_per_rss')
+ );
+ parent::__construct();
+ }
+};
+
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
+}
+
+$username = cliInitUser($cliOptions->user);
echo 'FreshRSS updating user “', $username, "”…\n";
+$values = [
+ 'language' => $cliOptions->language ?? null,
+ 'mail_login' => $cliOptions->email ?? null,
+ 'token' => $cliOptions->token ?? null,
+ 'old_entries' => $cliOptions->purgeAfterMonths ?? null,
+ 'keep_history_default' => $cliOptions->feedMinArticles ?? null,
+ 'ttl_default' => $cliOptions->feedTtl ?? null,
+ 'since_hours_posts_per_rss' => $cliOptions->sinceHoursPostsPerRss ?? null,
+ 'max_posts_per_rss' => $cliOptions->maxPostsPerRss ?? null,
+];
+
+$values = array_filter($values);
+
$ok = FreshRSS_user_Controller::updateUser(
$username,
- empty($options['valid']['email']) ? null : $options['valid']['email'],
- empty($options['valid']['password']) ? '' : $options['valid']['password'],
- $GLOBALS['values']);
+ isset($cliOptions->email) ? $cliOptions->email : null,
+ $cliOptions->password ?? '',
+ $values);
if (!$ok) {
fail('FreshRSS could not update user!');
}
-if (!empty($options['valid']['api_password'])) {
- $error = FreshRSS_api_Controller::updatePassword($options['valid']['api_password']);
+if (isset($cliOptions->apiPassword)) {
+ $error = FreshRSS_api_Controller::updatePassword($cliOptions->apiPassword);
if ($error) {
fail($error);
}
diff --git a/cli/user-info.php b/cli/user-info.php
index f492d26b2..6674ebc6b 100755
--- a/cli/user-info.php
+++ b/cli/user-info.php
@@ -5,45 +5,38 @@ require(__DIR__ . '/_cli.php');
const DATA_FORMAT = "%-7s | %-20s | %-5s | %-7s | %-25s | %-15s | %-10s | %-10s | %-10s | %-10s | %-10s | %-10s | %-5s | %-10s\n";
-$parameters = [
- 'long' => [
- 'user' => ':',
- 'header' => '',
- 'json' => '',
- 'human-readable' => '',
- ],
- 'short' => [
- 'human-readable' => 'h',
- ],
- 'deprecated' => [],
-];
+$cliOptions = new class extends CliOptionsParser {
+ /** @var array<int,string> $user */
+ public array $user;
+ public string $header;
+ public string $json;
+ public string $humanReadable;
-$options = parseCliParams($parameters);
+ public function __construct() {
+ $this->addOption('user', (new CliOption('user'))->typeOfArrayOfString());
+ $this->addOption('header', (new CliOption('header'))->withValueNone());
+ $this->addOption('json', (new CliOption('json'))->withValueNone());
+ $this->addOption('humanReadable', (new CliOption('human-readable', 'h'))->withValueNone());
+ parent::__construct();
+ }
+};
-if (!empty($options['invalid'])) {
- fail('Usage: ' . basename(__FILE__) . ' (--human-readable --header --json --user username --user username …)');
+if (!empty($cliOptions->errors)) {
+ fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
-if (empty($options['valid']['user'])) {
- $users = listUsers();
-} elseif (is_array($options['valid']['user'])) {
- /** @var array<string> $users */
- $users = $options['valid']['user'];
-} else {
- /** @var array<string> $users */
- $users = [$options['valid']['user']];
-}
+$users = $cliOptions->user ?? listUsers();
sort($users);
-$formatJson = isset($options['valid']['json']);
+$formatJson = isset($cliOptions->json);
$jsonOutput = [];
if ($formatJson) {
- unset($options['valid']['header']);
- unset($options['valid']['human-readable']);
+ unset($cliOptions->header);
+ unset($cliOptions->humanReadable);
}
-if (array_key_exists('header', $options['valid'])) {
+if (isset($cliOptions->header)) {
printf(
DATA_FORMAT,
'default',
@@ -92,7 +85,7 @@ foreach ($users as $username) {
'lang' => FreshRSS_Context::userConf()->language,
'mail_login' => FreshRSS_Context::userConf()->mail_login,
);
- if (isset($options['valid']['human-readable'])) { //Human format
+ if (isset($cliOptions->humanReadable)) { //Human format
$data['last_user_activity'] = date('c', $data['last_user_activity']);
$data['database_size'] = format_bytes($data['database_size']);
}