PHPStan level 9 for ./p/ and lib_rss.php (#5049)

And app/FreshRSS.php
Contributes to https://github.com/FreshRSS/FreshRSS/issues/4112
This commit is contained in:
Alexandre Alapetite
2023-01-29 18:53:51 +01:00
committed by GitHub
parent 2303b29e68
commit 4f316b2ed3
12 changed files with 1292 additions and 1196 deletions

View File

@@ -18,7 +18,7 @@ class FreshRSS extends Minz_FrontController {
* - Init notifications
* - Enable user extensions (need all the other initializations)
*/
public function init() {
public function init(): void {
if (!isset($_SESSION)) {
Minz_Session::init('FreshRSS');
}
@@ -71,10 +71,10 @@ class FreshRSS extends Minz_FrontController {
Minz_ExtensionManager::callHook('freshrss_init');
}
private static function initAuth() {
private static function initAuth(): void {
FreshRSS_Auth::init();
if (Minz_Request::isPost()) {
if (!(FreshRSS_Auth::isCsrfOk() ||
if (FreshRSS_Context::$system_conf == null || !(FreshRSS_Auth::isCsrfOk() ||
(Minz_Request::controllerName() === 'auth' && Minz_Request::actionName() === 'login') ||
(Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'create' && !FreshRSS_Auth::hasAccess('admin')) ||
(Minz_Request::controllerName() === 'feed' && Minz_Request::actionName() === 'actualize'
@@ -92,7 +92,7 @@ class FreshRSS extends Minz_FrontController {
}
}
private static function initI18n() {
private static function initI18n(): void {
$userLanguage = isset(FreshRSS_Context::$user_conf) ? FreshRSS_Context::$user_conf->language : null;
$systemLanguage = isset(FreshRSS_Context::$system_conf) ? FreshRSS_Context::$system_conf->language : null;
$language = Minz_Translate::getLanguage($userLanguage, Minz_Request::getPreferredLanguages(), $systemLanguage);
@@ -107,12 +107,15 @@ class FreshRSS extends Minz_FrontController {
date_default_timezone_set($timezone);
}
private static function getThemeFileUrl($theme_id, $filename) {
private static function getThemeFileUrl(string $theme_id, string $filename): string {
$filetime = @filemtime(PUBLIC_PATH . '/themes/' . $theme_id . '/' . $filename);
return '/themes/' . $theme_id . '/' . $filename . '?' . $filetime;
}
public static function loadStylesAndScripts() {
public static function loadStylesAndScripts(): void {
if (FreshRSS_Context::$user_conf == null) {
return;
}
$theme = FreshRSS_Themes::load(FreshRSS_Context::$user_conf->theme);
if ($theme) {
foreach(array_reverse($theme['files']) as $file) {
@@ -146,22 +149,23 @@ class FreshRSS extends Minz_FrontController {
FreshRSS_View::prependScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
}
private static function loadNotifications() {
private static function loadNotifications(): void {
$notif = Minz_Request::getNotification();
if ($notif) {
FreshRSS_View::_param('notification', $notif);
}
}
public static function preLayout() {
public static function preLayout(): void {
header("X-Content-Type-Options: nosniff");
FreshRSS_Share::load(join_path(APP_PATH, 'shares.php'));
self::loadStylesAndScripts();
}
private static function checkEmailValidated() {
$email_not_verified = FreshRSS_Auth::hasAccess() && FreshRSS_Context::$user_conf->email_validation_token !== '';
private static function checkEmailValidated(): void {
$email_not_verified = FreshRSS_Auth::hasAccess() &&
FreshRSS_Context::$user_conf !== null && FreshRSS_Context::$user_conf->email_validation_token !== '';
$action_is_allowed = (
Minz_Request::is('user', 'validateEmail') ||
Minz_Request::is('user', 'sendValidationEmail') ||

View File

@@ -17,10 +17,14 @@ class FreshRSS_Entry extends Minz_Model {
*/
private $guid;
/** @var string */
private $title;
private $authors;
/** @var string */
private $content;
/** @var string */
private $link;
/** @var int */
private $date;
private $date_added = 0; //In microseconds
/**
@@ -298,6 +302,7 @@ HTML;
public function link(): string {
return $this->link;
}
/** @return string|int */
public function date(bool $raw = false) {
if ($raw) {
return $this->date;

View File

@@ -1165,10 +1165,12 @@ SQL;
}
}
public function listByIds($ids, $order = 'DESC') {
/** @param array<string> $ids */
public function listByIds(array $ids, string $order = 'DESC') {
if (count($ids) < 1) {
yield false;
} elseif (count($ids) > FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER) {
return;
}
if (count($ids) > FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER) {
// Split a query with too many variables parameters
$idsChunks = array_chunk($ids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER);
foreach ($idsChunks as $idsChunk) {
@@ -1195,15 +1197,16 @@ SQL;
/**
* For API
* @return array<string>
*/
public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL,
$order = 'DESC', $limit = 1, $firstId = '', $filters = null) {
$order = 'DESC', $limit = 1, $firstId = '', $filters = null): array {
list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters);
$stm = $this->pdo->prepare($sql);
$stm->execute($values);
return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
return $stm->fetchAll(PDO::FETCH_COLUMN, 0) ?: [];
}
public function listHashForFeedGuids($id_feed, $guids) {

View File

@@ -26,7 +26,7 @@ class Minz_ModelPdo {
private static $sharedCurrentUser;
/**
* @var Minz_Pdo|null
* @var Minz_Pdo
*/
protected $pdo;

View File

@@ -87,10 +87,10 @@ class Minz_Translate {
* preferred languages then returns the default language
* @param string|null $user the connected user language (nullable)
* @param array<string> $preferred an array of the preferred languages
* @param string $default the preferred language to use
* @param string|null $default the preferred language to use
* @return string containing the language to use
*/
public static function getLanguage($user, $preferred, $default) {
public static function getLanguage(?string $user, array $preferred, ?string $default): string {
if (null !== $user) {
return $user;
}

View File

@@ -4,8 +4,8 @@ if (version_compare(PHP_VERSION, FRESHRSS_MIN_PHP_VERSION, '<')) {
}
if (!function_exists('mb_strcut')) {
function mb_strcut($str, $start, $length = null, $encoding = 'UTF-8') {
return substr($str, $start, $length);
function mb_strcut(string $str, int $start, ?int $length = null, string $encoding = 'UTF-8'): string {
return substr($str, $start, $length) ?: '';
}
}
@@ -34,7 +34,7 @@ function join_path(...$path_parts): string {
}
//<Auto-loading>
function classAutoloader($class) {
function classAutoloader(string $class): void {
if (strpos($class, 'FreshRSS') === 0) {
$components = explode('_', $class);
switch (count($components)) {
@@ -73,14 +73,10 @@ function classAutoloader($class) {
spl_autoload_register('classAutoloader');
//</Auto-loading>
/**
* @param string $url
* @return string
*/
function idn_to_puny($url) {
function idn_to_puny(string $url): string {
if (function_exists('idn_to_ascii')) {
$idn = parse_url($url, PHP_URL_HOST);
if ($idn != '') {
if (is_string($idn) && $idn != '') {
// https://wiki.php.net/rfc/deprecate-and-remove-intl_idna_variant_2003
if (defined('INTL_IDNA_VARIANT_UTS46')) {
$puny = idn_to_ascii($idn, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
@@ -90,7 +86,7 @@ function idn_to_puny($url) {
$puny = idn_to_ascii($idn);
}
$pos = strpos($url, $idn);
if ($puny != '' && $pos !== false) {
if ($puny != false && $pos !== false) {
$url = substr_replace($url, $puny, $pos, strlen($idn));
}
}
@@ -99,11 +95,9 @@ function idn_to_puny($url) {
}
/**
* @param string $url
* @param bool $fixScheme
* @return string|false
*/
function checkUrl($url, $fixScheme = true) {
function checkUrl(string $url, bool $fixScheme = true) {
$url = trim($url);
if ($url == '') {
return '';
@@ -127,31 +121,19 @@ function checkUrl($url, $fixScheme = true) {
* @return string
*/
function safe_ascii($text) {
return filter_var($text, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
return filter_var($text, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?: '';
}
if (function_exists('mb_convert_encoding')) {
/**
* @param string $text
* @return string
*/
function safe_utf8($text) {
return mb_convert_encoding($text, 'UTF-8', 'UTF-8');
function safe_utf8(string $text): string {
return mb_convert_encoding($text, 'UTF-8', 'UTF-8') ?: '';
}
} elseif (function_exists('iconv')) {
/**
* @param string $text
* @return string
*/
function safe_utf8($text) {
return iconv('UTF-8', 'UTF-8//IGNORE', $text);
function safe_utf8(string $text): string {
return iconv('UTF-8', 'UTF-8//IGNORE', $text) ?: '';
}
} else {
/**
* @param string $text
* @return string
*/
function safe_utf8($text) {
function safe_utf8(string $text): string {
return $text;
}
}
@@ -178,14 +160,14 @@ function escapeToUnicodeAlternative($text, $extended = true) {
return trim(str_replace($problem, $replace, $text));
}
function format_number($n, $precision = 0) {
function format_number(float $n, int $precision = 0): string {
// number_format does not seem to be Unicode-compatible
return str_replace(' ', '', // Thin non-breaking space
number_format($n, $precision, '.', ' ')
);
}
function format_bytes($bytes, $precision = 2, $system = 'IEC') {
function format_bytes(int $bytes, int $precision = 2, string $system = 'IEC'): string {
if ($system === 'IEC') {
$base = 1024;
$units = array('B', 'KiB', 'MiB', 'GiB', 'TiB');
@@ -202,7 +184,7 @@ function format_bytes($bytes, $precision = 2, $system = 'IEC') {
return format_number($bytes, $precision) . ' ' . $units[$pow];
}
function timestamptodate ($t, $hour = true) {
function timestamptodate(int $t, bool $hour = true): string {
$month = _t('gen.date.' . date('M', $t));
if ($hour) {
$date = _t('gen.date.format_date_hour', $month);
@@ -210,14 +192,13 @@ function timestamptodate ($t, $hour = true) {
$date = _t('gen.date.format_date', $month);
}
return @date ($date, $t);
return @date($date, $t) ?: '';
}
/**
* Decode HTML entities but preserve XML entities.
* @param string|null $text
*/
function html_only_entity_decode($text): string {
function html_only_entity_decode(?string $text): string {
static $htmlEntitiesOnly = null;
if ($htmlEntitiesOnly === null) {
$htmlEntitiesOnly = array_flip(array_diff(
@@ -225,7 +206,7 @@ function html_only_entity_decode($text): string {
get_html_translation_table(HTML_SPECIALCHARS, ENT_NOQUOTES, 'UTF-8') //Preserve XML entities
));
}
return $text == '' ? '' : strtr($text, $htmlEntitiesOnly);
return $text == null ? '' : strtr($text, $htmlEntitiesOnly);
}
/**
@@ -239,8 +220,10 @@ function sensitive_log($log) {
foreach ($log as $k => $v) {
if (in_array($k, ['api_key', 'Passwd', 'T'])) {
$log[$k] = '██';
} else {
} elseif (is_array($v) || is_string($v)) {
$log[$k] = sensitive_log($v);
} else {
return '';
}
}
} elseif (is_string($log)) {
@@ -248,7 +231,7 @@ function sensitive_log($log) {
'/\b(auth=.*?\/)[^&]+/i',
'/\b(Passwd=)[^&]+/i',
'/\b(Authorization)[^&]+/i',
], '$1█', $log);
], '$1█', $log) ?? '';
}
return $log;
}
@@ -257,6 +240,9 @@ function sensitive_log($log) {
* @param array<string,mixed> $attributes
*/
function customSimplePie($attributes = array()): SimplePie {
if (FreshRSS_Context::$system_conf === null) {
throw new FreshRSS_Context_Exception('System configuration not initialised!');
}
$limits = FreshRSS_Context::$system_conf->limits;
$simplePie = new SimplePie();
$simplePie->set_useragent(FRESHRSS_USERAGENT);
@@ -338,13 +324,13 @@ function customSimplePie($attributes = array()): SimplePie {
}
/**
* @param int|false $maxLength
* @param string $data
*/
function sanitizeHTML($data, string $base = '', $maxLength = false) {
if (!is_string($data) || ($maxLength !== false && $maxLength <= 0)) {
function sanitizeHTML($data, string $base = '', ?int $maxLength = null): string {
if (!is_string($data) || ($maxLength !== null && $maxLength <= 0)) {
return '';
}
if ($maxLength !== false) {
if ($maxLength !== null) {
$data = mb_strcut($data, 0, $maxLength, 'UTF-8');
}
static $simplePie = null;
@@ -353,7 +339,7 @@ function sanitizeHTML($data, string $base = '', $maxLength = false) {
$simplePie->init();
}
$result = html_only_entity_decode($simplePie->sanitize->sanitize($data, SIMPLEPIE_CONSTRUCT_HTML, $base));
if ($maxLength !== false && strlen($result) > $maxLength) {
if ($maxLength !== null && strlen($result) > $maxLength) {
//Sanitizing has made the result too long so try again shorter
$data = mb_strcut($result, 0, (2 * $maxLength) - strlen($result) - 2, 'UTF-8');
return sanitizeHTML($data, $base, $maxLength);
@@ -361,9 +347,9 @@ function sanitizeHTML($data, string $base = '', $maxLength = false) {
return $result;
}
function cleanCache(int $hours = 720) {
function cleanCache(int $hours = 720): void {
// N.B.: GLOB_BRACE is not available on all platforms
$files = array_merge(glob(CACHE_PATH . '/*.html', GLOB_NOSORT), glob(CACHE_PATH . '/*.spc', GLOB_NOSORT));
$files = array_merge(glob(CACHE_PATH . '/*.html', GLOB_NOSORT) ?: [], glob(CACHE_PATH . '/*.spc', GLOB_NOSORT) ?: []);
foreach ($files as $file) {
if (substr($file, -10) === 'index.html') {
continue;
@@ -412,13 +398,16 @@ function enforceHttpEncoding(string $html, string $contentType = ''): string {
* @param array<string,mixed> $attributes
*/
function httpGet(string $url, string $cachePath, string $type = 'html', array $attributes = []): string {
if (FreshRSS_Context::$system_conf === null) {
throw new FreshRSS_Context_Exception('System configuration not initialised!');
}
$limits = FreshRSS_Context::$system_conf->limits;
$feed_timeout = empty($attributes['timeout']) ? 0 : intval($attributes['timeout']);
$cacheMtime = @filemtime($cachePath);
if ($cacheMtime !== false && $cacheMtime > time() - intval($limits['cache_duration'])) {
$body = @file_get_contents($cachePath);
if ($body != '') {
if ($body != false) {
syslog(LOG_DEBUG, 'FreshRSS uses cache for ' . SimplePie_Misc::url_remove_credentials($url));
return $body;
}
@@ -472,7 +461,7 @@ function httpGet(string $url, string $cachePath, string $type = 'html', array $a
}
$body = curl_exec($ch);
$c_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$c_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); //TODO: Check if that may be null
$c_content_type = '' . curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$c_error = curl_error($ch);
curl_close($ch);
@@ -481,7 +470,7 @@ function httpGet(string $url, string $cachePath, string $type = 'html', array $a
$body = '';
// TODO: Implement HTTP 410 Gone
}
if ($body == false) {
if (!is_string($body)) {
$body = '';
} else {
$body = enforceHttpEncoding($body, $c_content_type);
@@ -498,10 +487,9 @@ function httpGet(string $url, string $cachePath, string $type = 'html', array $a
* Validate an email address, supports internationalized addresses.
*
* @param string $email The address to validate
*
* @return bool true if email is valid, else false
*/
function validateEmailAddress($email) {
function validateEmailAddress(string $email): bool {
$mailer = new PHPMailer\PHPMailer\PHPMailer();
$mailer->CharSet = 'utf-8';
$punyemail = $mailer->punyencodeAddress($email);
@@ -512,9 +500,8 @@ function validateEmailAddress($email) {
* Add support of image lazy loading
* Move content from src attribute to data-original
* @param string $content is the text we want to parse
* @return string
*/
function lazyimg($content) {
function lazyimg(string $content): string {
return preg_replace([
'/<((?:img|iframe)[^>]+?)src="([^"]+)"([^>]*)>/i',
"/<((?:img|iframe)[^>]+?)src='([^']+)'([^>]*)>/i",
@@ -523,18 +510,15 @@ function lazyimg($content) {
"<$1src='" . Minz_Url::display('/themes/icons/grey.gif') . "' data-original='$2'$3>",
],
$content
);
) ?? '';
}
/**
* @return string
*/
function uTimeString() {
function uTimeString(): string {
$t = @gettimeofday();
return $t['sec'] . str_pad('' . $t['usec'], 6, '0', STR_PAD_LEFT);
}
function invalidateHttpCache($username = '') {
function invalidateHttpCache(string $username = ''): bool {
if (!FreshRSS_user_Controller::checkUsername($username)) {
Minz_Session::_param('touch', uTimeString());
$username = Minz_Session::param('currentUser', '_');
@@ -549,12 +533,12 @@ function invalidateHttpCache($username = '') {
/**
* @return array<string>
*/
function listUsers() {
function listUsers(): array {
$final_list = array();
$base_path = join_path(DATA_PATH, 'users');
$dir_list = array_values(array_diff(
scandir($base_path),
array('..', '.', '_')
scandir($base_path) ?: [],
['..', '.', '_']
));
foreach ($dir_list as $file) {
if ($file[0] !== '.' && is_dir(join_path($base_path, $file)) && file_exists(join_path($base_path, $file, 'config.php'))) {
@@ -567,12 +551,14 @@ function listUsers() {
/**
* Return if the maximum number of registrations has been reached.
*
* Note a max_regstrations of 0 means there is no limit.
* Note a max_registrations of 0 means there is no limit.
*
* @return boolean true if number of users >= max registrations, false else.
*/
function max_registrations_reached() {
function max_registrations_reached(): bool {
if (FreshRSS_Context::$system_conf === null) {
throw new FreshRSS_Context_Exception('System configuration not initialised!');
}
$limit_registrations = FreshRSS_Context::$system_conf->limits['max_registrations'];
$number_accounts = count(listUsers());
@@ -589,7 +575,7 @@ function max_registrations_reached() {
* @param string $username the name of the user of which we want the configuration.
* @return FreshRSS_UserConfiguration|null object, or null if the configuration cannot be loaded.
*/
function get_user_configuration($username) {
function get_user_configuration(string $username) {
if (!FreshRSS_user_Controller::checkUsername($username)) {
return null;
}
@@ -621,7 +607,7 @@ function get_user_configuration($username) {
*/
function ipToBits(string $ip): string {
$binaryip = '';
foreach (str_split(inet_pton($ip)) as $char) {
foreach (str_split(inet_pton($ip) ?: '') as $char) {
$binaryip .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
}
return $binaryip;
@@ -654,6 +640,9 @@ function checkCIDR(string $ip, string $range): bool {
* @return boolean, true if the sender's IP is in one of the ranges defined in the configuration, else false
*/
function checkTrustedIP(): bool {
if (FreshRSS_Context::$system_conf === null) {
throw new FreshRSS_Context_Exception('System configuration not initialised!');
}
if (!empty($_SERVER['REMOTE_ADDR'])) {
foreach (FreshRSS_Context::$system_conf->trusted_sources as $cidr) {
if (checkCIDR($_SERVER['REMOTE_ADDR'], $cidr)) {
@@ -664,10 +653,7 @@ function checkTrustedIP(): bool {
return false;
}
/**
* @return string
*/
function httpAuthUser() {
function httpAuthUser(): string {
if (!empty($_SERVER['REMOTE_USER'])) {
return $_SERVER['REMOTE_USER'];
} elseif (!empty($_SERVER['HTTP_REMOTE_USER']) && checkTrustedIP()) {
@@ -680,10 +666,7 @@ function httpAuthUser() {
return '';
}
/**
* @return bool
*/
function cryptAvailable() {
function cryptAvailable(): bool {
try {
$hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
return $hash === @crypt('password', $hash);
@@ -699,7 +682,7 @@ function cryptAvailable() {
*
* @return array<string,bool> of tested values.
*/
function check_install_php() {
function check_install_php(): array {
$pdo_mysql = extension_loaded('pdo_mysql');
$pdo_pgsql = extension_loaded('pdo_pgsql');
$pdo_sqlite = extension_loaded('pdo_sqlite');
@@ -723,7 +706,7 @@ function check_install_php() {
*
* @return array<string,bool> of tested values.
*/
function check_install_files() {
function check_install_files(): array {
return array(
// @phpstan-ignore-next-line
'data' => DATA_PATH && touch(DATA_PATH . '/index.html'), // is_writable() is not reliable for a folder on NFS
@@ -742,7 +725,7 @@ function check_install_files() {
*
* @return array<string,bool> of tested values.
*/
function check_install_database() {
function check_install_database(): array {
$status = array(
'connection' => true,
'tables' => false,
@@ -773,17 +756,14 @@ function check_install_database() {
/**
* Remove a directory recursively.
*
* From http://php.net/rmdir#110489
*
* @param string $dir the directory to remove
*/
function recursive_unlink($dir) {
function recursive_unlink(string $dir): bool {
if (!is_dir($dir)) {
return true;
}
$files = array_diff(scandir($dir), array('.', '..'));
$files = array_diff(scandir($dir) ?: [], ['.', '..']);
foreach ($files as $filename) {
$filename = $dir . '/' . $filename;
if (is_dir($filename)) {
@@ -803,7 +783,7 @@ function recursive_unlink($dir) {
* @param array<int,array<string,string>> $queries an array of queries.
* @return array<int,array<string,string>> without queries where $get is appearing.
*/
function remove_query_by_get($get, $queries) {
function remove_query_by_get(string $get, array $queries): array {
$final_queries = array();
foreach ($queries as $key => $query) {
if (empty($query['get']) || $query['get'] !== $get) {
@@ -827,7 +807,11 @@ const SHORTCUT_KEYS = [
'End', 'Enter', 'Escape', 'Home', 'Insert', 'PageDown', 'PageUp', 'Space', 'Tab',
];
function getNonStandardShortcuts($shortcuts) {
/**
* @param array<string> $shortcuts
* @return array<string>
*/
function getNonStandardShortcuts(array $shortcuts): array {
$standard = strtolower(implode(' ', SHORTCUT_KEYS));
$nonStandard = array_filter($shortcuts, function ($shortcut) use ($standard) {
@@ -838,7 +822,7 @@ function getNonStandardShortcuts($shortcuts) {
return $nonStandard;
}
function errorMessageInfo($errorTitle, $error = '') {
function errorMessageInfo(string $errorTitle, string $error = ''): string {
$errorTitle = htmlspecialchars($errorTitle, ENT_NOQUOTES, 'UTF-8');
$message = '';

View File

@@ -17,7 +17,7 @@ require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
FreshRSS_Context::initSystem();
// check if API is enabled globally
if (!FreshRSS_Context::$system_conf->api_enabled) {
if (FreshRSS_Context::$system_conf == null || !FreshRSS_Context::$system_conf->api_enabled) {
Minz_Log::warning('Fever API: service unavailable!');
Minz_Log::debug('Fever API: serviceUnavailable() ' . debugInfo(), API_LOG);
header('HTTP/1.1 503 Service Unavailable');
@@ -29,12 +29,9 @@ Minz_Session::init('FreshRSS', true);
// ================================================================================================
// <Debug>
$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576);
$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576) ?: '';;
/**
* @return string
*/
function debugInfo() {
function debugInfo(): string {
if (function_exists('getallheaders')) {
$ALL_HEADERS = getallheaders();
} else { //nginx http://php.net/getallheaders#84262
@@ -62,8 +59,12 @@ function debugInfo() {
//Minz_Log::debug(debugInfo(), API_LOG);
// </Debug>
class FeverDAO extends Minz_ModelPdo
final class FeverDAO extends Minz_ModelPdo
{
/**
* @param array<string|int> $values
* @param array<string,string|int> $bindArray
*/
protected function bindParamArray(string $prefix, array $values, array &$bindArray): string {
$str = '';
for ($i = 0; $i < count($values); $i++) {
@@ -74,9 +75,11 @@ class FeverDAO extends Minz_ModelPdo
}
/**
* @param array<string|int> $feed_ids
* @param array<string> $entry_ids
* @return FreshRSS_Entry[]
*/
public function findEntries(array $feed_ids, array $entry_ids, string $max_id, string $since_id) {
public function findEntries(array $feed_ids, array $entry_ids, string $max_id, string $since_id): array {
$values = array();
$order = '';
$entryDAO = FreshRSS_Factory::createEntryDao();
@@ -110,36 +113,34 @@ class FeverDAO extends Minz_ModelPdo
$sql .= ' LIMIT 50';
$stm = $this->pdo->prepare($sql);
$stm->execute($values);
$result = $stm->fetchAll(PDO::FETCH_ASSOC);
if ($stm && $stm->execute($values)) {
$result = $stm->fetchAll(PDO::FETCH_ASSOC);
$entries = array();
foreach ($result as $dao) {
$entries[] = FreshRSS_Entry::fromArray($dao);
$entries = array();
foreach ($result as $dao) {
$entries[] = FreshRSS_Entry::fromArray($dao);
}
return $entries;
}
return $entries;
return [];
}
}
/**
* Class FeverAPI
*/
class FeverAPI
final class FeverAPI
{
const API_LEVEL = 3;
const STATUS_OK = 1;
const STATUS_ERR = 0;
/**
* @var FreshRSS_EntryDAO|null
*/
private $entryDAO = null;
/** @var FreshRSS_EntryDAO */
private $entryDAO;
/**
* @var FreshRSS_FeedDAO|null
*/
private $feedDAO = null;
/** @var FreshRSS_FeedDAO */
private $feedDAO;
/**
* Authenticate the user
@@ -148,6 +149,9 @@ class FeverAPI
* your FreshRSS "username:your-api-password" combination
*/
private function authenticate(): bool {
if (FreshRSS_Context::$system_conf === null) {
throw new FreshRSS_Context_Exception('System configuration not initialised!');
}
FreshRSS_Context::$user_conf = null;
Minz_Session::_param('currentUser');
$feverKey = empty($_POST['api_key']) ? '' : substr(trim($_POST['api_key']), 0, 128);
@@ -176,16 +180,12 @@ class FeverAPI
public function isAuthenticatedApiUser(): bool {
$this->authenticate();
if (FreshRSS_Context::$user_conf !== null) {
return true;
}
return false;
return FreshRSS_Context::$user_conf !== null;
}
/**
* This does all the processing, since the fever api does not have a specific variable that specifies the operation
* @return array<string,mixed>
* @throws Exception
*/
public function process(): array {
@@ -226,37 +226,54 @@ class FeverAPI
$response_arr['saved_item_ids'] = $this->getSavedItemIds();
}
$id = isset($_REQUEST['id']) ? '' . $_REQUEST['id'] : '';
if (isset($_REQUEST['mark'], $_REQUEST['as'], $_REQUEST['id']) && ctype_digit($id)) {
$method_name = 'set' . ucfirst($_REQUEST['mark']) . 'As' . ucfirst($_REQUEST['as']);
$allowedMethods = array(
'setFeedAsRead', 'setGroupAsRead', 'setItemAsRead',
'setItemAsSaved', 'setItemAsUnread', 'setItemAsUnsaved'
);
if (in_array($method_name, $allowedMethods)) {
switch (strtolower($_REQUEST['mark'])) {
case 'item':
$this->{$method_name}($id);
break;
case 'feed':
case 'group':
$before = $_REQUEST['before'] ?? '';
$this->{$method_name}($id, $before);
break;
}
switch ($_REQUEST['as']) {
case 'read':
case 'unread':
$response_arr['unread_item_ids'] = $this->getUnreadItemIds();
break;
case 'saved':
case 'unsaved':
$response_arr['saved_item_ids'] = $this->getSavedItemIds();
break;
}
if (isset($_REQUEST['mark'], $_REQUEST['as'], $_REQUEST['id']) && ctype_digit($_REQUEST['id'])) {
$id = intval($_REQUEST['id']);
$before = intval($_REQUEST['before'] ?? '0');
switch (strtolower($_REQUEST['mark'])) {
case 'item':
switch ($_REQUEST['as']) {
case 'read':
$this->setItemAsRead($id);
break;
case 'saved':
$this->setItemAsSaved($id);
break;
case 'unread':
$this->setItemAsUnread($id);
break;
case 'unsaved':
$this->setItemAsUnsaved($id);
break;
}
break;
case 'feed':
switch ($_REQUEST['as']) {
case 'read':
$this->setFeedAsRead($id, $before);
break;
}
break;
case 'group':
switch ($_REQUEST['as']) {
case 'read':
$this->setFeedAsRead($id, $before);
break;
}
break;
}
switch ($_REQUEST['as']) {
case 'read':
case 'unread':
$response_arr['unread_item_ids'] = $this->getUnreadItemIds();
break;
case 'saved':
case 'unsaved':
$response_arr['saved_item_ids'] = $this->getSavedItemIds();
break;
}
}
return $response_arr;
@@ -264,6 +281,7 @@ class FeverAPI
/**
* Returns the complete JSON, with 'api_version' and status as 'auth'.
* @param array<string,mixed> $reply
*/
public function wrap(int $status, array $reply = array()): string {
$arr = array('api_version' => self::API_LEVEL, 'auth' => $status);
@@ -273,7 +291,7 @@ class FeverAPI
$arr = array_merge($arr, $reply);
}
return json_encode($arr);
return json_encode($arr) ?: '';
}
/**
@@ -292,6 +310,7 @@ class FeverAPI
return $lastUpdate;
}
/** @return array<array<string,string|int>> */
protected function getFeeds(): array {
$feeds = array();
$myFeeds = $this->feedDAO->listFeeds();
@@ -312,6 +331,7 @@ class FeverAPI
return $feeds;
}
/** @return array<array<string,int|string>> */
protected function getGroups(): array {
$groups = array();
@@ -329,12 +349,15 @@ class FeverAPI
return $groups;
}
/** @return array<array<string,int|string>> */
protected function getFavicons(): array {
if (FreshRSS_Context::$system_conf == null) {
return [];
}
$favicons = array();
$salt = FreshRSS_Context::$system_conf->salt;
$myFeeds = $this->feedDAO->listFeeds();
/** @var FreshRSS_Feed $feed */
foreach ($myFeeds as $feed) {
$id = hash('crc32b', $salt . $feed->url());
@@ -345,7 +368,7 @@ class FeverAPI
$favicons[] = array(
'id' => $feed->id(),
'data' => image_type_to_mime_type(exif_imagetype($filename)) . ';base64,' . base64_encode(file_get_contents($filename))
'data' => image_type_to_mime_type(exif_imagetype($filename) ?: 0) . ';base64,' . base64_encode(file_get_contents($filename) ?: '')
);
}
@@ -359,17 +382,19 @@ class FeverAPI
return $this->entryDAO->count();
}
/**
* @return array<array<string,int|string>>
*/
protected function getFeedsGroup(): array {
$groups = array();
$ids = array();
$myFeeds = $this->feedDAO->listFeeds();
/** @var FreshRSS_Feed $feed */
foreach ($myFeeds as $feed) {
$ids[$feed->categoryId()][] = $feed->id();
}
foreach($ids as $category => $feedIds) {
foreach ($ids as $category => $feedIds) {
$groups[] = array(
'group_id' => $category,
'feed_ids' => implode(',', $feedIds)
@@ -381,13 +406,14 @@ class FeverAPI
/**
* AFAIK there is no 'hot links' alternative in FreshRSS
* @return array<string>
*/
protected function getLinks(): array {
return array();
}
/**
* @param array $ids
* @param array<string> $ids
*/
protected function entriesToIdList(array $ids = array()): string {
return implode(',', array_values($ids));
@@ -398,10 +424,7 @@ class FeverAPI
return $this->entriesToIdList($entries);
}
/**
* @return string
*/
protected function getSavedItemIds() {
protected function getSavedItemIds(): string {
$entries = $this->entryDAO->listIdsWhere('a', '', FreshRSS_Entry::STATE_FAVORITE, 'ASC', 0);
return $this->entriesToIdList($entries);
}
@@ -409,31 +432,32 @@ class FeverAPI
/**
* @return integer|false
*/
protected function setItemAsRead($id) {
protected function setItemAsRead(int $id) {
return $this->entryDAO->markRead($id, true);
}
/**
* @return integer|false
*/
protected function setItemAsUnread($id) {
protected function setItemAsUnread(int $id) {
return $this->entryDAO->markRead($id, false);
}
/**
* @return integer|false
*/
protected function setItemAsSaved($id) {
protected function setItemAsSaved(int $id) {
return $this->entryDAO->markFavorite($id, true);
}
/**
* @return integer|false
*/
protected function setItemAsUnsaved($id) {
protected function setItemAsUnsaved(int $id) {
return $this->entryDAO->markFavorite($id, false);
}
/** @return array<array<string,string|int>> */
protected function getItems(): array {
$feed_ids = array();
$entry_ids = array();
@@ -448,16 +472,16 @@ class FeverAPI
if (isset($_REQUEST['group_ids'])) {
$categoryDAO = FreshRSS_Factory::createCategoryDao();
$group_ids = explode(',', $_REQUEST['group_ids']);
$feeds = [];
foreach ($group_ids as $id) {
/** @var FreshRSS_Category $category */
$category = $categoryDAO->searchById($id); //TODO: Transform to SQL query without loop! Consider FreshRSS_CategoryDAO::listCategories(true)
/** @var FreshRSS_Feed $feed */
$feeds = [];
if ($category == null) {
continue;
}
foreach ($category->feeds() as $feed) {
$feeds[] = $feed->id();
}
}
$feed_ids = array_unique($feeds);
}
}
@@ -511,30 +535,30 @@ class FeverAPI
/**
* TODO replace by a dynamic fetch for id <= $before timestamp
*/
protected function convertBeforeToId(string $beforeTimestamp): string {
return $beforeTimestamp == '0' ? '0' : $beforeTimestamp . '000000';
protected function convertBeforeToId(int $beforeTimestamp): string {
return $beforeTimestamp == 0 ? '0' : $beforeTimestamp . '000000';
}
/**
* @return integer|false
*/
protected function setFeedAsRead(string $id, string $before) {
protected function setFeedAsRead(int $id, int $before) {
$before = $this->convertBeforeToId($before);
return $this->entryDAO->markReadFeed(intval($id), $before);
return $this->entryDAO->markReadFeed($id, $before);
}
/**
* @return integer|false
*/
protected function setGroupAsRead(string $id, string $before) {
protected function setGroupAsRead(int $id, int $before) {
$before = $this->convertBeforeToId($before);
// special case to mark all items as read
if ($id == '0') {
if ($id == 0) {
return $this->entryDAO->markReadEntries($before);
}
return $this->entryDAO->markReadCat(intval($id), $before);
return $this->entryDAO->markReadCat($id, $before);
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,13 @@ const MAX_PAYLOAD = 3145728;
header('Content-Type: text/plain; charset=UTF-8');
header('X-Content-Type-Options: nosniff');
$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, MAX_PAYLOAD);
$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, MAX_PAYLOAD) ?: '';
FreshRSS_Context::initSystem();
if (FreshRSS_Context::$system_conf == null) {
header('HTTP/1.1 500 Internal Server Error');
die('Invalid system init!');
}
FreshRSS_Context::$system_conf->auth_type = 'none'; // avoid necessity to be logged in (not saved!)
//Minz_Log::debug(print_r(array('_SERVER' => $_SERVER, '_GET' => $_GET, '_POST' => $_POST, 'INPUT' => $ORIGINAL_INPUT), true), PSHB_LOG);
@@ -41,7 +45,7 @@ if ($hubFile === false) {
die('Feed info not found!');
}
$hubJson = json_decode($hubFile, true);
if (!$hubJson || empty($hubJson['key']) || $hubJson['key'] !== $key) {
if (!is_array($hubJson) || empty($hubJson['key']) || $hubJson['key'] !== $key) {
header('HTTP/1.1 500 Internal Server Error');
Minz_Log::error('Error: Invalid key cross-check!: ' . $key, PSHB_LOG);
die('Invalid key cross-check!');
@@ -120,15 +124,12 @@ foreach ($users as $userFilename) {
try {
FreshRSS_Context::initUser($username);
if (FreshRSS_Context::$user_conf != null) {
Minz_ExtensionManager::enableByList(FreshRSS_Context::$user_conf->extensions_enabled);
Minz_Translate::reset(FreshRSS_Context::$user_conf->language);
}
if (!FreshRSS_Context::$user_conf->enabled) {
if (FreshRSS_Context::$user_conf == null || !FreshRSS_Context::$user_conf->enabled) {
Minz_Log::warning('FreshRSS skip disabled user ' . $username);
continue;
}
Minz_ExtensionManager::enableByList(FreshRSS_Context::$user_conf->extensions_enabled);
Minz_Translate::reset(FreshRSS_Context::$user_conf->language);
list($updated_feeds, $feed, $nb_new_articles) = FreshRSS_feed_Controller::actualizeFeed(0, $self, false, $simplePie);
if ($updated_feeds > 0 || $feed != false) {

View File

@@ -13,10 +13,7 @@ const SUPPORTED_TYPES = [
'svg' => 'image/svg+xml',
];
/**
* @return string
*/
function get_absolute_filename(string $file_name) {
function get_absolute_filename(string $file_name): string {
$core_extension = realpath(CORE_EXTENSIONS_PATH . '/' . $file_name);
if (false !== $core_extension) {
return $core_extension;
@@ -40,9 +37,12 @@ function get_absolute_filename(string $file_name) {
return '';
}
function is_valid_path_extension($path, $extensionPath, $isStatic = true) {
function is_valid_path_extension(string $path, string $extensionPath, bool $isStatic = true): bool {
// It must be under the extension path.
$real_ext_path = realpath($extensionPath);
if ($real_ext_path == false) {
return false;
}
//Windows compatibility
$real_ext_path = str_replace('\\', '/', $real_ext_path);
@@ -60,7 +60,7 @@ function is_valid_path_extension($path, $extensionPath, $isStatic = true) {
// Static files to serve must be under a `ext_dir/static/` directory.
$path_relative_to_ext = substr($path, strlen($real_ext_path) + 1);
list(,$static,$file) = sscanf($path_relative_to_ext, '%[^/]/%[^/]/%s');
list(, $static, $file) = sscanf($path_relative_to_ext, '%[^/]/%[^/]/%s') ?? [null, null, null];
if (null === $file || 'static' !== $static) {
return false;
}
@@ -78,16 +78,18 @@ function is_valid_path_extension($path, $extensionPath, $isStatic = true) {
* @return bool true if it can be served, false otherwise.
*
*/
function is_valid_path($path) {
function is_valid_path(string $path): bool {
return is_valid_path_extension($path, CORE_EXTENSIONS_PATH) || is_valid_path_extension($path, THIRDPARTY_EXTENSIONS_PATH)
|| is_valid_path_extension($path, USERS_PATH, false);
}
/** @return never */
function sendBadRequestResponse(string $message = null) {
header('HTTP/1.1 400 Bad Request');
die($message);
}
/** @return never */
function sendNotFoundResponse() {
header('HTTP/1.1 404 Not Found');
die();

View File

@@ -4,7 +4,7 @@ require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
require(LIB_PATH . '/favicons.php');
require(LIB_PATH . '/http-conditional.php');
function show_default_favicon($cacheSeconds = 3600) {
function show_default_favicon(int $cacheSeconds = 3600): void {
$default_mtime = @filemtime(DEFAULT_FAVICON);
if (!httpConditional($default_mtime, $cacheSeconds, 2)) {
header('Content-Type: image/x-icon');

View File

@@ -35,8 +35,8 @@ if (!file_exists($applied_migrations_path)) {
require(LIB_PATH . '/http-conditional.php');
$currentUser = Minz_Session::param('currentUser', '');
$dateLastModification = $currentUser === '' ? time() : max(
@filemtime(join_path(USERS_PATH, $currentUser, LOG_FILENAME)),
@filemtime(join_path(DATA_PATH, 'config.php'))
@filemtime(USERS_PATH . '/' . $currentUser . '/' . LOG_FILENAME),
@filemtime(DATA_PATH . '/config.php')
);
if (httpConditional($dateLastModification, 0, 0, false, PHP_COMPRESSION, true)) {
Minz_Session::init('FreshRSS');