This page lists files in the current directory. You can view content, get download/execute commands for Wget, Curl, or PowerShell, or filter the list using wildcards (e.g., `*.sh`).
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/.htaccess'
# Apache 2.2
<IfModule !mod_authz_core.c>
Order Allow,Deny
Deny from all
Satisfy all
</IfModule>
# Apache 2.4
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/CliOption.php'
<?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);
}
}
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/CliOptionsParser.php'
<?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 property.dynamicName
$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+))/";
}
}
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/README.md'
* 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 cli/access-permissions.sh
```
## Commands
Options in parenthesis are optional.
### System
```sh
cd /usr/share/FreshRSS
./cli/prepare.php
# Ensure the needed directories in ./data/
./cli/do-install.php --default-user admin [ --auth-type form --environment production --base-url https://rss.example.net --language en --title FreshRSS --allow-anonymous --allow-anonymous-refresh --api-enabled --db-type sqlite --db-host localhost:3306 --db-user freshrss --db-password dbPassword123 --db-base freshrss --db-prefix freshrss_ ]
# --default-user must be alphanumeric and not longer than 38 characters. The default user of this FreshRSS instance, used as the public user for anonymous reading.
# --auth-type can be: 'form' (default), 'http_auth' (using the Web server access control), 'none' (dangerous).
# --environment can be: 'production' (default), 'development' (for additional log messages).
# --base-url should be a public (routable) URL if possible, and is used for push (WebSub), for some API functions (e.g. favicons), and external URLs in FreshRSS.
# --language can be: 'en' (default), 'fr', or one of the [supported languages](../app/i18n/).
# --title web user interface title for this FreshRSS instance.
# --allow-anonymous sets whether non logged-in visitors are permitted to see the default user's feeds.
# --allow-anonymous-refresh sets whether to permit anonymous users to start the refresh process.
# --api-enabled sets whether the API may be used for mobile apps. API passwords must be set for individual users.
# --db-type can be: 'sqlite' (default), 'mysql' (MySQL or MariaDB), 'pgsql' (PostgreSQL).
# --db-host URL of the database server. Default is 'localhost'.
# --db-user sets database user.
# --db-password sets database password.
# --db-base sets database name.
# --db-prefix is an optional prefix in front of the names of the tables. We suggest using 'freshrss_' (default).
# 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.
```
> ℹ️ More options for [the configuration of your instance](../config.default.php#L3-L5) may be set in `./data/config.custom.php` before the install process, or in `./data/config.php` after the install process.
### User
```sh
cd /usr/share/FreshRSS
./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 --max-posts-per-rss 400 ]
# --user must be alphanumeric, not longer than 38 characters. The name of the user to be created/updated.
# --password sets the user's password.
# --api-password sets the user's api password.
# --language can be: 'en' (default), 'fr', or one of the [supported languages](../app/i18n/).
# --email sets an email for the user which will be used email validation if it forced email validation is enabled.
# --no-default-feeds do not add this FreshRSS instance's default feeds to the user during creation.
# --purge-after-months max age an article can reach before being archived. Default is '3'.
# --feed-min-articles-default number of articles in a feed at which archiving will pause. Default is '50'.
# --feed-ttl-default minimum number of seconds to elapse between feed refreshes. Default is '3600'.
# --max-posts-per-rss number of articles in a feed at which an old article will be archived before a new article is added. Default is '200'.
./cli/update-user.php --user username [ ... ]
# Same options as create-user.php, except --no-default-feeds which is only available for create-user.php.
```
> ℹ️ More options for [the configuration of users](../config-user.default.php#L3-L5) may be set in `./data/config-user.custom.php` prior to creating new users, or in `./data/users/*/config.php` for existing users.
```sh
./cli/actualize-user.php --user username
# Fetch feeds for the specified user.
./cli/delete-user.php --user username
# Deletes the specified user.
./cli/list-users.php
# Return a list of users, with the default/admin user first.
./cli/user-info.php [ --human-readable --header --json --user username1 --user username2 ... ]
# -h, --human-readable display output in a human readable format
# --header outputs some columns headers.
# --json JSON format (disables --header and --human-readable but uses ISO Zulu format for dates).
# --user indicates a username, and can be repeated.
# Returns: 1) a * if 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, 9) favourites, 10) tags,
# 11) language, 12) e-mail.
./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-sqlite-for-user.php --user username --filename /path/to/db.sqlite
# Export the user’s database to a new SQLite file.
./cli/import-sqlite-for-user.php --user username [ --force-overwrite ] --filename /path/to/db.sqlite
# Import the user’s database from an SQLite file.
# --force-overwrite will clear the target user database before import (import only works on an empty user database).
./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
```
### Database
```sh
cd /usr/share/FreshRSS
./cli/db-backup.php
# Back-up all users respective database to `data/users/*/backup.sqlite`
# -q, --quiet suppress non-error messages
./cli/db-restore.php --delete-backup --force-overwrite
# Restore all users respective database from `data/users/*/backup.sqlite`
# --delete-backup: delete `data/users/*/backup.sqlite` after successful import
# --force-overwrite: will clear the users respective database before import
./cli/db-optimize.php --user username
# Optimize database (reduces the size) for a given user (perform `OPTIMIZE TABLE` in MySQL, `VACUUM` in SQLite)
```
### Translation
```sh
cd /usr/share/FreshRSS
./cli/manipulate.translation.php --action [ --help --key --value --language --revert --origin-language ]
# manipulate translation files.
# -a, --action selects the action to perform. (can be either: add, delete, exist, format, or ignore)
# -h, --help displays the commands help file.
# -k, --key selects the key to work on.
# -v, --value selects the value to set.
# -l, --language selects the language to work on.
# -r, --revert revert the action (only used with ignore action).
# -o, --origin-language selects the origin language (only used with add language action).
./cli/check-translation.php [ ---display-result --help --language fr --display-report ]
# Check if translation files have missing keys or missing translations.
# -d, --display-result display results of check.
# -h, --help display help text and exit.
# -l, --language set the language check.
# -r, --display-report display completion report.
```
## Note about cron
Some commands display information on standard error; cron will send an email with this information every time the command will be executed (exited zero or non-zero).
To avoid cron sending email on success:
```text
@daily /usr/local/bin/my-command > /var/log/cron-freshrss-stdout.log 2>/var/log/cron-freshrss-stderr.log || cat /var/log/cron-freshrss-stderr.log
```
Explanations:
* `/usr/local/bin/my-command > /var/log/cron-freshrss-stdout.log`_ : redirect the standard output to a log file
* `/usr/local/bin/my-command 2> /var/log/cron-freshrss-stderr.log` : redirect the standard error to a log file
* `|| cat /var/log/cron-freshrss-stderr.log_ : if the exit code of _/usr/local/bin/my-command` is non-zero, then it send by mail the content error file.
Now, cron will send you an email only if the exit code is non-zero and with the content of the file containing the errors.
## 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 | sort -k2 -r
```
Example to get the number of feeds of a given user:
```sh
./cli/user-info.php --user alex | cut -f6
#or
./cli/user-info.php --user alex --json | jq '.[] | .feeds'
```
Example to get the name of the users who have not been active since a given date:
```sh
cli/user-info.php --json | jq '.[] | select(.last_user_activity < "2020-05-01") | .user'
```
Example to get the date and name of users who have not been active the past 24 hours (86400 seconds):
```sh
cli/user-info.php --json | jq -r '.[] | select((.last_user_activity | fromdate) < (now - 86400)) | [.last_user_activity, .user] | @csv'
```
# Install and updates
If you want to administrate FreshRSS using git, please read our [installation docs](https://freshrss.github.io/FreshRSS/en/admins/03_Installation.html)
and [update guidelines](https://freshrss.github.io/FreshRSS/en/admins/04_Updating.html).
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/_cli.php'
<?php
declare(strict_types=1);
if (php_sapi_name() !== 'cli') {
die('FreshRSS error: This PHP script may only be invoked from command line!');
}
const EXIT_CODE_ALREADY_EXISTS = 3;
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();
Minz_ExtensionManager::init();
Minz_Translate::init('en');
FreshRSS_Context::$isCli = true;
/** @return never */
function fail(string $message, int $exitCode = 1) {
fwrite(STDERR, $message . "\n");
die($exitCode);
}
function cliInitUser(string $username): string {
if (!FreshRSS_user_Controller::checkUsername($username)) {
fail('FreshRSS error: invalid username: ' . $username . "\n");
}
if (!FreshRSS_user_Controller::userExists($username)) {
fail('FreshRSS error: user not found: ' . $username . "\n");
}
FreshRSS_Context::initUser($username);
if (!FreshRSS_Context::hasUserConf()) {
fail('FreshRSS error: invalid configuration for user: ' . $username . "\n");
}
$ext_list = FreshRSS_Context::userConf()->extensions_enabled;
Minz_ExtensionManager::enableByList($ext_list, 'user');
return $username;
}
function accessRights(): void {
echo 'ℹ️ Remember to re-apply the appropriate access rights, such as:',
"\t", 'sudo cli/access-permissions.sh', "\n";
}
/** @return never */
function done(bool $ok = true) {
if (!$ok) {
fwrite(STDERR, (empty($_SERVER['argv'][0]) ? 'Process' : basename($_SERVER['argv'][0])) . ' failed!' . "\n");
}
exit($ok ? 0 : 1);
}
function performRequirementCheck(string $databaseType): void {
$requirements = checkRequirements($databaseType);
if ($requirements['all'] !== 'ok') {
$message = 'FreshRSS failed requirements:' . "\n";
foreach ($requirements as $requirement => $check) {
if ($check !== 'ok' && !in_array($requirement, ['all', 'pdo', 'message'], true)) {
$message .= '• ' . $requirement . "\n";
}
}
if (!empty($requirements['message']) && $requirements['message'] !== 'ok') {
$message .= '• ' . $requirements['message'] . "\n";
}
fail($message);
}
}
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/access-permissions.sh'
#!/bin/sh
# Apply access permissions
if [ ! -f './constants.php' ] || [ ! -d './cli/' ]; then
echo >&2 '⛔ It does not look like a FreshRSS directory; exiting!'
exit 2
fi
if [ "$(id -u)" -ne 0 ]; then
echo >&2 '⛔ Applying access permissions require running as root or sudo!'
exit 3
fi
# Based on group access
chown -R :www-data .
# Read files, and directory traversal
chmod -R g+rX .
# Write access
mkdir -p ./data/users/_/
chmod -R g+w ./data/
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/actualize-user.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$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($cliOptions->user);
Minz_ExtensionManager::callHookVoid('freshrss_user_maintenance');
fwrite(STDERR, 'FreshRSS actualizing user “' . $username . "”…\n");
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
$databaseDAO->minorDbMaintenance();
Minz_ExtensionManager::callHookVoid('freshrss_user_maintenance');
FreshRSS_feed_Controller::commitNewEntries();
$feedDAO = FreshRSS_Factory::createFeedDao();
$feedDAO->updateCachedValues();
$result = FreshRSS_category_Controller::refreshDynamicOpmls();
if (!empty($result['errors'])) {
$errors = $result['errors'];
fwrite(STDERR, "FreshRSS error refreshing $errors dynamic OPMLs!\n");
}
if (!empty($result['successes'])) {
$successes = $result['successes'];
echo "FreshRSS refreshed $successes dynamic OPMLs for $username\n";
}
[$nbUpdatedFeeds, , $nbNewArticles] = FreshRSS_feed_Controller::actualizeFeedsAndCommit();
echo "FreshRSS actualized $nbUpdatedFeeds feeds for $username ($nbNewArticles new articles)\n";
invalidateHttpCache($username);
done($nbUpdatedFeeds > 0);
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/check.translation.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require_once __DIR__ . '/_cli.php';
require_once __DIR__ . '/i18n/I18nCompletionValidator.php';
require_once __DIR__ . '/i18n/I18nData.php';
require_once __DIR__ . '/i18n/I18nFile.php';
require_once __DIR__ . '/i18n/I18nUsageValidator.php';
require_once __DIR__ . '/../constants.php';
$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();
}
};
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
if (isset($cliOptions->help)) {
checkHelp();
}
$i18nFile = new I18nFile();
$i18nData = new I18nData($i18nFile->load());
if (isset($cliOptions->language)) {
$languages = $cliOptions->language;
} else {
$languages = $i18nData->getAvailableLanguages();
}
$displayResults = isset($cliOptions->displayResult);
$displayReport = isset($cliOptions->displayReport);
$isValidated = true;
$result = [];
$report = [];
foreach ($languages as $language) {
if ($language === $i18nData::REFERENCE_LANGUAGE) {
$i18nValidator = new I18nUsageValidator($i18nData->getReferenceLanguage(), findUsedTranslations());
} else {
$i18nValidator = new I18nCompletionValidator($i18nData->getReferenceLanguage(), $i18nData->getLanguage($language));
}
$isValidated = $i18nValidator->validate() && $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<string>
*/
function findUsedTranslations(): array {
$directory = new RecursiveDirectoryIterator(__DIR__ . '/..');
$iterator = new RecursiveIteratorIterator($directory);
$regex = new RegexIterator($iterator, '/^.+\.(php|phtml)$/i', RecursiveRegexIterator::GET_MATCH);
$usedI18n = [];
foreach (array_keys(iterator_to_array($regex)) as $file) {
$fileContent = file_get_contents($file);
if ($fileContent === false) {
continue;
}
preg_match_all('/_t\([\'"](?P<strings>[^\'"]+)[\'"]/', $fileContent, $matches);
$usedI18n = array_merge($usedI18n, $matches['strings']);
}
return $usedI18n;
}
/**
* Output help message.
* @return never
*/
function checkHelp() {
$file = str_replace(__DIR__ . '/', '', __FILE__);
echo <<<HELP
NAME
$file
SYNOPSIS
php $file [OPTION]...
DESCRIPTION
Check if translation files have missing keys or missing translations.
-d, --display-result display results.
-h, --help display this help and exit.
-l, --language=LANG filter by LANG.
-r, --display-report display completion report.
HELP;
exit();
}
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/create-user.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.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;
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();
}
};
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
$username = $cliOptions->user;
if (preg_grep("/^$username$/i", listUsers())) {
fail('FreshRSS warning: username already exists “' . $username . '”', EXIT_CODE_ALREADY_EXISTS);
}
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,
isset($cliOptions->email) ? $cliOptions->email : null,
$cliOptions->password ?? '',
$values,
!isset($cliOptions->noDefaultFeeds)
);
if (!$ok) {
fail('FreshRSS could not create user!');
}
if (isset($cliOptions->apiPassword)) {
$username = cliInitUser($username);
$error = FreshRSS_api_Controller::updatePassword($cliOptions->apiPassword);
if ($error !== false) {
fail($error);
}
}
invalidateHttpCache(FreshRSS_Context::systemConf()->default_user);
echo 'ℹ️ Remember to refresh the feeds of the user: ', $username ,
"\t", './cli/actualize-user.php --user ', $username, "\n";
accessRights();
done($ok);
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/db-backup.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$ok = true;
$cliOptions = new class extends CliOptionsParser {
public string $quiet;
public function __construct() {
$this->addOption('quiet', (new CliOption('quiet', 'q'))->withValueNone());
parent::__construct();
}
};
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
foreach (listUsers() as $username) {
$username = cliInitUser($username);
$filename = DATA_PATH . '/users/' . $username . '/backup.sqlite';
@unlink($filename);
$verbose = !isset($cliOptions->quiet);
if ($verbose) {
echo 'FreshRSS backup database to SQLite for user “', $username, "”…\n";
}
$databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
$ok &= $databaseDAO->dbCopy($filename, FreshRSS_DatabaseDAO::SQLITE_EXPORT, false, $verbose);
}
done((bool)$ok);
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/db-optimize.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$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($cliOptions->user);
echo 'FreshRSS optimizing database for user “', $username, "”…\n";
$databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
$ok = $databaseDAO->optimize();
done($ok);
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/db-restore.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$cliOptions = new class extends CliOptionsParser {
public string $deleteBackup;
public string $forceOverwrite;
public function __construct() {
$this->addOption('deleteBackup', (new CliOption('delete-backup'))->withValueNone());
$this->addOption('forceOverwrite', (new CliOption('force-overwrite'))->withValueNone());
parent::__construct();
}
};
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
FreshRSS_Context::initSystem(true);
Minz_User::change(Minz_User::INTERNAL_USER);
$ok = false;
try {
$error = initDb();
if ($error != '') {
$_SESSION['bd_error'] = $error;
} else {
$ok = true;
}
} catch (Exception $ex) {
$_SESSION['bd_error'] = $ex->getMessage();
}
if (!$ok) {
fail('FreshRSS database error: ' . (empty($_SESSION['bd_error']) ? 'Unknown error' : $_SESSION['bd_error']));
}
foreach (listUsers() as $username) {
$username = cliInitUser($username);
$filename = DATA_PATH . "/users/{$username}/backup.sqlite";
if (!file_exists($filename)) {
fwrite(STDERR, "FreshRSS SQLite backup not found for user “{$username}”!\n");
$ok = false;
continue;
}
echo 'FreshRSS restore database from SQLite for user “', $username, "”…\n";
$databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
$clearFirst = isset($cliOptions->forceOverwrite);
$ok &= $databaseDAO->dbCopy($filename, FreshRSS_DatabaseDAO::SQLITE_IMPORT, $clearFirst);
if ($ok) {
if (isset($cliOptions->deleteBackup)) {
unlink($filename);
}
} else {
fwrite(STDERR, "FreshRSS database already exists for user “{$username}”!\n");
fwrite(STDERR, "If you would like to clear the user database first, use the option --force-overwrite\n");
}
invalidateHttpCache($username);
}
done((bool)$ok);
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/delete-user.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$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 = $cliOptions->user;
if (!FreshRSS_user_Controller::checkUsername($username)) {
fail('FreshRSS error: invalid username: ' . $username . "\n");
}
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 . '”');
}
echo 'FreshRSS deleting user “', $username, "”…\n";
$ok = FreshRSS_user_Controller::deleteUser($username);
invalidateHttpCache(FreshRSS_Context::systemConf()->default_user);
done($ok);
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/do-install.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
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);
}
$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($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,
);
$customConfigPath = DATA_PATH . '/config.custom.php';
if (file_exists($customConfigPath)) {
$customConfig = include($customConfigPath);
if (is_array($customConfig)) {
$config = array_merge($customConfig, $config);
}
}
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;
}
}
if ((!empty($config['base_url'])) && is_string($config['base_url']) && Minz_Request::serverIsPublic($config['base_url'])) {
$config['pubsubhubbub_enabled'] = true;
}
$config['db'] = array_merge($config['db'], array_filter($dbValues, static fn($value) => $value !== null));
performRequirementCheck($config['db']['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'));
}
if (function_exists('opcache_reset')) {
opcache_reset();
}
FreshRSS_Context::initSystem(true);
Minz_User::change(Minz_User::INTERNAL_USER);
$ok = false;
try {
$error = initDb();
if ($error != '') {
$_SESSION['bd_error'] = $error;
} else {
$ok = true;
}
} catch (Exception $ex) {
$_SESSION['bd_error'] = $ex->getMessage();
}
if (!$ok) {
@unlink(join_path(DATA_PATH, 'config.php'));
fail('FreshRSS database error: ' . (empty($_SESSION['bd_error']) ? 'Unknown error' : $_SESSION['bd_error']));
}
echo 'ℹ️ Remember to create the default user: ', $config['default_user'],
"\t", './cli/create-user.php --user ', $config['default_user'], " --password 'password' --more-options\n";
accessRights();
if (!setupMigrations()) {
fail('FreshRSS access right problem while creating migrations version file!');
}
done();
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/export-opml-for-user.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$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($cliOptions->user);
fwrite(STDERR, 'FreshRSS exporting OPML for user “' . $username . "”…\n");
$export_service = new FreshRSS_Export_Service($username);
[$filename, $content] = $export_service->generateOpml();
echo $content;
invalidateHttpCache($username);
done();
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/export-sqlite-for-user.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$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($cliOptions->user);
$filename = $cliOptions->filename;
if (pathinfo($filename, PATHINFO_EXTENSION) !== 'sqlite') {
fail('Only *.sqlite files are supported!');
}
echo 'FreshRSS exporting database to SQLite for user “', $username, "”…\n";
$databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
$ok = $databaseDAO->dbCopy($filename, FreshRSS_DatabaseDAO::SQLITE_EXPORT);
done($ok);
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/export-zip-for-user.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$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($cliOptions->user);
fwrite(STDERR, 'FreshRSS exporting ZIP for user “' . $username . "”…\n");
$export_service = new FreshRSS_Export_Service($username);
$number_entries = $cliOptions->maxFeedEntries;
$exported_files = [];
// First, we generate the OPML file
list($filename, $content) = $export_service->generateOpml();
$exported_files[$filename] = $content;
// Then, labelled and starred entries
list($filename, $content) = $export_service->generateStarredEntries('ST');
$exported_files[$filename] = $content;
// And a list of entries based on the complete list of feeds
$feeds_exported_files = $export_service->generateAllFeedEntries($number_entries);
$exported_files = array_merge($exported_files, $feeds_exported_files);
// Finally, we compress all these files into a single Zip archive and we output
// the content
list($filename, $content) = $export_service->zip($exported_files);
echo $content;
invalidateHttpCache($username);
done();
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/import-for-user.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$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($cliOptions->user);
$filename = $cliOptions->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);
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/import-sqlite-for-user.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$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($cliOptions->user);
$filename = $cliOptions->filename;
if (pathinfo($filename, PATHINFO_EXTENSION) !== 'sqlite') {
fail('Only *.sqlite files are supported!');
}
echo 'FreshRSS importing database from SQLite for user “', $username, "”…\n";
$databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
$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";
}
invalidateHttpCache($username);
done($ok);
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/index.html'
<!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>
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/list-users.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
$users = listUsers();
sort($users);
if (FreshRSS_Context::systemConf()->default_user !== ''
&& in_array(FreshRSS_Context::systemConf()->default_user, $users, true)) {
array_unshift($users, FreshRSS_Context::systemConf()->default_user);
$users = array_unique($users);
}
foreach ($users as $user) {
echo $user, "\n";
}
done();
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/manipulate.translation.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require_once __DIR__ . '/_cli.php';
require_once __DIR__ . '/i18n/I18nData.php';
require_once __DIR__ . '/i18n/I18nFile.php';
require_once __DIR__ . '/../constants.php';
$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 (isset($cliOptions->help)) {
manipulateHelp();
}
$data = new I18nFile();
$i18nData = new I18nData($data->load());
switch ($cliOptions->action) {
case 'add':
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 (isset($cliOptions->originLanguage)) {
$reference = $cliOptions->originLanguage;
}
$i18nData->addLanguage($cliOptions->language, $reference);
} else {
error('You need to specify a valid set of options.');
exit;
}
break;
case 'delete':
if (isset($cliOptions->key)) {
$i18nData->removeKey($cliOptions->key);
} else {
error('You need to specify the key to delete.');
exit;
}
break;
case 'exist':
if (isset($cliOptions->key)) {
$key = $cliOptions->key;
if ($i18nData->isKnown($key)) {
echo "The '{$key}' key is known.\n\n";
} else {
echo "The '{$key}' key is unknown.\n\n";
}
} else {
error('You need to specify the key to check.');
exit;
}
break;
case 'format':
break;
case 'ignore':
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 (isset($cliOptions->language)) {
$i18nData->ignore_unmodified($cliOptions->language, isset($cliOptions->revert));
} else {
error('You need to specify a valid set of options.');
exit;
}
break;
default:
manipulateHelp();
exit;
}
$data->dump($i18nData->getData());
/**
* Output error message.
*/
function error(string $message): void {
$error = <<<ERROR
WARNING
%s\n\n
ERROR;
echo sprintf($error, $message);
manipulateHelp();
}
/**
* Output help message.
*/
function manipulateHelp(): void {
$file = str_replace(__DIR__ . '/', '', __FILE__);
echo <<<HELP
NAME
$file
SYNOPSIS
php $file [OPTIONS]
DESCRIPTION
Manipulate translation files.
-a, --action=ACTION
select the action to perform. Available actions are add, delete,
exist, format, ignore, and ignore_unmodified. This option is mandatory.
-k, --key=KEY select the key to work on.
-v, --value=VAL select the value to set.
-l, --language=LANG select the language to work on.
-h, --help display this help and exit.
-r, --revert revert the action (only for ignore action)
-o, origin-language=LANG
select the origin language (only for add language action)
EXAMPLES
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. Adds a key to all supported languages.
php $file -a add -k my_key -v my_value
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. Deletes the selected key from all supported languages.
php $file -a delete -k my_key
Example 5: format i18n files.
php $file -a format
Example 6: ignore a key. Adds IGNORE comment to the key in the selected language, marking it as translated.
php $file -a ignore -k my_key -l my_lang
Example 7: revert ignore a key. Removes IGNORE comment from the key in the selected language.
php $file -a ignore -r -k my_key -l my_lang
Example 8: ignore all unmodified keys. Adds IGNORE comments to all unmodified keys in the selected language, marking them as translated.
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 IGNORE(s) 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
HELP;
exit();
}
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/prepare.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
$dirs = [
'/',
'/cache',
'/extensions-data',
'/favicons',
'/fever',
'/PubSubHubbub',
'/PubSubHubbub/feeds',
'/PubSubHubbub/keys',
'/tokens',
'/users',
'/users/_',
];
$ok = true;
foreach ($dirs as $dir) {
@mkdir(DATA_PATH . $dir, 0770, true);
$ok &= touch(DATA_PATH . $dir . '/index.html');
}
file_put_contents(DATA_PATH . '/.htaccess', <<<'EOF'
# Apache 2.2
<IfModule !mod_authz_core.c>
Order Allow,Deny
Deny from all
Satisfy all
</IfModule>
# Apache 2.4
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
EOF
);
accessRights();
done((bool)$ok);
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/reconfigure.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
$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->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($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
fwrite(STDERR, 'Reconfiguring FreshRSS…' . "\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,
];
$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 '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;
}
// @phpstan-ignore assign.propertyType, property.dynamicName
$systemConf->$name = $value;
}
}
$db = array_merge(FreshRSS_Context::systemConf()->db, array_filter($dbValues));
performRequirementCheck($db['type']);
FreshRSS_Context::systemConf()->db = $db;
FreshRSS_Context::systemConf()->save();
done();
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/sensitive-log.sh'
#!/bin/sh
# Strips sensitive passwords from (Apache) logs
# For e.g. GNU systems such as Debian
# N.B.: `sed -u` is not available in BusyBox and without it there are buffering delays (even with stdbuf)
sed -Eu 's/([?&])(Passwd|token)=[^& \t]+/\1\2=redacted/ig' 2>/dev/null ||
# For systems with gawk (not available by default in Docker of Debian or Alpine) or with BuzyBox such as Alpine
$(which gawk || which awk) -v IGNORECASE=1 '{ print gensub(/([?&])(Passwd|token)=[^& \t]+/, "\\1\\2=redacted", "g") }'
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/translation-update.sh'
#!/bin/bash
# This script performs the following:
# - Generate configuration file for po4a (can be configured in CONFIGFILE)
# - Generate POT file from pages/<DIRECTORY>/*.md
# - Update PO files in i18n directory with POT file
# - Generate localized pages.XX/<DIRECTORY>/*.md (where XX is the language code)
# - Remove unneeded new lines from generated pages
# Name of the po4a configuration file
CONFIGFILE='po4a.conf'
# List of supported languages
LANGS=(fr)
# Check if po4a is installed
if [ -z "$(command -v po4a)" ]; then
echo 'It seems that po4a is not installed on your system.'
echo 'Please install po4a to use this script.'
exit 1
fi
# Generate po4a.conf file with list of TLDR pages
echo 'Generating configuration file for po4a…'
{
echo '# WARNING: this file is generated with translation-update.sh'
echo '# DO NOT modify this file manually!'
echo "[po4a_langs] ${LANGS[*]}"
# shellcheck disable=SC2016
echo '[po4a_paths] i18n/templates/freshrss.pot $lang:i18n/freshrss.$lang.po'
} >$CONFIGFILE
for FILE in $(cd en && tree -f -i | grep ".md" | grep -v "admins"); do
echo "[type: text] en/$FILE \$lang:\$lang/$FILE opt:\"-o markdown\" opt:\"-M utf-8\"" >>$CONFIGFILE
done
# Generate POT file, PO files, and pages.XX pages
echo 'Generating POT file and translated pages…'
po4a -k 0 --msgid-bugs-address 'https://github.com/FreshRSS/FreshRSS/issues' $CONFIGFILE
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/update-user.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.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 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,
isset($cliOptions->email) ? $cliOptions->email : null,
$cliOptions->password ?? '',
$values);
if (!$ok) {
fail('FreshRSS could not update user!');
}
if (isset($cliOptions->apiPassword)) {
$error = FreshRSS_api_Controller::updatePassword($cliOptions->apiPassword);
if ($error) {
fail($error);
}
}
invalidateHttpCache($username);
accessRights();
done($ok);
wget 'https://sme10.lists2.roe3.org/FreshRSS/cli/user-info.php'
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
const DATA_FORMAT = "%-7s | %-20s | %-5s | %-7s | %-25s | %-15s | %-10s | %-10s | %-10s | %-10s | %-10s | %-10s | %-5s | %-10s\n";
$cliOptions = new class extends CliOptionsParser {
/** @var array<int,string> $user */
public array $user;
public string $header;
public string $json;
public string $humanReadable;
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($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
$users = $cliOptions->user ?? listUsers();
sort($users);
$formatJson = isset($cliOptions->json);
$jsonOutput = [];
if ($formatJson) {
unset($cliOptions->header);
unset($cliOptions->humanReadable);
}
if (isset($cliOptions->header)) {
printf(
DATA_FORMAT,
'default',
'user',
'admin',
'enabled',
'last user activity',
'space used',
'categories',
'feeds',
'reads',
'unreads',
'favourites',
'tags',
'lang',
'email'
);
}
foreach ($users as $username) {
$username = cliInitUser($username);
$catDAO = FreshRSS_Factory::createCategoryDao($username);
$feedDAO = FreshRSS_Factory::createFeedDao($username);
$entryDAO = FreshRSS_Factory::createEntryDao($username);
$tagDAO = FreshRSS_Factory::createTagDao($username);
$databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
$nbEntries = $entryDAO->countUnreadRead();
$nbFavorites = $entryDAO->countUnreadReadFavorites();
$feedList = $feedDAO->listFeedsIds();
$data = array(
'default' => $username === FreshRSS_Context::systemConf()->default_user ? '*' : '',
'user' => $username,
'admin' => FreshRSS_Context::userConf()->is_admin ? '*' : '',
'enabled' => FreshRSS_Context::userConf()->enabled ? '*' : '',
'last_user_activity' => FreshRSS_UserDAO::mtime($username),
'database_size' => $databaseDAO->size(),
'categories' => $catDAO->count(),
'feeds' => count($feedList),
'reads' => (int)$nbEntries['read'],
'unreads' => (int)$nbEntries['unread'],
'favourites' => (int)$nbFavorites['all'],
'tags' => $tagDAO->count(),
'lang' => FreshRSS_Context::userConf()->language,
'mail_login' => FreshRSS_Context::userConf()->mail_login,
);
if (isset($cliOptions->humanReadable)) { //Human format
$data['last_user_activity'] = date('c', $data['last_user_activity']);
$data['database_size'] = format_bytes($data['database_size']);
}
if ($formatJson) {
$data['default'] = !empty($data['default']);
$data['admin'] = !empty($data['admin']);
$data['enabled'] = !empty($data['enabled']);
$data['last_user_activity'] = gmdate('Y-m-d\TH:i:s\Z', (int)$data['last_user_activity']);
$jsonOutput[] = $data;
} else {
vprintf(DATA_FORMAT, $data);
}
}
if ($formatJson) {
echo json_encode($jsonOutput), "\n";
}
done();