aboutsummaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2017-12-16 15:24:13 +0100
committerGravatar GitHub <noreply@github.com> 2017-12-16 15:24:13 +0100
commitfdc9e0d75a786101a14f64bc418b48fdd1cb4890 (patch)
tree9a7a1d523ab1279e2efce84d2d0c73dd0ad47c70 /cli
parentf7560c585f211be41b093906e3a8fb5a6071c660 (diff)
parentccb829418d25af49d129ac227b0cbd09c085b8a3 (diff)
Merge branch 'dev' into hebrew-i18n
Diffstat (limited to 'cli')
-rw-r--r--cli/.htaccess3
-rw-r--r--cli/README.md103
-rw-r--r--cli/_cli.php66
-rw-r--r--cli/_update-or-create-user.php56
-rwxr-xr-xcli/actualize-user.php23
-rw-r--r--cli/check.translation.php106
-rwxr-xr-xcli/create-user.php36
-rwxr-xr-xcli/db-optimize.php20
-rwxr-xr-xcli/delete-user.php32
-rwxr-xr-xcli/do-install.php100
-rwxr-xr-xcli/export-opml-for-user.php24
-rwxr-xr-xcli/export-zip-for-user.php30
-rw-r--r--cli/i18n/I18nCompletionValidator.php49
-rw-r--r--cli/i18n/I18nData.php118
-rw-r--r--cli/i18n/I18nFile.php92
-rw-r--r--cli/i18n/I18nUsageValidator.php47
-rw-r--r--cli/i18n/I18nValidatorInterface.php26
-rw-r--r--cli/i18n/ignore/en.php105
-rw-r--r--cli/i18n/ignore/fr.php55
-rwxr-xr-xcli/import-for-user.php35
-rw-r--r--cli/index.html13
-rwxr-xr-xcli/list-users.php15
-rw-r--r--cli/manipulate.translation.php79
-rwxr-xr-xcli/reconfigure.php60
-rwxr-xr-xcli/update-user.php23
-rwxr-xr-xcli/user-info.php50
26 files changed, 1366 insertions, 0 deletions
diff --git a/cli/.htaccess b/cli/.htaccess
new file mode 100644
index 000000000..9e768397d
--- /dev/null
+++ b/cli/.htaccess
@@ -0,0 +1,3 @@
+Order Allow,Deny
+Deny from all
+Satisfy all
diff --git a/cli/README.md b/cli/README.md
new file mode 100644
index 000000000..b7e8ac7f5
--- /dev/null
+++ b/cli/README.md
@@ -0,0 +1,103 @@
+* Back to [main read-me](../README.md)
+
+# FreshRSS Command-Line Interface (CLI)
+
+## Note on access rights
+
+When using the command-line interface, remember that your user might not be the same as the one used by your Web server.
+This might create some access right problems.
+
+It is recommended to invoke commands using the same user as your Web server:
+
+```sh
+cd /usr/share/FreshRSS
+sudo -u www-data sh -c './cli/list-users.php'
+```
+
+In any case, when you are done with a series of commands, you should re-apply the access rights:
+
+```sh
+cd /usr/share/FreshRSS
+sudo chown -R :www-data .
+sudo chmod -R g+r .
+sudo chmod -R g+w ./data/
+```
+
+
+## Commands
+
+Options in parenthesis are optional.
+
+
+```sh
+cd /usr/share/FreshRSS
+
+./cli/do-install.php --default_user admin ( --auth_type form --environment production --base_url https://rss.example.net/ --language en --title FreshRSS --allow_anonymous --api_enabled --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123 --db-base freshrss --db-prefix freshrss )
+# --auth_type can be: 'form' (default), 'http_auth' (using the Web server access control), 'none' (dangerous)
+# --db-type can be: 'sqlite' (default), 'mysql' (MySQL or MariaDB), 'pgsql' (PostgreSQL)
+# --environment can be: 'production' (default), 'development' (for additional log messages)
+# --language can be: 'en' (default), 'fr', or one of the [supported languages](../app/i18n/)
+# --db-prefix is an optional prefix in front of the names of the tables. We suggest using 'freshrss_'
+# This command does not create the default user. Do that with ./cli/create-user.php
+
+./cli/reconfigure.php
+# Same parameters as for do-install.php. Used to update an existing installation.
+
+./cli/create-user.php --user username ( --password 'password' --api_password 'api_password' --language en --email user@example.net --token 'longRandomString' --no_default_feeds --purge_after_months 3 --feed_min_articles_default 50 --feed_ttl_default 3600 --since_hours_posts_per_rss 168 --min_posts_per_rss 2 --max_posts_per_rss 400 )
+# --language can be: 'en' (default), 'fr', or one of the [supported languages](../app/i18n/)
+
+./cli/update-user.php --user username ( ... )
+# Same options as create-user.php, except --no_default_feeds which is only available for create-user.php
+
+./cli/delete-user.php --user username
+
+./cli/list-users.php
+# Return a list of users, with the default/admin user first
+
+./cli/actualize-user.php --user username
+
+./cli/import-for-user.php --user username --filename /path/to/file.ext
+# The extension of the file { .json, .opml, .xml, .zip } is used to detect the type of import
+
+./cli/export-opml-for-user.php --user username > /path/to/file.opml.xml
+
+./cli/export-zip-for-user.php --user username ( --max-feed-entries 100 ) > /path/to/file.zip
+
+./cli/user-info.php -h --user username
+# -h is to use a human-readable format
+# --user can be a username, or '*' to loop on all users
+# Returns: 1) a * iff the user is admin, 2) the name of the user,
+# 3) the date/time of last user action, 4) the size occupied,
+# and the number of: 5) categories, 6) feeds, 7) read articles, 8) unread articles, and 9) favourites
+
+./cli/db-optimize.php --user username
+# Optimize database (reduces the size) for a given user (perform `OPTIMIZE TABLE` in MySQL, `VACUUM` in SQLite)
+```
+
+
+## Unix piping
+
+It is possible to invoke a command multiple times, e.g. with different usernames, thanks to the `xargs -n1` command.
+Example showing user information for all users which username starts with 'a':
+
+```sh
+./cli/list-users.php | grep '^a' | xargs -n1 ./cli/user-info.php -h --user
+```
+
+Example showing all users ranked by date of last activity:
+
+```sh
+./cli/user-info.php -h --user '*' | sort -k2 -r
+```
+
+Example to get the number of feeds of a given user:
+
+```sh
+./cli/user-info.php --user alex | cut -f6
+```
+
+
+# Install and updates
+
+If you want to administrate FreshRSS using git, please read our [installation docs](https://freshrss.github.io/FreshRSS/en/admins/02_Installation.html)
+and [update guidelines](https://freshrss.github.io/FreshRSS/en/users/08_Updating.html). \ No newline at end of file
diff --git a/cli/_cli.php b/cli/_cli.php
new file mode 100644
index 000000000..72629171c
--- /dev/null
+++ b/cli/_cli.php
@@ -0,0 +1,66 @@
+<?php
+if (php_sapi_name() !== 'cli') {
+ die('FreshRSS error: This PHP script may only be invoked from command line!');
+}
+
+require(__DIR__ . '/../constants.php');
+require(LIB_PATH . '/lib_rss.php');
+require(LIB_PATH . '/lib_install.php');
+
+Minz_Configuration::register('system',
+ DATA_PATH . '/config.php',
+ FRESHRSS_PATH . '/config.default.php');
+FreshRSS_Context::$system_conf = Minz_Configuration::get('system');
+Minz_Translate::init('en');
+
+FreshRSS_Context::$isCli = true;
+
+function fail($message) {
+ fwrite(STDERR, $message . "\n");
+ die(1);
+}
+
+function cliInitUser($username) {
+ if (!FreshRSS_user_Controller::checkUsername($username)) {
+ fail('FreshRSS error: invalid username: ' . $username . "\n");
+ }
+
+ $usernames = listUsers();
+ if (!in_array($username, $usernames)) {
+ fail('FreshRSS error: user not found: ' . $username . "\n");
+ }
+
+ FreshRSS_Context::$user_conf = get_user_configuration($username);
+ if (FreshRSS_Context::$user_conf == null) {
+ fail('FreshRSS error: invalid configuration for user: ' . $username . "\n");
+ }
+ new Minz_ModelPdo($username);
+
+ return $username;
+}
+
+function accessRights() {
+ echo '• Remember to re-apply the appropriate access rights, such as:' , "\n",
+ "\t", 'sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/', "\n";
+}
+
+function done($ok = true) {
+ fwrite(STDERR, 'Result: ' . ($ok ? 'success' : 'fail') . "\n");
+ exit($ok ? 0 : 1);
+}
+
+function performRequirementCheck($databaseType) {
+ $requirements = checkRequirements($databaseType);
+ if ($requirements['all'] !== 'ok') {
+ $message = 'FreshRSS install failed requirements:' . "\n";
+ foreach ($requirements as $requirement => $check) {
+ if ($check !== 'ok' && !in_array($requirement, array('all', 'pdo', 'message'))) {
+ $message .= '• ' . $requirement . "\n";
+ }
+ }
+ if (!empty($requirements['message'])) {
+ $message .= '• ' . $requirements['message'] . "\n";
+ }
+ fail($message);
+ }
+}
diff --git a/cli/_update-or-create-user.php b/cli/_update-or-create-user.php
new file mode 100644
index 000000000..a5960b58a
--- /dev/null
+++ b/cli/_update-or-create-user.php
@@ -0,0 +1,56 @@
+<?php
+require(__DIR__ . '/_cli.php');
+
+$params = array(
+ 'user:',
+ 'password:',
+ 'api_password:',
+ 'language:',
+ 'email:',
+ 'token:',
+ 'purge_after_months:',
+ 'feed_min_articles_default:',
+ 'feed_ttl_default:',
+ 'since_hours_posts_per_rss:',
+ 'min_posts_per_rss:',
+ 'max_posts_per_rss:',
+ );
+
+if (!$isUpdate) {
+ $params[] = 'no_default_feeds'; //Only for creating new users
+}
+
+$options = getopt('', $params);
+
+if (empty($options['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 --min_posts_per_rss 2 --max_posts_per_rss 400 )");
+}
+
+function strParam($name) {
+ global $options;
+ return isset($options[$name]) ? strval($options[$name]) : null;
+}
+
+function intParam($name) {
+ global $options;
+ return isset($options[$name]) && ctype_digit($options[$name]) ? intval($options[$name]) : null;
+}
+
+$values = array(
+ 'language' => strParam('language'),
+ 'mail_login' => strParam('email'),
+ 'token' => strParam('token'),
+ 'old_entries' => intParam('purge_after_months'),
+ 'keep_history_default' => intParam('feed_min_articles_default'),
+ 'ttl_default' => intParam('feed_ttl_default'),
+ 'since_hours_posts_per_rss' => intParam('since_hours_posts_per_rss'),
+ 'min_posts_per_rss' => intParam('min_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
new file mode 100755
index 000000000..dd07fc142
--- /dev/null
+++ b/cli/actualize-user.php
@@ -0,0 +1,23 @@
+#!/usr/bin/php
+<?php
+require(__DIR__ . '/_cli.php');
+
+$options = getopt('', array(
+ 'user:',
+ ));
+
+if (empty($options['user'])) {
+ fail('Usage: ' . basename(__FILE__) . " --user username");
+}
+
+$username = cliInitUser($options['user']);
+
+fwrite(STDERR, 'FreshRSS actualizing user “' . $username . "”…\n");
+
+list($nbUpdatedFeeds, $feed, $nbNewArticles) = FreshRSS_feed_Controller::actualizeFeed(0, '', true);
+
+echo "FreshRSS actualized $nbUpdatedFeeds feeds for $username ($nbNewArticles new articles)\n";
+
+invalidateHttpCache($username);
+
+done($nbUpdatedFeeds > 0);
diff --git a/cli/check.translation.php b/cli/check.translation.php
new file mode 100644
index 000000000..6ebd12973
--- /dev/null
+++ b/cli/check.translation.php
@@ -0,0 +1,106 @@
+<?php
+
+require_once __DIR__ . '/i18n/I18nFile.php';
+require_once __DIR__ . '/i18n/I18nCompletionValidator.php';
+require_once __DIR__ . '/i18n/I18nUsageValidator.php';
+
+$i18nFile = new I18nFile();
+$i18nData = $i18nFile->load();
+
+$options = getopt("dhl:r");
+
+if (array_key_exists('h', $options)) {
+ help();
+}
+if (array_key_exists('l', $options)) {
+ $languages = array($options['l']);
+} else {
+ $languages = $i18nData->getAvailableLanguages();
+}
+$displayResults = array_key_exists('d', $options);
+$displayReport = array_key_exists('r', $options);
+
+$isValidated = true;
+$result = array();
+$report = array();
+
+foreach ($languages as $language) {
+ if ($language === $i18nData::REFERENCE_LANGUAGE) {
+ $i18nValidator = new I18nUsageValidator($i18nData->getReferenceLanguage(), findUsedTranslations());
+ $isValidated = $i18nValidator->validate(include __DIR__ . '/i18n/ignore/' . $language . '.php') && $isValidated;
+ } else {
+ $i18nValidator = new I18nCompletionValidator($i18nData->getReferenceLanguage(), $i18nData->getLanguage($language));
+ if (file_exists(__DIR__ . '/i18n/ignore/' . $language . '.php')) {
+ $isValidated = $i18nValidator->validate(include __DIR__ . '/i18n/ignore/' . $language . '.php') && $isValidated;
+ } else {
+ $isValidated = $i18nValidator->validate(null) && $isValidated;
+ }
+ }
+
+ $report[$language] = sprintf('%-5s - %s', $language, $i18nValidator->displayReport());
+ $result[$language] = $i18nValidator->displayResult();
+}
+
+if ($displayResults) {
+ foreach ($result as $lang => $value) {
+ echo 'Language: ', $lang, PHP_EOL;
+ print_r($value);
+ echo PHP_EOL;
+ }
+}
+
+if ($displayReport) {
+ foreach ($report as $value) {
+ echo $value;
+ }
+}
+
+if (!$isValidated) {
+ exit(1);
+}
+
+/**
+ * Find used translation keys in the project
+ *
+ * Iterates through all php and phtml files in the whole project and extracts all
+ * translation keys used.
+ *
+ * @return array
+ */
+function findUsedTranslations() {
+ $directory = new RecursiveDirectoryIterator(__DIR__ . '/..');
+ $iterator = new RecursiveIteratorIterator($directory);
+ $regex = new RegexIterator($iterator, '/^.+\.(php|phtml)$/i', RecursiveRegexIterator::GET_MATCH);
+ $usedI18n = array();
+ foreach (array_keys(iterator_to_array($regex)) as $file) {
+ $fileContent = file_get_contents($file);
+ preg_match_all('/_t\([\'"](?P<strings>[^\'"]+)[\'"]/', $fileContent, $matches);
+ $usedI18n = array_merge($usedI18n, $matches['strings']);
+ }
+ return $usedI18n;
+}
+
+/**
+ * Output help message.
+ */
+function help() {
+ $help = <<<HELP
+NAME
+ %s
+
+SYNOPSIS
+ php %s [OPTION]...
+
+DESCRIPTION
+ Check if translation files have missing keys or missing translations.
+
+ -d display results.
+ -h display this help and exit.
+ -l=LANG filter by LANG.
+ -r display completion report.
+
+HELP;
+ $file = str_replace(__DIR__ . '/', '', __FILE__);
+ echo sprintf($help, $file, $file);
+ exit;
+}
diff --git a/cli/create-user.php b/cli/create-user.php
new file mode 100755
index 000000000..5bc6c1e6c
--- /dev/null
+++ b/cli/create-user.php
@@ -0,0 +1,36 @@
+#!/usr/bin/php
+<?php
+$isUpdate = false;
+require(__DIR__ . '/_update-or-create-user.php');
+
+$username = $options['user'];
+if (!FreshRSS_user_Controller::checkUsername($username)) {
+ fail('FreshRSS error: invalid username “' . $username .
+ '”! Must be matching ' . FreshRSS_user_Controller::USERNAME_PATTERN);
+}
+
+$usernames = listUsers();
+if (preg_grep("/^$username$/i", $usernames)) {
+ fail('FreshRSS error: username already taken “' . $username . '”');
+}
+
+echo 'FreshRSS creating user “', $username, "”…\n";
+
+$ok = FreshRSS_user_Controller::createUser($username,
+ empty($options['password']) ? '' : $options['password'],
+ empty($options['api_password']) ? '' : $options['api_password'],
+ $values,
+ !isset($options['no-default-feeds']));
+
+if (!$ok) {
+ fail('FreshRSS could not create user!');
+}
+
+invalidateHttpCache(FreshRSS_Context::$system_conf->default_user);
+
+echo '• Remember to refresh the feeds of the user: ', $username , "\n",
+ "\t", './cli/actualize-user.php --user ', $username, "\n";
+
+accessRights();
+
+done($ok);
diff --git a/cli/db-optimize.php b/cli/db-optimize.php
new file mode 100755
index 000000000..39dc97638
--- /dev/null
+++ b/cli/db-optimize.php
@@ -0,0 +1,20 @@
+#!/usr/bin/php
+<?php
+require(__DIR__ . '/_cli.php');
+
+$options = getopt('', array(
+ 'user:',
+ ));
+
+if (empty($options['user'])) {
+ fail('Usage: ' . basename(__FILE__) . " --user username");
+}
+
+$username = cliInitUser($options['user']);
+
+echo 'FreshRSS optimizing database for user “', $username, "”…\n";
+
+$databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
+$ok = $databaseDAO->optimize();
+
+done($ok);
diff --git a/cli/delete-user.php b/cli/delete-user.php
new file mode 100755
index 000000000..30cc31754
--- /dev/null
+++ b/cli/delete-user.php
@@ -0,0 +1,32 @@
+#!/usr/bin/php
+<?php
+require(__DIR__ . '/_cli.php');
+
+$options = getopt('', array(
+ 'user:',
+ ));
+
+if (empty($options['user'])) {
+ fail('Usage: ' . basename(__FILE__) . " --user username");
+}
+$username = $options['user'];
+if (!FreshRSS_user_Controller::checkUsername($username)) {
+ fail('FreshRSS error: invalid username “' . $username . '”');
+}
+
+$usernames = listUsers();
+if (!preg_grep("/^$username$/i", $usernames)) {
+ fail('FreshRSS error: username not found “' . $username . '”');
+}
+
+if (strcasecmp($username, FreshRSS_Context::$system_conf->default_user) === 0) {
+ fail('FreshRSS error: default user must not be deleted: “' . $username . '”');
+}
+
+echo 'FreshRSS deleting user “', $username, "”…\n";
+
+$ok = FreshRSS_user_Controller::deleteUser($username);
+
+invalidateHttpCache(FreshRSS_Context::$system_conf->default_user);
+
+done($ok);
diff --git a/cli/do-install.php b/cli/do-install.php
new file mode 100755
index 000000000..4ebba0469
--- /dev/null
+++ b/cli/do-install.php
@@ -0,0 +1,100 @@
+#!/usr/bin/php
+<?php
+require(__DIR__ . '/_cli.php');
+
+if (!file_exists(DATA_PATH . '/do-install.txt')) {
+ fail('FreshRSS looks to be already installed! Please use `./cli/reconfigure.php` instead.');
+}
+
+$params = array(
+ 'environment:',
+ 'base_url:',
+ 'language:',
+ 'title:',
+ 'default_user:',
+ 'allow_anonymous',
+ 'allow_anonymous_refresh',
+ 'auth_type:',
+ 'api_enabled',
+ 'allow_robots',
+ 'disable_update',
+ );
+
+$dBparams = array(
+ 'db-type:',
+ 'db-host:',
+ 'db-user:',
+ 'db-password:',
+ 'db-base:',
+ 'db-prefix:',
+ );
+
+$options = getopt('', array_merge($params, $dBparams));
+
+if (empty($options['default_user'])) {
+ fail('Usage: ' . basename(__FILE__) . " --default_user admin ( --auth_type form" .
+ " --environment production --base_url https://rss.example.net/" .
+ " --language en --title FreshRSS --allow_anonymous --api_enabled" .
+ " --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123" .
+ " --db-base freshrss --db-prefix freshrss_ --disable_update )");
+}
+
+fwrite(STDERR, 'FreshRSS install…' . "\n");
+
+$config = array(
+ 'salt' => generateSalt(),
+ 'db' => FreshRSS_Context::$system_conf->db,
+ );
+
+foreach ($params as $param) {
+ $param = rtrim($param, ':');
+ if (isset($options[$param])) {
+ $config[$param] = $options[$param] === false ? true : $options[$param];
+ }
+}
+
+if ((!empty($config['base_url'])) && server_is_public($config['base_url'])) {
+ $config['pubsubhubbub_enabled'] = true;
+}
+
+foreach ($dBparams as $dBparam) {
+ $dBparam = rtrim($dBparam, ':');
+ if (isset($options[$dBparam])) {
+ $param = substr($dBparam, strlen('db-'));
+ $config['db'][$param] = $options[$dBparam];
+ }
+}
+
+performRequirementCheck($config['db']['type']);
+
+if (!FreshRSS_user_Controller::checkUsername($options['default_user'])) {
+ fail('FreshRSS error: invalid default username “' . $options['default_user']
+ . '”! Must be matching ' . FreshRSS_user_Controller::USERNAME_PATTERN);
+}
+
+if (isset($options['auth_type']) && !in_array($options['auth_type'], array('form', 'http_auth', 'none'))) {
+ fail('FreshRSS invalid authentication method (auth_type must be one of { form, http_auth, none }): '
+ . $options['auth_type']);
+}
+
+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'));
+}
+
+$config['db']['default_user'] = $config['default_user'];
+if (!checkDb($config['db'])) {
+ @unlink(join_path(DATA_PATH, 'config.php'));
+ fail('FreshRSS database error: ' . (empty($config['db']['error']) ? 'Unknown error' : $config['db']['error']));
+}
+
+echo '• Remember to create the default user: ', $config['default_user'] , "\n",
+ "\t", './cli/create-user.php --user ', $config['default_user'], " --password 'password' --more-options\n";
+
+accessRights();
+
+if (!deleteInstall()) {
+ fail('FreshRSS access right problem while deleting install file!');
+}
+
+done();
diff --git a/cli/export-opml-for-user.php b/cli/export-opml-for-user.php
new file mode 100755
index 000000000..fd0993c35
--- /dev/null
+++ b/cli/export-opml-for-user.php
@@ -0,0 +1,24 @@
+#!/usr/bin/php
+<?php
+require(__DIR__ . '/_cli.php');
+
+$options = getopt('', array(
+ 'user:',
+ ));
+
+if (empty($options['user'])) {
+ fail('Usage: ' . basename(__FILE__) . " --user username > /path/to/file.opml.xml");
+}
+
+$username = cliInitUser($options['user']);
+
+fwrite(STDERR, 'FreshRSS exporting OPML for user “' . $username . "”…\n");
+
+$importController = new FreshRSS_importExport_Controller();
+
+$ok = false;
+$ok = $importController->exportFile(true, false, array(), 0, $username);
+
+invalidateHttpCache($username);
+
+done($ok);
diff --git a/cli/export-zip-for-user.php b/cli/export-zip-for-user.php
new file mode 100755
index 000000000..916ecbb45
--- /dev/null
+++ b/cli/export-zip-for-user.php
@@ -0,0 +1,30 @@
+#!/usr/bin/php
+<?php
+require(__DIR__ . '/_cli.php');
+
+$options = getopt('', array(
+ 'user:',
+ 'max-feed-entries:',
+ ));
+
+if (empty($options['user'])) {
+ fail('Usage: ' . basename(__FILE__) . " --user username ( --max-feed-entries 100 ) > /path/to/file.zip");
+}
+
+$username = cliInitUser($options['user']);
+
+fwrite(STDERR, 'FreshRSS exporting ZIP for user “' . $username . "”…\n");
+
+$importController = new FreshRSS_importExport_Controller();
+
+$ok = false;
+try {
+ $ok = $importController->exportFile(true, true, true,
+ empty($options['max-feed-entries']) ? 100 : intval($options['max-feed-entries']),
+ $username);
+} catch (FreshRSS_ZipMissing_Exception $zme) {
+ fail('FreshRSS error: Lacking php-zip extension!');
+}
+invalidateHttpCache($username);
+
+done($ok);
diff --git a/cli/i18n/I18nCompletionValidator.php b/cli/i18n/I18nCompletionValidator.php
new file mode 100644
index 000000000..2cb71acd5
--- /dev/null
+++ b/cli/i18n/I18nCompletionValidator.php
@@ -0,0 +1,49 @@
+<?php
+
+require_once __DIR__ . '/I18nValidatorInterface.php';
+
+class I18nCompletionValidator implements I18nValidatorInterface {
+
+ private $reference;
+ private $language;
+ private $totalEntries = 0;
+ private $passEntries = 0;
+ private $result = '';
+
+ public function __construct($reference, $language) {
+ $this->reference = $reference;
+ $this->language = $language;
+ }
+
+ public function displayReport() {
+ return sprintf('Translation is %5.1f%% complete.', $this->passEntries / $this->totalEntries * 100) . PHP_EOL;
+ }
+
+ public function displayResult() {
+ return $this->result;
+ }
+
+ public function validate($ignore) {
+ foreach ($this->reference as $file => $data) {
+ foreach ($data as $key => $value) {
+ $this->totalEntries++;
+ if (is_array($ignore) && in_array($key, $ignore)) {
+ $this->passEntries++;
+ continue;
+ }
+ if (!array_key_exists($key, $this->language[$file])) {
+ $this->result .= sprintf('Missing key %s', $key) . PHP_EOL;
+ continue;
+ }
+ if ($value === $this->language[$file][$key]) {
+ $this->result .= sprintf('Untranslated key %s - %s', $key, $value) . PHP_EOL;
+ continue;
+ }
+ $this->passEntries++;
+ }
+ }
+
+ return $this->totalEntries === $this->passEntries;
+ }
+
+}
diff --git a/cli/i18n/I18nData.php b/cli/i18n/I18nData.php
new file mode 100644
index 000000000..cd8ba0765
--- /dev/null
+++ b/cli/i18n/I18nData.php
@@ -0,0 +1,118 @@
+<?php
+
+class I18nData {
+
+ const REFERENCE_LANGUAGE = 'en';
+
+ private $data = array();
+ private $originalData = array();
+
+ public function __construct($data) {
+ $this->data = $data;
+ $this->originalData = $data;
+ }
+
+ public function getData() {
+ return $this->data;
+ }
+
+ /**
+ * Return the available languages
+ *
+ * @return array
+ */
+ public function getAvailableLanguages() {
+ $languages = array_keys($this->data);
+ sort($languages);
+
+ return $languages;
+ }
+
+ /**
+ * Add a new language. It's a copy of the reference language.
+ *
+ * @param string $language
+ */
+ public function addLanguage($language) {
+ if (array_key_exists($language, $this->data)) {
+ throw new Exception('The selected language already exist.');
+ }
+ $this->data[$language] = $this->data[static::REFERENCE_LANGUAGE];
+ }
+
+ /**
+ * Add a key in the reference language
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function addKey($key, $value) {
+ if (array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
+ throw new Exception('The selected key already exist.');
+ }
+ $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)][$key] = $value;
+ }
+
+ /**
+ * Duplicate a key from the reference language to all other languages
+ *
+ * @param string $key
+ */
+ public function duplicateKey($key) {
+ if (!array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
+ throw new Exception('The selected key does not exist.');
+ }
+ $value = $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)][$key];
+ foreach ($this->getAvailableLanguages() as $language) {
+ if (static::REFERENCE_LANGUAGE === $language) {
+ continue;
+ }
+ if (array_key_exists($key, $this->data[$language][$this->getFilenamePrefix($key)])) {
+ throw new Exception(sprintf('The selected key already exist in %s.', $language));
+ }
+ $this->data[$language][$this->getFilenamePrefix($key)][$key] = $value;
+ }
+ }
+
+ /**
+ * Remove a key in all languages
+ *
+ * @param string $key
+ */
+ public function removeKey($key) {
+ if (!array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
+ throw new Exception('The selected key does not exist.');
+ }
+ foreach ($this->getAvailableLanguages() as $language) {
+ if (array_key_exists($key, $this->data[$language][$this->getFilenamePrefix($key)])) {
+ unset($this->data[$language][$this->getFilenamePrefix($key)][$key]);
+ }
+ }
+ }
+
+ /**
+ * Check if the data has changed
+ *
+ * @return bool
+ */
+ public function hasChanged() {
+ return $this->data !== $this->originalData;
+ }
+
+ public function getLanguage($language) {
+ return $this->data[$language];
+ }
+
+ public function getReferenceLanguage() {
+ return $this->getLanguage(static::REFERENCE_LANGUAGE);
+ }
+
+ /**
+ * @param string $key
+ * @return string
+ */
+ private function getFilenamePrefix($key) {
+ return preg_replace('/\..*/', '.php', $key);
+ }
+
+}
diff --git a/cli/i18n/I18nFile.php b/cli/i18n/I18nFile.php
new file mode 100644
index 000000000..d6489ee21
--- /dev/null
+++ b/cli/i18n/I18nFile.php
@@ -0,0 +1,92 @@
+<?php
+
+require_once __DIR__ . '/I18nData.php';
+
+class i18nFile {
+
+ private $i18nPath;
+
+ public function __construct() {
+ $this->i18nPath = __DIR__ . '/../../app/i18n';
+ }
+
+ public function load() {
+ $dirs = new DirectoryIterator($this->i18nPath);
+ foreach ($dirs as $dir) {
+ if ($dir->isDot()) {
+ continue;
+ }
+ $files = new DirectoryIterator($dir->getPathname());
+ foreach ($files as $file) {
+ if (!$file->isFile()) {
+ continue;
+ }
+ $i18n[$dir->getFilename()][$file->getFilename()] = $this->flatten(include $file->getPathname(), $file->getBasename('.php'));
+ }
+ }
+
+ return new I18nData($i18n);
+ }
+
+ public function dump(I18nData $i18n) {
+ foreach ($i18n->getData() as $language => $file) {
+ $dir = $this->i18nPath . DIRECTORY_SEPARATOR . $language;
+ if (!file_exists($dir)) {
+ mkdir($dir);
+ }
+ foreach ($file as $name => $content) {
+ $filename = $dir . DIRECTORY_SEPARATOR . $name;
+ $fullContent = var_export($this->unflatten($content), true);
+ file_put_contents($filename, sprintf('<?php return %s;', $fullContent));
+ }
+ }
+ }
+
+ /**
+ * Flatten an array of translation
+ *
+ * @param array $translation
+ * @param string $prefix
+ * @return array
+ */
+ private function flatten($translation, $prefix = '') {
+ $a = array();
+
+ if ('' !== $prefix) {
+ $prefix .= '.';
+ }
+
+ foreach ($translation as $key => $value) {
+ if (is_array($value)) {
+ $a += $this->flatten($value, $prefix . $key);
+ } else {
+ $a[$prefix . $key] = $value;
+ }
+ }
+
+ return $a;
+ }
+
+ /**
+ * Unflatten an array of translation
+ *
+ * The first key is dropped since it represents the filename and we have
+ * no use of it.
+ *
+ * @param array $translation
+ * @return array
+ */
+ private function unflatten($translation) {
+ $a = array();
+
+ ksort($translation);
+ foreach ($translation as $compoundKey => $value) {
+ $keys = explode('.', $compoundKey);
+ array_shift($keys);
+ eval("\$a['" . implode("']['", $keys) . "'] = '" . $value . "';");
+ }
+
+ return $a;
+ }
+
+}
diff --git a/cli/i18n/I18nUsageValidator.php b/cli/i18n/I18nUsageValidator.php
new file mode 100644
index 000000000..8ab934971
--- /dev/null
+++ b/cli/i18n/I18nUsageValidator.php
@@ -0,0 +1,47 @@
+<?php
+
+require_once __DIR__ . '/I18nValidatorInterface.php';
+
+class I18nUsageValidator implements I18nValidatorInterface {
+
+ private $code;
+ private $reference;
+ private $totalEntries = 0;
+ private $failedEntries = 0;
+ private $result = '';
+
+ public function __construct($reference, $code) {
+ $this->code = $code;
+ $this->reference = $reference;
+ }
+
+ public function displayReport() {
+ return sprintf('%5.1f%% of translation keys are unused.', $this->failedEntries / $this->totalEntries * 100) . PHP_EOL;
+ }
+
+ public function displayResult() {
+ return $this->result;
+ }
+
+ public function validate($ignore) {
+ foreach ($this->reference as $file => $data) {
+ foreach ($data as $key => $value) {
+ $this->totalEntries++;
+ if (preg_match('/\._$/', $key) && in_array(preg_replace('/\._$/', '', $key), $this->code)) {
+ continue;
+ }
+ if (is_array($ignore) && in_array($key, $ignore)) {
+ continue;
+ }
+ if (!in_array($key, $this->code)) {
+ $this->result .= sprintf('Unused key %s - %s', $key, $value) . PHP_EOL;
+ $this->failedEntries++;
+ continue;
+ }
+ }
+ }
+
+ return 0 === $this->failedEntries;
+ }
+
+}
diff --git a/cli/i18n/I18nValidatorInterface.php b/cli/i18n/I18nValidatorInterface.php
new file mode 100644
index 000000000..edfe7aac0
--- /dev/null
+++ b/cli/i18n/I18nValidatorInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+interface I18nValidatorInterface {
+
+ /**
+ * Display the validation result.
+ * Empty if there are no errors.
+ *
+ * @return array
+ */
+ public function displayResult();
+
+ /**
+ * @param array $ignore Keys to ignore for validation
+ * @return bool
+ */
+ public function validate($ignore);
+
+ /**
+ * Display the validation report.
+ *
+ * @return array
+ */
+ public function displayReport();
+
+}
diff --git a/cli/i18n/ignore/en.php b/cli/i18n/ignore/en.php
new file mode 100644
index 000000000..e231afdda
--- /dev/null
+++ b/cli/i18n/ignore/en.php
@@ -0,0 +1,105 @@
+<?php
+
+return array(
+ 'admin.check_install.cache.nok',
+ 'admin.check_install.cache.ok',
+ 'admin.check_install.categories.nok',
+ 'admin.check_install.categories.ok',
+ 'admin.check_install.connection.nok',
+ 'admin.check_install.connection.ok',
+ 'admin.check_install.ctype.nok',
+ 'admin.check_install.ctype.ok',
+ 'admin.check_install.curl.nok',
+ 'admin.check_install.curl.ok',
+ 'admin.check_install.data.nok',
+ 'admin.check_install.data.ok',
+ 'admin.check_install.dom.nok',
+ 'admin.check_install.dom.ok',
+ 'admin.check_install.entries.nok',
+ 'admin.check_install.entries.ok',
+ 'admin.check_install.favicons.nok',
+ 'admin.check_install.favicons.ok',
+ 'admin.check_install.feeds.nok',
+ 'admin.check_install.feeds.ok',
+ 'admin.check_install.fileinfo.nok',
+ 'admin.check_install.fileinfo.ok',
+ 'admin.check_install.json.nok',
+ 'admin.check_install.json.ok',
+ 'admin.check_install.minz.nok',
+ 'admin.check_install.minz.ok',
+ 'admin.check_install.pcre.nok',
+ 'admin.check_install.pcre.ok',
+ 'admin.check_install.pdo.nok',
+ 'admin.check_install.pdo.ok',
+ 'admin.check_install.php.nok',
+ 'admin.check_install.php.ok',
+ 'admin.check_install.tables.nok',
+ 'admin.check_install.tables.ok',
+ 'admin.check_install.tokens.nok',
+ 'admin.check_install.tokens.ok',
+ 'admin.check_install.users.nok',
+ 'admin.check_install.users.ok',
+ 'admin.check_install.zip.nok',
+ 'admin.check_install.zip.ok',
+ 'conf.query.get_all',
+ 'conf.query.get_category',
+ 'conf.query.get_favorite',
+ 'conf.query.get_feed',
+ 'conf.query.order_asc',
+ 'conf.query.order_desc',
+ 'conf.query.state_0',
+ 'conf.query.state_1',
+ 'conf.query.state_2',
+ 'conf.query.state_3',
+ 'conf.query.state_4',
+ 'conf.query.state_5',
+ 'conf.query.state_6',
+ 'conf.query.state_7',
+ 'conf.query.state_8',
+ 'conf.query.state_9',
+ 'conf.query.state_10',
+ 'conf.query.state_11',
+ 'conf.query.state_12',
+ 'conf.query.state_13',
+ 'conf.query.state_14',
+ 'conf.query.state_15',
+ 'conf.sharing.blogotext',
+ 'conf.sharing.diaspora',
+ 'conf.sharing.email',
+ 'conf.sharing.facebook',
+ 'conf.sharing.g+',
+ 'conf.sharing.print',
+ 'conf.sharing.shaarli',
+ 'conf.sharing.twitter',
+ 'conf.sharing.wallabag',
+ 'gen.lang.cz',
+ 'gen.lang.de',
+ 'gen.lang.en',
+ 'gen.lang.es',
+ 'gen.lang.fr',
+ 'gen.lang.it',
+ 'gen.lang.kr',
+ 'gen.lang.nl',
+ 'gen.lang.pt-br',
+ 'gen.lang.ru',
+ 'gen.lang.tr',
+ 'gen.lang.zh-cn',
+ 'gen.share.blogotext',
+ 'gen.share.diaspora',
+ 'gen.share.email',
+ 'gen.share.facebook',
+ 'gen.share.g+',
+ 'gen.share.movim',
+ 'gen.share.print',
+ 'gen.share.shaarli',
+ 'gen.share.twitter',
+ 'gen.share.wallabag',
+ 'gen.share.wallabagv2',
+ 'gen.share.jdh',
+ 'gen.share.Known',
+ 'gen.share.gnusocial',
+ 'index.menu.non-starred',
+ 'index.menu.read',
+ 'index.menu.starred',
+ 'index.menu.unread',
+);
diff --git a/cli/i18n/ignore/fr.php b/cli/i18n/ignore/fr.php
new file mode 100644
index 000000000..0ac2e8758
--- /dev/null
+++ b/cli/i18n/ignore/fr.php
@@ -0,0 +1,55 @@
+<?php
+
+return array(
+ 'admin.extensions.title',
+ 'admin.stats.number_entries',
+ 'admin.user.articles_and_size',
+ 'conf.display.width.large',
+ 'conf.sharing.blogotext',
+ 'conf.sharing.diaspora',
+ 'conf.sharing.facebook',
+ 'conf.sharing.g+',
+ 'conf.sharing.print',
+ 'conf.sharing.shaarli',
+ 'conf.sharing.twitter',
+ 'conf.sharing.wallabag',
+ 'conf.shortcut.navigation',
+ 'conf.user.articles_and_size',
+ 'gen.freshrss._',
+ 'gen.lang.cz',
+ 'gen.lang.de',
+ 'gen.lang.en',
+ 'gen.lang.es',
+ 'gen.lang.fr',
+ 'gen.lang.it',
+ 'gen.lang.kr',
+ 'gen.lang.nl',
+ 'gen.lang.pt-br',
+ 'gen.lang.ru',
+ 'gen.lang.tr',
+ 'gen.lang.zh-cn',
+ 'gen.menu.admin',
+ 'gen.menu.configuration',
+ 'gen.menu.extensions',
+ 'gen.menu.logs',
+ 'gen.share.blogotext',
+ 'gen.share.diaspora',
+ 'gen.share.facebook',
+ 'gen.share.g+',
+ 'gen.share.movim',
+ 'gen.share.shaarli',
+ 'gen.share.twitter',
+ 'gen.share.wallabag',
+ 'gen.share.wallabagv2',
+ 'gen.share.jdh',
+ 'gen.share.gnusocial',
+ 'index.about.agpl3',
+ 'index.about.version',
+ 'index.log._',
+ 'index.log.title',
+ 'install.title',
+ 'install.this_is_the_end',
+ 'sub.bookmarklet.title',
+ 'sub.feed.description',
+ 'sub.feed.number_entries',
+);
diff --git a/cli/import-for-user.php b/cli/import-for-user.php
new file mode 100755
index 000000000..95ff18c8c
--- /dev/null
+++ b/cli/import-for-user.php
@@ -0,0 +1,35 @@
+#!/usr/bin/php
+<?php
+require(__DIR__ . '/_cli.php');
+
+$options = getopt('', array(
+ 'user:',
+ 'filename:',
+ ));
+
+if (empty($options['user']) || empty($options['filename'])) {
+ fail('Usage: ' . basename(__FILE__) . " --user username --filename /path/to/file.ext");
+}
+
+$username = cliInitUser($options['user']);
+
+$filename = $options['filename'];
+if (!is_readable($filename)) {
+ fail('FreshRSS error: file is not readable “' . $filename . '”');
+}
+
+echo 'FreshRSS importing ZIP/OPML/JSON for user “', $username, "”…\n";
+
+$importController = new FreshRSS_importExport_Controller();
+
+$ok = false;
+try {
+ $ok = $importController->importFile($filename, $filename, $username);
+} catch (FreshRSS_ZipMissing_Exception $zme) {
+ fail('FreshRSS error: Lacking php-zip extension!');
+} catch (FreshRSS_Zip_Exception $ze) {
+ fail('FreshRSS error: ZIP archive cannot be imported! Error code: ' . $ze->zipErrorCode());
+}
+invalidateHttpCache($username);
+
+done($ok);
diff --git a/cli/index.html b/cli/index.html
new file mode 100644
index 000000000..85faaa37e
--- /dev/null
+++ b/cli/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">
+<head>
+<meta charset="UTF-8" />
+<meta http-equiv="Refresh" content="0; url=/" />
+<title>Redirection</title>
+<meta name="robots" content="noindex" />
+</head>
+
+<body>
+<p><a href="/">Redirection</a></p>
+</body>
+</html>
diff --git a/cli/list-users.php b/cli/list-users.php
new file mode 100755
index 000000000..758bbdb46
--- /dev/null
+++ b/cli/list-users.php
@@ -0,0 +1,15 @@
+#!/usr/bin/php
+<?php
+require(__DIR__ . '/_cli.php');
+
+$users = listUsers();
+sort($users);
+if (FreshRSS_Context::$system_conf->default_user !== ''
+ && in_array(FreshRSS_Context::$system_conf->default_user, $users, true)) {
+ array_unshift($users, FreshRSS_Context::$system_conf->default_user);
+ $users = array_unique($users);
+}
+
+foreach ($users as $user) {
+ echo $user, "\n";
+}
diff --git a/cli/manipulate.translation.php b/cli/manipulate.translation.php
new file mode 100644
index 000000000..aace5723a
--- /dev/null
+++ b/cli/manipulate.translation.php
@@ -0,0 +1,79 @@
+<?php
+
+$options = getopt("h");
+
+if (array_key_exists('h', $options)) {
+ help();
+}
+
+if (1 === $argc || 4 < $argc) {
+ help();
+}
+
+require_once __DIR__ . '/i18n/I18nFile.php';
+
+$i18nFile = new I18nFile();
+$i18nData = $i18nFile->load();
+
+switch ($argv[1]) {
+ case 'add_language' :
+ $i18nData->addLanguage($argv[2]);
+ break;
+ case 'add_key' :
+ if (3 === $argc) {
+ help();
+ }
+ $i18nData->addKey($argv[2], $argv[3]);
+ break;
+ case 'duplicate_key' :
+ $i18nData->duplicateKey($argv[2]);
+ break;
+ case 'delete_key' :
+ $i18nData->removeKey($argv[2]);
+ break;
+ default :
+ help();
+}
+
+if ($i18nData->hasChanged()) {
+ $i18nFile->dump($i18nData);
+}
+
+/**
+ * Output help message.
+ */
+function help() {
+ $help = <<<HELP
+NAME
+ %s
+
+SYNOPSIS
+ php %s [OPTION] [OPERATION] [KEY] [VALUE]
+
+DESCRIPTION
+ Manipulate translation files. Available operations are
+ Check if translation files have missing keys or missing translations.
+
+ -h display this help and exit.
+
+OPERATION
+ add_language
+ add a new language by duplicating the referential. This operation
+ needs only a KEY.
+
+ add_key add a new key in the referential. This operation needs a KEY and
+ a VALUE.
+
+ duplicate_key
+ duplicate a referential key in other languages. This operation
+ needs only a KEY.
+
+ delete_key
+ delete a referential key from all languages. This operation needs
+ only a KEY.
+
+HELP;
+ $file = str_replace(__DIR__ . '/', '', __FILE__);
+ echo sprintf($help, $file, $file);
+ exit;
+}
diff --git a/cli/reconfigure.php b/cli/reconfigure.php
new file mode 100755
index 000000000..cfe713fa8
--- /dev/null
+++ b/cli/reconfigure.php
@@ -0,0 +1,60 @@
+#!/usr/bin/php
+<?php
+require(__DIR__ . '/_cli.php');
+
+$params = array(
+ 'environment:',
+ 'base_url:',
+ 'language:',
+ 'title:',
+ 'default_user:',
+ 'allow_anonymous',
+ 'allow_anonymous_refresh',
+ 'auth_type:',
+ 'api_enabled',
+ 'allow_robots',
+ 'disable_update',
+ );
+
+$dBparams = array(
+ 'db-type:',
+ 'db-host:',
+ 'db-user:',
+ 'db-password:',
+ 'db-base:',
+ 'db-prefix:',
+ );
+
+$options = getopt('', array_merge($params, $dBparams));
+
+fwrite(STDERR, 'Reconfiguring FreshRSS…' . "\n");
+
+$config = Minz_Configuration::get('system');
+foreach ($params as $param) {
+ $param = rtrim($param, ':');
+ if (isset($options[$param])) {
+ $config->$param = $options[$param] === false ? true : $options[$param];
+ }
+}
+$db = $config->db;
+foreach ($dBparams as $dBparam) {
+ $dBparam = rtrim($dBparam, ':');
+ if (isset($options[$dBparam])) {
+ $param = substr($dBparam, strlen('db-'));
+ $db[$param] = $options[$dBparam];
+ }
+}
+$config->db = $db;
+
+if (!FreshRSS_user_Controller::checkUsername($config->default_user)) {
+ fail('FreshRSS invalid default username (must be ASCII alphanumeric): ' . $config->default_user);
+}
+
+if (isset($config->auth_type) && !in_array($config->auth_type, array('form', 'http_auth', 'none'))) {
+ fail('FreshRSS invalid authentication method (auth_type must be one of { form, http_auth, none }: '
+ . $config->auth_type);
+}
+
+$config->save();
+
+done();
diff --git a/cli/update-user.php b/cli/update-user.php
new file mode 100755
index 000000000..4529b8531
--- /dev/null
+++ b/cli/update-user.php
@@ -0,0 +1,23 @@
+#!/usr/bin/php
+<?php
+$isUpdate = true;
+require(__DIR__ . '/_update-or-create-user.php');
+
+$username = cliInitUser($options['user']);
+
+echo 'FreshRSS updating user “', $username, "”…\n";
+
+$ok = FreshRSS_user_Controller::updateContextUser(
+ empty($options['password']) ? '' : $options['password'],
+ empty($options['api_password']) ? '' : $options['api_password'],
+ $values);
+
+if (!$ok) {
+ fail('FreshRSS could not update user!');
+}
+
+invalidateHttpCache($username);
+
+accessRights();
+
+done($ok);
diff --git a/cli/user-info.php b/cli/user-info.php
new file mode 100755
index 000000000..18a415217
--- /dev/null
+++ b/cli/user-info.php
@@ -0,0 +1,50 @@
+#!/usr/bin/php
+<?php
+require(__DIR__ . '/_cli.php');
+
+$options = getopt('h', array(
+ 'user:',
+ ));
+
+if (empty($options['user'])) {
+ fail('Usage: ' . basename(__FILE__) . " -h --user username");
+}
+
+$users = $options['user'] === '*' ? listUsers() : array($options['user']);
+
+foreach ($users as $username) {
+ $username = cliInitUser($username);
+ echo $username === FreshRSS_Context::$system_conf->default_user ? '*' : ' ', "\t";
+
+ $catDAO = new FreshRSS_CategoryDAO();
+ $feedDAO = FreshRSS_Factory::createFeedDao($username);
+ $entryDAO = FreshRSS_Factory::createEntryDao($username);
+ $databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
+
+ $nbEntries = $entryDAO->countUnreadRead();
+ $nbFavorites = $entryDAO->countUnreadReadFavorites();
+
+ if (isset($options['h'])) { //Human format
+ echo
+ $username, "\t",
+ date('c', FreshRSS_UserDAO::mtime($username)), "\t",
+ format_bytes($databaseDAO->size()), "\t",
+ $catDAO->count(), " categories\t",
+ count($feedDAO->listFeedsIds()), " feeds\t",
+ $nbEntries['read'], " reads\t",
+ $nbEntries['unread'], " unreads\t",
+ $nbFavorites['all'], " favourites\t",
+ "\n";
+ } else {
+ echo
+ $username, "\t",
+ FreshRSS_UserDAO::mtime($username), "\t",
+ $databaseDAO->size(), "\t",
+ $catDAO->count(), "\t",
+ count($feedDAO->listFeedsIds()), "\t",
+ $nbEntries['read'], "\t",
+ $nbEntries['unread'], "\t",
+ $nbFavorites['all'], "\t",
+ "\n";
+ }
+}