Files
FreshRSS/app/Controllers/updateController.php
Alexandre Alapetite 1a3912f25a Improve configuration checks (#8334)
Add a distinction between recommended and required extensions.
Add check for recommended php-intl extension as follow-up of https://github.com/FreshRSS/FreshRSS/pull/8329#issuecomment-3677686581
Improve related checks such as ZIP.
Reduce duplicated translations and tests.
2025-12-21 19:26:08 +01:00

406 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
class FreshRSS_update_Controller extends FreshRSS_ActionController {
private const LASTUPDATEFILE = 'last_update.txt';
public static function isGit(): bool {
return is_dir(FRESHRSS_PATH . '/.git/');
}
/**
* Automatic change to the new name of edge branch since FreshRSS 1.18.0,
* and perform checks for several git errors.
* @throws Minz_Exception
*/
public static function migrateToGitEdge(): bool {
if (!is_writable(FRESHRSS_PATH . '/.git/config')) {
throw new Minz_Exception('Error during git checkout: .git directory does not seem writeable! ' .
'Please git pull manually!');
}
if (!function_exists('exec')) {
throw new Minz_Exception('Error during git checkout: exec() function is disabled! ' .
'Please git pull manually!');
}
exec('git --version', $output, $return);
if ($return != 0) {
throw new Minz_Exception("Error {$return} git not found: Please update manually!");
}
//Note `git branch --show-current` requires git 2.22+
exec('git symbolic-ref --short HEAD 2>&1', $output, $return);
if ($return != 0) {
throw new Minz_Exception("Error {$return} during git symbolic-ref: " .
'Reapply `chown www-data:www-data -R ' . FRESHRSS_PATH . '` ' .
'or git pull manually! ' .
json_encode($output, JSON_UNESCAPED_SLASHES));
}
$line = implode('', $output);
if ($line !== 'master' && $line !== 'dev') {
return true; // not on master or dev, nothing to do
}
Minz_Log::warning('Automatic migration to git edge branch');
unset($output);
exec('git checkout edge --guess -f', $output, $return);
if ($return != 0) {
throw new Minz_Exception("Error {$return} during git checkout to edge branch! ' .
'Please change branch manually!");
}
unset($output);
exec('git reset --hard FETCH_HEAD', $output, $return);
if ($return != 0) {
throw new Minz_Exception("Error {$return} during git reset! Please git pull manually!");
}
return true;
}
public static function getCurrentGitBranch(): string {
$output = [];
exec('git branch --show-current', $output, $return);
if ($return === 0) {
return 'git branch: ' . $output[0];
} else {
return 'git';
}
}
public static function hasGitUpdate(): bool {
$cwd = getcwd();
if ($cwd === false) {
Minz_Log::warning('getcwd() failed');
return false;
}
chdir(FRESHRSS_PATH);
$output = [];
try {
/** @throws ValueError */
exec('git fetch --prune', $output, $return);
if ($return == 0) {
$output = [];
exec('git status -sb --porcelain remote', $output, $return);
} else {
$line = implode('; ', $output);
Minz_Log::warning('git fetch warning: ' . $line);
}
} catch (Throwable $e) {
Minz_Log::warning('git fetch error: ' . $e->getMessage());
}
chdir($cwd);
$line = implode('; ', $output);
return $line == '' ||
str_contains($line, '[behind') || str_contains($line, '[ahead') || str_contains($line, '[gone');
}
/** @return string|true */
public static function gitPull(): string|bool {
Minz_Log::notice(_t('admin.update.viaGit'));
$cwd = getcwd();
if ($cwd === false) {
Minz_Log::warning('getcwd() failed');
return 'getcwd() failed';
}
chdir(FRESHRSS_PATH);
$output = [];
$return = 1;
try {
exec('git fetch --prune', $output, $return);
if ($return == 0) {
$output = [];
exec('git reset --hard FETCH_HEAD', $output, $return);
}
$output = [];
self::migrateToGitEdge();
} catch (Throwable $e) {
Minz_Log::warning('Git error: ' . $e->getMessage());
$output = $e->getMessage();
$return = 1;
}
chdir($cwd);
$line = is_array($output) ? implode('; ', $output) : $output;
return $return == 0 ? true : 'Git error: ' . $line;
}
#[\Override]
public function firstAction(): void {
if (!FreshRSS_Auth::hasAccess('admin')) {
Minz_Error::error(403);
}
if (!(Minz_Request::actionName() === 'apply' && Minz_Request::paramBoolean('post_conf')) &&
FreshRSS_Auth::requestReauth()) {
return;
}
include_once LIB_PATH . '/lib_install.php';
invalidateHttpCache();
$this->view->is_release_channel_stable = $this->is_release_channel_stable(FRESHRSS_VERSION);
$this->view->update_to_apply = false;
$this->view->last_update_time = 'unknown';
$timestamp = @filemtime(join_path(DATA_PATH, self::LASTUPDATEFILE));
if ($timestamp !== false) {
$this->view->last_update_time = timestamptodate($timestamp);
}
}
public function indexAction(): void {
FreshRSS_View::prependTitle(_t('admin.update.title') . ' · ');
if (file_exists(UPDATE_FILENAME)) {
// There is an update file to apply!
$version = @file_get_contents(join_path(DATA_PATH, self::LASTUPDATEFILE));
if ($version == '') {
$version = 'unknown';
}
if (@touch(FRESHRSS_PATH . '/index.html')) {
$this->view->update_to_apply = true;
$this->view->message = [
'status' => 'good',
'title' => _t('gen.short.ok'),
'body' => _t('feedback.update.can_apply', $version),
];
} else {
$this->view->message = [
'status' => 'bad',
'title' => _t('gen.short.damn'),
'body' => _t('feedback.update.file_is_nok', $version, FRESHRSS_PATH),
];
}
}
}
private function is_release_channel_stable(string $currentVersion): bool {
return !str_contains($currentVersion, 'dev') && !str_contains($currentVersion, 'edge');
}
/* Check installation if there is a newer version.
via Git, if available.
Else via system configuration auto_update_url
*/
public function checkAction(): void {
FreshRSS_View::prependTitle(_t('admin.update.title') . ' · ');
$this->view->_path('update/index.phtml');
if (file_exists(UPDATE_FILENAME)) {
// There is already an update file to apply: we dont need to check
// the webserver!
// Or if already check during the last hour, do nothing.
Minz_Request::forward(['c' => 'update'], true);
return;
}
$script = '';
if (self::isGit()) {
if (self::hasGitUpdate()) {
$version = self::getCurrentGitBranch();
} else {
$this->view->message = [
'status' => 'latest',
'body' => _t('feedback.update.none'),
];
@touch(join_path(DATA_PATH, self::LASTUPDATEFILE));
return;
}
} else {
$auto_update_url = FreshRSS_Context::systemConf()->auto_update_url . '/?v=' . FRESHRSS_VERSION;
Minz_Log::debug('HTTP GET ' . $auto_update_url);
$curlResource = curl_init($auto_update_url);
if ($curlResource === false) {
Minz_Log::warning('curl_init() failed');
$this->view->message = [
'status' => 'bad',
'title' => _t('gen.short.damn'),
'body' => _t('feedback.update.server_not_found', $auto_update_url)
];
return;
}
curl_setopt($curlResource, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlResource, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($curlResource, CURLOPT_SSL_VERIFYHOST, 2);
$result = curl_exec($curlResource);
$curlGetinfo = curl_getinfo($curlResource, CURLINFO_HTTP_CODE);
$curlError = curl_error($curlResource);
if ($curlGetinfo !== 200) {
Minz_Log::warning(
'Error during update (HTTP code ' . $curlGetinfo . '): ' . $curlError
);
$this->view->message = [
'status' => 'bad',
'body' => _t('feedback.update.server_not_found', $auto_update_url),
];
return;
}
$res_array = explode("\n", (string)$result, 2);
$status = $res_array[0];
if (!str_starts_with($status, 'UPDATE')) {
$this->view->message = [
'status' => 'latest',
'body' => _t('feedback.update.none'),
];
@touch(join_path(DATA_PATH, self::LASTUPDATEFILE));
return;
}
$script = $res_array[1];
$version = explode(' ', $status, 2);
$version = $version[1];
Minz_Log::notice(_t('admin.update.copiedFromURL', $auto_update_url));
}
if (file_put_contents(UPDATE_FILENAME, $script) !== false) {
@file_put_contents(join_path(DATA_PATH, self::LASTUPDATEFILE), $version);
Minz_Request::forward(['c' => 'update'], true);
} else {
$this->view->message = [
'status' => 'bad',
'body' => _t('feedback.update.error', 'Cannot save the update script'),
];
}
}
public function applyAction(): void {
if (FreshRSS_Context::systemConf()->disable_update || !file_exists(UPDATE_FILENAME) || !touch(FRESHRSS_PATH . '/index.html')) {
Minz_Request::forward(['c' => 'update'], true);
}
if (Minz_Request::paramBoolean('post_conf')) {
if (self::isGit()) {
$res = !self::hasGitUpdate();
} else {
require UPDATE_FILENAME;
// @phpstan-ignore function.notFound
$res = do_post_update();
}
Minz_ExtensionManager::callHookVoid(Minz_HookType::PostUpdate);
if ($res === true) {
@unlink(UPDATE_FILENAME);
@file_put_contents(join_path(DATA_PATH, self::LASTUPDATEFILE), '');
Minz_Log::notice(_t('feedback.update.finished'));
Minz_Request::good(
_t('feedback.update.finished'),
showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
);
} else {
Minz_Log::error(_t('feedback.update.error', is_string($res) ? $res : 'unknown'));
Minz_Request::bad(_t('feedback.update.error', is_string($res) ? $res : 'unknown'), [ 'c' => 'update', 'a' => 'index' ]);
}
} else {
$res = false;
if (self::isGit()) {
$res = self::gitPull();
} else {
require UPDATE_FILENAME;
if (Minz_Request::isPost()) {
// @phpstan-ignore function.notFound
save_info_update();
}
// @phpstan-ignore function.notFound
if (!need_info_update()) {
// @phpstan-ignore function.notFound
$res = apply_update();
} else {
return;
}
}
if (function_exists('opcache_reset')) {
opcache_reset();
}
if ($res === true) {
Minz_Request::forward([
'c' => 'update',
'a' => 'apply',
'params' => ['post_conf' => '1'],
], true);
} else {
Minz_Log::error(_t('feedback.update.error', is_string($res) ? $res : 'unknown'));
Minz_Request::bad(_t('feedback.update.error', is_string($res) ? $res : 'unknown'), [ 'c' => 'update', 'a' => 'index' ]);
}
}
}
/**
* Check PHP and its extensions are well-installed.
*
* @return array<string,'ok'|'ko'|'warn'> of tested values.
*/
private static function check_install_php(): array {
require_once LIB_PATH . '/lib_install.php';
return checkRequirements(FreshRSS_Context::systemConf()->db['type'] ?? '', checkPhp: true, checkFiles: false);
}
/**
* Check different data files and directories exist.
* @return array<string,'ok'|'ko'|'warn'> of tested values.
*/
private static function check_install_files(): array {
require_once LIB_PATH . '/lib_install.php';
return checkRequirements(FreshRSS_Context::systemConf()->db['type'] ?? '', checkPhp: false, checkFiles: true);
}
/**
* Check database is well-installed.
*
* @return array<string,bool> of tested values.
*/
private static function check_install_database(): array {
$status = [
'connection' => true,
'tables' => false,
'categories' => false,
'feeds' => false,
'entries' => false,
'entrytmp' => false,
'tag' => false,
'entrytag' => false,
];
try {
$dbDAO = FreshRSS_Factory::createDatabaseDAO();
$status['tables'] = $dbDAO->tablesAreCorrect();
$status['categories'] = $dbDAO->categoryIsCorrect();
$status['feeds'] = $dbDAO->feedIsCorrect();
$status['entries'] = $dbDAO->entryIsCorrect();
$status['entrytmp'] = $dbDAO->entrytmpIsCorrect();
$status['tag'] = $dbDAO->tagIsCorrect();
$status['entrytag'] = $dbDAO->entrytagIsCorrect();
} catch (Minz_PDOConnectionException $e) {
$status['connection'] = false;
}
return $status;
}
/**
* This action displays information about installation.
*/
public function checkInstallAction(): void {
FreshRSS_View::prependTitle(_t('install.check._') . ' · ');
$this->view->status_php = self::check_install_php();
$this->view->status_files = self::check_install_files();
$this->view->status_database = self::check_install_database();
}
}