PHPStan 2.0 (#7131)

* PHPStan 2.0
fix https://github.com/FreshRSS/FreshRSS/issues/6989
https://github.com/phpstan/phpstan/releases/tag/2.0.0
https://github.com/phpstan/phpstan/blob/2.0.x/UPGRADING.md

* More

* More

* Done

* fix i18n CLI

* Restore a PHPStan Next test
For work towards PHPStan Level 10

* 4 more on Level 10

* fix getTagsForEntry

* API at Level 10

* More Level 10

* Finish Minz at Level 10

* Finish CLI at Level 10

* Finish Controllers at Level 10

* More Level 10

* More

* Pass bleedingEdge

* Clean PHPStan options and add TODOs

* Level 10 for main config

* More

* Consitency array vs. list

* Sanitize themes get_infos

* Simplify TagDAO->getTagsForEntries()

* Finish reportAnyTypeWideningInVarTag

* Prepare checkBenevolentUnionTypes and checkImplicitMixed

* Fixes

* Refix

* Another fix

* Casing of __METHOD__ constant
This commit is contained in:
Alexandre Alapetite
2024-12-27 12:12:49 +01:00
committed by GitHub
parent 897e4a3f4a
commit b1d24fbdb7
102 changed files with 1178 additions and 1014 deletions

View File

@@ -49,6 +49,9 @@ jobs:
- name: PHPStan
run: composer run-script phpstan
- name: PHPStan Next
run: composer run-script phpstan-next
# NPM tests
- name: Uses Node.js

View File

@@ -197,7 +197,6 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
}
// Remove related queries.
/** @var array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $queries */
$queries = remove_query_by_get('c_' . $id, FreshRSS_Context::userConf()->queries);
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();
@@ -239,7 +238,6 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
// Remove related queries
foreach ($feeds as $feed) {
/** @var array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> */
$queries = remove_query_by_get('f_' . $feed->id(), FreshRSS_Context::userConf()->queries);
FreshRSS_Context::userConf()->queries = $queries;
}

View File

@@ -176,10 +176,17 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
FreshRSS_View::appendScript(Minz_Url::display('/scripts/draggable.js?' . @filemtime(PUBLIC_PATH . '/scripts/draggable.js')));
if (Minz_Request::isPost()) {
$params = $_POST;
FreshRSS_Context::userConf()->sharing = $params['share'];
FreshRSS_Context::userConf()->save();
invalidateHttpCache();
$share = $_POST['share'] ?? null;
if (is_array($share)) {
$share = array_filter($share, fn($value, $key): bool =>
is_string($key) && is_array($value) &&
is_array_values_string($value),
ARRAY_FILTER_USE_BOTH);
/** @var array<string,array<string,string>> $share */
FreshRSS_Context::userConf()->sharing = $share;
FreshRSS_Context::userConf()->save();
invalidateHttpCache();
}
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'integration' ]);
}
@@ -308,7 +315,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
FreshRSS_View::appendScript(Minz_Url::display('/scripts/draggable.js?' . @filemtime(PUBLIC_PATH . '/scripts/draggable.js')));
if (Minz_Request::isPost()) {
/** @var array<int,array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}> $params */
/** @var array<int,array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string}> $params */
$params = Minz_Request::paramArray('queries');
$queries = [];
@@ -390,7 +397,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
$queryParams['search'] = htmlspecialchars_decode($params['search'], ENT_QUOTES);
}
if (!empty($params['state']) && is_array($params['state'])) {
$queryParams['state'] = (int)array_sum($params['state']);
$queryParams['state'] = (int)array_sum(array_map('intval', $params['state']));
}
if (empty($params['token']) || !is_string($params['token'])) {
$queryParams['token'] = FreshRSS_UserQuery::generateToken($name);
@@ -453,9 +460,10 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
foreach (FreshRSS_Context::userConf()->queries as $key => $query) {
$queries[$key] = (new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
}
$params = $_GET;
$params = array_filter($_GET, 'is_string', ARRAY_FILTER_USE_KEY);
unset($params['name']);
unset($params['rid']);
/** @var array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string,shareRss?:bool,shareOpml?:bool,description?:string,imageUrl?:string} $params */
$params['url'] = Minz_Url::display(['params' => $params]);
$params['name'] = _t('conf.query.number', count($queries) + 1);
$queries[] = (new FreshRSS_UserQuery($params, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();

View File

@@ -162,7 +162,7 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
}
}
} else {
/** @var array<numeric-string> $idArray */
/** @var list<numeric-string> $idArray */
$idArray = Minz_Request::paramArrayString('id');
$idString = Minz_Request::paramString('id');
if (count($idArray) > 0) {
@@ -177,7 +177,7 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
$tagsForEntries = $tagDAO->getTagsForEntries($ids) ?: [];
$tags = [];
foreach ($tagsForEntries as $line) {
$tags['t_' . $line['id_tag']][] = $line['id_entry'];
$tags['t_' . $line['id_tag']][] = (string)$line['id_entry'];
}
$this->view->tagsForEntries = $tags;
}

View File

@@ -39,8 +39,8 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
}
/**
* fetch extension list from GitHub
* @return array<array{'name':string,'author':string,'description':string,'version':string,'entrypoint':string,'type':'system'|'user','url':string,'method':string,'directory':string}>
* Fetch extension list from GitHub
* @return list<array{name:string,author:string,description:string,version:string,entrypoint:string,type:'system'|'user',url:string,method:string,directory:string}>
*/
protected function getAvailableExtensionList(): array {
$extensionListUrl = 'https://raw.githubusercontent.com/FreshRSS/Extensions/master/extensions.json';
@@ -76,17 +76,24 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
// the current implementation for now, unless it becomes too much effort maintain the extension list manually
$extensions = [];
foreach ($list['extensions'] as $extension) {
if (!is_array($extension)) {
continue;
}
if (isset($extension['version']) && is_numeric($extension['version'])) {
$extension['version'] = (string)$extension['version'];
}
foreach (['author', 'description', 'directory', 'entrypoint', 'method', 'name', 'type', 'url', 'version'] as $key) {
if (empty($extension[$key]) || !is_string($extension[$key])) {
$keys = ['author', 'description', 'directory', 'entrypoint', 'method', 'name', 'type', 'url', 'version'];
$extension = array_intersect_key($extension, array_flip($keys)); // Keep only valid keys
$extension = array_filter($extension, 'is_string');
foreach ($keys as $key) {
if (empty($extension[$key])) {
continue 2;
}
}
if (!in_array($extension['type'], ['system', 'user'], true)) {
continue;
}
/** @var array{name:string,author:string,description:string,version:string,entrypoint:string,type:'system'|'user',url:string,method:string,directory:string} $extension */
$extensions[] = $extension;
}
return $extensions;

View File

@@ -799,7 +799,6 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
}
$entryDAO = FreshRSS_Factory::createEntryDao();
/** @var array<array{id_tag:int,id_entry:string}> $applyLabels */
$applyLabels = [];
foreach (FreshRSS_Entry::fromTraversable($entryDAO->selectAll($nbNewEntries)) as $entry) {
foreach ($labels as $label) {
@@ -1003,7 +1002,6 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
// TODO: Delete old favicon
// Remove related queries
/** @var array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $queries */
$queries = remove_query_by_get('f_' . $feed_id, FreshRSS_Context::userConf()->queries);
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();

View File

@@ -169,12 +169,13 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
Minz_Request::forward(['c' => 'importExport', 'a' => 'index'], true);
}
$file = $_FILES['file'];
$status_file = $file['error'];
$file = $_FILES['file'] ?? null;
$status_file = is_array($file) ? $file['error'] ?? -1 : -1;
if ($status_file !== 0) {
Minz_Log::warning('File cannot be uploaded. Error code: ' . $status_file);
if (!is_array($file) || $status_file !== 0 || !is_string($file['name'] ?? null) || !is_string($file['tmp_name'] ?? null)) {
Minz_Log::warning('File cannot be uploaded. Error code: ' . (is_numeric($status_file) ? $status_file : -1));
Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'), [ 'c' => 'importExport', 'a' => 'index' ]);
return;
}
if (function_exists('set_time_limit')) {
@@ -232,33 +233,36 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
private function ttrssXmlToJson(string $xml): string|false {
$table = (array)simplexml_load_string($xml, options: LIBXML_NOBLANKS | LIBXML_NOCDATA);
$table['items'] = $table['article'] ?? [];
if (!is_array($table['items'])) {
$table['items'] = [];
}
unset($table['article']);
for ($i = count($table['items']) - 1; $i >= 0; $i--) {
$item = (array)($table['items'][$i]);
$item = array_filter($item, static fn($v) =>
// Filter out empty properties, potentially reported as empty objects
(is_string($v) && trim($v) !== '') || !empty($v));
$item['updated'] = isset($item['updated']) ? strtotime($item['updated']) : '';
$item['updated'] = is_string($item['updated'] ?? null) ? strtotime($item['updated']) : '';
$item['published'] = $item['updated'];
$item['content'] = ['content' => $item['content'] ?? ''];
$item['categories'] = isset($item['tag_cache']) ? [$item['tag_cache']] : [];
$item['categories'] = is_string($item['tag_cache'] ?? null) ? [$item['tag_cache']] : [];
if (!empty($item['marked'])) {
$item['categories'][] = 'user/-/state/com.google/starred';
}
if (!empty($item['published'])) {
$item['categories'][] = 'user/-/state/com.google/broadcast';
}
if (!empty($item['label_cache'])) {
if (is_string($item['label_cache'] ?? null)) {
$labels_cache = json_decode($item['label_cache'], true);
if (is_array($labels_cache)) {
foreach ($labels_cache as $label_cache) {
if (!empty($label_cache[1]) && is_string($label_cache[1])) {
if (is_array($label_cache) && !empty($label_cache[1]) && is_string($label_cache[1])) {
$item['categories'][] = 'user/-/label/' . trim($label_cache[1]);
}
}
}
}
$item['alternate'][0]['href'] = $item['link'] ?? '';
$item['alternate'] = [['href' => $item['link'] ?? '']];
$item['origin'] = [
'title' => $item['feed_title'] ?? '',
'feedUrl' => $item['feed_url'] ?? '',
@@ -290,6 +294,9 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
return false;
}
$items = $article_object['items'] ?? $article_object;
if (!is_array($items)) {
$items = [];
}
$mark_as_read = FreshRSS_Context::userConf()->mark_when['reception'] ? 1 : 0;
@@ -302,29 +309,32 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
// First, we check feeds of articles are in DB (and add them if needed).
foreach ($items as &$item) {
if (!isset($item['guid']) && isset($item['id'])) {
$item['guid'] = $item['id'];
}
if (empty($item['guid'])) {
if (!is_array($item)) {
continue;
}
if (empty($item['origin'])) {
if (!is_string($item['guid'] ?? null) && is_string($item['id'] ?? null)) {
$item['guid'] = $item['id'];
}
if (!is_string($item['guid'] ?? null)) {
continue;
}
if (!is_array($item['origin'] ?? null)) {
$item['origin'] = [];
}
if (empty($item['origin']['title']) || trim($item['origin']['title']) === '') {
if (!is_string($item['origin']['title'] ?? null) || trim($item['origin']['title']) === '') {
$item['origin']['title'] = 'Import';
}
if (!empty($item['origin']['feedUrl'])) {
if (is_string($item['origin']['feedUrl'] ?? null)) {
$feedUrl = $item['origin']['feedUrl'];
} elseif (!empty($item['origin']['streamId']) && str_starts_with($item['origin']['streamId'], 'feed/')) {
} elseif (is_string($item['origin']['streamId'] ?? null) && str_starts_with($item['origin']['streamId'], 'feed/')) {
$feedUrl = substr($item['origin']['streamId'], 5); //Google Reader
$item['origin']['feedUrl'] = $feedUrl;
} elseif (!empty($item['origin']['htmlUrl'])) {
} elseif (is_string($item['origin']['htmlUrl'] ?? null)) {
$feedUrl = $item['origin']['htmlUrl'];
} else {
$feedUrl = 'http://import.localhost/import.xml';
$item['origin']['feedUrl'] = $feedUrl;
$item['origin']['disable'] = true;
$item['origin']['disable'] = 'true';
}
$feed = new FreshRSS_Feed($feedUrl);
$feed = $this->feedDAO->searchByUrl($feed->url());
@@ -335,7 +345,8 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
// Oops, no more place!
Minz_Log::warning(_t('feedback.sub.feed.over_max', $limits['max_feeds']));
} else {
$feed = $this->addFeedJson($item['origin']);
$origin = array_filter($item['origin'], fn($value, $key): bool => is_string($key) && is_string($value), ARRAY_FILTER_USE_BOTH);
$feed = $this->addFeedJson($origin);
}
if ($feed === null) {
@@ -375,19 +386,24 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
$newGuids = [];
$this->entryDAO->beginTransaction();
foreach ($items as &$item) {
if (empty($item['guid']) || empty($article_to_feed[$item['guid']])) {
if (!is_array($item) || empty($item['guid']) || !is_string($item['guid']) || empty($article_to_feed[$item['guid']])) {
// Related feed does not exist for this entry, do nothing.
continue;
}
$feed_id = $article_to_feed[$item['guid']];
$author = $item['author'] ?? '';
$author = is_string($item['author'] ?? null) ? $item['author'] : '';
$is_starred = null; // null is used to preserve the current state if that item exists and is already starred
$is_read = null;
$tags = empty($item['categories']) ? [] : $item['categories'];
$tags = is_array($item['categories'] ?? null) ? $item['categories'] : [];
$labels = [];
for ($i = count($tags) - 1; $i >= 0; $i--) {
$tag = trim($tags[$i]);
$tag = $tags[$i];
if (!is_string($tag)) {
unset($tags[$i]);
continue;
}
$tag = trim($tag);
if (preg_match('%^user/[A-Za-z0-9_-]+/%', $tag)) {
if (preg_match('%^user/[A-Za-z0-9_-]+/state/com.google/starred$%', $tag)) {
$is_starred = true;
@@ -401,6 +417,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
unset($tags[$i]);
}
}
$tags = array_values(array_filter($tags, 'is_string'));
if ($starred && !$is_starred) {
//If the article has no label, mark it as starred (old format)
$is_starred = empty($labels);
@@ -409,41 +426,38 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
$is_read = $mark_as_read;
}
if (isset($item['alternate'][0]['href'])) {
if (is_array($item['alternate']) && is_array($item['alternate'][0] ?? null) && is_string($item['alternate'][0]['href'] ?? null)) {
$url = $item['alternate'][0]['href'];
} elseif (isset($item['url'])) {
} elseif (is_string($item['url'] ?? null)) {
$url = $item['url']; //FeedBin
} else {
$url = '';
}
if (!is_string($url)) {
$url = '';
}
$title = empty($item['title']) ? $url : $item['title'];
$title = is_string($item['title'] ?? null) ? $item['title'] : $url;
if (isset($item['content']['content']) && is_string($item['content']['content'])) {
if (is_array($item['content'] ?? null) && is_string($item['content']['content'] ?? null)) {
$content = $item['content']['content'];
} elseif (isset($item['summary']['content']) && is_string($item['summary']['content'])) {
} elseif (is_array($item['summary']) && is_string($item['summary']['content'] ?? null)) {
$content = $item['summary']['content'];
} elseif (isset($item['content']) && is_string($item['content'])) {
} elseif (is_string($item['content'] ?? null)) {
$content = $item['content']; //FeedBin
} else {
$content = '';
}
$content = sanitizeHTML($content, $url);
if (!empty($item['published'])) {
$published = '' . $item['published'];
} elseif (!empty($item['timestampUsec'])) {
$published = substr('' . $item['timestampUsec'], 0, -6);
} elseif (!empty($item['updated'])) {
$published = '' . $item['updated'];
if (is_int($item['published'] ?? null) || is_string($item['published'] ?? null)) {
$published = (string)$item['published'];
} elseif (is_int($item['timestampUsec'] ?? null) || is_string($item['timestampUsec'] ?? null)) {
$published = substr((string)$item['timestampUsec'], 0, -6);
} elseif (is_int($item['updated'] ?? null) || is_string($item['updated'] ?? null)) {
$published = (string)$item['updated'];
} else {
$published = '0';
}
if (!ctype_digit($published)) {
$published = '' . strtotime($published);
$published = (string)strtotime($published);
}
if (strlen($published) > 10) { // Milliseconds, e.g. Feedly
$published = substr($published, 0, -3);

View File

@@ -170,8 +170,10 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
$this->view->html_url = Minz_Url::display('', 'html', true);
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
$queryString = $_SERVER['QUERY_STRING'] ?? '';
$this->view->rss_url = htmlspecialchars(
PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']), ENT_COMPAT, 'UTF-8');
PUBLIC_TO_INDEX_PATH . '/' . ($queryString === '' || !is_string($queryString) ? '' : '?' . $queryString), ENT_COMPAT, 'UTF-8');
// No layout for RSS output.
$this->view->_layout(null);
@@ -216,7 +218,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
Minz_Error::error(404);
return;
}
$this->view->categories = [ $cat->id() => $cat ];
$this->view->categories = [ $cat ];
break;
case 'f':
// We most likely already have the feed object in cache
@@ -229,7 +231,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
return;
}
}
$this->view->feeds = [ $feed->id() => $feed ];
$this->view->feeds = [ $feed ];
break;
case 's':
case 't':

View File

@@ -5,6 +5,7 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
/**
* @var FreshRSS_ViewJavascript
* @phpstan-ignore property.phpDocType
*/
protected $view;
@@ -53,6 +54,10 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
header('Pragma: no-cache');
$user = $_GET['user'] ?? '';
if (!is_string($user) || $user === '') {
Minz_Error::error(400);
return;
}
FreshRSS_Context::initUser($user);
if (FreshRSS_Context::hasUserConf()) {
try {

View File

@@ -8,6 +8,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
/**
* @var FreshRSS_ViewStats
* @phpstan-ignore property.phpDocType
*/
protected $view;

View File

@@ -287,8 +287,8 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
Minz_Log::notice(_t('feedback.update.finished'));
Minz_Request::good(_t('feedback.update.finished'));
} else {
Minz_Log::error(_t('feedback.update.error', $res));
Minz_Request::bad(_t('feedback.update.error', $res), [ 'c' => 'update', 'a' => 'index' ]);
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;
@@ -321,8 +321,8 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
'params' => ['post_conf' => '1'],
], true);
} else {
Minz_Log::error(_t('feedback.update.error', $res));
Minz_Request::bad(_t('feedback.update.error', $res), [ 'c' => 'update', 'a' => 'index' ]);
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' ]);
}
}
}

View File

@@ -8,6 +8,7 @@ class FreshRSS_User_Mailer extends Minz_Mailer {
/**
* @var FreshRSS_View
* @phpstan-ignore property.phpDocType
*/
protected $view;

View File

@@ -5,6 +5,7 @@ abstract class FreshRSS_ActionController extends Minz_ActionController {
/**
* @var FreshRSS_View
* @phpstan-ignore property.phpDocType
*/
protected $view;

View File

@@ -53,6 +53,7 @@ trait FreshRSS_AttributesTrait {
$values = json_decode($values, true);
}
if (is_array($values)) {
$values = array_filter($values, 'is_string', ARRAY_FILTER_USE_KEY);
$this->attributes = $values;
}
}

View File

@@ -75,8 +75,8 @@ class FreshRSS_Auth {
if (!$login_ok && FreshRSS_Context::systemConf()->http_auth_auto_register) {
$email = null;
if (FreshRSS_Context::systemConf()->http_auth_auto_register_email_field !== '' &&
isset($_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field])) {
$email = (string)$_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field];
is_string($_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field] ?? null)) {
$email = $_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field];
}
$language = Minz_Translate::getLanguage(null, Minz_Request::getPreferredLanguages(), FreshRSS_Context::systemConf()->language);
Minz_Translate::init($language);

View File

@@ -7,7 +7,7 @@ declare(strict_types=1);
class FreshRSS_BooleanSearch implements \Stringable {
private string $raw_input = '';
/** @var array<FreshRSS_BooleanSearch|FreshRSS_Search> */
/** @var list<FreshRSS_BooleanSearch|FreshRSS_Search> */
private array $searches = [];
/**
@@ -400,7 +400,7 @@ class FreshRSS_BooleanSearch implements \Stringable {
/**
* Either a list of FreshRSS_BooleanSearch combined by implicit AND
* or a series of FreshRSS_Search combined by explicit OR
* @return array<FreshRSS_BooleanSearch|FreshRSS_Search>
* @return list<FreshRSS_BooleanSearch|FreshRSS_Search>
*/
public function searches(): array {
return $this->searches;

View File

@@ -19,7 +19,7 @@ class FreshRSS_Category extends Minz_Model {
private string $name;
private int $nbFeeds = -1;
private int $nbNotRead = -1;
/** @var array<FreshRSS_Feed>|null */
/** @var list<FreshRSS_Feed>|null */
private ?array $feeds = null;
/** @var bool|int */
private $hasFeedsWithError = false;
@@ -100,7 +100,7 @@ class FreshRSS_Category extends Minz_Model {
}
/**
* @return array<int,FreshRSS_Feed>
* @return list<FreshRSS_Feed>
* @throws Minz_ConfigurationNamespaceException
* @throws Minz_PDOConnectionException
*/
@@ -142,11 +142,11 @@ class FreshRSS_Category extends Minz_Model {
}
/** @param array<FreshRSS_Feed>|FreshRSS_Feed $values */
public function _feeds($values): void {
public function _feeds(array|FreshRSS_Feed $values): void {
if (!is_array($values)) {
$values = [$values];
}
$this->feeds = $values;
$this->feeds = array_values($values);
$this->sortFeeds();
}
@@ -243,7 +243,7 @@ class FreshRSS_Category extends Minz_Model {
if ($this->feeds === null) {
return;
}
uasort($this->feeds, static fn(FreshRSS_Feed $a, FreshRSS_Feed $b) => strnatcasecmp($a->name(), $b->name()));
usort($this->feeds, static fn(FreshRSS_Feed $a, FreshRSS_Feed $b) => strnatcasecmp($a->name(), $b->name()));
}
/**
@@ -265,13 +265,13 @@ class FreshRSS_Category extends Minz_Model {
/**
* Access cached feeds
* @param array<FreshRSS_Category> $categories
* @return array<int,FreshRSS_Feed>
* @return list<FreshRSS_Feed>
*/
public static function findFeeds(array $categories): array {
$result = [];
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
$result[$feed->id()] = $feed;
$result[] = $feed;
}
}
return $result;

View File

@@ -19,7 +19,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
if ($this->pdo->inTransaction()) {
$this->pdo->commit();
}
Minz_Log::warning(__method__ . ': ' . $name);
Minz_Log::warning(__METHOD__ . ': ' . $name);
try {
if ($name === 'kind') { //v1.20.0
return $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN kind SMALLINT DEFAULT 0') !== false;
@@ -30,8 +30,8 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
} elseif ('attributes' === $name) { //v1.15.0
$ok = $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN attributes TEXT') !== false;
/** @var array<array{'id':int,'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'keep_history':?int,'ttl':int,'attributes':string}> $feeds */
/** @var list<array{id:int,url:string,kind:int,category:int,name:string,website:string,lastUpdate:int,
* priority:int,pathEntries:string,httpAuth:string,error:int,keep_history:?int,ttl:int,attributes:string}> $feeds */
$feeds = $this->fetchAssoc('SELECT * FROM `_feed`') ?? [];
$stm = $this->pdo->prepare('UPDATE `_feed` SET attributes = :attributes WHERE id = :id');
@@ -51,15 +51,17 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
if (!is_array($attributes)) {
$attributes = [];
}
$archiving = is_array($attributes['archiving'] ?? null) ? $attributes['archiving'] : [];
if ($keepHistory > 0) {
$attributes['archiving']['keep_min'] = (int)$keepHistory;
$archiving['keep_min'] = (int)$keepHistory;
} elseif ($keepHistory == -1) { //Infinite
$attributes['archiving']['keep_period'] = false;
$attributes['archiving']['keep_max'] = false;
$attributes['archiving']['keep_min'] = false;
$archiving['keep_period'] = false;
$archiving['keep_max'] = false;
$archiving['keep_min'] = false;
} else {
continue;
}
$attributes['archiving'] = $archiving;
if (!($stm->bindValue(':id', $feed['id'], PDO::PARAM_INT) &&
$stm->bindValue(':attributes', json_encode($attributes, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)) &&
$stm->execute())) {
@@ -78,12 +80,12 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
return $ok;
}
} catch (Exception $e) {
Minz_Log::error(__method__ . ': ' . $e->getMessage());
Minz_Log::error(__METHOD__ . ': ' . $e->getMessage());
}
return false;
}
/** @param array<string|int> $errorInfo */
/** @param array{0:string,1:int,2:string} $errorInfo */
protected function autoUpdateDb(array $errorInfo): bool {
if (isset($errorInfo[0])) {
if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
@@ -99,7 +101,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
}
/**
* @param array{'name':string,'id'?:int,'kind'?:int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string|array<string,mixed>} $valuesTmp
* @param array{name:string,id?:int,kind?:int,lastUpdate?:int,error?:int|bool,attributes?:string|array<string,mixed>} $valuesTmp
*/
public function addCategory(array $valuesTmp): int|false {
// TRIM() to provide a type hint as text
@@ -127,6 +129,7 @@ SQL;
return $catId === false ? false : (int)$catId;
} else {
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
return $this->addCategory($valuesTmp);
}
@@ -150,7 +153,7 @@ SQL;
}
/**
* @param array{'name':string,'kind':int,'attributes'?:array<string,mixed>|mixed|null} $valuesTmp
* @param array{name:string,kind:int,attributes?:array<string,mixed>|mixed|null} $valuesTmp
*/
public function updateCategory(int $id, array $valuesTmp): int|false {
// No tag of the same name
@@ -176,6 +179,7 @@ SQL;
return $stm->rowCount();
} else {
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
return $this->updateCategory($id, $valuesTmp);
}
@@ -217,21 +221,22 @@ SQL;
}
}
/** @return Traversable<array{'id':int,'name':string,'kind':int,'lastUpdate':int,'error':int,'attributes'?:array<string,mixed>}> */
/** @return Traversable<array{id:int,name:string,kind:int,lastUpdate:int,error:int,attributes?:array<string,mixed>}> */
public function selectAll(): Traversable {
$sql = 'SELECT id, name, kind, `lastUpdate`, error, attributes FROM `_category`';
$stm = $this->pdo->query($sql);
if ($stm !== false) {
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
/** @var array{'id':int,'name':string,'kind':int,'lastUpdate':int,'error':int,'attributes'?:array<string,mixed>} $row */
/** @var array{id:int,name:string,kind:int,lastUpdate:int,error:int,attributes?:array<string,mixed>} $row */
yield $row;
}
} else {
$info = $this->pdo->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
yield from $this->selectAll();
} else {
Minz_Log::error(__method__ . ' error: ' . json_encode($info));
Minz_Log::error(__METHOD__ . ' error: ' . json_encode($info));
}
}
}
@@ -239,24 +244,24 @@ SQL;
public function searchById(int $id): ?FreshRSS_Category {
$sql = 'SELECT * FROM `_category` WHERE id=:id';
$res = $this->fetchAssoc($sql, ['id' => $id]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate':int,'error':int|bool,'attributes':string}> $res */
$categories = self::daoToCategories($res);
/** @var array<array{name:string,id:int,kind:int,lastUpdate?:int,error:int|bool,attributes?:string}> $res */
$categories = self::daoToCategories($res); // @phpstan-ignore varTag.type
return reset($categories) ?: null;
}
public function searchByName(string $name): ?FreshRSS_Category {
$sql = 'SELECT * FROM `_category` WHERE name=:name';
$res = $this->fetchAssoc($sql, ['name' => $name]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate':int,'error':int|bool,'attributes':string}> $res */
$categories = self::daoToCategories($res);
/** @var array<array{name:string,id:int,kind:int,lastUpdate:int,error:int|bool,attributes:string}> $res */
$categories = self::daoToCategories($res); // @phpstan-ignore varTag.type
return reset($categories) ?: null;
}
/** @return array<int,FreshRSS_Category> */
/** @return list<FreshRSS_Category> */
public function listSortedCategories(bool $prePopulateFeeds = true, bool $details = false): array {
$categories = $this->listCategories($prePopulateFeeds, $details);
uasort($categories, static function (FreshRSS_Category $a, FreshRSS_Category $b) {
usort($categories, static function (FreshRSS_Category $a, FreshRSS_Category $b) {
$aPosition = $a->attributeInt('position');
$bPosition = $b->attributeInt('position');
if ($aPosition === $bPosition) {
@@ -272,7 +277,7 @@ SQL;
return $categories;
}
/** @return array<int,FreshRSS_Category> */
/** @return list<FreshRSS_Category> */
public function listCategories(bool $prePopulateFeeds = true, bool $details = false): array {
if ($prePopulateFeeds) {
$sql = 'SELECT c.id AS c_id, c.name AS c_name, c.kind AS c_kind, c.`lastUpdate` AS c_last_update, c.error AS c_error, c.attributes AS c_attributes, '
@@ -286,11 +291,12 @@ SQL;
$values = [ ':priority' => FreshRSS_Feed::PRIORITY_CATEGORY ];
if ($stm !== false && $stm->execute($values)) {
$res = $stm->fetchAll(PDO::FETCH_ASSOC) ?: [];
/** @var array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
* 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'category'?:int,'website'?:string,'priority'?:int,'error'?:int|bool,'attributes'?:string,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $res */
/** @var list<array{c_name:string,c_id:int,c_kind:int,c_last_update:int,c_error:int|bool,c_attributes?:string,
* id?:int,name?:string,url?:string,kind?:int,category?:int,website?:string,priority?:int,error?:int|bool,attributes?:string,cache_nbEntries?:int,cache_nbUnreads?:int,ttl?:int}> $res */
return self::daoToCategoriesPrepopulated($res);
} else {
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
return $this->listCategories($prePopulateFeeds, $details);
}
@@ -298,13 +304,13 @@ SQL;
return [];
}
} else {
$res = $this->fetchAssoc('SELECT * FROM `_category` ORDER BY name');
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
return empty($res) ? [] : self::daoToCategories($res);
$res = $this->fetchAssoc('SELECT * FROM `_category` ORDER BY name') ?? [];
/** @var list<array{name:string,id:int,kind:int,lastUpdate?:int,error?:int|bool,attributes?:string}> $res */
return empty($res) ? [] : self::daoToCategories($res); // @phpstan-ignore varTag.type
}
}
/** @return array<int,FreshRSS_Category> */
/** @return list<FreshRSS_Category> */
public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0): array {
$sql = 'SELECT * FROM `_category` WHERE kind = :kind AND `lastUpdate` < :lu ORDER BY `lastUpdate`'
. ($limit < 1 ? '' : ' LIMIT ' . $limit);
@@ -313,9 +319,12 @@ SQL;
$stm->bindValue(':kind', FreshRSS_Category::KIND_DYNAMIC_OPML, PDO::PARAM_INT) &&
$stm->bindValue(':lu', time() - $defaultCacheDuration, PDO::PARAM_INT) &&
$stm->execute()) {
return self::daoToCategories($stm->fetchAll(PDO::FETCH_ASSOC));
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
/** @var list<array{name:string,id:int,kind:int,lastUpdate:int,error?:int|bool,attributes?:string}> $res */
return self::daoToCategories($res);
} else {
$info = $stm !== false ? $stm->errorInfo() : $this->pdo->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
return $this->listCategoriesOrderUpdate($defaultCacheDuration, $limit);
}
@@ -327,10 +336,10 @@ SQL;
public function getDefault(): ?FreshRSS_Category {
$sql = 'SELECT * FROM `_category` WHERE id=:id';
$res = $this->fetchAssoc($sql, [':id' => self::DEFAULTCATEGORYID]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
$categories = self::daoToCategories($res);
if (isset($categories[self::DEFAULTCATEGORYID])) {
return $categories[self::DEFAULTCATEGORYID];
/** @var array<array{name:string,id:int,kind:int,lastUpdate?:int,error?:int|bool,attributes?:string}> $res */
$categories = self::daoToCategories($res); // @phpstan-ignore varTag.type
if (isset($categories[0])) {
return $categories[0];
} else {
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS database error: Default category not found!' . "\n");
@@ -388,7 +397,7 @@ SQL;
return isset($res[0]) ? (int)$res[0] : -1;
}
/** @return array<int,string> */
/** @return list<string> */
public function listTitles(int $id, int $limit = 0): array {
$sql = <<<'SQL'
SELECT e.title FROM `_entry` e
@@ -398,15 +407,15 @@ SQL;
SQL;
$sql .= ($limit < 1 ? '' : ' LIMIT ' . intval($limit));
$res = $this->fetchColumn($sql, 0, [':id_category' => $id]) ?? [];
/** @var array<int,string> $res */
/** @var list<string> $res */
return $res;
}
/**
* @param array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
* 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'website'?:string,'priority'?:int,
* 'error'?:int|bool,'attributes'?:string,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $listDAO
* @return array<int,FreshRSS_Category>
* @param array<array{c_name:string,c_id:int,c_kind:int,c_last_update:int,c_error:int|bool,c_attributes?:string,
* id?:int,name?:string,url?:string,kind?:int,website?:string,priority?:int,
* error?:int|bool,attributes?:string,cache_nbEntries?:int,cache_nbUnreads?:int,ttl?:int}> $listDAO
* @return list<FreshRSS_Category>
*/
private static function daoToCategoriesPrepopulated(array $listDAO): array {
$list = [];
@@ -414,8 +423,6 @@ SQL;
$feedsDao = [];
$feedDao = FreshRSS_Factory::createFeedDao();
foreach ($listDAO as $line) {
FreshRSS_DatabaseDAO::pdoInt($line, ['c_id', 'c_kind', 'c_last_update', 'c_error',
'id', 'kind', 'priority', 'error', 'cache_nbEntries', 'cache_nbUnreads', 'ttl']);
if (!empty($previousLine['c_id']) && $line['c_id'] !== $previousLine['c_id']) {
// End of the current category, we add it to the $list
$cat = new FreshRSS_Category(
@@ -425,7 +432,7 @@ SQL;
);
$cat->_kind($previousLine['c_kind']);
$cat->_attributes($previousLine['c_attributes'] ?? '[]');
$list[$cat->id()] = $cat;
$list[] = $cat;
$feedsDao = []; //Prepare for next category
}
@@ -445,20 +452,19 @@ SQL;
$cat->_lastUpdate($previousLine['c_last_update'] ?? 0);
$cat->_error($previousLine['c_error'] ?? 0);
$cat->_attributes($previousLine['c_attributes'] ?? []);
$list[$cat->id()] = $cat;
$list[] = $cat;
}
return $list;
}
/**
* @param array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $listDAO
* @return array<int,FreshRSS_Category>
* @param array<array{name:string,id:int,kind:int,lastUpdate?:int,error?:int|bool,attributes?:string}> $listDAO
* @return list<FreshRSS_Category>
*/
private static function daoToCategories(array $listDAO): array {
$list = [];
foreach ($listDAO as $dao) {
FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'kind', 'lastUpdate', 'error']);
$cat = new FreshRSS_Category(
$dao['name'],
$dao['id']
@@ -467,7 +473,7 @@ SQL;
$cat->_lastUpdate($dao['lastUpdate'] ?? 0);
$cat->_error($dao['error'] ?? 0);
$cat->_attributes($dao['attributes'] ?? '');
$list[$cat->id()] = $cat;
$list[] = $cat;
}
return $list;
}

View File

@@ -3,7 +3,7 @@ declare(strict_types=1);
class FreshRSS_CategoryDAOSQLite extends FreshRSS_CategoryDAO {
/** @param array<int|string> $errorInfo */
/** @param array{0:string,1:int,2:string} $errorInfo */
#[\Override]
protected function autoUpdateDb(array $errorInfo): bool {
if (($tableInfo = $this->pdo->query("PRAGMA table_info('category')")) !== false) {

View File

@@ -8,11 +8,11 @@ declare(strict_types=1);
final class FreshRSS_Context {
/**
* @var array<int,FreshRSS_Category>
* @var list<FreshRSS_Category>
*/
private static array $categories = [];
/**
* @var array<int,FreshRSS_Tag>
* @var list<FreshRSS_Tag>
*/
private static array $tags = [];
public static string $name = '';
@@ -176,7 +176,7 @@ final class FreshRSS_Context {
FreshRSS_Context::$user_conf = null;
}
/** @return array<int,FreshRSS_Category> */
/** @return list<FreshRSS_Category> */
public static function categories(): array {
if (empty(self::$categories)) {
$catDAO = FreshRSS_Factory::createCategoryDao();
@@ -185,12 +185,12 @@ final class FreshRSS_Context {
return self::$categories;
}
/** @return array<int,FreshRSS_Feed> */
/** @return list<FreshRSS_Feed> */
public static function feeds(): array {
return FreshRSS_Category::findFeeds(self::categories());
}
/** @return array<int,FreshRSS_Tag> */
/** @return list<FreshRSS_Tag> */
public static function labels(bool $precounts = false): array {
if (empty(self::$tags) || $precounts) {
$tagDAO = FreshRSS_Factory::createTagDao();
@@ -429,7 +429,6 @@ final class FreshRSS_Context {
self::$name = _t('index.feed.title_fav');
self::$description = FreshRSS_Context::systemConf()->meta_description;
self::$get_unread = self::$total_starred['unread'];
// Update state if favorite is not yet enabled.
self::$state = self::$state | FreshRSS_Entry::STATE_FAVORITE;
break;
@@ -437,11 +436,7 @@ final class FreshRSS_Context {
// We try to find the corresponding feed. When allowing robots, always retrieve the full feed including description
$feed = FreshRSS_Context::systemConf()->allow_robots ? null : FreshRSS_Category::findFeed(self::$categories, $id);
if ($feed === null) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$feed = $feedDAO->searchById($id);
if ($feed === null) {
throw new FreshRSS_Context_Exception('Invalid feed: ' . $id);
}
throw new FreshRSS_Context_Exception('Invalid feed: ' . $id);
}
self::$current_get['feed'] = $id;
self::$current_get['category'] = $feed->categoryId();
@@ -452,15 +447,15 @@ final class FreshRSS_Context {
case 'c':
// We try to find the corresponding category.
self::$current_get['category'] = $id;
if (!isset(self::$categories[$id])) {
$catDAO = FreshRSS_Factory::createCategoryDao();
$cat = $catDAO->searchById($id);
if ($cat === null) {
throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
$cat = null;
foreach (self::$categories as $category) {
if ($category->id() === $id) {
$cat = $category;
break;
}
self::$categories[$id] = $cat;
} else {
$cat = self::$categories[$id];
}
if ($cat === null) {
throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
}
self::$name = $cat->name();
self::$get_unread = $cat->nbNotRead();
@@ -468,15 +463,15 @@ final class FreshRSS_Context {
case 't':
// We try to find the corresponding tag.
self::$current_get['tag'] = $id;
if (!isset(self::$tags[$id])) {
$tagDAO = FreshRSS_Factory::createTagDao();
$tag = $tagDAO->searchById($id);
if ($tag === null) {
throw new FreshRSS_Context_Exception('Invalid tag: ' . $id);
$tag = null;
foreach (self::$tags as $t) {
if ($t->id() === $id) {
$tag = $t;
break;
}
self::$tags[$id] = $tag;
} else {
$tag = self::$tags[$id];
}
if ($tag === null) {
throw new FreshRSS_Context_Exception('Invalid tag: ' . $id);
}
self::$name = $tag->name();
self::$get_unread = $tag->nbUnread();

View File

@@ -25,10 +25,14 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
$db = FreshRSS_Context::systemConf()->db;
try {
$sql = sprintf($GLOBALS['SQL_CREATE_DB'], empty($db['base']) ? '' : $db['base']);
$sql = $GLOBALS['SQL_CREATE_DB'];
if (!is_string($sql)) {
throw new Exception('SQL_CREATE_DB is not a string!');
}
$sql = sprintf($sql, empty($db['base']) ? '' : $db['base']);
return $this->pdo->exec($sql) === false ? 'Error during CREATE DATABASE' : '';
} catch (Exception $e) {
syslog(LOG_DEBUG, __method__ . ' notice: ' . $e->getMessage());
syslog(LOG_DEBUG, __METHOD__ . ' notice: ' . $e->getMessage());
return $e->getMessage();
}
}
@@ -43,7 +47,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
return $res == false ? 'Error during SQL connection fetch test!' : '';
} catch (Exception $e) {
syslog(LOG_DEBUG, __method__ . ' warning: ' . $e->getMessage());
syslog(LOG_DEBUG, __METHOD__ . ' warning: ' . $e->getMessage());
return $e->getMessage();
}
}
@@ -81,7 +85,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
return count(array_keys($tables, true, true)) === count($tables);
}
/** @return array<array{name:string,type:string,notnull:bool,default:mixed}> */
/** @return list<array{name:string,type:string,notnull:bool,default:mixed}> */
public function getSchema(string $table): array {
$res = $this->fetchAssoc('DESC `_' . $table . '`');
return $res == null ? [] : $this->listDaoToSchema($res);
@@ -164,16 +168,16 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
*/
public function daoToSchema(array $dao): array {
return [
'name' => (string)($dao['Field']),
'type' => strtolower((string)($dao['Type'])),
'notnull' => (bool)$dao['Null'],
'default' => $dao['Default'],
'name' => is_string($dao['Field'] ?? null) ? $dao['Field'] : '',
'type' => is_string($dao['Type'] ?? null) ? strtolower($dao['Type']) : '',
'notnull' => empty($dao['Null']),
'default' => is_scalar($dao['Default'] ?? null) ? $dao['Default'] : null,
];
}
/**
* @param array<array<string,string|int|bool|null>> $listDAO
* @return array<array{name:string,type:string,notnull:bool,default:mixed}>
* @return list<array{name:string,type:string,notnull:bool,default:mixed}>
*/
public function listDaoToSchema(array $listDAO): array {
$list = [];
@@ -198,7 +202,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
return self::$staticVersion;
}
static $version = null;
if ($version === null) {
if (!is_string($version)) {
$version = $this->fetchValue('SELECT version()') ?? '';
}
return $version;
@@ -256,7 +260,7 @@ SQL;
$catDAO->resetDefaultCategoryName();
include_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
if (!empty($GLOBALS['SQL_UPDATE_MINOR'])) {
if (!empty($GLOBALS['SQL_UPDATE_MINOR']) && is_string($GLOBALS['SQL_UPDATE_MINOR'])) {
$sql = $GLOBALS['SQL_UPDATE_MINOR'];
$isMariaDB = false;
@@ -272,7 +276,7 @@ SQL;
if ($this->pdo->exec($sql) === false) {
$info = $this->pdo->errorInfo();
if ($this->pdo->dbType() === 'mysql' &&
!$isMariaDB && !empty($info[2]) && (stripos($info[2], "Can't DROP ") !== false)) {
!$isMariaDB && is_string($info[2] ?? null) && (stripos($info[2], "Can't DROP ") !== false)) {
// Too bad for MySQL, but ignore error
return;
}
@@ -444,7 +448,7 @@ SQL;
foreach ($tagFrom->selectEntryTag() as $entryTag) {
if (!empty($idMaps['t' . $entryTag['id_tag']])) {
$entryTag['id_tag'] = $idMaps['t' . $entryTag['id_tag']];
if (!$tagTo->tagEntry($entryTag['id_tag'], $entryTag['id_entry'])) {
if (!$tagTo->tagEntry($entryTag['id_tag'], (string)$entryTag['id_entry'])) {
$error = 'Error during SQLite copy of entry-tags!';
return self::stdError($error);
}
@@ -454,31 +458,4 @@ SQL;
return true;
}
/**
* Ensure that some PDO columns are `int` and not `string`.
* Compatibility with PHP 7.
* @param array<string|int|null> $table
* @param array<string> $columns
*/
public static function pdoInt(array &$table, array $columns): void {
foreach ($columns as $column) {
if (isset($table[$column]) && is_string($table[$column])) {
$table[$column] = (int)$table[$column];
}
}
}
/**
* Ensure that some PDO columns are `string` and not `bigint`.
* @param array<string|int|null> $table
* @param array<string> $columns
*/
public static function pdoString(array &$table, array $columns): void {
foreach ($columns as $column) {
if (isset($table[$column])) {
$table[$column] = (string)$table[$column];
}
}
}
}

View File

@@ -34,7 +34,7 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
return count(array_keys($tables, true, true)) === count($tables);
}
/** @return array<array{name:string,type:string,notnull:bool,default:mixed}> */
/** @return list<array{name:string,type:string,notnull:bool,default:mixed}> */
#[\Override]
public function getSchema(string $table): array {
$sql = <<<'SQL'
@@ -52,10 +52,10 @@ SQL;
#[\Override]
public function daoToSchema(array $dao): array {
return [
'name' => (string)($dao['field']),
'type' => strtolower((string)($dao['type'])),
'notnull' => (bool)$dao['null'],
'default' => $dao['default'],
'name' => is_string($dao['field'] ?? null) ? $dao['field'] : '',
'type' => is_string($dao['type'] ?? null) ? strtolower($dao['type']) : '',
'notnull' => empty($dao['null']),
'default' => is_scalar($dao['default'] ?? null) ? $dao['default'] : null,
];
}

View File

@@ -24,18 +24,25 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
$this->pdo->prefix() . 'entrytag' => false,
];
foreach ($res as $value) {
$tables[$value['name']] = true;
if (is_array($value) && is_string($value['name'] ?? null)) {
$tables[$value['name']] = true;
}
}
return count(array_keys($tables, true, true)) == count($tables);
}
/** @return array<array{name:string,type:string,notnull:bool,default:mixed}> */
/** @return list<array{name:string,type:string,notnull:bool,default:mixed}> */
#[\Override]
public function getSchema(string $table): array {
$sql = 'PRAGMA table_info(' . $table . ')';
$stm = $this->pdo->query($sql);
return $stm !== false ? $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC) ?: []) : [];
if ($stm !== false) {
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
/** @var list<array{name:string,type:string,notnull:bool,dflt_value:string|int|bool|null}> $res */
return $this->listDaoToSchema($res ?: []);
}
return [];
}
#[\Override]
@@ -59,10 +66,10 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
#[\Override]
public function daoToSchema(array $dao): array {
return [
'name' => (string)$dao['name'],
'type' => strtolower((string)$dao['type']),
'notnull' => $dao['notnull'] == '1' ? true : false,
'default' => $dao['dflt_value'],
'name' => is_string($dao['name'] ?? null) ? $dao['name'] : '',
'type' => is_string($dao['type'] ?? null) ? strtolower($dao['type']) : '',
'notnull' => empty($dao['notnull']),
'default' => is_scalar($dao['dflt_value'] ?? null) ? $dao['dflt_value'] : null,
];
}

View File

@@ -52,12 +52,10 @@ class FreshRSS_Entry extends Minz_Model {
$this->_guid($guid);
}
/** @param array{'id'?:string,'id_feed'?:int,'guid'?:string,'title'?:string,'author'?:string,'content'?:string,'link'?:string,'date'?:int|string,'lastSeen'?:int,
* 'hash'?:string,'is_read'?:bool|int,'is_favorite'?:bool|int,'tags'?:string|array<string>,'attributes'?:?string,'thumbnail'?:string,'timestamp'?:string} $dao */
/** @param array{id?:string,id_feed?:int,guid?:string,title?:string,author?:string,content?:string,link?:string,date?:int|string,lastSeen?:int,
* hash?:string,is_read?:bool|int,is_favorite?:bool|int,tags?:string|array<string>,attributes?:?string,thumbnail?:string,timestamp?:string} $dao */
public static function fromArray(array $dao): FreshRSS_Entry {
FreshRSS_DatabaseDAO::pdoInt($dao, ['id_feed', 'date', 'lastSeen', 'is_read', 'is_favorite']);
if (empty($dao['content'])) {
if (empty($dao['content']) || !is_string($dao['content'])) {
$dao['content'] = '';
}
@@ -83,7 +81,7 @@ class FreshRSS_Entry extends Minz_Model {
$dao['is_favorite'] ?? false,
$dao['tags'] ?? ''
);
if (!empty($dao['id'])) {
if (!empty($dao['id']) && is_numeric($dao['id'])) {
$entry->_id($dao['id']);
}
if (!empty($dao['timestamp'])) {
@@ -241,7 +239,9 @@ HTML;
$content .= '<figure class="enclosure">';
foreach ($thumbnails as $thumbnail) {
$content .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" title="' . $etitle . '" /></p>';
if (is_string($thumbnail)) {
$content .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" title="' . $etitle . '" /></p>';
}
}
if (self::enclosureIsImage($enclosure)) {
@@ -283,9 +283,9 @@ HTML;
/** @return Traversable<array{'url':string,'type'?:string,'medium'?:string,'length'?:int,'title'?:string,'description'?:string,'credit'?:string|array<string>,'height'?:int,'width'?:int,'thumbnails'?:array<string>}> */
public function enclosures(bool $searchBodyImages = false): Traversable {
$attributeEnclosures = $this->attributeArray('enclosures');
if (is_iterable($attributeEnclosures)) {
if (is_array($attributeEnclosures)) {
// FreshRSS 1.20.1+: The enclosures are saved as attributes
/** @var iterable<array{'url':string,'type'?:string,'medium'?:string,'length'?:int,'title'?:string,'description'?:string,'credit'?:string|array<string>,'height'?:int,'width'?:int,'thumbnails'?:array<string>}> $attributeEnclosures */
/** @var list<array{'url':string,'type'?:string,'medium'?:string,'length'?:int,'title'?:string,'description'?:string,'credit'?:string|array<string>,'height'?:int,'width'?:int,'thumbnails'?:array<string>}> $attributeEnclosures */
yield from $attributeEnclosures;
}
try {
@@ -354,7 +354,7 @@ HTML;
public function thumbnail(bool $searchEnclosures = true): ?array {
$thumbnail = $this->attributeArray('thumbnail') ?? [];
// First, use the provided thumbnail, if any
if (!empty($thumbnail['url'])) {
if (is_string($thumbnail['url'] ?? null)) {
/** @var array{'url':string,'height'?:int,'width'?:int,'time'?:string} $thumbnail */
return $thumbnail;
}

View File

@@ -35,7 +35,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
return [];
}
/** @param array<int|string> $values */
/** @param list<int|string> $values */
protected static function sqlRegex(string $expression, string $regex, array &$values): string {
// The implementation of this function is solely for MySQL and MariaDB
static $databaseDAOMySQL = null;
@@ -90,7 +90,7 @@ SQL;
$ok = $this->pdo->exec($sql) !== false;
} catch (Exception $e) {
$ok = false;
Minz_Log::error(__method__ . ' error: ' . $e->getMessage());
Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage());
}
return $ok;
}
@@ -99,7 +99,7 @@ SQL;
if ($this->pdo->inTransaction()) {
$this->pdo->commit();
}
Minz_Log::warning(__method__ . ': ' . $name);
Minz_Log::warning(__METHOD__ . ': ' . $name);
try {
if ($name === 'attributes') { //v1.20.0
$sql = <<<'SQL'
@@ -109,13 +109,13 @@ SQL;
return $this->pdo->exec($sql) !== false;
}
} catch (Exception $e) {
Minz_Log::error(__method__ . ' error: ' . $e->getMessage());
Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage());
}
return false;
}
//TODO: Move the database auto-updates to DatabaseDAO
/** @param array<string|int> $errorInfo */
/** @param array{0:string,1:int,2:string} $errorInfo */
protected function autoUpdateDb(array $errorInfo): bool {
if (isset($errorInfo[0])) {
if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
@@ -201,6 +201,7 @@ SQL;
return true;
} else {
$info = $this->addEntryPrepared == false ? $this->pdo->errorInfo() : $this->addEntryPrepared->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
$this->addEntryPrepared = null;
return $this->addEntry($valuesTmp);
@@ -310,6 +311,7 @@ SQL;
return true;
} else {
$info = $this->updateEntryPrepared == false ? $this->pdo->errorInfo() : $this->updateEntryPrepared->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
return $this->updateEntry($valuesTmp);
}
@@ -336,7 +338,7 @@ SQL;
* @todo simplify the query by removing the str_repeat. I am pretty sure
* there is an other way to do that.
*
* @param numeric-string|array<numeric-string> $ids
* @param numeric-string|list<numeric-string> $ids
*/
public function markFavorite($ids, bool $is_favorite = true): int|false {
if (!is_array($ids)) {
@@ -414,7 +416,7 @@ SQL;
* Toggle the read marker on one or more article.
* Then the cache is updated.
*
* @param numeric-string|array<numeric-string> $ids
* @param numeric-string|list<numeric-string> $ids
* @return int|false affected rows
*/
public function markRead(array|string $ids, bool $is_read = true): int|false {
@@ -720,16 +722,17 @@ SQL;
return $stm->rowCount();
} else {
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
return $this->cleanOldEntries($id_feed, $options);
}
Minz_Log::error(__method__ . ' error:' . json_encode($info));
Minz_Log::error(__METHOD__ . ' error:' . json_encode($info));
return false;
}
}
/** @return Traversable<array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'lastSeen':int,
* 'hash':string,'is_read':bool,'is_favorite':bool,'id_feed':int,'tags':string,'attributes':?string}> */
/** @return Traversable<array{id:string,guid:string,title:string,author:string,content:string,link:string,date:int,lastSeen:int,
* hash:string,is_read:bool,is_favorite:bool,id_feed:int,tags:string,attributes:?string}> */
public function selectAll(?int $limit = null): Traversable {
$content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content';
$hash = static::sqlHexEncode('hash');
@@ -743,16 +746,17 @@ SQL;
$stm = $this->pdo->query($sql);
if ($stm != false) {
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
/** @var array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'lastSeen':int,
* 'hash':string,'is_read':bool,'is_favorite':bool,'id_feed':int,'tags':string,'attributes':?string} $row */
/** @var array{id:string,guid:string,title:string,author:string,content:string,link:string,date:int,lastSeen:int,
* hash:string,is_read:bool,is_favorite:bool,id_feed:int,tags:string,attributes:?string} $row */
yield $row;
}
} else {
$info = $this->pdo->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
yield from $this->selectAll();
} else {
Minz_Log::error(__method__ . ' error: ' . json_encode($info));
Minz_Log::error(__METHOD__ . ' error: ' . json_encode($info));
}
}
}
@@ -765,8 +769,8 @@ SELECT id, guid, title, author, link, date, is_read, is_favorite, {$hash} AS has
FROM `_entry` WHERE id_feed=:id_feed AND guid=:guid
SQL;
$res = $this->fetchAssoc($sql, [':id_feed' => $id_feed, ':guid' => $guid]);
/** @var array<array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
* 'is_read':int,'is_favorite':int,'tags':string,'attributes':?string}> $res */
/** @var list<array{id:string,id_feed:int,guid:string,title:string,author:string,content:string,link:string,date:int,
* is_read:int,is_favorite:int,tags:string,attributes:?string}> $res */
return isset($res[0]) ? FreshRSS_Entry::fromArray($res[0]) : null;
}
@@ -778,7 +782,7 @@ SELECT id, guid, title, author, link, date, is_read, is_favorite, {$hash} AS has
FROM `_entry` WHERE id=:id
SQL;
$res = $this->fetchAssoc($sql, [':id' => $id]);
/** @var array<array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
/** @var list<array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
* 'is_read':int,'is_favorite':int,'tags':string,'attributes':?string}> $res */
return isset($res[0]) ? FreshRSS_Entry::fromArray($res[0]) : null;
}
@@ -789,7 +793,7 @@ SQL;
return empty($res[0]) ? null : (string)($res[0]);
}
/** @return array{0:array<int|string>,1:string} */
/** @return array{0:list<int|string>,1:string} */
public static function sqlBooleanSearch(string $alias, FreshRSS_BooleanSearch $filters, int $level = 0): array {
$search = '';
$values = [];
@@ -1104,7 +1108,7 @@ SQL;
/**
* @param 'ASC'|'DESC' $order
* @return array{0:array<int|string>,1:string}
* @return array{0:list<int|string>,1:string}
* @throws FreshRSS_EntriesGetter_Exception
*/
protected function sqlListEntriesWhere(string $alias = '', ?FreshRSS_BooleanSearch $filters = null,
@@ -1173,7 +1177,7 @@ SQL;
* @phpstan-param 'a'|'A'|'i'|'s'|'S'|'c'|'f'|'t'|'T'|'ST'|'Z' $type
* @param int $id category/feed/tag ID
* @param 'ASC'|'DESC' $order
* @return array{0:array<int|string>,1:string}
* @return array{0:list<int|string>,1:string}
* @throws FreshRSS_EntriesGetter_Exception
*/
private function sqlListWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
@@ -1269,6 +1273,7 @@ SQL;
return $stm;
} else {
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
return $this->listWhereRaw($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min);
}
@@ -1347,7 +1352,7 @@ SQL;
* @phpstan-param 'a'|'A'|'s'|'S'|'c'|'f'|'t'|'T'|'ST'|'Z' $type
* @param int $id category/feed/tag ID
* @param 'ASC'|'DESC' $order
* @return array<numeric-string>|null
* @return list<numeric-string>|null
* @throws FreshRSS_EntriesGetter_Exception
*/
public function listIdsWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
@@ -1356,7 +1361,8 @@ SQL;
[$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $offset, $firstId, $filters);
$stm = $this->pdo->prepare($sql);
if ($stm !== false && $stm->execute($values) && ($res = $stm->fetchAll(PDO::FETCH_COLUMN, 0)) !== false) {
/** @var array<numeric-string> $res */
$res = array_map('strval', $res);
/** @var list<numeric-string> $res */
return $res;
}
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
@@ -1366,7 +1372,7 @@ SQL;
/**
* @param array<string> $guids
* @return array<string>|false
* @return array<string,string>|false
*/
public function listHashForFeedGuids(int $id_feed, array $guids): array|false {
$result = [];
@@ -1376,7 +1382,7 @@ SQL;
// Split a query with too many variables parameters
$guidsChunks = array_chunk($guids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER);
foreach ($guidsChunks as $guidsChunk) {
$result += $this->listHashForFeedGuids($id_feed, $guidsChunk);
$result += $this->listHashForFeedGuids($id_feed, $guidsChunk) ?: [];
}
return $result;
}
@@ -1394,9 +1400,6 @@ SQL;
return $result;
} else {
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->listHashForFeedGuids($id_feed, $guids);
}
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info)
. ' while querying feed ' . $id_feed);
return false;
@@ -1430,9 +1433,6 @@ SQL;
return $stm->rowCount();
} else {
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->updateLastSeen($id_feed, $guids);
}
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info)
. ' while updating feed ' . $id_feed);
return false;

View File

@@ -49,7 +49,7 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
// Nothing to do for PostgreSQL
}
/** @param array<string|int> $errorInfo */
/** @param array{0:string,1:int,2:string} $errorInfo */
#[\Override]
protected function autoUpdateDb(array $errorInfo): bool {
if (isset($errorInfo[0])) {

View File

@@ -49,7 +49,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
);
}
/** @param array<string|int> $errorInfo */
/** @param array{0:string,1:int,2:string} $errorInfo */
#[\Override]
protected function autoUpdateDb(array $errorInfo): bool {
if (($tableInfo = $this->pdo->query("PRAGMA table_info('entry')")) !== false) {

View File

@@ -122,13 +122,13 @@ class FreshRSS_Feed extends Minz_Model {
}
/**
* @return array<FreshRSS_Entry>|null
* @return list<FreshRSS_Entry>|null
* @deprecated
*/
public function entries(): ?array {
Minz_Log::warning(__method__ . ' is deprecated since FreshRSS 1.16.1!');
Minz_Log::warning(__METHOD__ . ' is deprecated since FreshRSS 1.16.1!');
$simplePie = $this->load(false, true);
return $simplePie == null ? [] : iterator_to_array($this->loadEntries($simplePie));
return $simplePie == null ? [] : array_values(iterator_to_array($this->loadEntries($simplePie)));
}
public function name(bool $raw = false): string {
return $raw || $this->name != '' ? $this->name : (preg_replace('%^https?://(www[.])?%i', '', $this->url) ?? '');
@@ -479,7 +479,7 @@ class FreshRSS_Feed extends Minz_Model {
* @param float $invalidGuidsTolerance (default 0.05) The maximum ratio (rounded) of invalid GUIDs to tolerate before degrading the unicity criteria.
* Example for 0.05 (5% rounded): tolerate 0 invalid GUIDs for up to 9 articles, 1 for 10, 2 for 30, 3 for 50, 4 for 70, 5 for 90, 6 for 110, etc.
* The default value of 5% rounded was chosen to allow 1 invalid GUID for feeds of 10 articles, which is a frequently observed amount of articles.
* @return array<string>
* @return list<string>
*/
public function loadGuids(\SimplePie\SimplePie $simplePie, float $invalidGuidsTolerance = 0.05): array {
$invalidGuids = 0;
@@ -1077,13 +1077,13 @@ class FreshRSS_Feed extends Minz_Model {
$hubFilename = $path . '/!hub.json';
if (($hubFile = @file_get_contents($hubFilename)) != false) {
$hubJson = json_decode($hubFile, true);
if (!is_array($hubJson) || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) {
if (!is_array($hubJson) || empty($hubJson['key']) || !is_string($hubJson['key']) || !ctype_xdigit($hubJson['key'])) {
$text = 'Invalid JSON for WebSub: ' . $this->url;
Minz_Log::warning($text);
Minz_Log::warning($text, PSHB_LOG);
return false;
}
if ((!empty($hubJson['lease_end'])) && ($hubJson['lease_end'] < (time() + (3600 * 23)))) { //TODO: Make a better policy
if (!empty($hubJson['lease_end']) && is_int($hubJson['lease_end']) && $hubJson['lease_end'] < (time() + (3600 * 23))) { //TODO: Make a better policy
$text = 'WebSub lease ends at '
. date('c', empty($hubJson['lease_end']) ? time() : $hubJson['lease_end'])
. ' and needs renewal: ' . $this->url;
@@ -1131,7 +1131,8 @@ class FreshRSS_Feed extends Minz_Model {
return false;
}
$hubJson = json_decode($hubFile, true);
if (!is_array($hubJson) || empty($hubJson['key']) || !ctype_xdigit($hubJson['key']) || empty($hubJson['hub'])) {
if (!is_array($hubJson) || empty($hubJson['key']) || !is_string($hubJson['key']) || !ctype_xdigit($hubJson['key']) ||
empty($hubJson['hub']) || !is_string($hubJson['hub'])) {
Minz_Log::warning('Invalid JSON for WebSub: ' . $this->url);
return false;
}

View File

@@ -7,18 +7,18 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
if ($this->pdo->inTransaction()) {
$this->pdo->commit();
}
Minz_Log::warning(__method__ . ': ' . $name);
Minz_Log::warning(__METHOD__ . ': ' . $name);
try {
if ($name === 'kind') { //v1.20.0
return $this->pdo->exec('ALTER TABLE `_feed` ADD COLUMN kind SMALLINT DEFAULT 0') !== false;
}
} catch (Exception $e) {
Minz_Log::error(__method__ . ' error: ' . $e->getMessage());
Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage());
}
return false;
}
/** @param array<int|string> $errorInfo */
/** @param array{0:string,1:int,2:string} $errorInfo */
protected function autoUpdateDb(array $errorInfo): bool {
if (isset($errorInfo[0])) {
if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
@@ -34,8 +34,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
}
/**
* @param array{'url':string,'kind':int,'category':int,'name':string,'website':string,'description':string,'lastUpdate':int,'priority'?:int,
* 'pathEntries'?:string,'httpAuth':string,'error':int|bool,'ttl'?:int,'attributes'?:string|array<string|mixed>} $valuesTmp
* @param array{url:string,kind:int,category:int,name:string,website:string,description:string,lastUpdate:int,priority?:int,
* pathEntries?:string,httpAuth:string,error:int|bool,ttl?:int,attributes?:string|array<string|mixed>} $valuesTmp
*/
public function addFeed(array $valuesTmp): int|false {
$sql = 'INSERT INTO `_feed` (url, kind, category, name, website, description, `lastUpdate`, priority, `pathEntries`, `httpAuth`, error, ttl, attributes)
@@ -72,6 +72,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
return $feedId === false ? false : (int)$feedId;
} else {
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
return $this->addFeed($valuesTmp);
}
@@ -177,6 +178,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
return true;
} else {
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
return $this->updateFeed($id, $originalValues);
}
@@ -290,8 +292,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
}
}
/** @return Traversable<array{'id':int,'url':string,'kind':int,'category':int,'name':string,'website':string,'description':string,'lastUpdate':int,'priority'?:int,
* 'pathEntries'?:string,'httpAuth':string,'error':int|bool,'ttl'?:int,'attributes'?:string}> */
/** @return Traversable<array{id:int,url:string,kind:int,category:int,name:string,website:string,description:string,lastUpdate:int,priority?:int,
* pathEntries?:string,httpAuth:string,error:int|bool,ttl?:int,attributes?:string}> */
public function selectAll(): Traversable {
$sql = <<<'SQL'
SELECT id, url, kind, category, name, website, description, `lastUpdate`,
@@ -301,16 +303,17 @@ SQL;
$stm = $this->pdo->query($sql);
if ($stm !== false) {
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
/** @var array{'id':int,'url':string,'kind':int,'category':int,'name':string,'website':string,'description':string,'lastUpdate':int,'priority'?:int,
* 'pathEntries'?:string,'httpAuth':string,'error':int|bool,'ttl'?:int,'attributes'?:string} $row */
/** @var array{id:int,url:string,kind:int,category:int,name:string,website:string,description:string,lastUpdate:int,priority?:int,
* pathEntries?:string,httpAuth:string,error:int|bool,ttl?:int,attributes?:string} $row */
yield $row;
}
} else {
$info = $this->pdo->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
yield from $this->selectAll();
} else {
Minz_Log::error(__method__ . ' error: ' . json_encode($info));
Minz_Log::error(__METHOD__ . ' error: ' . json_encode($info));
}
}
}
@@ -318,40 +321,34 @@ SQL;
public function searchById(int $id): ?FreshRSS_Feed {
$sql = 'SELECT * FROM `_feed` WHERE id=:id';
$res = $this->fetchAssoc($sql, [':id' => $id]);
if ($res == null) {
if (!is_array($res)) {
return null;
}
/** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
$feeds = self::daoToFeeds($res);
return $feeds[$id] ?? null;
$feeds = self::daoToFeeds($res); // @phpstan-ignore argument.type
return $feeds[0] ?? null;
}
public function searchByUrl(string $url): ?FreshRSS_Feed {
$sql = 'SELECT * FROM `_feed` WHERE url=:url';
$res = $this->fetchAssoc($sql, [':url' => $url]);
/** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
return empty($res[0]) ? null : (current(self::daoToFeeds($res)) ?: null);
return empty($res[0]) ? null : (current(self::daoToFeeds($res)) ?: null); // @phpstan-ignore argument.type
}
/** @return array<int> */
/** @return list<int> */
public function listFeedsIds(): array {
$sql = 'SELECT id FROM `_feed`';
/** @var array<int> $res */
/** @var list<int> $res */
$res = $this->fetchColumn($sql, 0) ?? [];
return $res;
}
/**
* @return array<int,FreshRSS_Feed>
* @return list<FreshRSS_Feed>
*/
public function listFeeds(): array {
$sql = 'SELECT * FROM `_feed` ORDER BY name';
$res = $this->fetchAssoc($sql);
/** @var array<array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'ttl':int,'attributes':string}>|null $res */
return $res == null ? [] : self::daoToFeeds($res);
return $res == null ? [] : self::daoToFeeds($res); // @phpstan-ignore argument.type
}
/** @return array<string,string> */
@@ -363,7 +360,7 @@ SQL;
$sql .= 'WHERE id_feed=' . intval($id_feed);
}
$res = $this->fetchAssoc($sql);
/** @var array<array{'id_feed':int,'newest_item_us':string}>|null $res */
/** @var list<array{'id_feed':int,'newest_item_us':string}>|null $res */
if ($res == null) {
return [];
}
@@ -376,7 +373,7 @@ SQL;
/**
* @param int $defaultCacheDuration Use -1 to return all feeds, without filtering them by TTL.
* @return array<int,FreshRSS_Feed>
* @return list<FreshRSS_Feed>
*/
public function listFeedsOrderUpdate(int $defaultCacheDuration = 3600, int $limit = 0): array {
$sql = 'SELECT id, url, kind, category, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes, `cache_nbEntries`, `cache_nbUnreads` '
@@ -391,6 +388,7 @@ SQL;
return self::daoToFeeds($stm->fetchAll(PDO::FETCH_ASSOC));
} else {
$info = $this->pdo->errorInfo();
/** @var array{0:string,1:int,2:string} $info */
if ($this->autoUpdateDb($info)) {
return $this->listFeedsOrderUpdate($defaultCacheDuration, $limit);
}
@@ -399,19 +397,19 @@ SQL;
}
}
/** @return array<int,string> */
/** @return list<string> */
public function listTitles(int $id, int $limit = 0): array {
$sql = 'SELECT title FROM `_entry` WHERE id_feed=:id_feed ORDER BY id DESC'
. ($limit < 1 ? '' : ' LIMIT ' . intval($limit));
$res = $this->fetchColumn($sql, 0, [':id_feed' => $id]) ?? [];
/** @var array<int,string> $res */
/** @var list<string> $res */
return $res;
}
/**
* @param bool|null $muted to include only muted feeds
* @param bool|null $errored to include only errored feeds
* @return array<int,FreshRSS_Feed>
* @return list<FreshRSS_Feed>
*/
public function listByCategory(int $cat, ?bool $muted = null, ?bool $errored = null): array {
$sql = 'SELECT * FROM `_feed` WHERE category=:category';
@@ -422,18 +420,11 @@ SQL;
$sql .= ' AND error <> 0';
}
$res = $this->fetchAssoc($sql, [':category' => $cat]);
if ($res == null) {
if (!is_array($res)) {
return [];
}
/**
* @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res
*/
$feeds = self::daoToFeeds($res);
uasort($feeds, static fn(FreshRSS_Feed $a, FreshRSS_Feed $b) => strnatcasecmp($a->name(), $b->name()));
$feeds = self::daoToFeeds($res); // @phpstan-ignore argument.type
usort($feeds, static fn(FreshRSS_Feed $a, FreshRSS_Feed $b) => strnatcasecmp($a->name(), $b->name()));
return $feeds;
}
@@ -576,23 +567,19 @@ SQL;
}
/**
* @param array<int,array{'id'?:int,'url'?:string,'kind'?:int,'category'?:int,'name'?:string,'website'?:string,'description'?:string,'lastUpdate'?:int,'priority'?:int,
* 'pathEntries'?:string,'httpAuth'?:string,'error'?:int|bool,'ttl'?:int,'attributes'?:string,'cache_nbUnreads'?:int,'cache_nbEntries'?:int}> $listDAO
* @return array<int,FreshRSS_Feed>
* @param array<array{id?:int,url?:string,kind?:int,category?:int,name?:string,website?:string,description?:string,lastUpdate?:int,priority?:int,
* pathEntries?:string,httpAuth?:string,error?:int|bool,ttl?:int,attributes?:string,cache_nbUnreads?:int,cache_nbEntries?:int}> $listDAO
* @return list<FreshRSS_Feed>
*/
public static function daoToFeeds(array $listDAO, ?int $catID = null): array {
$list = [];
foreach ($listDAO as $key => $dao) {
FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'kind', 'category', 'lastUpdate', 'priority', 'error', 'ttl', 'cache_nbUnreads', 'cache_nbEntries']);
if (!isset($dao['name'])) {
foreach ($listDAO as $dao) {
if (!is_string($dao['name'] ?? null)) {
continue;
}
if (isset($dao['id'])) {
$key = (int)$dao['id'];
}
if ($catID === null) {
$category = $dao['category'] ?? 0;
$category = is_numeric($dao['category'] ?? null) ? (int)$dao['category'] : 0;
} else {
$category = $catID;
}
@@ -615,7 +602,7 @@ SQL;
if (isset($dao['id'])) {
$myFeed->_id($dao['id']);
}
$list[$key] = $myFeed;
$list[] = $myFeed;
}
return $list;

View File

@@ -3,7 +3,7 @@ declare(strict_types=1);
class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
/** @param array<int|string> $errorInfo */
/** @param array{0:string,1:int,2:string} $errorInfo */
#[\Override]
protected function autoUpdateDb(array $errorInfo): bool {
if (($tableInfo = $this->pdo->query("PRAGMA table_info('feed')")) !== false) {

View File

@@ -3,7 +3,7 @@ declare(strict_types=1);
class FreshRSS_FilterAction {
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $actions = null;
/** @param array<string> $actions */
@@ -15,7 +15,7 @@ class FreshRSS_FilterAction {
return $this->booleanSearch;
}
/** @return array<string> */
/** @return list<string> */
public function actions(): array {
return $this->actions ?? [];
}
@@ -23,7 +23,7 @@ class FreshRSS_FilterAction {
/** @param array<string> $actions */
public function _actions(?array $actions): void {
if (is_array($actions)) {
$this->actions = array_unique($actions);
$this->actions = array_values(array_unique($actions));
} else {
$this->actions = null;
}
@@ -42,7 +42,8 @@ class FreshRSS_FilterAction {
/** @param array|mixed|null $json */
public static function fromJSON($json): ?FreshRSS_FilterAction {
if (is_array($json) && !empty($json['search']) && !empty($json['actions']) && is_array($json['actions'])) {
if (is_array($json) && !empty($json['search']) && is_string($json['search']) &&
!empty($json['actions']) && is_array($json['actions']) && is_array_values_string($json['actions'])) {
return new FreshRSS_FilterAction(new FreshRSS_BooleanSearch($json['search']), $json['actions']);
}
return null;

View File

@@ -6,11 +6,11 @@ declare(strict_types=1);
*/
trait FreshRSS_FilterActionsTrait {
/** @var array<FreshRSS_FilterAction>|null $filterActions */
/** @var list<FreshRSS_FilterAction>|null $filterActions */
private ?array $filterActions = null;
/**
* @return array<FreshRSS_FilterAction>
* @return list<FreshRSS_FilterAction>
*/
private function filterActions(): array {
if (empty($this->filterActions)) {
@@ -30,7 +30,7 @@ trait FreshRSS_FilterActionsTrait {
* @param array<FreshRSS_FilterAction>|null $filterActions
*/
private function _filterActions(?array $filterActions): void {
$this->filterActions = $filterActions;
$this->filterActions = is_array($filterActions) ? array_values($filterActions) : null;
if ($this->filterActions !== null && !empty($this->filterActions)) {
$this->_attribute('filters', array_map(
static fn(?FreshRSS_FilterAction $af) => $af == null ? null : $af->toJSON(),
@@ -40,7 +40,7 @@ trait FreshRSS_FilterActionsTrait {
}
}
/** @return array<FreshRSS_BooleanSearch> */
/** @return list<FreshRSS_BooleanSearch> */
public function filtersAction(string $action): array {
$action = trim($action);
if ($action == '') {
@@ -121,6 +121,7 @@ trait FreshRSS_FilterActionsTrait {
/**
* @param bool $applyLabel Parameter by reference, which will be set to true if the callers needs to apply a label to the article entry.
* @param-out bool $applyLabel
*/
public function applyFilterActions(FreshRSS_Entry $entry, ?bool &$applyLabel = null): void {
$applyLabel = false;

View File

@@ -14,7 +14,7 @@ class FreshRSS_FormAuth {
return password_verify($nonce . $hash, $challenge);
}
/** @return array<string> */
/** @return list<string> */
public static function getCredentialsFromCookie(): array {
$token = Minz_Session::getLongTermCookie('FreshRSS_login');
if (!ctype_alnum($token)) {

View File

@@ -9,7 +9,7 @@ final class FreshRSS_LogDAO {
return USERS_PATH . '/' . (Minz_User::name() ?? Minz_User::INTERNAL_USER) . '/' . $logFileName;
}
/** @return array<FreshRSS_Log> */
/** @return list<FreshRSS_Log> */
public static function lines(?string $logFileName = null): array {
$logs = [];
$handle = @fopen(self::logPath($logFileName), 'r');

View File

@@ -59,7 +59,7 @@ class FreshRSS_ReadingMode {
}
/**
* @return array<FreshRSS_ReadingMode> the built-in reading modes
* @return list<FreshRSS_ReadingMode> the built-in reading modes
*/
public static function getReadingModes(): array {
$actualView = Minz_Request::actionName();

View File

@@ -17,17 +17,17 @@ class FreshRSS_Search implements \Stringable {
private string $raw_input = '';
// The following properties are extracted from the raw input
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $entry_ids = null;
/** @var array<int>|null */
/** @var list<int>|null */
private ?array $feed_ids = null;
/** @var array<int>|'*'|null */
/** @var list<int>|'*'|null */
private $label_ids = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $label_names = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $intitle = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $intitle_regex = null;
/** @var int|false|null */
private $min_date = null;
@@ -37,34 +37,34 @@ class FreshRSS_Search implements \Stringable {
private $min_pubdate = null;
/** @var int|false|null */
private $max_pubdate = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $inurl = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $inurl_regex = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $author = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $author_regex = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $tags = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $tags_regex = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $search = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $search_regex = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $not_entry_ids = null;
/** @var array<int>|null */
/** @var list<int>|null */
private ?array $not_feed_ids = null;
/** @var array<int>|'*'|null */
/** @var list<int>|'*'|null */
private $not_label_ids = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $not_label_names = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $not_intitle = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $not_intitle_regex = null;
/** @var int|false|null */
private $not_min_date = null;
@@ -74,21 +74,21 @@ class FreshRSS_Search implements \Stringable {
private $not_min_pubdate = null;
/** @var int|false|null */
private $not_max_pubdate = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $not_inurl = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $not_inurl_regex = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $not_author = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $not_author_regex = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $not_tags = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $not_tags_regex = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $not_search = null;
/** @var array<string>|null */
/** @var list<string>|null */
private ?array $not_search_regex = null;
public function __construct(string $input) {
@@ -137,54 +137,54 @@ class FreshRSS_Search implements \Stringable {
return $this->raw_input;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getEntryIds(): ?array {
return $this->entry_ids;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getNotEntryIds(): ?array {
return $this->not_entry_ids;
}
/** @return array<int>|null */
/** @return list<int>|null */
public function getFeedIds(): ?array {
return $this->feed_ids;
}
/** @return array<int>|null */
/** @return list<int>|null */
public function getNotFeedIds(): ?array {
return $this->not_feed_ids;
}
/** @return array<int>|'*'|null */
/** @return list<int>|'*'|null */
public function getLabelIds(): array|string|null {
return $this->label_ids;
}
/** @return array<int>|'*'|null */
/** @return list<int>|'*'|null */
public function getNotLabelIds(): array|string|null {
return $this->not_label_ids;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getLabelNames(): ?array {
return $this->label_names;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getNotLabelNames(): ?array {
return $this->not_label_names;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getIntitle(): ?array {
return $this->intitle;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getIntitleRegex(): ?array {
return $this->intitle_regex;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getNotIntitle(): ?array {
return $this->not_intitle;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getNotIntitleRegex(): ?array {
return $this->not_intitle_regex;
}
@@ -223,90 +223,90 @@ class FreshRSS_Search implements \Stringable {
return $this->not_max_pubdate ?: null;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getInurl(): ?array {
return $this->inurl;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getInurlRegex(): ?array {
return $this->inurl_regex;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getNotInurl(): ?array {
return $this->not_inurl;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getNotInurlRegex(): ?array {
return $this->not_inurl_regex;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getAuthor(): ?array {
return $this->author;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getAuthorRegex(): ?array {
return $this->author_regex;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getNotAuthor(): ?array {
return $this->not_author;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getNotAuthorRegex(): ?array {
return $this->not_author_regex;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getTags(): ?array {
return $this->tags;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getTagsRegex(): ?array {
return $this->tags_regex;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getNotTags(): ?array {
return $this->not_tags;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getNotTagsRegex(): ?array {
return $this->not_tags_regex;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getSearch(): ?array {
return $this->search;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getSearchRegex(): ?array {
return $this->search_regex;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getNotSearch(): ?array {
return $this->not_search;
}
/** @return array<string>|null */
/** @return list<string>|null */
public function getNotSearchRegex(): ?array {
return $this->not_search_regex;
}
/**
* @param array<string>|null $anArray
* @return array<string>
* @param list<string>|null $anArray
* @return list<string>
*/
private static function removeEmptyValues(?array $anArray): array {
return empty($anArray) ? [] : array_filter($anArray, static fn(string $value) => $value !== '');
return empty($anArray) ? [] : array_values(array_filter($anArray, static fn(string $value) => $value !== ''));
}
/**
* @param array<string>|string $value
* @return ($value is array ? array<string> : string)
* @param list<string>|string $value
* @return ($value is string ? string : list<string>)
*/
private static function decodeSpaces($value): array|string {
private static function decodeSpaces(array|string $value): array|string {
if (is_array($value)) {
for ($i = count($value) - 1; $i >= 0; $i--) {
$value[$i] = self::decodeSpaces($value[$i]);
foreach ($value as &$val) {
$val = self::decodeSpaces($val);
}
} else {
$value = trim(str_replace('+', ' ', $value));
@@ -315,8 +315,8 @@ class FreshRSS_Search implements \Stringable {
}
/**
* @param array<string> $strings
* @return array<string>
* @param list<string> $strings
* @return list<string>
*/
private static function htmlspecialchars_decodes(array $strings): array {
return array_map(static fn(string $s) => htmlspecialchars_decode($s, ENT_QUOTES), $strings);
@@ -365,7 +365,7 @@ class FreshRSS_Search implements \Stringable {
foreach ($ids_lists as $ids_list) {
$feed_ids = explode(',', $ids_list);
$feed_ids = self::removeEmptyValues($feed_ids);
/** @var array<int> $feed_ids */
/** @var list<int> $feed_ids */
$feed_ids = array_map('intval', $feed_ids);
if (!empty($feed_ids)) {
$this->feed_ids = array_merge($this->feed_ids, $feed_ids);
@@ -383,7 +383,7 @@ class FreshRSS_Search implements \Stringable {
foreach ($ids_lists as $ids_list) {
$feed_ids = explode(',', $ids_list);
$feed_ids = self::removeEmptyValues($feed_ids);
/** @var array<int> $feed_ids */
/** @var list<int> $feed_ids */
$feed_ids = array_map('intval', $feed_ids);
if (!empty($feed_ids)) {
$this->not_feed_ids = array_merge($this->not_feed_ids, $feed_ids);
@@ -408,7 +408,7 @@ class FreshRSS_Search implements \Stringable {
}
$label_ids = explode(',', $ids_list);
$label_ids = self::removeEmptyValues($label_ids);
/** @var array<int> $label_ids */
/** @var list<int> $label_ids */
$label_ids = array_map('intval', $label_ids);
if (!empty($label_ids)) {
$this->label_ids = array_merge($this->label_ids, $label_ids);
@@ -430,7 +430,7 @@ class FreshRSS_Search implements \Stringable {
}
$label_ids = explode(',', $ids_list);
$label_ids = self::removeEmptyValues($label_ids);
/** @var array<int> $label_ids */
/** @var list<int> $label_ids */
$label_ids = array_map('intval', $label_ids);
if (!empty($label_ids)) {
$this->not_label_ids = array_merge($this->not_label_ids, $label_ids);

View File

@@ -13,8 +13,8 @@ class FreshRSS_Share {
/**
* Register a new sharing option.
* @param array{'type':string,'url':string,'transform'?:array<callable>|array<string,array<callable>>,'field'?:string,'help'?:string,'form'?:'simple'|'advanced',
* 'method'?:'GET'|'POST','HTMLtag'?:'button','deprecated'?:bool} $share_options is an array defining the share option.
* @param array{type:string,url:string,transform?:array<callable>|array<string,array<callable>>,field?:string,help?:string,form?:'simple'|'advanced',
* method?:'GET'|'POST',HTMLtag?:'button',deprecated?:bool} $share_options is an array defining the share option.
*/
public static function register(array $share_options): void {
$type = $share_options['type'];
@@ -46,7 +46,12 @@ class FreshRSS_Share {
}
foreach ($shares_from_file as $share_type => $share_options) {
if (!is_array($share_options)) {
continue;
}
$share_options['type'] = $share_type;
/** @var array{type:string,url:string,transform?:array<callable>|array<string,array<callable>>,field?:string,help?:string,form?:'simple'|'advanced',
* method?:'GET'|'POST',HTMLtag?:'button',deprecated?:bool} $share_options */
self::register($share_options);
}
@@ -233,8 +238,8 @@ class FreshRSS_Share {
'~LINK~',
];
$replaces = [
$this->id(),
$this->base_url,
$this->id() ?? '',
$this->base_url ?? '',
$this->title(),
$this->link(),
];
@@ -298,7 +303,10 @@ class FreshRSS_Share {
}
foreach ($transform as $action) {
$data = call_user_func($action, $data);
$return = call_user_func($action, $data);
if (is_string($return)) {
$data = $return;
}
}
return $data;
@@ -307,7 +315,7 @@ class FreshRSS_Share {
/**
* Get the list of transformations for the given attribute.
* @param string $attr the attribute of which we want the transformations.
* @return array<callable> containing a list of transformations to apply.
* @return list<callable> containing a list of transformations to apply.
*/
private function getTransform(string $attr): array {
if (array_key_exists($attr, $this->transforms)) {

View File

@@ -29,7 +29,7 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo {
* - unread entries
* - favorite entries
*
* @return array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false
* @return array{total:int,count_unreads:int,count_reads:int,count_favorites:int}|false
*/
public function calculateEntryRepartitionPerFeed(?int $feed = null, bool $only_main = false): array|false {
$filter = '';
@@ -49,10 +49,9 @@ WHERE e.id_feed = f.id
{$filter}
SQL;
$res = $this->fetchAssoc($sql);
if (!empty($res[0])) {
if (is_array($res) && !empty($res[0])) {
$dao = $res[0];
/** @var array<array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}> $res */
FreshRSS_DatabaseDAO::pdoInt($dao, ['total', 'count_unreads', 'count_reads', 'count_favorites']);
/** @var array{total:int,count_unreads:int,count_reads:int,count_favorites:int} $dao */
return $dao;
}
return false;
@@ -78,10 +77,10 @@ GROUP BY day
ORDER BY day ASC
SQL;
$res = $this->fetchAssoc($sql);
if ($res == false) {
if (!is_array($res)) {
return [];
}
/** @var array<array{'day':int,'count':int}> $res */
/** @var list<array{day:int,count:int}> $res */
foreach ($res as $value) {
$count[(int)($value['day'])] = (int)($value['count']);
}
@@ -123,7 +122,6 @@ SQL;
return $monthRepartition;
}
/**
* Calculates the number of article per period per feed
* @param string $period format string to use for grouping
@@ -228,7 +226,7 @@ SQL;
/**
* Calculates feed count per category.
* @return array<array{'label':string,'data':int}>
* @return list<array{'label':string,'data':int}>
*/
public function calculateFeedByCategory(): array {
$sql = <<<SQL
@@ -239,14 +237,14 @@ WHERE c.id = f.category
GROUP BY label
ORDER BY data DESC
SQL;
/** @var array<array{'label':string,'data':int}>|null @res */
/** @var list<array{'label':string,'data':int}>|null @res */
$res = $this->fetchAssoc($sql);
return $res == null ? [] : $res;
}
/**
* Calculates entry count per category.
* @return array<array{'label':string,'data':int}>
* @return list<array{'label':string,'data':int}>
*/
public function calculateEntryByCategory(): array {
$sql = <<<SQL
@@ -259,13 +257,13 @@ GROUP BY label
ORDER BY data DESC
SQL;
$res = $this->fetchAssoc($sql);
/** @var array<array{'label':string,'data':int}>|null $res */
/** @var list<array{'label':string,'data':int}>|null $res */
return $res == null ? [] : $res;
}
/**
* Calculates the 10 top feeds based on their number of entries
* @return array<array{'id':int,'name':string,'category':string,'count':int}>
* @return list<array{'id':int,'name':string,'category':string,'count':int}>
*/
public function calculateTopFeed(): array {
$sql = <<<SQL
@@ -281,11 +279,8 @@ ORDER BY count DESC
LIMIT 10
SQL;
$res = $this->fetchAssoc($sql);
/** @var array<array{'id':int,'name':string,'category':string,'count':int}>|null $res */
/** @var list<array{'id':int,'name':string,'category':string,'count':int}>|null $res */
if (is_array($res)) {
foreach ($res as &$dao) {
FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'count']);
}
return $res;
}
return [];
@@ -293,7 +288,7 @@ SQL;
/**
* Calculates the last publication date for each feed
* @return array<array{'id':int,'name':string,'last_date':int,'nb_articles':int}>
* @return list<array{'id':int,'name':string,'last_date':int,'nb_articles':int}>
*/
public function calculateFeedLastDate(): array {
$sql = <<<SQL
@@ -307,11 +302,8 @@ GROUP BY f.id
ORDER BY name
SQL;
$res = $this->fetchAssoc($sql);
/** @var array<array{'id':int,'name':string,'last_date':int,'nb_articles':int}>|null $res */
/** @var list<array{'id':int,'name':string,'last_date':int,'nb_articles':int}>|null $res */
if (is_array($res)) {
foreach ($res as &$dao) {
FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'last_date', 'nb_articles']);
}
return $res;
}
return [];
@@ -319,7 +311,7 @@ SQL;
/**
* Gets days ready for graphs
* @return array<string>
* @return list<string>
*/
public function getDays(): array {
return $this->convertToTranslatedJson([
@@ -335,7 +327,7 @@ SQL;
/**
* Gets months ready for graphs
* @return array<string>
* @return list<string>
*/
public function getMonths(): array {
return $this->convertToTranslatedJson([
@@ -356,8 +348,8 @@ SQL;
/**
* Translates array content
* @param array<string> $data
* @return array<string>
* @param list<string> $data
* @return list<string>
*/
private function convertToTranslatedJson(array $data = []): array {
$translated = array_map(static fn(string $a) => _t('gen.date.' . $a), $data);

View File

@@ -117,7 +117,7 @@ SQL;
}
}
/** @return Traversable<array{'id':int,'name':string,'attributes'?:array<string,mixed>}> */
/** @return Traversable<array{id:int,name:string,attributes?:array<string,mixed>}> */
public function selectAll(): Traversable {
$sql = 'SELECT id, name, attributes FROM `_tag`';
$stm = $this->pdo->query($sql);
@@ -126,12 +126,12 @@ SQL;
return;
}
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
/** @var array{'id':int,'name':string,'attributes'?:array<string,mixed>} $row */
/** @var array{id:int,name:string,attributes?:array<string,mixed>} $row */
yield $row;
}
}
/** @return Traversable<array{'id_tag':int,'id_entry':string}> */
/** @return Traversable<array{id_tag:int,id_entry:int|numeric-string}> */
public function selectEntryTag(): Traversable {
$sql = 'SELECT id_tag, id_entry FROM `_entrytag`';
$stm = $this->pdo->query($sql);
@@ -140,9 +140,8 @@ SQL;
return;
}
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
FreshRSS_DatabaseDAO::pdoInt($row, ['id_tag']);
FreshRSS_DatabaseDAO::pdoString($row, ['id_entry']);
yield $row;
/** @var array{id_tag:int,id_entry:int|numeric-string}> $row */
yield $row; // @phpstan-ignore generator.valueType
}
}
@@ -173,17 +172,17 @@ SQL;
public function searchById(int $id): ?FreshRSS_Tag {
$res = $this->fetchAssoc('SELECT * FROM `_tag` WHERE id=:id', [':id' => $id]);
/** @var array<array{'id':int,'name':string,'attributes'?:string}>|null $res */
/** @var list<array{id:int,name:string,attributes?:string}>|null $res */
return $res === null ? null : (current(self::daoToTags($res)) ?: null);
}
public function searchByName(string $name): ?FreshRSS_Tag {
$res = $this->fetchAssoc('SELECT * FROM `_tag` WHERE name=:name', [':name' => $name]);
/** @var array<array{'id':int,'name':string,'attributes'?:string}>|null $res */
/** @var list<array{id:int,name:string,attributes?:string}>|null $res */
return $res === null ? null : (current(self::daoToTags($res)) ?: null);
}
/** @return array<int,FreshRSS_Tag>|false */
/** @return list<FreshRSS_Tag>|false */
public function listTags(bool $precounts = false): array|false {
if ($precounts) {
$sql = <<<'SQL'
@@ -291,16 +290,16 @@ SQL;
}
/**
* @param array<array{id_tag:int,id_entry:string}> $addLabels Labels to insert as batch
* @param iterable<array{id_tag:int,id_entry:numeric-string|int}> $addLabels Labels to insert as batch
* @return int|false Number of new entries or false in case of error
*/
public function tagEntries(array $addLabels): int|false {
public function tagEntries(iterable $addLabels): int|false {
$hasValues = false;
$sql = 'INSERT ' . $this->sqlIgnore() . ' INTO `_entrytag`(id_tag, id_entry) VALUES ';
foreach ($addLabels as $addLabel) {
$id_tag = (int)($addLabel['id_tag'] ?? 0);
$id_entry = $addLabel['id_entry'] ?? '';
if ($id_tag > 0 && ctype_digit($id_entry)) {
if ($id_tag > 0 && (is_int($id_entry) || ctype_digit($id_entry))) {
$sql .= "({$id_tag},{$id_entry}),";
$hasValues = true;
}
@@ -320,7 +319,7 @@ SQL;
}
/**
* @return array<int,array{'id':int,'name':string,'id_entry':string,'checked':bool}>|false
* @return array<int,array{id:int,name:string,checked:bool}>|false
*/
public function getTagsForEntry(string $id_entry): array|false {
$sql = <<<'SQL'
@@ -347,8 +346,8 @@ SQL;
}
/**
* @param array<FreshRSS_Entry|numeric-string|array<string,string>> $entries
* @return array<array{'id_entry':string,'id_tag':int,'name':string}>|false
* @param list<FreshRSS_Entry|numeric-string> $entries
* @return list<array{id_entry:int|numeric-string,id_tag:int,name:string}>|false
*/
public function getTagsForEntries(array $entries): array|false {
$sql = <<<'SQL'
@@ -372,29 +371,16 @@ SQL;
return $values;
}
$sql .= ' AND et.id_entry IN (' . str_repeat('?,', count($entries) - 1) . '?)';
if (is_array($entries[0])) {
/** @var array<array<string,string>> $entries */
foreach ($entries as $entry) {
if (!empty($entry['id'])) {
$values[] = $entry['id'];
}
}
} elseif (is_object($entries[0])) {
/** @var array<FreshRSS_Entry> $entries */
foreach ($entries as $entry) {
$values[] = $entry->id();
}
} else {
/** @var array<numeric-string> $entries */
foreach ($entries as $entry) {
$values[] = $entry;
}
foreach ($entries as $entry) {
$values[] = is_object($entry) ? $entry->id() : $entry;
}
}
$stm = $this->pdo->prepare($sql);
if ($stm !== false && $stm->execute($values)) {
return $stm->fetchAll(PDO::FETCH_ASSOC);
$result = $stm->fetchAll(PDO::FETCH_ASSOC);
/** @var list<array{id_entry:int|numeric-string,id_tag:int,name:string}> $result; */
return $result;
}
$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
@@ -404,7 +390,7 @@ SQL;
/**
* Produces an array: for each entry ID (prefixed by `e_`), associate a list of labels.
* Used by API and by JSON export, to speed up queries (would be very expensive to perform a label look-up on each entry individually).
* @param array<FreshRSS_Entry|numeric-string> $entries the list of entries for which to retrieve the labels.
* @param list<FreshRSS_Entry|numeric-string> $entries the list of entries for which to retrieve the labels.
* @return array<string,array<string>> An array of the shape `[e_id_entry => ["label 1", "label 2"]]`
*/
public function getEntryIdsTagNames(array $entries): array {
@@ -421,8 +407,8 @@ SQL;
}
/**
* @param iterable<array{'id':int,'name':string,'attributes'?:string}> $listDAO
* @return array<int,FreshRSS_Tag>
* @param iterable<array{id:int,name:string,attributes?:string}> $listDAO
* @return list<FreshRSS_Tag>
*/
private static function daoToTags(iterable $listDAO): array {
$list = [];
@@ -438,7 +424,7 @@ SQL;
if (isset($dao['unreads'])) {
$tag->_nbUnread($dao['unreads']);
}
$list[$tag->id()] = $tag;
$list[] = $tag;
}
return $list;
}

View File

@@ -7,7 +7,7 @@ class FreshRSS_Themes extends Minz_Model {
private static string $defaultIconsUrl = '/themes/icons/';
public static string $defaultTheme = 'Origine';
/** @return array<string> */
/** @return list<string> */
public static function getList(): array {
return array_values(array_diff(
scandir(PUBLIC_PATH . self::$themesUrl) ?: [],
@@ -15,7 +15,7 @@ class FreshRSS_Themes extends Minz_Model {
));
}
/** @return array<string,array{'id':string,'name':string,'author':string,'description':string,'version':float|string,'files':array<string>,'theme-color'?:string|array{'dark'?:string,'light'?:string,'default'?:string}}> */
/** @return array<string,array{id:string,name:string,author:string,description:string,version:float|string,files:array<string>,theme-color?:string|array{dark?:string,light?:string,default?:string}}> */
public static function get(): array {
$themes_list = self::getList();
$list = [];
@@ -29,7 +29,7 @@ class FreshRSS_Themes extends Minz_Model {
}
/**
* @return false|array{'id':string,'name':string,'author':string,'description':string,'version':float|string,'files':array<string>,'theme-color'?:string|array{'dark'?:string,'light'?:string,'default'?:string}}
* @return false|array{id:string,name:string,author:string,description:string,version:float|string,files:array<string>,theme-color?:string|array{dark?:string,light?:string,default?:string}}
*/
public static function get_infos(string $theme_id): array|false {
$theme_dir = PUBLIC_PATH . self::$themesUrl . $theme_id;
@@ -38,13 +38,24 @@ class FreshRSS_Themes extends Minz_Model {
if (file_exists($json_filename)) {
$content = file_get_contents($json_filename) ?: '';
$res = json_decode($content, true);
if (is_array($res) &&
!empty($res['name']) &&
isset($res['files']) &&
is_array($res['files'])) {
$res['id'] = $theme_id;
/** @var array{'id':string,'name':string,'author':string,'description':string,'version':float|string,'files':array<string>,'theme-color'?:string|array{'dark'?:string,'light'?:string,'default'?:string}} */
return $res;
if (is_array($res)) {
$result = [
'id' => $theme_id,
'name' => is_string($res['name'] ?? null) ? $res['name'] : '',
'author' => is_string($res['author'] ?? null) ? $res['author'] : '',
'description' => is_string($res['description'] ?? null) ? $res['description'] : '',
'version' => is_string($res['version'] ?? null) || is_numeric($res['version'] ?? null) ? $res['version'] : '0',
'files' => is_array($res['files']) && is_array_values_string($res['files']) ? array_values($res['files']) : [],
'theme-color' => is_string($res['theme-color'] ?? null) ? $res['theme-color'] : '',
];
if (empty($result['theme-color']) && is_array($res['theme-color'])) {
$result['theme-color'] = [
'dark' => is_string($res['theme-color']['dark'] ?? null) ? $res['theme-color']['dark'] : '',
'light' => is_string($res['theme-color']['light'] ?? null) ? $res['theme-color']['light'] : '',
'default' => is_string($res['theme-color']['default'] ?? null) ? $res['theme-color']['default'] : '',
];
}
return $result;
}
}
}
@@ -56,7 +67,7 @@ class FreshRSS_Themes extends Minz_Model {
private static array $themeIcons;
/**
* @return false|array{'id':string,'name':string,'author':string,'description':string,'version':float|string,'files':array<string>,'theme-color'?:string|array{'dark'?:string,'light'?:string,'default'?:string}}
* @return false|array{id:string,name:string,author:string,description:string,version:float|string,files:array<string>,theme-color?:string|array{dark?:string,light?:string,default?:string}}
*/
public static function load(string $theme_id): array|false {
$infos = self::get_infos($theme_id);

View File

@@ -3,7 +3,7 @@ declare(strict_types=1);
/**
* @property string $apiPasswordHash
* @property array{'keep_period':string|false,'keep_max':int|false,'keep_min':int|false,'keep_favourites':bool,'keep_labels':bool,'keep_unreads':bool} $archiving
* @property array{keep_period:string|false,keep_max:int|false,keep_min:int|false,keep_favourites:bool,keep_labels:bool,keep_unreads:bool} $archiving
* @property bool $auto_load_more
* @property bool $auto_remove_article
* @property bool $bottomline_date
@@ -42,7 +42,7 @@ declare(strict_types=1);
* @property bool $onread_jump_next
* @property string $passwordHash
* @property int $posts_per_page
* @property array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}> $queries
* @property array<int,array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string}> $queries
* @property bool $reading_confirm
* @property int $since_hours_posts_per_rss
* @property bool $show_fav_unread
@@ -51,7 +51,7 @@ declare(strict_types=1);
* @property int $simplify_over_n_feeds
* @property bool $show_nav_buttons
* @property 'ASC'|'DESC' $sort_order
* @property array<string,array<string>> $sharing
* @property array<string,array<string,string>> $sharing
* @property array<string,string> $shortcuts
* @property bool $sides_close_article
* @property bool $sticky_post
@@ -94,8 +94,9 @@ final class FreshRSS_UserConfiguration extends Minz_Configuration {
* @throws Minz_FileNotExistException
*/
public static function default(): FreshRSS_UserConfiguration {
/** @var FreshRSS_UserConfiguration|null $default_user_conf */
static $default_user_conf = null;
if ($default_user_conf == null) {
if ($default_user_conf === null) {
$namespace = 'user_default';
FreshRSS_UserConfiguration::register($namespace, '_', FRESHRSS_PATH . '/config-user.default.php');
$default_user_conf = FreshRSS_UserConfiguration::get($namespace);

View File

@@ -8,6 +8,9 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
try {
$sql = $GLOBALS['SQL_CREATE_TABLES'];
if (!is_string($sql)) {
throw new Exception('SQL_CREATE_TABLES is not a string!');
}
$ok = $this->pdo->exec($sql) !== false; //Note: Only exec() can take multiple statements safely.
} catch (Exception $e) {
$ok = false;
@@ -29,7 +32,11 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
}
require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
$ok = $this->pdo->exec($GLOBALS['SQL_DROP_TABLES']) !== false;
$sql = $GLOBALS['SQL_DROP_TABLES'];
if (!is_string($sql)) {
throw new Exception('SQL_DROP_TABLES is not a string!');
}
$ok = $this->pdo->exec($sql) !== false;
if ($ok) {
$this->close();

View File

@@ -10,7 +10,7 @@ class FreshRSS_View extends Minz_View {
public $callbackBeforeFeeds;
/** @var callable */
public $callbackBeforePagination;
/** @var array<int,FreshRSS_Category> */
/** @var list<FreshRSS_Category> */
public array $categories;
public ?FreshRSS_Category $category = null;
public ?FreshRSS_Tag $tag = null;
@@ -19,12 +19,12 @@ class FreshRSS_View extends Minz_View {
public $entries;
public ?FreshRSS_Entry $entry = null;
public ?FreshRSS_Feed $feed = null;
/** @var array<int,FreshRSS_Feed> */
/** @var list<FreshRSS_Feed> */
public array $feeds;
public int $nbUnreadTags;
/** @var array<int,FreshRSS_Tag> */
/** @var list<FreshRSS_Tag> */
public array $tags;
/** @var array<int,array{'id':int,'name':string,'id_entry':string,'checked':bool}> */
/** @var array<int,array{id:int,name:string,checked:bool}> */
public array $tagsForEntry;
/** @var array<string,array<string>> */
public array $tagsForEntries;
@@ -37,12 +37,12 @@ class FreshRSS_View extends Minz_View {
public bool $signalError;
// Manage users
/** @var array{'feed_count':int,'article_count':int,'database_size':int,'language':string,'mail_login':string,'enabled':bool,'is_admin':bool,'last_user_activity':string,'is_default':bool} */
/** @var array{feed_count:int,article_count:int,database_size:int,language:string,mail_login:string,enabled:bool,is_admin:bool,last_user_activity:string,is_default:bool} */
public array $details;
public bool $disable_aside;
public bool $show_email_field;
public string $username;
/** @var array<array{'language':string,'enabled':bool,'is_admin':bool,'enabled':bool,'article_count':int,'database_size':int,'last_user_activity':string,'mail_login':string,'feed_count':int,'is_default':bool}> */
/** @var array<array{language:string,enabled:bool,is_admin:bool,enabled:bool,article_count:int,database_size:int,last_user_activity:string,mail_login:string,feed_count:int,is_default:bool}> */
public array $users;
// Updates
@@ -62,7 +62,7 @@ class FreshRSS_View extends Minz_View {
public int $size_user;
// Display
/** @var array<string,array{'id':string,'name':string,'author':string,'description':string,'version':float|string,'files':array<string>,'theme-color'?:string|array{'dark'?:string,'light'?:string,'default'?:string}}> */
/** @var array<string,array{id:string,name:string,author:string,description:string,version:float|string,files:array<string>,theme-color?:string|array{dark?:string,light?:string,default?:string}}> */
public array $themes;
// Shortcuts
@@ -118,10 +118,10 @@ class FreshRSS_View extends Minz_View {
public bool $selectorSuccess;
// Extensions
/** @var array<array{'name':string,'author':string,'description':string,'version':string,'entrypoint':string,'type':'system'|'user','url':string,'method':string,'directory':string}> */
/** @var array<array{name:string,author:string,description:string,version:string,entrypoint:string,type:'system'|'user',url:string,method:string,directory:string}> */
public array $available_extensions;
public ?Minz_Extension $ext_details = null;
/** @var array{'system':array<Minz_Extension>,'user':array<Minz_Extension>} */
/** @var array{system:array<Minz_Extension>,user:array<Minz_Extension>} */
public array $extension_list;
public ?Minz_Extension $extension = null;
/** @var array<string,string> */

View File

@@ -3,11 +3,11 @@ declare(strict_types=1);
final class FreshRSS_ViewJavascript extends FreshRSS_View {
/** @var array<int,FreshRSS_Category> */
/** @var list<FreshRSS_Category> */
public array $categories;
/** @var array<int,FreshRSS_Feed> */
/** @var list<FreshRSS_Feed> */
public array $feeds;
/** @var array<int,FreshRSS_Tag> */
/** @var list<FreshRSS_Tag> */
public array $tags;
public string $nonce;

View File

@@ -3,10 +3,10 @@ declare(strict_types=1);
final class FreshRSS_ViewStats extends FreshRSS_View {
/** @var array<int,FreshRSS_Category> */
/** @var list<FreshRSS_Category> */
public array $categories;
public ?FreshRSS_Feed $feed = null;
/** @var array<int,FreshRSS_Feed> */
/** @var list<FreshRSS_Feed> */
public array $feeds;
public bool $displaySlider = false;
@@ -14,7 +14,7 @@ final class FreshRSS_ViewStats extends FreshRSS_View {
public float $averageDayOfWeek;
public float $averageHour;
public float $averageMonth;
/** @var array<string> */
/** @var list<string> */
public array $days;
/** @var array<string,array<int,int|string>> */
public array $entryByCategory;
@@ -30,11 +30,11 @@ final class FreshRSS_ViewStats extends FreshRSS_View {
public array $last30DaysLabel;
/** @var array<int,string> */
public array $last30DaysLabels;
/** @var array<string,string> */
/** @var list<string> */
public array $months;
/** @var array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false */
/** @var array{total:int,count_unreads:int,count_reads:int,count_favorites:int}|false */
public $repartition;
/** @var array{'main_stream':array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false,'all_feeds':array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false} */
/** @var array{main_stream:array{total:int,count_unreads:int,count_reads:int,count_favorites:int}|false,all_feeds:array{total:int,count_unreads:int,count_reads:int,count_favorites:int}|false} */
public array $repartitions;
/** @var array<int,int> */
public array $repartitionDayOfWeek;
@@ -42,6 +42,6 @@ final class FreshRSS_ViewStats extends FreshRSS_View {
public array $repartitionHour;
/** @var array<int,int> */
public array $repartitionMonth;
/** @var array<array{'id':int,'name':string,'category':string,'count':int}> */
/** @var list<array{id:int,name:string,category:string,count:int}> */
public array $topFeed;
}

View File

@@ -65,6 +65,7 @@ final class FreshRSS_dotNotation_Util
* Determine if the given key exists in the provided array.
*
* @param \ArrayAccess<string,mixed>|array<string,mixed>|mixed $array
* @phpstan-assert-if-true \ArrayAccess<string,mixed>|array<string,mixed> $array
*/
private static function exists($array, string $key): bool {
if ($array instanceof \ArrayAccess) {
@@ -85,7 +86,7 @@ final class FreshRSS_dotNotation_Util
* mapping fields from the JSON object into RSS equivalents
* according to the dot-separated paths
*
* @param array<string> $jf json feed
* @param array<int|string,mixed> $jf json feed
* @param string $feedSourceUrl the source URL for the feed
* @param array<string,string> $dotNotation dot notation to map JSON into RSS
* @param string $defaultRssTitle Default title of the RSS feed, if not already provided in dotNotation `feedTitle`

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env php
<?php
// declare(strict_types=1); // Need to wait for PHP 8+ due to https://php.net/ob-implicit-flush
declare(strict_types=1);
require(__DIR__ . '/../cli/_cli.php');
session_cache_limiter('');

View File

@@ -10,7 +10,7 @@ require(LIB_PATH . '/lib_install.php');
Minz_Session::init('FreshRSS');
if (isset($_GET['step'])) {
if (isset($_GET['step']) && is_numeric($_GET['step'])) {
define('STEP', (int)$_GET['step']);
} else {
define('STEP', 0);
@@ -41,7 +41,7 @@ function initTranslate(): void {
}
function get_best_language(): string {
$accept = empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? '' : $_SERVER['HTTP_ACCEPT_LANGUAGE'];
$accept = empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) || !is_string($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? '' : $_SERVER['HTTP_ACCEPT_LANGUAGE'];
return strtolower(substr($accept, 0, 2));
}
@@ -102,19 +102,22 @@ function saveStep2(): void {
'bd_prefix' => false,
]);
} else {
if (empty($_POST['type']) ||
empty($_POST['host']) ||
empty($_POST['user']) ||
empty($_POST['base'])) {
if (empty($_POST['type']) || !is_string($_POST['type']) ||
empty($_POST['host']) || !is_string($_POST['host']) ||
empty($_POST['user']) || !is_string($_POST['user']) ||
empty($_POST['base']) || !is_string($_POST['base']) ||
!is_string($_POST['pass'] ?? null) || !is_string($_POST['prefix'] ?? null)
) {
Minz_Session::_param('bd_error', 'Missing parameters!');
}
Minz_Session::_params([
} else {
Minz_Session::_params([
'bd_base' => substr($_POST['base'], 0, 64),
'bd_host' => $_POST['host'],
'bd_user' => $_POST['user'],
'bd_password' => $_POST['pass'],
'bd_prefix' => substr($_POST['prefix'], 0, 16),
]);
}
}
// We use dirname to remove the /i part
@@ -143,6 +146,9 @@ function saveStep2(): void {
$customConfig = include($customConfigPath);
if (is_array($customConfig)) {
$config_array = array_merge($customConfig, $config_array);
if (!is_string($config_array['default_user'] ?? null)) {
$config_array['default_user'] = '_';
}
}
}
@@ -157,6 +163,9 @@ function saveStep2(): void {
$ok = false;
try {
if (!is_string($config_array['default_user'])) {
throw new Exception('Invalid default user name');
}
Minz_User::change($config_array['default_user']);
$error = initDb();
Minz_User::change();
@@ -327,11 +336,11 @@ function checkStep3(): array {
$form = Minz_Session::paramString('auth_type') != '';
$defaultUser = empty($_POST['default_user']) ? null : $_POST['default_user'];
if ($defaultUser === null) {
$defaultUser = is_string($_POST['default_user'] ?? null) ? trim($_POST['default_user']) : '';
if ($defaultUser === '') {
$defaultUser = Minz_Session::paramString('default_user') == '' ? '' : Minz_Session::paramString('default_user');
}
$data = is_writable(join_path(USERS_PATH, $defaultUser, 'config.php'));
$data = is_writable(USERS_PATH . '/' . $defaultUser . '/config.php');
return [
'conf' => $conf ? 'ok' : 'ko',
@@ -445,16 +454,15 @@ function getProcessUsername(): string {
/* check system environment */
function printStep1(): void {
$res = checkRequirements();
$processUsername = getProcessUsername();
?>
<h2><?= _t('admin.check_install.php') ?></h2>
<noscript><p class="alert alert-warn"><span class="alert-head"><?= _t('gen.short.attention') ?></span> <?= _t('install.javascript_is_better') ?></p></noscript>
<?php
$version = function_exists('curl_version') ? curl_version() : [];
printStep1Template('php', $res['php'], [PHP_VERSION, FRESHRSS_MIN_PHP_VERSION]);
printStep1Template('pdo', $res['pdo']);
printStep1Template('curl', $res['curl'], [$version['version'] ?? '']);
$curlVersion = function_exists('curl_version') ? curl_version() : [];
$curlVersion = is_string($curlVersion['version'] ?? null) ? $curlVersion['version'] : '';
printStep1Template('curl', $res['curl'], [$curlVersion]);
printStep1Template('json', $res['json']);
printStep1Template('pcre', $res['pcre']);
printStep1Template('ctype', $res['ctype']);
@@ -465,6 +473,7 @@ function printStep1(): void {
?>
<h2><?= _t('admin.check_install.files') ?></h2>
<?php
$processUsername = getProcessUsername();
printStep1Template('data', $res['data'], [DATA_PATH, $processUsername]);
printStep1Template('cache', $res['cache'], [CACHE_PATH, $processUsername]);
printStep1Template('tmp', $res['tmp'], [TMP_PATH, $processUsername]);
@@ -516,7 +525,7 @@ function printStep2(): void {
<p class="alert alert-success"><span class="alert-head"><?= _t('gen.short.ok') ?></span> <?= _t('install.bdd.conf.ok') ?></p>
<?php } elseif ($s2['conn'] == 'ko') { ?>
<p class="alert alert-error"><span class="alert-head"><?= _t('gen.short.damn') ?></span> <?= _t('install.bdd.conf.ko'),
(empty($_SESSION['bd_error']) ? '' : ' : ' . $_SESSION['bd_error']) ?></p>
(empty($_SESSION['bd_error']) || !is_string($_SESSION['bd_error']) ? '' : ' : ' . $_SESSION['bd_error']) ?></p>
<?php } ?>
<h2><?= _t('install.bdd.conf') ?></h2>
@@ -527,19 +536,19 @@ function printStep2(): void {
<select name="type" id="type" tabindex="1">
<?php if (extension_loaded('pdo_sqlite')) {?>
<option value="sqlite"
<?= isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'sqlite' ? 'selected="selected"' : '' ?>>
<?= ($_SESSION['bd_type'] ?? null) === 'sqlite' ? 'selected="selected"' : '' ?>>
SQLite
</option>
<?php }?>
<?php if (extension_loaded('pdo_mysql')) {?>
<option value="mysql"
<?= isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'mysql' ? 'selected="selected"' : '' ?>>
<?= ($_SESSION['bd_type'] ?? null) === 'mysql' ? 'selected="selected"' : '' ?>>
MySQL / MariaDB
</option>
<?php }?>
<?php if (extension_loaded('pdo_pgsql')) {?>
<option value="pgsql"
<?= isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'pgsql' ? 'selected="selected"' : '' ?>>
<?= ($_SESSION['bd_type'] ?? null) === 'pgsql' ? 'selected="selected"' : '' ?>>
PostgreSQL
</option>
<?php }?>
@@ -548,11 +557,18 @@ function printStep2(): void {
</div>
<div id="mysql">
<?php
$bd_base = is_string($_SESSION['bd_base'] ?? null) ? $_SESSION['bd_base'] : null;
$bd_host = is_string($_SESSION['bd_host'] ?? null) ? $_SESSION['bd_host'] : null;
$bd_password = is_string($_SESSION['bd_password'] ?? null) ? $_SESSION['bd_password'] : null;
$bd_prefix = is_string($_SESSION['bd_prefix'] ?? null) ? $_SESSION['bd_prefix'] : null;
$bd_user = is_string($_SESSION['bd_user'] ?? null) ? $_SESSION['bd_user'] : null;
?>
<div class="form-group">
<label class="group-name" for="host"><?= _t('install.bdd.host') ?></label>
<div class="group-controls">
<input type="text" id="host" name="host" pattern="[0-9A-Z\/a-z_.\-]{1,64}(:[0-9]{2,5})?" value="<?=
$_SESSION['bd_host'] ?? $system_default_config->db['host'] ?? '' ?>" tabindex="2" />
$bd_host ?? $system_default_config->db['host'] ?? '' ?>" tabindex="2" />
</div>
</div>
@@ -560,7 +576,7 @@ function printStep2(): void {
<label class="group-name" for="user"><?= _t('install.bdd.username') ?></label>
<div class="group-controls">
<input type="text" id="user" name="user" maxlength="64" pattern="[0-9A-Za-z@_.\-]{1,64}" value="<?=
$_SESSION['bd_user'] ?? '' ?>" tabindex="3" />
$bd_user ?? '' ?>" tabindex="3" />
</div>
</div>
@@ -569,7 +585,7 @@ function printStep2(): void {
<div class="group-controls">
<div class="stick">
<input type="password" id="pass" name="pass" value="<?=
$_SESSION['bd_password'] ?? '' ?>" tabindex="4" autocomplete="off" />
$bd_password ?? '' ?>" tabindex="4" autocomplete="off" />
<a class="btn toggle-password" data-toggle="pass" tabindex="5"><?= FreshRSS_Themes::icon('key') ?></a>
</div>
</div>
@@ -579,7 +595,7 @@ function printStep2(): void {
<label class="group-name" for="base"><?= _t('install.bdd') ?></label>
<div class="group-controls">
<input type="text" id="base" name="base" maxlength="64" pattern="[0-9A-Za-z_\-]{1,64}" value="<?=
$_SESSION['bd_base'] ?? '' ?>" tabindex="6" />
$bd_base ?? '' ?>" tabindex="6" />
</div>
</div>
@@ -587,7 +603,7 @@ function printStep2(): void {
<label class="group-name" for="prefix"><?= _t('install.bdd.prefix') ?></label>
<div class="group-controls">
<input type="text" id="prefix" name="prefix" maxlength="16" pattern="[0-9A-Za-z_]{1,16}" value="<?=
$_SESSION['bd_prefix'] ?? $system_default_config->db['prefix'] ?? '' ?>" tabindex="7" />
$bd_prefix ?? $system_default_config->db['prefix'] ?? '' ?>" tabindex="7" />
</div>
</div>
</div>
@@ -611,7 +627,8 @@ function no_auth(string $auth_type): bool {
/* Create default user */
function printStep3(): void {
$auth_type = $_SESSION['auth_type'] ?? '';
$auth_type = is_string($_SESSION['auth_type'] ?? null) ? $_SESSION['auth_type'] : '';
$default_user = is_string($_SESSION['default_user'] ?? null) ? $_SESSION['default_user'] : '';
$s3 = checkStep3();
if ($s3['all'] == 'ok') { ?>
<p class="alert alert-success"><span class="alert-head"><?= _t('gen.short.ok') ?></span> <?= _t('install.conf.ok') ?></p>
@@ -625,7 +642,7 @@ function printStep3(): void {
<label class="group-name" for="default_user"><?= _t('install.default_user') ?></label>
<div class="group-controls">
<input type="text" id="default_user" name="default_user" autocomplete="username" required="required" size="16"
pattern="<?= FreshRSS_user_Controller::USERNAME_PATTERN ?>" value="<?= $_SESSION['default_user'] ?? '' ?>"
pattern="<?= FreshRSS_user_Controller::USERNAME_PATTERN ?>" value="<?= $default_user ?>"
placeholder="<?= httpAuthUser(false) == '' ? 'alice' : httpAuthUser(false) ?>" tabindex="1" />
<p class="help"><?= _i('help') ?> <?= _t('install.default_user.max_char') ?></p>
</div>

View File

@@ -7,7 +7,7 @@ header('Content-Type: application/json; charset=UTF-8');
$url = [
'c' => Minz_Request::controllerName(),
'a' => Minz_Request::actionName(),
'params' => $_GET,
'params' => array_filter($_GET, 'is_string', ARRAY_FILTER_USE_KEY),
];
$url['params']['is_favorite'] = (Minz_Request::paramTernary('is_favorite') ?? true) ? '0' : '1';

View File

@@ -116,7 +116,7 @@
<legend><?= _t('sub.category.archiving') ?></legend>
<?php
$archiving = $this->category->attributeArray('archiving');
/** @var array<'default'?:bool,'keep_period'?:string,'keep_max'?:int,'keep_min'?:int,'keep_favourites'?:bool,'keep_labels'?:bool,'keep_unreads'?:bool>|null $archiving */
/** @var array{default?:bool,keep_period?:string,keep_max?:int,keep_min?:int,keep_favourites?:bool,keep_labels?:bool,keep_unreads?:bool}|null $archiving */
if (empty($archiving)) {
$archiving = [ 'default' => true ];
} else {

View File

@@ -3,7 +3,7 @@ declare(strict_types=1);
/**
* @param array<FreshRSS_Feed> $feeds
* @return array<array<string,string|bool|int>>
* @return list<array<string,string|bool|int>>
*/
function feedsToOutlines(array $feeds, bool $excludeMutedFeeds = false): array {
$outlines = [];
@@ -112,7 +112,9 @@ function feedsToOutlines(array $feeds, bool $excludeMutedFeeds = false): array {
if (!empty($curl_params[CURLOPT_HTTPHEADER]) && is_array($curl_params[CURLOPT_HTTPHEADER])) {
$headers = '';
foreach ($curl_params[CURLOPT_HTTPHEADER] as $header) {
$headers .= $header . "\n";
if (is_string($header)) {
$headers .= $header . "\n";
}
}
$headers = trim($headers);
$outline['frss:CURLOPT_HTTPHEADER'] = $headers;

View File

@@ -305,7 +305,7 @@
</div>
<?php
$archiving = $this->feed->attributeArray('archiving');
/** @var array<'default'?:bool,'keep_period'?:string,'keep_max'?:int,'keep_min'?:int,'keep_favourites'?:bool,'keep_labels'?:bool,'keep_unreads'?:bool>|null $archiving */
/** @var array{default?:bool,keep_period?:string,keep_max?:int,keep_min?:int,keep_favourites?:bool,keep_labels?:bool,keep_unreads?:bool}|null $archiving */
if (empty($archiving)) {
$archiving = [ 'default' => true ];
} else {

View File

@@ -3,7 +3,7 @@
/** @var FreshRSS_View $this */
$c = Minz_Request::controllerName();
$a = Minz_Request::actionName();
$params = $_GET;
$params = array_filter($_GET, 'is_string', ARRAY_FILTER_USE_KEY);
?>
<?php if ($this->nbPage > 1) { ?>
<nav class="nav-pagination nav-list">

View File

@@ -39,7 +39,7 @@
<main id="stream" class="global<?= $class ?>">
<h1 class="title_hidden"><?= _t('conf.reading.view.global') ?></h1>
<?php
$params = $_GET;
$params = array_filter($_GET, 'is_string', ARRAY_FILTER_USE_KEY);
unset($params['c']);
unset($params['a']);
$url_base = [

View File

@@ -11,14 +11,14 @@
<h1><?= _t('index.log') ?></h1>
<?php
/** @var array<FreshRSS_Log> $items */
/** @var list<FreshRSS_Log> $items */
$items = $this->logsPaginator->items();
?>
<?php if (!empty($items)) { ?>
<form method="post" action="<?= _url('index', 'logs') ?>">
<?php $this->logsPaginator->render('logs_pagination.phtml', 'page'); ?>
<div id="loglist-wrapper" class="table-wrapper scrollbar-thin">
<table id="loglist">
<thead>
@@ -46,7 +46,7 @@
</table>
</div>
<?php $this->logsPaginator->render('logs_pagination.phtml', 'page'); ?>
<div class="form-group form-actions">
<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
<input type="hidden" name="clearLogs" />

View File

@@ -102,7 +102,7 @@
* Generate a color palette.
*
* @param int $count The number of colors to generate.
* @return array<int, string> An array of HSL color strings.
* @return array<int,string> An array of HSL color strings.
*/
function generateColorPalette(int $count): array {
$colors = [];

View File

@@ -129,7 +129,7 @@ abstract class CliOptionsParser {
/**
* @param array<string> $userInputs
* @return array<string>
* @return list<string>
*/
private function getAliasesUsed(array $userInputs, string $regex): array {
$foundAliases = [];

View File

@@ -52,7 +52,8 @@ function accessRights(): void {
function done(bool $ok = true): never {
if (!$ok) {
fwrite(STDERR, (empty($_SERVER['argv'][0]) ? 'Process' : basename($_SERVER['argv'][0])) . ' failed!' . "\n");
fwrite(STDERR, (isset($_SERVER['argv']) && is_array($_SERVER['argv']) && !empty($_SERVER['argv'][0]) && is_string($_SERVER['argv'][0]) ?
basename($_SERVER['argv'][0]) : 'Process') . ' failed!' . "\n");
}
exit($ok ? 0 : 1);
}

View File

@@ -82,7 +82,7 @@ if (!$isValidated) {
* Iterates through all php and phtml files in the whole project and extracts all
* translation keys used.
*
* @return array<string>
* @return list<string>
*/
function findUsedTranslations(): array {
$directory = new RecursiveDirectoryIterator(__DIR__ . '/..');
@@ -90,6 +90,9 @@ function findUsedTranslations(): array {
$regex = new RegexIterator($iterator, '/^.+\.(php|phtml)$/i', RecursiveRegexIterator::GET_MATCH);
$usedI18n = [];
foreach (array_keys(iterator_to_array($regex)) as $file) {
if (!is_string($file) || $file === '') {
continue;
}
$fileContent = file_get_contents($file);
if ($fileContent === false) {
continue;

View File

@@ -34,7 +34,7 @@ try {
$_SESSION['bd_error'] = $ex->getMessage();
}
if (!$ok) {
fail('FreshRSS database error: ' . (empty($_SESSION['bd_error']) ? 'Unknown error' : $_SESSION['bd_error']));
fail('FreshRSS database error: ' . (is_string($_SESSION['bd_error'] ?? null) ? $_SESSION['bd_error'] : 'Unknown error'));
}
foreach (listUsers() as $username) {

View File

@@ -100,7 +100,7 @@ $config = [
$customConfigPath = DATA_PATH . '/config.custom.php';
if (file_exists($customConfigPath)) {
$customConfig = include($customConfigPath);
if (is_array($customConfig)) {
if (is_array($customConfig) && is_array_keys_string($customConfig)) {
$config = array_merge($customConfig, $config);
}
}
@@ -132,8 +132,14 @@ if ((!empty($config['base_url'])) && is_string($config['base_url']) && Minz_Requ
$config['pubsubhubbub_enabled'] = true;
}
if (!is_array($config['db'])) {
$config['db'] = [];
}
$config['db'] = array_merge($config['db'], array_filter($dbValues, static fn($value) => $value !== null));
if (!is_string($config['db']['type'] ?? null)) {
$config['db']['type'] = '';
}
performRequirementCheck($config['db']['type']);
if (file_put_contents(join_path(DATA_PATH, 'config.php'),
@@ -162,9 +168,12 @@ try {
if (!$ok) {
@unlink(join_path(DATA_PATH, 'config.php'));
fail('FreshRSS database error: ' . (empty($_SESSION['bd_error']) ? 'Unknown error' : $_SESSION['bd_error']));
fail('FreshRSS database error: ' . (is_string($_SESSION['bd_error'] ?? null) ? $_SESSION['bd_error'] : 'Unknown error'));
}
if (!is_string($config['default_user'] ?? null)) {
fail('FreshRSS default user not set!');
}
echo ' Remember to create the default user: ', $config['default_user'],
"\t", './cli/create-user.php --user ', $config['default_user'], " --password 'password' --more-options\n";

View File

@@ -3,6 +3,7 @@ declare(strict_types=1);
class I18nData {
/** @var string */
public const REFERENCE_LANGUAGE = 'en';
/** @param array<string,array<string,array<string,I18nValue>>> $data */
@@ -74,22 +75,21 @@ class I18nData {
/**
* Return the available languages
* @return array<string>
* @return list<string>
*/
public function getAvailableLanguages(): array {
$languages = array_keys($this->data);
sort($languages);
return $languages;
}
/**
* Return all available languages without the reference language
* @return array<string>
* @return list<string>
*/
private function getNonReferenceLanguages(): array {
return array_filter(array_keys($this->data),
static fn(string $value) => static::REFERENCE_LANGUAGE !== $value);
return array_values(array_filter(array_keys($this->data),
static fn(string $value) => static::REFERENCE_LANGUAGE !== $value));
}
/**
@@ -129,7 +129,7 @@ class I18nData {
* Return the siblings for a specified key.
* To get the siblings, we need to find all matches with the parent.
*
* @return array<string>
* @return list<string>
*/
private function getSiblings(string $key): array {
if (!array_key_exists($this->getFilenamePrefix($key), $this->data[static::REFERENCE_LANGUAGE])) {

View File

@@ -4,6 +4,23 @@ declare(strict_types=1);
require_once __DIR__ . '/I18nValue.php';
class I18nFile {
/**
* @param array<mixed,mixed> $array
* @phpstan-assert-if-true array<string,string|array<string,mixed>> $array
*/
public static function is_array_recursive_string(array $array): bool {
foreach ($array as $key => $value) {
if (!is_string($key)) {
return false;
}
if (!is_string($value) && !(is_array($value) && self::is_array_recursive_string($value))) {
return false;
}
}
return true;
}
/**
* @return array<string,array<string,array<string,I18nValue>>>
*/
@@ -45,7 +62,7 @@ class I18nFile {
/**
* Process the content of an i18n file
* @return array<string,array<string,I18nValue>>
* @return array<string,string|array<string,mixed>>
*/
private function process(string $filename): array {
$fileContent = file_get_contents($filename) ?: [];
@@ -71,7 +88,7 @@ class I18nFile {
die(1);
}
if (is_array($content)) {
if (is_array($content) && self::is_array_recursive_string($content)) {
return $content;
}
@@ -81,7 +98,7 @@ class I18nFile {
/**
* Flatten an array of translation
*
* @param array<string,I18nValue|array<string,I18nValue>> $translation
* @param array<string,I18nValue|string|array<string,I18nValue>|mixed> $translation
* @return array<string,I18nValue>
*/
private function flatten(array $translation, string $prefix = ''): array {
@@ -92,9 +109,9 @@ class I18nFile {
}
foreach ($translation as $key => $value) {
if (is_array($value)) {
if (is_array($value) && is_array_keys_string($value)) {
$a += $this->flatten($value, $prefix . $key);
} else {
} elseif (is_string($value) || $value instanceof I18nValue) {
$a[$prefix . $key] = new I18nValue($value);
}
}

View File

@@ -57,9 +57,9 @@
"ext-phar": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
"phpstan/phpstan": "^1.12",
"phpstan/phpstan-phpunit": "^1.4",
"phpstan/phpstan-strict-rules": "^1.6",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^10",
"squizlabs/php_codesniffer": "^3.11"
},
@@ -68,8 +68,8 @@
"phtml-lint": "find . -type d -name 'vendor' -prune -o -name '*.phtml' -print0 | xargs -0 -n1 -P4 php -l 1>/dev/null",
"phpcs": "phpcs . -s",
"phpcbf": "phpcbf . -p -s",
"phpstan": "phpstan analyse --memory-limit 512M .",
"phpstan-next": "phpstan analyse --memory-limit 512M -c phpstan-next.neon .",
"phpstan": "phpstan analyse .",
"phpstan-next": "phpstan analyse -c phpstan-next.neon .",
"phpunit": "phpunit --bootstrap ./tests/bootstrap.php --display-notices --display-phpunit-deprecations ./tests",
"translations": "cli/manipulate.translation.php -a format",
"test": [
@@ -77,7 +77,8 @@
"@phtml-lint",
"@phpunit",
"@phpcs",
"@phpstan"
"@phpstan",
"@phpstan-next"
],
"fix": [
"@translations",

82
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c334a3d4e7b54d959f917e9f171db65f",
"content-hash": "4cf78584eba0020d488de0659d55923b",
"packages": [],
"packages-dev": [
{
@@ -245,20 +245,20 @@
},
{
"name": "phpstan/phpstan",
"version": "1.12.12",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0"
"reference": "50d276fc3bf1430ec315f2f109bbde2769821524"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0",
"reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/50d276fc3bf1430ec315f2f109bbde2769821524",
"reference": "50d276fc3bf1430ec315f2f109bbde2769821524",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0"
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
@@ -299,34 +299,33 @@
"type": "github"
}
],
"time": "2024-11-28T22:13:23+00:00"
"time": "2024-12-17T17:14:01+00:00"
},
{
"name": "phpstan/phpstan-phpunit",
"version": "1.4.1",
"version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-phpunit.git",
"reference": "11d4235fbc6313ecbf93708606edfd3222e44949"
"reference": "e32ac656788a5bf3dedda89e6a2cad5643bf1a18"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/11d4235fbc6313ecbf93708606edfd3222e44949",
"reference": "11d4235fbc6313ecbf93708606edfd3222e44949",
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/e32ac656788a5bf3dedda89e6a2cad5643bf1a18",
"reference": "e32ac656788a5bf3dedda89e6a2cad5643bf1a18",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.12"
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.0.4"
},
"conflict": {
"phpunit/phpunit": "<7.0"
},
"require-dev": {
"nikic/php-parser": "^4.13.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/phpstan-strict-rules": "^1.5.1",
"phpunit/phpunit": "^9.5"
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.6"
},
"type": "phpstan-extension",
"extra": {
@@ -349,34 +348,33 @@
"description": "PHPUnit extensions and rules for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-phpunit/issues",
"source": "https://github.com/phpstan/phpstan-phpunit/tree/1.4.1"
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.3"
},
"time": "2024-11-12T12:43:59+00:00"
"time": "2024-12-19T09:14:43+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
"version": "1.6.1",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
"reference": "daeec748b53de80a97498462513066834ec28f8b"
"reference": "ed6fea0ad4ad9c7e25f3ad2e7c4d420cf1e67fe3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/daeec748b53de80a97498462513066834ec28f8b",
"reference": "daeec748b53de80a97498462513066834ec28f8b",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/ed6fea0ad4ad9c7e25f3ad2e7c4d420cf1e67fe3",
"reference": "ed6fea0ad4ad9c7e25f3ad2e7c4d420cf1e67fe3",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.12.4"
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.0.4"
},
"require-dev": {
"nikic/php-parser": "^4.13.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/phpstan-deprecation-rules": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^9.5"
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^9.6"
},
"type": "phpstan-extension",
"extra": {
@@ -398,9 +396,9 @@
"description": "Extra strict and opinionated rules for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.1"
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.1"
},
"time": "2024-09-20T14:04:44+00:00"
"time": "2024-12-12T20:21:10+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -725,16 +723,16 @@
},
{
"name": "phpunit/phpunit",
"version": "10.5.38",
"version": "10.5.39",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132"
"reference": "4e89eff200b801db58f3d580ad7426431949eaa9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a86773b9e887a67bc53efa9da9ad6e3f2498c132",
"reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4e89eff200b801db58f3d580ad7426431949eaa9",
"reference": "4e89eff200b801db58f3d580ad7426431949eaa9",
"shasum": ""
},
"require": {
@@ -744,7 +742,7 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.12.0",
"myclabs/deep-copy": "^1.12.1",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.1",
@@ -806,7 +804,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.38"
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.39"
},
"funding": [
{
@@ -822,7 +820,7 @@
"type": "tidelift"
}
],
"time": "2024-10-28T13:06:21+00:00"
"time": "2024-12-11T10:51:07+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -1742,16 +1740,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.11.1",
"version": "3.11.2",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
"reference": "19473c30efe4f7b3cd42522d0b2e6e7f243c6f87"
"reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/19473c30efe4f7b3cd42522d0b2e6e7f243c6f87",
"reference": "19473c30efe4f7b3cd42522d0b2e6e7f243c6f87",
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1368f4a58c3c52114b86b1abe8f4098869cb0079",
"reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079",
"shasum": ""
},
"require": {
@@ -1818,7 +1816,7 @@
"type": "open_collective"
}
],
"time": "2024-11-16T12:02:36+00:00"
"time": "2024-12-11T16:04:26+00:00"
},
{
"name": "theseer/tokenizer",

View File

@@ -3,26 +3,38 @@ declare(strict_types=1);
//NB: Do not edit; use ./constants.local.php instead.
//<Not customisable>
/** @var string */
const FRESHRSS_MIN_PHP_VERSION = '8.1.0';
/** @var string */
const FRESHRSS_VERSION = '1.25.1-dev';
/** @var string */
const FRESHRSS_WEBSITE = 'https://freshrss.org';
/** @var string */
const FRESHRSS_WIKI = 'https://freshrss.github.io/FreshRSS/';
/** @var string */
const APP_NAME = 'FreshRSS';
/** @var string */
const FRESHRSS_PATH = __DIR__;
/** @var string */
const PUBLIC_PATH = FRESHRSS_PATH . '/p';
/** @var string */
const PUBLIC_TO_INDEX_PATH = '/i';
/** @var string */
const INDEX_PATH = PUBLIC_PATH . PUBLIC_TO_INDEX_PATH;
/** @var string */
const PUBLIC_RELATIVE = '..';
/** @var string */
const LIB_PATH = FRESHRSS_PATH . '/lib';
/** @var string */
const APP_PATH = FRESHRSS_PATH . '/app';
/** @var string */
const I18N_PATH = APP_PATH . '/i18n';
/** @var string */
const CORE_EXTENSIONS_PATH = LIB_PATH . '/core-extensions';
/** @var string */
const TESTS_PATH = FRESHRSS_PATH . '/tests';
//</Not customisable>
if (file_exists(__DIR__ . '/constants.local.php')) {
//Include custom / local settings:
include(__DIR__ . '/constants.local.php');

View File

@@ -45,7 +45,7 @@ class Minz_Configuration {
*/
public static function load(string $filename): array {
$data = @include($filename);
if (is_array($data)) {
if (is_array($data) && is_array_keys_string($data)) {
return $data;
} else {
throw new Minz_FileNotExistException($filename);
@@ -117,9 +117,10 @@ class Minz_Configuration {
}
try {
$this->data = array_replace_recursive(
$overloaded = array_replace_recursive(
$this->data, self::load($this->config_filename)
);
$this->data = array_filter($overloaded, 'is_string', ARRAY_FILTER_USE_KEY);
} catch (Minz_FileNotExistException $e) {
if ($this->default_filename == null) {
throw $e;

View File

@@ -15,13 +15,13 @@ class Minz_Error {
/**
* Permet de lancer une erreur
* @param int $code le type de l'erreur, par défaut 404 (page not found)
* @param string|array<'error'|'warning'|'notice',array<string>> $logs logs d'erreurs découpés de la forme
* @param string|array<'error'|'warning'|'notice',list<string>> $logs logs d'erreurs découpés de la forme
* > $logs['error']
* > $logs['warning']
* > $logs['notice']
* @param bool $redirect indique s'il faut forcer la redirection (les logs ne seront pas transmis)
*/
public static function error(int $code = 404, $logs = [], bool $redirect = true): void {
public static function error(int $code = 404, string|array $logs = [], bool $redirect = true): void {
$logs = self::processLogs($logs);
$error_filename = APP_PATH . '/Controllers/errorController.php';
@@ -49,8 +49,8 @@ class Minz_Error {
/**
* Returns filtered logs
* @param string|array<'error'|'warning'|'notice',array<string>> $logs logs sorted by category (error, warning, notice)
* @return array<string> list of matching logs, without the category, according to environment preferences (production / development)
* @param string|array<'error'|'warning'|'notice',list<string>> $logs logs sorted by category (error, warning, notice)
* @return list<string> list of matching logs, without the category, according to environment preferences (production / development)
*/
private static function processLogs($logs): array {
if (is_string($logs)) {
@@ -61,13 +61,13 @@ class Minz_Error {
$warning = [];
$notice = [];
if (isset($logs['error']) && is_array($logs['error'])) {
if (is_array($logs['error'] ?? null)) {
$error = $logs['error'];
}
if (isset($logs['warning']) && is_array($logs['warning'])) {
if (is_array($logs['warning'] ?? null)) {
$warning = $logs['warning'];
}
if (isset($logs['notice']) && is_array($logs['notice'])) {
if (is_array($logs['notice'] ?? null)) {
$notice = $logs['notice'];
}

View File

@@ -26,7 +26,7 @@ abstract class Minz_Extension {
private bool $is_enabled;
/** @var string[] */
/** @var array<string,string> */
protected array $csp_policies = [];
/**
@@ -411,11 +411,11 @@ abstract class Minz_Extension {
}
/**
* @param string[] $policies
* @param array<string,string> $policies
*/
public function amendCsp(array &$policies): void {
foreach ($this->csp_policies as $policy => $source) {
if (array_key_exists($policy, $policies)) {
if (isset($policies[$policy])) {
$policies[$policy] .= ' ' . $source;
} else {
$policies[$policy] = $source;

View File

@@ -136,7 +136,6 @@ final class Minz_ExtensionManager {
array_walk($list_core_extensions, function (&$s) { $s = CORE_EXTENSIONS_PATH . '/' . $s; });
array_walk($list_thirdparty_extensions, function (&$s) { $s = THIRDPARTY_EXTENSIONS_PATH . '/' . $s; });
/** @var array<string> */
$list_potential_extensions = array_merge($list_core_extensions, $list_thirdparty_extensions);
$system_conf = Minz_Configuration::get('system');
@@ -403,7 +402,10 @@ final class Minz_ExtensionManager {
public static function callHookString(string $hook_name): string {
$result = '';
foreach (self::$hook_list[$hook_name]['list'] ?? [] as $function) {
$result = $result . call_user_func($function);
$return = call_user_func($function);
if (is_scalar($return)) {
$result .= $return;
}
}
return $result;
}

View File

@@ -42,7 +42,7 @@ class Minz_FrontController {
$url = Minz_Url::build();
$url['params'] = array_merge(
empty($url['params']) || !is_array($url['params']) ? [] : $url['params'],
$_POST
array_filter($_POST, 'is_string', ARRAY_FILTER_USE_KEY)
);
Minz_Request::forward($url);
} catch (Minz_Exception $e) {

View File

@@ -176,6 +176,7 @@ class Minz_Migrator
public function migrations(): array {
$migrations = $this->migrations;
uksort($migrations, 'strnatcmp');
/** @var array<string,callable> $migrations */
return $migrations;
}
@@ -237,7 +238,7 @@ class Minz_Migrator
* considered as successful. It is considered as good practice to return
* true on success though.
*
* @return array<string|bool> Return the results of each executed migration. If an
* @return array<string,bool|string> Return the results of each executed migration. If an
* exception was raised in a migration, its result is set to
* the exception message.
*/
@@ -251,7 +252,7 @@ class Minz_Migrator
try {
$migration_result = $callback();
$result[$version] = $migration_result;
$result[$version] = (bool)$migration_result;
} catch (Exception $e) {
$migration_result = false;
$result[$version] = $e->getMessage();

View File

@@ -40,7 +40,7 @@ class Minz_ModelArray {
if ($data === false) {
throw new Minz_PermissionDeniedException($this->filename);
} elseif (!is_array($data)) {
} elseif (!is_array($data) || !is_array_keys_string($data)) {
$data = [];
}
return $data;

View File

@@ -176,8 +176,8 @@ class Minz_ModelPdo {
/**
* @param array<string,int|string|null> $values
* @phpstan-return ($mode is PDO::FETCH_ASSOC ? array<array<string,int|string|null>>|null : array<int|string|null>|null)
* @return array<array<string,int|string|null>>|array<int|string|null>|null
* @phpstan-return ($mode is PDO::FETCH_ASSOC ? list<array<string,int|string|null>>|null : list<int|string|null>|null)
* @return list<array<string,int|string|null>>|list<int|string|null>|null
*/
private function fetchAny(string $sql, array $values, int $mode, int $column = 0): ?array {
$stm = $this->pdo->prepare($sql);
@@ -204,15 +204,15 @@ class Minz_ModelPdo {
switch ($mode) {
case PDO::FETCH_COLUMN:
$res = $stm->fetchAll(PDO::FETCH_COLUMN, $column);
/** @var list<int|string|null> $res */
break;
case PDO::FETCH_ASSOC:
default:
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
/** @var list<array<string,int|string|null>> $res */
break;
}
if ($res !== false) {
return $res;
}
return $res;
}
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 6);
@@ -231,7 +231,7 @@ class Minz_ModelPdo {
/**
* @param array<string,int|string|null> $values
* @return array<array<string,int|string|null>>|null
* @return list<array<string,int|string|null>>|null
*/
public function fetchAssoc(string $sql, array $values = []): ?array {
return $this->fetchAny($sql, $values, PDO::FETCH_ASSOC);
@@ -239,7 +239,7 @@ class Minz_ModelPdo {
/**
* @param array<string,int|string|null> $values
* @return array<int|string|null>|null
* @return list<int|string|null>|null
*/
public function fetchColumn(string $sql, int $column, array $values = []): ?array {
return $this->fetchAny($sql, $values, PDO::FETCH_COLUMN, $column);
@@ -257,6 +257,6 @@ class Minz_ModelPdo {
Minz_Log::error('SQL error ' . json_encode($stm->errorInfo()) . ' during ' . $sql);
return null;
}
return isset($columns[0]) ? (string)$columns[0] : null;
return is_scalar($columns[0] ?? null) ? (string)$columns[0] : null;
}
}

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
*/
class Minz_Paginator {
/**
* @var array<Minz_Model> tableau des éléments à afficher/gérer
* @var list<Minz_Model> tableau des éléments à afficher/gérer
*/
private array $items = [];
@@ -37,7 +37,7 @@ class Minz_Paginator {
/**
* Constructeur
* @param array<Minz_Model> $items les éléments à gérer
* @param list<Minz_Model> $items les éléments à gérer
*/
public function __construct(array $items) {
$this->_items($items);
@@ -116,10 +116,10 @@ class Minz_Paginator {
*/
/**
* @param bool $all si à true, retourne tous les éléments sans prendre en compte la pagination
* @return array<Minz_Model>
* @return list<Minz_Model>
*/
public function items(bool $all = false): array {
$array = array ();
$array = [];
$nbItems = $this->nbItems();
if ($nbItems <= $this->nbItemsPerPage || $all) {
@@ -129,9 +129,9 @@ class Minz_Paginator {
$counter = 0;
$i = 0;
foreach ($this->items as $key => $item) {
foreach ($this->items as $item) {
if ($i >= $begin) {
$array[$key] = $item;
$array[] = $item;
$counter++;
}
if ($counter >= $this->nbItemsPerPage) {
@@ -159,7 +159,7 @@ class Minz_Paginator {
/**
* SETTEURS
*/
/** @param array<Minz_Model> $items */
/** @param list<Minz_Model> $items */
public function _items(?array $items): void {
$this->items = $items ?? [];
$this->_nbPage();

View File

@@ -69,18 +69,33 @@ class Minz_Request {
if (empty(self::$params[$key]) || !is_array(self::$params[$key])) {
return [];
}
return $plaintext ? self::$params[$key] : Minz_Helper::htmlspecialchars_utf8(self::$params[$key]);
$result = [];
foreach (self::$params[$key] as $k => $v) {
if (is_string($v)) {
$result[$k] = $v;
} elseif (is_array($v)) {
$vs = [];
foreach ($v as $k2 => $v2) {
if (is_string($k2) && (is_string($v2) || is_int($v2) || is_bool($v2))) {
$vs[$k2] = $v2;
}
}
$result[$k] = $vs;
}
}
return $plaintext ? $result : Minz_Helper::htmlspecialchars_utf8($result);
}
/**
* @param bool $plaintext `true` to return special characters without any escaping (unsafe), `false` (default) to XML-encode them
* @return array<string>
* @return list<string>
*/
public static function paramArrayString(string $key, bool $plaintext = false): array {
if (empty(self::$params[$key]) || !is_array(self::$params[$key])) {
return [];
}
$result = array_filter(self::$params[$key], 'is_string');
$result = array_values($result);
return $plaintext ? $result : Minz_Helper::htmlspecialchars_utf8($result);
}
@@ -143,7 +158,7 @@ class Minz_Request {
* character is used to break the text into lines. This method is well suited to use
* to split textarea content.
* @param bool $plaintext `true` to return special characters without any escaping (unsafe), `false` (default) to XML-encode them
* @return array<string>
* @return list<string>
*/
public static function paramTextToArray(string $key, bool $plaintext = false): array {
if (isset(self::$params[$key]) && is_string(self::$params[$key])) {
@@ -214,7 +229,7 @@ class Minz_Request {
* Initialise la Request
*/
public static function init(): void {
self::_params($_GET);
self::_params(array_filter($_GET, 'is_string', ARRAY_FILTER_USE_KEY));
self::initJSON();
}
@@ -227,8 +242,8 @@ class Minz_Request {
* Return true if the request is over HTTPS, false otherwise (HTTP)
*/
public static function isHttps(): bool {
$header = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '';
if ('' != $header) {
$header = is_string($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_PROTO'] : '';
if ('' !== $header) {
return 'https' === strtolower($header);
}
return 'on' === ($_SERVER['HTTPS'] ?? '');
@@ -250,34 +265,37 @@ class Minz_Request {
}
private static function extractProtocol(): string {
if (self::isHttps()) {
return 'https';
}
return 'http';
return self::isHttps() ? 'https' : 'http';
}
private static function extractHost(): string {
if ('' != $host = ($_SERVER['HTTP_X_FORWARDED_HOST'] ?? '')) {
$host = is_string($_SERVER['HTTP_X_FORWARDED_HOST'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_HOST'] : '';
if ($host !== '') {
return parse_url("http://{$host}", PHP_URL_HOST) ?: 'localhost';
}
if ('' != $host = ($_SERVER['HTTP_HOST'] ?? '')) {
$host = is_string($_SERVER['HTTP_HOST'] ?? null) ? $_SERVER['HTTP_HOST'] : '';
if ($host !== '') {
// Might contain a port number, and mind IPv6 addresses
return parse_url("http://{$host}", PHP_URL_HOST) ?: 'localhost';
}
if ('' != $host = ($_SERVER['SERVER_NAME'] ?? '')) {
$host = is_string($_SERVER['SERVER_NAME'] ?? null) ? $_SERVER['SERVER_NAME'] : '';
if ($host !== '') {
return $host;
}
return 'localhost';
}
private static function extractPort(): int {
if ('' != $port = ($_SERVER['HTTP_X_FORWARDED_PORT'] ?? '')) {
$port = is_numeric($_SERVER['HTTP_X_FORWARDED_PORT'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_PORT'] : '';
if ($port !== '') {
return intval($port);
}
if ('' != $proto = ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) {
$proto = is_string($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_PROTO'] : '';
if ($proto !== '') {
return 'https' === strtolower($proto) ? 443 : 80;
}
if ('' != $port = ($_SERVER['SERVER_PORT'] ?? '')) {
$port = is_numeric($_SERVER['SERVER_PORT'] ?? null) ? $_SERVER['SERVER_PORT'] : '';
if ($port !== '') {
return intval($port);
}
return self::isHttps() ? 443 : 80;
@@ -294,15 +312,16 @@ class Minz_Request {
}
private static function extractPrefix(): string {
if ('' != $prefix = ($_SERVER['HTTP_X_FORWARDED_PREFIX'] ?? '')) {
$prefix = is_string($_SERVER['HTTP_X_FORWARDED_PREFIX'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_PREFIX'] : '';
if ($prefix !== '') {
return rtrim($prefix, '/ ');
}
return '';
}
private static function extractPath(): string {
$path = $_SERVER['REQUEST_URI'] ?? '';
if ($path != '') {
$path = is_string($_SERVER['REQUEST_URI'] ?? null) ? $_SERVER['REQUEST_URI'] : '';
if ($path !== '') {
$path = parse_url($path, PHP_URL_PATH) ?: '';
return substr($path, -1) === '/' ? rtrim($path, '/') : dirname($path);
}
@@ -356,7 +375,7 @@ class Minz_Request {
}
private static function requestId(): string {
if (empty($_GET['rid']) || !ctype_xdigit($_GET['rid'])) {
if (!is_string($_GET['rid'] ?? null) || !ctype_xdigit($_GET['rid'])) {
$_GET['rid'] = uniqid();
}
return $_GET['rid'];
@@ -476,7 +495,8 @@ class Minz_Request {
}
private static function extractContentType(): string {
return strtolower(trim($_SERVER['CONTENT_TYPE'] ?? ''));
$contentType = is_string($_SERVER['CONTENT_TYPE'] ?? null) ? $_SERVER['CONTENT_TYPE'] : '';
return strtolower(trim($contentType));
}
public static function isPost(): bool {
@@ -484,10 +504,11 @@ class Minz_Request {
}
/**
* @return array<string>
* @return list<string>
*/
public static function getPreferredLanguages(): array {
if (preg_match_all('/(^|,)\s*(?P<lang>[^;,]+)/', $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '', $matches) > 0) {
$acceptLanguage = is_string($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? null) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
if (preg_match_all('/(^|,)\s*(?P<lang>[^;,]+)/', $acceptLanguage, $matches) > 0) {
return $matches['lang'];
}
return ['en'];

View File

@@ -72,7 +72,13 @@ class Minz_Session {
if (empty($_SESSION[$key]) || !is_array($_SESSION[$key])) {
return [];
}
return $_SESSION[$key];
$result = [];
foreach ($_SESSION[$key] as $k => $v) {
if (is_string($v) || (is_array($v) && is_array_keys_string($v))) {
$result[$k] = $v;
}
}
return $result;
}
public static function paramTernary(string $key): ?bool {
@@ -97,10 +103,7 @@ class Minz_Session {
}
public static function paramInt(string $key): int {
if (!empty($_SESSION[$key])) {
return intval($_SESSION[$key]);
}
return 0;
return empty($_SESSION[$key]) || !is_numeric($_SESSION[$key]) ? 0 : (int)$_SESSION[$key];
}
public static function paramString(string $key): string {
@@ -175,10 +178,10 @@ class Minz_Session {
public static function getCookieDir(): string {
// Get the script_name (e.g. /p/i/index.php) and keep only the path.
$cookie_dir = '';
if (!empty($_SERVER['HTTP_X_FORWARDED_PREFIX'])) {
if (!empty($_SERVER['HTTP_X_FORWARDED_PREFIX']) && is_string($_SERVER['HTTP_X_FORWARDED_PREFIX'])) {
$cookie_dir .= rtrim($_SERVER['HTTP_X_FORWARDED_PREFIX'], '/ ');
}
$cookie_dir .= empty($_SERVER['REQUEST_URI']) ? '/' : $_SERVER['REQUEST_URI'];
$cookie_dir .= empty($_SERVER['REQUEST_URI']) || !is_string($_SERVER['REQUEST_URI']) ? '/' : $_SERVER['REQUEST_URI'];
if (substr($cookie_dir, -1) !== '/') {
$cookie_dir = dirname($cookie_dir) . '/';
}
@@ -210,7 +213,7 @@ class Minz_Session {
}
public static function getLongTermCookie(string $name): string {
return $_COOKIE[$name] ?? '';
return is_string($_COOKIE[$name] ?? null) ? $_COOKIE[$name] : '';
}
}

View File

@@ -63,7 +63,7 @@ class Minz_Translate {
/**
* Return the list of available languages.
* @return array<string> containing langs found in different registered paths.
* @return list<string> containing langs found in different registered paths.
*/
public static function availableLanguages(): array {
$list_langs = [];
@@ -81,7 +81,7 @@ class Minz_Translate {
}
}
return array_unique($list_langs);
return array_values(array_unique($list_langs));
}
/**
@@ -197,7 +197,7 @@ class Minz_Translate {
Minz_Log::debug($key . ' is not in a valid format');
$top_level = 'gen';
} else {
$top_level = array_shift($group);
$top_level = array_shift($group) ?? '';
}
// If $translates[$top_level] is null it means we have to load the
@@ -218,6 +218,9 @@ class Minz_Translate {
$level_processed = 0;
$translation_value = $key;
foreach ($group as $i18n_level) {
if (!is_array($translates)) {
continue; // Not needed. To help PHPStan
}
$level_processed++;
if (!isset($translates[$i18n_level])) {
Minz_Log::debug($key . ' is not a valid key');
@@ -231,10 +234,9 @@ class Minz_Translate {
}
}
if (is_array($translation_value)) {
if (isset($translation_value['_'])) {
$translation_value = $translation_value['_'];
} else {
if (!is_string($translation_value)) {
$translation_value = is_array($translation_value) ? ($translation_value['_'] ?? null) : null;
if (!is_string($translation_value)) {
Minz_Log::debug($key . ' is not a valid key');
return $key;
}

View File

@@ -145,10 +145,16 @@ class Minz_Url {
* @return array{c?:string,a?:string,params?:array<string,string>} URL representation
*/
public static function build(): array {
$get = [];
foreach ($_GET as $key => $value) {
if (is_string($key) && is_string($value)) {
$get[$key] = $value;
}
}
$url = [
'c' => $_GET['c'] ?? Minz_Request::defaultControllerName(),
'a' => $_GET['a'] ?? Minz_Request::defaultActionName(),
'params' => $_GET,
'c' => is_string($_GET['c'] ?? null) ? $_GET['c'] : Minz_Request::defaultControllerName(),
'a' => is_string($_GET['a'] ?? null) ? $_GET['a'] : Minz_Request::defaultActionName(),
'params' => $get,
];
// post-traitement
@@ -166,7 +172,7 @@ function _url(string $controller, string $action, int|string ...$args): string|f
return false;
}
$params = array ();
$params = [];
for ($i = 0; $i < $nb_args; $i += 2) {
$arg = '' . $args[$i];
$params[$arg] = '' . $args[$i + 1];

View File

@@ -19,11 +19,11 @@ class Minz_View {
/** @var array<string> */
private static array $base_pathnames = [APP_PATH];
private static string $title = '';
/** @var array<array{'media':string,'url':string}> */
/** @var array<array{media:string,url:string}> */
private static array $styles = [];
/** @var array<array{'url':string,'id':string,'defer':bool,'async':bool}> */
/** @var array<array{url:string,id:string,defer:bool,async:bool}> */
private static array $scripts = [];
/** @var string|array{'dark'?:string,'light'?:string,'default'?:string} */
/** @var string|array{dark?:string,light?:string,default?:string} */
private static $themeColors;
/** @var array<string,mixed> */
private static array $params = [];
@@ -245,7 +245,7 @@ class Minz_View {
}
/**
* @param string|array{'dark'?:string,'light'?:string,'default'?:string} $themeColors
* @param string|array{dark?:string,light?:string,default?:string} $themeColors
*/
public static function appendThemeColors($themeColors): void {
self::$themeColors = $themeColors;

View File

@@ -22,7 +22,7 @@ function isImgMime(string $content): bool {
return $isImage;
}
/** @param array<int,int|bool> $curlOptions */
/** @param array<int,int|bool|string> $curlOptions */
function downloadHttp(string &$url, array $curlOptions = []): string {
syslog(LOG_INFO, 'FreshRSS Favicon GET ' . $url);
$url2 = checkUrl($url);
@@ -61,7 +61,7 @@ function downloadHttp(string &$url, array $curlOptions = []): string {
$url = $url2; //Possible redirect
}
}
return $info['http_code'] == 200 ? $response : '';
return is_array($info) && $info['http_code'] == 200 ? $response : '';
}
function searchFavicon(string &$url): string {
@@ -103,6 +103,9 @@ function searchFavicon(string &$url): string {
}
$iri = $href->get_iri();
if ($iri == false) {
return '';
}
$favicon = downloadHttp($iri, [CURLOPT_REFERER => $url]);
if (isImgMime($favicon)) {
return $favicon;
@@ -115,7 +118,7 @@ function download_favicon(string $url, string $dest): bool {
$url = trim($url);
$favicon = searchFavicon($url);
if ($favicon == '') {
$rootUrl = preg_replace('%^(https?://[^/]+).*$%i', '$1/', $url);
$rootUrl = preg_replace('%^(https?://[^/]+).*$%i', '$1/', $url) ?? $url;
if ($rootUrl != $url) {
$url = $rootUrl;
$favicon = searchFavicon($url);

View File

@@ -29,7 +29,7 @@ declare(strict_types=1);
?>
```
Version 1.9, 2023-04-08, https://alexandre.alapetite.fr/doc-alex/php-http-304/
Version 1.10, 2024-12-22, https://alexandre.alapetite.fr/doc-alex/php-http-304/
------------------------------------------------------------------
Written by Alexandre Alapetite in 2004, https://alexandre.alapetite.fr/cv/
@@ -82,8 +82,8 @@ $_sessionMode = false;
function httpConditional(int $UnixTimeStamp, int $cacheSeconds = 0, int $cachePrivacy = 0, bool $feedMode = false, bool $compression = false, bool $session = false): bool {
if (headers_sent()) return false;
if (isset($_SERVER['SCRIPT_FILENAME'])) $scriptName = $_SERVER['SCRIPT_FILENAME'];
elseif (isset($_SERVER['PATH_TRANSLATED'])) $scriptName = $_SERVER['PATH_TRANSLATED'];
if (is_string($_SERVER['SCRIPT_FILENAME'] ?? null)) $scriptName = $_SERVER['SCRIPT_FILENAME'];
elseif (is_string($_SERVER['PATH_TRANSLATED'] ?? null)) $scriptName = $_SERVER['PATH_TRANSLATED'];
else return false;
if ((!$feedMode) && (($modifScript = (int)filemtime($scriptName)) > $UnixTimeStamp))
@@ -98,7 +98,7 @@ function httpConditional(int $UnixTimeStamp, int $cacheSeconds = 0, int $cachePr
$dateCacheClient = 'Thu, 10 Jan 1980 20:30:40 GMT';
//rfc2616-sec14.html#sec14.19 //='"0123456789abcdef0123456789abcdef"'
if (isset($_SERVER['QUERY_STRING'])) $myQuery = '?' . $_SERVER['QUERY_STRING'];
if (is_string($_SERVER['QUERY_STRING'] ?? null)) $myQuery = '?' . $_SERVER['QUERY_STRING'];
else $myQuery = '';
if ($session && isset($_SESSION)) {
global $_sessionMode;
@@ -108,13 +108,13 @@ function httpConditional(int $UnixTimeStamp, int $cacheSeconds = 0, int $cachePr
$etagServer = '"' . md5($scriptName . $myQuery . '#' . $dateLastModif) . '"';
// @phpstan-ignore booleanNot.alwaysTrue
if ((!$is412) && isset($_SERVER['HTTP_IF_MATCH'])) { //rfc2616-sec14.html#sec14.24
if ((!$is412) && is_string($_SERVER['HTTP_IF_MATCH'] ?? null)) { //rfc2616-sec14.html#sec14.24
$etagsClient = stripslashes($_SERVER['HTTP_IF_MATCH']);
$etagsClient = str_ireplace('-gzip', '', $etagsClient);
$is412 = (($etagsClient !== '*') && (strpos($etagsClient, $etagServer) === false));
}
// @phpstan-ignore booleanAnd.leftAlwaysTrue
if ($is304 && isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { //rfc2616-sec14.html#sec14.25 //rfc1945.txt
if ($is304 && is_string($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? null)) { //rfc2616-sec14.html#sec14.25 //rfc1945.txt
$nbCond++;
$dateCacheClient = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
$p = strpos($dateCacheClient, ';');
@@ -122,13 +122,13 @@ function httpConditional(int $UnixTimeStamp, int $cacheSeconds = 0, int $cachePr
$dateCacheClient = substr($dateCacheClient, 0, $p);
$is304 = ($dateCacheClient == $dateLastModif);
}
if ($is304 && isset($_SERVER['HTTP_IF_NONE_MATCH'])) { //rfc2616-sec14.html#sec14.26
if ($is304 && is_string($_SERVER['HTTP_IF_NONE_MATCH'] ?? null)) { //rfc2616-sec14.html#sec14.26
$nbCond++;
$etagClient = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']);
$etagClient = str_ireplace('-gzip', '', $etagClient);
$is304 = (($etagClient === $etagServer) || ($etagClient === '*'));
}
if ((!$is412) && isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) { //rfc2616-sec14.html#sec14.28
if ((!$is412) && is_string($_SERVER['HTTP_IF_UNMODIFIED_SINCE'] ?? null)) { //rfc2616-sec14.html#sec14.28
$dateCacheClient = $_SERVER['HTTP_IF_UNMODIFIED_SINCE'];
$p = strpos($dateCacheClient, ';');
if ($p !== false)
@@ -200,13 +200,13 @@ function _httpConditionalCallBack(string $buffer, int $mode = 5): string {
function httpConditionalRefresh(int $UnixTimeStamp): void {
if (headers_sent()) return;
if (isset($_SERVER['SCRIPT_FILENAME'])) $scriptName = $_SERVER['SCRIPT_FILENAME'];
elseif (isset($_SERVER['PATH_TRANSLATED'])) $scriptName = $_SERVER['PATH_TRANSLATED'];
if (is_string($_SERVER['SCRIPT_FILENAME'] ?? null)) $scriptName = $_SERVER['SCRIPT_FILENAME'];
elseif (is_string($_SERVER['PATH_TRANSLATED'] ?? null)) $scriptName = $_SERVER['PATH_TRANSLATED'];
else return;
$dateLastModif = gmdate('D, d M Y H:i:s \G\M\T', $UnixTimeStamp);
if (isset($_SERVER['QUERY_STRING'])) $myQuery = '?' . $_SERVER['QUERY_STRING'];
if (is_string($_SERVER['QUERY_STRING'] ?? null)) $myQuery = '?' . $_SERVER['QUERY_STRING'];
else $myQuery = '';
global $_sessionMode;
if ($_sessionMode && isset($_SESSION))

View File

@@ -84,6 +84,32 @@ function classAutoloader(string $class): void {
spl_autoload_register('classAutoloader');
//</Auto-loading>
/**
* @param array<mixed,mixed> $array
* @phpstan-assert-if-true array<string,mixed> $array
*/
function is_array_keys_string(array $array): bool {
foreach ($array as $key => $value) {
if (!is_string($key)) {
return false;
}
}
return true;
}
/**
* @param array<mixed,mixed> $array
* @phpstan-assert-if-true array<mixed,string> $array
*/
function is_array_values_string(array $array): bool {
foreach ($array as $value) {
if (!is_string($value)) {
return false;
}
}
return true;
}
/**
* Memory efficient replacement of `echo json_encode(...)`
* @param array<mixed>|mixed $json
@@ -231,6 +257,7 @@ function timestamptodate(int $t, bool $hour = true): string {
* Decode HTML entities but preserve XML entities.
*/
function html_only_entity_decode(?string $text): string {
/** @var array<string,string>|null $htmlEntitiesOnly */
static $htmlEntitiesOnly = null;
if ($htmlEntitiesOnly === null) {
$htmlEntitiesOnly = array_flip(array_diff(
@@ -252,7 +279,7 @@ function sensitive_log($log): array|string {
foreach ($log as $k => $v) {
if (in_array($k, ['api_key', 'Passwd', 'T'], true)) {
$log[$k] = '██';
} elseif (is_array($v) || is_string($v)) {
} elseif ((is_array($v) && is_array_keys_string($v)) || is_string($v)) {
$log[$k] = sensitive_log($v);
} else {
return '';
@@ -298,7 +325,9 @@ function customSimplePie(array $attributes = [], array $curl_options = []): \Sim
}
if (!empty($attributes['curl_params']) && is_array($attributes['curl_params'])) {
foreach ($attributes['curl_params'] as $co => $v) {
$curl_options[$co] = $v;
if (is_int($co)) {
$curl_options[$co] = $v;
}
}
}
$simplePie->set_curl_options($curl_options);
@@ -366,13 +395,18 @@ function sanitizeHTML(string $data, string $base = '', ?int $maxLength = null):
if ($maxLength !== null) {
$data = mb_strcut($data, 0, $maxLength, 'UTF-8');
}
/** @var \SimplePie\SimplePie|null $simplePie */
static $simplePie = null;
if ($simplePie == null) {
if ($simplePie === null) {
$simplePie = customSimplePie();
$simplePie->enable_cache(false);
$simplePie->init();
}
$result = html_only_entity_decode($simplePie->sanitize->sanitize($data, \SimplePie\SimplePie::CONSTRUCT_HTML, $base));
$sanitized = $simplePie->sanitize->sanitize($data, \SimplePie\SimplePie::CONSTRUCT_HTML, $base);
if (!is_string($sanitized)) {
return '';
}
$result = html_only_entity_decode($sanitized);
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');
@@ -504,6 +538,9 @@ function httpGet(string $url, string $cachePath, string $type = 'html', array $a
// TODO: Implement HTTP 1.1 conditional GET If-Modified-Since
$ch = curl_init();
if ($ch === false) {
return '';
}
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_HTTPHEADER => ['Accept: ' . $accept],
@@ -592,9 +629,10 @@ function lazyimg(string $content): string {
/** @return numeric-string */
function uTimeString(): string {
$t = @gettimeofday();
$result = $t['sec'] . str_pad('' . $t['usec'], 6, '0', STR_PAD_LEFT);
/** @var numeric-string @result */
return $result;
$sec = is_numeric($t['sec']) ? (int)$t['sec'] : 0;
$usec = is_numeric($t['usec']) ? (int)$t['usec'] : 0;
$result = ((string)$sec) . str_pad((string)$usec, 6, '0', STR_PAD_LEFT);
return ctype_digit($result) ? $result : '0';
}
function invalidateHttpCache(string $username = ''): bool {
@@ -606,7 +644,7 @@ function invalidateHttpCache(string $username = ''): bool {
}
/**
* @return array<string>
* @return list<string>
*/
function listUsers(): array {
$final_list = [];
@@ -712,9 +750,9 @@ function checkCIDR(string $ip, string $range): bool {
* Use CONN_REMOTE_ADDR (if available, to be robust even when using Apache mod_remoteip) or REMOTE_ADDR environment variable to determine the connection IP.
*/
function connectionRemoteAddress(): string {
$remoteIp = $_SERVER['CONN_REMOTE_ADDR'] ?? '';
$remoteIp = is_string($_SERVER['CONN_REMOTE_ADDR'] ?? null) ? $_SERVER['CONN_REMOTE_ADDR'] : '';
if ($remoteIp == '') {
$remoteIp = $_SERVER['REMOTE_ADDR'] ?? '';
$remoteIp = is_string($_SERVER['REMOTE_ADDR'] ?? null) ? $_SERVER['REMOTE_ADDR'] : '';
}
if ($remoteIp == 0) {
$remoteIp = '';
@@ -752,17 +790,17 @@ function checkTrustedIP(): bool {
}
function httpAuthUser(bool $onlyTrusted = true): string {
if (!empty($_SERVER['REMOTE_USER'])) {
if (!empty($_SERVER['REMOTE_USER']) && is_string($_SERVER['REMOTE_USER'])) {
return $_SERVER['REMOTE_USER'];
}
if (!empty($_SERVER['REDIRECT_REMOTE_USER'])) {
if (!empty($_SERVER['REDIRECT_REMOTE_USER']) && is_string($_SERVER['REDIRECT_REMOTE_USER'])) {
return $_SERVER['REDIRECT_REMOTE_USER'];
}
if (!$onlyTrusted || checkTrustedIP()) {
if (!empty($_SERVER['HTTP_REMOTE_USER'])) {
if (!empty($_SERVER['HTTP_REMOTE_USER']) && is_string($_SERVER['HTTP_REMOTE_USER'])) {
return $_SERVER['HTTP_REMOTE_USER'];
}
if (!empty($_SERVER['HTTP_X_WEBAUTH_USER'])) {
if (!empty($_SERVER['HTTP_X_WEBAUTH_USER']) && is_string($_SERVER['HTTP_X_WEBAUTH_USER'])) {
return $_SERVER['HTTP_X_WEBAUTH_USER'];
}
}
@@ -872,14 +910,14 @@ function recursive_unlink(string $dir): bool {
/**
* Remove queries where $get is appearing.
* @param string $get the get attribute which should be removed.
* @param array<int,array<string,string|int>> $queries an array of queries.
* @return array<int,array<string,string|int>> without queries where $get is appearing.
* @param array<int,array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string}> $queries an array of queries.
* @return array<int,array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string}> without queries where $get is appearing.
*/
function remove_query_by_get(string $get, array $queries): array {
$final_queries = [];
foreach ($queries as $key => $query) {
foreach ($queries as $query) {
if (empty($query['get']) || $query['get'] !== $get) {
$final_queries[$key] = $query;
$final_queries[] = $query;
}
}
return $final_queries;
@@ -901,7 +939,7 @@ const SHORTCUT_KEYS = [
/**
* @param array<string> $shortcuts
* @return array<string>
* @return list<string>
*/
function getNonStandardShortcuts(array $shortcuts): array {
$standard = strtolower(implode(' ', SHORTCUT_KEYS));
@@ -911,7 +949,7 @@ function getNonStandardShortcuts(array $shortcuts): array {
return $shortcut !== '' && stripos($standard, $shortcut) === false;
});
return $nonStandard;
return array_values($nonStandard);
}
function errorMessageInfo(string $errorTitle, string $error = ''): string {

View File

@@ -31,7 +31,7 @@ Minz_Session::init('FreshRSS', true);
// ================================================================================================
// <Debug>
$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1_048_576) ?: '';;
$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1_048_576) ?: '';
function debugInfo(): string {
if (function_exists('getallheaders')) {
@@ -39,7 +39,7 @@ function debugInfo(): string {
} else { //nginx http://php.net/getallheaders#84262
$ALL_HEADERS = [];
foreach ($_SERVER as $name => $value) {
if (str_starts_with($name, 'HTTP_')) {
if (is_string($name) && str_starts_with($name, 'HTTP_')) {
$ALL_HEADERS[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
@@ -120,6 +120,8 @@ final class FeverDAO extends Minz_ModelPdo
$entries = [];
foreach ($result as $dao) {
/** @var array{id?:string,id_feed?:int,guid?:string,title?:string,author?:string,content?:string,link?:string,date?:int|string,lastSeen?:int,
* hash?:string,is_read?:bool|int,is_favorite?:bool|int,tags?:string|array<string>,attributes?:?string,thumbnail?:string,timestamp?:string} $dao */
$entries[] = FreshRSS_Entry::fromArray($dao);
}
@@ -151,7 +153,7 @@ final class FeverAPI
private function authenticate(): bool {
FreshRSS_Context::clearUserConf();
Minz_User::change();
$feverKey = empty($_POST['api_key']) ? '' : substr(trim($_POST['api_key']), 0, 128);
$feverKey = empty($_POST['api_key']) || !is_string($_POST['api_key']) ? '' : substr(trim($_POST['api_key']), 0, 128);
if (ctype_xdigit($feverKey)) {
$feverKey = strtolower($feverKey);
$username = @file_get_contents(DATA_PATH . '/fever/.key-' . sha1(FreshRSS_Context::systemConf()->salt) . '-' . $feverKey . '.txt', false);
@@ -223,9 +225,9 @@ final class FeverAPI
$response_arr['saved_item_ids'] = $this->getSavedItemIds();
}
if (isset($_REQUEST['mark'], $_REQUEST['as'], $_REQUEST['id']) && ctype_digit($_REQUEST['id'])) {
$id = (string)$_REQUEST['id'];
$before = (int)($_REQUEST['before'] ?? '0');
if (is_string($_REQUEST['mark'] ?? null) && is_string($_REQUEST['as'] ?? null) && is_string($_REQUEST['id'] ?? null) && ctype_digit($_REQUEST['id'])) {
$id = $_REQUEST['id'];
$before = is_numeric($_REQUEST['before'] ?? null) ? (int)$_REQUEST['before'] : 0;
switch (strtolower($_REQUEST['mark'])) {
case 'item':
switch ($_REQUEST['as']) {
@@ -306,7 +308,7 @@ final class FeverAPI
return $lastUpdate;
}
/** @return array<array<string,string|int>> */
/** @return list<array{id:int,favicon_id:int,title:string,url:string,site_url:string,is_spark:int,last_updated_on_time:int}> */
private function getFeeds(): array {
$feeds = [];
$myFeeds = $this->feedDAO->listFeeds();
@@ -328,7 +330,7 @@ final class FeverAPI
return $feeds;
}
/** @return array<array<string,int|string>> */
/** @return list<array{id:int,title:string}> */
private function getGroups(): array {
$groups = [];
@@ -345,7 +347,7 @@ final class FeverAPI
return $groups;
}
/** @return array<array<string,int|string>> */
/** @return list<array{id:int,data:string}> */
private function getFavicons(): array {
if (!FreshRSS_Context::hasSystemConf()) {
return [];
@@ -378,7 +380,7 @@ final class FeverAPI
}
/**
* @return array<array<string,int|string>>
* @return list<array<string,int|string>>
*/
private function getFeedsGroup(): array {
$groups = [];
@@ -401,7 +403,7 @@ final class FeverAPI
/**
* AFAIK there is no 'hot links' alternative in FreshRSS
* @return array<string>
* @return list<string>
*/
private function getLinks(): array {
return [];
@@ -452,46 +454,42 @@ final class FeverAPI
return $this->entryDAO->markFavorite($id, false);
}
/** @return array<array<string,string|int>> */
/** @return list<array<string,string|int>> */
private function getItems(): array {
$feed_ids = [];
$entry_ids = [];
$max_id = '';
$since_id = '';
if (isset($_REQUEST['feed_ids']) || isset($_REQUEST['group_ids'])) {
if (isset($_REQUEST['feed_ids'])) {
$feed_ids = explode(',', $_REQUEST['feed_ids']);
}
if (isset($_REQUEST['group_ids'])) {
$categoryDAO = FreshRSS_Factory::createCategoryDao();
$group_ids = explode(',', $_REQUEST['group_ids']);
$feeds = [];
foreach ($group_ids as $id) {
$category = $categoryDAO->searchById((int)$id); //TODO: Transform to SQL query without loop! Consider FreshRSS_CategoryDAO::listCategories(true)
if ($category == null) {
continue;
}
foreach ($category->feeds() as $feed) {
$feeds[] = $feed->id();
}
if (is_string($_REQUEST['feed_ids'] ?? null)) {
$feed_ids = explode(',', $_REQUEST['feed_ids']);
} elseif (is_string($_REQUEST['group_ids'] ?? null)) {
$categoryDAO = FreshRSS_Factory::createCategoryDao();
$group_ids = explode(',', $_REQUEST['group_ids']);
$feeds = [];
foreach ($group_ids as $id) {
$category = $categoryDAO->searchById((int)$id); //TODO: Transform to SQL query without loop! Consider FreshRSS_CategoryDAO::listCategories(true)
if ($category == null) {
continue;
}
foreach ($category->feeds() as $feed) {
$feeds[] = $feed->id();
}
$feed_ids = array_unique($feeds);
}
$feed_ids = array_unique($feeds);
}
if (isset($_REQUEST['max_id'])) {
if (is_string($_REQUEST['max_id'] ?? null)) {
// use the max_id argument to request the previous $item_limit items
$max_id = '' . $_REQUEST['max_id'];
$max_id = $_REQUEST['max_id'];
if (!ctype_digit($max_id)) {
$max_id = '';
}
} elseif (isset($_REQUEST['with_ids'])) {
} elseif (is_string($_REQUEST['with_ids'] ?? null)) {
$entry_ids = explode(',', $_REQUEST['with_ids']);
} elseif (isset($_REQUEST['since_id'])) {
} elseif (is_string($_REQUEST['since_id'] ?? null)) {
// use the since_id argument to request the next $item_limit items
$since_id = '' . $_REQUEST['since_id'];
$since_id = $_REQUEST['since_id'];
if (!ctype_digit($since_id)) {
$since_id = '';
}

View File

@@ -28,8 +28,6 @@ Server-side API compatible with Google Reader API layer 2
require(__DIR__ . '/../../constants.php');
require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576) ?: '';
if (PHP_INT_SIZE < 8) { //32-bit
/** @return numeric-string */
function hex2dec(string $hex): string {
@@ -53,13 +51,13 @@ const JSON_OPTIONS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
function headerVariable(string $headerName, string $varName): string {
$header = '';
$upName = 'HTTP_' . strtoupper($headerName);
if (isset($_SERVER[$upName])) {
if (is_string($_SERVER[$upName] ?? null)) {
$header = '' . $_SERVER[$upName];
} elseif (isset($_SERVER['REDIRECT_' . $upName])) {
} elseif (is_string($_SERVER['REDIRECT_' . $upName] ?? null)) {
$header = '' . $_SERVER['REDIRECT_' . $upName];
} elseif (function_exists('getallheaders')) {
$ALL_HEADERS = getallheaders();
if (isset($ALL_HEADERS[$headerName])) {
if (is_string($ALL_HEADERS[$headerName] ?? null)) {
$header = '' . $ALL_HEADERS[$headerName];
}
}
@@ -70,47 +68,47 @@ function headerVariable(string $headerName, string $varName): string {
return is_string($pairs[$varName]) ? $pairs[$varName] : '';
}
/** @return array<string> */
function multiplePosts(string $name): array {
//https://bugs.php.net/bug.php?id=51633
global $ORIGINAL_INPUT;
$inputs = explode('&', $ORIGINAL_INPUT);
$result = [];
$prefix = $name . '=';
$prefixLength = strlen($prefix);
foreach ($inputs as $input) {
if (str_starts_with($input, $prefix)) {
$result[] = urldecode(substr($input, $prefixLength));
}
}
return $result;
}
final class GReaderAPI {
function debugInfo(): string {
if (function_exists('getallheaders')) {
$ALL_HEADERS = getallheaders();
} else { //nginx http://php.net/getallheaders#84262
$ALL_HEADERS = [];
foreach ($_SERVER as $name => $value) {
if (str_starts_with($name, 'HTTP_')) {
$ALL_HEADERS[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
private static string $ORIGINAL_INPUT = '';
/** @return list<string> */
private static function multiplePosts(string $name): array {
//https://bugs.php.net/bug.php?id=51633
$inputs = explode('&', self::$ORIGINAL_INPUT);
$result = [];
$prefix = $name . '=';
$prefixLength = strlen($prefix);
foreach ($inputs as $input) {
if (str_starts_with($input, $prefix)) {
$result[] = urldecode(substr($input, $prefixLength));
}
}
return $result;
}
global $ORIGINAL_INPUT;
$log = sensitive_log([
'date' => date('c'),
'headers' => $ALL_HEADERS,
'_SERVER' => $_SERVER,
'_GET' => $_GET,
'_POST' => $_POST,
'_COOKIE' => $_COOKIE,
'INPUT' => $ORIGINAL_INPUT,
]);
return print_r($log, true);
}
final class GReaderAPI {
private static function debugInfo(): string {
if (function_exists('getallheaders')) {
$ALL_HEADERS = getallheaders();
} else { //nginx http://php.net/getallheaders#84262
$ALL_HEADERS = [];
foreach ($_SERVER as $name => $value) {
if (is_string($name) && str_starts_with($name, 'HTTP_')) {
$ALL_HEADERS[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
}
$log = sensitive_log([
'date' => date('c'),
'headers' => $ALL_HEADERS,
'_SERVER' => $_SERVER,
'_GET' => $_GET,
'_POST' => $_POST,
'_COOKIE' => $_COOKIE,
'INPUT' => self::$ORIGINAL_INPUT,
]);
return print_r($log, true);
}
private static function noContent(): never {
header('HTTP/1.1 204 No Content');
@@ -119,7 +117,7 @@ final class GReaderAPI {
private static function badRequest(): never {
Minz_Log::warning(__METHOD__, API_LOG);
Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG);
Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG);
header('HTTP/1.1 400 Bad Request');
header('Content-Type: text/plain; charset=UTF-8');
die('Bad Request!');
@@ -127,7 +125,7 @@ final class GReaderAPI {
private static function unauthorized(): never {
Minz_Log::warning(__METHOD__, API_LOG);
Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG);
Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG);
header('HTTP/1.1 401 Unauthorized');
header('Content-Type: text/plain; charset=UTF-8');
header('Google-Bad-Token: true');
@@ -136,7 +134,7 @@ final class GReaderAPI {
private static function internalServerError(): never {
Minz_Log::warning(__METHOD__, API_LOG);
Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG);
Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG);
header('HTTP/1.1 500 Internal Server Error');
header('Content-Type: text/plain; charset=UTF-8');
die('Internal Server Error!');
@@ -144,7 +142,7 @@ final class GReaderAPI {
private static function notImplemented(): never {
Minz_Log::warning(__METHOD__, API_LOG);
Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG);
Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG);
header('HTTP/1.1 501 Not Implemented');
header('Content-Type: text/plain; charset=UTF-8');
die('Not Implemented!');
@@ -152,7 +150,7 @@ final class GReaderAPI {
private static function serviceUnavailable(): never {
Minz_Log::warning(__METHOD__, API_LOG);
Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG);
Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG);
header('HTTP/1.1 503 Service Unavailable');
header('Content-Type: text/plain; charset=UTF-8');
die('Service Unavailable!');
@@ -160,7 +158,7 @@ final class GReaderAPI {
private static function checkCompatibility(): never {
Minz_Log::warning(__METHOD__, API_LOG);
Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG);
Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG);
header('Content-Type: text/plain; charset=UTF-8');
if (PHP_INT_SIZE < 8 && !function_exists('gmp_init')) {
die('FAIL 64-bit or GMP extension! Wrong PHP configuration.');
@@ -365,9 +363,9 @@ final class GReaderAPI {
}
/**
* @param array<string> $streamNames StreamId(s) to operate on. The parameter may be repeated to edit multiple subscriptions at once
* @param array<string> $titles Title(s) to use for the subscription(s). Each title is associated with the corresponding streamName
* @param 'subscribe'|'unsubscribe'|'edit' $action
* @param list<string> $streamNames StreamId(s) to operate on. The parameter may be repeated to edit multiple subscriptions at once
* @param list<string> $titles Title(s) to use for the subscription(s). Each title is associated with the corresponding streamName
* @param string $action 'subscribe'|'unsubscribe'|'edit'
* @param string $add StreamId to add the subscription(s) to (generally a category)
* @param string $remove StreamId to remove the subscription(s) from (generally a category)
*/
@@ -544,8 +542,8 @@ final class GReaderAPI {
}
/**
* @param array<FreshRSS_Entry> $entries
* @return array<array<string,mixed>>
* @param list<FreshRSS_Entry> $entries
* @return list<array<string,mixed>>
*/
private static function entriesToArray(array $entries): array {
if (empty($entries)) {
@@ -668,7 +666,7 @@ final class GReaderAPI {
$entryDAO = FreshRSS_Factory::createEntryDao();
$entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, 0, $continuation, $searches);
$entries = iterator_to_array($entries); //TODO: Improve
$entries = array_values(iterator_to_array($entries)); //TODO: Improve
$items = self::entriesToArray($entries);
@@ -754,7 +752,7 @@ final class GReaderAPI {
}
/**
* @param array<string> $e_ids
* @param list<string> $e_ids
*/
private static function streamContentsItems(array $e_ids, string $order): never {
header('Content-Type: application/json; charset=UTF-8');
@@ -765,11 +763,11 @@ final class GReaderAPI {
$e_ids[$i] = hex2dec(basename($e_id)); //Strip prefix 'tag:google.com,2005:reader/item/'
}
}
/** @var array<numeric-string> $e_ids */
/** @var list<numeric-string> $e_ids */
$entryDAO = FreshRSS_Factory::createEntryDao();
$entries = $entryDAO->listByIds($e_ids, $order === 'o' ? 'ASC' : 'DESC');
$entries = iterator_to_array($entries); //TODO: Improve
$entries = array_values(iterator_to_array($entries)); //TODO: Improve
$items = self::entriesToArray($entries);
@@ -785,9 +783,9 @@ final class GReaderAPI {
}
/**
* @param array<string> $e_ids IDs of the items to edit
* @param array<string> $as tags to add to all the listed items
* @param array<string> $rs tags to remove from all the listed items
* @param list<string> $e_ids IDs of the items to edit
* @param list<string> $as tags to add to all the listed items
* @param list<string> $rs tags to remove from all the listed items
*/
private static function editTag(array $e_ids, array $as, array $rs): never {
foreach ($e_ids as $i => $e_id) {
@@ -795,7 +793,7 @@ final class GReaderAPI {
$e_ids[$i] = hex2dec(basename($e_id)); //Strip prefix 'tag:google.com,2005:reader/item/'
}
}
/** @var array<numeric-string> $e_ids */
/** @var list<numeric-string> $e_ids */
$entryDAO = FreshRSS_Factory::createEntryDao();
$tagDAO = FreshRSS_Factory::createTagDao();
@@ -960,8 +958,6 @@ final class GReaderAPI {
}
public static function parse(): never {
global $ORIGINAL_INPUT;
header('Access-Control-Allow-Headers: Authorization');
header('Access-Control-Allow-Methods: GET, POST');
header('Access-Control-Allow-Origin: *');
@@ -971,8 +967,8 @@ final class GReaderAPI {
}
$pathInfo = '';
if (empty($_SERVER['PATH_INFO'])) {
if (!empty($_SERVER['ORIG_PATH_INFO'])) {
if (empty($_SERVER['PATH_INFO']) || !is_string($_SERVER['PATH_INFO'])) {
if (!empty($_SERVER['ORIG_PATH_INFO']) && is_string($_SERVER['ORIG_PATH_INFO'])) {
// Compatibility https://php.net/reserved.variables.server
$pathInfo = $_SERVER['ORIG_PATH_INFO'];
}
@@ -992,7 +988,7 @@ final class GReaderAPI {
FreshRSS_Context::initSystem();
//Minz_Log::debug('----------------------------------------------------------------', API_LOG);
//Minz_Log::debug(debugInfo(), API_LOG);
//Minz_Log::debug(self::debugInfo(), API_LOG);
if (!FreshRSS_Context::hasSystemConf() || !FreshRSS_Context::systemConf()->api_enabled) {
self::serviceUnavailable();
@@ -1013,15 +1009,18 @@ final class GReaderAPI {
Minz_Translate::init();
}
self::$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576) ?: '';
if ($pathInfos[1] === 'accounts') {
if (($pathInfos[2] === 'ClientLogin') && isset($_REQUEST['Email']) && isset($_REQUEST['Passwd'])) {
if (($pathInfos[2] === 'ClientLogin') && is_string($_REQUEST['Email'] ?? null) && is_string($_REQUEST['Passwd'] ?? null)) {
self::clientLogin($_REQUEST['Email'], $_REQUEST['Passwd']);
}
} elseif (isset($pathInfos[3], $pathInfos[4]) && $pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && $pathInfos[3] === '0') {
if (Minz_User::name() === null) {
self::unauthorized();
}
$timestamp = isset($_GET['ck']) ? (int)$_GET['ck'] : 0; //ck=[unix timestamp] : Use the current Unix time here, helps Google with caching.
// ck=[unix timestamp]: Use the current Unix time here, helps Google with caching
$timestamp = is_numeric($_GET['ck'] ?? null) ? (int)$_GET['ck'] : 0;
switch ($pathInfos[4]) {
case 'stream':
/**
@@ -1031,41 +1030,41 @@ final class GReaderAPI {
* exclude items from a particular feed (obviously not useful in this request,
* but xt appears in other listing requests).
*/
$exclude_target = $_GET['xt'] ?? '';
$filter_target = $_GET['it'] ?? '';
$exclude_target = is_string($_GET['xt'] ?? null) ? $_GET['xt'] : '';
$filter_target = is_string($_GET['it'] ?? null) ? $_GET['it'] : '';
//n=[integer] : The maximum number of results to return.
$count = isset($_GET['n']) ? (int)$_GET['n'] : 20;
$count = is_numeric($_GET['n'] ?? null) ? (int)$_GET['n'] : 20;
//r=[d|n|o] : Sort order of item results. d or n gives items in descending date order, o in ascending order.
$order = $_GET['r'] ?? 'd';
$order = is_string($_GET['r'] ?? null) ? $_GET['r'] : 'd';
/**
* ot=[unix timestamp] : The time from which you want to retrieve items.
* Only items that have been crawled by Google Reader after this time will be returned.
*/
$start_time = isset($_GET['ot']) ? (int)$_GET['ot'] : 0;
$stop_time = isset($_GET['nt']) ? (int)$_GET['nt'] : 0;
$start_time = is_numeric($_GET['ot'] ?? null) ? (int)$_GET['ot'] : 0;
$stop_time = is_numeric($_GET['nt'] ?? null) ? (int)$_GET['nt'] : 0;
/**
* Continuation token. If a StreamContents response does not represent
* all items in a timestamp range, it will have a continuation attribute.
* The same request can be re-issued with the value of that attribute put
* in this parameter to get more items
*/
$continuation = isset($_GET['c']) ? trim((string)$_GET['c']) : '';
$continuation = is_string($_GET['c'] ?? null) ? trim($_GET['c']) : '';
if (!ctype_digit($continuation)) {
$continuation = '';
}
if (isset($pathInfos[5]) && $pathInfos[5] === 'contents') {
if (!isset($pathInfos[6]) && isset($_GET['s'])) {
if (!isset($pathInfos[6]) && is_string($_GET['s'] ?? null)) {
// Compatibility BazQux API https://github.com/bazqux/bazqux-api#fetching-streams
$streamIdInfos = explode('/', $_GET['s']);
foreach ($streamIdInfos as $streamIdInfo) {
$pathInfos[] = $streamIdInfo;
}
}
if (isset($pathInfos[6]) && isset($pathInfos[7])) {
if (isset($pathInfos[6], $pathInfos[7])) {
if ($pathInfos[6] === 'feed') {
$include_target = $pathInfos[7];
if ($include_target != '' && !is_numeric($include_target)) {
$include_target = empty($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI'];
if ($include_target !== '' && !is_numeric($include_target)) {
$include_target = empty($_SERVER['REQUEST_URI']) || !is_string($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI'];
if (preg_match('#/reader/api/0/stream/contents/feed/([A-Za-z0-9\'!*()%$_.~+-]+)#', $include_target, $matches) === 1) {
$include_target = urldecode($matches[1]);
} else {
@@ -1095,13 +1094,13 @@ final class GReaderAPI {
$count, $order, $filter_target, $exclude_target, $continuation);
}
} elseif ($pathInfos[5] === 'items') {
if ($pathInfos[6] === 'ids' && isset($_GET['s'])) {
if ($pathInfos[6] === 'ids' && is_string($_GET['s'] ?? null)) {
// StreamId for which to fetch the item IDs.
// TODO: support multiple streams
$streamId = $_GET['s'];
self::streamContentsItemsIds($streamId, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation);
} elseif ($pathInfos[6] === 'contents' && isset($_POST['i'])) { //FeedMe
$e_ids = multiplePosts('i'); //item IDs
$e_ids = self::multiplePosts('i'); //item IDs
self::streamContentsItems($e_ids, $order);
}
}
@@ -1120,8 +1119,8 @@ final class GReaderAPI {
self::subscriptionExport();
// Always exits
case 'import':
if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && $ORIGINAL_INPUT != '') {
self::subscriptionImport($ORIGINAL_INPUT);
if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && self::$ORIGINAL_INPUT != '') {
self::subscriptionImport(self::$ORIGINAL_INPUT);
}
break;
case 'list':
@@ -1132,23 +1131,23 @@ final class GReaderAPI {
case 'edit':
if (isset($_REQUEST['s'], $_REQUEST['ac'])) {
// StreamId to operate on. The parameter may be repeated to edit multiple subscriptions at once
$streamNames = empty($_POST['s']) && isset($_GET['s']) ? [$_GET['s']] : multiplePosts('s');
$streamNames = empty($_POST['s']) && is_string($_GET['s'] ?? null) ? [$_GET['s']] : self::multiplePosts('s');
/* Title to use for the subscription. For the `subscribe` action,
* if not specified then the feeds current title will be used. Can
* be used with the `edit` action to rename a subscription */
$titles = empty($_POST['t']) && isset($_GET['t']) ? [$_GET['t']] : multiplePosts('t');
$titles = empty($_POST['t']) && is_string($_GET['t'] ?? null) ? [$_GET['t']] : self::multiplePosts('t');
// Action to perform on the given StreamId. Possible values are `subscribe`, `unsubscribe` and `edit`
$action = $_REQUEST['ac'];
$action = is_string($_REQUEST['ac'] ?? null) ? $_REQUEST['ac'] : '';
// StreamId to add the subscription to (generally a user label)
// (in FreshRSS, we do not support repeated values since a feed can only be in one category)
$add = $_REQUEST['a'] ?? '';
$add = is_string($_REQUEST['a'] ?? null) ? $_REQUEST['a'] : '';
// StreamId to remove the subscription from (generally a user label) (in FreshRSS, we do not support repeated values)
$remove = $_REQUEST['r'] ?? '';
$remove = is_string($_REQUEST['r'] ?? null) ? $_REQUEST['r'] : '';
self::subscriptionEdit($streamNames, $titles, $action, $add, $remove);
}
break;
case 'quickadd': //https://github.com/theoldreader/api
if (isset($_REQUEST['quickadd'])) {
if (is_string($_REQUEST['quickadd'] ?? null)) {
self::quickadd($_REQUEST['quickadd']);
}
break;
@@ -1161,35 +1160,35 @@ final class GReaderAPI {
self::unreadCount();
// Always exits
case 'edit-tag': // https://web.archive.org/web/20200616071132/https://blog.martindoms.com/2010/01/20/using-the-google-reader-api-part-3
$token = isset($_POST['T']) ? trim($_POST['T']) : '';
$token = is_string($_POST['T'] ?? null) ? trim($_POST['T']) : '';
self::checkToken(FreshRSS_Context::userConf(), $token);
// Add (Can be repeated to add multiple tags at once): user/-/state/com.google/read user/-/state/com.google/starred
$as = multiplePosts('a');
$as = self::multiplePosts('a');
// Remove (Can be repeated to remove multiple tags at once): user/-/state/com.google/read user/-/state/com.google/starred
$rs = multiplePosts('r');
$e_ids = multiplePosts('i'); //item IDs
$rs = self::multiplePosts('r');
$e_ids = self::multiplePosts('i'); //item IDs
self::editTag($e_ids, $as, $rs);
// Always exits
case 'rename-tag': //https://github.com/theoldreader/api
$token = isset($_POST['T']) ? trim($_POST['T']) : '';
$token = is_string($_POST['T'] ?? null) ? trim($_POST['T']) : '';
self::checkToken(FreshRSS_Context::userConf(), $token);
$s = $_POST['s'] ?? ''; //user/-/label/Folder
$dest = $_POST['dest'] ?? ''; //user/-/label/NewFolder
$s = is_string($_POST['s'] ?? null) ? trim($_POST['s']) : ''; //user/-/label/Folder
$dest = is_string($_POST['dest'] ?? null) ? trim($_POST['dest']) : ''; //user/-/label/NewFolder
self::renameTag($s, $dest);
// Always exits
case 'disable-tag': //https://github.com/theoldreader/api
$token = isset($_POST['T']) ? trim($_POST['T']) : '';
$token = is_string($_POST['T'] ?? null) ? trim($_POST['T']) : '';
self::checkToken(FreshRSS_Context::userConf(), $token);
$s_s = multiplePosts('s');
$s_s = self::multiplePosts('s');
foreach ($s_s as $s) {
self::disableTag($s); //user/-/label/Folder
}
// Always exits
case 'mark-all-as-read':
$token = isset($_POST['T']) ? trim($_POST['T']) : '';
$token = is_string($_POST['T'] ?? null) ? trim($_POST['T']) : '';
self::checkToken(FreshRSS_Context::userConf(), $token);
$streamId = trim($_POST['s'] ?? '');
$ts = trim($_POST['ts'] ?? '0'); //Older than timestamp in nanoseconds
$streamId = is_string($_POST['s'] ?? null) ? trim($_POST['s']) : '';
$ts = is_string($_POST['ts'] ?? null) ? trim($_POST['ts']) : '0'; //Older than timestamp in nanoseconds
if (!ctype_digit($ts)) {
self::badRequest();
}

View File

@@ -19,7 +19,7 @@ FreshRSS_Context::systemConf()->auth_type = 'none'; // avoid necessity to be log
// Minz_Log::debug(print_r(['_SERVER' => $_SERVER, '_GET' => $_GET, '_POST' => $_POST, 'INPUT' => $ORIGINAL_INPUT], true), PSHB_LOG);
$key = isset($_GET['k']) ? substr($_GET['k'], 0, 128) : '';
$key = isset($_GET['k']) && is_string($_GET['k']) ? substr($_GET['k'], 0, 128) : '';
if (!ctype_xdigit($key)) {
header('HTTP/1.1 422 Unprocessable Entity');
die('Invalid feed key format!');
@@ -67,7 +67,7 @@ if (empty($users)) {
}
if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] === 'subscribe') {
$leaseSeconds = empty($_REQUEST['hub_lease_seconds']) ? 0 : (int)$_REQUEST['hub_lease_seconds'];
$leaseSeconds = empty($_REQUEST['hub_lease_seconds']) || !is_numeric($_REQUEST['hub_lease_seconds']) ? 0 : (int)$_REQUEST['hub_lease_seconds'];
if ($leaseSeconds > 60) {
$hubJson['lease_end'] = time() + $leaseSeconds;
} else {

View File

@@ -134,7 +134,7 @@ switch ($type) {
Minz_Error::error(404, "Category {$id} not found!");
die();
}
$view->categories = [ $cat->id() => $cat ];
$view->categories = [ $cat ];
break;
case 'f': // Feed
$feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
@@ -142,7 +142,7 @@ switch ($type) {
Minz_Error::error(404, "Feed {$id} not found!");
die();
}
$view->feeds = [ $feed->id() => $feed ];
$view->feeds = [ $feed ];
$view->categories = [];
break;
default:

View File

@@ -15,7 +15,7 @@ function show_default_favicon(int $cacheSeconds = 3600): void {
}
$id = $_SERVER['QUERY_STRING'] ?? '0';
if (!ctype_xdigit($id)) {
if (!is_string($id) || !ctype_xdigit($id)) {
$id = '0';
}

View File

@@ -3,6 +3,20 @@ includes:
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
parameters:
level: max
checkBenevolentUnionTypes: true # TODO pass
checkImplicitMixed: true # TODO pass
strictRules:
strictArrayFilter: false # TODO pass maybe
excludePaths:
analyse:
# TODO: Update files below and remove them from this list
- app/Models/Entry.php
- app/Models/EntryDAO.php
- app/Models/Feed.php
- app/Models/FeedDAO.php
- app/Models/TagDAO.php
- app/Models/Themes.php
- app/Services/ImportService.php
- app/views/helpers/feed/update.phtml
- cli/CliOptionsParser.php

View File

@@ -1,5 +1,8 @@
parameters:
level: 9 # https://phpstan.org/user-guide/rule-levels
phpVersion:
min: 80100 # PHP 8.1
max: 80499 # PHP 8.4
level: 10 # https://phpstan.org/user-guide/rule-levels
fileExtensions:
- php
- phtml
@@ -33,23 +36,17 @@ parameters:
- STDOUT
- TMP_PATH
- USERS_PATH
checkBenevolentUnionTypes: false # TODO pass
checkImplicitMixed: false # TODO pass
checkMissingOverrideMethodAttribute: true
reportMaybesInPropertyPhpDocTypes: false
checkTooWideReturnTypesInProtectedAndPublicMethods: true
reportAnyTypeWideningInVarTag: true
treatPhpDocTypesAsCertain: false
strictRules:
allRules: false
booleansInConditions: true
closureUsesThis: true
disallowedConstructs: false
disallowedEmpty: false
disallowedLooseComparison: false
matchingInheritedMethodNames: true
noVariableVariables: true
numericOperandsInArithmeticOperators: true
overwriteVariablesWithLoop: true
requireParentConstructorCall: true
strictCalls: true
switchConditionsMatchingType: true
uselessCast: true
disallowedShortTernary: false
strictArrayFilter: false # TODO pass
exceptions:
check:
missingCheckedExceptionInThrows: false # TODO pass

View File

@@ -7,17 +7,17 @@ class CategoryTest extends PHPUnit\Framework\TestCase {
public static function test__construct_whenNoParameters_createsObjectWithDefaultValues(): void {
$category = new FreshRSS_Category();
self::assertEquals(0, $category->id());
self::assertEquals('', $category->name());
self::assertSame(0, $category->id());
self::assertSame('', $category->name());
}
#[DataProvider('provideValidNames')]
public static function test_name_whenValidValue_storesModifiedValue(string $input, string $expected): void {
$category = new FreshRSS_Category($input);
self::assertEquals($expected, $category->name());
self::assertSame($expected, $category->name());
}
/** @return array<array{string,string}> */
/** @return list<array{string,string}> */
public static function provideValidNames(): array {
return [
['', ''],
@@ -60,11 +60,11 @@ class CategoryTest extends PHPUnit\Framework\TestCase {
self::assertCount(3, $feeds);
$feed = reset($feeds) ?: FreshRSS_Feed::default();
self::assertEquals('AAA', $feed->name());
self::assertSame('AAA', $feed->name());
$feed = next($feeds) ?: FreshRSS_Feed::default();
self::assertEquals('lll', $feed->name());
self::assertSame('lll', $feed->name());
$feed = next($feeds) ?: FreshRSS_Feed::default();
self::assertEquals('ZZZ', $feed->name());
self::assertSame('ZZZ', $feed->name());
/** @var FreshRSS_Feed&PHPUnit\Framework\MockObject\MockObject */
$feed_4 = $this->getMockBuilder(FreshRSS_Feed::class)
@@ -79,12 +79,12 @@ class CategoryTest extends PHPUnit\Framework\TestCase {
self::assertCount(4, $feeds);
$feed = reset($feeds) ?: FreshRSS_Feed::default();
self::assertEquals('AAA', $feed->name());
self::assertSame('AAA', $feed->name());
$feed = next($feeds) ?: FreshRSS_Feed::default();
self::assertEquals('BBB', $feed->name());
self::assertSame('BBB', $feed->name());
$feed = next($feeds) ?: FreshRSS_Feed::default();
self::assertEquals('lll', $feed->name());
self::assertSame('lll', $feed->name());
$feed = next($feeds) ?: FreshRSS_Feed::default();
self::assertEquals('ZZZ', $feed->name());
self::assertSame('ZZZ', $feed->name());
}
}

View File

@@ -5,7 +5,7 @@ class FeedDAOTest extends PHPUnit\Framework\TestCase {
public static function test_ttl_min(): void {
$feed = new FreshRSS_Feed('https://example.net/', false);
$feed->_ttl(-5);
self::assertEquals(-5, $feed->ttl(true));
self::assertEquals(true, $feed->mute());
self::assertSame(-5, $feed->ttl(true));
self::assertTrue($feed->mute());
}
}

View File

@@ -22,15 +22,14 @@ class LogDAOTest extends TestCase {
}
public function test_lines_is_array_and_truncate_function_work(): void {
self::assertEquals(USERS_PATH . '/' . Minz_User::INTERNAL_USER . '/' . self::LOG_FILE_TEST, $this->logPath);
self::assertSame(USERS_PATH . '/' . Minz_User::INTERNAL_USER . '/' . self::LOG_FILE_TEST, $this->logPath);
$line = $this->logDAO::lines(self::LOG_FILE_TEST);
self::assertCount(1, $line);
self::assertInstanceOf(FreshRSS_Log::class, $line[0]);
self::assertEquals('Wed, 08 Feb 2023 15:35:05 +0000', $line[0]->date());
self::assertEquals('notice', $line[0]->level());
self::assertEquals("Migration 2019_12_22_FooBar: OK", $line[0]->info());
self::assertSame('Wed, 08 Feb 2023 15:35:05 +0000', $line[0]->date());
self::assertSame('notice', $line[0]->level());
self::assertSame("Migration 2019_12_22_FooBar: OK", $line[0]->info());
$this->logDAO::truncate(self::LOG_FILE_TEST);

View File

@@ -10,7 +10,7 @@ class SearchTest extends PHPUnit\Framework\TestCase {
#[DataProvider('provideEmptyInput')]
public static function test__construct_whenInputIsEmpty_getsOnlyNullValues(string $input): void {
$search = new FreshRSS_Search($input);
self::assertEquals('', $search->getRawInput());
self::assertSame('', $search->getRawInput());
self::assertNull($search->getIntitle());
self::assertNull($search->getMinDate());
self::assertNull($search->getMaxDate());
@@ -40,12 +40,12 @@ class SearchTest extends PHPUnit\Framework\TestCase {
#[DataProvider('provideIntitleSearch')]
public static function test__construct_whenInputContainsIntitle_setsIntitleProperty(string $input, ?array $intitle_value, ?array $search_value): void {
$search = new FreshRSS_Search($input);
self::assertEquals($intitle_value, $search->getIntitle());
self::assertEquals($search_value, $search->getSearch());
self::assertSame($intitle_value, $search->getIntitle());
self::assertSame($search_value, $search->getSearch());
}
/**
* @return array<array<mixed>>
* @return list<list<mixed>>
*/
public static function provideIntitleSearch(): array {
return [
@@ -77,12 +77,12 @@ class SearchTest extends PHPUnit\Framework\TestCase {
#[DataProvider('provideAuthorSearch')]
public static function test__construct_whenInputContainsAuthor_setsAuthorValue(string $input, ?array $author_value, ?array $search_value): void {
$search = new FreshRSS_Search($input);
self::assertEquals($author_value, $search->getAuthor());
self::assertEquals($search_value, $search->getSearch());
self::assertSame($author_value, $search->getAuthor());
self::assertSame($search_value, $search->getSearch());
}
/**
* @return array<array<mixed>>
* @return list<list<mixed>>
*/
public static function provideAuthorSearch(): array {
return [
@@ -114,12 +114,12 @@ class SearchTest extends PHPUnit\Framework\TestCase {
#[DataProvider('provideInurlSearch')]
public static function test__construct_whenInputContainsInurl_setsInurlValue(string $input, ?array $inurl_value, ?array $search_value): void {
$search = new FreshRSS_Search($input);
self::assertEquals($inurl_value, $search->getInurl());
self::assertEquals($search_value, $search->getSearch());
self::assertSame($inurl_value, $search->getInurl());
self::assertSame($search_value, $search->getSearch());
}
/**
* @return array<array<mixed>>
* @return list<list<mixed>>
*/
public static function provideInurlSearch(): array {
return [
@@ -137,12 +137,12 @@ class SearchTest extends PHPUnit\Framework\TestCase {
#[DataProvider('provideDateSearch')]
public static function test__construct_whenInputContainsDate_setsDateValues(string $input, ?int $min_date_value, ?int $max_date_value): void {
$search = new FreshRSS_Search($input);
self::assertEquals($min_date_value, $search->getMinDate());
self::assertEquals($max_date_value, $search->getMaxDate());
self::assertSame($min_date_value, $search->getMinDate());
self::assertSame($max_date_value, $search->getMaxDate());
}
/**
* @return array<array<mixed>>
* @return list<list<mixed>>
*/
public static function provideDateSearch(): array {
return [
@@ -158,12 +158,12 @@ class SearchTest extends PHPUnit\Framework\TestCase {
#[DataProvider('providePubdateSearch')]
public static function test__construct_whenInputContainsPubdate_setsPubdateValues(string $input, ?int $min_pubdate_value, ?int $max_pubdate_value): void {
$search = new FreshRSS_Search($input);
self::assertEquals($min_pubdate_value, $search->getMinPubdate());
self::assertEquals($max_pubdate_value, $search->getMaxPubdate());
self::assertSame($min_pubdate_value, $search->getMinPubdate());
self::assertSame($max_pubdate_value, $search->getMaxPubdate());
}
/**
* @return array<array<mixed>>
* @return list<list<mixed>>
*/
public static function providePubdateSearch(): array {
return [
@@ -183,12 +183,12 @@ class SearchTest extends PHPUnit\Framework\TestCase {
#[DataProvider('provideTagsSearch')]
public static function test__construct_whenInputContainsTags_setsTagsValue(string $input, ?array $tags_value, ?array $search_value): void {
$search = new FreshRSS_Search($input);
self::assertEquals($tags_value, $search->getTags());
self::assertEquals($search_value, $search->getSearch());
self::assertSame($tags_value, $search->getTags());
self::assertSame($search_value, $search->getSearch());
}
/**
* @return array<array<string|array<string>|null>>
* @return list<list<string|list<string>|null>>
*/
public static function provideTagsSearch(): array {
return [
@@ -215,19 +215,19 @@ class SearchTest extends PHPUnit\Framework\TestCase {
?int $max_date_value, ?array $intitle_value, ?array $inurl_value, ?int $min_pubdate_value,
?int $max_pubdate_value, ?array $tags_value, ?array $search_value): void {
$search = new FreshRSS_Search($input);
self::assertEquals($author_value, $search->getAuthor());
self::assertEquals($min_date_value, $search->getMinDate());
self::assertEquals($max_date_value, $search->getMaxDate());
self::assertEquals($intitle_value, $search->getIntitle());
self::assertEquals($inurl_value, $search->getInurl());
self::assertEquals($min_pubdate_value, $search->getMinPubdate());
self::assertEquals($max_pubdate_value, $search->getMaxPubdate());
self::assertEquals($tags_value, $search->getTags());
self::assertEquals($search_value, $search->getSearch());
self::assertEquals($input, $search->getRawInput());
self::assertSame($author_value, $search->getAuthor());
self::assertSame($min_date_value, $search->getMinDate());
self::assertSame($max_date_value, $search->getMaxDate());
self::assertSame($intitle_value, $search->getIntitle());
self::assertSame($inurl_value, $search->getInurl());
self::assertSame($min_pubdate_value, $search->getMinPubdate());
self::assertSame($max_pubdate_value, $search->getMaxPubdate());
self::assertSame($tags_value, $search->getTags());
self::assertSame($search_value, $search->getSearch());
self::assertSame($input, $search->getRawInput());
}
/** @return array<array<mixed>> */
/** @return list<list<mixed>> */
public static function provideMultipleSearch(): array {
return [
[
@@ -283,10 +283,10 @@ class SearchTest extends PHPUnit\Framework\TestCase {
#[DataProvider('provideAddOrParentheses')]
public static function test__addOrParentheses(string $input, string $output): void {
self::assertEquals($output, FreshRSS_BooleanSearch::addOrParentheses($input));
self::assertSame($output, FreshRSS_BooleanSearch::addOrParentheses($input));
}
/** @return array<array{string,string}> */
/** @return list<list{string,string}> */
public static function provideAddOrParentheses(): array {
return [
['ab', 'ab'],
@@ -302,10 +302,10 @@ class SearchTest extends PHPUnit\Framework\TestCase {
#[DataProvider('provideconsistentOrParentheses')]
public static function test__consistentOrParentheses(string $input, string $output): void {
self::assertEquals($output, FreshRSS_BooleanSearch::consistentOrParentheses($input));
self::assertSame($output, FreshRSS_BooleanSearch::consistentOrParentheses($input));
}
/** @return array<array{string,string}> */
/** @return list<list{string,string}> */
public static function provideconsistentOrParentheses(): array {
return [
['ab cd ef', 'ab cd ef'],
@@ -332,24 +332,24 @@ class SearchTest extends PHPUnit\Framework\TestCase {
#[DataProvider('provideParentheses')]
public function test__parentheses(string $input, string $sql, array $values): void {
[$filterValues, $filterSearch] = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
self::assertEquals(trim($sql), trim($filterSearch));
self::assertEquals($values, $filterValues);
self::assertSame(trim($sql), trim($filterSearch));
self::assertSame($values, $filterValues);
}
/** @return array<array<mixed>> */
/** @return list<list<mixed>> */
public static function provideParentheses(): array {
return [
[
'f:1 (f:2 OR f:3 OR f:4) (f:5 OR (f:6 OR f:7))',
' ((e.id_feed IN (?) )) AND ((e.id_feed IN (?) ) OR (e.id_feed IN (?) ) OR (e.id_feed IN (?) )) AND' .
' (((e.id_feed IN (?) )) OR ((e.id_feed IN (?) ) OR (e.id_feed IN (?) ))) ',
['1', '2', '3', '4', '5', '6', '7']
[1, 2, 3, 4, 5, 6, 7]
],
[
'#tag Hello OR (author:Alice inurl:example) OR (f:3 intitle:World) OR L:12',
" ((TRIM(e.tags) || ' #' LIKE ? AND (e.title LIKE ? OR e.content LIKE ?) )) OR ((e.author LIKE ? AND e.link LIKE ? )) OR" .
' ((e.id_feed IN (?) AND e.title LIKE ? )) OR ((e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?)) )) ',
['%tag #%', '%Hello%', '%Hello%', '%Alice%', '%example%', '3', '%World%', '12']
['%tag #%', '%Hello%', '%Hello%', '%Alice%', '%example%', 3, '%World%', 12]
],
[
'#tag Hello (author:Alice inurl:example) (f:3 intitle:World) label:Bleu',
@@ -357,7 +357,7 @@ class SearchTest extends PHPUnit\Framework\TestCase {
' ((e.author LIKE ? AND e.link LIKE ? )) AND' .
' ((e.id_feed IN (?) AND e.title LIKE ? )) AND' .
' ((e.id IN (SELECT et.id_entry FROM `_entrytag` et, `_tag` t WHERE et.id_tag = t.id AND t.name IN (?)) )) ',
['%tag #%', '%Hello%', '%Hello%', '%Alice%', '%example%', '3', '%World%', 'Bleu']
['%tag #%', '%Hello%', '%Hello%', '%Alice%', '%example%', 3, '%World%', 'Bleu']
],
[
'!((author:Alice intitle:hello) OR (author:Bob intitle:world))',
@@ -478,11 +478,11 @@ class SearchTest extends PHPUnit\Framework\TestCase {
*/
public function test__regex_postgresql(string $input, string $sql, array $values): void {
[$filterValues, $filterSearch] = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
self::assertEquals(trim($sql), trim($filterSearch));
self::assertEquals($values, $filterValues);
self::assertSame(trim($sql), trim($filterSearch));
self::assertSame($values, $filterValues);
}
/** @return array<array<mixed>> */
/** @return list<list<mixed>> */
public static function provideRegexPostreSQL(): array {
return [
[
@@ -551,11 +551,11 @@ class SearchTest extends PHPUnit\Framework\TestCase {
FreshRSS_DatabaseDAO::$dummyConnection = true;
FreshRSS_DatabaseDAO::setStaticVersion('11.4.3-MariaDB-ubu2404');
[$filterValues, $filterSearch] = FreshRSS_EntryDAO::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
self::assertEquals(trim($sql), trim($filterSearch));
self::assertEquals($values, $filterValues);
self::assertSame(trim($sql), trim($filterSearch));
self::assertSame($values, $filterValues);
}
/** @return array<array<mixed>> */
/** @return list<list<mixed>> */
public static function provideRegexMariaDB(): array {
return [
[
@@ -584,11 +584,11 @@ class SearchTest extends PHPUnit\Framework\TestCase {
FreshRSS_DatabaseDAO::$dummyConnection = true;
FreshRSS_DatabaseDAO::setStaticVersion('9.0.1');
[$filterValues, $filterSearch] = FreshRSS_EntryDAO::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
self::assertEquals(trim($sql), trim($filterSearch));
self::assertEquals($values, $filterValues);
self::assertSame(trim($sql), trim($filterSearch));
self::assertSame($values, $filterValues);
}
/** @return array<array<mixed>> */
/** @return list<list<mixed>> */
public static function provideRegexMySQL(): array {
return [
[
@@ -615,11 +615,11 @@ class SearchTest extends PHPUnit\Framework\TestCase {
*/
public function test__regex_sqlite(string $input, string $sql, array $values): void {
[$filterValues, $filterSearch] = FreshRSS_EntryDAOSQLite::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
self::assertEquals(trim($sql), trim($filterSearch));
self::assertEquals($values, $filterValues);
self::assertSame(trim($sql), trim($filterSearch));
self::assertSame($values, $filterValues);
}
/** @return array<array<mixed>> */
/** @return list<list<mixed>> */
public static function provideRegexSQLite(): array {
return [
[

View File

@@ -10,13 +10,13 @@ class UserQueryTest extends TestCase {
public static function test__construct_whenAllQuery_storesAllParameters(): void {
$query = ['get' => 'a'];
$user_query = new FreshRSS_UserQuery($query, [], []);
self::assertEquals('all', $user_query->getGetType());
self::assertSame('all', $user_query->getGetType());
}
public static function test__construct_whenFavoriteQuery_storesFavoriteParameters(): void {
$query = ['get' => 's'];
$user_query = new FreshRSS_UserQuery($query, [], []);
self::assertEquals('favorite', $user_query->getGetType());
self::assertSame('favorite', $user_query->getGetType());
}
public function test__construct_whenCategoryQuery_storesCategoryParameters(): void {
@@ -29,8 +29,8 @@ class UserQueryTest extends TestCase {
->willReturn($category_name);
$query = ['get' => 'c_1'];
$user_query = new FreshRSS_UserQuery($query, [1 => $cat], []);
self::assertEquals($category_name, $user_query->getGetName());
self::assertEquals('category', $user_query->getGetType());
self::assertSame($category_name, $user_query->getGetName());
self::assertSame('category', $user_query->getGetType());
}
public function test__construct_whenFeedQuery_storesFeedParameters(): void {
@@ -53,8 +53,8 @@ class UserQueryTest extends TestCase {
->willReturn([1 => $feed]);
$query = ['get' => 'f_1'];
$user_query = new FreshRSS_UserQuery($query, [1 => $cat], []);
self::assertEquals($feed_name, $user_query->getGetName());
self::assertEquals('feed', $user_query->getGetType());
self::assertSame($feed_name, $user_query->getGetName());
self::assertSame('feed', $user_query->getGetType());
}
public static function test__construct_whenUnknownQuery_doesStoreParameters(): void {
@@ -68,28 +68,28 @@ class UserQueryTest extends TestCase {
$name = 'some name';
$query = ['name' => $name];
$user_query = new FreshRSS_UserQuery($query, [], []);
self::assertEquals($name, $user_query->getName());
self::assertSame($name, $user_query->getName());
}
public static function test__construct_whenOrder_storesOrder(): void {
$order = 'some order';
$query = ['order' => $order];
$user_query = new FreshRSS_UserQuery($query, [], []);
self::assertEquals($order, $user_query->getOrder());
self::assertSame($order, $user_query->getOrder());
}
public static function test__construct_whenState_storesState(): void {
$state = FreshRSS_Entry::STATE_NOT_READ | FreshRSS_Entry::STATE_FAVORITE;
$query = ['state' => $state];
$user_query = new FreshRSS_UserQuery($query, [], []);
self::assertEquals($state, $user_query->getState());
self::assertSame($state, $user_query->getState());
}
public static function test__construct_whenUrl_storesUrl(): void {
$url = 'some url';
$query = ['url' => $url];
$user_query = new FreshRSS_UserQuery($query, [], []);
self::assertEquals($url, $user_query->getUrl());
self::assertSame($url, $user_query->getUrl());
}
public static function testToArray_whenNoData_returnsEmptyArray(): void {
@@ -108,7 +108,7 @@ class UserQueryTest extends TestCase {
];
$user_query = new FreshRSS_UserQuery($query, [], []);
self::assertCount(6, $user_query->toArray());
self::assertEquals($query, $user_query->toArray());
self::assertSame($query, $user_query->toArray());
}
public static function testHasSearch_whenSearch_returnsTrue(): void {

View File

@@ -41,6 +41,6 @@ class dotNotationUtilTest extends PHPUnit\Framework\TestCase {
#[DataProvider('provideJsonDots')]
public static function testJsonDots(array $array, string $key, string $expected): void {
$value = FreshRSS_dotNotation_Util::get($array, $key);
self::assertEquals($expected, $value);
self::assertSame($expected, $value);
}
}

View File

@@ -54,131 +54,131 @@ class CliOptionsParserTest extends TestCase {
public static function testInvalidOptionSetWithValueReturnsError(): void {
$result = self::runOptionalOptions('--invalid=invalid');
self::assertEquals(['invalid' => 'unknown option: invalid'], $result->errors);
self::assertSame(['invalid' => 'unknown option: invalid'], $result->errors);
}
public static function testInvalidOptionSetWithoutValueReturnsError(): void {
$result = self::runOptionalOptions('--invalid');
self::assertEquals(['invalid' => 'unknown option: invalid'], $result->errors);
self::assertSame(['invalid' => 'unknown option: invalid'], $result->errors);
}
public static function testValidOptionSetWithValidValueAndInvalidOptionSetWithValueReturnsValueForValidOptionAndErrorForInvalidOption(): void {
$result = self::runOptionalOptions('--string=string --invalid=invalid');
self::assertEquals('string', $result->string);
self::assertEquals(['invalid' => 'unknown option: invalid'], $result->errors);
self::assertSame('string', $result->string);
self::assertSame(['invalid' => 'unknown option: invalid'], $result->errors);
}
public static function testOptionWithValueTypeOfStringSetOnceWithValidValueReturnsValueAsString(): void {
$result = self::runOptionalOptions('--string=string');
self::assertEquals('string', $result->string);
self::assertSame('string', $result->string);
}
public static function testOptionWithRequiredValueTypeOfIntSetOnceWithValidValueReturnsValueAsInt(): void {
$result = self::runOptionalOptions('--int=111');
self::assertEquals(111, $result->int);
self::assertSame(111, $result->int);
}
public static function testOptionWithRequiredValueTypeOfBoolSetOnceWithValidValueReturnsValueAsBool(): void {
$result = self::runOptionalOptions('--bool=on');
self::assertEquals(true, $result->bool);
self::assertTrue($result->bool);
}
public static function testOptionWithValueTypeOfArrayOfStringSetOnceWithValidValueReturnsValueAsArrayOfString(): void {
$result = self::runOptionalOptions('--array-of-string=string');
self::assertEquals(['string'], $result->arrayOfString);
self::assertSame(['string'], $result->arrayOfString);
}
public static function testOptionWithValueTypeOfStringSetMultipleTimesWithValidValueReturnsLastValueSetAsString(): void {
$result = self::runOptionalOptions('--string=first --string=second');
self::assertEquals('second', $result->string);
self::assertSame('second', $result->string);
}
public static function testOptionWithValueTypeOfIntSetMultipleTimesWithValidValueReturnsLastValueSetAsInt(): void {
$result = self::runOptionalOptions('--int=111 --int=222');
self::assertEquals(222, $result->int);
self::assertSame(222, $result->int);
}
public static function testOptionWithValueTypeOfBoolSetMultipleTimesWithValidValueReturnsLastValueSetAsBool(): void {
$result = self::runOptionalOptions('--bool=on --bool=off');
self::assertEquals(false, $result->bool);
self::assertFalse($result->bool);
}
public static function testOptionWithValueTypeOfArrayOfStringSetMultipleTimesWithValidValueReturnsAllSetValuesAsArrayOfString(): void {
$result = self::runOptionalOptions('--array-of-string=first --array-of-string=second');
self::assertEquals(['first', 'second'], $result->arrayOfString);
self::assertSame(['first', 'second'], $result->arrayOfString);
}
public static function testOptionWithValueTypeOfIntSetWithInvalidValueReturnsAnError(): void {
$result = self::runOptionalOptions('--int=one');
self::assertEquals(['int' => 'invalid input: int must be an integer'], $result->errors);
self::assertSame(['int' => 'invalid input: int must be an integer'], $result->errors);
}
public static function testOptionWithValueTypeOfBoolSetWithInvalidValuesReturnsAnError(): void {
$result = self::runOptionalOptions('--bool=bad');
self::assertEquals(['bool' => 'invalid input: bool must be a boolean'], $result->errors);
self::assertSame(['bool' => 'invalid input: bool must be a boolean'], $result->errors);
}
public static function testOptionWithValueTypeOfIntSetMultipleTimesWithValidAndInvalidValuesReturnsLastValidValueSetAsIntAndError(): void {
$result = self::runOptionalOptions('--int=111 --int=one --int=222 --int=two');
self::assertEquals(222, $result->int);
self::assertEquals(['int' => 'invalid input: int must be an integer'], $result->errors);
self::assertSame(222, $result->int);
self::assertSame(['int' => 'invalid input: int must be an integer'], $result->errors);
}
public static function testOptionWithValueTypeOfBoolSetMultipleTimesWithWithValidAndInvalidValuesReturnsLastValidValueSetAsBoolAndError(): void {
$result = self::runOptionalOptions('--bool=on --bool=good --bool=off --bool=bad');
self::assertEquals(false, $result->bool);
self::assertEquals(['bool' => 'invalid input: bool must be a boolean'], $result->errors);
self::assertFalse($result->bool);
self::assertSame(['bool' => 'invalid input: bool must be a boolean'], $result->errors);
}
public static function testNotSetOptionWithDefaultInputReturnsDefaultInput(): void {
$result = self::runOptionalOptions('');
self::assertEquals('default', $result->defaultInput);
self::assertSame('default', $result->defaultInput);
}
public static function testOptionWithDefaultInputSetWithValidValueReturnsCorrectlyTypedValue(): void {
$result = self::runOptionalOptions('--default-input=input');
self::assertEquals('input', $result->defaultInput);
self::assertSame('input', $result->defaultInput);
}
public static function testOptionWithOptionalValueSetWithoutValueReturnsEmptyString(): void {
$result = self::runOptionalOptions('--optional-value');
self::assertEquals('', $result->optionalValue);
self::assertSame('', $result->optionalValue);
}
public static function testOptionWithOptionalValueDefaultSetWithoutValueReturnsOptionalValueDefault(): void {
$result = self::runOptionalOptions('--optional-value-with-default');
self::assertEquals(true, $result->optionalValueWithDefault);
self::assertTrue($result->optionalValueWithDefault);
}
public static function testNotSetOptionWithOptionalValueDefaultAndDefaultInputReturnsDefaultInput(): void {
$result = self::runOptionalOptions('');
self::assertEquals('default', $result->defaultInputAndOptionalValueWithDefault);
self::assertSame('default', $result->defaultInputAndOptionalValueWithDefault);
}
public static function testOptionWithOptionalValueDefaultAndDefaultInputSetWithoutValueReturnsOptionalValueDefault(): void {
$result = self::runOptionalOptions('--default-input-and-optional-value-with-default');
self::assertEquals('optional', $result->defaultInputAndOptionalValueWithDefault);
self::assertSame('optional', $result->defaultInputAndOptionalValueWithDefault);
}
public static function testRequiredOptionNotSetReturnsError(): void {
$result = self::runOptionalAndRequiredOptions('');
self::assertEquals(['required' => 'invalid input: required cannot be empty'], $result->errors);
self::assertSame(['required' => 'invalid input: required cannot be empty'], $result->errors);
}
public static function testOptionSetWithDeprecatedAliasGeneratesDeprecationWarningAndReturnsValue(): void {
$result = self::runCommandReadingStandardError('--deprecated-string=string');
self::assertEquals('FreshRSS deprecation warning: the CLI option(s): deprecated-string are deprecated ' .
self::assertSame('FreshRSS deprecation warning: the CLI option(s): deprecated-string are deprecated ' .
'and will be removed in a future release. Use: string instead',
$result
);
$result = self::runOptionalOptions('--deprecated-string=string');
self::assertEquals('string', $result->string);
self::assertSame('string', $result->string);
}
public static function testAlwaysReturnUsageMessageWithUsageInfoForAllOptions(): void {
$result = self::runOptionalAndRequiredOptions('');
self::assertEquals('Usage: cli-parser-test.php --required=<required> [-s --string=<string>] [-i --int=<int>] [-b --bool=<bool>] [-f --flag]',
self::assertSame('Usage: cli-parser-test.php --required=<required> [-s --string=<string>] [-i --int=<int>] [-b --bool=<bool>] [-f --flag]',
$result->usage,
);
}

View File

@@ -17,23 +17,23 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
public function testDisplayReport(): void {
$validator = new I18nCompletionValidator([], []);
self::assertEquals("There is no data.\n", $validator->displayReport());
self::assertSame("There is no data.\n", $validator->displayReport());
$reflectionTotalEntries = new ReflectionProperty(I18nCompletionValidator::class, 'totalEntries');
$reflectionTotalEntries->setAccessible(true);
$reflectionTotalEntries->setValue($validator, 100);
self::assertEquals("Translation is 0.0% complete.\n", $validator->displayReport());
self::assertSame("Translation is 0.0% complete.\n", $validator->displayReport());
$reflectionPassEntries = new ReflectionProperty(I18nCompletionValidator::class, 'passEntries');
$reflectionPassEntries->setAccessible(true);
$reflectionPassEntries->setValue($validator, 25);
self::assertEquals("Translation is 25.0% complete.\n", $validator->displayReport());
self::assertSame("Translation is 25.0% complete.\n", $validator->displayReport());
$reflectionPassEntries->setValue($validator, 100);
self::assertEquals("Translation is 100.0% complete.\n", $validator->displayReport());
self::assertSame("Translation is 100.0% complete.\n", $validator->displayReport());
$reflectionPassEntries->setValue($validator, 200);
@@ -45,7 +45,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
public static function testValidateWhenNoData(): void {
$validator = new I18nCompletionValidator([], []);
self::assertTrue($validator->validate());
self::assertEquals('', $validator->displayResult());
self::assertSame('', $validator->displayResult());
}
public function testValidateWhenKeyIsMissing(): void {
@@ -59,7 +59,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
], []);
self::assertFalse($validator->validate());
self::assertEquals("Missing key file1.l1.l2.k1\nMissing key file2.l1.l2.k1\n", $validator->displayResult());
self::assertSame("Missing key file1.l1.l2.k1\nMissing key file2.l1.l2.k1\n", $validator->displayResult());
}
public function testValidateWhenKeyIsIgnored(): void {
@@ -84,7 +84,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
]);
self::assertTrue($validator->validate());
self::assertEquals('', $validator->displayResult());
self::assertSame('', $validator->displayResult());
}
public function testValidateWhenValueIsEqual(): void {
@@ -112,7 +112,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
]);
self::assertFalse($validator->validate());
self::assertEquals("Untranslated key file1.l1.l2.k1 - \nUntranslated key file2.l1.l2.k1 - \n", $validator->displayResult());
self::assertSame("Untranslated key file1.l1.l2.k1 - \nUntranslated key file2.l1.l2.k1 - \n", $validator->displayResult());
}
public function testValidateWhenValueIsDifferent(): void {
@@ -140,6 +140,6 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase {
]);
self::assertTrue($validator->validate());
self::assertEquals('', $validator->displayResult());
self::assertSame('', $validator->displayResult());
}
}

View File

@@ -36,7 +36,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
public function testConstructWhenReferenceOnly(): void {
$data = new I18nData($this->referenceData);
self::assertEquals($this->referenceData, $data->getData());
self::assertSame($this->referenceData, $data->getData());
}
public function testConstructorWhenLanguageIsMissingFile(): void {
@@ -278,7 +278,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
'nl' => [],
]);
$data = new I18nData($rawData);
self::assertEquals([
self::assertSame([
'en',
'fr',
'nl',
@@ -292,7 +292,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
'de' => [],
]);
$data = new I18nData($rawData);
self::assertEquals([
self::assertSame([
'de',
'en',
'fr',
@@ -310,7 +310,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
public function testAddLanguageWhenNoReferenceProvided(): void {
$data = new I18nData($this->referenceData);
$data->addLanguage('fr');
self::assertEquals([
self::assertSame([
'en' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
@@ -347,7 +347,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
public function testAddLanguageWhenUnknownReferenceProvided(): void {
$data = new I18nData($this->referenceData);
$data->addLanguage('fr', 'unknown');
self::assertEquals([
self::assertSame([
'en' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
@@ -384,7 +384,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
public function testAddLanguageWhenKnownReferenceProvided(): void {
$data = new I18nData($this->referenceData);
$data->addLanguage('fr', 'en');
self::assertEquals([
self::assertSame([
'en' => [
'file1.php' => [
'file1.l1.l2.k1' => $this->value,
@@ -480,9 +480,9 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
$enValue = $getTargetedValue($data, 'en');
$frValue = $getTargetedValue($data, 'fr');
self::assertInstanceOf(I18nValue::class, $enValue);
self::assertEquals('value', $enValue->getValue());
self::assertSame('value', $enValue->getValue());
self::assertTrue($enValue->isTodo());
self::assertEquals($frValue, $enValue);
self::assertSame($frValue, $enValue);
}
public function testAddValueWhenLanguageDoesNotExist(): void {
@@ -520,9 +520,9 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
self::assertEquals($this->value, $beforeEnValue);
self::assertEquals($this->value, $beforeFrValue);
self::assertInstanceOf(I18nValue::class, $afterEnValue);
self::assertEquals('new value', $afterEnValue->getValue());
self::assertSame('new value', $afterEnValue->getValue());
self::assertInstanceOf(I18nValue::class, $afterFrValue);
self::assertEquals('new value', $afterFrValue->getValue());
self::assertSame('new value', $afterFrValue->getValue());
}
public function testAddValueWhenLanguageIsReferenceAndValueInOtherLanguageHasChange(): void {
@@ -554,7 +554,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
self::assertEquals($this->value, $beforeEnValue);
self::assertEquals($value, $beforeFrValue);
self::assertInstanceOf(I18nValue::class, $afterEnValue);
self::assertEquals('new value', $afterEnValue->getValue());
self::assertSame('new value', $afterEnValue->getValue());
self::assertEquals($value, $afterFrValue);
}
@@ -575,7 +575,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
self::assertEquals($this->value, $beforeFrValue);
self::assertEquals($this->value, $afterEnValue);
self::assertInstanceOf(I18nValue::class, $afterFrValue);
self::assertEquals('new value', $afterFrValue->getValue());
self::assertSame('new value', $afterFrValue->getValue());
}
public function testRemoveKeyWhenKeyDoesNotExist(): void {
@@ -723,7 +723,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
'nl' => [],
]);
$data = new I18nData($rawData);
self::assertEquals($this->referenceData['en'], $data->getLanguage('en'));
self::assertSame($this->referenceData['en'], $data->getLanguage('en'));
}
public function testGetReferenceLanguage(): void {
@@ -732,6 +732,6 @@ class I18nDataTest extends PHPUnit\Framework\TestCase {
'nl' => [],
]);
$data = new I18nData($rawData);
self::assertEquals($this->referenceData['en'], $data->getReferenceLanguage());
self::assertSame($this->referenceData['en'], $data->getReferenceLanguage());
}
}

View File

@@ -12,7 +12,7 @@ class I18nFileTest extends PHPUnit\Framework\TestCase {
$after = $this->computeFilesHash();
self::assertEquals($before, $after);
self::assertSame($before, $after);
}
/** @return array<string,string|false> */

Some files were not shown because too many files have changed in this diff Show More