mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2025-12-23 21:47:44 -05:00
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:
committed by
GitHub
parent
897e4a3f4a
commit
b1d24fbdb7
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -8,6 +8,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
|
||||
|
||||
/**
|
||||
* @var FreshRSS_ViewStats
|
||||
* @phpstan-ignore property.phpDocType
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
|
||||
@@ -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' ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ class FreshRSS_User_Mailer extends Minz_Mailer {
|
||||
|
||||
/**
|
||||
* @var FreshRSS_View
|
||||
* @phpstan-ignore property.phpDocType
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ abstract class FreshRSS_ActionController extends Minz_ActionController {
|
||||
|
||||
/**
|
||||
* @var FreshRSS_View
|
||||
* @phpstan-ignore property.phpDocType
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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])) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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> */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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])) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
82
composer.lock
generated
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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] : '';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = '';
|
||||
}
|
||||
|
||||
@@ -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 feed’s 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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
2
p/f.php
2
p/f.php
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 [
|
||||
[
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user