summaryrefslogtreecommitdiff
path: root/classes
diff options
context:
space:
mode:
authorAndrew Dolgov <[email protected]>2021-11-18 07:32:28 +0300
committerAndrew Dolgov <[email protected]>2021-11-18 07:32:28 +0300
commit63ec5a89657bb7f9650582b96e0bb255a0889b48 (patch)
tree074b61eedd7304ba1d8d7deec01d26973ef8e6b8 /classes
parent3a3fde1a2e0beac6d179c6449eaad726100710d7 (diff)
parent2d830c6281c19a7ee29cd379f5aedc82deef3775 (diff)
Merge branch 'wip-phpstan-level6'
Diffstat (limited to 'classes')
-rwxr-xr-xclasses/api.php113
-rwxr-xr-xclasses/article.php97
-rw-r--r--classes/auth/base.php22
-rw-r--r--classes/config.php44
-rw-r--r--classes/counters.php50
-rwxr-xr-xclasses/db.php9
-rw-r--r--classes/db/migrations.php44
-rw-r--r--classes/db/prefs.php10
-rw-r--r--classes/debug.php86
-rw-r--r--classes/digest.php7
-rw-r--r--classes/diskcache.php99
-rw-r--r--classes/errors.php6
-rw-r--r--classes/feedenclosure.php11
-rw-r--r--classes/feeditem.php30
-rwxr-xr-xclasses/feeditem/atom.php57
-rwxr-xr-xclasses/feeditem/common.php50
-rwxr-xr-xclasses/feeditem/rss.php41
-rw-r--r--classes/feedparser.php80
-rwxr-xr-xclasses/feeds.php306
-rw-r--r--classes/handler.php15
-rw-r--r--classes/handler/administrative.php2
-rw-r--r--classes/handler/protected.php2
-rwxr-xr-xclasses/handler/public.php48
-rw-r--r--classes/iauthmodule.php17
-rw-r--r--classes/ihandler.php6
-rw-r--r--classes/labels.php41
-rwxr-xr-xclasses/logger.php13
-rw-r--r--classes/logger/adapter.php4
-rwxr-xr-xclasses/logger/sql.php2
-rw-r--r--classes/logger/stdout.php3
-rw-r--r--classes/logger/syslog.php3
-rw-r--r--classes/mailer.php18
-rw-r--r--classes/opml.php34
-rw-r--r--classes/plugin.php614
-rw-r--r--classes/pluginhandler.php4
-rwxr-xr-xclasses/pluginhost.php393
-rwxr-xr-xclasses/pref/feeds.php177
-rwxr-xr-xclasses/pref/filters.php102
-rw-r--r--classes/pref/labels.php23
-rw-r--r--classes/pref/prefs.php239
-rw-r--r--classes/pref/system.php21
-rw-r--r--classes/pref/users.php16
-rw-r--r--classes/prefs.php54
-rwxr-xr-xclasses/rpc.php88
-rwxr-xr-xclasses/rssutils.php124
-rw-r--r--classes/sanitizer.php19
-rw-r--r--classes/timehelper.php8
-rw-r--r--classes/urlhelper.php64
-rw-r--r--classes/userhelper.php36
49 files changed, 2409 insertions, 943 deletions
diff --git a/classes/api.php b/classes/api.php
index 033aa8654..764cb04da 100755
--- a/classes/api.php
+++ b/classes/api.php
@@ -13,13 +13,20 @@ class API extends Handler {
const E_UNKNOWN_METHOD = "UNKNOWN_METHOD";
const E_OPERATION_FAILED = "E_OPERATION_FAILED";
+ /** @var int|null */
private $seq;
- private static function _param_to_bool($p) {
+ /**
+ * @param mixed $p
+ */
+ private static function _param_to_bool($p): bool {
return $p && ($p !== "f" && $p !== "false");
}
- private function _wrap($status, $reply) {
+ /**
+ * @param array<int|string, mixed> $reply
+ */
+ private function _wrap(int $status, array $reply): void {
print json_encode([
"seq" => $this->seq,
"status" => $status,
@@ -27,7 +34,7 @@ class API extends Handler {
]);
}
- function before($method) {
+ function before(string $method): bool {
if (parent::before($method)) {
header("Content-Type: text/json");
@@ -48,17 +55,17 @@ class API extends Handler {
return false;
}
- function getVersion() {
+ function getVersion(): void {
$rv = array("version" => Config::get_version());
$this->_wrap(self::STATUS_OK, $rv);
}
- function getApiLevel() {
+ function getApiLevel(): void {
$rv = array("level" => self::API_LEVEL);
$this->_wrap(self::STATUS_OK, $rv);
}
- function login() {
+ function login(): void {
if (session_status() == PHP_SESSION_ACTIVE) {
session_destroy();
@@ -87,22 +94,20 @@ class API extends Handler {
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_API_DISABLED));
}
- } else {
- $this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
- return;
}
+ $this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
}
- function logout() {
+ function logout(): void {
UserHelper::logout();
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
- function isLoggedIn() {
+ function isLoggedIn(): void {
$this->_wrap(self::STATUS_OK, array("status" => $_SESSION["uid"] != ''));
}
- function getUnread() {
+ function getUnread(): void {
$feed_id = clean($_REQUEST["feed_id"] ?? "");
$is_cat = clean($_REQUEST["is_cat"] ?? "");
@@ -114,12 +119,12 @@ class API extends Handler {
}
/* Method added for ttrss-reader for Android */
- function getCounters() {
+ function getCounters(): void {
$this->_wrap(self::STATUS_OK, Counters::get_all());
}
- function getFeeds() {
- $cat_id = clean($_REQUEST["cat_id"]);
+ function getFeeds(): void {
+ $cat_id = (int) clean($_REQUEST["cat_id"]);
$unread_only = self::_param_to_bool(clean($_REQUEST["unread_only"] ?? 0));
$limit = (int) clean($_REQUEST["limit"] ?? 0);
$offset = (int) clean($_REQUEST["offset"] ?? 0);
@@ -130,7 +135,7 @@ class API extends Handler {
$this->_wrap(self::STATUS_OK, $feeds);
}
- function getCategories() {
+ function getCategories(): void {
$unread_only = self::_param_to_bool(clean($_REQUEST["unread_only"] ?? false));
$enable_nested = self::_param_to_bool(clean($_REQUEST["enable_nested"] ?? false));
$include_empty = self::_param_to_bool(clean($_REQUEST['include_empty'] ?? false));
@@ -186,11 +191,11 @@ class API extends Handler {
$this->_wrap(self::STATUS_OK, $cats);
}
- function getHeadlines() {
+ function getHeadlines(): void {
$feed_id = clean($_REQUEST["feed_id"]);
- if ($feed_id !== "") {
+ if ($feed_id !== "" && is_numeric($feed_id)) {
- if (is_numeric($feed_id)) $feed_id = (int) $feed_id;
+ $feed_id = (int) $feed_id;
$limit = (int)clean($_REQUEST["limit"] ?? 0 );
@@ -237,7 +242,7 @@ class API extends Handler {
}
}
- function updateArticle() {
+ function updateArticle(): void {
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
$mode = (int) clean($_REQUEST["mode"]);
$data = clean($_REQUEST["data"] ?? "");
@@ -303,7 +308,7 @@ class API extends Handler {
}
- function getArticle() {
+ function getArticle(): void {
$article_ids = explode(',', clean($_REQUEST['article_id'] ?? ''));
$sanitize_content = self::_param_to_bool($_REQUEST['sanitize'] ?? true);
@@ -351,7 +356,7 @@ class API extends Handler {
$article['content'] = Sanitizer::sanitize(
$entry->content,
self::_param_to_bool($entry->hide_images),
- false, $entry->site_url, false, $entry->id);
+ null, $entry->site_url, null, $entry->id);
} else {
$article['content'] = $entry->content;
}
@@ -375,7 +380,10 @@ class API extends Handler {
}
}
- private function _get_config() {
+ /**
+ * @return array<string, array<string, string>|bool|int|string>
+ */
+ private function _get_config(): array {
$config = [
"icons_dir" => Config::get(Config::ICONS_DIR),
"icons_url" => Config::get(Config::ICONS_URL)
@@ -391,13 +399,13 @@ class API extends Handler {
return $config;
}
- function getConfig() {
+ function getConfig(): void {
$config = $this->_get_config();
$this->_wrap(self::STATUS_OK, $config);
}
- function updateFeed() {
+ function updateFeed(): void {
$feed_id = (int) clean($_REQUEST["feed_id"]);
if (!ini_get("open_basedir")) {
@@ -407,10 +415,10 @@ class API extends Handler {
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
- function catchupFeed() {
+ function catchupFeed(): void {
$feed_id = clean($_REQUEST["feed_id"]);
$is_cat = clean($_REQUEST["is_cat"]);
- @$mode = clean($_REQUEST["mode"]);
+ $mode = clean($_REQUEST["mode"] ?? "");
if (!in_array($mode, ["all", "1day", "1week", "2week"]))
$mode = "all";
@@ -420,13 +428,13 @@ class API extends Handler {
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
- function getPref() {
+ function getPref(): void {
$pref_name = clean($_REQUEST["pref_name"]);
$this->_wrap(self::STATUS_OK, array("value" => get_pref($pref_name)));
}
- function getLabels() {
+ function getLabels(): void {
$article_id = (int)clean($_REQUEST['article_id'] ?? -1);
$rv = [];
@@ -462,7 +470,7 @@ class API extends Handler {
$this->_wrap(self::STATUS_OK, $rv);
}
- function setArticleLabel() {
+ function setArticleLabel(): void {
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
$label_id = (int) clean($_REQUEST['label_id']);
@@ -477,9 +485,9 @@ class API extends Handler {
foreach ($article_ids as $id) {
if ($assign)
- Labels::add_article($id, $label, $_SESSION["uid"]);
+ Labels::add_article((int)$id, $label, $_SESSION["uid"]);
else
- Labels::remove_article($id, $label, $_SESSION["uid"]);
+ Labels::remove_article((int)$id, $label, $_SESSION["uid"]);
++$num_updated;
@@ -491,7 +499,7 @@ class API extends Handler {
}
- function index($method) {
+ function index(string $method): void {
$plugin = PluginHost::getInstance()->get_api_method(strtolower($method));
if ($plugin && method_exists($plugin, $method)) {
@@ -504,7 +512,7 @@ class API extends Handler {
}
}
- function shareToPublished() {
+ function shareToPublished(): void {
$title = strip_tags(clean($_REQUEST["title"]));
$url = strip_tags(clean($_REQUEST["url"]));
$content = strip_tags(clean($_REQUEST["content"]));
@@ -516,13 +524,12 @@ class API extends Handler {
}
}
- private static function _api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested = false) {
+ /**
+ * @return array<int, array{'id': int, 'title': string, 'unread': int, 'cat_id': int}>
+ */
+ private static function _api_get_feeds(int $cat_id, bool $unread_only, int $limit, int $offset, bool $include_nested = false): array {
$feeds = [];
- $limit = (int) $limit;
- $offset = (int) $offset;
- $cat_id = (int) $cat_id;
-
/* Labels */
/* API only: -4 All feeds, including virtual feeds */
@@ -632,13 +639,16 @@ class API extends Handler {
return $feeds;
}
- private static function _api_get_headlines($feed_id, $limit, $offset,
- $filter, $is_cat, $show_excerpt, $show_content, $view_mode, $order,
- $include_attachments, $since_id,
- $search = "", $include_nested = false, $sanitize_content = true,
- $force_update = false, $excerpt_length = 100, $check_first_id = false, $skip_first_id_check = false) {
+ /**
+ * @return array{0: array<int, array<string, mixed>>, 1: array<string, mixed>} $headlines, $headlines_header
+ */
+ private static function _api_get_headlines(int $feed_id, int $limit, int $offset,
+ string $filter, bool $is_cat, bool $show_excerpt, bool $show_content, ?string $view_mode, string $order,
+ bool $include_attachments, int $since_id, string $search = "", bool $include_nested = false,
+ bool $sanitize_content = true, bool $force_update = false, int $excerpt_length = 100, ?int $check_first_id = null,
+ bool $skip_first_id_check = false): array {
- if ($force_update && $feed_id > 0 && is_numeric($feed_id)) {
+ if ($force_update && is_numeric($feed_id) && $feed_id > 0) {
// Update the feed if required with some basic flood control
$feed = ORM::for_table('ttrss_feeds')
@@ -746,7 +756,7 @@ class API extends Handler {
$headline_row["content"] = Sanitizer::sanitize(
$line["content"],
self::_param_to_bool($line['hide_images']),
- false, $line["site_url"], false, $line["id"]);
+ null, $line["site_url"], null, $line["id"]);
} else {
$headline_row["content"] = $line["content"];
}
@@ -803,7 +813,7 @@ class API extends Handler {
return array($headlines, $headlines_header);
}
- function unsubscribeFeed() {
+ function unsubscribeFeed(): void {
$feed_id = (int) clean($_REQUEST["feed_id"]);
$feed_exists = ORM::for_table('ttrss_feeds')
@@ -818,7 +828,7 @@ class API extends Handler {
}
}
- function subscribeToFeed() {
+ function subscribeToFeed(): void {
$feed_url = clean($_REQUEST["feed_url"]);
$category_id = (int) clean($_REQUEST["category_id"]);
$login = clean($_REQUEST["login"]);
@@ -833,7 +843,7 @@ class API extends Handler {
}
}
- function getFeedTree() {
+ function getFeedTree(): void {
$include_empty = self::_param_to_bool(clean($_REQUEST['include_empty']));
$pf = new Pref_Feeds($_REQUEST);
@@ -846,7 +856,7 @@ class API extends Handler {
}
// only works for labels or uncategorized for the time being
- private function _is_cat_empty($id) {
+ private function _is_cat_empty(int $id): bool {
if ($id == -2) {
$label_count = ORM::for_table('ttrss_labels2')
->where('owner_uid', $_SESSION['uid'])
@@ -865,7 +875,8 @@ class API extends Handler {
return false;
}
- private function _get_custom_sort_types() {
+ /** @return array<string, string> */
+ private function _get_custom_sort_types(): array {
$ret = [];
PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_HEADLINES_CUSTOM_SORT_MAP, function ($result) use (&$ret) {
diff --git a/classes/article.php b/classes/article.php
index 04855ac9d..e113ed219 100755
--- a/classes/article.php
+++ b/classes/article.php
@@ -4,7 +4,11 @@ class Article extends Handler_Protected {
const ARTICLE_KIND_VIDEO = 2;
const ARTICLE_KIND_YOUTUBE = 3;
- function redirect() {
+ const CATCHUP_MODE_MARK_AS_READ = 0;
+ const CATCHUP_MODE_MARK_AS_UNREAD = 1;
+ const CATCHUP_MODE_TOGGLE = 2;
+
+ function redirect(): void {
$article = ORM::for_table('ttrss_entries')
->table_alias('e')
->join('ttrss_user_entries', [ 'ref_id', '=', 'e.id'], 'ue')
@@ -24,8 +28,7 @@ class Article extends Handler_Protected {
print "Article not found or has an empty URL.";
}
- static function _create_published_article($title, $url, $content, $labels_str,
- $owner_uid) {
+ static function _create_published_article(string $title, string $url, string $content, string $labels_str, int $owner_uid): bool {
$guid = 'SHA1:' . sha1("ttshared:" . $url . $owner_uid); // include owner_uid to prevent global GUID clash
@@ -158,14 +161,14 @@ class Article extends Handler_Protected {
return $rc;
}
- function printArticleTags() {
+ function printArticleTags(): void {
$id = (int) clean($_REQUEST['id'] ?? 0);
print json_encode(["id" => $id,
"tags" => self::_get_tags($id)]);
}
- function setScore() {
+ function setScore(): void {
$ids = array_map("intval", clean($_REQUEST['ids'] ?? []));
$score = (int)clean($_REQUEST['score']);
@@ -179,14 +182,14 @@ class Article extends Handler_Protected {
print json_encode(["id" => $ids, "score" => $score]);
}
- function setArticleTags() {
+ function setArticleTags(): void {
$id = clean($_REQUEST["id"]);
//$tags_str = clean($_REQUEST["tags_str"]);
//$tags = array_unique(array_map('trim', explode(",", $tags_str)));
- $tags = FeedItem_Common::normalize_categories(explode(",", clean($_REQUEST["tags_str"])));
+ $tags = FeedItem_Common::normalize_categories(explode(",", clean($_REQUEST["tags_str"] ?? "")));
$this->pdo->beginTransaction();
@@ -254,18 +257,18 @@ class Article extends Handler_Protected {
print "</ul>";
}*/
- function assigntolabel() {
- return $this->_label_ops(true);
+ function assigntolabel(): void {
+ $this->_label_ops(true);
}
- function removefromlabel() {
- return $this->_label_ops(false);
+ function removefromlabel(): void {
+ $this->_label_ops(false);
}
- private function _label_ops($assign) {
+ private function _label_ops(bool $assign): void {
$reply = array();
- $ids = explode(",", clean($_REQUEST["ids"]));
+ $ids = array_map("intval", array_filter(explode(",", clean($_REQUEST["ids"] ?? "")), "strlen"));
$label_id = clean($_REQUEST["lid"]);
$label = Labels::find_caption($label_id, $_SESSION["uid"]);
@@ -289,11 +292,10 @@ class Article extends Handler_Protected {
print json_encode($reply);
}
- static function _format_enclosures($id,
- $always_display_enclosures,
- $article_content,
- $hide_images = false) {
-
+ /**
+ * @return array{'formatted': string, 'entries': array<int, array<string, mixed>>}
+ */
+ static function _format_enclosures(int $id, bool $always_display_enclosures, string $article_content, bool $hide_images = false): array {
$enclosures = self::_get_enclosures($id);
$enclosures_formatted = "";
@@ -366,7 +368,10 @@ class Article extends Handler_Protected {
return $rv;
}
- static function _get_tags($id, $owner_uid = 0, $tag_cache = false) {
+ /**
+ * @return array<int, string>
+ */
+ static function _get_tags(int $id, int $owner_uid = 0, ?string $tag_cache = null): array {
$a_id = $id;
@@ -383,12 +388,14 @@ class Article extends Handler_Protected {
/* check cache first */
- if ($tag_cache === false) {
+ if (!$tag_cache) {
$csth = $pdo->prepare("SELECT tag_cache FROM ttrss_user_entries
WHERE ref_id = ? AND owner_uid = ?");
$csth->execute([$id, $owner_uid]);
- if ($row = $csth->fetch()) $tag_cache = $row["tag_cache"];
+ if ($row = $csth->fetch()) {
+ $tag_cache = $row["tag_cache"];
+ }
}
if ($tag_cache) {
@@ -416,7 +423,7 @@ class Article extends Handler_Protected {
return $tags;
}
- function getmetadatabyid() {
+ function getmetadatabyid(): void {
$article = ORM::for_table('ttrss_entries')
->join('ttrss_user_entries', ['ref_id', '=', 'id'], 'ue')
->where('ue.owner_uid', $_SESSION['uid'])
@@ -429,7 +436,10 @@ class Article extends Handler_Protected {
}
}
- static function _get_enclosures($id) {
+ /**
+ * @return array<int, array<string, mixed>>
+ */
+ static function _get_enclosures(int $id): array {
$encs = ORM::for_table('ttrss_enclosures')
->where('post_id', $id)
->find_many();
@@ -452,7 +462,7 @@ class Article extends Handler_Protected {
}
- static function _purge_orphans() {
+ static function _purge_orphans(): void {
// purge orphaned posts in main content table
@@ -471,7 +481,11 @@ class Article extends Handler_Protected {
}
}
- static function _catchup_by_id($ids, $cmode, $owner_uid = false) {
+ /**
+ * @param array<int, int> $ids
+ * @param int $cmode Article::CATCHUP_MODE_*
+ */
+ static function _catchup_by_id($ids, int $cmode, ?int $owner_uid = null): void {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
@@ -479,11 +493,11 @@ class Article extends Handler_Protected {
$ids_qmarks = arr_qmarks($ids);
- if ($cmode == 1) {
+ if ($cmode == Article::CATCHUP_MODE_MARK_AS_UNREAD) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET
unread = true
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
- } else if ($cmode == 2) {
+ } else if ($cmode == Article::CATCHUP_MODE_TOGGLE) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET
unread = NOT unread,last_read = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
@@ -496,7 +510,10 @@ class Article extends Handler_Protected {
$sth->execute(array_merge($ids, [$owner_uid]));
}
- static function _get_labels($id, $owner_uid = false) {
+ /**
+ * @return array<int, array<int, int|string>>
+ */
+ static function _get_labels(int $id, ?int $owner_uid = null): array {
$rv = array();
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
@@ -536,6 +553,8 @@ class Article extends Handler_Protected {
}
if (count($rv) > 0)
+ // PHPStan has issues with the shape of $rv for some reason (array vs non-empty-array).
+ // @phpstan-ignore-next-line
Labels::update_cache($owner_uid, $id, $rv);
else
Labels::update_cache($owner_uid, $id, array("no-labels" => 1));
@@ -543,6 +562,12 @@ class Article extends Handler_Protected {
return $rv;
}
+ /**
+ * @param array<int, array<string, mixed>> $enclosures
+ * @param array<string, mixed> $headline
+ *
+ * @return array<int, Article::ARTICLE_KIND_*|string>
+ */
static function _get_image(array $enclosures, string $content, string $site_url, array $headline) {
$article_image = "";
@@ -577,6 +602,7 @@ class Article extends Handler_Protected {
} else if ($e->nodeName == "video") {
$article_image = $e->getAttribute("poster");
+ /** @var DOMElement|null $src */
$src = $tmpxpath->query("//source[@src]", $e)->item(0);
if ($src) {
@@ -603,14 +629,14 @@ class Article extends Handler_Protected {
}
if ($article_image) {
- $article_image = rewrite_relative_url($site_url, $article_image);
+ $article_image = UrlHelper::rewrite_relative($site_url, $article_image);
if (!$article_kind && (count($enclosures) > 1 || (isset($elems) && $elems->length > 1)))
$article_kind = Article::ARTICLE_KIND_ALBUM;
}
if ($article_stream)
- $article_stream = rewrite_relative_url($site_url, $article_stream);
+ $article_stream = UrlHelper::rewrite_relative($site_url, $article_stream);
}
$cache = new DiskCache("images");
@@ -624,7 +650,12 @@ class Article extends Handler_Protected {
return [$article_image, $article_stream, $article_kind];
}
- // only cached, returns label ids (not label feed ids)
+ /**
+ * only cached, returns label ids (not label feed ids)
+ *
+ * @param array<int, int> $article_ids
+ * @return array<int, int>
+ */
static function _labels_of(array $article_ids) {
if (count($article_ids) == 0)
return [];
@@ -651,6 +682,10 @@ class Article extends Handler_Protected {
return array_unique($rv);
}
+ /**
+ * @param array<int, int> $article_ids
+ * @return array<int, int>
+ */
static function _feeds_of(array $article_ids) {
if (count($article_ids) == 0)
return [];
diff --git a/classes/auth/base.php b/classes/auth/base.php
index 82ea06e1b..d8128400d 100644
--- a/classes/auth/base.php
+++ b/classes/auth/base.php
@@ -8,13 +8,18 @@ abstract class Auth_Base extends Plugin implements IAuthModule {
$this->pdo = Db::pdo();
}
- // compatibility wrapper, because of how pluginhost works (hook name == method name)
- function hook_auth_user(...$args) {
- return $this->authenticate(...$args);
+ function hook_auth_user($login, $password, $service = '') {
+ return $this->authenticate($login, $password, $service);
}
- // Auto-creates specified user if allowed by system configuration
- // Can be used instead of find_user_by_login() by external auth modules
+ /** Auto-creates specified user if allowed by system configuration.
+ * Can be used instead of find_user_by_login() by external auth modules
+ * @param string $login
+ * @param string|false $password
+ * @return null|int
+ * @throws Exception
+ * @throws PDOException
+ */
function auto_create_user(string $login, $password = false) {
if ($login && Config::get(Config::AUTH_AUTO_CREATE)) {
$user_id = UserHelper::find_user_by_login($login);
@@ -42,7 +47,12 @@ abstract class Auth_Base extends Plugin implements IAuthModule {
return UserHelper::find_user_by_login($login);
}
- // @deprecated
+
+ /** replaced with UserHelper::find_user_by_login()
+ * @param string $login
+ * @return null|int
+ * @deprecated
+ */
function find_user_by_login(string $login) {
return UserHelper::find_user_by_login($login);
}
diff --git a/classes/config.php b/classes/config.php
index 53a31b61c..6832846a2 100644
--- a/classes/config.php
+++ b/classes/config.php
@@ -231,9 +231,13 @@ class Config {
Config::T_STRING ],
];
+ /** @var Config|null */
private static $instance;
+ /** @var array<string, array<bool|int|string>> */
private $params = [];
+
+ /** @var array<string, mixed> */
private $version = [];
/** @var Db_Migrations|null $migrations */
@@ -268,10 +272,16 @@ class Config {
directory, its contents are displayed instead of git commit-based version, this could be generated
based on source git tree commit used when creating the package */
+ /**
+ * @return array<string, mixed>|string
+ */
static function get_version(bool $as_string = true) {
return self::get_instance()->_get_version($as_string);
}
+ /**
+ * @return array<string, mixed>|string
+ */
private function _get_version(bool $as_string = true) {
$root_dir = dirname(__DIR__);
@@ -289,7 +299,10 @@ class Config {
user_error("Unable to determine version: " . $this->version["version"], E_USER_WARNING);
$this->version["version"] = "UNKNOWN (Unsupported, Git error)";
+ } else if (!getenv("TTRSS_SELF_URL_PATH") || !file_exists("/.dockerenv")) {
+ $this->version["version"] .= " (Unsupported)";
}
+
} else {
$this->version["version"] = "UNKNOWN (Unsupported)";
}
@@ -298,7 +311,10 @@ class Config {
return $as_string ? $this->version["version"] : $this->version;
}
- static function get_version_from_git(string $dir) {
+ /**
+ * @return array<string, int|string>
+ */
+ static function get_version_from_git(string $dir): array {
$descriptorspec = [
1 => ["pipe", "w"], // STDOUT
2 => ["pipe", "w"], // STDERR
@@ -364,6 +380,9 @@ class Config {
return self::get_migrations()->get_version();
}
+ /**
+ * @return bool|int|string
+ */
static function cast_to(string $value, int $type_hint) {
switch ($type_hint) {
case self::T_BOOL:
@@ -375,24 +394,30 @@ class Config {
}
}
+ /**
+ * @return bool|int|string
+ */
private function _get(string $param) {
list ($value, $type_hint) = $this->params[$param];
return $this->cast_to($value, $type_hint);
}
- private function _add(string $param, string $default, int $type_hint) {
+ private function _add(string $param, string $default, int $type_hint): void {
$override = getenv(self::_ENVVAR_PREFIX . $param);
$this->params[$param] = [ self::cast_to($override !== false ? $override : $default, $type_hint), $type_hint ];
}
- static function add(string $param, string $default, int $type_hint = Config::T_STRING) {
+ static function add(string $param, string $default, int $type_hint = Config::T_STRING): void {
$instance = self::get_instance();
- return $instance->_add($param, $default, $type_hint);
+ $instance->_add($param, $default, $type_hint);
}
+ /**
+ * @return bool|int|string
+ */
static function get(string $param) {
$instance = self::get_instance();
@@ -431,6 +456,9 @@ class Config {
/* sanity check stuff */
+ /**
+ * @return array<int, array<string, string>> A list of entries identifying tt-rss tables with bad config
+ */
private static function check_mysql_tables() {
$pdo = Db::pdo();
@@ -447,7 +475,7 @@ class Config {
return $bad_tables;
}
- static function sanity_check() {
+ static function sanity_check(): void {
/*
we don't actually need the DB object right now but some checks below might use ORM which won't be initialized
@@ -621,11 +649,11 @@ class Config {
}
}
- private static function format_error($msg) {
+ private static function format_error(string $msg): string {
return "<div class=\"alert alert-danger\">$msg</div>";
}
- static function get_override_links() {
+ static function get_override_links(): string {
$rv = "";
$local_css = get_theme_path(self::get(self::LOCAL_OVERRIDE_STYLESHEET));
@@ -637,7 +665,7 @@ class Config {
return $rv;
}
- static function get_user_agent() {
+ static function get_user_agent(): string {
return sprintf(self::get(self::HTTP_USER_AGENT), self::get_version());
}
}
diff --git a/classes/counters.php b/classes/counters.php
index 8a8b8bc71..c42e938f8 100644
--- a/classes/counters.php
+++ b/classes/counters.php
@@ -1,7 +1,10 @@
<?php
class Counters {
- static function get_all() {
+ /**
+ * @return array<int, array<string, int|string>>
+ */
+ static function get_all(): array {
return array_merge(
self::get_global(),
self::get_virt(),
@@ -11,7 +14,12 @@ class Counters {
);
}
- static function get_conditional(array $feed_ids = null, array $label_ids = null) {
+ /**
+ * @param array<int> $feed_ids
+ * @param array<int> $label_ids
+ * @return array<int, array<string, int|string>>
+ */
+ static function get_conditional(array $feed_ids = null, array $label_ids = null): array {
return array_merge(
self::get_global(),
self::get_virt(),
@@ -21,7 +29,10 @@ class Counters {
);
}
- static private function get_cat_children(int $cat_id, int $owner_uid) {
+ /**
+ * @return array<int, int>
+ */
+ static private function get_cat_children(int $cat_id, int $owner_uid): array {
$unread = 0;
$marked = 0;
@@ -40,7 +51,11 @@ class Counters {
return [$unread, $marked];
}
- private static function get_cats(array $cat_ids = null) {
+ /**
+ * @param array<int> $cat_ids
+ * @return array<int, array<string, int|string>>
+ */
+ private static function get_cats(array $cat_ids = null): array {
$ret = [];
/* Labels category */
@@ -129,7 +144,11 @@ class Counters {
return $ret;
}
- private static function get_feeds(array $feed_ids = null) {
+ /**
+ * @param array<int> $feed_ids
+ * @return array<int, array<string, int|string>>
+ */
+ private static function get_feeds(array $feed_ids = null): array {
$ret = [];
@@ -199,7 +218,10 @@ class Counters {
return $ret;
}
- private static function get_global() {
+ /**
+ * @return array<int, array<string, int|string>>
+ */
+ private static function get_global(): array {
$ret = [
[
"id" => "global-unread",
@@ -219,7 +241,10 @@ class Counters {
return $ret;
}
- private static function get_virt() {
+ /**
+ * @return array<int, array<string, int|string>>
+ */
+ private static function get_virt(): array {
$ret = [];
@@ -248,6 +273,11 @@ class Counters {
if (is_array($feeds)) {
foreach ($feeds as $feed) {
+ /** @var IVirtualFeed $feed['sender'] */
+
+ if (!implements_interface($feed['sender'], 'IVirtualFeed'))
+ continue;
+
$cv = [
"id" => PluginHost::pfeed_to_feed_id($feed['id']),
"counter" => $feed['sender']->get_unread($feed['id'])
@@ -263,7 +293,11 @@ class Counters {
return $ret;
}
- static function get_labels(array $label_ids = null) {
+ /**
+ * @param array<int> $label_ids
+ * @return array<int, array<string, int|string>>
+ */
+ static function get_labels(array $label_ids = null): array {
$ret = [];
diff --git a/classes/db.php b/classes/db.php
index 7b669cf32..2cc89f5ba 100755
--- a/classes/db.php
+++ b/classes/db.php
@@ -17,7 +17,7 @@ class Db
}
}
- static function NOW() {
+ static function NOW(): string {
return date("Y-m-d H:i:s", time());
}
@@ -25,7 +25,7 @@ class Db
//
}
- public static function get_dsn() {
+ public static function get_dsn(): string {
$db_port = Config::get(Config::DB_PORT) ? ';port=' . Config::get(Config::DB_PORT) : '';
$db_host = Config::get(Config::DB_HOST) ? ';host=' . Config::get(Config::DB_HOST) : '';
if (Config::get(Config::DB_TYPE) == "mysql" && Config::get(Config::MYSQL_CHARSET)) {
@@ -88,12 +88,11 @@ class Db
return self::$instance->pdo;
}
- public static function sql_random_function() {
+ public static function sql_random_function(): string {
if (Config::get(Config::DB_TYPE) == "mysql") {
return "RAND()";
- } else {
- return "RANDOM()";
}
+ return "RANDOM()";
}
}
diff --git a/classes/db/migrations.php b/classes/db/migrations.php
index 3008af535..aecd9186c 100644
--- a/classes/db/migrations.php
+++ b/classes/db/migrations.php
@@ -1,29 +1,46 @@
<?php
class Db_Migrations {
+ // TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
+ /** @var string */
private $base_filename = "schema.sql";
+
+ /** @var string */
private $base_path;
+
+ /** @var string */
private $migrations_path;
+
+ /** @var string */
private $migrations_table;
+
+ /** @var bool */
private $base_is_latest;
+
+ /** @var PDO */
private $pdo;
- private $cached_version;
- private $cached_max_version;
+ /** @var int */
+ private $cached_version = 0;
+
+ /** @var int */
+ private $cached_max_version = 0;
+
+ /** @var int */
private $max_version_override;
function __construct() {
$this->pdo = Db::pdo();
}
- function initialize_for_plugin(Plugin $plugin, bool $base_is_latest = true, string $schema_suffix = "sql") {
+ function initialize_for_plugin(Plugin $plugin, bool $base_is_latest = true, string $schema_suffix = "sql"): void {
$plugin_dir = PluginHost::getInstance()->get_plugin_dir($plugin);
$this->initialize($plugin_dir . "/${schema_suffix}",
strtolower("ttrss_migrations_plugin_" . get_class($plugin)),
$base_is_latest);
}
- function initialize(string $root_path, string $migrations_table, bool $base_is_latest = true, int $max_version_override = 0) {
+ function initialize(string $root_path, string $migrations_table, bool $base_is_latest = true, int $max_version_override = 0): void {
$this->base_path = "$root_path/" . Config::get(Config::DB_TYPE);
$this->migrations_path = $this->base_path . "/migrations";
$this->migrations_table = $migrations_table;
@@ -31,7 +48,7 @@ class Db_Migrations {
$this->max_version_override = $max_version_override;
}
- private function set_version(int $version) {
+ private function set_version(int $version): void {
Debug::log("Updating table {$this->migrations_table} with version ${version}...", Debug::LOG_EXTENDED);
$sth = $this->pdo->query("SELECT * FROM {$this->migrations_table}");
@@ -48,7 +65,7 @@ class Db_Migrations {
}
function get_version() : int {
- if (isset($this->cached_version))
+ if ($this->cached_version)
return $this->cached_version;
try {
@@ -66,11 +83,15 @@ class Db_Migrations {
}
}
- private function create_migrations_table() {
+ private function create_migrations_table(): void {
$this->pdo->query("CREATE TABLE IF NOT EXISTS {$this->migrations_table} (schema_version integer not null)");
}
- private function migrate_to(int $version) {
+ /**
+ * @throws PDOException
+ * @return bool false if the migration failed, otherwise true (or an exception)
+ */
+ private function migrate_to(int $version): bool {
try {
if ($version <= $this->get_version()) {
Debug::log("Refusing to apply version $version: current version is higher", Debug::LOG_VERBOSE);
@@ -110,8 +131,10 @@ class Db_Migrations {
Debug::log("Migration finished, current version: " . $this->get_version(), Debug::LOG_VERBOSE);
Logger::log(E_USER_NOTICE, "Applied migration to version $version for {$this->migrations_table}");
+ return true;
} else {
Debug::log("Migration failed: schema file is empty or missing.", Debug::LOG_VERBOSE);
+ return false;
}
} catch (PDOException $e) {
@@ -129,7 +152,7 @@ class Db_Migrations {
if ($this->max_version_override > 0)
return $this->max_version_override;
- if (isset($this->cached_max_version))
+ if ($this->cached_max_version)
return $this->cached_max_version;
$migrations = glob("{$this->migrations_path}/*.sql");
@@ -174,6 +197,9 @@ class Db_Migrations {
return !$this->is_migration_needed();
}
+ /**
+ * @return array<int, string>
+ */
private function get_lines(int $version) : array {
if ($version > 0)
$filename = "{$this->migrations_path}/${version}.sql";
diff --git a/classes/db/prefs.php b/classes/db/prefs.php
index 821216622..209ef58c1 100644
--- a/classes/db/prefs.php
+++ b/classes/db/prefs.php
@@ -2,11 +2,17 @@
class Db_Prefs {
// this class is a stub for the time being (to be removed)
- function read($pref_name, $user_id = false, $die_on_error = false) {
+ /**
+ * @return bool|int|null|string
+ */
+ function read(string $pref_name, ?int $user_id = null, bool $die_on_error = false) {
return get_pref($pref_name, $user_id);
}
- function write($pref_name, $value, $user_id = false, $strip_tags = true) {
+ /**
+ * @param mixed $value
+ */
+ function write(string $pref_name, $value, ?int $user_id = null, bool $strip_tags = true): bool {
return set_pref($pref_name, $value, $user_id, $strip_tags);
}
}
diff --git a/classes/debug.php b/classes/debug.php
index 2ae81e41a..e20126b86 100644
--- a/classes/debug.php
+++ b/classes/debug.php
@@ -1,52 +1,90 @@
<?php
class Debug {
const LOG_DISABLED = -1;
- const LOG_NORMAL = 0;
- const LOG_VERBOSE = 1;
- const LOG_EXTENDED = 2;
-
- /** @deprecated */
+ const LOG_NORMAL = 0;
+ const LOG_VERBOSE = 1;
+ const LOG_EXTENDED = 2;
+
+ const ALL_LOG_LEVELS = [
+ Debug::LOG_DISABLED,
+ Debug::LOG_NORMAL,
+ Debug::LOG_VERBOSE,
+ Debug::LOG_EXTENDED,
+ ];
+
+ // TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
+ /**
+ * @deprecated
+ * @var int
+ */
public static $LOG_DISABLED = self::LOG_DISABLED;
- /** @deprecated */
- public static $LOG_NORMAL = self::LOG_NORMAL;
+ /**
+ * @deprecated
+ * @var int
+ */
+ public static $LOG_NORMAL = self::LOG_NORMAL;
+
+ /**
+ * @deprecated
+ * @var int
+ */
+ public static $LOG_VERBOSE = self::LOG_VERBOSE;
+
+ /**
+ * @deprecated
+ * @var int
+ */
+ public static $LOG_EXTENDED = self::LOG_EXTENDED;
- /** @deprecated */
- public static $LOG_VERBOSE = self::LOG_VERBOSE;
+ /** @var bool */
+ private static $enabled = false;
- /** @deprecated */
- public static $LOG_EXTENDED = self::LOG_EXTENDED;
+ /** @var bool */
+ private static $quiet = false;
- private static $enabled = false;
- private static $quiet = false;
- private static $logfile = false;
+ /** @var string|null */
+ private static $logfile = null;
+
+ /**
+ * @var int Debug::LOG_*
+ */
private static $loglevel = self::LOG_NORMAL;
- public static function set_logfile($logfile) {
+ public static function set_logfile(string $logfile): void {
self::$logfile = $logfile;
}
- public static function enabled() {
+ public static function enabled(): bool {
return self::$enabled;
}
- public static function set_enabled($enable) {
+ public static function set_enabled(bool $enable): void {
self::$enabled = $enable;
}
- public static function set_quiet($quiet) {
+ public static function set_quiet(bool $quiet): void {
self::$quiet = $quiet;
}
- public static function set_loglevel($level) {
+ /**
+ * @param int $level Debug::LOG_*
+ */
+ public static function set_loglevel(int $level): void {
self::$loglevel = $level;
}
- public static function get_loglevel() {
+ /**
+ * @return int Debug::LOG_*
+ */
+ public static function get_loglevel(): int {
return self::$loglevel;
}
- public static function log($message, int $level = 0) {
+ /**
+ * @param int $level Debug::LOG_*
+ */
+ public static function log(string $message, int $level = Debug::LOG_NORMAL): bool {
if (!self::$enabled || self::$loglevel < $level) return false;
@@ -73,7 +111,7 @@ class Debug {
if (!$locked) {
fclose($fp);
user_error("Unable to lock debugging log file: " . self::$logfile, E_USER_WARNING);
- return;
+ return false;
}
}
@@ -86,7 +124,7 @@ class Debug {
fclose($fp);
if (self::$quiet)
- return;
+ return false;
} else {
user_error("Unable to open debugging log file: " . self::$logfile, E_USER_WARNING);
@@ -94,5 +132,7 @@ class Debug {
}
print "[$ts] $message\n";
+
+ return true;
}
}
diff --git a/classes/digest.php b/classes/digest.php
index 94e5cd1fc..15203166b 100644
--- a/classes/digest.php
+++ b/classes/digest.php
@@ -1,7 +1,7 @@
<?php
class Digest
{
- static function send_headlines_digests() {
+ static function send_headlines_digests(): void {
$user_limit = 15; // amount of users to process (e.g. emails to send out)
$limit = 1000; // maximum amount of headlines to include
@@ -62,7 +62,7 @@ class Digest
if ($rc && $do_catchup) {
Debug::log("Marking affected articles as read...");
- Article::_catchup_by_id($affected_ids, 0, $line["id"]);
+ Article::_catchup_by_id($affected_ids, Article::CATCHUP_MODE_MARK_AS_READ, $line["id"]);
}
} else {
Debug::log("No headlines");
@@ -78,6 +78,9 @@ class Digest
Debug::log("All done.");
}
+ /**
+ * @return array{0: string, 1: int, 2: array<int>, 3: string}
+ */
static function prepare_headlines_digest(int $user_id, int $days = 1, int $limit = 1000) {
$tpl = new Templator();
diff --git a/classes/diskcache.php b/classes/diskcache.php
index d7ea26d3b..0df8d7cd4 100644
--- a/classes/diskcache.php
+++ b/classes/diskcache.php
@@ -1,9 +1,15 @@
<?php
class DiskCache {
+ // TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
+ /** @var string */
private $dir;
- // https://stackoverflow.com/a/53662733
- private $mimeMap = [
+ /**
+ * https://stackoverflow.com/a/53662733
+ *
+ * @var array<string, string>
+ */
+ private array $mimeMap = [
'video/3gpp2' => '3g2',
'video/3gp' => '3gp',
'video/3gpp' => '3gp',
@@ -190,21 +196,22 @@ class DiskCache {
'text/x-scriptzsh' => 'zsh'
];
- public function __construct($dir) {
+ public function __construct(string $dir) {
$this->dir = Config::get(Config::CACHE_DIR) . "/" . basename(clean($dir));
}
- public function get_dir() {
+ public function get_dir(): string {
return $this->dir;
}
- public function make_dir() {
+ public function make_dir(): bool {
if (!is_dir($this->dir)) {
return mkdir($this->dir);
}
+ return false;
}
- public function is_writable($filename = "") {
+ public function is_writable(?string $filename = null): bool {
if ($filename) {
if (file_exists($this->get_full_path($filename)))
return is_writable($this->get_full_path($filename));
@@ -215,44 +222,75 @@ class DiskCache {
}
}
- public function exists($filename) {
+ public function exists(string $filename): bool {
return file_exists($this->get_full_path($filename));
}
- public function get_size($filename) {
+ /**
+ * @return int|false -1 if the file doesn't exist, false if an error occurred, size in bytes otherwise
+ */
+ public function get_size(string $filename) {
if ($this->exists($filename))
return filesize($this->get_full_path($filename));
else
return -1;
}
- public function get_full_path($filename) {
+ public function get_full_path(string $filename): string {
return $this->dir . "/" . basename(clean($filename));
}
- public function put($filename, $data) {
+ /**
+ * @param mixed $data
+ *
+ * @return int|false Bytes written or false if an error occurred.
+ */
+ public function put(string $filename, $data) {
return file_put_contents($this->get_full_path($filename), $data);
}
- public function touch($filename) {
+ public function touch(string $filename): bool {
return touch($this->get_full_path($filename));
}
- public function get($filename) {
+ /** Downloads $url to cache as $local_filename if its missing (unless $force-ed)
+ * @param string $url
+ * @param string $local_filename
+ * @param array<string,string|int|false> $options (additional params to UrlHelper::fetch())
+ * @param bool $force
+ * @return bool
+ */
+ public function download(string $url, string $local_filename, array $options = [], bool $force = false) : bool {
+ if ($this->exists($local_filename) && !$force)
+ return true;
+
+ $data = UrlHelper::fetch(array_merge(["url" => $url,
+ "max_size" => Config::get(Config::MAX_CACHE_FILE_SIZE)], $options));
+
+ if ($data)
+ return $this->put($local_filename, $data) > 0;
+
+ return false;
+ }
+
+ public function get(string $filename): ?string {
if ($this->exists($filename))
return file_get_contents($this->get_full_path($filename));
else
return null;
}
- public function get_mime_type($filename) {
+ /**
+ * @return false|null|string false if detection failed, null if the file doesn't exist, string mime content type otherwise
+ */
+ public function get_mime_type(string $filename) {
if ($this->exists($filename))
return mime_content_type($this->get_full_path($filename));
else
return null;
}
- public function get_fake_extension($filename) {
+ public function get_fake_extension(string $filename): string {
$mimetype = $this->get_mime_type($filename);
if ($mimetype)
@@ -261,7 +299,10 @@ class DiskCache {
return "";
}
- public function send($filename) {
+ /**
+ * @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent
+ */
+ public function send(string $filename) {
$fake_extension = $this->get_fake_extension($filename);
if ($fake_extension)
@@ -272,7 +313,7 @@ class DiskCache {
return $this->send_local_file($this->get_full_path($filename));
}
- public function get_url($filename) {
+ public function get_url(string $filename): string {
return Config::get_self_url() . "/public.php?op=cached&file=" . basename($this->dir) . "/" . basename($filename);
}
@@ -280,8 +321,7 @@ class DiskCache {
// this is called separately after sanitize() and plugin render article hooks to allow
// plugins work on original source URLs used before caching
// NOTE: URLs should be already absolutized because this is called after sanitize()
- static public function rewrite_urls($str)
- {
+ static public function rewrite_urls(string $str): string {
$res = trim($str);
if (!$res) return '';
@@ -338,7 +378,7 @@ class DiskCache {
return $res;
}
- static function expire() {
+ static function expire(): void {
$dirs = array_filter(glob(Config::get(Config::CACHE_DIR) . "/*"), "is_dir");
foreach ($dirs as $cache_dir) {
@@ -362,14 +402,19 @@ class DiskCache {
}
}
- /* this is essentially a wrapper for readfile() which allows plugins to hook
- output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
-
- hook function should return true if request was handled (or at least attempted to)
-
- note that this can be called without user context so the plugin to handle this
- should be loaded systemwide in config.php */
- function send_local_file($filename) {
+ /* */
+ /**
+ * this is essentially a wrapper for readfile() which allows plugins to hook
+ * output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
+ *
+ * hook function should return true if request was handled (or at least attempted to)
+ *
+ * note that this can be called without user context so the plugin to handle this
+ * should be loaded systemwide in config.php
+ *
+ * @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent
+ */
+ function send_local_file(string $filename) {
if (file_exists($filename)) {
if (is_writable($filename)) touch($filename);
diff --git a/classes/errors.php b/classes/errors.php
index 3599c2639..31be558cf 100644
--- a/classes/errors.php
+++ b/classes/errors.php
@@ -7,7 +7,11 @@ class Errors {
const E_SCHEMA_MISMATCH = "E_SCHEMA_MISMATCH";
const E_URL_SCHEME_MISMATCH = "E_URL_SCHEME_MISMATCH";
- static function to_json(string $code, array $params = []) {
+ /**
+ * @param Errors::E_* $code
+ * @param array<string, string> $params
+ */
+ static function to_json(string $code, array $params = []): string {
return json_encode(["error" => ["code" => $code, "params" => $params]]);
}
}
diff --git a/classes/feedenclosure.php b/classes/feedenclosure.php
index 2435f6854..b5f5cc411 100644
--- a/classes/feedenclosure.php
+++ b/classes/feedenclosure.php
@@ -1,10 +1,21 @@
<?php
class FeedEnclosure {
+ /** @var string */
public $link;
+
+ /** @var string */
public $type;
+
+ /** @var string */
public $length;
+
+ /** @var string */
public $title;
+
+ /** @var string */
public $height;
+
+ /** @var string */
public $width;
}
diff --git a/classes/feeditem.php b/classes/feeditem.php
index 3a5e5dc09..fd7c54883 100644
--- a/classes/feeditem.php
+++ b/classes/feeditem.php
@@ -1,16 +1,24 @@
<?php
abstract class FeedItem {
- abstract function get_id();
+ abstract function get_id(): string;
+
+ /** @return int|false a timestamp on success, false otherwise */
abstract function get_date();
- abstract function get_link();
- abstract function get_title();
- abstract function get_description();
- abstract function get_content();
- abstract function get_comments_url();
- abstract function get_comments_count();
- abstract function get_categories();
- abstract function get_enclosures();
- abstract function get_author();
- abstract function get_language();
+
+ abstract function get_link(): string;
+ abstract function get_title(): string;
+ abstract function get_description(): string;
+ abstract function get_content(): string;
+ abstract function get_comments_url(): string;
+ abstract function get_comments_count(): int;
+
+ /** @return array<int, string> */
+ abstract function get_categories(): array;
+
+ /** @return array<int, FeedEnclosure> */
+ abstract function get_enclosures(): array;
+
+ abstract function get_author(): string;
+ abstract function get_language(): string;
}
diff --git a/classes/feeditem/atom.php b/classes/feeditem/atom.php
index 51358f36c..36a2e91f5 100755
--- a/classes/feeditem/atom.php
+++ b/classes/feeditem/atom.php
@@ -2,7 +2,7 @@
class FeedItem_Atom extends FeedItem_Common {
const NS_XML = "http://www.w3.org/XML/1998/namespace";
- function get_id() {
+ function get_id(): string {
$id = $this->elem->getElementsByTagName("id")->item(0);
if ($id) {
@@ -12,6 +12,9 @@ class FeedItem_Atom extends FeedItem_Common {
}
}
+ /**
+ * @return int|false a timestamp on success, false otherwise
+ */
function get_date() {
$updated = $this->elem->getElementsByTagName("updated")->item(0);
@@ -30,10 +33,13 @@ class FeedItem_Atom extends FeedItem_Common {
if ($date) {
return strtotime($date->nodeValue);
}
+
+ // consistent with strtotime failing to parse
+ return false;
}
- function get_link() {
+ function get_link(): string {
$links = $this->elem->getElementsByTagName("link");
foreach ($links as $link) {
@@ -44,24 +50,27 @@ class FeedItem_Atom extends FeedItem_Common {
$base = $this->xpath->evaluate("string(ancestor-or-self::*[@xml:base][1]/@xml:base)", $link);
if ($base)
- return rewrite_relative_url($base, clean(trim($link->getAttribute("href"))));
+ return UrlHelper::rewrite_relative($base, clean(trim($link->getAttribute("href"))));
else
return clean(trim($link->getAttribute("href")));
-
}
}
+
+ return '';
}
- function get_title() {
+ function get_title(): string {
$title = $this->elem->getElementsByTagName("title")->item(0);
-
- if ($title) {
- return clean(trim($title->nodeValue));
- }
+ return $title ? clean(trim($title->nodeValue)) : '';
}
- /** $base is optional (returns $content if $base is null), $content is an HTML string */
- private function rewrite_content_to_base($base, $content) {
+ /**
+ * @param string|null $base optional (returns $content if $base is null)
+ * @param string $content an HTML string
+ *
+ * @return string the rewritten XML or original $content
+ */
+ private function rewrite_content_to_base(?string $base = null, string $content) {
if (!empty($base) && !empty($content)) {
@@ -81,14 +90,17 @@ class FeedItem_Atom extends FeedItem_Common {
}
}
- return $tmpdoc->saveXML();
+ // Fall back to $content if saveXML somehow fails (i.e. returns false)
+ $modified_content = $tmpdoc->saveXML();
+ return $modified_content !== false ? $modified_content : $content;
}
}
return $content;
}
- function get_content() {
+ function get_content(): string {
+ /** @var DOMElement|null */
$content = $this->elem->getElementsByTagName("content")->item(0);
if ($content) {
@@ -108,10 +120,13 @@ class FeedItem_Atom extends FeedItem_Common {
return $this->rewrite_content_to_base($base, $this->subtree_or_text($content));
}
+
+ return '';
}
// TODO: duplicate code should be merged with get_content()
- function get_description() {
+ function get_description(): string {
+ /** @var DOMElement|null */
$content = $this->elem->getElementsByTagName("summary")->item(0);
if ($content) {
@@ -132,9 +147,13 @@ class FeedItem_Atom extends FeedItem_Common {
return $this->rewrite_content_to_base($base, $this->subtree_or_text($content));
}
+ return '';
}
- function get_categories() {
+ /**
+ * @return array<int, string>
+ */
+ function get_categories(): array {
$categories = $this->elem->getElementsByTagName("category");
$cats = [];
@@ -152,7 +171,10 @@ class FeedItem_Atom extends FeedItem_Common {
return $this->normalize_categories($cats);
}
- function get_enclosures() {
+ /**
+ * @return array<int, FeedEnclosure>
+ */
+ function get_enclosures(): array {
$links = $this->elem->getElementsByTagName("link");
$encs = [];
@@ -182,7 +204,7 @@ class FeedItem_Atom extends FeedItem_Common {
return $encs;
}
- function get_language() {
+ function get_language(): string {
$lang = $this->elem->getAttributeNS(self::NS_XML, "lang");
if (!empty($lang)) {
@@ -195,5 +217,6 @@ class FeedItem_Atom extends FeedItem_Common {
}
}
}
+ return '';
}
}
diff --git a/classes/feeditem/common.php b/classes/feeditem/common.php
index 18afeaa94..6a9be8aca 100755
--- a/classes/feeditem/common.php
+++ b/classes/feeditem/common.php
@@ -1,16 +1,20 @@
<?php
abstract class FeedItem_Common extends FeedItem {
+ /** @var DOMElement */
protected $elem;
- protected $xpath;
+
+ /** @var DOMDocument */
protected $doc;
- function __construct($elem, $doc, $xpath) {
+ /** @var DOMXPath */
+ protected $xpath;
+
+ function __construct(DOMElement $elem, DOMDocument $doc, DOMXPath $xpath) {
$this->elem = $elem;
$this->xpath = $xpath;
$this->doc = $doc;
try {
-
$source = $elem->getElementsByTagName("source")->item(0);
// we don't need <source> element
@@ -21,11 +25,12 @@ abstract class FeedItem_Common extends FeedItem {
}
}
- function get_element() {
+ function get_element(): DOMElement {
return $this->elem;
}
- function get_author() {
+ function get_author(): string {
+ /** @var DOMElement|null */
$author = $this->elem->getElementsByTagName("author")->item(0);
if ($author) {
@@ -51,7 +56,7 @@ abstract class FeedItem_Common extends FeedItem {
return implode(", ", $authors);
}
- function get_comments_url() {
+ function get_comments_url(): string {
//RSS only. Use a query here to avoid namespace clashes (e.g. with slash).
//might give a wrong result if a default namespace was declared (possible with XPath 2.0)
$com_url = $this->xpath->query("comments", $this->elem)->item(0);
@@ -65,20 +70,28 @@ abstract class FeedItem_Common extends FeedItem {
if ($com_url)
return clean($com_url->nodeValue);
+
+ return '';
}
- function get_comments_count() {
+ function get_comments_count(): int {
//also query for ATE stuff here
$query = "slash:comments|thread:total|atom:link[@rel='replies']/@thread:count";
$comments = $this->xpath->query($query, $this->elem)->item(0);
- if ($comments) {
- return clean($comments->nodeValue);
+ if ($comments && is_numeric($comments->nodeValue)) {
+ return (int) clean($comments->nodeValue);
}
+
+ return 0;
}
- // this is common for both Atom and RSS types and deals with various media: elements
- function get_enclosures() {
+ /**
+ * this is common for both Atom and RSS types and deals with various 'media:' elements
+ *
+ * @return array<int, FeedEnclosure>
+ */
+ function get_enclosures(): array {
$encs = [];
$enclosures = $this->xpath->query("media:content", $this->elem);
@@ -108,6 +121,7 @@ abstract class FeedItem_Common extends FeedItem {
foreach ($enclosures as $enclosure) {
$enc = new FeedEnclosure();
+ /** @var DOMElement|null */
$content = $this->xpath->query("media:content", $enclosure)->item(0);
if ($content) {
@@ -150,11 +164,14 @@ abstract class FeedItem_Common extends FeedItem {
return $encs;
}
- function count_children($node) {
+ function count_children(DOMElement $node): int {
return $node->getElementsByTagName("*")->length;
}
- function subtree_or_text($node) {
+ /**
+ * @return false|string false on failure, otherwise string contents
+ */
+ function subtree_or_text(DOMElement $node) {
if ($this->count_children($node) == 0) {
return $node->nodeValue;
} else {
@@ -162,7 +179,12 @@ abstract class FeedItem_Common extends FeedItem {
}
}
- static function normalize_categories($cats) {
+ /**
+ * @param array<int, string> $cats
+ *
+ * @return array<int, string>
+ */
+ static function normalize_categories(array $cats): array {
$tmp = [];
diff --git a/classes/feeditem/rss.php b/classes/feeditem/rss.php
index 1f7953c51..7017d04e9 100755
--- a/classes/feeditem/rss.php
+++ b/classes/feeditem/rss.php
@@ -1,6 +1,6 @@
<?php
class FeedItem_RSS extends FeedItem_Common {
- function get_id() {
+ function get_id(): string {
$id = $this->elem->getElementsByTagName("guid")->item(0);
if ($id) {
@@ -10,6 +10,9 @@ class FeedItem_RSS extends FeedItem_Common {
}
}
+ /**
+ * @return int|false a timestamp on success, false otherwise
+ */
function get_date() {
$pubDate = $this->elem->getElementsByTagName("pubDate")->item(0);
@@ -22,9 +25,12 @@ class FeedItem_RSS extends FeedItem_Common {
if ($date) {
return strtotime($date->nodeValue);
}
+
+ // consistent with strtotime failing to parse
+ return false;
}
- function get_link() {
+ function get_link(): string {
$links = $this->xpath->query("atom:link", $this->elem);
foreach ($links as $link) {
@@ -37,6 +43,7 @@ class FeedItem_RSS extends FeedItem_Common {
}
}
+ /** @var DOMElement|null */
$link = $this->elem->getElementsByTagName("guid")->item(0);
if ($link && $link->hasAttributes() && $link->getAttribute("isPermaLink") == "true") {
@@ -48,9 +55,11 @@ class FeedItem_RSS extends FeedItem_Common {
if ($link) {
return clean(trim($link->nodeValue));
}
+
+ return '';
}
- function get_title() {
+ function get_title(): string {
$title = $this->xpath->query("title", $this->elem)->item(0);
if ($title) {
@@ -64,10 +73,15 @@ class FeedItem_RSS extends FeedItem_Common {
if ($title) {
return clean(trim($title->nodeValue));
}
+
+ return '';
}
- function get_content() {
+ function get_content(): string {
+ /** @var DOMElement|null */
$contentA = $this->xpath->query("content:encoded", $this->elem)->item(0);
+
+ /** @var DOMElement|null */
$contentB = $this->elem->getElementsByTagName("description")->item(0);
if ($contentA && !$contentB) {
@@ -85,17 +99,24 @@ class FeedItem_RSS extends FeedItem_Common {
return mb_strlen($resultA) > mb_strlen($resultB) ? $resultA : $resultB;
}
+
+ return '';
}
- function get_description() {
+ function get_description(): string {
$summary = $this->elem->getElementsByTagName("description")->item(0);
if ($summary) {
return $summary->nodeValue;
}
+
+ return '';
}
- function get_categories() {
+ /**
+ * @return array<int, string>
+ */
+ function get_categories(): array {
$categories = $this->elem->getElementsByTagName("category");
$cats = [];
@@ -112,7 +133,10 @@ class FeedItem_RSS extends FeedItem_Common {
return $this->normalize_categories($cats);
}
- function get_enclosures() {
+ /**
+ * @return array<int, FeedEnclosure>
+ */
+ function get_enclosures(): array {
$enclosures = $this->elem->getElementsByTagName("enclosure");
$encs = array();
@@ -134,7 +158,7 @@ class FeedItem_RSS extends FeedItem_Common {
return $encs;
}
- function get_language() {
+ function get_language(): string {
$languages = $this->doc->getElementsByTagName('language');
if (count($languages) == 0) {
@@ -143,5 +167,4 @@ class FeedItem_RSS extends FeedItem_Common {
return clean($languages[0]->textContent);
}
-
}
diff --git a/classes/feedparser.php b/classes/feedparser.php
index daba271fb..6ce69cc89 100644
--- a/classes/feedparser.php
+++ b/classes/feedparser.php
@@ -1,19 +1,35 @@
<?php
class FeedParser {
+
+ /** @var DOMDocument */
private $doc;
- private $error;
- private $libxml_errors = array();
- private $items;
+
+ /** @var string|null */
+ private $error = null;
+
+ /** @var array<string> */
+ private $libxml_errors = [];
+
+ /** @var array<FeedItem> */
+ private $items = [];
+
+ /** @var string|null */
private $link;
+
+ /** @var string|null */
private $title;
+
+ /** @var FeedParser::FEED_*|null */
private $type;
+
+ /** @var DOMXPath|null */
private $xpath;
const FEED_RDF = 0;
const FEED_RSS = 1;
const FEED_ATOM = 2;
- function __construct($data) {
+ function __construct(string $data) {
libxml_use_internal_errors(true);
libxml_clear_errors();
$this->doc = new DOMDocument();
@@ -26,18 +42,18 @@ class FeedParser {
if ($error) {
foreach (libxml_get_errors() as $error) {
if ($error->level == LIBXML_ERR_FATAL) {
- if(!isset($this->error)) //currently only the first error is reported
+ // currently only the first error is reported
+ if (!isset($this->error)) {
$this->error = $this->format_error($error);
- $this->libxml_errors [] = $this->format_error($error);
+ }
+ $this->libxml_errors[] = $this->format_error($error);
}
}
}
libxml_clear_errors();
-
- $this->items = array();
}
- function init() {
+ function init() : void {
$root = $this->doc->firstChild;
$xpath = new DOMXPath($this->doc);
$xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
@@ -51,10 +67,12 @@ class FeedParser {
$this->xpath = $xpath;
- $root = $xpath->query("(//atom03:feed|//atom:feed|//channel|//rdf:rdf|//rdf:RDF)");
+ $root_list = $xpath->query("(//atom03:feed|//atom:feed|//channel|//rdf:rdf|//rdf:RDF)");
+
+ if (!empty($root_list) && $root_list->length > 0) {
- if (!empty($root) && $root->length > 0) {
- $root = $root->item(0);
+ /** @var DOMElement|null $root */
+ $root = $root_list->item(0);
if ($root) {
switch (mb_strtolower($root->tagName)) {
@@ -69,7 +87,7 @@ class FeedParser {
$this->type = $this::FEED_ATOM;
break;
default:
- if( !isset($this->error) ){
+ if (!isset($this->error)) {
$this->error = "Unknown/unsupported feed type";
}
return;
@@ -100,6 +118,7 @@ class FeedParser {
if (!$link)
$link = $xpath->query("//atom03:feed/atom03:link[@rel='alternate']")->item(0);
+ /** @var DOMElement|null $link */
if ($link && $link->hasAttributes()) {
$this->link = $link->getAttribute("href");
}
@@ -121,6 +140,7 @@ class FeedParser {
$this->title = $title->nodeValue;
}
+ /** @var DOMElement|null $link */
$link = $xpath->query("//channel/link")->item(0);
if ($link) {
@@ -166,46 +186,44 @@ class FeedParser {
if ($this->link) $this->link = trim($this->link);
} else {
- if( !isset($this->error) ){
+ if (!isset($this->error)) {
$this->error = "Unknown/unsupported feed type";
}
return;
}
}
- function format_error($error) {
- if ($error) {
- return sprintf("LibXML error %s at line %d (column %d): %s",
- $error->code, $error->line, $error->column,
- $error->message);
- } else {
- return "";
- }
+ function format_error(LibXMLError $error) : string {
+ return sprintf("LibXML error %s at line %d (column %d): %s",
+ $error->code, $error->line, $error->column,
+ $error->message);
}
// libxml may have invalid unicode data in error messages
- function error() {
+ function error() : string {
return UConverter::transcode($this->error, 'UTF-8', 'UTF-8');
}
- // WARNING: may return invalid unicode data
- function errors() {
+ /** @return array<string> - WARNING: may return invalid unicode data */
+ function errors() : array {
return $this->libxml_errors;
}
- function get_link() {
- return clean($this->link);
+ function get_link() : string {
+ return clean($this->link ?? '');
}
- function get_title() {
- return clean($this->title);
+ function get_title() : string {
+ return clean($this->title ?? '');
}
- function get_items() {
+ /** @return array<FeedItem> */
+ function get_items() : array {
return $this->items;
}
- function get_links($rel) {
+ /** @return array<string> */
+ function get_links(string $rel) : array {
$rv = array();
switch ($this->type) {
diff --git a/classes/feeds.php b/classes/feeds.php
index cd2633ffb..0c75215c8 100755
--- a/classes/feeds.php
+++ b/classes/feeds.php
@@ -5,18 +5,24 @@ class Feeds extends Handler_Protected {
const NEVER_GROUP_FEEDS = [ -6, 0 ];
const NEVER_GROUP_BY_DATE = [ -2, -1, -3 ];
- private $viewfeed_timestamp;
- private $viewfeed_timestamp_last;
+ /** @var int|float int on 64-bit, float on 32-bit */
+ private $viewfeed_timestamp;
- function csrf_ignore($method) {
+ /** @var int|float int on 64-bit, float on 32-bit */
+ private $viewfeed_timestamp_last;
+
+ function csrf_ignore(string $method): bool {
$csrf_ignored = array("index");
return array_search($method, $csrf_ignored) !== false;
}
- private function _format_headlines_list($feed, $method, $view_mode, $limit, $cat_view,
- $offset, $override_order = false, $include_children = false, $check_first_id = false,
- $skip_first_id_check = false, $order_by = false) {
+ /**
+ * @return array{0: array<int, int>, 1: int, 2: int, 3: bool, 4: array<string, mixed>} $topmost_article_ids, $headlines_count, $feed, $disable_cache, $reply
+ */
+ private function _format_headlines_list(int $feed, string $method, string $view_mode, int $limit, bool $cat_view,
+ int $offset, string $override_order, bool $include_children, ?int $check_first_id = null,
+ bool $skip_first_id_check, string $order_by): array {
$disable_cache = false;
@@ -80,6 +86,8 @@ class Feeds extends Handler_Protected {
"include_children" => $include_children,
"order_by" => $order_by);
+ // Implemented by a plugin, so ignore the undefined 'get_headlines' method.
+ // @phpstan-ignore-next-line
$qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id($feed),
$options);
}
@@ -271,7 +279,7 @@ class Feeds extends Handler_Protected {
$this->_mark_timestamp(" pre-sanitize");
$line["content"] = Sanitizer::sanitize($line["content"],
- $line['hide_images'], false, $line["site_url"], $highlight_words, $line["id"]);
+ $line['hide_images'], null, $line["site_url"], $highlight_words, $line["id"]);
$this->_mark_timestamp(" sanitize");
@@ -289,9 +297,9 @@ class Feeds extends Handler_Protected {
if ($line["num_enclosures"] > 0) {
$line["enclosures"] = Article::_format_enclosures($id,
- $line["always_display_enclosures"],
+ sql_bool_to_bool($line["always_display_enclosures"]),
$line["content"],
- $line["hide_images"]);
+ sql_bool_to_bool($line["hide_images"]));
} else {
$line["enclosures"] = [ 'formatted' => '', 'entries' => [] ];
}
@@ -299,7 +307,7 @@ class Feeds extends Handler_Protected {
$this->_mark_timestamp(" enclosures");
$line["updated_long"] = TimeHelper::make_local_datetime($line["updated"],true);
- $line["updated"] = TimeHelper::make_local_datetime($line["updated"], false, false, false, true);
+ $line["updated"] = TimeHelper::make_local_datetime($line["updated"], false, null, false, true);
$line['imported'] = T_sprintf("Imported at %s",
TimeHelper::make_local_datetime($line["date_entered"], false));
@@ -433,7 +441,7 @@ class Feeds extends Handler_Protected {
return array($topmost_article_ids, $headlines_count, $feed, $disable_cache, $reply);
}
- function catchupAll() {
+ function catchupAll(): void {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
last_read = NOW(), unread = false WHERE unread = true AND owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
@@ -441,7 +449,7 @@ class Feeds extends Handler_Protected {
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
- function view() {
+ function view(): void {
$reply = array();
$feed = $_REQUEST["feed"];
@@ -450,7 +458,7 @@ class Feeds extends Handler_Protected {
$limit = 30;
$cat_view = $_REQUEST["cat"] == "true";
$next_unread_feed = $_REQUEST["nuf"] ?? 0;
- $offset = $_REQUEST["skip"] ?? 0;
+ $offset = (int) ($_REQUEST["skip"] ?? 0);
$order_by = $_REQUEST["order_by"] ?? "";
$check_first_id = $_REQUEST["fid"] ?? 0;
@@ -538,7 +546,10 @@ class Feeds extends Handler_Protected {
print json_encode($reply);
}
- private function _generate_dashboard_feed() {
+ /**
+ * @return array<string, array<string, mixed>>
+ */
+ private function _generate_dashboard_feed(): array {
$reply = array();
$reply['headlines']['id'] = -5;
@@ -580,7 +591,10 @@ class Feeds extends Handler_Protected {
return $reply;
}
- private function _generate_error_feed($error) {
+ /**
+ * @return array<string, mixed>
+ */
+ private function _generate_error_feed(string $error): array {
$reply = array();
$reply['headlines']['id'] = -7;
@@ -596,13 +610,13 @@ class Feeds extends Handler_Protected {
return $reply;
}
- function subscribeToFeed() {
+ function subscribeToFeed(): void {
print json_encode([
"cat_select" => \Controls\select_feeds_cats("cat")
]);
}
- function search() {
+ function search(): void {
print json_encode([
"show_language" => Config::get(Config::DB_TYPE) == "pgsql",
"show_syntax_help" => count(PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH)) == 0,
@@ -611,7 +625,7 @@ class Feeds extends Handler_Protected {
]);
}
- function opensite() {
+ function opensite(): void {
$feed = ORM::for_table('ttrss_feeds')
->find_one((int)$_REQUEST['feed_id']);
@@ -628,10 +642,14 @@ class Feeds extends Handler_Protected {
print "Feed not found or has an empty site URL.";
}
- function updatedebugger() {
+ function updatedebugger(): void {
header("Content-type: text/html");
- $xdebug = isset($_REQUEST["xdebug"]) ? (int)$_REQUEST["xdebug"] : 1;
+ $xdebug = isset($_REQUEST["xdebug"]) ? (int)$_REQUEST["xdebug"] : Debug::LOG_VERBOSE;
+
+ if (!in_array($xdebug, Debug::ALL_LOG_LEVELS)) {
+ $xdebug = Debug::LOG_VERBOSE;
+ }
Debug::set_enabled(true);
Debug::set_loglevel($xdebug);
@@ -644,9 +662,9 @@ class Feeds extends Handler_Protected {
$sth->execute([$feed_id, $_SESSION['uid']]);
if (!$sth->fetch()) {
- print "Access denied.";
- return;
- }
+ print "Access denied.";
+ return;
+ }
?>
<!DOCTYPE html>
<html>
@@ -731,7 +749,10 @@ class Feeds extends Handler_Protected {
}
- static function _catchup($feed, $cat_view, $owner_uid = false, $mode = 'all', $search = false) {
+ /**
+ * @param array<int, string> $search
+ */
+ static function _catchup(string $feed_id_or_tag_name, bool $cat_view, ?int $owner_uid = null, string $mode = 'all', ?array $search = null): void {
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
@@ -785,14 +806,16 @@ class Feeds extends Handler_Protected {
$date_qpart = "true";
}
- if (is_numeric($feed)) {
+ if (is_numeric($feed_id_or_tag_name)) {
+ $feed_id = (int) $feed_id_or_tag_name;
+
if ($cat_view) {
- if ($feed >= 0) {
+ if ($feed_id >= 0) {
- if ($feed > 0) {
- $children = self::_get_child_cats($feed, $owner_uid);
- array_push($children, $feed);
+ if ($feed_id > 0) {
+ $children = self::_get_child_cats($feed_id, $owner_uid);
+ array_push($children, $feed_id);
$children = array_map("intval", $children);
$children = join(",", $children);
@@ -810,7 +833,7 @@ class Feeds extends Handler_Protected {
(SELECT id FROM ttrss_feeds WHERE $cat_qpart) AND $date_qpart AND $search_qpart) as tmp)");
$sth->execute([$owner_uid]);
- } else if ($feed == -2) {
+ } else if ($feed_id == -2) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false,last_read = NOW() WHERE (SELECT COUNT(*)
@@ -819,18 +842,18 @@ class Feeds extends Handler_Protected {
$sth->execute([$owner_uid]);
}
- } else if ($feed > 0) {
+ } else if ($feed_id > 0) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false, last_read = NOW() WHERE ref_id IN
(SELECT id FROM
(SELECT DISTINCT id FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id
AND owner_uid = ? AND unread = true AND feed_id = ? AND $date_qpart AND $search_qpart) as tmp)");
- $sth->execute([$owner_uid, $feed]);
+ $sth->execute([$owner_uid, $feed_id]);
- } else if ($feed < 0 && $feed > LABEL_BASE_INDEX) { // special, like starred
+ } else if ($feed_id < 0 && $feed_id > LABEL_BASE_INDEX) { // special, like starred
- if ($feed == -1) {
+ if ($feed_id == -1) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false, last_read = NOW() WHERE ref_id IN
(SELECT id FROM
@@ -839,7 +862,7 @@ class Feeds extends Handler_Protected {
$sth->execute([$owner_uid]);
}
- if ($feed == -2) {
+ if ($feed_id == -2) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false, last_read = NOW() WHERE ref_id IN
(SELECT id FROM
@@ -848,7 +871,7 @@ class Feeds extends Handler_Protected {
$sth->execute([$owner_uid]);
}
- if ($feed == -3) {
+ if ($feed_id == -3) {
$intl = (int) get_pref(Prefs::FRESH_ARTICLE_MAX_AGE);
@@ -867,7 +890,7 @@ class Feeds extends Handler_Protected {
$sth->execute([$owner_uid]);
}
- if ($feed == -4) {
+ if ($feed_id == -4) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false, last_read = NOW() WHERE ref_id IN
(SELECT id FROM
@@ -875,10 +898,9 @@ class Feeds extends Handler_Protected {
AND owner_uid = ? AND unread = true AND $date_qpart AND $search_qpart) as tmp)");
$sth->execute([$owner_uid]);
}
+ } else if ($feed_id < LABEL_BASE_INDEX) { // label
- } else if ($feed < LABEL_BASE_INDEX) { // label
-
- $label_id = Labels::feed_to_label_id($feed);
+ $label_id = Labels::feed_to_label_id($feed_id);
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false, last_read = NOW() WHERE ref_id IN
@@ -887,23 +909,21 @@ class Feeds extends Handler_Protected {
AND label_id = ? AND ref_id = article_id
AND owner_uid = ? AND unread = true AND $date_qpart AND $search_qpart) as tmp)");
$sth->execute([$label_id, $owner_uid]);
-
}
-
} else { // tag
+ $tag_name = $feed_id_or_tag_name;
+
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false, last_read = NOW() WHERE ref_id IN
(SELECT id FROM
(SELECT DISTINCT ttrss_entries.id FROM ttrss_entries, ttrss_user_entries, ttrss_tags WHERE ref_id = ttrss_entries.id
AND post_int_id = int_id AND tag_name = ?
AND ttrss_user_entries.owner_uid = ? AND unread = true AND $date_qpart AND $search_qpart) as tmp)");
- $sth->execute([$feed, $owner_uid]);
-
+ $sth->execute([$tag_name, $owner_uid]);
}
}
- static function _get_counters($feed, $is_cat = false, $unread_only = false,
- $owner_uid = false) {
+ static function _get_counters(int $feed, bool $is_cat = false, bool $unread_only = false, ?int $owner_uid = null): int {
$n_feed = (int) $feed;
$need_entries = false;
@@ -1002,7 +1022,7 @@ class Feeds extends Handler_Protected {
}
}
- function add() {
+ function add(): void {
$feed = clean($_REQUEST['feed']);
$cat = clean($_REQUEST['cat'] ?? '');
$need_auth = isset($_REQUEST['need_auth']);
@@ -1015,7 +1035,7 @@ class Feeds extends Handler_Protected {
}
/**
- * @return array (code => Status code, message => error message if available)
+ * @return array<string, mixed> (code => Status code, message => error message if available)
*
* 0 - OK, Feed already exists
* 1 - OK, Feed added
@@ -1029,8 +1049,7 @@ class Feeds extends Handler_Protected {
* 7 - Error while creating feed database entry.
* 8 - Permission denied (ACCESS_LEVEL_READONLY).
*/
- static function _subscribe($url, $cat_id = 0,
- $auth_login = '', $auth_pass = '') : array {
+ static function _subscribe(string $url, int $cat_id = 0, string $auth_login = '', string $auth_pass = ''): array {
$user = ORM::for_table("ttrss_users")->find_one($_SESSION['uid']);
@@ -1109,15 +1128,18 @@ class Feeds extends Handler_Protected {
}
}
- static function _get_icon_file($feed_id) {
+ static function _get_icon_file(int $feed_id): string {
return Config::get(Config::ICONS_DIR) . "/$feed_id.ico";
}
- static function _has_icon($id) {
+ static function _has_icon(int $id): bool {
return is_file(Config::get(Config::ICONS_DIR) . "/$id.ico") && filesize(Config::get(Config::ICONS_DIR) . "/$id.ico") > 0;
}
- static function _get_icon($id) {
+ /**
+ * @return false|string false if the icon ID was unrecognized, otherwise, the icon identifier string
+ */
+ static function _get_icon(int $id) {
switch ($id) {
case 0:
return "archive";
@@ -1137,7 +1159,7 @@ class Feeds extends Handler_Protected {
} else {
$icon = self::_get_icon_file($id);
- if ($icon && file_exists($icon)) {
+ if ($icon && file_exists($icon)) {
return Config::get(Config::ICONS_URL) . "/" . basename($icon) . "?" . filemtime($icon);
}
}
@@ -1147,6 +1169,9 @@ class Feeds extends Handler_Protected {
return false;
}
+ /**
+ * @return false|int false if the feed couldn't be found by URL+owner, otherwise the feed ID
+ */
static function _find_by_url(string $feed_url, int $owner_uid) {
$feed = ORM::for_table('ttrss_feeds')
->where('owner_uid', $owner_uid)
@@ -1160,7 +1185,11 @@ class Feeds extends Handler_Protected {
}
}
- /** $owner_uid defaults to $_SESSION['uid] */
+ /**
+ * $owner_uid defaults to $_SESSION['uid']
+ *
+ * @return false|int false if the category/feed couldn't be found by title, otherwise its ID
+ */
static function _find_by_title(string $title, bool $cat = false, int $owner_uid = 0) {
$res = false;
@@ -1184,8 +1213,8 @@ class Feeds extends Handler_Protected {
}
}
- static function _get_title($id, bool $cat = false) {
- $pdo = Db::pdo();
+ static function _get_title(int $id, bool $cat = false): string {
+ $pdo = Db::pdo();
if ($cat) {
return self::_get_cat_title($id);
@@ -1197,7 +1226,7 @@ class Feeds extends Handler_Protected {
return __("Fresh articles");
} else if ($id == -4) {
return __("All articles");
- } else if ($id === 0 || $id === "0") {
+ } else if ($id === 0) {
return __("Archived articles");
} else if ($id == -6) {
return __("Recently read");
@@ -1226,12 +1255,12 @@ class Feeds extends Handler_Protected {
}
} else {
- return $id;
+ return "$id";
}
}
// only real cats
- static function _get_cat_marked(int $cat, int $owner_uid = 0) {
+ static function _get_cat_marked(int $cat, int $owner_uid = 0): int {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
@@ -1245,16 +1274,17 @@ class Feeds extends Handler_Protected {
WHERE (cat_id = :cat OR (:cat IS NULL AND cat_id IS NULL))
AND owner_uid = :uid)
AND owner_uid = :uid");
+
$sth->execute(["cat" => $cat ? $cat : null, "uid" => $owner_uid]);
- $row = $sth->fetch();
- return $row["marked"];
- } else {
- return 0;
+ if ($row = $sth->fetch()) {
+ return (int) $row["marked"];
+ }
}
+ return 0;
}
- static function _get_cat_unread(int $cat, int $owner_uid = 0) {
+ static function _get_cat_unread(int $cat, int $owner_uid = 0): int {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
@@ -1265,14 +1295,15 @@ class Feeds extends Handler_Protected {
$sth = $pdo->prepare("SELECT SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS unread
FROM ttrss_user_entries
WHERE feed_id IN (SELECT id FROM ttrss_feeds
- WHERE (cat_id = :cat OR (:cat IS NULL AND cat_id IS NULL))
+ WHERE (cat_id = :cat OR (:cat IS NULL AND cat_id IS NULL))
AND owner_uid = :uid)
AND owner_uid = :uid");
- $sth->execute(["cat" => $cat ? $cat : null, "uid" => $owner_uid]);
- $row = $sth->fetch();
- return $row["unread"];
+ $sth->execute(["cat" => $cat ? $cat : null, "uid" => $owner_uid]);
+ if ($row = $sth->fetch()) {
+ return (int) $row["unread"];
+ }
} else if ($cat == -1) {
return 0;
} else if ($cat == -2) {
@@ -1280,15 +1311,19 @@ class Feeds extends Handler_Protected {
$sth = $pdo->prepare("SELECT COUNT(DISTINCT article_id) AS unread
FROM ttrss_user_entries ue, ttrss_user_labels2 l
WHERE article_id = ref_id AND unread IS true AND ue.owner_uid = :uid");
+
$sth->execute(["uid" => $owner_uid]);
- $row = $sth->fetch();
- return $row["unread"];
+ if ($row = $sth->fetch()) {
+ return (int) $row["unread"];
+ }
}
+
+ return 0;
}
// only accepts real cats (>= 0)
- static function _get_cat_children_unread(int $cat, int $owner_uid = 0) {
+ static function _get_cat_children_unread(int $cat, int $owner_uid = 0): int {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
@@ -1307,7 +1342,7 @@ class Feeds extends Handler_Protected {
return $unread;
}
- static function _get_global_unread(int $user_id = 0) {
+ static function _get_global_unread(int $user_id = 0): int {
if (!$user_id) $user_id = $_SESSION["uid"];
@@ -1323,7 +1358,7 @@ class Feeds extends Handler_Protected {
return $row["count"];
}
- static function _get_cat_title(int $cat_id) {
+ static function _get_cat_title(int $cat_id): string {
switch ($cat_id) {
case 0:
return __("Uncategorized");
@@ -1343,7 +1378,7 @@ class Feeds extends Handler_Protected {
}
}
- private static function _get_label_unread($label_id, int $owner_uid = 0) {
+ private static function _get_label_unread(int $label_id, ?int $owner_uid = null): int {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
@@ -1360,7 +1395,11 @@ class Feeds extends Handler_Protected {
}
}
- static function _get_headlines($params) {
+ /**
+ * @param array<string, mixed> $params
+ * @return array<int, mixed> $result, $feed_title, $feed_site_url, $last_error, $last_updated, $highlight_words, $first_id, $is_vfeed, $query_error_override
+ */
+ static function _get_headlines($params): array {
$pdo = Db::pdo();
@@ -1577,7 +1616,7 @@ class Feeds extends Handler_Protected {
} else if ($feed <= LABEL_BASE_INDEX) { // labels
$label_id = Labels::feed_to_label_id($feed);
- $query_strategy_part = "label_id = ".$pdo->quote($label_id)." AND
+ $query_strategy_part = "label_id = $label_id AND
ttrss_labels2.id = ttrss_user_labels2.label_id AND
ttrss_user_labels2.article_id = ref_id";
@@ -1857,7 +1896,10 @@ class Feeds extends Handler_Protected {
}
- static function _get_parent_cats(int $cat, int $owner_uid) {
+ /**
+ * @return array<int, int>
+ */
+ static function _get_parent_cats(int $cat, int $owner_uid): array {
$rv = array();
$pdo = Db::pdo();
@@ -1874,7 +1916,10 @@ class Feeds extends Handler_Protected {
return $rv;
}
- static function _get_child_cats(int $cat, int $owner_uid) {
+ /**
+ * @return array<int, int>
+ */
+ static function _get_child_cats(int $cat, int $owner_uid): array {
$rv = array();
$pdo = Db::pdo();
@@ -1891,7 +1936,11 @@ class Feeds extends Handler_Protected {
return $rv;
}
- static function _cats_of(array $feeds, int $owner_uid, bool $with_parents = false) {
+ /**
+ * @param array<int, int> $feeds
+ * @return array<int, int>
+ */
+ static function _cats_of(array $feeds, int $owner_uid, bool $with_parents = false): array {
if (count($feeds) == 0)
return [];
@@ -1930,24 +1979,27 @@ class Feeds extends Handler_Protected {
}
}
- private function _color_of($name) {
- $colormap = [ "#1cd7d7","#d91111","#1212d7","#8e16e5","#7b7b7b",
- "#39f110","#0bbea6","#ec0e0e","#1534f2","#b9e416",
- "#479af2","#f36b14","#10c7e9","#1e8fe7","#e22727" ];
+ private function _color_of(string $name): string {
+ $colormap = [ "#1cd7d7","#d91111","#1212d7","#8e16e5","#7b7b7b",
+ "#39f110","#0bbea6","#ec0e0e","#1534f2","#b9e416",
+ "#479af2","#f36b14","#10c7e9","#1e8fe7","#e22727" ];
- $sum = 0;
+ $sum = 0;
- for ($i = 0; $i < strlen($name); $i++) {
- $sum += ord($name[$i]);
- }
+ for ($i = 0; $i < strlen($name); $i++) {
+ $sum += ord($name[$i]);
+ }
- $sum %= count($colormap);
+ $sum %= count($colormap);
- return $colormap[$sum];
+ return $colormap[$sum];
}
- private static function _get_feeds_from_html($url, $content) {
- $url = UrlHelper::validate($url);
+ /**
+ * @return array<string, string> array of feed URL -> feed title
+ */
+ private static function _get_feeds_from_html(string $url, string $content): array {
+ $url = UrlHelper::validate($url);
$baseUrl = substr($url, 0, strrpos($url, '/') + 1);
$feedUrls = [];
@@ -1964,9 +2016,7 @@ class Feeds extends Handler_Protected {
if ($title == '') {
$title = $entry->getAttribute('type');
}
- $feedUrl = rewrite_relative_url(
- $baseUrl, $entry->getAttribute('href')
- );
+ $feedUrl = UrlHelper::rewrite_relative($baseUrl, $entry->getAttribute('href'));
$feedUrls[$feedUrl] = $title;
}
}
@@ -1974,11 +2024,11 @@ class Feeds extends Handler_Protected {
return $feedUrls;
}
- static function _is_html($content) {
+ static function _is_html(string $content): bool {
return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 8192)) !== 0;
}
- static function _remove_cat(int $id, int $owner_uid) {
+ static function _remove_cat(int $id, int $owner_uid): void {
$cat = ORM::for_table('ttrss_feed_categories')
->where('owner_uid', $owner_uid)
->find_one($id);
@@ -1987,7 +2037,7 @@ class Feeds extends Handler_Protected {
$cat->delete();
}
- static function _add_cat(string $title, int $owner_uid, int $parent_cat = null, int $order_id = 0) {
+ static function _add_cat(string $title, int $owner_uid, int $parent_cat = null, int $order_id = 0): bool {
$cat = ORM::for_table('ttrss_feed_categories')
->where('owner_uid', $owner_uid)
@@ -2011,13 +2061,18 @@ class Feeds extends Handler_Protected {
return false;
}
- static function _clear_access_keys(int $owner_uid) {
+ static function _clear_access_keys(int $owner_uid): void {
$key = ORM::for_table('ttrss_access_keys')
->where('owner_uid', $owner_uid)
->delete_many();
}
- static function _update_access_key(string $feed_id, bool $is_cat, int $owner_uid) {
+ /**
+ * @param string $feed_id may be a feed ID or tag
+ *
+ * @see Handler_Public#generate_syndicated_feed()
+ */
+ static function _update_access_key(string $feed_id, bool $is_cat, int $owner_uid): ?string {
$key = ORM::for_table('ttrss_access_keys')
->where('owner_uid', $owner_uid)
->where('feed_id', $feed_id)
@@ -2027,7 +2082,12 @@ class Feeds extends Handler_Protected {
return self::_get_access_key($feed_id, $is_cat, $owner_uid);
}
- static function _get_access_key(string $feed_id, bool $is_cat, int $owner_uid) {
+ /**
+ * @param string $feed_id may be a feed ID or tag
+ *
+ * @see Handler_Public#generate_syndicated_feed()
+ */
+ static function _get_access_key(string $feed_id, bool $is_cat, int $owner_uid): ?string {
$key = ORM::for_table('ttrss_access_keys')
->where('owner_uid', $owner_uid)
->where('feed_id', $feed_id)
@@ -2036,21 +2096,23 @@ class Feeds extends Handler_Protected {
if ($key) {
return $key->access_key;
- } else {
- $key = ORM::for_table('ttrss_access_keys')->create();
+ }
- $key->owner_uid = $owner_uid;
- $key->feed_id = $feed_id;
- $key->is_cat = $is_cat;
- $key->access_key = uniqid_short();
+ $key = ORM::for_table('ttrss_access_keys')->create();
- if ($key->save()) {
- return $key->access_key;
- }
+ $key->owner_uid = $owner_uid;
+ $key->feed_id = $feed_id;
+ $key->is_cat = $is_cat;
+ $key->access_key = uniqid_short();
+
+ if ($key->save()) {
+ return $key->access_key;
}
+
+ return null;
}
- static function _purge(int $feed_id, int $purge_interval) {
+ static function _purge(int $feed_id, int $purge_interval): ?int {
if (!$purge_interval) $purge_interval = self::_get_purge_interval($feed_id);
@@ -2079,7 +2141,7 @@ class Feeds extends Handler_Protected {
if ($purge_interval <= 0) {
Debug::log("purge_feed: purging disabled for this feed, nothing to do.", Debug::$LOG_VERBOSE);
- return;
+ return null;
}
if (!$purge_unread)
@@ -2120,7 +2182,7 @@ class Feeds extends Handler_Protected {
return $rows_deleted;
}
- private static function _get_purge_interval(int $feed_id) {
+ private static function _get_purge_interval(int $feed_id): int {
$feed = ORM::for_table('ttrss_feeds')->find_one($feed_id);
if ($feed) {
@@ -2133,7 +2195,10 @@ class Feeds extends Handler_Protected {
}
}
- private static function _search_to_sql($search, $search_language, $owner_uid) {
+ /**
+ * @return array{0: string, 1: array<int, string>} [$search_query_part, $search_words]
+ */
+ private static function _search_to_sql(string $search, string $search_language, int $owner_uid): array {
$keywords = str_getcsv(preg_replace('/(-?\w+)\:"(\w+)/', '"${1}:${2}', trim($search)), ' ');
$query_keywords = array();
$search_words = array();
@@ -2226,7 +2291,7 @@ class Feeds extends Handler_Protected {
array_push($query_keywords, "($not
(ttrss_entries.id IN (
SELECT article_id FROM ttrss_user_labels2 WHERE
- label_id = ".$pdo->quote($label_id).")))");
+ label_id = $label_id)))");
} else {
array_push($query_keywords, "(false)");
}
@@ -2300,7 +2365,10 @@ class Feeds extends Handler_Protected {
return array($search_query_part, $search_words);
}
- static function _order_to_override_query($order) {
+ /**
+ * @return array{0: string, 1: bool}
+ */
+ static function _order_to_override_query(string $order): array {
$query = "";
$skip_first_id = false;
@@ -2310,7 +2378,9 @@ class Feeds extends Handler_Protected {
},
$order);
- if ($query) return [$query, $skip_first_id];
+ if (is_string($query) && $query !== "") {
+ return [$query, $skip_first_id];
+ }
switch ($order) {
case "title":
@@ -2328,7 +2398,7 @@ class Feeds extends Handler_Protected {
return [$query, $skip_first_id];
}
- private function _mark_timestamp($label) {
+ private function _mark_timestamp(string $label): void {
if (empty($_REQUEST['timestamps']))
return;
diff --git a/classes/handler.php b/classes/handler.php
index 09557c284..3ee42cedb 100644
--- a/classes/handler.php
+++ b/classes/handler.php
@@ -1,22 +1,29 @@
<?php
class Handler implements IHandler {
+ // TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
+ /** @var PDO */
protected $pdo;
+
+ /** @var array<int|string, mixed> */
protected $args;
- function __construct($args) {
+ /**
+ * @param array<int|string, mixed> $args
+ */
+ function __construct(array $args) {
$this->pdo = Db::pdo();
$this->args = $args;
}
- function csrf_ignore($method) {
+ function csrf_ignore(string $method): bool {
return false;
}
- function before($method) {
+ function before(string $method): bool {
return true;
}
- function after() {
+ function after(): bool {
return true;
}
diff --git a/classes/handler/administrative.php b/classes/handler/administrative.php
index f2f5b36ba..533cb3630 100644
--- a/classes/handler/administrative.php
+++ b/classes/handler/administrative.php
@@ -1,6 +1,6 @@
<?php
class Handler_Administrative extends Handler_Protected {
- function before($method) {
+ function before(string $method): bool {
if (parent::before($method)) {
if (($_SESSION["access_level"] ?? 0) >= UserHelper::ACCESS_LEVEL_ADMIN) {
return true;
diff --git a/classes/handler/protected.php b/classes/handler/protected.php
index 8e9e5ca1d..a15fc0956 100644
--- a/classes/handler/protected.php
+++ b/classes/handler/protected.php
@@ -1,7 +1,7 @@
<?php
class Handler_Protected extends Handler {
- function before($method) {
+ function before(string $method): bool {
return parent::before($method) && !empty($_SESSION['uid']);
}
}
diff --git a/classes/handler/public.php b/classes/handler/public.php
index 14474d0bb..b5282c222 100755
--- a/classes/handler/public.php
+++ b/classes/handler/public.php
@@ -1,10 +1,12 @@
<?php
class Handler_Public extends Handler {
- // $feed may be a tag
+ /**
+ * @param string $feed may be a feed ID or tag
+ */
private function generate_syndicated_feed(int $owner_uid, string $feed, bool $is_cat,
int $limit, int $offset, string $search, string $view_mode = "",
- string $format = 'atom', string $order = "", string $orig_guid = "", string $start_ts = "") {
+ string $format = 'atom', string $order = "", string $orig_guid = "", string $start_ts = ""): void {
$note_style = "background-color : #fff7d5;
border-width : 1px; ".
@@ -52,11 +54,13 @@ class Handler_Public extends Handler {
PluginHost::feed_to_pfeed_id((int)$feed));
if ($handler) {
+ // 'get_headlines' is implemented by the plugin.
+ // @phpstan-ignore-next-line
$qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id((int)$feed), $params);
} else {
user_error("Failed to find handler for plugin feed ID: $feed", E_USER_ERROR);
- return false;
+ return;
}
} else {
@@ -89,11 +93,13 @@ class Handler_Public extends Handler {
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
$line["tags"] = Article::_get_tags($line["id"], $owner_uid);
+ $max_excerpt_length = 250;
+
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
function ($result) use (&$line) {
$line = $result;
},
- $line);
+ $line, $max_excerpt_length);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_EXPORT_FEED,
function ($result) use (&$line) {
@@ -109,7 +115,7 @@ class Handler_Public extends Handler {
$tpl->setVariable('ARTICLE_EXCERPT', $line["content_preview"], true);
$content = Sanitizer::sanitize($line["content"], false, $owner_uid,
- $feed_site_url, false, $line["id"]);
+ $feed_site_url, null, $line["id"]);
$content = DiskCache::rewrite_urls($content);
@@ -207,7 +213,7 @@ class Handler_Public extends Handler {
$article['link'] = $line['link'];
$article['title'] = $line['title'];
$article['excerpt'] = $line["content_preview"];
- $article['content'] = Sanitizer::sanitize($line["content"], false, $owner_uid, $feed_site_url, false, $line["id"]);
+ $article['content'] = Sanitizer::sanitize($line["content"], false, $owner_uid, $feed_site_url, null, $line["id"]);
$article['updated'] = date('c', strtotime($line["updated"]));
if (!empty($line['note'])) $article['note'] = $line['note'];
@@ -247,7 +253,7 @@ class Handler_Public extends Handler {
}
}
- function getUnread() {
+ function getUnread(): void {
$login = clean($_REQUEST["login"]);
$fresh = clean($_REQUEST["fresh"]) == "1";
@@ -265,7 +271,7 @@ class Handler_Public extends Handler {
}
}
- function getProfiles() {
+ function getProfiles(): void {
$login = clean($_REQUEST["login"]);
$rv = [];
@@ -288,7 +294,7 @@ class Handler_Public extends Handler {
print json_encode($rv);
}
- function logout() {
+ function logout(): void {
if (validate_csrf($_POST["csrf_token"])) {
UserHelper::logout();
header("Location: index.php");
@@ -298,7 +304,7 @@ class Handler_Public extends Handler {
}
}
- function rss() {
+ function rss(): void {
$feed = clean($_REQUEST["id"]);
$key = clean($_REQUEST["key"]);
$is_cat = clean($_REQUEST["is_cat"] ?? false);
@@ -333,21 +339,21 @@ class Handler_Public extends Handler {
header('HTTP/1.1 403 Forbidden');
}
- function updateTask() {
+ function updateTask(): void {
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_UPDATE_TASK);
}
- function housekeepingTask() {
+ function housekeepingTask(): void {
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
}
- function globalUpdateFeeds() {
+ function globalUpdateFeeds(): void {
RPC::updaterandomfeed_real();
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_UPDATE_TASK);
}
- function login() {
+ function login(): void {
if (!Config::get(Config::SINGLE_USER_MODE)) {
$login = clean($_POST["login"]);
@@ -403,12 +409,12 @@ class Handler_Public extends Handler {
}
}
- function index() {
+ function index(): void {
header("Content-Type: text/plain");
print Errors::to_json(Errors::E_UNKNOWN_METHOD);
}
- function forgotpass() {
+ function forgotpass(): void {
startup_gettext();
session_start();
@@ -587,7 +593,7 @@ class Handler_Public extends Handler {
print "</html>";
}
- function dbupdate() {
+ function dbupdate(): void {
startup_gettext();
if (!Config::get(Config::SINGLE_USER_MODE) && ($_SESSION["access_level"] ?? 0) < 10) {
@@ -730,7 +736,7 @@ class Handler_Public extends Handler {
<?php
}
- function cached() {
+ function cached(): void {
list ($cache_dir, $filename) = explode("/", $_GET["file"], 2);
// we do not allow files with extensions at the moment
@@ -746,7 +752,7 @@ class Handler_Public extends Handler {
}
}
- private function _make_article_tag_uri($id, $timestamp) {
+ private function _make_article_tag_uri(int $id, string $timestamp): string {
$timestamp = date("Y-m-d", strtotime($timestamp));
@@ -756,7 +762,7 @@ class Handler_Public extends Handler {
// this should be used very carefully because this endpoint is exposed to unauthenticated users
// plugin data is not loaded because there's no user context and owner_uid/session may or may not be available
// in general, don't do anything user-related in here and do not modify $_SESSION
- public function pluginhandler() {
+ public function pluginhandler(): void {
$host = new PluginHost();
$plugin_name = basename(clean($_REQUEST["plugin"]));
@@ -788,7 +794,7 @@ class Handler_Public extends Handler {
}
}
- static function _render_login_form(string $return_to = "") {
+ static function _render_login_form(string $return_to = ""): void {
header('Cache-Control: public');
if ($return_to)
diff --git a/classes/iauthmodule.php b/classes/iauthmodule.php
index e714cc6ca..dbf8c5587 100644
--- a/classes/iauthmodule.php
+++ b/classes/iauthmodule.php
@@ -1,5 +1,18 @@
<?php
interface IAuthModule {
- function authenticate($login, $password); // + optional third parameter: $service
- function hook_auth_user(...$args); // compatibility wrapper due to how hooks work
+ /**
+ * @param string $login
+ * @param string $password
+ * @param string $service
+ * @return int|false user_id
+ */
+ function authenticate($login, $password, $service = '');
+
+ /** this is a pluginhost compatibility wrapper that invokes $this->authenticate(...$args) (Auth_Base)
+ * @param string $login
+ * @param string $password
+ * @param string $service
+ * @return int|false user_id
+ */
+ function hook_auth_user($login, $password, $service = '');
}
diff --git a/classes/ihandler.php b/classes/ihandler.php
index 01c9e3109..215143370 100644
--- a/classes/ihandler.php
+++ b/classes/ihandler.php
@@ -1,6 +1,6 @@
<?php
interface IHandler {
- function csrf_ignore($method);
- function before($method);
- function after();
+ function csrf_ignore(string $method): bool;
+ function before(string $method): bool;
+ function after(): bool;
}
diff --git a/classes/labels.php b/classes/labels.php
index 570f24f4f..026e6621f 100644
--- a/classes/labels.php
+++ b/classes/labels.php
@@ -1,15 +1,15 @@
<?php
class Labels
{
- static function label_to_feed_id($label) {
+ static function label_to_feed_id(int $label): int {
return LABEL_BASE_INDEX - 1 - abs($label);
}
- static function feed_to_label_id($feed) {
+ static function feed_to_label_id(int $feed): int {
return LABEL_BASE_INDEX - 1 + abs($feed);
}
- static function find_id($label, $owner_uid) {
+ static function find_id(string $label, int $owner_uid): int {
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT id FROM ttrss_labels2 WHERE LOWER(caption) = LOWER(?)
@@ -23,7 +23,7 @@ class Labels
}
}
- static function find_caption($label, $owner_uid) {
+ static function find_caption(int $label, int $owner_uid): string {
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT caption FROM ttrss_labels2 WHERE id = ?
@@ -37,18 +37,24 @@ class Labels
}
}
- static function get_as_hash($owner_uid) {
+ /**
+ * @return array<int, array<string, string>>
+ */
+ static function get_as_hash(int $owner_uid): array {
$rv = [];
$labels = Labels::get_all($owner_uid);
foreach ($labels as $i => $label) {
- $rv[$label["id"]] = $labels[$i];
+ $rv[(int)$label["id"]] = $labels[$i];
}
return $rv;
}
- static function get_all($owner_uid) {
+ /**
+ * @return array<int, array<string, string>> An array of label detail arrays
+ */
+ static function get_all(int $owner_uid) {
$rv = array();
$pdo = Db::pdo();
@@ -64,7 +70,13 @@ class Labels
return $rv;
}
- static function update_cache($owner_uid, $id, $labels = false, $force = false) {
+ /**
+ * @param array{'no-labels': 1}|array<int, array<int, array{0: int, 1: string, 2: string, 3: string}>> $labels
+ * [label_id, caption, fg_color, bg_color]
+ *
+ * @see Article::_get_labels()
+ */
+ static function update_cache(int $owner_uid, int $id, array $labels, bool $force = false): void {
$pdo = Db::pdo();
if ($force)
@@ -81,7 +93,7 @@ class Labels
}
- static function clear_cache($id) {
+ static function clear_cache(int $id): void {
$pdo = Db::pdo();
@@ -91,7 +103,7 @@ class Labels
}
- static function remove_article($id, $label, $owner_uid) {
+ static function remove_article(int $id, string $label, int $owner_uid): void {
$label_id = self::find_id($label, $owner_uid);
@@ -109,7 +121,7 @@ class Labels
self::clear_cache($id);
}
- static function add_article($id, $label, $owner_uid) {
+ static function add_article(int $id, string $label, int $owner_uid): void {
$label_id = self::find_id($label, $owner_uid);
@@ -138,7 +150,7 @@ class Labels
}
- static function remove($id, $owner_uid) {
+ static function remove(int $id, int $owner_uid): void {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
@@ -182,7 +194,10 @@ class Labels
if (!$tr_in_progress) $pdo->commit();
}
- static function create($caption, $fg_color = '', $bg_color = '', $owner_uid = false) {
+ /**
+ * @return false|int false if the check for an existing label failed, otherwise the number of rows inserted (1 on success)
+ */
+ static function create(string $caption, ?string $fg_color = '', ?string $bg_color = '', ?int $owner_uid = null) {
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
diff --git a/classes/logger.php b/classes/logger.php
index 42ab4452c..ef6173a42 100755
--- a/classes/logger.php
+++ b/classes/logger.php
@@ -1,6 +1,9 @@
<?php
class Logger {
+ /** @var Logger|null */
private static $instance;
+
+ /** @var Logger_Adapter|null */
private $adapter;
const LOG_DEST_SQL = "sql";
@@ -25,11 +28,11 @@ class Logger {
16384 => 'E_USER_DEPRECATED',
32767 => 'E_ALL'];
- static function log_error(int $errno, string $errstr, string $file, int $line, $context) {
+ static function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
return self::get_instance()->_log_error($errno, $errstr, $file, $line, $context);
}
- private function _log_error($errno, $errstr, $file, $line, $context) {
+ private function _log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
//if ($errno == E_NOTICE) return false;
if ($this->adapter)
@@ -38,11 +41,11 @@ class Logger {
return false;
}
- static function log(int $errno, string $errstr, $context = "") {
+ static function log(int $errno, string $errstr, string $context = ""): bool {
return self::get_instance()->_log($errno, $errstr, $context);
}
- private function _log(int $errno, string $errstr, $context = "") {
+ private function _log(int $errno, string $errstr, string $context = ""): bool {
if ($this->adapter)
return $this->adapter->log_error($errno, $errstr, '', 0, $context);
else
@@ -65,7 +68,7 @@ class Logger {
$this->adapter = new Logger_Stdout();
break;
default:
- $this->adapter = false;
+ $this->adapter = null;
}
if ($this->adapter && !implements_interface($this->adapter, "Logger_Adapter"))
diff --git a/classes/logger/adapter.php b/classes/logger/adapter.php
index 79f641441..b0287b5fa 100644
--- a/classes/logger/adapter.php
+++ b/classes/logger/adapter.php
@@ -1,4 +1,4 @@
<?php
interface Logger_Adapter {
- function log_error(int $errno, string $errstr, string $file, int $line, $context);
-} \ No newline at end of file
+ function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool;
+}
diff --git a/classes/logger/sql.php b/classes/logger/sql.php
index 0f6177a5b..5f3c67852 100755
--- a/classes/logger/sql.php
+++ b/classes/logger/sql.php
@@ -10,7 +10,7 @@ class Logger_SQL implements Logger_Adapter {
ORM::configure('return_result_sets', true, $conn);
}
- function log_error(int $errno, string $errstr, string $file, int $line, $context) {
+ function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
if (Config::get_schema_version() > 117) {
diff --git a/classes/logger/stdout.php b/classes/logger/stdout.php
index e906853ce..b15649028 100644
--- a/classes/logger/stdout.php
+++ b/classes/logger/stdout.php
@@ -1,7 +1,7 @@
<?php
class Logger_Stdout implements Logger_Adapter {
- function log_error(int $errno, string $errstr, string $file, int $line, $context) {
+ function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
switch ($errno) {
case E_ERROR:
@@ -25,6 +25,7 @@ class Logger_Stdout implements Logger_Adapter {
print "[EEE] $priority $errname ($file:$line) $errstr\n";
+ return true;
}
}
diff --git a/classes/logger/syslog.php b/classes/logger/syslog.php
index 3ad9858f3..568398ee0 100644
--- a/classes/logger/syslog.php
+++ b/classes/logger/syslog.php
@@ -1,7 +1,7 @@
<?php
class Logger_Syslog implements Logger_Adapter {
- function log_error(int $errno, string $errstr, string $file, int $line, $context) {
+ function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
switch ($errno) {
case E_ERROR:
@@ -25,6 +25,7 @@ class Logger_Syslog implements Logger_Adapter {
syslog($priority, "[tt-rss] $errname ($file:$line) $errstr");
+ return true;
}
}
diff --git a/classes/mailer.php b/classes/mailer.php
index 8238904ee..60b1ce4fd 100644
--- a/classes/mailer.php
+++ b/classes/mailer.php
@@ -1,8 +1,14 @@
<?php
class Mailer {
+ // TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
+ /** @var string */
private $last_error = "";
- function mail($params) {
+ /**
+ * @param array<string, mixed> $params
+ * @return bool|int bool if the default mail function handled the request, otherwise an int as described in Mailer#mail()
+ */
+ function mail(array $params) {
$to_name = $params["to_name"] ?? "";
$to_address = $params["to_address"];
@@ -25,6 +31,8 @@ class Mailer {
// 3. any other return value will allow cycling to the next handler and, eventually, to default mail() function
// 4. set error message if needed via passed Mailer instance function set_error()
+ $hooks_tried = 0;
+
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEND_MAIL) as $p) {
$rc = $p->hook_send_mail($this, $params);
@@ -33,6 +41,8 @@ class Mailer {
if ($rc == -1)
return 0;
+
+ ++$hooks_tried;
}
$headers = [ "From: $from_combined", "Content-Type: text/plain; charset=UTF-8" ];
@@ -40,18 +50,18 @@ class Mailer {
$rc = mail($to_combined, $subject, $message, implode("\r\n", array_merge($headers, $additional_headers)));
if (!$rc) {
- $this->set_error(error_get_last()['message']);
+ $this->set_error(error_get_last()['message'] ?? T_sprintf("Unknown error while sending mail. Hooks tried: %d.", $hooks_tried));
}
return $rc;
}
- function set_error($message) {
+ function set_error(string $message): void {
$this->last_error = $message;
user_error("Error sending mail: $message", E_USER_WARNING);
}
- function error() {
+ function error(): string {
return $this->last_error;
}
}
diff --git a/classes/opml.php b/classes/opml.php
index e0dbebcda..b9f5f2eab 100644
--- a/classes/opml.php
+++ b/classes/opml.php
@@ -1,12 +1,15 @@
<?php
class OPML extends Handler_Protected {
- function csrf_ignore($method) {
+ function csrf_ignore(string $method): bool {
$csrf_ignored = array("export", "import");
return array_search($method, $csrf_ignored) !== false;
}
+ /**
+ * @return bool|int|void false if writing the file failed, true if printing succeeded, int if bytes were written to a file, or void if $owner_uid is missing
+ */
function export() {
$output_name = sprintf("tt-rss_%s_%s.opml", $_SESSION["name"], date("Y-m-d"));
$include_settings = $_REQUEST["include_settings"] == "1";
@@ -17,7 +20,7 @@ class OPML extends Handler_Protected {
return $rc;
}
- function import() {
+ function import(): void {
$owner_uid = $_SESSION["uid"];
header('Content-Type: text/html; charset=utf-8');
@@ -42,13 +45,11 @@ class OPML extends Handler_Protected {
</form>";
print "</div></body></html>";
-
-
}
// Export
- private function opml_export_category(int $owner_uid, int $cat_id, bool $hide_private_feeds = false, bool $include_settings = true) {
+ private function opml_export_category(int $owner_uid, int $cat_id, bool $hide_private_feeds = false, bool $include_settings = true): string {
if ($hide_private_feeds)
$hide_qpart = "(private IS false AND auth_login = '' AND auth_pass = '')";
@@ -124,6 +125,9 @@ class OPML extends Handler_Protected {
return $out;
}
+ /**
+ * @return bool|int|void false if writing the file failed, true if printing succeeded, int if bytes were written to a file, or void if $owner_uid is missing
+ */
function opml_export(string $filename, int $owner_uid, bool $hide_private_feeds = false, bool $include_settings = true, bool $file_output = false) {
if (!$owner_uid) return;
@@ -290,13 +294,14 @@ class OPML extends Handler_Protected {
if ($file_output)
return file_put_contents($filename, $res) > 0;
- else
- print $res;
+
+ print $res;
+ return true;
}
// Import
- private function opml_import_feed(DOMNode $node, int $cat_id, int $owner_uid, int $nest) {
+ private function opml_import_feed(DOMNode $node, int $cat_id, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$feed_title = mb_substr($attrs->getNamedItem('text')->nodeValue, 0, 250);
@@ -341,7 +346,7 @@ class OPML extends Handler_Protected {
}
}
- private function opml_import_label(DOMNode $node, int $owner_uid, int $nest) {
+ private function opml_import_label(DOMNode $node, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$label_name = $attrs->getNamedItem('label-name')->nodeValue;
@@ -358,7 +363,7 @@ class OPML extends Handler_Protected {
}
}
- private function opml_import_preference(DOMNode $node, int $owner_uid, int $nest) {
+ private function opml_import_preference(DOMNode $node, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$pref_name = $attrs->getNamedItem('pref-name')->nodeValue;
@@ -372,7 +377,7 @@ class OPML extends Handler_Protected {
}
}
- private function opml_import_filter(DOMNode $node, int $owner_uid, int $nest) {
+ private function opml_import_filter(DOMNode $node, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$filter_type = $attrs->getNamedItem('filter-type')->nodeValue;
@@ -526,7 +531,7 @@ class OPML extends Handler_Protected {
}
}
- private function opml_import_category(DOMDocument $doc, ?DOMNode $root_node, int $owner_uid, int $parent_id, int $nest) {
+ private function opml_import_category(DOMDocument $doc, ?DOMNode $root_node, int $owner_uid, int $parent_id, int $nest): void {
$default_cat_id = (int) $this->get_feed_category('Imported feeds', $owner_uid, 0);
if ($root_node) {
@@ -601,6 +606,9 @@ class OPML extends Handler_Protected {
}
/** $filename is optional; assumes HTTP upload with $_FILES otherwise */
+ /**
+ * @return bool|void false on failure, true if successful, void if $owner_uid is missing
+ */
function opml_import(int $owner_uid, string $filename = "") {
if (!$owner_uid) return;
@@ -667,7 +675,7 @@ class OPML extends Handler_Protected {
return true;
}
- private function opml_notice(string $msg, int $prefix_length = 0) {
+ private function opml_notice(string $msg, int $prefix_length = 0): void {
if (php_sapi_name() == "cli") {
Debug::log(str_repeat(" ", $prefix_length) . $msg);
} else {
diff --git a/classes/plugin.php b/classes/plugin.php
index ecafa7888..0a9f837fc 100644
--- a/classes/plugin.php
+++ b/classes/plugin.php
@@ -5,8 +5,14 @@ abstract class Plugin {
/** @var PDO $pdo */
protected $pdo;
- abstract function init(PluginHost $host);
+ /**
+ * @param PluginHost $host
+ *
+ * @return void
+ * */
+ abstract function init($host);
+ /** @return array<null|float|string|bool> */
abstract function about();
// return array(1.0, "plugin", "No description", "No author", false);
@@ -14,6 +20,7 @@ abstract class Plugin {
$this->pdo = Db::pdo();
}
+ /** @return array<string,bool> */
function flags() {
/* associative array, possible keys:
needs_curl = boolean
@@ -21,36 +28,65 @@ abstract class Plugin {
return array();
}
+ /**
+ * @param string $method
+ *
+ * @return bool */
function is_public_method($method) {
return false;
}
+ /**
+ * @param string $method
+ *
+ * @return bool */
function csrf_ignore($method) {
return false;
}
+ /** @return string */
function get_js() {
return "";
}
+ /** @return string */
+ function get_css() {
+ return "";
+ }
+
+ /** @return string */
function get_prefs_js() {
return "";
}
+ /** @return int */
function api_version() {
return Plugin::API_VERSION_COMPAT;
}
/* gettext-related helpers */
+ /**
+ * @param string $msgid
+ *
+ * @return string */
function __($msgid) {
+ /** @var Plugin $this -- this is a strictly template-related hack */
return _dgettext(PluginHost::object_to_domain($this), $msgid);
}
+ /**
+ * @param string $singular
+ * @param string $plural
+ * @param int $number
+ *
+ * @return string */
function _ngettext($singular, $plural, $number) {
+ /** @var Plugin $this -- this is a strictly template-related hack */
return _dngettext(PluginHost::object_to_domain($this), $singular, $plural, $number);
}
+ /** @return string */
function T_sprintf() {
$args = func_get_args();
$msgid = array_shift($args);
@@ -58,4 +94,580 @@ abstract class Plugin {
return vsprintf($this->__($msgid), $args);
}
+ /* plugin hook methods */
+
+ /* GLOBAL hooks are invoked in global context, only available to system plugins (loaded via .env for all users) */
+
+ /** Adds buttons for article (on the right) - e.g. mail, share, add note.
+ * @param array<string,mixed> $line
+ * @return string
+ * @see PluginHost::HOOK_ARTICLE_BUTTON
+ * @see Plugin::hook_article_left_button()
+ */
+ function hook_article_button($line) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /** Allows plugins to alter article data as gathered from feed XML, i.e. embed images, get full text content, etc.
+ * @param array<string,mixed> $article
+ * @return array<string,mixed>
+ * @see PluginHost::HOOK_ARTICLE_FILTER
+ */
+ function hook_article_filter($article) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return [];
+ }
+
+ /** Allow adding new UI elements (e.g. accordion panes) to (top) tab contents in Preferences
+ * @param string $tab
+ * @return void
+ * @see PluginHost::HOOK_PREFS_TAB
+ */
+ function hook_prefs_tab($tab) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+ }
+
+ /** Allow adding new content to various sections of preferences UI (i.e. OPML import/export pane)
+ * @param string $section
+ * @return void
+ * @see PluginHost::HOOK_PREFS_TAB_SECTION
+ */
+ function hook_prefs_tab_section($section) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+ }
+
+ /** Allows adding new (top) tabs in preferences UI
+ * @return void
+ * @see PluginHost::HOOK_PREFS_TABS
+ */
+ function hook_prefs_tabs() {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+ }
+
+ /** Invoked when feed XML is processed by FeedParser class
+ * @param FeedParser $parser
+ * @param int $feed_id
+ * @return void
+ * @see PluginHost::HOOK_FEED_PARSED
+ */
+ function hook_feed_parsed($parser, $feed_id) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+ }
+
+ /** GLOBAL: Invoked when a feed update task finishes
+ * @param array<string,string> $cli_options
+ * @return void
+ * @see PluginHost::HOOK_UPDATE_TASK
+ */
+ function hook_update_task($cli_options) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+ }
+
+ /** This is a pluginhost compatibility wrapper that invokes $this->authenticate(...$args) (Auth_Base)
+ * @param string $login
+ * @param string $password
+ * @param string $service
+ * @return int|false user_id
+ * @see PluginHost::HOOK_AUTH_USER
+ */
+ function hook_auth_user($login, $password, $service = '') {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+ return false;
+ }
+
+ /** IAuthModule only
+ * @param string $login
+ * @param string $password
+ * @param string $service
+ * @return int|false user_id
+ */
+ function authenticate($login, $password, $service = '') {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+ return false;
+ }
+
+ /** Allows plugins to modify global hotkey map (hotkey sequence -> action)
+ * @param array<string, string> $hotkeys
+ * @return array<string, string>
+ * @see PluginHost::HOOK_HOTKEY_MAP
+ * @see Plugin::hook_hotkey_info()
+ */
+ function hook_hotkey_map($hotkeys) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return [];
+ }
+
+ /** Invoked when article is rendered by backend (before it gets passed to frontent JS code) - three panel mode
+ * @param array<string, mixed> $article
+ * @return array<string, mixed>
+ * @see PluginHost::HOOK_RENDER_ARTICLE
+ */
+ function hook_render_article($article) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return [];
+ }
+
+ /** Invoked when article is rendered by backend (before it gets passed to frontent JS code) - combined mode
+ * @param array<string, mixed> $article
+ * @return array<string, mixed>
+ * @see PluginHost::HOOK_RENDER_ARTICLE_CDM
+ */
+ function hook_render_article_cdm($article) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return [];
+ }
+
+ /** Invoked when raw feed XML data has been successfully downloaded (but not parsed yet)
+ * @param string $feed_data
+ * @param string $fetch_url
+ * @param int $owner_uid
+ * @param int $feed
+ * @return string
+ * @see PluginHost::HOOK_FEED_FETCHED
+ */
+ function hook_feed_fetched($feed_data, $fetch_url, $owner_uid, $feed) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /** Invoked on article content when it is sanitized (i.e. potentially harmful tags removed)
+ * @param DOMDocument $doc
+ * @param string $site_url
+ * @param array<string> $allowed_elements
+ * @param array<string> $disallowed_attributes
+ * @param int $article_id
+ * @return DOMDocument|array<int,DOMDocument|array<string>>
+ * @see PluginHost::HOOK_SANITIZE
+ */
+ function hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return $doc;
+ }
+
+ /** Invoked when article is rendered by backend (before it gets passed to frontent JS code) - exclusive to API clients
+ * @param array{'article': array<string,mixed>|null, 'headline': array<string,mixed>|null} $params
+ * @return array<string, string>
+ * @see PluginHost::HOOK_RENDER_ARTICLE_API
+ */
+ function hook_render_article_api($params) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return [];
+ }
+
+ /** Allows adding new UI elements to tt-rss main toolbar (to the right, before Actions... dropdown)
+ * @return string
+ * @see PluginHost::HOOK_TOOLBAR_BUTTON
+ */
+ function hook_toolbar_button() {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /** Allows adding new items to tt-rss main Actions... dropdown menu
+ * @return string
+ * @see PluginHost::HOOK_ACTION_ITEM
+ */
+ function hook_action_item() {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /** Allows adding new UI elements to the toolbar area related to currently loaded feed headlines
+ * @param int $feed_id
+ * @param bool $is_cat
+ * @return string
+ * @see PluginHost::HOOK_HEADLINE_TOOLBAR_BUTTON
+ */
+ function hook_headline_toolbar_button($feed_id, $is_cat) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /** Allows adding new hotkey action names and descriptions
+ * @param array<string, array<string, string>> $hotkeys
+ * @return array<string, array<string, string>>
+ * @see PluginHost::HOOK_HOTKEY_INFO
+ * @see Plugin::hook_hotkey_map()
+ */
+ function hook_hotkey_info($hotkeys) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return [];
+ }
+
+ /** Adds per-article buttons on the left side
+ * @param array<string,mixed> $row
+ * @return string
+ * @see PluginHost::HOOK_ARTICLE_LEFT_BUTTON
+ * @see Plugin::hook_article_button()
+ */
+ function hook_article_left_button($row) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /** Allows adding new UI elements to the "Plugins" tab of the feed editor UI
+ * @param int $feed_id
+ * @return void
+ * @see PluginHost::HOOK_PREFS_EDIT_FEED
+ */
+ function hook_prefs_edit_feed($feed_id) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+ }
+
+ /** Invoked when data is saved in the feed editor
+ * @param int $feed_id
+ * @return void
+ * @see PluginHost::HOOK_PREFS_SAVE_FEED
+ */
+ function hook_prefs_save_feed($feed_id) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+ }
+
+ /** Allows overriding built-in fetching mechanism for feeds, substituting received data if necessary
+ * (i.e. origin site doesn't actually provide any RSS feeds), or XML is invalid
+ * @param string $feed_data
+ * @param string $fetch_url
+ * @param int $owner_uid
+ * @param int $feed
+ * @param int $last_article_timestamp
+ * @param string $auth_login
+ * @param string $auth_pass
+ * @return string (possibly mangled feed data)
+ * @see PluginHost::HOOK_FETCH_FEED
+ */
+ function hook_fetch_feed($feed_data, $fetch_url, $owner_uid, $feed, $last_article_timestamp, $auth_login, $auth_pass) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /** Invoked when headlines data ($row) has been retrieved from the database
+ * @param array<string,mixed> $row
+ * @param int $excerpt_length
+ * @return array<string,mixed>
+ * @see PluginHost::HOOK_QUERY_HEADLINES
+ */
+ function hook_query_headlines($row, $excerpt_length) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return [];
+ }
+
+ /** GLOBAL: This is run periodically by the update daemon when idle
+ * @return void
+ * @see PluginHost::HOOK_HOUSE_KEEPING */
+ function hook_house_keeping() {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+ }
+
+ /** Allows overriding built-in article search
+ * @param string $query
+ * @return array<int, string|array<string>> - list(SQL search query, highlight keywords)
+ * @see PluginHost::HOOK_SEARCH
+ */
+ function hook_search($query) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return [];
+ }
+
+ /** Invoked when enclosures are rendered to HTML (when article itself is rendered)
+ * @param string $enclosures_formatted
+ * @param array<int, array<string, mixed>> $enclosures
+ * @param int $article_id
+ * @param bool $always_display_enclosures
+ * @param string $article_content
+ * @param bool $hide_images
+ * @return string|array<string,array<int, array<string, mixed>>> ($enclosures_formatted, $enclosures)
+ * @see PluginHost::HOOK_FORMAT_ENCLOSURES
+ */
+ function hook_format_enclosures($enclosures_formatted, $enclosures, $article_id, $always_display_enclosures, $article_content, $hide_images) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /** Invoked during feed subscription (after data has been fetched)
+ * @param string $contents
+ * @param string $url
+ * @param string $auth_login
+ * @param string $auth_pass
+ * @return string (possibly mangled feed data)
+ * @see PluginHost::HOOK_SUBSCRIBE_FEED
+ */
+ function hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /**
+ * @param int $feed
+ * @param bool $is_cat
+ * @param array<string,mixed> $qfh_ret (headlines object)
+ * @return string
+ * @see PluginHost::HOOK_HEADLINES_BEFORE
+ */
+ function hook_headlines_before($feed, $is_cat, $qfh_ret) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /**
+ * @param array<string,mixed> $entry
+ * @param int $article_id
+ * @param array<string,mixed> $rv
+ * @return string
+ * @see PluginHost::HOOK_RENDER_ENCLOSURE
+ */
+ function hook_render_enclosure($entry, $article_id, $rv) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /**
+ * @param array<string,mixed> $article
+ * @param string $action
+ * @return array<string,mixed> ($article)
+ * @see PluginHost::HOOK_ARTICLE_FILTER_ACTION
+ */
+ function hook_article_filter_action($article, $action) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return [];
+ }
+
+ /**
+ * @param array<string,mixed> $line
+ * @param int $feed
+ * @param bool $is_cat
+ * @param int $owner_uid
+ * @return array<string,mixed> ($line)
+ * @see PluginHost::HOOK_ARTICLE_EXPORT_FEED
+ */
+ function hook_article_export_feed($line, $feed, $is_cat, $owner_uid) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return [];
+ }
+
+ /** Allows adding custom buttons to tt-rss main toolbar (left side)
+ * @return void
+ * @see PluginHost::HOOK_MAIN_TOOLBAR_BUTTON
+ */
+ function hook_main_toolbar_button() {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+ }
+
+ /** Invoked for every enclosure entry as article is being rendered
+ * @param array<string,string> $entry
+ * @param int $id
+ * @param array{'formatted': string, 'entries': array<int, array<string, mixed>>} $rv
+ * @return array<string,string> ($entry)
+ * @see PluginHost::HOOK_ENCLOSURE_ENTRY
+ */
+ function hook_enclosure_entry($entry, $id, $rv) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return [];
+ }
+
+ /** Share plugins run this when article is being rendered as HTML for sharing
+ * @param string $html
+ * @param array<string,mixed> $row
+ * @return string ($html)
+ * @see PluginHost::HOOK_FORMAT_ARTICLE
+ */
+ function hook_format_article($html, $row) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /** Invoked when basic feed information (title, site_url) is being collected, useful to override default if feed doesn't provide anything (or feed itself is synthesized)
+ * @param array{"title": string, "site_url": string} $basic_info
+ * @param string $fetch_url
+ * @param int $owner_uid
+ * @param int $feed_id
+ * @param string $auth_login
+ * @param string $auth_pass
+ * @return array{"title": string, "site_url": string}
+ * @see PluginHost::HOOK_FEED_BASIC_INFO
+ */
+ function hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed_id, $auth_login, $auth_pass) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return $basic_info;
+ }
+
+ /** Invoked when file (e.g. cache entry, static data) is being sent to client, may override default mechanism
+ * using faster httpd-specific implementation (see nginx_xaccel)
+ * @param string $filename
+ * @return bool
+ * @see PluginHost::HOOK_SEND_LOCAL_FILE
+ */
+ function hook_send_local_file($filename) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return false;
+ }
+
+ /** Invoked when user tries to unsubscribe from a feed, returning true would prevent any further default actions
+ * @param int $feed_id
+ * @param int $owner_uid
+ * @return bool
+ * @see PluginHost::HOOK_UNSUBSCRIBE_FEED
+ */
+ function hook_unsubscribe_feed($feed_id, $owner_uid) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return false;
+ }
+
+ /** Invoked when mail is being sent (if no hooks are registered, tt-rss uses PHP mail() as a fallback)
+ * @param Mailer $mailer
+ * @param array<string,mixed> $params
+ * @return int
+ * @see PluginHost::HOOK_SEND_MAIL
+ */
+ function hook_send_mail($mailer, $params) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return -1;
+ }
+
+ /** Invoked when filter is triggered on an article, may be used to implement logging for filters
+ * NOTE: $article_filters should be renamed $filter_actions because that's what this is
+ * @param int $feed_id
+ * @param int $owner_uid
+ * @param array<string,mixed> $article
+ * @param array<string,mixed> $matched_filters
+ * @param array<string,string|bool|int> $matched_rules
+ * @param array<string,string> $article_filters
+ * @return void
+ * @see PluginHost::HOOK_FILTER_TRIGGERED
+ */
+ function hook_filter_triggered($feed_id, $owner_uid, $article, $matched_filters, $matched_rules, $article_filters) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+ }
+
+ /** Plugins may provide this to allow getting full article text (af_readbility implements this)
+ * @param string $url
+ * @return string|false
+ * @see PluginHost::HOOK_GET_FULL_TEXT
+ */
+ function hook_get_full_text($url) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /** Invoked when article flavor image is being determined, allows overriding default selection logic
+ * @param array<string,string> $enclosures
+ * @param string $content
+ * @param string $site_url
+ * @param array<string,mixed> $article
+ * @return string|array<int,string>
+ * @see PluginHost::HOOK_ARTICLE_IMAGE
+ */
+ function hook_article_image($enclosures, $content, $site_url, $article) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /** Allows adding arbitrary elements before feed tree
+ * @return string HTML
+ * @see PluginHost::HOOK_FEED_TREE
+ * */
+ function hook_feed_tree() {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /** Invoked for every iframe to determine if it is allowed to be displayed
+ * @param string $url
+ * @return bool
+ * @see PluginHost::HOOK_IFRAME_WHITELISTED
+ */
+ function hook_iframe_whitelisted($url) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return false;
+ }
+
+ /**
+ * @param object $enclosure
+ * @param int $feed
+ * @return object ($enclosure)
+ * @see PluginHost::HOOK_ENCLOSURE_IMPORTED
+ */
+ function hook_enclosure_imported($enclosure, $feed) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return $enclosure;
+ }
+
+ /** Allows adding custom elements to headline sort dropdown (name -> caption)
+ * @return array<string,string>
+ * @see PluginHost::HOOK_HEADLINES_CUSTOM_SORT_MAP
+ */
+ function hook_headlines_custom_sort_map() {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return ["" => ""];
+ }
+
+ /** Allows overriding headline sorting (or provide custom sort methods)
+ * @param string $order
+ * @return array<int, string|bool> -- (query, skip_first_id)
+ * @see PluginHost::HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE
+ */
+ function hook_headlines_custom_sort_override($order) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return ["", false];
+ }
+
+ /** Allows adding custom elements to headlines Select... dropdown
+ * @param int $feed_id
+ * @param int $is_cat
+ * @return string
+ * @see PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM
+ */
+ function hook_headline_toolbar_select_menu_item($feed_id, $is_cat) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return "";
+ }
+
+ /** Invoked when user tries to subscribe to feed, may override information (i.e. feed URL) used afterwards
+ * @param string $url
+ * @param string $auth_login
+ * @param string $auth_pass
+ * @return bool
+ * @see PluginHost::HOOK_PRE_SUBSCRIBE
+ */
+ function hook_pre_subscribe(&$url, $auth_login, $auth_pass) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
+ return false;
+ }
}
diff --git a/classes/pluginhandler.php b/classes/pluginhandler.php
index 75b823822..5c73920e5 100644
--- a/classes/pluginhandler.php
+++ b/classes/pluginhandler.php
@@ -1,10 +1,10 @@
<?php
class PluginHandler extends Handler_Protected {
- function csrf_ignore($method) {
+ function csrf_ignore(string $method): bool {
return true;
}
- function catchall($method) {
+ function catchall(string $method): void {
$plugin_name = clean($_REQUEST["plugin"]);
$plugin = PluginHost::getInstance()->get_plugin($plugin_name);
$csrf_token = ($_POST["csrf_token"] ?? "");
diff --git a/classes/pluginhost.php b/classes/pluginhost.php
index b506a957a..f89cc5c32 100755
--- a/classes/pluginhost.php
+++ b/classes/pluginhost.php
@@ -1,186 +1,211 @@
<?php
class PluginHost {
- private $pdo;
- /* separate handle for plugin data so transaction while saving wouldn't clash with possible main
- tt-rss code transactions; only initialized when first needed */
- private $pdo_data;
- private $hooks = array();
- private $plugins = array();
- private $handlers = array();
- private $commands = array();
- private $storage = array();
- private $feeds = array();
- private $api_methods = array();
- private $plugin_actions = array();
- private $owner_uid;
- private $data_loaded;
- private static $instance;
+ // TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
+ /** @var PDO|null */
+ private $pdo = null;
+
+ /**
+ * separate handle for plugin data so transaction while saving wouldn't clash with possible main
+ * tt-rss code transactions; only initialized when first needed
+ *
+ * @var PDO|null
+ */
+ private $pdo_data = null;
+
+ /** @var array<string, array<int, array<int, Plugin>>> hook types -> priority levels -> Plugins */
+ private $hooks = [];
+
+ /** @var array<string, Plugin> */
+ private $plugins = [];
+
+ /** @var array<string, array<string, Plugin>> handler type -> method type -> Plugin */
+ private $handlers = [];
+
+ /** @var array<string, array{'description': string, 'suffix': string, 'arghelp': string, 'class': Plugin}> command type -> details array */
+ private $commands = [];
+
+ /** @var array<string, array<string, mixed>> plugin name -> (potential profile array) -> key -> value */
+ private $storage = [];
+
+ /** @var array<int, array<int, array{'id': int, 'title': string, 'sender': Plugin, 'icon': string}>> */
+ private $feeds = [];
+
+ /** @var array<string, Plugin> API method name, Plugin sender */
+ private $api_methods = [];
+
+ /** @var array<string, array<int, array{'action': string, 'description': string, 'sender': Plugin}>> */
+ private $plugin_actions = [];
+
+ /** @var int|null */
+ private $owner_uid = null;
+
+ /** @var bool */
+ private $data_loaded = false;
+
+ /** @var PluginHost|null */
+ private static $instance = null;
const API_VERSION = 2;
const PUBLIC_METHOD_DELIMITER = "--";
- // Hooks marked with *1 are run in global context and available
- // to plugins loaded in config.php only
-
- /** hook_article_button($line) */
+ /** @see Plugin::hook_article_button() */
const HOOK_ARTICLE_BUTTON = "hook_article_button";
- /** hook_article_filter($article) */
+ /** @see Plugin::hook_article_filter() */
const HOOK_ARTICLE_FILTER = "hook_article_filter";
- /** hook_prefs_tab($tab) */
+ /** @see Plugin::hook_prefs_tab() */
const HOOK_PREFS_TAB = "hook_prefs_tab";
- /** hook_prefs_tab_section($section) */
+ /** @see Plugin::hook_prefs_tab_section() */
const HOOK_PREFS_TAB_SECTION = "hook_prefs_tab_section";
- /** hook_prefs_tabs() */
+ /** @see Plugin::hook_prefs_tabs() */
const HOOK_PREFS_TABS = "hook_prefs_tabs";
- /** hook_feed_parsed($parser, $feed_id) */
+ /** @see Plugin::hook_feed_parsed() */
const HOOK_FEED_PARSED = "hook_feed_parsed";
- /** GLOBAL: hook_update_task($cli_options) */
+ /** @see Plugin::hook_update_task() */
const HOOK_UPDATE_TASK = "hook_update_task"; //*1
- /** hook_auth_user($login, $password, $service) (byref) */
+ /** @see Plugin::hook_auth_user() */
const HOOK_AUTH_USER = "hook_auth_user";
- /** hook_hotkey_map($hotkeys) (byref) */
+ /** @see Plugin::hook_hotkey_map() */
const HOOK_HOTKEY_MAP = "hook_hotkey_map";
- /** hook_render_article($article) */
+ /** @see Plugin::hook_render_article() */
const HOOK_RENDER_ARTICLE = "hook_render_article";
- /** hook_render_article_cdm($article) */
+ /** @see Plugin::hook_render_article_cdm() */
const HOOK_RENDER_ARTICLE_CDM = "hook_render_article_cdm";
- /** hook_feed_fetched($feed_data, $fetch_url, $owner_uid, $feed) (byref) */
+ /** @see Plugin::hook_feed_fetched() */
const HOOK_FEED_FETCHED = "hook_feed_fetched";
- /** hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) (byref) */
+ /** @see Plugin::hook_sanitize() */
const HOOK_SANITIZE = "hook_sanitize";
- /** hook_render_article_api($params) */
+ /** @see Plugin::hook_render_article_api() */
const HOOK_RENDER_ARTICLE_API = "hook_render_article_api";
- /** hook_toolbar_button() */
+ /** @see Plugin::hook_toolbar_button() */
const HOOK_TOOLBAR_BUTTON = "hook_toolbar_button";
- /** hook_action_item() */
+ /** @see Plugin::hook_action_item() */
const HOOK_ACTION_ITEM = "hook_action_item";
- /** hook_headline_toolbar_button($feed_id, $is_cat) */
+ /** @see Plugin::hook_headline_toolbar_button() */
const HOOK_HEADLINE_TOOLBAR_BUTTON = "hook_headline_toolbar_button";
- /** hook_hotkey_info($hotkeys) (byref) */
+ /** @see Plugin::hook_hotkey_info() */
const HOOK_HOTKEY_INFO = "hook_hotkey_info";
- /** hook_article_left_button($row) */
+ /** @see Plugin::hook_article_left_button() */
const HOOK_ARTICLE_LEFT_BUTTON = "hook_article_left_button";
- /** hook_prefs_edit_feed($feed_id) */
+ /** @see Plugin::hook_prefs_edit_feed() */
const HOOK_PREFS_EDIT_FEED = "hook_prefs_edit_feed";
- /** hook_prefs_save_feed($feed_id) */
+ /** @see Plugin::hook_prefs_save_feed() */
const HOOK_PREFS_SAVE_FEED = "hook_prefs_save_feed";
- /** hook_fetch_feed($feed_data, $fetch_url, $owner_uid, $feed, $last_article_timestamp, $auth_login, $auth_pass) (byref) */
+ /** @see Plugin::hook_fetch_feed() */
const HOOK_FETCH_FEED = "hook_fetch_feed";
- /** hook_query_headlines($row) (byref) */
+ /** @see Plugin::hook_query_headlines() */
const HOOK_QUERY_HEADLINES = "hook_query_headlines";
- /** GLOBAL: hook_house_keeping() */
+ /** @see Plugin::hook_house_keeping() */
const HOOK_HOUSE_KEEPING = "hook_house_keeping"; //*1
- /** hook_search($query) */
+ /** @see Plugin::hook_search() */
const HOOK_SEARCH = "hook_search";
- /** hook_format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images) (byref) */
+ /** @see Plugin::hook_format_enclosures() */
const HOOK_FORMAT_ENCLOSURES = "hook_format_enclosures";
- /** hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) (byref) */
+ /** @see Plugin::hook_subscribe_feed() */
const HOOK_SUBSCRIBE_FEED = "hook_subscribe_feed";
- /** hook_headlines_before($feed, $is_cat, $qfh_ret) */
+ /** @see Plugin::hook_headlines_before() */
const HOOK_HEADLINES_BEFORE = "hook_headlines_before";
- /** hook_render_enclosure($entry, $id, $rv) */
+ /** @see Plugin::hook_render_enclosure() */
const HOOK_RENDER_ENCLOSURE = "hook_render_enclosure";
- /** hook_article_filter_action($article, $action) */
+ /** @see Plugin::hook_article_filter_action() */
const HOOK_ARTICLE_FILTER_ACTION = "hook_article_filter_action";
- /** hook_article_export_feed($line, $feed, $is_cat, $owner_uid) (byref) */
+ /** @see Plugin::hook_article_export_feed() */
const HOOK_ARTICLE_EXPORT_FEED = "hook_article_export_feed";
- /** hook_main_toolbar_button() */
+ /** @see Plugin::hook_main_toolbar_button() */
const HOOK_MAIN_TOOLBAR_BUTTON = "hook_main_toolbar_button";
- /** hook_enclosure_entry($entry, $id, $rv) (byref) */
+ /** @see Plugin::hook_enclosure_entry() */
const HOOK_ENCLOSURE_ENTRY = "hook_enclosure_entry";
- /** hook_format_article($html, $row) */
+ /** @see Plugin::hook_format_article() */
const HOOK_FORMAT_ARTICLE = "hook_format_article";
- /** @deprecated removed, do not use */
+ /** @see Plugin::hook_format_article_cdm() */
const HOOK_FORMAT_ARTICLE_CDM = "hook_format_article_cdm";
- /** hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed_id, $auth_login, $auth_pass) (byref) */
+ /** @see Plugin::hook_feed_basic_info() */
const HOOK_FEED_BASIC_INFO = "hook_feed_basic_info";
- /** hook_send_local_file($filename) */
+ /** @see Plugin::hook_send_local_file() */
const HOOK_SEND_LOCAL_FILE = "hook_send_local_file";
- /** hook_unsubscribe_feed($feed_id, $owner_uid) */
+ /** @see Plugin::hook_unsubscribe_feed() */
const HOOK_UNSUBSCRIBE_FEED = "hook_unsubscribe_feed";
- /** hook_send_mail(Mailer $mailer, $params) */
+ /** @see Plugin::hook_send_mail() */
const HOOK_SEND_MAIL = "hook_send_mail";
- /** hook_filter_triggered($feed_id, $owner_uid, $article, $matched_filters, $matched_rules, $article_filters) */
+ /** @see Plugin::hook_filter_triggered() */
const HOOK_FILTER_TRIGGERED = "hook_filter_triggered";
- /** hook_get_full_text($url) */
+ /** @see Plugin::hook_get_full_text() */
const HOOK_GET_FULL_TEXT = "hook_get_full_text";
- /** hook_article_image($enclosures, $content, $site_url) */
+ /** @see Plugin::hook_article_image() */
const HOOK_ARTICLE_IMAGE = "hook_article_image";
- /** hook_feed_tree() */
+ /** @see Plugin::hook_feed_tree() */
const HOOK_FEED_TREE = "hook_feed_tree";
- /** hook_iframe_whitelisted($url) */
+ /** @see Plugin::hook_iframe_whitelisted() */
const HOOK_IFRAME_WHITELISTED = "hook_iframe_whitelisted";
- /** hook_enclosure_imported($enclosure, $feed) */
+ /** @see Plugin::hook_enclosure_imported() */
const HOOK_ENCLOSURE_IMPORTED = "hook_enclosure_imported";
- /** hook_headlines_custom_sort_map() */
+ /** @see Plugin::hook_headlines_custom_sort_map() */
const HOOK_HEADLINES_CUSTOM_SORT_MAP = "hook_headlines_custom_sort_map";
- /** hook_headlines_custom_sort_override($order) */
+ /** @see Plugin::hook_headlines_custom_sort_override() */
const HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE = "hook_headlines_custom_sort_override";
- /** hook_headline_toolbar_select_menu_item($feed_id, $is_cat) */
+ /** @see Plugin::hook_headline_toolbar_select_menu_item() */
const HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM = "hook_headline_toolbar_select_menu_item";
-
- /** hook_pre_subscribe($url, $auth_login, $auth_pass) (byref) */
+ /** @see Plugin::hook_pre_subscribe() */
const HOOK_PRE_SUBSCRIBE = "hook_pre_subscribe";
const KIND_ALL = 1;
const KIND_SYSTEM = 2;
const KIND_USER = 3;
- static function object_to_domain(Plugin $plugin) {
+ static function object_to_domain(Plugin $plugin): string {
return strtolower(get_class($plugin));
}
function __construct() {
$this->pdo = Db::pdo();
- $this->storage = array();
+ $this->storage = [];
}
private function __clone() {
@@ -194,18 +219,18 @@ class PluginHost {
return self::$instance;
}
- private function register_plugin(string $name, Plugin $plugin) {
+ private function register_plugin(string $name, Plugin $plugin): void {
//array_push($this->plugins, $plugin);
$this->plugins[$name] = $plugin;
}
/** needed for compatibility with API 1 */
- function get_link() {
+ function get_link(): bool {
return false;
}
/** needed for compatibility with API 2 (?) */
- function get_dbh() {
+ function get_dbh(): bool {
return false;
}
@@ -213,8 +238,11 @@ class PluginHost {
return $this->pdo;
}
- function get_plugin_names() {
- $names = array();
+ /**
+ * @return array<int, string>
+ */
+ function get_plugin_names(): array {
+ $names = [];
foreach ($this->plugins as $p) {
array_push($names, get_class($p));
@@ -223,15 +251,22 @@ class PluginHost {
return $names;
}
- function get_plugins() {
+ /**
+ * @return array<Plugin>
+ */
+ function get_plugins(): array {
return $this->plugins;
}
- function get_plugin(string $name) {
+ function get_plugin(string $name): ?Plugin {
return $this->plugins[strtolower($name)] ?? null;
}
- function run_hooks(string $hook, ...$args) {
+ /**
+ * @param PluginHost::HOOK_* $hook
+ * @param mixed $args
+ */
+ function run_hooks(string $hook, ...$args): void {
$method = strtolower($hook);
foreach ($this->get_hooks($hook) as $plugin) {
@@ -247,7 +282,12 @@ class PluginHost {
}
}
- function run_hooks_until(string $hook, $check, ...$args) {
+ /**
+ * @param PluginHost::HOOK_* $hook
+ * @param mixed $args
+ * @param mixed $check
+ */
+ function run_hooks_until(string $hook, $check, ...$args): bool {
$method = strtolower($hook);
foreach ($this->get_hooks($hook) as $plugin) {
@@ -267,7 +307,11 @@ class PluginHost {
return false;
}
- function run_hooks_callback(string $hook, Closure $callback, ...$args) {
+ /**
+ * @param PluginHost::HOOK_* $hook
+ * @param mixed $args
+ */
+ function run_hooks_callback(string $hook, Closure $callback, ...$args): void {
$method = strtolower($hook);
foreach ($this->get_hooks($hook) as $plugin) {
@@ -284,7 +328,11 @@ class PluginHost {
}
}
- function chain_hooks_callback(string $hook, Closure $callback, &...$args) {
+ /**
+ * @param PluginHost::HOOK_* $hook
+ * @param mixed $args
+ */
+ function chain_hooks_callback(string $hook, Closure $callback, &...$args): void {
$method = strtolower($hook);
foreach ($this->get_hooks($hook) as $plugin) {
@@ -301,7 +349,10 @@ class PluginHost {
}
}
- function add_hook(string $type, Plugin $sender, int $priority = 50) {
+ /**
+ * @param PluginHost::HOOK_* $type
+ */
+ function add_hook(string $type, Plugin $sender, int $priority = 50): void {
$priority = (int) $priority;
if (!method_exists($sender, strtolower($type))) {
@@ -325,7 +376,10 @@ class PluginHost {
ksort($this->hooks[$type]);
}
- function del_hook(string $type, Plugin $sender) {
+ /**
+ * @param PluginHost::HOOK_* $type
+ */
+ function del_hook(string $type, Plugin $sender): void {
if (is_array($this->hooks[$type])) {
foreach (array_keys($this->hooks[$type]) as $prio) {
$key = array_search($sender, $this->hooks[$type][$prio]);
@@ -337,6 +391,10 @@ class PluginHost {
}
}
+ /**
+ * @param PluginHost::HOOK_* $type
+ * @return array<int, Plugin>
+ */
function get_hooks(string $type) {
if (isset($this->hooks[$type])) {
$tmp = [];
@@ -346,11 +404,14 @@ class PluginHost {
}
return $tmp;
- } else {
- return [];
}
+ return [];
}
- function load_all(int $kind, int $owner_uid = null, bool $skip_init = false) {
+
+ /**
+ * @param PluginHost::KIND_* $kind
+ */
+ function load_all(int $kind, int $owner_uid = null, bool $skip_init = false): void {
$plugins = array_merge(glob("plugins/*"), glob("plugins.local/*"));
$plugins = array_filter($plugins, "is_dir");
@@ -361,7 +422,10 @@ class PluginHost {
$this->load(join(",", $plugins), $kind, $owner_uid, $skip_init);
}
- function load(string $classlist, int $kind, int $owner_uid = null, bool $skip_init = false) {
+ /**
+ * @param PluginHost::KIND_* $kind
+ */
+ function load(string $classlist, int $kind, int $owner_uid = null, bool $skip_init = false): void {
$plugins = explode(",", $classlist);
$this->owner_uid = (int) $owner_uid;
@@ -381,8 +445,28 @@ class PluginHost {
}
if (!isset($this->plugins[$class])) {
+
+ // WIP hack
+ // we can't catch incompatible method signatures via Throwable
+ // this also enables global tt-rss safe mode in case there are more plugins like this
+ if (($_SESSION["plugin_blacklist"][$class] ?? 0)) {
+
+ // only report once per-plugin per-session
+ if ($_SESSION["plugin_blacklist"][$class] < 2) {
+ user_error("Plugin $class has caused a PHP fatal error so it won't be loaded again in this session.", E_USER_WARNING);
+ $_SESSION["plugin_blacklist"][$class] = 2;
+ }
+
+ $_SESSION["safe_mode"] = 1;
+
+ continue;
+ }
+
try {
+ $_SESSION["plugin_blacklist"][$class] = 1;
require_once $file;
+ unset($_SESSION["plugin_blacklist"][$class]);
+
} catch (Error $err) {
user_error($err, E_USER_WARNING);
continue;
@@ -434,27 +518,27 @@ class PluginHost {
$this->load_data();
}
- function is_system(Plugin $plugin) {
+ function is_system(Plugin $plugin): bool {
$about = $plugin->about();
- return $about[3] ?? false;
+ return ($about[3] ?? false) === true;
}
// only system plugins are allowed to modify routing
- function add_handler(string $handler, $method, Plugin $sender) {
+ function add_handler(string $handler, string $method, Plugin $sender): void {
$handler = str_replace("-", "_", strtolower($handler));
$method = strtolower($method);
if ($this->is_system($sender)) {
if (!isset($this->handlers[$handler])) {
- $this->handlers[$handler] = array();
+ $this->handlers[$handler] = [];
}
$this->handlers[$handler][$method] = $sender;
}
}
- function del_handler(string $handler, $method, Plugin $sender) {
+ function del_handler(string $handler, string $method, Plugin $sender): void {
$handler = str_replace("-", "_", strtolower($handler));
$method = strtolower($method);
@@ -463,7 +547,10 @@ class PluginHost {
}
}
- function lookup_handler($handler, $method) {
+ /**
+ * @return false|Plugin false if the handler couldn't be found, otherwise the Plugin/handler
+ */
+ function lookup_handler(string $handler, string $method) {
$handler = str_replace("-", "_", strtolower($handler));
$method = strtolower($method);
@@ -478,7 +565,7 @@ class PluginHost {
return false;
}
- function add_command(string $command, string $description, Plugin $sender, string $suffix = "", string $arghelp = "") {
+ function add_command(string $command, string $description, Plugin $sender, string $suffix = "", string $arghelp = ""): void {
$command = str_replace("-", "_", strtolower($command));
$this->commands[$command] = array("description" => $description,
@@ -487,27 +574,34 @@ class PluginHost {
"class" => $sender);
}
- function del_command(string $command) {
+ function del_command(string $command): void {
$command = "-" . strtolower($command);
unset($this->commands[$command]);
}
- function lookup_command($command) {
+ /**
+ * @return false|Plugin false if the command couldn't be found, otherwise the registered Plugin
+ */
+ function lookup_command(string $command) {
$command = "-" . strtolower($command);
- if (is_array($this->commands[$command])) {
+ if (array_key_exists($command, $this->commands) && is_array($this->commands[$command])) {
return $this->commands[$command]["class"];
} else {
return false;
}
}
+ /** @return array<string, array{'description': string, 'suffix': string, 'arghelp': string, 'class': Plugin}>> command type -> details array */
function get_commands() {
return $this->commands;
}
- function run_commands(array $args) {
+ /**
+ * @param array<string, mixed> $args
+ */
+ function run_commands(array $args): void {
foreach ($this->get_commands() as $command => $data) {
if (isset($args[$command])) {
$command = str_replace("-", "", $command);
@@ -516,7 +610,7 @@ class PluginHost {
}
}
- private function load_data() {
+ private function load_data(): void {
if ($this->owner_uid && !$this->data_loaded && get_schema_version() > 100) {
$sth = $this->pdo->prepare("SELECT name, content FROM ttrss_plugin_storage
WHERE owner_uid = ?");
@@ -530,7 +624,7 @@ class PluginHost {
}
}
- private function save_data(string $plugin) {
+ private function save_data(string $plugin): void {
if ($this->owner_uid) {
if (!$this->pdo_data)
@@ -543,7 +637,7 @@ class PluginHost {
$sth->execute([$this->owner_uid, $plugin]);
if (!isset($this->storage[$plugin]))
- $this->storage[$plugin] = array();
+ $this->storage[$plugin] = [];
$content = serialize($this->storage[$plugin]);
@@ -563,8 +657,12 @@ class PluginHost {
}
}
- // same as set(), but sets data to current preference profile
- function profile_set(Plugin $sender, string $name, $value) {
+ /**
+ * same as set(), but sets data to current preference profile
+ *
+ * @param mixed $value
+ */
+ function profile_set(Plugin $sender, string $name, $value): void {
$profile_id = $_SESSION["profile"] ?? null;
if ($profile_id) {
@@ -582,26 +680,32 @@ class PluginHost {
$this->save_data(get_class($sender));
} else {
- return $this->set($sender, $name, $value);
+ $this->set($sender, $name, $value);
}
}
- function set(Plugin $sender, string $name, $value) {
+ /**
+ * @param mixed $value
+ */
+ function set(Plugin $sender, string $name, $value): void {
$idx = get_class($sender);
if (!isset($this->storage[$idx]))
- $this->storage[$idx] = array();
+ $this->storage[$idx] = [];
$this->storage[$idx][$name] = $value;
$this->save_data(get_class($sender));
}
- function set_array(Plugin $sender, array $params) {
+ /**
+ * @param array<int|string, mixed> $params
+ */
+ function set_array(Plugin $sender, array $params): void {
$idx = get_class($sender);
if (!isset($this->storage[$idx]))
- $this->storage[$idx] = array();
+ $this->storage[$idx] = [];
foreach ($params as $name => $value)
$this->storage[$idx][$name] = $value;
@@ -609,7 +713,12 @@ class PluginHost {
$this->save_data(get_class($sender));
}
- // same as get(), but sets data to current preference profile
+ /**
+ * same as get(), but sets data to current preference profile
+ *
+ * @param mixed $default_value
+ * @return mixed
+ */
function profile_get(Plugin $sender, string $name, $default_value = false) {
$profile_id = $_SESSION["profile"] ?? null;
@@ -629,6 +738,10 @@ class PluginHost {
}
}
+ /**
+ * @param mixed $default_value
+ * @return mixed
+ */
function get(Plugin $sender, string $name, $default_value = false) {
$idx = get_class($sender);
@@ -641,6 +754,10 @@ class PluginHost {
}
}
+ /**
+ * @param array<int|string, mixed> $default_value
+ * @return array<int|string, mixed>
+ */
function get_array(Plugin $sender, string $name, array $default_value = []) {
$tmp = $this->get($sender, $name);
@@ -649,13 +766,16 @@ class PluginHost {
return $tmp;
}
- function get_all($sender) {
+ /**
+ * @return array<string, mixed>
+ */
+ function get_all(Plugin $sender) {
$idx = get_class($sender);
return $this->storage[$idx] ?? [];
}
- function clear_data(Plugin $sender) {
+ function clear_data(Plugin $sender): void {
if ($this->owner_uid) {
$idx = get_class($sender);
@@ -670,7 +790,7 @@ class PluginHost {
// Plugin feed functions are *EXPERIMENTAL*!
// cat_id: only -1 is supported (Special)
- function add_feed(int $cat_id, string $title, string $icon, Plugin $sender) {
+ function add_feed(int $cat_id, string $title, string $icon, Plugin $sender): int {
if (empty($this->feeds[$cat_id]))
$this->feeds[$cat_id] = [];
@@ -683,12 +803,15 @@ class PluginHost {
return $id;
}
+ /**
+ * @return array<int, array{'id': int, 'title': string, 'sender': Plugin, 'icon': string}>
+ */
function get_feeds(int $cat_id) {
return $this->feeds[$cat_id] ?? [];
}
// convert feed_id (e.g. -129) to pfeed_id first
- function get_feed_handler(int $pfeed_id) {
+ function get_feed_handler(int $pfeed_id): ?Plugin {
foreach ($this->feeds as $cat) {
foreach ($cat as $feed) {
if ($feed['id'] == $pfeed_id) {
@@ -696,46 +819,54 @@ class PluginHost {
}
}
}
+ return null;
}
- static function pfeed_to_feed_id(int $pfeed) {
+ static function pfeed_to_feed_id(int $pfeed): int {
return PLUGIN_FEED_BASE_INDEX - 1 - abs($pfeed);
}
- static function feed_to_pfeed_id(int $feed) {
+ static function feed_to_pfeed_id(int $feed): int {
return PLUGIN_FEED_BASE_INDEX - 1 + abs($feed);
}
- function add_api_method(string $name, Plugin $sender) {
+ function add_api_method(string $name, Plugin $sender): void {
if ($this->is_system($sender)) {
$this->api_methods[strtolower($name)] = $sender;
}
}
- function get_api_method(string $name) {
- return $this->api_methods[$name];
+ function get_api_method(string $name): ?Plugin {
+ return $this->api_methods[$name] ?? null;
}
- function add_filter_action(Plugin $sender, string $action_name, string $action_desc) {
+ function add_filter_action(Plugin $sender, string $action_name, string $action_desc): void {
$sender_class = get_class($sender);
if (!isset($this->plugin_actions[$sender_class]))
- $this->plugin_actions[$sender_class] = array();
+ $this->plugin_actions[$sender_class] = [];
array_push($this->plugin_actions[$sender_class],
array("action" => $action_name, "description" => $action_desc, "sender" => $sender));
}
+ /**
+ * @return array<string, array<int, array{'action': string, 'description': string, 'sender': Plugin}>>
+ */
function get_filter_actions() {
return $this->plugin_actions;
}
- function get_owner_uid() {
+ function get_owner_uid(): ?int {
return $this->owner_uid;
}
- // handled by classes/pluginhandler.php, requires valid session
- function get_method_url(Plugin $sender, string $method, $params = []) {
+ /**
+ * handled by classes/pluginhandler.php, requires valid session
+ *
+ * @param array<int|string, mixed> $params
+ */
+ function get_method_url(Plugin $sender, string $method, array $params = []): string {
return Config::get_self_url() . "/backend.php?" .
http_build_query(
array_merge(
@@ -758,8 +889,12 @@ class PluginHost {
$params));
} */
- // WARNING: endpoint in public.php, exposed to unauthenticated users
- function get_public_method_url(Plugin $sender, string $method, $params = []) {
+ /**
+ * WARNING: endpoint in public.php, exposed to unauthenticated users
+ *
+ * @param array<int|string, mixed> $params
+ */
+ function get_public_method_url(Plugin $sender, string $method, array $params = []): ?string {
if ($sender->is_public_method($method)) {
return Config::get_self_url() . "/public.php?" .
http_build_query(
@@ -768,18 +903,18 @@ class PluginHost {
"op" => strtolower(get_class($sender) . self::PUBLIC_METHOD_DELIMITER . $method),
],
$params));
- } else {
- user_error("get_public_method_url: requested method '$method' of '" . get_class($sender) . "' is private.");
}
+ user_error("get_public_method_url: requested method '$method' of '" . get_class($sender) . "' is private.");
+ return null;
}
- function get_plugin_dir(Plugin $plugin) {
+ function get_plugin_dir(Plugin $plugin): string {
$ref = new ReflectionClass(get_class($plugin));
return dirname($ref->getFileName());
}
// TODO: use get_plugin_dir()
- function is_local(Plugin $plugin) {
+ function is_local(Plugin $plugin): bool {
$ref = new ReflectionClass(get_class($plugin));
return basename(dirname(dirname($ref->getFileName()))) == "plugins.local";
}
diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php
index ac0874259..47479e124 100755
--- a/classes/pref/feeds.php
+++ b/classes/pref/feeds.php
@@ -5,13 +5,16 @@ class Pref_Feeds extends Handler_Protected {
const E_ICON_UPLOAD_FAILED = 'E_ICON_UPLOAD_FAILED';
const E_ICON_UPLOAD_SUCCESS = 'E_ICON_UPLOAD_SUCCESS';
- function csrf_ignore($method) {
+ function csrf_ignore(string $method): bool {
$csrf_ignored = array("index", "getfeedtree", "savefeedorder");
return array_search($method, $csrf_ignored) !== false;
}
- public static function get_ts_languages() {
+ /**
+ * @return array<int, string>
+ */
+ public static function get_ts_languages(): array {
if (Config::get(Config::DB_TYPE) == 'pgsql') {
return array_map('ucfirst',
array_column(ORM::for_table('pg_ts_config')->select('cfgname')->find_array(), 'cfgname'));
@@ -20,7 +23,7 @@ class Pref_Feeds extends Handler_Protected {
return [];
}
- function renameCat() {
+ function renameCat(): void {
$cat = ORM::for_table("ttrss_feed_categories")
->where("owner_uid", $_SESSION["uid"])
->find_one($_REQUEST['id']);
@@ -33,7 +36,10 @@ class Pref_Feeds extends Handler_Protected {
}
}
- private function get_category_items($cat_id) {
+ /**
+ * @return array<int, array<string, bool|int|string>>
+ */
+ private function get_category_items(int $cat_id): array {
if (clean($_REQUEST['mode'] ?? 0) != 2)
$search = $_SESSION["prefs_feed_search"] ?? "";
@@ -103,11 +109,14 @@ class Pref_Feeds extends Handler_Protected {
return $items;
}
- function getfeedtree() {
+ function getfeedtree(): void {
print json_encode($this->_makefeedtree());
}
- function _makefeedtree() {
+ /**
+ * @return array<string, array<int|string, mixed>|string>
+ */
+ function _makefeedtree(): array {
if (clean($_REQUEST['mode'] ?? 0) != 2)
$search = $_SESSION["prefs_feed_search"] ?? "";
@@ -184,7 +193,7 @@ class Pref_Feeds extends Handler_Protected {
if (count($labels)) {
foreach ($labels as $label) {
$label_id = Labels::label_to_feed_id($label->id);
- $feed = $this->feedlist_init_feed($label_id, false, 0);
+ $feed = $this->feedlist_init_feed($label_id, null, false);
$feed['fg_color'] = $label->fg_color;
$feed['bg_color'] = $label->bg_color;
array_push($cat['items'], $feed);
@@ -319,19 +328,22 @@ class Pref_Feeds extends Handler_Protected {
];
}
- function catsortreset() {
+ function catsortreset(): void {
$sth = $this->pdo->prepare("UPDATE ttrss_feed_categories
SET order_id = 0 WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
}
- function feedsortreset() {
+ function feedsortreset(): void {
$sth = $this->pdo->prepare("UPDATE ttrss_feeds
SET order_id = 0 WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
}
- private function process_category_order(&$data_map, $item_id, $parent_id = false, $nest_level = 0) {
+ /**
+ * @param array<string, mixed> $data_map
+ */
+ private function process_category_order(array &$data_map, string $item_id = '', string $parent_id = '', int $nest_level = 0): void {
$prefix = "";
for ($i = 0; $i < $nest_level; $i++)
@@ -403,7 +415,7 @@ class Pref_Feeds extends Handler_Protected {
}
}
- function savefeedorder() {
+ function savefeedorder(): void {
$data = json_decode($_POST['payload'], true);
#file_put_contents("/tmp/saveorder.json", clean($_POST['payload']));
@@ -417,8 +429,9 @@ class Pref_Feeds extends Handler_Protected {
if (is_array($data) && is_array($data['items'])) {
# $cat_order_id = 0;
+ /** @var array<int, mixed> */
$data_map = array();
- $root_item = false;
+ $root_item = '';
foreach ($data['items'] as $item) {
@@ -439,7 +452,7 @@ class Pref_Feeds extends Handler_Protected {
}
}
- function removeIcon() {
+ function removeIcon(): void {
$feed_id = (int) $_REQUEST["feed_id"];
$icon_file = Config::get(Config::ICONS_DIR) . "/$feed_id.ico";
@@ -459,7 +472,7 @@ class Pref_Feeds extends Handler_Protected {
}
}
- function uploadIcon() {
+ function uploadIcon(): void {
$feed_id = (int) $_REQUEST['feed_id'];
$tmp_file = tempnam(Config::get(Config::CACHE_DIR) . '/upload', 'icon');
@@ -502,7 +515,7 @@ class Pref_Feeds extends Handler_Protected {
print json_encode(['rc' => $rc, 'icon_url' => Feeds::_get_icon($feed_id)]);
}
- function editfeed() {
+ function editfeed(): void {
global $purge_intervals;
global $update_intervals;
@@ -564,12 +577,12 @@ class Pref_Feeds extends Handler_Protected {
}
}
- private function _batch_toggle_checkbox($name) {
+ private function _batch_toggle_checkbox(string $name): string {
return \Controls\checkbox_tag("", false, "",
["data-control-for" => $name, "title" => __("Check to enable field"), "onchange" => "App.dialogOf(this).toggleField(this)"]);
}
- function editfeeds() {
+ function editfeeds(): void {
global $purge_intervals;
global $update_intervals;
@@ -677,15 +690,15 @@ class Pref_Feeds extends Handler_Protected {
<?php
}
- function batchEditSave() {
- return $this->editsaveops(true);
+ function batchEditSave(): void {
+ $this->editsaveops(true);
}
- function editSave() {
- return $this->editsaveops(false);
+ function editSave(): void {
+ $this->editsaveops(false);
}
- private function editsaveops($batch) {
+ private function editsaveops(bool $batch): void {
$feed_title = clean($_POST["title"]);
$feed_url = clean($_POST["feed_url"]);
@@ -774,11 +787,11 @@ class Pref_Feeds extends Handler_Protected {
break;
case "update_interval":
- $qpart = "update_interval = " . $this->pdo->quote($upd_intl);
+ $qpart = "update_interval = " . $upd_intl; // made int above
break;
case "purge_interval":
- $qpart = "purge_interval =" . $this->pdo->quote($purge_intl);
+ $qpart = "purge_interval = " . $purge_intl; // made int above
break;
case "auth_login":
@@ -790,33 +803,33 @@ class Pref_Feeds extends Handler_Protected {
break;
case "private":
- $qpart = "private = " . $this->pdo->quote($private);
+ $qpart = "private = " . $private; // made int above
break;
case "include_in_digest":
- $qpart = "include_in_digest = " . $this->pdo->quote($include_in_digest);
+ $qpart = "include_in_digest = " . $include_in_digest; // made int above
break;
case "always_display_enclosures":
- $qpart = "always_display_enclosures = " . $this->pdo->quote($always_display_enclosures);
+ $qpart = "always_display_enclosures = " . $always_display_enclosures; // made int above
break;
case "mark_unread_on_update":
- $qpart = "mark_unread_on_update = " . $this->pdo->quote($mark_unread_on_update);
+ $qpart = "mark_unread_on_update = " . $mark_unread_on_update; // made int above
break;
case "cache_images":
- $qpart = "cache_images = " . $this->pdo->quote($cache_images);
+ $qpart = "cache_images = " . $cache_images; // made int above
break;
case "hide_images":
- $qpart = "hide_images = " . $this->pdo->quote($hide_images);
+ $qpart = "hide_images = " . $hide_images; // made int above
break;
case "cat_id":
if (get_pref(Prefs::ENABLE_FEED_CATS)) {
if ($cat_id) {
- $qpart = "cat_id = " . $this->pdo->quote($cat_id);
+ $qpart = "cat_id = " . $cat_id; // made int above
} else {
$qpart = 'cat_id = NULL';
}
@@ -841,39 +854,36 @@ class Pref_Feeds extends Handler_Protected {
$this->pdo->commit();
}
- return;
}
- function remove() {
-
- $ids = explode(",", clean($_REQUEST["ids"]));
+ function remove(): void {
+ /** @var array<int, int> */
+ $ids = array_map('intval', explode(",", clean($_REQUEST["ids"])));
foreach ($ids as $id) {
self::remove_feed($id, $_SESSION["uid"]);
}
-
- return;
}
- function removeCat() {
+ function removeCat(): void {
$ids = explode(",", clean($_REQUEST["ids"]));
foreach ($ids as $id) {
Feeds::_remove_cat((int)$id, $_SESSION["uid"]);
}
}
- function addCat() {
+ function addCat(): void {
$feed_cat = clean($_REQUEST["cat"]);
Feeds::_add_cat($feed_cat, $_SESSION['uid']);
}
- function importOpml() {
+ function importOpml(): void {
$opml = new OPML($_REQUEST);
$opml->opml_import($_SESSION["uid"]);
}
- private function index_feeds() {
+ private function index_feeds(): void {
$error_button = "<button dojoType='dijit.form.Button'
id='pref_feeds_errors_btn' style='display : none'
onclick='CommonDialogs.showFeedsWithErrors()'>".
@@ -984,7 +994,7 @@ class Pref_Feeds extends Handler_Protected {
}
- private function index_opml() {
+ private function index_opml(): void {
?>
<form id='opml_import_form' method='post' enctype='multipart/form-data'>
@@ -1020,7 +1030,7 @@ class Pref_Feeds extends Handler_Protected {
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefFeedsOPML");
}
- private function index_shared() {
+ private function index_shared(): void {
?>
<?= format_notice('Published articles can be subscribed by anyone who knows the following URL:') ?></h3>
@@ -1040,7 +1050,7 @@ class Pref_Feeds extends Handler_Protected {
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefFeedsPublishedGenerated");
}
- function index() {
+ function index(): void {
?>
<div dojoType='dijit.layout.TabContainer' tabPosition='left-h'>
@@ -1079,44 +1089,44 @@ class Pref_Feeds extends Handler_Protected {
<?php
}
- private function feedlist_init_cat($cat_id) {
- $obj = array();
- $cat_id = (int) $cat_id;
-
- $obj['id'] = 'CAT:' . $cat_id;
- $obj['items'] = array();
- $obj['name'] = Feeds::_get_cat_title($cat_id);
- $obj['type'] = 'category';
- $obj['unread'] = -1; //(int) Feeds::_get_cat_unread($cat_id);
- $obj['bare_id'] = $cat_id;
-
- return $obj;
+ /**
+ * @return array<string, mixed>
+ */
+ private function feedlist_init_cat(int $cat_id): array {
+ return [
+ 'id' => 'CAT:' . $cat_id,
+ 'items' => array(),
+ 'name' => Feeds::_get_cat_title($cat_id),
+ 'type' => 'category',
+ 'unread' => -1, //(int) Feeds::_get_cat_unread($cat_id);
+ 'bare_id' => $cat_id,
+ ];
}
- private function feedlist_init_feed($feed_id, $title = false, $unread = false, $error = '', $updated = '') {
- $obj = array();
- $feed_id = (int) $feed_id;
-
+ /**
+ * @return array<string, mixed>
+ */
+ private function feedlist_init_feed(int $feed_id, ?string $title = null, bool $unread = false, string $error = '', string $updated = ''): array {
if (!$title)
$title = Feeds::_get_title($feed_id, false);
if ($unread === false)
$unread = getFeedUnread($feed_id, false);
- $obj['id'] = 'FEED:' . $feed_id;
- $obj['name'] = $title;
- $obj['unread'] = (int) $unread;
- $obj['type'] = 'feed';
- $obj['error'] = $error;
- $obj['updated'] = $updated;
- $obj['icon'] = Feeds::_get_icon($feed_id);
- $obj['bare_id'] = $feed_id;
- $obj['auxcounter'] = 0;
-
- return $obj;
+ return [
+ 'id' => 'FEED:' . $feed_id,
+ 'name' => $title,
+ 'unread' => (int) $unread,
+ 'type' => 'feed',
+ 'error' => $error,
+ 'updated' => $updated,
+ 'icon' => Feeds::_get_icon($feed_id),
+ 'bare_id' => $feed_id,
+ 'auxcounter' => 0,
+ ];
}
- function inactiveFeeds() {
+ function inactiveFeeds(): void {
if (Config::get(Config::DB_TYPE) == "pgsql") {
$interval_qpart = "NOW() - INTERVAL '3 months'";
@@ -1150,7 +1160,7 @@ class Pref_Feeds extends Handler_Protected {
print json_encode($inactive_feeds);
}
- function feedsWithErrors() {
+ function feedsWithErrors(): void {
print json_encode(ORM::for_table('ttrss_feeds')
->select_many('id', 'title', 'feed_url', 'last_error', 'site_url')
->where_not_equal('last_error', '')
@@ -1158,7 +1168,7 @@ class Pref_Feeds extends Handler_Protected {
->find_array());
}
- static function remove_feed($id, $owner_uid) {
+ static function remove_feed(int $id, int $owner_uid): void {
if (PluginHost::getInstance()->run_hooks_until(PluginHost::HOOK_UNSUBSCRIBE_FEED, true, $id, $owner_uid))
return;
@@ -1199,14 +1209,14 @@ class Pref_Feeds extends Handler_Protected {
}
}
- function batchSubscribe() {
+ function batchSubscribe(): void {
print json_encode([
"enable_cats" => (int)get_pref(Prefs::ENABLE_FEED_CATS),
"cat_select" => \Controls\select_feeds_cats("cat")
]);
}
- function batchAddFeeds() {
+ function batchAddFeeds(): void {
$cat_id = clean($_REQUEST['cat']);
$feeds = explode("\n", clean($_REQUEST['feeds']));
$login = clean($_REQUEST['login']);
@@ -1216,7 +1226,7 @@ class Pref_Feeds extends Handler_Protected {
// TODO: we should return some kind of error code to frontend here
if ($user->access_level == UserHelper::ACCESS_LEVEL_READONLY) {
- return false;
+ return;
}
$csth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
@@ -1244,11 +1254,11 @@ class Pref_Feeds extends Handler_Protected {
}
}
- function clearKeys() {
- return Feeds::_clear_access_keys($_SESSION['uid']);
+ function clearKeys(): void {
+ Feeds::_clear_access_keys($_SESSION['uid']);
}
- function regenFeedKey() {
+ function regenFeedKey(): void {
$feed_id = clean($_REQUEST['id']);
$is_cat = clean($_REQUEST['is_cat']);
@@ -1257,7 +1267,7 @@ class Pref_Feeds extends Handler_Protected {
print json_encode(["link" => $new_key]);
}
- function getSharedURL() {
+ function getSharedURL(): void {
$feed_id = clean($_REQUEST['id']);
$is_cat = clean($_REQUEST['is_cat']) == "true";
$search = clean($_REQUEST['search']);
@@ -1276,7 +1286,10 @@ class Pref_Feeds extends Handler_Protected {
]);
}
- private function calculate_children_count($cat) {
+ /**
+ * @param array<string, mixed> $cat
+ */
+ private function calculate_children_count(array $cat): int {
$c = 0;
foreach ($cat['items'] ?? [] as $child) {
diff --git a/classes/pref/filters.php b/classes/pref/filters.php
index c4017e4ec..6e6e3d9ee 100755
--- a/classes/pref/filters.php
+++ b/classes/pref/filters.php
@@ -1,20 +1,19 @@
<?php
class Pref_Filters extends Handler_Protected {
- function csrf_ignore($method) {
+ function csrf_ignore(string $method): bool {
$csrf_ignored = array("index", "getfiltertree", "savefilterorder");
return array_search($method, $csrf_ignored) !== false;
}
- function filtersortreset() {
+ function filtersortreset(): void {
$sth = $this->pdo->prepare("UPDATE ttrss_filters2
SET order_id = 0 WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
- return;
}
- function savefilterorder() {
+ function savefilterorder(): void {
$data = json_decode($_POST['payload'], true);
#file_put_contents("/tmp/saveorder.json", clean($_POST['payload']));
@@ -40,11 +39,9 @@ class Pref_Filters extends Handler_Protected {
}
}
}
-
- return;
}
- function testFilterDo() {
+ function testFilterDo(): void {
$offset = (int) clean($_REQUEST["offset"]);
$limit = (int) clean($_REQUEST["limit"]);
@@ -59,7 +56,9 @@ class Pref_Filters extends Handler_Protected {
$res = $this->pdo->query("SELECT id,name FROM ttrss_filter_types");
- $filter_types = array();
+ /** @var array<int, string> */
+ $filter_types = [];
+
while ($line = $res->fetch()) {
$filter_types[$line["id"]] = $line["name"];
}
@@ -67,7 +66,10 @@ class Pref_Filters extends Handler_Protected {
$scope_qparts = array();
$rctr = 0;
+
+ /** @var string $r */
foreach (clean($_REQUEST["rule"]) AS $r) {
+ /** @var array{'reg_exp': string, 'filter_type': int, 'feed_id': array<int, int|string>, 'name': string}|null */
$rule = json_decode($r, true);
if ($rule && $rctr < 5) {
@@ -75,19 +77,21 @@ class Pref_Filters extends Handler_Protected {
unset($rule["filter_type"]);
$scope_inner_qparts = [];
+
+ /** @var int|string $feed_id may be a category string (e.g. 'CAT:7') or feed ID int */
foreach ($rule["feed_id"] as $feed_id) {
- if (strpos($feed_id, "CAT:") === 0) {
- $cat_id = (int) substr($feed_id, 4);
- array_push($scope_inner_qparts, "cat_id = " . $this->pdo->quote($cat_id));
- } else if ($feed_id > 0) {
- array_push($scope_inner_qparts, "feed_id = " . $this->pdo->quote($feed_id));
- }
- }
+ if (strpos("$feed_id", "CAT:") === 0) {
+ $cat_id = (int) substr("$feed_id", 4);
+ array_push($scope_inner_qparts, "cat_id = " . $cat_id);
+ } else if (is_numeric($feed_id) && $feed_id > 0) {
+ array_push($scope_inner_qparts, "feed_id = " . (int)$feed_id);
+ }
+ }
- if (count($scope_inner_qparts) > 0) {
- array_push($scope_qparts, "(" . implode(" OR ", $scope_inner_qparts) . ")");
- }
+ if (count($scope_inner_qparts) > 0) {
+ array_push($scope_qparts, "(" . implode(" OR ", $scope_inner_qparts) . ")");
+ }
array_push($filter["rules"], $rule);
@@ -162,7 +166,7 @@ class Pref_Filters extends Handler_Protected {
print json_encode($rv);
}
- private function _get_rules_list($filter_id) {
+ private function _get_rules_list(int $filter_id): string {
$sth = $this->pdo->prepare("SELECT reg_exp,
inverse,
match_on,
@@ -222,7 +226,7 @@ class Pref_Filters extends Handler_Protected {
return $rv;
}
- function getfiltertree() {
+ function getfiltertree(): void {
$root = array();
$root['id'] = 'root';
$root['name'] = __('Filters');
@@ -307,10 +311,9 @@ class Pref_Filters extends Handler_Protected {
$fl['items'] = array($root);
print json_encode($fl);
- return;
}
- function edit() {
+ function edit(): void {
$filter_id = (int) clean($_REQUEST["id"] ?? 0);
@@ -406,7 +409,10 @@ class Pref_Filters extends Handler_Protected {
}
}
- private function _get_rule_name($rule) {
+ /**
+ * @param array<string, mixed>|null $rule
+ */
+ private function _get_rule_name(?array $rule = null): string {
if (!$rule) $rule = json_decode(clean($_REQUEST["rule"]), true);
$feeds = $rule["feed_id"];
@@ -446,11 +452,18 @@ class Pref_Filters extends Handler_Protected {
"<span class='field'>$filter_type</span>", "<span class='feed'>$feed</span>", isset($rule["inverse"]) ? __("(inverse)") : "") . "</span>";
}
- function printRuleName() {
+ function printRuleName(): void {
print $this->_get_rule_name(json_decode(clean($_REQUEST["rule"]), true));
}
- private function _get_action_name($action) {
+ /**
+ * @param array<string, mixed>|null $action
+ */
+ private function _get_action_name(?array $action = null): string {
+ if (!$action) {
+ return "";
+ }
+
$sth = $this->pdo->prepare("SELECT description FROM
ttrss_filter_actions WHERE id = ?");
$sth->execute([(int)$action["action_id"]]);
@@ -484,12 +497,12 @@ class Pref_Filters extends Handler_Protected {
return $title;
}
- function printActionName() {
- print $this->_get_action_name(json_decode(clean($_REQUEST["action"]), true));
+ function printActionName(): void {
+ print $this->_get_action_name(json_decode(clean($_REQUEST["action"] ?? ""), true));
}
- function editSave() {
- $filter_id = clean($_REQUEST["id"]);
+ function editSave(): void {
+ $filter_id = (int) clean($_REQUEST["id"]);
$enabled = checkbox_to_sql_bool(clean($_REQUEST["enabled"] ?? false));
$match_any_rule = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"] ?? false));
$inverse = checkbox_to_sql_bool(clean($_REQUEST["inverse"] ?? false));
@@ -510,7 +523,7 @@ class Pref_Filters extends Handler_Protected {
$this->pdo->commit();
}
- function remove() {
+ function remove(): void {
$ids = explode(",", clean($_REQUEST["ids"]));
$ids_qmarks = arr_qmarks($ids);
@@ -520,7 +533,7 @@ class Pref_Filters extends Handler_Protected {
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
}
- private function _save_rules_and_actions($filter_id) {
+ private function _save_rules_and_actions(int $filter_id): void {
$sth = $this->pdo->prepare("DELETE FROM ttrss_filters2_rules WHERE filter_id = ?");
$sth->execute([$filter_id]);
@@ -597,7 +610,7 @@ class Pref_Filters extends Handler_Protected {
}
}
- function add () {
+ function add(): void {
$enabled = checkbox_to_sql_bool(clean($_REQUEST["enabled"] ?? false));
$match_any_rule = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"] ?? false));
$title = clean($_REQUEST["title"]);
@@ -625,7 +638,7 @@ class Pref_Filters extends Handler_Protected {
$this->pdo->commit();
}
- function index() {
+ function index(): void {
if (array_key_exists("search", $_REQUEST)) {
$filter_search = clean($_REQUEST["search"]);
$_SESSION["prefs_filter_search"] = $filter_search;
@@ -691,7 +704,8 @@ class Pref_Filters extends Handler_Protected {
<?php
}
- function editrule() {
+ function editrule(): void {
+ /** @var array<int, int|string> */
$feed_ids = explode(",", clean($_REQUEST["ids"]));
print json_encode([
@@ -699,7 +713,10 @@ class Pref_Filters extends Handler_Protected {
]);
}
- private function _get_name($id) {
+ /**
+ * @return array<int, string>
+ */
+ private function _get_name(int $id): array {
$sth = $this->pdo->prepare(
"SELECT title,match_any_rule,f.inverse AS inverse,COUNT(DISTINCT r.id) AS num_rules,COUNT(DISTINCT a.id) AS num_actions
@@ -745,8 +762,9 @@ class Pref_Filters extends Handler_Protected {
return [];
}
- function join() {
- $ids = explode(",", clean($_REQUEST["ids"]));
+ function join(): void {
+ /** @var array<int, int> */
+ $ids = array_map("intval", explode(",", clean($_REQUEST["ids"])));
if (count($ids) > 1) {
$base_id = array_shift($ids);
@@ -775,7 +793,7 @@ class Pref_Filters extends Handler_Protected {
}
}
- private function _optimize($id) {
+ private function _optimize(int $id): void {
$this->pdo->beginTransaction();
@@ -830,9 +848,11 @@ class Pref_Filters extends Handler_Protected {
$this->pdo->commit();
}
- private function _feed_multi_select($id, $default_ids = [],
- $attributes = "", $include_all_feeds = true,
- $root_id = null, $nest_level = 0) {
+ /**
+ * @param array<int, int|string> $default_ids
+ */
+ private function _feed_multi_select(string $id, array $default_ids = [], string $attributes = "",
+ bool $include_all_feeds = true, ?int $root_id = null, int $nest_level = 0): string {
$pdo = Db::pdo();
diff --git a/classes/pref/labels.php b/classes/pref/labels.php
index 0eb88ea36..a50a85a66 100644
--- a/classes/pref/labels.php
+++ b/classes/pref/labels.php
@@ -1,13 +1,13 @@
<?php
class Pref_Labels extends Handler_Protected {
- function csrf_ignore($method) {
+ function csrf_ignore(string $method): bool {
$csrf_ignored = array("index", "getlabeltree");
return array_search($method, $csrf_ignored) !== false;
}
- function edit() {
+ function edit(): void {
$label = ORM::for_table('ttrss_labels2')
->where('owner_uid', $_SESSION['uid'])
->find_one($_REQUEST['id']);
@@ -17,7 +17,7 @@ class Pref_Labels extends Handler_Protected {
}
}
- function getlabeltree() {
+ function getlabeltree(): void {
$root = array();
$root['id'] = 'root';
$root['name'] = __('Labels');
@@ -48,10 +48,9 @@ class Pref_Labels extends Handler_Protected {
$fl['items'] = array($root);
print json_encode($fl);
- return;
}
- function colorset() {
+ function colorset(): void {
$kind = clean($_REQUEST["kind"]);
$ids = explode(',', clean($_REQUEST["ids"]));
$color = clean($_REQUEST["color"]);
@@ -84,7 +83,7 @@ class Pref_Labels extends Handler_Protected {
}
}
- function colorreset() {
+ function colorreset(): void {
$ids = explode(',', clean($_REQUEST["ids"]));
foreach ($ids as $id) {
@@ -101,7 +100,7 @@ class Pref_Labels extends Handler_Protected {
}
}
- function save() {
+ function save(): void {
$id = clean($_REQUEST["id"]);
$caption = clean($_REQUEST["caption"]);
@@ -148,9 +147,9 @@ class Pref_Labels extends Handler_Protected {
}
- function remove() {
-
- $ids = explode(",", clean($_REQUEST["ids"]));
+ function remove(): void {
+ /** @var array<int, int> */
+ $ids = array_map("intval", explode(",", clean($_REQUEST["ids"])));
foreach ($ids as $id) {
Labels::remove($id, $_SESSION["uid"]);
@@ -158,7 +157,7 @@ class Pref_Labels extends Handler_Protected {
}
- function add() {
+ function add(): void {
$caption = clean($_REQUEST["caption"]);
$output = clean($_REQUEST["output"] ?? false);
@@ -171,7 +170,7 @@ class Pref_Labels extends Handler_Protected {
}
}
- function index() {
+ function index(): void {
?>
<div dojoType='dijit.layout.BorderContainer' gutters='false'>
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='top'>
diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php
index c45d6d6ea..d3a5a1370 100644
--- a/classes/pref/prefs.php
+++ b/classes/pref/prefs.php
@@ -2,12 +2,21 @@
use chillerlan\QRCode;
class Pref_Prefs extends Handler_Protected {
-
+ // TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
+ /** @var array<Prefs::*, array<int, string>> */
private $pref_help = [];
+
+ /** @var array<string, array<int, string>> pref items are Prefs::*|Pref_Prefs::BLOCK_SEPARATOR (PHPStan was complaining) */
private $pref_item_map = [];
+
+ /** @var array<string, string> */
private $pref_help_bottom = [];
+
+ /** @var array<int, string> */
private $pref_blacklist = [];
+ private const BLOCK_SEPARATOR = 'BLOCK_SEPARATOR';
+
const PI_RES_ALREADY_INSTALLED = "PI_RES_ALREADY_INSTALLED";
const PI_RES_SUCCESS = "PI_RES_SUCCESS";
const PI_ERR_NO_CLASS = "PI_ERR_NO_CLASS";
@@ -17,7 +26,8 @@ class Pref_Prefs extends Handler_Protected {
const PI_ERR_PLUGIN_NOT_FOUND = "PI_ERR_PLUGIN_NOT_FOUND";
const PI_ERR_NO_WORKDIR = "PI_ERR_NO_WORKDIR";
- function csrf_ignore($method) {
+ /** @param string $method */
+ function csrf_ignore($method) : bool {
$csrf_ignored = array("index", "updateself", "otpqrcode");
return array_search($method, $csrf_ignored) !== false;
@@ -30,35 +40,35 @@ class Pref_Prefs extends Handler_Protected {
__('General') => [
Prefs::USER_LANGUAGE,
Prefs::USER_TIMEZONE,
- 'BLOCK_SEPARATOR',
+ self::BLOCK_SEPARATOR,
Prefs::USER_CSS_THEME,
- 'BLOCK_SEPARATOR',
+ self::BLOCK_SEPARATOR,
Prefs::ENABLE_API_ACCESS,
],
__('Feeds') => [
Prefs::DEFAULT_UPDATE_INTERVAL,
Prefs::FRESH_ARTICLE_MAX_AGE,
Prefs::DEFAULT_SEARCH_LANGUAGE,
- 'BLOCK_SEPARATOR',
+ self::BLOCK_SEPARATOR,
Prefs::ENABLE_FEED_CATS,
- 'BLOCK_SEPARATOR',
+ self::BLOCK_SEPARATOR,
Prefs::CONFIRM_FEED_CATCHUP,
Prefs::ON_CATCHUP_SHOW_NEXT_FEED,
- 'BLOCK_SEPARATOR',
+ self::BLOCK_SEPARATOR,
Prefs::HIDE_READ_FEEDS,
Prefs::HIDE_READ_SHOWS_SPECIAL,
],
__('Articles') => [
Prefs::PURGE_OLD_DAYS,
Prefs::PURGE_UNREAD_ARTICLES,
- 'BLOCK_SEPARATOR',
+ self::BLOCK_SEPARATOR,
Prefs::COMBINED_DISPLAY_MODE,
Prefs::CDM_EXPANDED,
Prefs::CDM_ENABLE_GRID,
- 'BLOCK_SEPARATOR',
+ self::BLOCK_SEPARATOR,
Prefs::CDM_AUTO_CATCHUP,
Prefs::VFEED_GROUP_BY_FEED,
- 'BLOCK_SEPARATOR',
+ self::BLOCK_SEPARATOR,
Prefs::SHOW_CONTENT_PREVIEW,
Prefs::STRIP_IMAGES,
],
@@ -69,12 +79,12 @@ class Pref_Prefs extends Handler_Protected {
],
__('Advanced') => [
Prefs::BLACKLISTED_TAGS,
- 'BLOCK_SEPARATOR',
+ self::BLOCK_SEPARATOR,
Prefs::LONG_DATE_FORMAT,
Prefs::SHORT_DATE_FORMAT,
- 'BLOCK_SEPARATOR',
+ self::BLOCK_SEPARATOR,
Prefs::SSL_CERT_SERIAL,
- 'BLOCK_SEPARATOR',
+ self::BLOCK_SEPARATOR,
Prefs::DISABLE_CONDITIONAL_COUNTERS,
Prefs::HEADLINES_NO_DISTINCT,
],
@@ -127,7 +137,7 @@ class Pref_Prefs extends Handler_Protected {
];
}
- function changepassword() {
+ function changepassword(): void {
if (Config::get(Config::FORBID_PASSWORD_CHANGES)) {
print "ERROR: ".format_error("Access forbidden.");
@@ -173,7 +183,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
- function saveconfig() {
+ function saveconfig(): void {
$boolean_prefs = explode(",", clean($_POST["boolean_prefs"]));
foreach ($boolean_prefs as $pref) {
@@ -223,7 +233,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
- function changePersonalData() {
+ function changePersonalData(): void {
$user = ORM::for_table('ttrss_users')->find_one($_SESSION['uid']);
$new_email = clean($_POST['email']);
@@ -264,13 +274,13 @@ class Pref_Prefs extends Handler_Protected {
print __("Your personal data has been saved.");
}
- function resetconfig() {
+ function resetconfig(): void {
Prefs::reset($_SESSION["uid"], $_SESSION["profile"]);
print "PREFS_NEED_RELOAD";
}
- private function index_auth_personal() {
+ private function index_auth_personal(): void {
$user = ORM::for_table('ttrss_users')->find_one($_SESSION['uid']);
@@ -310,7 +320,7 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
- private function index_auth_password() {
+ private function index_auth_password(): void {
if ($_SESSION["auth_module"]) {
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
} else {
@@ -385,7 +395,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
- private function index_auth_app_passwords() {
+ private function index_auth_app_passwords(): void {
print_notice("Separate passwords used for API clients. Required if you enable OTP.");
?>
@@ -409,7 +419,7 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
- private function index_auth_2fa() {
+ private function index_auth_2fa(): void {
$otp_enabled = UserHelper::is_otp_enabled($_SESSION["uid"]);
if ($_SESSION["auth_module"] == "auth_internal") {
@@ -515,7 +525,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
- function index_auth() {
+ function index_auth(): void {
?>
<div dojoType='dijit.layout.TabContainer'>
<div dojoType='dijit.layout.ContentPane' title="<?= __('Personal data') ?>">
@@ -534,35 +544,38 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
- private function index_prefs_list() {
+ private function index_prefs_list(): void {
$profile = $_SESSION["profile"] ?? null;
if ($profile) {
print_notice(__("Some preferences are only available in default profile."));
}
+ /** @var array<string, array{'type_hint': Config::T_*, 'value': bool|int|string, 'help_text': string, 'short_desc': string}> */
$prefs_available = [];
+
+ /** @var array<int, string> */
$listed_boolean_prefs = [];
- foreach (Prefs::get_all($_SESSION["uid"], $profile) as $line) {
+ foreach (Prefs::get_all($_SESSION["uid"], $profile) as $pref) {
- if (in_array($line["pref_name"], $this->pref_blacklist)) {
+ if (in_array($pref["pref_name"], $this->pref_blacklist)) {
continue;
}
- if ($profile && in_array($line["pref_name"], Prefs::_PROFILE_BLACKLIST)) {
+ if ($profile && in_array($pref["pref_name"], Prefs::_PROFILE_BLACKLIST)) {
continue;
}
- $pref_name = $line["pref_name"];
+ $pref_name = $pref["pref_name"];
$short_desc = $this->_get_short_desc($pref_name);
if (!$short_desc)
continue;
$prefs_available[$pref_name] = [
- 'type_hint' => $line['type_hint'],
- 'value' => $line['value'],
+ 'type_hint' => $pref['type_hint'],
+ 'value' => $pref['value'],
'help_text' => $this->_get_help_text($pref_name),
'short_desc' => $short_desc
];
@@ -574,12 +587,12 @@ class Pref_Prefs extends Handler_Protected {
foreach ($this->pref_item_map[$section] as $pref_name) {
- if ($pref_name == 'BLOCK_SEPARATOR' && !$profile) {
+ if ($pref_name == self::BLOCK_SEPARATOR && !$profile) {
print "<hr/>";
continue;
}
- if ($pref_name == "DEFAULT_SEARCH_LANGUAGE" && Config::get(Config::DB_TYPE) != "pgsql") {
+ if ($pref_name == Prefs::DEFAULT_SEARCH_LANGUAGE && Config::get(Config::DB_TYPE) != "pgsql") {
continue;
}
@@ -596,17 +609,17 @@ class Pref_Prefs extends Handler_Protected {
$value = $item['value'];
$type_hint = $item['type_hint'];
- if ($pref_name == "USER_LANGUAGE") {
+ if ($pref_name == Prefs::USER_LANGUAGE) {
print \Controls\select_hash($pref_name, $value, get_translations(),
["style" => 'width : 220px; margin : 0px']);
- } else if ($pref_name == "USER_TIMEZONE") {
+ } else if ($pref_name == Prefs::USER_TIMEZONE) {
$timezones = explode("\n", file_get_contents("lib/timezones.txt"));
print \Controls\select_tag($pref_name, $value, $timezones, ["dojoType" => "dijit.form.FilteringSelect"]);
- } else if ($pref_name == "BLACKLISTED_TAGS") { # TODO: other possible <textarea> prefs go here
+ } else if ($pref_name == Prefs::BLACKLISTED_TAGS) { # TODO: other possible <textarea> prefs go here
print "<div>";
@@ -618,7 +631,7 @@ class Pref_Prefs extends Handler_Protected {
print "</div>";
- } else if ($pref_name == "USER_CSS_THEME") {
+ } else if ($pref_name == Prefs::USER_CSS_THEME) {
$theme_files = array_map("basename",
array_merge(glob("themes/*.php"),
@@ -642,13 +655,13 @@ class Pref_Prefs extends Handler_Protected {
<?php
- } else if ($pref_name == "DEFAULT_UPDATE_INTERVAL") {
+ } else if ($pref_name == Prefs::DEFAULT_UPDATE_INTERVAL) {
global $update_intervals_nodefault;
print \Controls\select_hash($pref_name, $value, $update_intervals_nodefault);
- } else if ($pref_name == "DEFAULT_SEARCH_LANGUAGE") {
+ } else if ($pref_name == Prefs::DEFAULT_SEARCH_LANGUAGE) {
print \Controls\select_tag($pref_name, $value, Pref_Feeds::get_ts_languages());
@@ -656,7 +669,7 @@ class Pref_Prefs extends Handler_Protected {
array_push($listed_boolean_prefs, $pref_name);
- if ($pref_name == "PURGE_UNREAD_ARTICLES" && Config::get(Config::FORCE_ARTICLE_PURGE) != 0) {
+ if ($pref_name == Prefs::PURGE_UNREAD_ARTICLES && Config::get(Config::FORCE_ARTICLE_PURGE) != 0) {
$is_disabled = true;
$is_checked = true;
} else {
@@ -672,10 +685,10 @@ class Pref_Prefs extends Handler_Protected {
['onclick' => 'Helpers.Digest.preview()', 'style' => 'margin-left : 10px']);
}
- } else if (in_array($pref_name, ['FRESH_ARTICLE_MAX_AGE',
- 'PURGE_OLD_DAYS', 'LONG_DATE_FORMAT', 'SHORT_DATE_FORMAT'])) {
+ } else if (in_array($pref_name, [Prefs::FRESH_ARTICLE_MAX_AGE,
+ Prefs::PURGE_OLD_DAYS, Prefs::LONG_DATE_FORMAT, Prefs::SHORT_DATE_FORMAT])) {
- if ($pref_name == "PURGE_OLD_DAYS" && Config::get(Config::FORCE_ARTICLE_PURGE) != 0) {
+ if ($pref_name == Prefs::PURGE_OLD_DAYS && Config::get(Config::FORCE_ARTICLE_PURGE) != 0) {
$attributes = ["disabled" => true, "required" => true];
$value = Config::get(Config::FORCE_ARTICLE_PURGE);
} else {
@@ -687,7 +700,7 @@ class Pref_Prefs extends Handler_Protected {
else
print \Controls\input_tag($pref_name, $value, "text", $attributes);
- } else if ($pref_name == "SSL_CERT_SERIAL") {
+ } else if ($pref_name == Prefs::SSL_CERT_SERIAL) {
print \Controls\input_tag($pref_name, $value, "text", ["readonly" => true], "SSL_CERT_SERIAL");
@@ -727,7 +740,7 @@ class Pref_Prefs extends Handler_Protected {
print \Controls\hidden_tag("boolean_prefs", htmlspecialchars(join(",", $listed_boolean_prefs)));
}
- private function index_prefs() {
+ private function index_prefs(): void {
?>
<form dojoType='dijit.form.Form' id='changeSettingsForm'>
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
@@ -783,7 +796,7 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
- function getPluginsList() {
+ function getPluginsList(): void {
$system_enabled = array_map("trim", explode(",", (string)Config::get(Config::PLUGINS)));
$user_enabled = array_map("trim", explode(",", get_pref(Prefs::_ENABLED_PLUGINS)));
@@ -816,7 +829,7 @@ class Pref_Prefs extends Handler_Protected {
print json_encode(['plugins' => $rv, 'is_admin' => $_SESSION['access_level'] >= UserHelper::ACCESS_LEVEL_ADMIN]);
}
- function index_plugins() {
+ function index_plugins(): void {
?>
<form dojoType="dijit.form.Form" id="changePluginsForm">
@@ -912,7 +925,7 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
- function index() {
+ function index(): void {
?>
<div dojoType='dijit.layout.AccordionContainer' region='center'>
<div dojoType='dijit.layout.AccordionPane' title="<i class='material-icons'>person</i> <?= __('Personal data / Authentication')?>">
@@ -937,7 +950,7 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
- function _get_otp_qrcode_img() {
+ function _get_otp_qrcode_img(): ?string {
$secret = UserHelper::get_otp_secret($_SESSION["uid"]);
$login = UserHelper::get_login_by_id($_SESSION["uid"]);
@@ -949,15 +962,16 @@ class Pref_Prefs extends Handler_Protected {
return $qrcode->render($otpurl);
}
- return false;
+ return null;
}
- function otpenable() {
+ function otpenable(): void {
$password = clean($_REQUEST["password"]);
$otp_check = clean($_REQUEST["otp"]);
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
+ /** @var Auth_Internal|false $authenticator -- this is only here to make check_password() visible to static analyzer */
if ($authenticator->check_password($_SESSION["uid"], $password)) {
if (UserHelper::enable_otp($_SESSION["uid"], $otp_check)) {
print "OK";
@@ -969,9 +983,10 @@ class Pref_Prefs extends Handler_Protected {
}
}
- function otpdisable() {
+ function otpdisable(): void {
$password = clean($_REQUEST["password"]);
+ /** @var Auth_Internal|false $authenticator -- this is only here to make check_password() visible to static analyzer */
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
if ($authenticator->check_password($_SESSION["uid"], $password)) {
@@ -1008,38 +1023,42 @@ class Pref_Prefs extends Handler_Protected {
}
- function setplugins() {
+ function setplugins(): void {
$plugins = array_filter($_REQUEST["plugins"], 'clean') ?? [];
set_pref(Prefs::_ENABLED_PLUGINS, implode(",", $plugins));
}
- function _get_plugin_version(Plugin $plugin) {
+ function _get_plugin_version(Plugin $plugin): string {
$about = $plugin->about();
if (!empty($about[0])) {
return T_sprintf("v%.2f, by %s", $about[0], $about[2]);
- } else {
- $ref = new ReflectionClass(get_class($plugin));
+ }
- $plugin_dir = dirname($ref->getFileName());
+ $ref = new ReflectionClass(get_class($plugin));
- if (basename($plugin_dir) == "plugins") {
- return "";
- }
+ $plugin_dir = dirname($ref->getFileName());
- if (is_dir("$plugin_dir/.git")) {
- $ver = Config::get_version_from_git($plugin_dir);
+ if (basename($plugin_dir) == "plugins") {
+ return "";
+ }
- return $ver["status"] == 0 ? T_sprintf("v%s, by %s", $ver["version"], $about[2]) : $ver["version"];
- }
+ if (is_dir("$plugin_dir/.git")) {
+ $ver = Config::get_version_from_git($plugin_dir);
+
+ return $ver["status"] == 0 ? T_sprintf("v%s, by %s", $ver["version"], $about[2]) : $ver["version"];
}
+
+ return "";
}
- static function _get_updated_plugins() {
+ /**
+ * @return array<int, array{'plugin': string, 'rv': array{'stdout': false|string, 'stderr': false|string, 'git_status': int, 'need_update': bool}|null}>
+ */
+ static function _get_updated_plugins(): array {
$root_dir = dirname(dirname(__DIR__)); # we're in classes/pref/
$plugin_dirs = array_filter(glob("$root_dir/plugins.local/*"), "is_dir");
-
$rv = [];
foreach ($plugin_dirs as $dir) {
@@ -1057,7 +1076,10 @@ class Pref_Prefs extends Handler_Protected {
return $rv;
}
- private static function _plugin_needs_update($root_dir, $plugin_name) {
+ /**
+ * @return array{'stdout': false|string, 'stderr': false|string, 'git_status': int, 'need_update': bool}|null
+ */
+ private static function _plugin_needs_update(string $root_dir, string $plugin_name): ?array {
$plugin_dir = "$root_dir/plugins.local/" . basename($plugin_name);
$rv = null;
@@ -1086,7 +1108,10 @@ class Pref_Prefs extends Handler_Protected {
}
- private function _update_plugin($root_dir, $plugin_name) {
+ /**
+ * @return array{'stdout': false|string, 'stderr': false|string, 'git_status': int}
+ */
+ private function _update_plugin(string $root_dir, string $plugin_name): array {
$plugin_dir = "$root_dir/plugins.local/" . basename($plugin_name);
$rv = [];
@@ -1112,7 +1137,7 @@ class Pref_Prefs extends Handler_Protected {
}
// https://gist.github.com/mindplay-dk/a4aad91f5a4f1283a5e2#gistcomment-2036828
- private function _recursive_rmdir(string $dir, bool $keep_root = false) {
+ private function _recursive_rmdir(string $dir, bool $keep_root = false): bool {
// Handle bad arguments.
if (empty($dir) || !file_exists($dir)) {
return true; // No such file/dir$dir exists.
@@ -1137,7 +1162,7 @@ class Pref_Prefs extends Handler_Protected {
}
// https://stackoverflow.com/questions/7153000/get-class-name-from-file
- private function _get_class_name_from_file($file) {
+ private function _get_class_name_from_file(string $file): string {
$tokens = token_get_all(file_get_contents($file));
for ($i = 0; $i < count($tokens); $i++) {
@@ -1149,9 +1174,11 @@ class Pref_Prefs extends Handler_Protected {
}
}
}
+
+ return "";
}
- function uninstallPlugin() {
+ function uninstallPlugin(): void {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) {
$plugin_name = basename(clean($_REQUEST['plugin']));
$status = 0;
@@ -1166,7 +1193,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
- function installPlugin() {
+ function installPlugin(): void {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
$plugin_name = basename(clean($_REQUEST['plugin']));
$all_plugins = $this->_get_available_plugins();
@@ -1251,47 +1278,59 @@ class Pref_Prefs extends Handler_Protected {
}
}
- private function _get_available_plugins() {
+ /**
+ * @return array<int, array{'name': string, 'description': string, 'topics': array<int, string>, 'html_url': string, 'clone_url': string, 'last_update': string}>
+ */
+ private function _get_available_plugins(): array {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
- return json_decode(UrlHelper::fetch(['url' => 'https://tt-rss.org/plugins.json']), true);
+ $content = json_decode(UrlHelper::fetch(['url' => 'https://tt-rss.org/plugins.json']), true);
+
+ if ($content) {
+ return $content;
+ }
}
+
+ return [];
}
- function getAvailablePlugins() {
+
+ function getAvailablePlugins(): void {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) {
print json_encode($this->_get_available_plugins());
+ } else {
+ print "[]";
}
}
- function checkForPluginUpdates() {
+ function checkForPluginUpdates(): void {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::CHECK_FOR_UPDATES) && Config::get(Config::CHECK_FOR_PLUGIN_UPDATES)) {
$plugin_name = $_REQUEST["name"] ?? "";
-
$root_dir = dirname(dirname(__DIR__)); # we're in classes/pref/
- if (!empty($plugin_name)) {
- $rv = [["plugin" => $plugin_name, "rv" => self::_plugin_needs_update($root_dir, $plugin_name)]];
- } else {
- $rv = self::_get_updated_plugins();
- }
+ $rv = empty($plugin_name) ? self::_get_updated_plugins() : [
+ ["plugin" => $plugin_name, "rv" => self::_plugin_needs_update($root_dir, $plugin_name)],
+ ];
print json_encode($rv);
}
}
- function updateLocalPlugins() {
+ function updateLocalPlugins(): void {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) {
$plugins = explode(",", $_REQUEST["plugins"] ?? "");
+ if ($plugins !== false) {
+ $plugins = array_filter($plugins, 'strlen');
+ }
+
# we're in classes/pref/
$root_dir = dirname(dirname(__DIR__));
$rv = [];
- if (count($plugins) > 0) {
+ if ($plugins) {
foreach ($plugins as $plugin_name) {
array_push($rv, ["plugin" => $plugin_name, "rv" => $this->_update_plugin($root_dir, $plugin_name)]);
}
- // @phpstan-ignore-next-line
} else {
$plugin_dirs = array_filter(glob("$root_dir/plugins.local/*"), "is_dir");
@@ -1301,7 +1340,7 @@ class Pref_Prefs extends Handler_Protected {
$test = self::_plugin_needs_update($root_dir, $plugin_name);
- if (!empty($test["o"]))
+ if (!empty($test["stdout"]))
array_push($rv, ["plugin" => $plugin_name, "rv" => $this->_update_plugin($root_dir, $plugin_name)]);
}
}
@@ -1311,20 +1350,20 @@ class Pref_Prefs extends Handler_Protected {
}
}
- function clearplugindata() {
+ function clearplugindata(): void {
$name = clean($_REQUEST["name"]);
PluginHost::getInstance()->clear_data(PluginHost::getInstance()->get_plugin($name));
}
- function customizeCSS() {
+ function customizeCSS(): void {
$value = get_pref(Prefs::USER_STYLESHEET);
$value = str_replace("<br/>", "\n", $value);
print json_encode(["value" => $value]);
}
- function activateprofile() {
+ function activateprofile(): void {
$id = (int) ($_REQUEST['id'] ?? 0);
$profile = ORM::for_table('ttrss_settings_profiles')
@@ -1338,7 +1377,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
- function cloneprofile() {
+ function cloneprofile(): void {
$old_profile = $_REQUEST["old_profile"] ?? 0;
$new_title = clean($_REQUEST["new_title"]);
@@ -1367,7 +1406,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
- function remprofiles() {
+ function remprofiles(): void {
$ids = $_REQUEST["ids"] ?? [];
ORM::for_table('ttrss_settings_profiles')
@@ -1377,7 +1416,7 @@ class Pref_Prefs extends Handler_Protected {
->delete_many();
}
- function addprofile() {
+ function addprofile(): void {
$title = clean($_REQUEST["title"]);
if ($title) {
@@ -1396,7 +1435,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
- function saveprofile() {
+ function saveprofile(): void {
$id = (int)$_REQUEST["id"];
$title = clean($_REQUEST["value"]);
@@ -1413,7 +1452,7 @@ class Pref_Prefs extends Handler_Protected {
}
// TODO: this maybe needs to be unified with Public::getProfiles()
- function getProfiles() {
+ function getProfiles(): void {
$rv = [];
$profiles = ORM::for_table('ttrss_settings_profiles')
@@ -1442,21 +1481,21 @@ class Pref_Prefs extends Handler_Protected {
print json_encode($rv);
}
- private function _get_short_desc($pref_name) {
+ private function _get_short_desc(string $pref_name): string {
if (isset($this->pref_help[$pref_name][0])) {
return $this->pref_help[$pref_name][0];
}
return "";
}
- private function _get_help_text($pref_name) {
+ private function _get_help_text(string $pref_name): string {
if (isset($this->pref_help[$pref_name][1])) {
return $this->pref_help[$pref_name][1];
}
return "";
}
- private function appPasswordList() {
+ private function appPasswordList(): void {
?>
<div dojoType='fox.Toolbar'>
<div dojoType='fox.form.DropDownButton'>
@@ -1506,7 +1545,7 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
- function deleteAppPasswords() {
+ function deleteAppPasswords(): void {
$passwords = ORM::for_table('ttrss_app_passwords')
->where('owner_uid', $_SESSION['uid'])
->where_in('id', $_REQUEST['ids'] ?? [])
@@ -1515,7 +1554,7 @@ class Pref_Prefs extends Handler_Protected {
$this->appPasswordList();
}
- function generateAppPassword() {
+ function generateAppPassword(): void {
$title = clean($_REQUEST['title']);
$new_password = make_password(16);
$new_salt = UserHelper::get_salt();
@@ -1536,11 +1575,11 @@ class Pref_Prefs extends Handler_Protected {
$this->appPasswordList();
}
- function previewDigest() {
+ function previewDigest(): void {
print json_encode(Digest::prepare_headlines_digest($_SESSION["uid"], 1, 16));
}
- static function _get_ssl_certificate_id() {
+ static function _get_ssl_certificate_id(): string {
if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] ?? false) {
return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
$_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
@@ -1556,7 +1595,7 @@ class Pref_Prefs extends Handler_Protected {
return "";
}
- private function format_otp_secret($secret) {
+ private function format_otp_secret(string $secret): string {
return implode(" ", str_split($secret, 4));
}
}
diff --git a/classes/pref/system.php b/classes/pref/system.php
index 8bebcc7ce..10f196b55 100644
--- a/classes/pref/system.php
+++ b/classes/pref/system.php
@@ -2,19 +2,19 @@
class Pref_System extends Handler_Administrative {
- private $log_page_limit = 15;
+ private const LOG_PAGE_LIMIT = 15;
- function csrf_ignore($method) {
+ function csrf_ignore(string $method): bool {
$csrf_ignored = array("index");
return array_search($method, $csrf_ignored) !== false;
}
- function clearLog() {
+ function clearLog(): void {
$this->pdo->query("DELETE FROM ttrss_error_log");
}
- function sendTestEmail() {
+ function sendTestEmail(): void {
$mail_address = clean($_REQUEST["mail_address"]);
$mailer = new Mailer();
@@ -28,7 +28,7 @@ class Pref_System extends Handler_Administrative {
print json_encode(['rc' => $rc, 'error' => $mailer->error()]);
}
- function getphpinfo() {
+ function getphpinfo(): void {
ob_start();
phpinfo();
$info = ob_get_contents();
@@ -37,7 +37,7 @@ class Pref_System extends Handler_Administrative {
print preg_replace( '%^.*<body>(.*)</body>.*$%ms','$1', (string)$info);
}
- private function _log_viewer(int $page, int $severity) {
+ private function _log_viewer(int $page, int $severity): void {
$errno_values = [];
switch ($severity) {
@@ -56,8 +56,7 @@ class Pref_System extends Handler_Administrative {
$errno_filter_qpart = "true";
}
- $limit = $this->log_page_limit;
- $offset = $limit * $page;
+ $offset = self::LOG_PAGE_LIMIT * $page;
$sth = $this->pdo->prepare("SELECT
COUNT(id) AS total_pages
@@ -69,7 +68,7 @@ class Pref_System extends Handler_Administrative {
$sth->execute($errno_values);
if ($res = $sth->fetch()) {
- $total_pages = (int)($res["total_pages"] / $limit);
+ $total_pages = (int)($res["total_pages"] / self::LOG_PAGE_LIMIT);
} else {
$total_pages = 0;
}
@@ -134,7 +133,7 @@ class Pref_System extends Handler_Administrative {
$errno_filter_qpart
ORDER BY
ttrss_error_log.id DESC
- LIMIT $limit OFFSET $offset");
+ LIMIT ". self::LOG_PAGE_LIMIT ." OFFSET $offset");
$sth->execute($errno_values);
@@ -159,7 +158,7 @@ class Pref_System extends Handler_Administrative {
<?php
}
- function index() {
+ function index(): void {
$severity = (int) ($_REQUEST["severity"] ?? E_USER_WARNING);
$page = (int) ($_REQUEST["page"] ?? 0);
diff --git a/classes/pref/users.php b/classes/pref/users.php
index aeba296b2..c48619614 100644
--- a/classes/pref/users.php
+++ b/classes/pref/users.php
@@ -1,10 +1,10 @@
<?php
class Pref_Users extends Handler_Administrative {
- function csrf_ignore($method) {
+ function csrf_ignore(string $method): bool {
return $method == "index";
}
- function edit() {
+ function edit(): void {
$user = ORM::for_table('ttrss_users')
->select_expr("id,login,access_level,email,full_name,otp_enabled")
->find_one((int)$_REQUEST["id"])
@@ -20,7 +20,7 @@ class Pref_Users extends Handler_Administrative {
}
}
- function userdetails() {
+ function userdetails(): void {
$id = (int) clean($_REQUEST["id"]);
$sth = $this->pdo->prepare("SELECT login,
@@ -103,7 +103,7 @@ class Pref_Users extends Handler_Administrative {
}
- function editSave() {
+ function editSave(): void {
$id = (int)$_REQUEST['id'];
$password = clean($_REQUEST["password"]);
$user = ORM::for_table('ttrss_users')->find_one($id);
@@ -132,7 +132,7 @@ class Pref_Users extends Handler_Administrative {
}
}
- function remove() {
+ function remove(): void {
$ids = explode(",", clean($_REQUEST["ids"]));
foreach ($ids as $id) {
@@ -149,7 +149,7 @@ class Pref_Users extends Handler_Administrative {
}
}
- function add() {
+ function add(): void {
$login = clean($_REQUEST["login"]);
if (!$login) return; // no blank usernames
@@ -178,11 +178,11 @@ class Pref_Users extends Handler_Administrative {
}
}
- function resetPass() {
+ function resetPass(): void {
UserHelper::reset_password(clean($_REQUEST["id"]));
}
- function index() {
+ function index(): void {
global $access_level_names;
diff --git a/classes/prefs.php b/classes/prefs.php
index 85e7c34db..7e6033f4d 100644
--- a/classes/prefs.php
+++ b/classes/prefs.php
@@ -141,7 +141,10 @@ class Prefs {
Prefs::_PREFS_MIGRATED
];
+ /** @var Prefs|null */
private static $instance;
+
+ /** @var array<string, bool|int|string> */
private $cache = [];
/** @var PDO */
@@ -154,10 +157,13 @@ class Prefs {
return self::$instance;
}
- static function is_valid(string $pref_name) {
+ static function is_valid(string $pref_name): bool {
return isset(self::_DEFAULTS[$pref_name]);
}
+ /**
+ * @return bool|int|null|string
+ */
static function get_default(string $pref_name) {
if (self::is_valid($pref_name))
return self::_DEFAULTS[$pref_name][0];
@@ -181,10 +187,16 @@ class Prefs {
//
}
+ /**
+ * @return array<int, array<string, bool|int|null|string>>
+ */
static function get_all(int $owner_uid, int $profile_id = null) {
return self::get_instance()->_get_all($owner_uid, $profile_id);
}
+ /**
+ * @return array<int, array<string, bool|int|null|string>>
+ */
private function _get_all(int $owner_uid, int $profile_id = null) {
$rv = [];
@@ -205,7 +217,7 @@ class Prefs {
return $rv;
}
- private function cache_all(int $owner_uid, $profile_id = null) {
+ private function cache_all(int $owner_uid, ?int $profile_id): void {
if (!$profile_id) $profile_id = null;
// fill cache with defaults
@@ -232,11 +244,17 @@ class Prefs {
}
}
- static function get(string $pref_name, int $owner_uid, int $profile_id = null) {
+ /**
+ * @return bool|int|null|string
+ */
+ static function get(string $pref_name, int $owner_uid, ?int $profile_id) {
return self::get_instance()->_get($pref_name, $owner_uid, $profile_id);
}
- private function _get(string $pref_name, int $owner_uid, int $profile_id = null) {
+ /**
+ * @return bool|int|null|string
+ */
+ private function _get(string $pref_name, int $owner_uid, ?int $profile_id) {
if (isset(self::_DEFAULTS[$pref_name])) {
if (!$profile_id || in_array($pref_name, self::_PROFILE_BLACKLIST)) $profile_id = null;
@@ -274,12 +292,15 @@ class Prefs {
return null;
}
- private function _is_cached(string $pref_name, int $owner_uid, int $profile_id = null) {
+ private function _is_cached(string $pref_name, int $owner_uid, ?int $profile_id): bool {
$cache_key = sprintf("%d/%d/%s", $owner_uid, $profile_id, $pref_name);
return isset($this->cache[$cache_key]);
}
- private function _get_cache(string $pref_name, int $owner_uid, int $profile_id = null) {
+ /**
+ * @return bool|int|null|string
+ */
+ private function _get_cache(string $pref_name, int $owner_uid, ?int $profile_id) {
$cache_key = sprintf("%d/%d/%s", $owner_uid, $profile_id, $pref_name);
if (isset($this->cache[$cache_key]))
@@ -288,17 +309,23 @@ class Prefs {
return null;
}
- private function _set_cache(string $pref_name, $value, int $owner_uid, int $profile_id = null) {
+ /**
+ * @param bool|int|string $value
+ */
+ private function _set_cache(string $pref_name, $value, int $owner_uid, ?int $profile_id): void {
$cache_key = sprintf("%d/%d/%s", $owner_uid, $profile_id, $pref_name);
$this->cache[$cache_key] = $value;
}
- static function set(string $pref_name, $value, int $owner_uid, int $profile_id = null, bool $strip_tags = true) {
+ /**
+ * @param bool|int|string $value
+ */
+ static function set(string $pref_name, $value, int $owner_uid, ?int $profile_id, bool $strip_tags = true): bool {
return self::get_instance()->_set($pref_name, $value, $owner_uid, $profile_id);
}
- private function _delete(string $pref_name, int $owner_uid, int $profile_id = null) {
+ private function _delete(string $pref_name, int $owner_uid, ?int $profile_id): bool {
$sth = $this->pdo->prepare("DELETE FROM ttrss_user_prefs2
WHERE pref_name = :name AND owner_uid = :uid AND
(profile = :profile OR (:profile IS NULL AND profile IS NULL))");
@@ -306,7 +333,10 @@ class Prefs {
return $sth->execute(["uid" => $owner_uid, "profile" => $profile_id, "name" => $pref_name ]);
}
- private function _set(string $pref_name, $value, int $owner_uid, int $profile_id = null, bool $strip_tags = true) {
+ /**
+ * @param bool|int|string $value
+ */
+ private function _set(string $pref_name, $value, int $owner_uid, ?int $profile_id, bool $strip_tags = true): bool {
if (!$profile_id) $profile_id = null;
if ($profile_id && in_array($pref_name, self::_PROFILE_BLACKLIST))
@@ -359,7 +389,7 @@ class Prefs {
return false;
}
- function migrate(int $owner_uid, int $profile_id = null) {
+ function migrate(int $owner_uid, ?int $profile_id): void {
if (get_schema_version() < 141)
return;
@@ -401,7 +431,7 @@ class Prefs {
}
}
- static function reset(int $owner_uid, int $profile_id = null) {
+ static function reset(int $owner_uid, ?int $profile_id): void {
if (!$profile_id) $profile_id = null;
$sth = Db::pdo()->prepare("DELETE FROM ttrss_user_prefs2
diff --git a/classes/rpc.php b/classes/rpc.php
index 0432ed2d3..75d008b8b 100755
--- a/classes/rpc.php
+++ b/classes/rpc.php
@@ -1,13 +1,16 @@
<?php
class RPC extends Handler_Protected {
- /*function csrf_ignore($method) {
+ /*function csrf_ignore(string $method): bool {
$csrf_ignored = array("completelabels");
return array_search($method, $csrf_ignored) !== false;
}*/
- private function _translations_as_array() {
+ /**
+ * @return array<string, string>
+ */
+ private function _translations_as_array(): array {
global $text_domains;
@@ -37,7 +40,7 @@ class RPC extends Handler_Protected {
}
- function togglepref() {
+ function togglepref(): void {
$key = clean($_REQUEST["key"]);
set_pref($key, !get_pref($key));
$value = get_pref($key);
@@ -45,7 +48,7 @@ class RPC extends Handler_Protected {
print json_encode(array("param" =>$key, "value" => $value));
}
- function setpref() {
+ function setpref(): void {
// set_pref escapes input, so no need to double escape it here
$key = clean($_REQUEST['key']);
$value = $_REQUEST['value'];
@@ -55,7 +58,7 @@ class RPC extends Handler_Protected {
print json_encode(array("param" =>$key, "value" => $value));
}
- function mark() {
+ function mark(): void {
$mark = clean($_REQUEST["mark"]);
$id = clean($_REQUEST["id"]);
@@ -68,7 +71,7 @@ class RPC extends Handler_Protected {
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
- function delete() {
+ function delete(): void {
$ids = explode(",", clean($_REQUEST["ids"]));
$ids_qmarks = arr_qmarks($ids);
@@ -81,7 +84,7 @@ class RPC extends Handler_Protected {
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
- function publ() {
+ function publ(): void {
$pub = clean($_REQUEST["pub"]);
$id = clean($_REQUEST["id"]);
@@ -94,7 +97,7 @@ class RPC extends Handler_Protected {
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
- function getRuntimeInfo() {
+ function getRuntimeInfo(): void {
$reply = [
'runtime-info' => $this->_make_runtime_info()
];
@@ -102,7 +105,7 @@ class RPC extends Handler_Protected {
print json_encode($reply);
}
- function getAllCounters() {
+ function getAllCounters(): void {
@$seq = (int) $_REQUEST['seq'];
$feed_id_count = (int)$_REQUEST["feed_id_count"];
@@ -133,7 +136,7 @@ class RPC extends Handler_Protected {
}
/* GET["cmode"] = 0 - mark as read, 1 - as unread, 2 - toggle */
- function catchupSelected() {
+ function catchupSelected(): void {
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
$cmode = (int)clean($_REQUEST["cmode"]);
@@ -145,7 +148,7 @@ class RPC extends Handler_Protected {
"feeds" => Article::_feeds_of($ids)]);
}
- function markSelected() {
+ function markSelected(): void {
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
$cmode = (int)clean($_REQUEST["cmode"]);
@@ -157,7 +160,7 @@ class RPC extends Handler_Protected {
"feeds" => Article::_feeds_of($ids)]);
}
- function publishSelected() {
+ function publishSelected(): void {
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
$cmode = (int)clean($_REQUEST["cmode"]);
@@ -169,7 +172,7 @@ class RPC extends Handler_Protected {
"feeds" => Article::_feeds_of($ids)]);
}
- function sanityCheck() {
+ function sanityCheck(): void {
$_SESSION["hasSandbox"] = clean($_REQUEST["hasSandbox"]) === "true";
$_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]);
@@ -220,14 +223,14 @@ class RPC extends Handler_Protected {
print "</ul>";
}*/
- function catchupFeed() {
+ function catchupFeed(): void {
$feed_id = clean($_REQUEST['feed_id']);
$is_cat = clean($_REQUEST['is_cat']) == "true";
$mode = clean($_REQUEST['mode'] ?? '');
$search_query = clean($_REQUEST['search_query']);
$search_lang = clean($_REQUEST['search_lang']);
- Feeds::_catchup($feed_id, $is_cat, false, $mode, [$search_query, $search_lang]);
+ Feeds::_catchup($feed_id, $is_cat, null, $mode, [$search_query, $search_lang]);
// return counters here synchronously so that frontend can figure out next unread feed properly
print json_encode(['counters' => Counters::get_all()]);
@@ -235,7 +238,7 @@ class RPC extends Handler_Protected {
//print json_encode(array("message" => "UPDATE_COUNTERS"));
}
- function setWidescreen() {
+ function setWidescreen(): void {
$wide = (int) clean($_REQUEST["wide"]);
set_pref(Prefs::WIDESCREEN_MODE, $wide);
@@ -243,7 +246,7 @@ class RPC extends Handler_Protected {
print json_encode(["wide" => $wide]);
}
- static function updaterandomfeed_real() {
+ static function updaterandomfeed_real(): void {
$default_interval = (int) Prefs::get_default(Prefs::DEFAULT_UPDATE_INTERVAL);
@@ -336,19 +339,22 @@ class RPC extends Handler_Protected {
}
- function updaterandomfeed() {
+ function updaterandomfeed(): void {
self::updaterandomfeed_real();
}
- private function markArticlesById($ids, $cmode) {
+ /**
+ * @param array<int, int> $ids
+ */
+ private function markArticlesById(array $ids, int $cmode): void {
$ids_qmarks = arr_qmarks($ids);
- if ($cmode == 0) {
+ if ($cmode == Article::CATCHUP_MODE_MARK_AS_READ) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
marked = false, last_marked = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
- } else if ($cmode == 1) {
+ } else if ($cmode == Article::CATCHUP_MODE_MARK_AS_UNREAD) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
marked = true, last_marked = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
@@ -361,15 +367,18 @@ class RPC extends Handler_Protected {
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
}
- private function publishArticlesById($ids, $cmode) {
+ /**
+ * @param array<int, int> $ids
+ */
+ private function publishArticlesById(array $ids, int $cmode): void {
$ids_qmarks = arr_qmarks($ids);
- if ($cmode == 0) {
+ if ($cmode == Article::CATCHUP_MODE_MARK_AS_READ) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
published = false, last_published = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
- } else if ($cmode == 1) {
+ } else if ($cmode == Article::CATCHUP_MODE_MARK_AS_UNREAD) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
published = true, last_published = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
@@ -382,7 +391,7 @@ class RPC extends Handler_Protected {
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
}
- function log() {
+ function log(): void {
$msg = clean($_REQUEST['msg'] ?? "");
$file = basename(clean($_REQUEST['file'] ?? ""));
$line = (int) clean($_REQUEST['line'] ?? 0);
@@ -396,7 +405,7 @@ class RPC extends Handler_Protected {
}
}
- function checkforupdates() {
+ function checkforupdates(): void {
$rv = ["changeset" => [], "plugins" => []];
$version = Config::get_version(false);
@@ -425,7 +434,10 @@ class RPC extends Handler_Protected {
print json_encode($rv);
}
- private function _make_init_params() {
+ /**
+ * @return array<string, mixed>
+ */
+ private function _make_init_params(): array {
$params = array();
foreach ([Prefs::ON_CATCHUP_SHOW_NEXT_FEED, Prefs::HIDE_READ_FEEDS,
@@ -481,7 +493,7 @@ class RPC extends Handler_Protected {
return $params;
}
- private function image_to_base64($filename) {
+ private function image_to_base64(string $filename): string {
if (file_exists($filename)) {
$ext = pathinfo($filename, PATHINFO_EXTENSION);
@@ -493,7 +505,10 @@ class RPC extends Handler_Protected {
}
}
- static function _make_runtime_info() {
+ /**
+ * @return array<string, mixed>
+ */
+ static function _make_runtime_info(): array {
$data = array();
$pdo = Db::pdo();
@@ -562,7 +577,10 @@ class RPC extends Handler_Protected {
return $data;
}
- static function get_hotkeys_info() {
+ /**
+ * @return array<string, array<string, string>>
+ */
+ static function get_hotkeys_info(): array {
$hotkeys = array(
__("Navigation") => array(
"next_feed" => __("Open next feed"),
@@ -642,8 +660,12 @@ class RPC extends Handler_Protected {
return $hotkeys;
}
- // {3} - 3 panel mode only
- // {C} - combined mode only
+ /**
+ * {3} - 3 panel mode only
+ * {C} - combined mode only
+ *
+ * @return array{0: array<int, string>, 1: array<string, string>} $prefixes, $hotkeys
+ */
static function get_hotkeys_map() {
$hotkeys = array(
"k" => "next_feed",
@@ -728,7 +750,7 @@ class RPC extends Handler_Protected {
return array($prefixes, $hotkeys);
}
- function hotkeyHelp() {
+ function hotkeyHelp(): void {
$info = self::get_hotkeys_info();
$imap = self::get_hotkeys_map();
$omap = array();
diff --git a/classes/rssutils.php b/classes/rssutils.php
index 927a6c251..b886a060c 100755
--- a/classes/rssutils.php
+++ b/classes/rssutils.php
@@ -1,6 +1,9 @@
<?php
class RSSUtils {
- static function calculate_article_hash($article, $pluginhost) {
+ /**
+ * @param array<string, mixed> $article
+ */
+ static function calculate_article_hash(array $article, PluginHost $pluginhost): string {
$tmp = "";
$ignored_fields = [ "feed", "guid", "guid_hashed", "owner_uid", "force_catchup" ];
@@ -21,16 +24,16 @@ class RSSUtils {
}
// Strips utf8mb4 characters (i.e. emoji) for mysql
- static function strip_utf8mb4(string $str) {
+ static function strip_utf8mb4(string $str): string {
return preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $str);
}
- static function cleanup_feed_browser() {
+ static function cleanup_feed_browser(): void {
$pdo = Db::pdo();
$pdo->query("DELETE FROM ttrss_feedbrowser_cache");
}
- static function cleanup_feed_icons() {
+ static function cleanup_feed_icons(): void {
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ?");
@@ -52,7 +55,10 @@ class RSSUtils {
}
}
- static function update_daemon_common(int $limit = 0, array $options = []) {
+ /**
+ * @param array<string, false|string> $options
+ */
+ static function update_daemon_common(int $limit = 0, array $options = []): int {
if (!$limit) $limit = Config::get(Config::DAEMON_FEED_LIMIT);
if (Config::get_schema_version() != Config::SCHEMA_VERSION) {
@@ -272,7 +278,7 @@ class RSSUtils {
}
/** this is used when subscribing */
- static function update_basic_info(int $feed_id) {
+ static function update_basic_info(int $feed_id): void {
$feed = ORM::for_table('ttrss_feeds')
->select_many('id', 'owner_uid', 'feed_url', 'auth_pass', 'auth_login', 'title', 'site_url')
->find_one($feed_id);
@@ -681,7 +687,7 @@ class RSSUtils {
}
$entry_comments = mb_substr(strip_tags($item->get_comments_url()), 0, 245);
- $num_comments = (int) $item->get_comments_count();
+ $num_comments = $item->get_comments_count();
$entry_author = strip_tags($item->get_author());
$entry_guid = mb_substr($entry_guid, 0, 245);
@@ -728,6 +734,7 @@ class RSSUtils {
},
$e, $feed);
+ // TODO: Just use FeedEnclosure (and modify it to cover whatever justified this)?
$e_item = array(
rewrite_relative_url($site_url, $e->link),
$e->type, $e->length, $e->title, $e->width, $e->height);
@@ -1265,8 +1272,14 @@ class RSSUtils {
return true;
}
- /* TODO: move to DiskCache? */
- static function cache_enclosures($enclosures, $site_url) {
+ /**
+ * TODO: move to DiskCache?
+ *
+ * @param array<int, array<string>> $enclosures An array of "enclosure arrays" [string $link, string $type, string $length, string, $title, string $width, string $height]
+ * @see RSSUtils::update_rss_feed()
+ * @see FeedEnclosure
+ */
+ static function cache_enclosures(array $enclosures, string $site_url): void {
$cache = new DiskCache("images");
if ($cache->is_writable()) {
@@ -1298,7 +1311,7 @@ class RSSUtils {
}
/* TODO: move to DiskCache? */
- static function cache_media_url($cache, $url, $site_url) {
+ static function cache_media_url(DiskCache $cache, string $url, string $site_url): void {
$url = rewrite_relative_url($site_url, $url);
$local_filename = sha1($url);
@@ -1322,7 +1335,7 @@ class RSSUtils {
}
/* TODO: move to DiskCache? */
- static function cache_media($html, $site_url) {
+ static function cache_media(string $html, string $site_url): void {
$cache = new DiskCache("images");
if ($html && $cache->is_writable()) {
@@ -1351,7 +1364,7 @@ class RSSUtils {
}
}
- static function expire_error_log() {
+ static function expire_error_log(): void {
Debug::log("Removing old error log entries...");
$pdo = Db::pdo();
@@ -1365,14 +1378,16 @@ class RSSUtils {
}
}
- // deprecated; table not used
- static function expire_feed_archive() {
+ /**
+ * @deprecated table not used
+ */
+ static function expire_feed_archive(): void {
$pdo = Db::pdo();
$pdo->query("DELETE FROM ttrss_archived_feeds");
}
- static function expire_lock_files() {
+ static function expire_lock_files(): void {
Debug::log("Removing old lock files...", Debug::LOG_VERBOSE);
$num_deleted = 0;
@@ -1413,7 +1428,15 @@ class RSSUtils {
return $params;
} */
- static function get_article_filters($filters, $title, $content, $link, $author, $tags, &$matched_rules = false, &$matched_filters = false) {
+ /**
+ * @param array<int, array<string, mixed>> $filters
+ * @param array<int, string> $tags
+ * @param array<int, array<string, mixed>>|null $matched_rules
+ * @param array<int, array<string, mixed>>|null $matched_filters
+ *
+ * @return array<int, array<string, string>> An array of filter action arrays with keys "type" and "param"
+ */
+ static function get_article_filters(array $filters, string $title, string $content, string $link, string $author, array $tags, array &$matched_rules = null, array &$matched_filters = null): array {
$matches = array();
foreach ($filters as $filter) {
@@ -1497,16 +1520,26 @@ class RSSUtils {
return $matches;
}
- static function find_article_filter($filters, $filter_name) {
+ /**
+ * @param array<int, array<string, string>> $filters An array of filter action arrays with keys "type" and "param"
+ *
+ * @return array<string, string>|null A filter action array with keys "type" and "param"
+ */
+ static function find_article_filter(array $filters, string $filter_name): ?array {
foreach ($filters as $f) {
if ($f["type"] == $filter_name) {
return $f;
};
}
- return false;
+ return null;
}
- static function find_article_filters($filters, $filter_name) {
+ /**
+ * @param array<int, array<string, string>> $filters An array of filter action arrays with keys "type" and "param"
+ *
+ * @return array<int, array<string, string>> An array of filter action arrays with keys "type" and "param"
+ */
+ static function find_article_filters(array $filters, string $filter_name): array {
$results = array();
foreach ($filters as $f) {
@@ -1517,7 +1550,10 @@ class RSSUtils {
return $results;
}
- static function calculate_article_score($filters) {
+ /**
+ * @param array<int, array<string, string>> $filters An array of filter action arrays with keys "type" and "param"
+ */
+ static function calculate_article_score(array $filters): int {
$score = 0;
foreach ($filters as $f) {
@@ -1528,7 +1564,12 @@ class RSSUtils {
return $score;
}
- static function labels_contains_caption($labels, $caption) {
+ /**
+ * @param array<int, array<int, int|string>> $labels An array of label arrays like [int $feed_id, string $caption, string $fg_color, string $bg_color]
+ *
+ * @see Article::_get_labels()
+ */
+ static function labels_contains_caption(array $labels, string $caption): bool {
foreach ($labels as $label) {
if ($label[1] == $caption) {
return true;
@@ -1538,7 +1579,11 @@ class RSSUtils {
return false;
}
- static function assign_article_to_label_filters($id, $filters, $owner_uid, $article_labels) {
+ /**
+ * @param array<int, array<string, string>> $filters An array of filter action arrays with keys "type" and "param"
+ * @param array<int, array<int, int|string>> $article_labels An array of label arrays like [int $feed_id, string $caption, string $fg_color, string $bg_color]
+ */
+ static function assign_article_to_label_filters(int $id, array $filters, int $owner_uid, $article_labels): void {
foreach ($filters as $f) {
if ($f["type"] == "label") {
if (!self::labels_contains_caption($article_labels, $f["param"])) {
@@ -1548,20 +1593,20 @@ class RSSUtils {
}
}
- static function make_guid_from_title($title) {
+ static function make_guid_from_title(string $title): ?string {
return preg_replace("/[ \"\',.:;]/", "-",
mb_strtolower(strip_tags($title), 'utf-8'));
}
/* counter cache is no longer used, if called truncate leftover data */
- static function cleanup_counters_cache() {
+ static function cleanup_counters_cache(): void {
$pdo = Db::pdo();
$pdo->query("DELETE FROM ttrss_counters_cache");
$pdo->query("DELETE FROM ttrss_cat_counters_cache");
}
- static function disable_failed_feeds() {
+ static function disable_failed_feeds(): void {
if (Config::get(Config::DAEMON_UNSUCCESSFUL_DAYS_LIMIT) > 0) {
$pdo = Db::pdo();
@@ -1599,7 +1644,7 @@ class RSSUtils {
}
}
- static function housekeeping_user($owner_uid) {
+ static function housekeeping_user(int $owner_uid): void {
$tmph = new PluginHost();
UserHelper::load_user_plugins($owner_uid, $tmph);
@@ -1607,7 +1652,7 @@ class RSSUtils {
$tmph->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
}
- static function housekeeping_common() {
+ static function housekeeping_common(): void {
DiskCache::expire();
self::expire_lock_files();
@@ -1623,6 +1668,9 @@ class RSSUtils {
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
}
+ /**
+ * @return false|string
+ */
static function update_favicon(string $site_url, int $feed) {
$icon_file = Config::get(Config::ICONS_DIR) . "/$feed.ico";
@@ -1687,11 +1735,14 @@ class RSSUtils {
return $icon_file;
}
- static function is_gzipped($feed_data) {
+ static function is_gzipped(string $feed_data): bool {
return strpos(substr($feed_data, 0, 3),
"\x1f" . "\x8b" . "\x08", 0) === 0;
}
+ /**
+ * @return array<int, array<string, mixed>> An array of filter arrays with keys "id", "match_any_rule", "inverse", "rules", and "actions"
+ */
static function load_filters(int $feed_id, int $owner_uid) {
$filters = array();
@@ -1809,7 +1860,7 @@ class RSSUtils {
*
* @param string $url A feed or page URL
* @access public
- * @return mixed The favicon URL, or false if none was found.
+ * @return false|string The favicon URL string, or false if none was found.
*/
static function get_favicon_url(string $url) {
@@ -1843,8 +1894,12 @@ class RSSUtils {
return $favicon_url;
}
- // https://community.tt-rss.org/t/problem-with-img-srcset/3519
- static function decode_srcset($srcset) {
+ /**
+ * @see https://community.tt-rss.org/t/problem-with-img-srcset/3519
+ *
+ * @return array<int, array<string, string>> An array of srcset subitem arrays with keys "url" and "size"
+ */
+ static function decode_srcset(string $srcset): array {
$matches = [];
preg_match_all(
@@ -1862,7 +1917,10 @@ class RSSUtils {
return $matches;
}
- static function encode_srcset($matches) {
+ /**
+ * @param array<int, array<string, string>> $matches An array of srcset subitem arrays with keys "url" and "size"
+ */
+ static function encode_srcset(array $matches): string {
$tokens = [];
foreach ($matches as $m) {
@@ -1872,7 +1930,7 @@ class RSSUtils {
return implode(",", $tokens);
}
- static function function_enabled($func) {
+ static function function_enabled(string $func): bool {
return !in_array($func,
explode(',', str_replace(" ", "", ini_get('disable_functions'))));
}
diff --git a/classes/sanitizer.php b/classes/sanitizer.php
index 3f6e9504e..e0db811c6 100644
--- a/classes/sanitizer.php
+++ b/classes/sanitizer.php
@@ -1,6 +1,10 @@
<?php
class Sanitizer {
- private static function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
+ /**
+ * @param array<int, string> $allowed_elements
+ * @param array<int, string> $disallowed_attributes
+ */
+ private static function strip_harmful_tags(DOMDocument $doc, array $allowed_elements, $disallowed_attributes): DOMDocument {
$xpath = new DOMXPath($doc);
$entries = $xpath->query('//*');
@@ -40,7 +44,7 @@ class Sanitizer {
return $doc;
}
- public static function iframe_whitelisted($entry) {
+ public static function iframe_whitelisted(DOMElement $entry): bool {
$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
if (!empty($src))
@@ -49,11 +53,16 @@ class Sanitizer {
return false;
}
- private static function is_prefix_https() {
+ private static function is_prefix_https(): bool {
return parse_url(Config::get(Config::SELF_URL_PATH), PHP_URL_SCHEME) == 'https';
}
- public static function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
+ /**
+ * @param array<int, string>|null $highlight_words Words to highlight in the HTML output.
+ *
+ * @return false|string The HTML, or false if an error occurred.
+ */
+ public static function sanitize(string $str, bool $force_remove_images = false, int $owner = null, string $site_url = null, array $highlight_words = null, int $article_id = null) {
if (!$owner && isset($_SESSION["uid"]))
$owner = $_SESSION["uid"];
@@ -183,7 +192,7 @@ class Sanitizer {
$div->appendChild($entry);
}
- if ($highlight_words && is_array($highlight_words)) {
+ if (is_array($highlight_words)) {
foreach ($highlight_words as $word) {
// http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
diff --git a/classes/timehelper.php b/classes/timehelper.php
index 4317f343f..453ee0cee 100644
--- a/classes/timehelper.php
+++ b/classes/timehelper.php
@@ -1,7 +1,7 @@
<?php
class TimeHelper {
- static function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
+ static function smart_date_time(int $timestamp, int $tz_offset = 0, int $owner_uid = null, bool $eta_min = false): string {
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
@@ -21,8 +21,8 @@ class TimeHelper {
}
}
- static function make_local_datetime($timestamp, $long, $owner_uid = false,
- $no_smart_dt = false, $eta_min = false) {
+ static function make_local_datetime(?string $timestamp, bool $long, int $owner_uid = null,
+ bool $no_smart_dt = false, bool $eta_min = false): string {
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
if (!$timestamp) $timestamp = '1970-01-01 0:00';
@@ -67,7 +67,7 @@ class TimeHelper {
}
}
- static function convert_timestamp($timestamp, $source_tz, $dest_tz) {
+ static function convert_timestamp(int $timestamp, string $source_tz, string $dest_tz): int {
try {
$source_tz = new DateTimeZone($source_tz);
diff --git a/classes/urlhelper.php b/classes/urlhelper.php
index 4d11b5a4d..91e1d4822 100644
--- a/classes/urlhelper.php
+++ b/classes/urlhelper.php
@@ -6,16 +6,35 @@ class UrlHelper {
"tel"
];
+ // TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
+ /** @var string */
static $fetch_last_error;
+
+ /** @var int */
static $fetch_last_error_code;
+
+ /** @var string */
static $fetch_last_error_content;
+
+ /** @var string */
static $fetch_last_content_type;
+
+ /** @var string */
static $fetch_last_modified;
+
+ /** @var string */
static $fetch_effective_url;
+
+ /** @var string */
static $fetch_effective_ip_addr;
+
+ /** @var bool */
static $fetch_curl_used;
- static function build_url($parts) {
+ /**
+ * @param array<string, string|int> $parts
+ */
+ static function build_url(array $parts): string {
$tmp = $parts['scheme'] . "://" . $parts['host'];
if (isset($parts['path'])) $tmp .= $parts['path'];
@@ -34,12 +53,22 @@ class UrlHelper {
* @param string $owner_element Owner element tag name (i.e. "a") (optional)
* @param string $owner_attribute Owner attribute (i.e. "href") (optional)
*
- * @return string Absolute URL
+ * @return false|string Absolute URL or false on failure (either during URL parsing or validation)
*/
public static function rewrite_relative($base_url, $rel_url, string $owner_element = "", string $owner_attribute = "") {
-
$rel_parts = parse_url($rel_url);
+ /**
+ * If parse_url failed to parse $rel_url return false to match the current "invalid thing" behavior
+ * of UrlHelper::validate().
+ *
+ * TODO: There are many places where a string return value is assumed. We should either update those
+ * to account for the possibility of failure, or look into updating this function's return values.
+ */
+ if ($rel_parts === false) {
+ return false;
+ }
+
if (!empty($rel_parts['host']) && !empty($rel_parts['scheme'])) {
return self::validate($rel_url);
@@ -80,8 +109,10 @@ class UrlHelper {
}
}
- // extended filtering involves validation for safe ports and loopback
- static function validate($url, $extended_filtering = false) {
+ /** extended filtering involves validation for safe ports and loopback
+ * @return false|string false if something went wrong, otherwise the URL string
+ */
+ static function validate(string $url, bool $extended_filtering = false) {
$url = clean($url);
@@ -107,6 +138,11 @@ class UrlHelper {
} else {
$tokens['host'] = idn_to_ascii($tokens['host']);
}
+
+ // if `idn_to_ascii` failed
+ if ($tokens['host'] === false) {
+ return false;
+ }
}
}
@@ -138,7 +174,10 @@ class UrlHelper {
return $url;
}
- static function resolve_redirects($url, $timeout, $nest = 0) {
+ /**
+ * @return false|string
+ */
+ static function resolve_redirects(string $url, int $timeout, int $nest = 0) {
// too many redirects
if ($nest > 10)
@@ -189,12 +228,16 @@ class UrlHelper {
return false;
}
- // TODO: max_size currently only works for CURL transfers
+ /**
+ * @param array<string, bool|int|string>|string $options
+ * @return false|string false if something went wrong, otherwise string contents
+ */
+ // TODO: max_size currently only works for CURL transfers
// TODO: multiple-argument way is deprecated, first parameter is a hash now
public static function fetch($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
- self::$fetch_last_error = false;
+ self::$fetch_last_error = "";
self::$fetch_last_error_code = -1;
self::$fetch_last_error_content = "";
self::$fetch_last_content_type = "";
@@ -510,7 +553,10 @@ class UrlHelper {
}
}
- public static function url_to_youtube_vid($url) {
+ /**
+ * @return false|string false if the provided URL didn't match expected patterns, otherwise the video ID string
+ */
+ public static function url_to_youtube_vid(string $url) {
$url = str_replace("youtube.com", "youtube-nocookie.com", $url);
$regexps = [
diff --git a/classes/userhelper.php b/classes/userhelper.php
index ea714b76b..90d073d55 100644
--- a/classes/userhelper.php
+++ b/classes/userhelper.php
@@ -32,7 +32,7 @@ class UserHelper {
/** has administrator permissions */
const ACCESS_LEVEL_ADMIN = 10;
- static function authenticate(string $login = null, string $password = null, bool $check_only = false, string $service = null) {
+ static function authenticate(string $login = null, string $password = null, bool $check_only = false, string $service = null): bool {
if (!Config::get(Config::SINGLE_USER_MODE)) {
$user_id = false;
$auth_module = false;
@@ -99,7 +99,7 @@ class UserHelper {
}
}
- static function load_user_plugins(int $owner_uid, PluginHost $pluginhost = null) {
+ static function load_user_plugins(int $owner_uid, PluginHost $pluginhost = null): void {
if (!$pluginhost) $pluginhost = PluginHost::getInstance();
@@ -114,7 +114,7 @@ class UserHelper {
}
}
- static function login_sequence() {
+ static function login_sequence(): void {
$pdo = Db::pdo();
if (Config::get(Config::SINGLE_USER_MODE)) {
@@ -159,7 +159,7 @@ class UserHelper {
}
}
- static function print_user_stylesheet() {
+ static function print_user_stylesheet(): void {
$value = get_pref(Prefs::USER_STYLESHEET);
if ($value) {
@@ -170,7 +170,7 @@ class UserHelper {
}
- static function get_user_ip() {
+ static function get_user_ip(): ?string {
foreach (["HTTP_X_REAL_IP", "REMOTE_ADDR"] as $hdr) {
if (isset($_SERVER[$hdr]))
return $_SERVER[$hdr];
@@ -179,7 +179,7 @@ class UserHelper {
return null;
}
- static function get_login_by_id(int $id) {
+ static function get_login_by_id(int $id): ?string {
$user = ORM::for_table('ttrss_users')
->find_one($id);
@@ -189,7 +189,7 @@ class UserHelper {
return null;
}
- static function find_user_by_login(string $login) {
+ static function find_user_by_login(string $login): ?int {
$user = ORM::for_table('ttrss_users')
->where('login', $login)
->find_one();
@@ -200,7 +200,7 @@ class UserHelper {
return null;
}
- static function logout() {
+ static function logout(): void {
if (session_status() === PHP_SESSION_ACTIVE)
session_destroy();
@@ -211,11 +211,11 @@ class UserHelper {
session_commit();
}
- static function get_salt() {
+ static function get_salt(): string {
return substr(bin2hex(get_random_bytes(125)), 0, 250);
}
- static function reset_password($uid, $format_output = false, $new_password = "") {
+ static function reset_password(int $uid, bool $format_output = false, string $new_password = ""): void {
$user = ORM::for_table('ttrss_users')->find_one($uid);
$message = "";
@@ -298,7 +298,7 @@ class UserHelper {
}
}
- static function get_otp_secret(int $owner_uid, bool $show_if_enabled = false) {
+ static function get_otp_secret(int $owner_uid, bool $show_if_enabled = false): ?string {
$user = ORM::for_table('ttrss_users')->find_one($owner_uid);
if ($user) {
@@ -333,7 +333,9 @@ class UserHelper {
return null;
}
- static function is_default_password() {
+ static function is_default_password(): bool {
+
+ /** @var Auth_Internal|false $authenticator -- this is only here to make check_password() visible to static analyzer */
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
if ($authenticator &&
@@ -345,10 +347,12 @@ class UserHelper {
return false;
}
- static function hash_password(string $pass, string $salt, string $algo = "") {
-
- if (!$algo) $algo = self::HASH_ALGOS[0];
-
+ /**
+ * @param string $algo should be one of UserHelper::HASH_ALGO_*
+ *
+ * @return false|string False if the password couldn't be hashed, otherwise the hash string.
+ */
+ static function hash_password(string $pass, string $salt, string $algo = self::HASH_ALGOS[0]) {
$pass_hash = "";
switch ($algo) {