From a18c35046daee15e7ac5f85db290d54541a03e3c Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Tue, 11 Nov 2025 08:17:12 +0100 Subject: Housekeeping lib_rss.php (#8193) * Housekeeping lib_rss.php `lib_rss.php` had become much too large, especially after https://github.com/FreshRSS/FreshRSS/pull/7924 Moved most functions to other places. Mostly no change of code otherwise (see comments). * Extension: composer run-script phpstan-third-party --- app/Controllers/authController.php | 6 +-- app/Controllers/categoryController.php | 8 ++-- app/Controllers/configureController.php | 26 +++++++++- app/Controllers/extensionController.php | 2 +- app/Controllers/feedController.php | 10 ++-- app/Controllers/importExportController.php | 2 +- app/Controllers/indexController.php | 2 +- app/Controllers/javascriptController.php | 2 +- app/Controllers/subscriptionController.php | 6 +-- app/Controllers/updateController.php | 77 ++++++++++++++++++++++++++++-- app/Controllers/userController.php | 63 ++++++++++++++++++++---- 11 files changed, 171 insertions(+), 33 deletions(-) (limited to 'app/Controllers') diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php index 64526a884..61f4f5aaf 100644 --- a/app/Controllers/authController.php +++ b/app/Controllers/authController.php @@ -85,8 +85,8 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController { 'http_auth' => Minz_Error::error(403, [ 'error' => [ _t('feedback.access.denied'), - ' [HTTP Remote-User=' . htmlspecialchars(httpAuthUser(false), ENT_NOQUOTES, 'UTF-8') . - ' ; Remote IP address=' . connectionRemoteAddress() . ']' + ' [HTTP Remote-User=' . htmlspecialchars(FreshRSS_http_Util::httpAuthUser(onlyTrusted: false), ENT_NOQUOTES, 'UTF-8') . + ' ; Remote IP address=' . Minz_Request::connectionRemoteAddress() . ']' ] ], false), 'none' => Minz_Error::error(404), // It should not happen! @@ -297,7 +297,7 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController { Minz_Request::forward(['c' => 'index', 'a' => 'index'], true); } - if (max_registrations_reached()) { + if (FreshRSS_user_Controller::max_registrations_reached()) { Minz_Error::error(403); } diff --git a/app/Controllers/categoryController.php b/app/Controllers/categoryController.php index 2212a158b..5b1dd9d17 100644 --- a/app/Controllers/categoryController.php +++ b/app/Controllers/categoryController.php @@ -59,7 +59,7 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController { Minz_Request::bad(_t('feedback.tag.name_exists', $cat->name()), $url_redirect); } - $opml_url = checkUrl(Minz_Request::paramString('opml_url', plaintext: true)); + $opml_url = FreshRSS_http_Util::checkUrl(Minz_Request::paramString('opml_url', plaintext: true)); if ($opml_url != '') { $cat->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML); $cat->_attribute('opml_url', $opml_url); @@ -141,7 +141,7 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController { $position = Minz_Request::paramInt('position') ?: null; $category->_attribute('position', $position); - $opml_url = checkUrl(Minz_Request::paramString('opml_url', plaintext: true)); + $opml_url = FreshRSS_http_Util::checkUrl(Minz_Request::paramString('opml_url', plaintext: true)); if ($opml_url != '') { $category->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML); $category->_attribute('opml_url', $opml_url); @@ -229,7 +229,7 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController { } // Remove related queries. - $queries = remove_query_by_get('c_' . $id, FreshRSS_Context::userConf()->queries); + $queries = FreshRSS_UserQuery::remove_query_by_get('c_' . $id, FreshRSS_Context::userConf()->queries); FreshRSS_Context::userConf()->queries = $queries; FreshRSS_Context::userConf()->save(); @@ -274,7 +274,7 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController { // Remove related queries foreach ($feeds as $feed) { - $queries = remove_query_by_get('f_' . $feed->id(), FreshRSS_Context::userConf()->queries); + $queries = FreshRSS_UserQuery::remove_query_by_get('f_' . $feed->id(), FreshRSS_Context::userConf()->queries); FreshRSS_Context::userConf()->queries = $queries; } FreshRSS_Context::userConf()->save(); diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index 17c6c20bd..451e98a8b 100644 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -236,6 +236,30 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController { FreshRSS_View::prependTitle(_t('conf.sharing.title') . ' · '); } + private const SHORTCUT_KEYS = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', + 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'Backspace', 'Delete', + 'End', 'Enter', 'Escape', 'Home', 'Insert', 'PageDown', 'PageUp', 'Space', 'Tab', + ]; + + /** + * @param array $shortcuts + * @return list + */ + public static function getNonStandardShortcuts(array $shortcuts): array { + $standard = strtolower(implode(' ', self::SHORTCUT_KEYS)); + + $nonStandard = array_filter($shortcuts, static function (string $shortcut) use ($standard) { + $shortcut = trim($shortcut); + return $shortcut !== '' && stripos($standard, $shortcut) === false; + }); + + return array_values($nonStandard); + } + /** * This action handles the shortcut configuration page. * @@ -249,7 +273,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController { * tab and up. */ public function shortcutAction(): void { - $this->view->list_keys = SHORTCUT_KEYS; + $this->view->list_keys = self::SHORTCUT_KEYS; if (Minz_Request::isPost()) { $shortcuts = Minz_Request::paramArray('shortcuts', plaintext: true); diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 315d37101..9b52cd168 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -50,7 +50,7 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController { $cacheFile = CACHE_PATH . '/extension_list.json'; if (FreshRSS_Context::userConf()->retrieve_extension_list === true) { if (!file_exists($cacheFile) || (time() - (filemtime($cacheFile) ?: 0) > 86400)) { - $json = httpGet($extensionListUrl, $cacheFile, 'json')['body']; + $json = FreshRSS_http_Util::httpGet($extensionListUrl, $cacheFile, 'json')['body']; } else { $json = @file_get_contents($cacheFile) ?: ''; } diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 678388cbb..b6ecbeec2 100644 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -417,14 +417,14 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController { } /** - * @param \SimplePie\SimplePie|null $simplePiePush Used by WebSub (PubSubHubbub) to push updates + * @param FreshRSS_SimplePieCustom|null $simplePiePush Used by WebSub (PubSubHubbub) to push updates * @param string $selfUrl Used by WebSub (PubSubHubbub) to override the feed URL * @return array{0:int,1:FreshRSS_Feed|null,2:int,3:array} Number of updated feeds, first feed or null, number of new articles, * list of feeds for which a cache refresh is needed * @throws FreshRSS_BadUrl_Exception */ public static function actualizeFeeds(?int $feed_id = null, ?string $feed_url = null, ?int $maxFeeds = null, - ?\SimplePie\SimplePie $simplePiePush = null, string $selfUrl = ''): array { + ?FreshRSS_SimplePieCustom $simplePiePush = null, string $selfUrl = ''): array { if (function_exists('set_time_limit')) { @set_time_limit(300); } @@ -866,14 +866,14 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController { } /** - * @param \SimplePie\SimplePie|null $simplePiePush Used by WebSub (PubSubHubbub) to push updates + * @param FreshRSS_SimplePieCustom|null $simplePiePush Used by WebSub (PubSubHubbub) to push updates * @param string $selfUrl Used by WebSub (PubSubHubbub) to override the feed URL * @return array{0:int,1:FreshRSS_Feed|null,2:int,3:array} Number of updated feeds, first feed or null, number of new articles, * list of feeds for which a cache refresh is needed * @throws FreshRSS_BadUrl_Exception */ public static function actualizeFeedsAndCommit(?int $feed_id = null, ?string $feed_url = null, ?int $maxFeeds = null, - ?SimplePie\SimplePie $simplePiePush = null, string $selfUrl = ''): array { + ?FreshRSS_SimplePieCustom $simplePiePush = null, string $selfUrl = ''): array { $entryDAO = FreshRSS_Factory::createEntryDao(); [$nbUpdatedFeeds, $feed, $nbNewArticles, $feedsCacheToRefresh] = FreshRSS_feed_Controller::actualizeFeeds($feed_id, $feed_url, $maxFeeds, $simplePiePush, $selfUrl); @@ -1066,7 +1066,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController { } // Remove related queries - $queries = remove_query_by_get('f_' . $feed_id, FreshRSS_Context::userConf()->queries); + $queries = FreshRSS_UserQuery::remove_query_by_get('f_' . $feed_id, FreshRSS_Context::userConf()->queries); FreshRSS_Context::userConf()->queries = $queries; FreshRSS_Context::userConf()->save(); return true; diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 7dc825b9e..d1635a046 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -448,7 +448,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController { } else { $content = ''; } - $content = sanitizeHTML($content, $url); + $content = FreshRSS_SimplePieCustom::sanitizeHTML($content, $url); if (is_int($item['published'] ?? null) || is_string($item['published'] ?? null)) { $published = (string)$item['published']; diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index 9005dff93..59302d3f1 100644 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -383,7 +383,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController { } $this->view->terms_of_service = $terms_of_service; - $this->view->can_register = !max_registrations_reached(); + $this->view->can_register = !FreshRSS_user_Controller::max_registrations_reached(); FreshRSS_View::prependTitle(_t('index.tos.title') . ' · '); } diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php index eda468dff..32bdee305 100644 --- a/app/Controllers/javascriptController.php +++ b/app/Controllers/javascriptController.php @@ -71,7 +71,7 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController { Minz_Error::error(400); return; } - $user_conf = get_user_configuration($user); + $user_conf = FreshRSS_UserConfiguration::getForUser($user); if ($user_conf !== null) { try { $s = $user_conf->passwordHash; diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php index 244a16671..a2d1c1d07 100644 --- a/app/Controllers/subscriptionController.php +++ b/app/Controllers/subscriptionController.php @@ -337,9 +337,9 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController { $values = [ 'name' => Minz_Request::paramString('name'), 'kind' => $feed->kind(), - 'description' => sanitizeHTML(Minz_Request::paramString('description', true)), - 'website' => checkUrl(Minz_Request::paramString('website')) ?: '', - 'url' => checkUrl(Minz_Request::paramString('url')) ?: '', + 'description' => FreshRSS_SimplePieCustom::sanitizeHTML(Minz_Request::paramString('description', true)), + 'website' => FreshRSS_http_Util::checkUrl(Minz_Request::paramString('website')) ?: '', + 'url' => FreshRSS_http_Util::checkUrl(Minz_Request::paramString('url')) ?: '', 'category' => Minz_Request::paramInt('category'), 'pathEntries' => Minz_Request::paramString('path_entries'), 'priority' => Minz_Request::paramTernary('priority') === null ? FreshRSS_Feed::PRIORITY_MAIN_STREAM : Minz_Request::paramInt('priority'), diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php index 7c204de8c..98d552688 100644 --- a/app/Controllers/updateController.php +++ b/app/Controllers/updateController.php @@ -334,14 +334,85 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController { } } + /** + * Check PHP and its extensions are well-installed. + * + * @return array of tested values. + */ + private static function check_install_php(): array { + $pdo_mysql = extension_loaded('pdo_mysql'); + $pdo_pgsql = extension_loaded('pdo_pgsql'); + $pdo_sqlite = extension_loaded('pdo_sqlite'); + return [ + 'php' => version_compare(PHP_VERSION, FRESHRSS_MIN_PHP_VERSION) >= 0, + 'curl' => extension_loaded('curl'), + 'pdo' => $pdo_mysql || $pdo_sqlite || $pdo_pgsql, + 'pcre' => extension_loaded('pcre'), + 'ctype' => extension_loaded('ctype'), + 'fileinfo' => extension_loaded('fileinfo'), + 'dom' => class_exists('DOMDocument'), + 'json' => extension_loaded('json'), + 'mbstring' => extension_loaded('mbstring'), + 'zip' => extension_loaded('zip'), + ]; + } + + /** + * Check different data files and directories exist. + * @return array of tested values. + */ + private static function check_install_files(): array { + return [ + 'data' => is_dir(DATA_PATH) && touch(DATA_PATH . '/index.html'), // is_writable() is not reliable for a folder on NFS + 'cache' => is_dir(CACHE_PATH) && touch(CACHE_PATH . '/index.html'), + 'users' => is_dir(USERS_PATH) && touch(USERS_PATH . '/index.html'), + 'favicons' => is_dir(DATA_PATH) && touch(DATA_PATH . '/favicons/index.html'), + 'tokens' => is_dir(DATA_PATH) && touch(DATA_PATH . '/tokens/index.html'), + ]; + } + + /** + * Check database is well-installed. + * + * @return array 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('admin.check_install.title') . ' · '); - $this->view->status_php = check_install_php(); - $this->view->status_files = check_install_files(); - $this->view->status_database = check_install_database(); + $this->view->status_php = self::check_install_php(); + $this->view->status_files = self::check_install_files(); + $this->view->status_database = self::check_install_database(); } } diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index a7a79b067..50a89eb3c 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -15,6 +15,37 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1; } + /** + * Validate an email address, supports internationalized addresses. + * + * @param string $email The address to validate + * @return bool true if email is valid, else false + */ + private static function validateEmailAddress(string $email): bool { + $mailer = new PHPMailer\PHPMailer\PHPMailer(); + $mailer->CharSet = 'utf-8'; + $punyemail = $mailer->punyencodeAddress($email); + return PHPMailer\PHPMailer\PHPMailer::validateAddress($punyemail, 'html5'); + } + + /** + * @return list + */ + public static function listUsers(): array { + $final_list = []; + $base_path = join_path(DATA_PATH, 'users'); + $dir_list = array_values(array_diff( + scandir($base_path) ?: [], + ['..', '.', Minz_User::INTERNAL_USER] + )); + foreach ($dir_list as $file) { + if ($file[0] !== '.' && is_dir(join_path($base_path, $file)) && file_exists(join_path($base_path, $file, 'config.php'))) { + $final_list[] = $file; + } + } + return $final_list; + } + public static function userExists(string $username): bool { $config_path = USERS_PATH . '/' . $username . '/config.php'; if (@file_exists($config_path)) { @@ -30,9 +61,21 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { return false; } + /** + * Return if the maximum number of registrations has been reached. + * Note a max_registrations of 0 means there is no limit. + * + * @return bool true if number of users >= max registrations, false else. + */ + public static function max_registrations_reached(): bool { + $limit_registrations = FreshRSS_Context::systemConf()->limits['max_registrations']; + $number_accounts = count(self::listUsers()); + return $limit_registrations > 0 && $number_accounts >= $limit_registrations; + } + /** @param array $userConfigUpdated */ public static function updateUser(string $user, ?string $email, string $passwordPlain, array $userConfigUpdated = []): bool { - $userConfig = get_user_configuration($user); + $userConfig = FreshRSS_UserConfiguration::getForUser($user); if ($userConfig === null) { return false; } @@ -166,7 +209,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { ); } - if (!empty($email) && !validateEmailAddress($email)) { + if (!empty($email) && !self::validateEmailAddress($email)) { Minz_Request::bad( _t('user.email.feedback.invalid'), ['c' => 'user', 'a' => 'profile'] @@ -285,7 +328,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { $this->view->show_email_field = FreshRSS_Context::systemConf()->force_email_validation; $this->view->current_user = Minz_Request::paramString('u'); - foreach (listUsers() as $user) { + foreach (self::listUsers() as $user) { $this->view->users[$user] = $this->retrieveUserDetails($user); } } @@ -322,7 +365,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { $userConfig['language'] = Minz_Translate::DEFAULT_LANGUAGE; } - $ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers()), true); //Not an existing user, case-insensitive + $ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', self::listUsers()), true); //Not an existing user, case-insensitive $configPath = join_path($homeDir, 'config.php'); $ok &= !file_exists($configPath); @@ -370,7 +413,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { * @todo clean up this method. Idea: write a method to init a user with basic information. */ public function createAction(): void { - if (!FreshRSS_Auth::hasAccess('admin') && max_registrations_reached()) { + if (!FreshRSS_Auth::hasAccess('admin') && self::max_registrations_reached()) { Minz_Error::error(403); } @@ -424,7 +467,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { ); } - if (!empty($email) && !validateEmailAddress($email)) { + if (!empty($email) && !self::validateEmailAddress($email)) { Minz_Request::bad( _t('user.email.feedback.invalid'), $badRedirectUrl @@ -451,7 +494,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { // user just created its account himself so he probably wants to // get started immediately. if ($ok && !FreshRSS_Auth::hasAccess('admin')) { - $user_conf = get_user_configuration($new_user_name); + $user_conf = FreshRSS_UserConfiguration::getForUser($new_user_name); if ($user_conf !== null) { Minz_Session::_params([ Minz_User::CURRENT_USER => $new_user_name, @@ -531,7 +574,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { $token = Minz_Request::paramString('token'); if ($username !== '') { - $user_config = get_user_configuration($username); + $user_config = FreshRSS_UserConfiguration::getForUser($username); } elseif (FreshRSS_Auth::hasAccess()) { $user_config = FreshRSS_Context::userConf(); } else { @@ -711,7 +754,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { Minz_Error::error(404); } - if (null === $userConfig = get_user_configuration($username)) { + if (null === $userConfig = FreshRSS_UserConfiguration::getForUser($username)) { Minz_Error::error(500); return; } @@ -769,7 +812,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { $entryDAO = FreshRSS_Factory::createEntryDao($username); $databaseDAO = FreshRSS_Factory::createDatabaseDAO($username); - $userConfiguration = get_user_configuration($username); + $userConfiguration = FreshRSS_UserConfiguration::getForUser($username); if ($userConfiguration === null) { throw new Exception('Error loading user configuration!'); } -- cgit v1.2.3