diff options
| author | 2017-12-16 15:24:13 +0100 | |
|---|---|---|
| committer | 2017-12-16 15:24:13 +0100 | |
| commit | fdc9e0d75a786101a14f64bc418b48fdd1cb4890 (patch) | |
| tree | 9a7a1d523ab1279e2efce84d2d0c73dd0ad47c70 /cli | |
| parent | f7560c585f211be41b093906e3a8fb5a6071c660 (diff) | |
| parent | ccb829418d25af49d129ac227b0cbd09c085b8a3 (diff) | |
Merge branch 'dev' into hebrew-i18n
Diffstat (limited to 'cli')
| -rw-r--r-- | cli/.htaccess | 3 | ||||
| -rw-r--r-- | cli/README.md | 103 | ||||
| -rw-r--r-- | cli/_cli.php | 66 | ||||
| -rw-r--r-- | cli/_update-or-create-user.php | 56 | ||||
| -rwxr-xr-x | cli/actualize-user.php | 23 | ||||
| -rw-r--r-- | cli/check.translation.php | 106 | ||||
| -rwxr-xr-x | cli/create-user.php | 36 | ||||
| -rwxr-xr-x | cli/db-optimize.php | 20 | ||||
| -rwxr-xr-x | cli/delete-user.php | 32 | ||||
| -rwxr-xr-x | cli/do-install.php | 100 | ||||
| -rwxr-xr-x | cli/export-opml-for-user.php | 24 | ||||
| -rwxr-xr-x | cli/export-zip-for-user.php | 30 | ||||
| -rw-r--r-- | cli/i18n/I18nCompletionValidator.php | 49 | ||||
| -rw-r--r-- | cli/i18n/I18nData.php | 118 | ||||
| -rw-r--r-- | cli/i18n/I18nFile.php | 92 | ||||
| -rw-r--r-- | cli/i18n/I18nUsageValidator.php | 47 | ||||
| -rw-r--r-- | cli/i18n/I18nValidatorInterface.php | 26 | ||||
| -rw-r--r-- | cli/i18n/ignore/en.php | 105 | ||||
| -rw-r--r-- | cli/i18n/ignore/fr.php | 55 | ||||
| -rwxr-xr-x | cli/import-for-user.php | 35 | ||||
| -rw-r--r-- | cli/index.html | 13 | ||||
| -rwxr-xr-x | cli/list-users.php | 15 | ||||
| -rw-r--r-- | cli/manipulate.translation.php | 79 | ||||
| -rwxr-xr-x | cli/reconfigure.php | 60 | ||||
| -rwxr-xr-x | cli/update-user.php | 23 | ||||
| -rwxr-xr-x | cli/user-info.php | 50 |
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"; + } +} |
