summaryrefslogtreecommitdiff
path: root/classes
diff options
context:
space:
mode:
Diffstat (limited to 'classes')
-rwxr-xr-xclasses/api.php240
-rwxr-xr-xclasses/article.php97
-rw-r--r--classes/auth/base.php22
-rw-r--r--classes/config.php336
-rw-r--r--classes/counters.php52
-rwxr-xr-xclasses/db.php13
-rw-r--r--classes/db/migrations.php44
-rw-r--r--classes/db/prefs.php10
-rw-r--r--classes/debug.php88
-rw-r--r--classes/digest.php7
-rw-r--r--classes/diskcache.php97
-rw-r--r--classes/errors.php29
-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.php81
-rwxr-xr-xclasses/feeds.php444
-rw-r--r--classes/handler.php22
-rw-r--r--classes/handler/administrative.php4
-rw-r--r--classes/handler/protected.php2
-rwxr-xr-xclasses/handler/public.php75
-rw-r--r--classes/iauthmodule.php17
-rw-r--r--classes/ihandler.php6
-rw-r--r--classes/ivirtualfeed.php11
-rw-r--r--classes/labels.php41
-rwxr-xr-xclasses/logger.php13
-rw-r--r--classes/logger/adapter.php4
-rwxr-xr-xclasses/logger/sql.php4
-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.php354
-rw-r--r--classes/plugin.php636
-rw-r--r--classes/pluginhandler.php4
-rwxr-xr-xclasses/pluginhost.php524
-rwxr-xr-xclasses/pref/feeds.php607
-rwxr-xr-xclasses/pref/filters.php132
-rw-r--r--classes/pref/labels.php25
-rw-r--r--classes/pref/prefs.php257
-rw-r--r--classes/pref/system.php23
-rw-r--r--classes/pref/users.php18
-rw-r--r--classes/prefs.php54
-rwxr-xr-xclasses/rpc.php104
-rwxr-xr-xclasses/rssutils.php196
-rw-r--r--classes/sanitizer.php26
-rw-r--r--classes/timehelper.php8
-rw-r--r--classes/urlhelper.php128
-rw-r--r--classes/userhelper.php57
50 files changed, 3514 insertions, 1611 deletions
diff --git a/classes/api.php b/classes/api.php
index 5f825e551..b17114693 100755
--- a/classes/api.php
+++ b/classes/api.php
@@ -1,7 +1,7 @@
<?php
class API extends Handler {
- const API_LEVEL = 17;
+ const API_LEVEL = 18;
const STATUS_OK = 0;
const STATUS_ERR = 1;
@@ -13,21 +13,22 @@ 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) {
- return $p && ($p !== "f" && $p !== "false");
- }
-
- private function _wrap($status, $reply) {
+ /**
+ * @param array<int|string, mixed> $reply
+ */
+ private function _wrap(int $status, array $reply): bool {
print json_encode([
"seq" => $this->seq,
"status" => $status,
"content" => $reply
]);
+ return true;
}
- function before($method) {
+ function before(string $method): bool {
if (parent::before($method)) {
header("Content-Type: text/json");
@@ -48,17 +49,17 @@ class API extends Handler {
return false;
}
- function getVersion() {
+ function getVersion(): bool {
$rv = array("version" => Config::get_version());
- $this->_wrap(self::STATUS_OK, $rv);
+ return $this->_wrap(self::STATUS_OK, $rv);
}
- function getApiLevel() {
+ function getApiLevel(): bool {
$rv = array("level" => self::API_LEVEL);
- $this->_wrap(self::STATUS_OK, $rv);
+ return $this->_wrap(self::STATUS_OK, $rv);
}
- function login() {
+ function login(): bool {
if (session_status() == PHP_SESSION_ACTIVE) {
session_destroy();
@@ -78,62 +79,60 @@ class API extends Handler {
// needed for _get_config()
UserHelper::load_user_plugins($_SESSION['uid']);
- $this->_wrap(self::STATUS_OK, array("session_id" => session_id(),
+ return $this->_wrap(self::STATUS_OK, array("session_id" => session_id(),
"config" => $this->_get_config(),
"api_level" => self::API_LEVEL));
} else {
- $this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
+ return $this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
}
} else {
- $this->_wrap(self::STATUS_ERR, array("error" => self::E_API_DISABLED));
+ return $this->_wrap(self::STATUS_ERR, array("error" => self::E_API_DISABLED));
}
- } else {
- $this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
- return;
}
+ return $this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
}
- function logout() {
+ function logout(): bool {
UserHelper::logout();
- $this->_wrap(self::STATUS_OK, array("status" => "OK"));
+ return $this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
- function isLoggedIn() {
- $this->_wrap(self::STATUS_OK, array("status" => $_SESSION["uid"] != ''));
+ function isLoggedIn(): bool {
+ return $this->_wrap(self::STATUS_OK, array("status" => $_SESSION["uid"] != ''));
}
- function getUnread() {
+ function getUnread(): bool {
$feed_id = clean($_REQUEST["feed_id"] ?? "");
- $is_cat = clean($_REQUEST["is_cat"] ?? "");
+ $is_cat = self::_param_to_bool($_REQUEST["is_cat"] ?? false);
if ($feed_id) {
- $this->_wrap(self::STATUS_OK, array("unread" => getFeedUnread($feed_id, $is_cat)));
+ return $this->_wrap(self::STATUS_OK, array("unread" => Feeds::_get_counters($feed_id, $is_cat, true)));
} else {
- $this->_wrap(self::STATUS_OK, array("unread" => Feeds::_get_global_unread()));
+ return $this->_wrap(self::STATUS_OK, array("unread" => Feeds::_get_global_unread()));
}
}
/* Method added for ttrss-reader for Android */
- function getCounters() {
- $this->_wrap(self::STATUS_OK, Counters::get_all());
+ function getCounters(): bool {
+ return $this->_wrap(self::STATUS_OK, Counters::get_all());
}
- function getFeeds() {
- $cat_id = clean($_REQUEST["cat_id"]);
- $unread_only = self::_param_to_bool(clean($_REQUEST["unread_only"] ?? 0));
+ function getFeeds(): bool {
+ $cat_id = (int) clean($_REQUEST["cat_id"]);
+ $unread_only = self::_param_to_bool($_REQUEST["unread_only"] ?? false);
$limit = (int) clean($_REQUEST["limit"] ?? 0);
$offset = (int) clean($_REQUEST["offset"] ?? 0);
- $include_nested = self::_param_to_bool(clean($_REQUEST["include_nested"] ?? false));
+ $include_nested = self::_param_to_bool($_REQUEST["include_nested"] ?? false);
$feeds = $this->_api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested);
- $this->_wrap(self::STATUS_OK, $feeds);
+ return $this->_wrap(self::STATUS_OK, $feeds);
}
- function getCategories() {
- $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));
+ function getCategories(): bool {
+ $unread_only = self::_param_to_bool($_REQUEST["unread_only"] ?? false);
+ $enable_nested = self::_param_to_bool($_REQUEST["enable_nested"] ?? false);
+ $include_empty = self::_param_to_bool($_REQUEST["include_empty"] ?? false);
// TODO do not return empty categories, return Uncategorized and standard virtual cats
@@ -153,7 +152,7 @@ class API extends Handler {
foreach ($categories->find_many() as $category) {
if ($include_empty || $category->num_feeds > 0 || $category->num_cats > 0) {
- $unread = getFeedUnread($category->id, true);
+ $unread = Feeds::_get_counters($category->id, true, true);
if ($enable_nested)
$unread += Feeds::_get_cat_children_unread($category->id);
@@ -171,7 +170,7 @@ class API extends Handler {
foreach ([-2,-1,0] as $cat_id) {
if ($include_empty || !$this->_is_cat_empty($cat_id)) {
- $unread = getFeedUnread($cat_id, true);
+ $unread = Feeds::_get_counters($cat_id, true, true);
if ($unread || !$unread_only) {
array_push($cats, [
@@ -183,40 +182,37 @@ class API extends Handler {
}
}
- $this->_wrap(self::STATUS_OK, $cats);
+ return $this->_wrap(self::STATUS_OK, $cats);
}
- function getHeadlines() {
- $feed_id = clean($_REQUEST["feed_id"]);
- if ($feed_id !== "") {
-
- if (is_numeric($feed_id)) $feed_id = (int) $feed_id;
+ function getHeadlines(): bool {
+ $feed_id = clean($_REQUEST["feed_id"] ?? "");
+ if (!empty($feed_id) || is_numeric($feed_id)) { // is_numeric for feed_id "0"
$limit = (int)clean($_REQUEST["limit"] ?? 0 );
if (!$limit || $limit >= 200) $limit = 200;
$offset = (int)clean($_REQUEST["skip"] ?? 0);
$filter = clean($_REQUEST["filter"] ?? "");
- $is_cat = self::_param_to_bool(clean($_REQUEST["is_cat"] ?? false));
- $show_excerpt = self::_param_to_bool(clean($_REQUEST["show_excerpt"] ?? false));
- $show_content = self::_param_to_bool(clean($_REQUEST["show_content"] ?? false));
+ $is_cat = self::_param_to_bool($_REQUEST["is_cat"] ?? false);
+ $show_excerpt = self::_param_to_bool($_REQUEST["show_excerpt"] ?? false);
+ $show_content = self::_param_to_bool($_REQUEST["show_content"] ?? false);
/* all_articles, unread, adaptive, marked, updated */
$view_mode = clean($_REQUEST["view_mode"] ?? null);
- $include_attachments = self::_param_to_bool(clean($_REQUEST["include_attachments"] ?? false));
+ $include_attachments = self::_param_to_bool($_REQUEST["include_attachments"] ?? false);
$since_id = (int)clean($_REQUEST["since_id"] ?? 0);
- $include_nested = self::_param_to_bool(clean($_REQUEST["include_nested"] ?? false));
- $sanitize_content = !isset($_REQUEST["sanitize"]) ||
- self::_param_to_bool($_REQUEST["sanitize"]);
- $force_update = self::_param_to_bool(clean($_REQUEST["force_update"] ?? false));
- $has_sandbox = self::_param_to_bool(clean($_REQUEST["has_sandbox"] ?? false));
+ $include_nested = self::_param_to_bool($_REQUEST["include_nested"] ?? false);
+ $sanitize_content = self::_param_to_bool($_REQUEST["sanitize"] ?? true);
+ $force_update = self::_param_to_bool($_REQUEST["force_update"] ?? false);
+ $has_sandbox = self::_param_to_bool($_REQUEST["has_sandbox"] ?? false);
$excerpt_length = (int)clean($_REQUEST["excerpt_length"] ?? 0);
$check_first_id = (int)clean($_REQUEST["check_first_id"] ?? 0);
- $include_header = self::_param_to_bool(clean($_REQUEST["include_header"] ?? false));
+ $include_header = self::_param_to_bool($_REQUEST["include_header"] ?? false);
$_SESSION['hasSandbox'] = $has_sandbox;
- list($override_order, $skip_first_id_check) = Feeds::_order_to_override_query(clean($_REQUEST["order_by"] ?? null));
+ list($override_order, $skip_first_id_check) = Feeds::_order_to_override_query(clean($_REQUEST["order_by"] ?? ""));
/* do not rely on params below */
@@ -228,16 +224,16 @@ class API extends Handler {
$include_nested, $sanitize_content, $force_update, $excerpt_length, $check_first_id, $skip_first_id_check);
if ($include_header) {
- $this->_wrap(self::STATUS_OK, array($headlines_header, $headlines));
+ return $this->_wrap(self::STATUS_OK, array($headlines_header, $headlines));
} else {
- $this->_wrap(self::STATUS_OK, $headlines);
+ return $this->_wrap(self::STATUS_OK, $headlines);
}
} else {
- $this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
+ return $this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
}
}
- function updateArticle() {
+ function updateArticle(): bool {
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
$mode = (int) clean($_REQUEST["mode"]);
$data = clean($_REQUEST["data"] ?? "");
@@ -294,19 +290,19 @@ class API extends Handler {
$num_updated = $sth->rowCount();
- $this->_wrap(self::STATUS_OK, array("status" => "OK",
+ return $this->_wrap(self::STATUS_OK, array("status" => "OK",
"updated" => $num_updated));
} else {
- $this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
+ return $this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
}
-
}
- function getArticle() {
+ function getArticle(): bool {
$article_ids = explode(',', clean($_REQUEST['article_id'] ?? ''));
$sanitize_content = self::_param_to_bool($_REQUEST['sanitize'] ?? true);
+ // @phpstan-ignore-next-line
if (count($article_ids)) {
$entries = ORM::for_table('ttrss_entries')
->table_alias('e')
@@ -350,7 +346,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;
}
@@ -368,14 +364,16 @@ class API extends Handler {
array_push($articles, $article);
}
- $this->_wrap(self::STATUS_OK, $articles);
- // @phpstan-ignore-next-line
+ return $this->_wrap(self::STATUS_OK, $articles);
} else {
- $this->_wrap(self::STATUS_ERR, ['error' => self::E_INCORRECT_USAGE]);
+ return $this->_wrap(self::STATUS_ERR, ['error' => self::E_INCORRECT_USAGE]);
}
}
- 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,42 +389,42 @@ class API extends Handler {
return $config;
}
- function getConfig() {
+ function getConfig(): bool {
$config = $this->_get_config();
- $this->_wrap(self::STATUS_OK, $config);
+ return $this->_wrap(self::STATUS_OK, $config);
}
- function updateFeed() {
+ function updateFeed(): bool {
$feed_id = (int) clean($_REQUEST["feed_id"]);
if (!ini_get("open_basedir")) {
RSSUtils::update_rss_feed($feed_id);
}
- $this->_wrap(self::STATUS_OK, array("status" => "OK"));
+ return $this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
- function catchupFeed() {
+ function catchupFeed(): bool {
$feed_id = clean($_REQUEST["feed_id"]);
- $is_cat = clean($_REQUEST["is_cat"]);
- @$mode = clean($_REQUEST["mode"]);
+ $is_cat = self::_param_to_bool($_REQUEST["is_cat"] ?? false);
+ $mode = clean($_REQUEST["mode"] ?? "");
if (!in_array($mode, ["all", "1day", "1week", "2week"]))
$mode = "all";
Feeds::_catchup($feed_id, $is_cat, $_SESSION["uid"], $mode);
- $this->_wrap(self::STATUS_OK, array("status" => "OK"));
+ return $this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
- function getPref() {
+ function getPref(): bool {
$pref_name = clean($_REQUEST["pref_name"]);
- $this->_wrap(self::STATUS_OK, array("value" => get_pref($pref_name)));
+ return $this->_wrap(self::STATUS_OK, array("value" => get_pref($pref_name)));
}
- function getLabels() {
+ function getLabels(): bool {
$article_id = (int)clean($_REQUEST['article_id'] ?? -1);
$rv = [];
@@ -459,10 +457,10 @@ class API extends Handler {
]);
}
- $this->_wrap(self::STATUS_OK, $rv);
+ return $this->_wrap(self::STATUS_OK, $rv);
}
- function setArticleLabel() {
+ function setArticleLabel(): bool {
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
$label_id = (int) clean($_REQUEST['label_id']);
@@ -477,52 +475,51 @@ 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;
}
}
- $this->_wrap(self::STATUS_OK, array("status" => "OK",
+ return $this->_wrap(self::STATUS_OK, array("status" => "OK",
"updated" => $num_updated));
}
- function index($method) {
+ function index(string $method): bool {
$plugin = PluginHost::getInstance()->get_api_method(strtolower($method));
if ($plugin && method_exists($plugin, $method)) {
$reply = $plugin->$method();
- $this->_wrap($reply[0], $reply[1]);
+ return $this->_wrap($reply[0], $reply[1]);
} else {
- $this->_wrap(self::STATUS_ERR, array("error" => self::E_UNKNOWN_METHOD, "method" => $method));
+ return $this->_wrap(self::STATUS_ERR, array("error" => self::E_UNKNOWN_METHOD, "method" => $method));
}
}
- function shareToPublished() {
+ function shareToPublished(): bool {
$title = strip_tags(clean($_REQUEST["title"]));
$url = strip_tags(clean($_REQUEST["url"]));
$content = strip_tags(clean($_REQUEST["content"]));
if (Article::_create_published_article($title, $url, $content, "", $_SESSION["uid"])) {
- $this->_wrap(self::STATUS_OK, array("status" => 'OK'));
+ return $this->_wrap(self::STATUS_OK, array("status" => 'OK'));
} else {
- $this->_wrap(self::STATUS_ERR, array("error" => self::E_OPERATION_FAILED));
+ return $this->_wrap(self::STATUS_ERR, array("error" => self::E_OPERATION_FAILED));
}
}
- 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 */
@@ -549,7 +546,7 @@ class API extends Handler {
if ($cat_id == -4 || $cat_id == -1) {
foreach ([-1, -2, -3, -4, -6, 0] as $i) {
- $unread = getFeedUnread($i);
+ $unread = Feeds::_get_counters($i, false, true);
if ($unread || !$unread_only) {
$title = Feeds::_get_title($i);
@@ -576,7 +573,7 @@ class API extends Handler {
->find_many();
foreach ($categories as $category) {
- $unread = getFeedUnread($category->id, true) +
+ $unread = Feeds::_get_counters($category->id, true, true) +
Feeds::_get_cat_children_unread($category->id);
if ($unread || !$unread_only) {
@@ -610,7 +607,7 @@ class API extends Handler {
}
foreach ($feeds_obj->find_many() as $feed) {
- $unread = getFeedUnread($feed->id);
+ $unread = Feeds::_get_counters($feed->id, false, true);
$has_icon = Feeds::_has_icon($feed->id);
if ($unread || !$unread_only) {
@@ -632,13 +629,17 @@ 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) {
-
- if ($force_update && $feed_id > 0 && is_numeric($feed_id)) {
+ /**
+ * @param string|int $feed_id
+ * @return array{0: array<int, array<string, mixed>>, 1: array<string, mixed>} $headlines, $headlines_header
+ */
+ private static function _api_get_headlines($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 && 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 +747,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 +804,7 @@ class API extends Handler {
return array($headlines, $headlines_header);
}
- function unsubscribeFeed() {
+ function unsubscribeFeed(): bool {
$feed_id = (int) clean($_REQUEST["feed_id"]);
$feed_exists = ORM::for_table('ttrss_feeds')
@@ -812,28 +813,28 @@ class API extends Handler {
if ($feed_exists) {
Pref_Feeds::remove_feed($feed_id, $_SESSION['uid']);
- $this->_wrap(self::STATUS_OK, ['status' => 'OK']);
+ return $this->_wrap(self::STATUS_OK, ['status' => 'OK']);
} else {
- $this->_wrap(self::STATUS_ERR, ['error' => self::E_OPERATION_FAILED]);
+ return $this->_wrap(self::STATUS_ERR, ['error' => self::E_OPERATION_FAILED]);
}
}
- function subscribeToFeed() {
+ function subscribeToFeed(): bool {
$feed_url = clean($_REQUEST["feed_url"]);
$category_id = (int) clean($_REQUEST["category_id"]);
- $login = clean($_REQUEST["login"]);
- $password = clean($_REQUEST["password"]);
+ $login = clean($_REQUEST["login"] ?? "");
+ $password = clean($_REQUEST["password"] ?? "");
if ($feed_url) {
$rc = Feeds::_subscribe($feed_url, $category_id, $login, $password);
- $this->_wrap(self::STATUS_OK, array("status" => $rc));
+ return $this->_wrap(self::STATUS_OK, array("status" => $rc));
} else {
- $this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
+ return $this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
}
}
- function getFeedTree() {
+ function getFeedTree(): bool {
$include_empty = self::_param_to_bool(clean($_REQUEST['include_empty']));
$pf = new Pref_Feeds($_REQUEST);
@@ -841,12 +842,12 @@ class API extends Handler {
$_REQUEST['mode'] = 2;
$_REQUEST['force_show_empty'] = $include_empty;
- $this->_wrap(self::STATUS_OK,
+ return $this->_wrap(self::STATUS_OK,
array("categories" => $pf->_makefeedtree()));
}
// 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 +866,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 4ae4a2407..cc089b7ba 100644
--- a/classes/config.php
+++ b/classes/config.php
@@ -6,171 +6,190 @@ class Config {
const T_STRING = 2;
const T_INT = 3;
- const SCHEMA_VERSION = 145;
-
- /* override defaults, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX:
-
- DB_TYPE becomes:
-
- .env:
-
- TTRSS_DB_TYPE=pgsql
-
- or config.php:
-
- putenv('TTRSS_DB_TYPE=pgsql');
-
- etc, etc.
+ const SCHEMA_VERSION = 146;
+
+ /** override default values, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX:
+ *
+ * DB_TYPE becomes:
+ *
+ * .env (docker environment):
+ *
+ * TTRSS_DB_TYPE=pgsql
+ *
+ * or config.php:
+ *
+ * putenv('TTRSS_DB_TYPE=pgsql');
+ *
+ * note lack of quotes and spaces before and after "=".
+ *
*/
+ /** database type: pgsql or mysql */
const DB_TYPE = "DB_TYPE";
+
+ /** database server hostname */
const DB_HOST = "DB_HOST";
+
+ /** database user */
const DB_USER = "DB_USER";
+
+ /** database name */
const DB_NAME = "DB_NAME";
+
+ /** database password */
const DB_PASS = "DB_PASS";
+
+ /** database server port */
const DB_PORT = "DB_PORT";
- // database credentials
+ /** connection charset for MySQL. if you have a legacy database and/or experience
+ * garbage unicode characters with this option, try setting it to a blank string. */
const MYSQL_CHARSET = "MYSQL_CHARSET";
- // connection charset for MySQL. if you have a legacy database and/or experience
- // garbage unicode characters with this option, try setting it to a blank string.
+ /** this should be set to a fully qualified URL used to access
+ * your tt-rss instance over the net, such as: https://example.com/tt-rss/
+ * if your tt-rss instance is behind a reverse proxy, use external URL.
+ * tt-rss will likely help you pick correct value for this on startup */
const SELF_URL_PATH = "SELF_URL_PATH";
- // this should be set to a fully qualified URL used to access
- // your tt-rss instance over the net, such as: https://example.com/tt-rss/
- // if your tt-rss instance is behind a reverse proxy, use external URL.
- // tt-rss will likely help you pick correct value for this on startup
+ /** operate in single user mode, disables all functionality related to
+ * multiple users and authentication. enabling this assumes you have
+ * your tt-rss directory protected by other means (e.g. http auth). */
const SINGLE_USER_MODE = "SINGLE_USER_MODE";
- // operate in single user mode, disables all functionality related to
- // multiple users and authentication. enabling this assumes you have
- // your tt-rss directory protected by other means (e.g. http auth).
+ /** enables fallback update mode where tt-rss tries to update feeds in
+ * background while tt-rss is open in your browser.
+ * if you don't have a lot of feeds and don't want to or can't run
+ * background processes while not running tt-rss, this method is generally
+ * viable to keep your feeds up to date. */
const SIMPLE_UPDATE_MODE = "SIMPLE_UPDATE_MODE";
- // enables fallback update mode where tt-rss tries to update feeds in
- // background while tt-rss is open in your browser.
- // if you don't have a lot of feeds and don't want to or can't run
- // background processes while not running tt-rss, this method is generally
- // viable to keep your feeds up to date.
+ /** use this PHP CLI executable to start various tasks */
const PHP_EXECUTABLE = "PHP_EXECUTABLE";
- // use this PHP CLI executable to start various tasks
+ /** base directory for lockfiles (must be writable) */
const LOCK_DIRECTORY = "LOCK_DIRECTORY";
- // base directory for lockfiles (must be writable)
+ /** base directory for local cache (must be writable) */
const CACHE_DIR = "CACHE_DIR";
- // base directory for local cache (must be writable)
+ /** directory for feed favicons (directory must be writable) */
const ICONS_DIR = "ICONS_DIR";
+
+ /** URL for feed favicons */
const ICONS_URL = "ICONS_URL";
- // directory and URL for feed favicons (directory must be writable)
+ /** auto create users authenticated via external modules */
const AUTH_AUTO_CREATE = "AUTH_AUTO_CREATE";
- // auto create users authenticated via external modules
+ /** auto log in users authenticated via external modules i.e. auth_remote */
const AUTH_AUTO_LOGIN = "AUTH_AUTO_LOGIN";
- // auto log in users authenticated via external modules i.e. auth_remote
+ /** unconditinally purge all articles older than this amount, in days
+ * overrides user-controlled purge interval */
const FORCE_ARTICLE_PURGE = "FORCE_ARTICLE_PURGE";
- // unconditinally purge all articles older than this amount, in days
- // overrides user-controlled purge interval
+ /** default lifetime of a session (e.g. login) cookie. In seconds,
+ * 0 means cookie will be deleted when browser closes. */
const SESSION_COOKIE_LIFETIME = "SESSION_COOKIE_LIFETIME";
- // default lifetime of a session (e.g. login) cookie. In seconds,
- // 0 means cookie will be deleted when browser closes.
+ /** send email using this name */
const SMTP_FROM_NAME = "SMTP_FROM_NAME";
+
+ /** send email using this address */
const SMTP_FROM_ADDRESS = "SMTP_FROM_ADDRESS";
- // send email using this name and address
+ /** default subject for email digest */
const DIGEST_SUBJECT = "DIGEST_SUBJECT";
- // default subject for email digest
+ /** enable built-in update checker, both for core code and plugins (using git) */
const CHECK_FOR_UPDATES = "CHECK_FOR_UPDATES";
- // enable built-in update checker, both for core code and plugins (using git)
+ /** system plugins enabled for all users, comma separated list, no quotes
+ * keep at least one auth module in there (i.e. auth_internal) */
const PLUGINS = "PLUGINS";
- // system plugins enabled for all users, comma separated list, no quotes
- // keep at least one auth module in there (i.e. auth_internal)
+ /** available options: sql (default, event log), syslog, stdout (for debugging) */
const LOG_DESTINATION = "LOG_DESTINATION";
- // available options: sql (default, event log), syslog, stdout (for debugging)
+ /** link this stylesheet on all pages (if it exists), should be placed in themes.local */
const LOCAL_OVERRIDE_STYLESHEET = "LOCAL_OVERRIDE_STYLESHEET";
- // link this stylesheet on all pages (if it exists), should be placed in themes.local
+ /** same but this javascript file (you can use that for polyfills), should be placed in themes.local */
const LOCAL_OVERRIDE_JS = "LOCAL_OVERRIDE_JS";
- // same but this javascript file (you can use that for polyfills), should be placed in themes.local
+ /** in seconds, terminate update tasks that ran longer than this interval */
const DAEMON_MAX_CHILD_RUNTIME = "DAEMON_MAX_CHILD_RUNTIME";
- // in seconds, terminate update tasks that ran longer than this interval
+ /** max concurrent update jobs forking update daemon starts */
const DAEMON_MAX_JOBS = "DAEMON_MAX_JOBS";
- // max concurrent update jobs forking update daemon starts
+ /** How long to wait for response when requesting feed from a site (seconds) */
const FEED_FETCH_TIMEOUT = "FEED_FETCH_TIMEOUT";
- // How long to wait for response when requesting feed from a site (seconds)
+ /** How long to wait for response when requesting uncached feed from a site (seconds) */
const FEED_FETCH_NO_CACHE_TIMEOUT = "FEED_FETCH_NO_CACHE_TIMEOUT";
- // Same but not cached
+ /** Default timeout when fetching files from remote sites */
const FILE_FETCH_TIMEOUT = "FILE_FETCH_TIMEOUT";
- // Default timeout when fetching files from remote sites
+ /** How long to wait for initial response from website when fetching remote files */
const FILE_FETCH_CONNECT_TIMEOUT = "FILE_FETCH_CONNECT_TIMEOUT";
- // How long to wait for initial response from website when fetching files from remote sites
+ /** stop updating feeds if user haven't logged in for X days */
const DAEMON_UPDATE_LOGIN_LIMIT = "DAEMON_UPDATE_LOGIN_LIMIT";
- // stop updating feeds if user haven't logged in for X days
+ /** how many feeds to update in one batch */
const DAEMON_FEED_LIMIT = "DAEMON_FEED_LIMIT";
- // how many feeds to update in one batch
+ /** default sleep interval between feed updates (sec) */
const DAEMON_SLEEP_INTERVAL = "DAEMON_SLEEP_INTERVAL";
- // default sleep interval between feed updates (sec)
+ /** do not cache files larger than that (bytes) */
const MAX_CACHE_FILE_SIZE = "MAX_CACHE_FILE_SIZE";
- // do not cache files larger than that (bytes)
+ /** do not download files larger than that (bytes) */
const MAX_DOWNLOAD_FILE_SIZE = "MAX_DOWNLOAD_FILE_SIZE";
- // do not download files larger than that (bytes)
+ /** max file size for downloaded favicons (bytes) */
const MAX_FAVICON_FILE_SIZE = "MAX_FAVICON_FILE_SIZE";
- // max file size for downloaded favicons (bytes)
+ /** max age in days for various automatically cached (temporary) files */
const CACHE_MAX_DAYS = "CACHE_MAX_DAYS";
- // max age in days for various automatically cached (temporary) files
+ /** max interval between forced unconditional updates for servers
+ * not complying with http if-modified-since (seconds) */
const MAX_CONDITIONAL_INTERVAL = "MAX_CONDITIONAL_INTERVAL";
- // max interval between forced unconditional updates for servers not complying with http if-modified-since (seconds)
+ /** automatically disable updates for feeds which failed to
+ * update for this amount of days; 0 disables */
const DAEMON_UNSUCCESSFUL_DAYS_LIMIT = "DAEMON_UNSUCCESSFUL_DAYS_LIMIT";
- // automatically disable updates for feeds which failed to
- // update for this amount of days; 0 disables
+ /** log all sent emails in the event log */
const LOG_SENT_MAIL = "LOG_SENT_MAIL";
- // log all sent emails in the event log
+ /** use HTTP proxy for requests */
const HTTP_PROXY = "HTTP_PROXY";
- // use HTTP proxy for requests
+ /** prevent users from changing passwords */
const FORBID_PASSWORD_CHANGES = "FORBID_PASSWORD_CHANGES";
- // prevent users from changing passwords
+ /** default session cookie name */
const SESSION_NAME = "SESSION_NAME";
- // default session cookie name
+ /** enable plugin update checker (using git) */
const CHECK_FOR_PLUGIN_UPDATES = "CHECK_FOR_PLUGIN_UPDATES";
- // enable plugin update checker (using git)
+ /** allow installing first party plugins using plugin installer in prefs */
const ENABLE_PLUGIN_INSTALLER = "ENABLE_PLUGIN_INSTALLER";
- // allow installing first party plugins using plugin installer in prefs
+ /** minimum amount of seconds required between authentication attempts */
const AUTH_MIN_INTERVAL = "AUTH_MIN_INTERVAL";
- // minimum amount of seconds required between authentication attempts
- // default values for all of the above:
+ /** http user agent (changing this is not recommended) */
+ const HTTP_USER_AGENT = "HTTP_USER_AGENT";
+
+ /** default values for all global configuration options */
private const _DEFAULTS = [
Config::DB_TYPE => [ "pgsql", Config::T_STRING ],
Config::DB_HOST => [ "db", Config::T_STRING ],
@@ -224,15 +243,20 @@ class Config {
Config::CHECK_FOR_PLUGIN_UPDATES => [ "true", Config::T_BOOL ],
Config::ENABLE_PLUGIN_INSTALLER => [ "true", Config::T_BOOL ],
Config::AUTH_MIN_INTERVAL => [ 5, Config::T_INT ],
+ Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://tt-rss.org/)',
+ Config::T_STRING ],
];
+ /** @var Config|null */
private static $instance;
+ /** @var array<string, array<bool|int|string>> */
private $params = [];
- private $schema_version = null;
+
+ /** @var array<string, mixed> */
private $version = [];
- /** @var Db_Migrations $migrations */
+ /** @var Db_Migrations|null $migrations */
private $migrations;
public static function get_instance() : Config {
@@ -250,24 +274,30 @@ class Config {
$ref = new ReflectionClass(get_class($this));
foreach ($ref->getConstants() as $const => $cvalue) {
- if (isset($this::_DEFAULTS[$const])) {
- $override = getenv($this::_ENVVAR_PREFIX . $const);
+ if (isset(self::_DEFAULTS[$const])) {
+ $override = getenv(self::_ENVVAR_PREFIX . $const);
- list ($defval, $deftype) = $this::_DEFAULTS[$const];
+ list ($defval, $deftype) = self::_DEFAULTS[$const];
$this->params[$cvalue] = [ self::cast_to($override !== false ? $override : $defval, $deftype), $deftype ];
}
}
}
- /* package maintainers who don't use git: if version_static.txt exists in tt-rss root
- 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 */
-
+ /** determine tt-rss version (using git)
+ *
+ * package maintainers who don't use git: if version_static.txt exists in tt-rss root
+ * 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__);
@@ -278,6 +308,8 @@ class Config {
$ttrss_version["version"] = "UNKNOWN (Unsupported, Darwin)";
} else if (file_exists("$root_dir/version_static.txt")) {
$this->version["version"] = trim(file_get_contents("$root_dir/version_static.txt")) . " (Unsupported)";
+ } else if (ini_get("open_basedir")) {
+ $this->version["version"] = "UNKNOWN (Unsupported, open_basedir)";
} else if (is_dir("$root_dir/.git")) {
$this->version = self::get_version_from_git($root_dir);
@@ -285,7 +317,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("SCRIPT_ROOT") || !file_exists("/.dockerenv")) {
+ $this->version["version"] .= " (Unsupported)";
}
+
} else {
$this->version["version"] = "UNKNOWN (Unsupported)";
}
@@ -294,7 +329,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
@@ -321,7 +359,7 @@ class Config {
if ($check == "version") {
- $rv["version"] = strftime("%y.%m", (int)$timestamp) . "-$commit";
+ $rv["version"] = date("y.m", (int)$timestamp) . "-$commit";
$rv["commit"] = $commit;
$rv["timestamp"] = $timestamp;
@@ -360,6 +398,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:
@@ -371,24 +412,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) {
- $override = getenv($this::_ENVVAR_PREFIX . $param);
+ 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();
@@ -427,6 +474,9 @@ class Config {
/* sanity check stuff */
+ /** checks for mysql tables not using InnoDB (tt-rss is incompatible with MyISAM)
+ * @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();
@@ -443,7 +493,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
@@ -460,16 +510,53 @@ class Config {
array_push($errors, "Please enable at least one authentication module via PLUGINS");
}
- if (function_exists('posix_getuid') && posix_getuid() == 0) {
- array_push($errors, "Please don't run this script as root.");
- }
+ /* we assume our dependencies are sane under docker, so some sanity checks are skipped.
+ this also allows tt-rss process to run under root if requested (I'm using this for development
+ under podman because of uidmapping issues with rootless containers, don't use in production -fox) */
+ if (!getenv("container")) {
+ if (function_exists('posix_getuid') && posix_getuid() == 0) {
+ array_push($errors, "Please don't run this script as root.");
+ }
- if (version_compare(PHP_VERSION, '7.1.0', '<')) {
- array_push($errors, "PHP version 7.1.0 or newer required. You're using " . PHP_VERSION . ".");
- }
+ if (version_compare(PHP_VERSION, '7.1.0', '<')) {
+ array_push($errors, "PHP version 7.1.0 or newer required. You're using " . PHP_VERSION . ".");
+ }
+
+ if (!class_exists("UConverter")) {
+ array_push($errors, "PHP UConverter class is missing, it's provided by the Internationalization (intl) module.");
+ }
+
+ if (!function_exists("curl_init") && !ini_get("allow_url_fopen")) {
+ array_push($errors, "PHP configuration option allow_url_fopen is disabled, and CURL functions are not present. Either enable allow_url_fopen or install PHP extension for CURL.");
+ }
+
+ if (!function_exists("json_encode")) {
+ array_push($errors, "PHP support for JSON is required, but was not found.");
+ }
+
+ if (!class_exists("PDO")) {
+ array_push($errors, "PHP support for PDO is required but was not found.");
+ }
+
+ if (!function_exists("mb_strlen")) {
+ array_push($errors, "PHP support for mbstring functions is required but was not found.");
+ }
+
+ if (!function_exists("hash")) {
+ array_push($errors, "PHP support for hash() function is required but was not found.");
+ }
+
+ if (ini_get("safe_mode")) {
+ array_push($errors, "PHP safe mode setting is obsolete and not supported by tt-rss.");
+ }
- if (!class_exists("UConverter")) {
- array_push($errors, "PHP UConverter class is missing, it's provided by the Internationalization (intl) module.");
+ if (!function_exists("mime_content_type")) {
+ array_push($errors, "PHP function mime_content_type() is missing, try enabling fileinfo module.");
+ }
+
+ if (!class_exists("DOMDocument")) {
+ array_push($errors, "PHP support for DOMDocument is required, but was not found.");
+ }
}
if (!is_writable(self::get(Config::CACHE_DIR) . "/images")) {
@@ -484,6 +571,14 @@ class Config {
array_push($errors, "Data export cache is not writable (chmod -R 777 ".self::get(Config::CACHE_DIR)."/export)");
}
+ if (!is_writable(self::get(Config::ICONS_DIR))) {
+ array_push($errors, "ICONS_DIR defined in config.php is not writable (chmod -R 777 ".self::get(Config::ICONS_DIR).").\n");
+ }
+
+ if (!is_writable(self::get(Config::LOCK_DIRECTORY))) {
+ array_push($errors, "LOCK_DIRECTORY is not writable (chmod -R 777 ".self::get(Config::LOCK_DIRECTORY).").\n");
+ }
+
// ttrss_users won't be there on initial startup (before migrations are done)
if (!Config::is_migration_needed() && self::get(Config::SINGLE_USER_MODE)) {
if (UserHelper::get_login_by_id(1) != "admin") {
@@ -491,6 +586,7 @@ class Config {
}
}
+ // skip check for CLI scripts so that we could install database schema if it is missing.
if (php_sapi_name() != "cli") {
if (self::get_schema_version() < 0) {
@@ -515,46 +611,6 @@ class Config {
}
}
- if (!is_writable(self::get(Config::ICONS_DIR))) {
- array_push($errors, "ICONS_DIR defined in config.php is not writable (chmod -R 777 ".self::get(Config::ICONS_DIR).").\n");
- }
-
- if (!is_writable(self::get(Config::LOCK_DIRECTORY))) {
- array_push($errors, "LOCK_DIRECTORY is not writable (chmod -R 777 ".self::get(Config::LOCK_DIRECTORY).").\n");
- }
-
- if (!function_exists("curl_init") && !ini_get("allow_url_fopen")) {
- array_push($errors, "PHP configuration option allow_url_fopen is disabled, and CURL functions are not present. Either enable allow_url_fopen or install PHP extension for CURL.");
- }
-
- if (!function_exists("json_encode")) {
- array_push($errors, "PHP support for JSON is required, but was not found.");
- }
-
- if (!class_exists("PDO")) {
- array_push($errors, "PHP support for PDO is required but was not found.");
- }
-
- if (!function_exists("mb_strlen")) {
- array_push($errors, "PHP support for mbstring functions is required but was not found.");
- }
-
- if (!function_exists("hash")) {
- array_push($errors, "PHP support for hash() function is required but was not found.");
- }
-
- if (ini_get("safe_mode")) {
- array_push($errors, "PHP safe mode setting is obsolete and not supported by tt-rss.");
- }
-
- if (!function_exists("mime_content_type")) {
- array_push($errors, "PHP function mime_content_type() is missing, try enabling fileinfo module.");
- }
-
- if (!class_exists("DOMDocument")) {
- array_push($errors, "PHP support for DOMDocument is required, but was not found.");
- }
-
if (self::get(Config::DB_TYPE) == "mysql") {
$bad_tables = self::check_mysql_tables();
@@ -617,11 +673,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));
@@ -632,4 +688,8 @@ class Config {
return $rv;
}
+
+ 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..8756b5acf 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,13 +241,16 @@ class Counters {
return $ret;
}
- private static function get_virt() {
+ /**
+ * @return array<int, array<string, int|string>>
+ */
+ private static function get_virt(): array {
$ret = [];
for ($i = 0; $i >= -4; $i--) {
- $count = getFeedUnread($i);
+ $count = Feeds::_get_counters($i, false, true);
if ($i == 0 || $i == -1 || $i == -2)
$auxctr = Feeds::_get_counters($i, false);
@@ -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 a09c44628..2cc89f5ba 100755
--- a/classes/db.php
+++ b/classes/db.php
@@ -4,9 +4,7 @@ class Db
/** @var Db $instance */
private static $instance;
- private $link;
-
- /** @var PDO $pdo */
+ /** @var PDO|null $pdo */
private $pdo;
function __construct() {
@@ -19,7 +17,7 @@ class Db
}
}
- static function NOW() {
+ static function NOW(): string {
return date("Y-m-d H:i:s", time());
}
@@ -27,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)) {
@@ -90,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..fbdf260e0 100644
--- a/classes/debug.php
+++ b/classes/debug.php
@@ -1,56 +1,94 @@
<?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;
- $ts = strftime("%H:%M:%S", time());
+ $ts = date("H:i:s", time());
if (function_exists('posix_getpid')) {
$ts = "$ts/" . posix_getpid();
}
@@ -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..34bba25f1 100644
--- a/classes/diskcache.php
+++ b/classes/diskcache.php
@@ -1,8 +1,14 @@
<?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
+ /**
+ * https://stackoverflow.com/a/53662733
+ *
+ * @var array<string, string>
+ */
private $mimeMap = [
'video/3gpp2' => '3g2',
'video/3gp' => '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..aa626d017 100644
--- a/classes/errors.php
+++ b/classes/errors.php
@@ -7,7 +7,34 @@ 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]]);
}
+
+ static function libxml_last_error() : string {
+ $error = libxml_get_last_error();
+ $error_formatted = "";
+
+ if ($error) {
+ foreach (libxml_get_errors() as $error) {
+ if ($error->level == LIBXML_ERR_FATAL) {
+ // currently only the first error is reported
+ $error_formatted = self::format_libxml_error($error);
+ break;
+ }
+ }
+ }
+
+ return UConverter::transcode($error_formatted, 'UTF-8', 'UTF-8');
+ }
+
+ static function format_libxml_error(LibXMLError $error) : string {
+ return sprintf("LibXML error %s at line %d (column %d): %s",
+ $error->code, $error->line, $error->column,
+ $error->message);
+ }
}
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..cac6d8c54 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..fc2489e2d 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,43 @@ 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 "";
- }
+ /** @deprecated use Errors::format_libxml_error() instead */
+ function format_error(LibXMLError $error) : string {
+ return Errors::format_libxml_error($error);
}
// libxml may have invalid unicode data in error messages
- function error() {
- return UConverter::transcode($this->error, 'UTF-8', 'UTF-8');
+ 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 42673ca95..a06486883 100755
--- a/classes/feeds.php
+++ b/classes/feeds.php
@@ -5,20 +5,25 @@ class Feeds extends Handler_Protected {
const NEVER_GROUP_FEEDS = [ -6, 0 ];
const NEVER_GROUP_BY_DATE = [ -2, -1, -3 ];
- private $params;
+ /** @var int|float int on 64-bit, float on 32-bit */
+ private $viewfeed_timestamp;
- private $viewfeed_timestamp;
- private $viewfeed_timestamp_last;
+ /** @var int|float int on 64-bit, float on 32-bit */
+ private $viewfeed_timestamp_last;
- function csrf_ignore($method) {
+ 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) {
+ /**
+ * @param string|int $feed
+ * @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($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 = false, ? string $order_by = ''): array {
$disable_cache = false;
@@ -65,6 +70,8 @@ class Feeds extends Handler_Protected {
$qfh_ret = [];
if (!$cat_view && is_numeric($feed) && $feed < PLUGIN_FEED_BASE_INDEX && $feed > LABEL_BASE_INDEX) {
+
+ /** @var IVirtualFeed|false $handler */
$handler = PluginHost::getInstance()->get_feed_handler(
PluginHost::feed_to_pfeed_id($feed));
@@ -126,7 +133,7 @@ class Feeds extends Handler_Protected {
$reply['vfeed_group_enabled'] = $vfeed_group_enabled;
$plugin_menu_items = "";
- PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM,
+ PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2,
function ($result) use (&$plugin_menu_items) {
$plugin_menu_items .= $result;
},
@@ -188,7 +195,11 @@ class Feeds extends Handler_Protected {
// frontend doesn't expect pdo returning booleans as strings on mysql
if (Config::get(Config::DB_TYPE) == "mysql") {
foreach (["unread", "marked", "published"] as $k) {
- $line[$k] = $line[$k] === "1";
+ if (is_integer($line[$k])) {
+ $line[$k] = $line[$k] === 1;
+ } else {
+ $line[$k] = $line[$k] === "1";
+ }
}
}
@@ -230,24 +241,58 @@ class Feeds extends Handler_Protected {
$line["feed_title"] = $line["feed_title"] ?? "";
+ $button_doc = new DOMDocument();
+
$line["buttons_left"] = "";
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_LEFT_BUTTON,
- function ($result) use (&$line) {
- $line["buttons_left"] .= $result;
+ function ($result, $plugin) use (&$line, &$button_doc) {
+ if ($result && $button_doc->loadXML($result)) {
+
+ /** @var DOMElement|null */
+ $child = $button_doc->firstChild;
+
+ if ($child) {
+ do {
+ $child->setAttribute('data-plugin-name', get_class($plugin));
+ } while ($child = $child->nextSibling);
+
+ $line["buttons_left"] .= $button_doc->saveXML($button_doc->firstChild);
+ }
+ } else if ($result) {
+ user_error(get_class($plugin) .
+ " plugin: content provided in HOOK_ARTICLE_LEFT_BUTTON is not valid XML: " .
+ Errors::libxml_last_error() . " $result", E_USER_WARNING);
+ }
},
$line);
$line["buttons"] = "";
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_BUTTON,
- function ($result) use (&$line) {
- $line["buttons"] .= $result;
+ function ($result, $plugin) use (&$line, &$button_doc) {
+ if ($result && $button_doc->loadXML($result)) {
+
+ /** @var DOMElement|null */
+ $child = $button_doc->firstChild;
+
+ if ($child) {
+ do {
+ $child->setAttribute('data-plugin-name', get_class($plugin));
+ } while ($child = $child->nextSibling);
+
+ $line["buttons"] .= $button_doc->saveXML($button_doc->firstChild);
+ }
+ } else if ($result) {
+ user_error(get_class($plugin) .
+ " plugin: content provided in HOOK_ARTICLE_BUTTON is not valid XML: " .
+ Errors::libxml_last_error() . " $result", E_USER_WARNING);
+ }
},
$line);
$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");
@@ -265,9 +310,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' => [] ];
}
@@ -275,7 +320,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));
@@ -409,7 +454,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']]);
@@ -417,16 +462,16 @@ class Feeds extends Handler_Protected {
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
- function view() {
+ function view(): void {
$reply = array();
$feed = $_REQUEST["feed"];
$method = $_REQUEST["m"] ?? "";
$view_mode = $_REQUEST["view_mode"] ?? "";
$limit = 30;
- $cat_view = $_REQUEST["cat"] == "true";
+ $cat_view = self::_param_to_bool($_REQUEST["cat"] ?? false);
$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;
@@ -514,7 +559,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;
@@ -556,7 +604,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;
@@ -572,13 +623,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,
@@ -587,10 +638,31 @@ class Feeds extends Handler_Protected {
]);
}
- function updatedebugger() {
+ function opensite(): void {
+ $feed = ORM::for_table('ttrss_feeds')
+ ->find_one((int)$_REQUEST['feed_id']);
+
+ if ($feed) {
+ $site_url = UrlHelper::validate($feed->site_url);
+
+ if ($site_url) {
+ header("Location: $site_url");
+ return;
+ }
+ }
+
+ header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
+ print "Feed not found or has an empty site URL.";
+ }
+
+ 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);
@@ -603,9 +675,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>
@@ -658,7 +730,7 @@ class Feeds extends Handler_Protected {
<fieldset>
<label>
<?= \Controls\select_hash("xdebug", $xdebug,
- [Debug::$LOG_VERBOSE => "LOG_VERBOSE", Debug::$LOG_EXTENDED => "LOG_EXTENDED"]);
+ [Debug::LOG_VERBOSE => "LOG_VERBOSE", Debug::LOG_EXTENDED => "LOG_EXTENDED"]);
?></label>
</fieldset>
@@ -690,7 +762,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'];
@@ -744,14 +819,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);
@@ -769,7 +846,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(*)
@@ -778,18 +855,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
@@ -798,7 +875,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
@@ -807,7 +884,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);
@@ -826,7 +903,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
@@ -834,10 +911,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
@@ -846,23 +922,29 @@ 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) {
+ /**
+ * @param int|string $feed feed id or tag name
+ * @param bool $is_cat
+ * @param bool $unread_only
+ * @param null|int $owner_uid
+ * @return int
+ * @throws PDOException
+ */
+ static function _get_counters($feed, bool $is_cat = false, bool $unread_only = false, ?int $owner_uid = null): int {
$n_feed = (int) $feed;
$need_entries = false;
@@ -883,6 +965,7 @@ class Feeds extends Handler_Protected {
return self::_get_cat_unread($n_feed, $owner_uid);
} else if ($n_feed == -6) {
return 0;
+ // tags
} else if ($feed != "0" && $n_feed == 0) {
$sth = $pdo->prepare("SELECT SUM((SELECT COUNT(int_id)
@@ -893,7 +976,8 @@ class Feeds extends Handler_Protected {
$sth->execute([$owner_uid, $feed]);
$row = $sth->fetch();
- return $row["count"];
+ // Handle 'SUM()' returning null if there are no results
+ return $row["count"] ?? 0;
} else if ($n_feed == -1) {
$match_part = "marked = true";
@@ -961,9 +1045,9 @@ class Feeds extends Handler_Protected {
}
}
- function add() {
+ function add(): void {
$feed = clean($_REQUEST['feed']);
- $cat = clean($_REQUEST['cat'] ?? '');
+ $cat = (int) clean($_REQUEST['cat'] ?? '');
$need_auth = isset($_REQUEST['need_auth']);
$login = $need_auth ? clean($_REQUEST['login']) : '';
$pass = $need_auth ? clean($_REQUEST['pass']) : '';
@@ -974,7 +1058,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
@@ -986,9 +1070,15 @@ class Feeds extends Handler_Protected {
* 5 - Couldn't download the URL content.
* 6 - Content is an invalid XML.
* 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']);
+
+ if ($user && $user->access_level == UserHelper::ACCESS_LEVEL_READONLY) {
+ return ["code" => 8];
+ }
$pdo = Db::pdo();
@@ -996,6 +1086,13 @@ class Feeds extends Handler_Protected {
if (!$url) return ["code" => 2];
+ PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_PRE_SUBSCRIBE,
+ /** @phpstan-ignore-next-line */
+ function ($result) use (&$url, &$auth_login, &$auth_pass) {
+ // arguments are updated inside the hook (if needed)
+ },
+ $url, $auth_login, $auth_pass);
+
$contents = UrlHelper::fetch($url, false, $auth_login, $auth_pass);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_SUBSCRIBE_FEED,
@@ -1054,15 +1151,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";
@@ -1082,7 +1182,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);
}
}
@@ -1092,6 +1192,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)
@@ -1105,8 +1208,39 @@ class Feeds extends Handler_Protected {
}
}
- static function _get_title($id, bool $cat = false) {
- $pdo = Db::pdo();
+ /**
+ * $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;
+
+ if ($cat) {
+ $res = ORM::for_table('ttrss_feed_categories')
+ ->where('owner_uid', $owner_uid ? $owner_uid : $_SESSION['uid'])
+ ->where('title', $title)
+ ->find_one();
+ } else {
+ $res = ORM::for_table('ttrss_feeds')
+ ->where('owner_uid', $owner_uid ? $owner_uid : $_SESSION['uid'])
+ ->where('title', $title)
+ ->find_one();
+ }
+
+ if ($res) {
+ return $res->id;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param string|int $id
+ */
+ static function _get_title($id, bool $cat = false): string {
+ $pdo = Db::pdo();
if ($cat) {
return self::_get_cat_title($id);
@@ -1118,7 +1252,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");
@@ -1147,12 +1281,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"];
@@ -1166,16 +1300,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"];
@@ -1186,14 +1321,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) {
@@ -1201,15 +1337,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();
@@ -1228,7 +1368,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"];
@@ -1241,10 +1381,11 @@ class Feeds extends Handler_Protected {
$sth->execute([$user_id]);
$row = $sth->fetch();
- return $row["count"];
+ // Handle 'SUM()' returning null if there are no articles/results (e.g. admin user with no feeds)
+ return $row["count"] ?? 0;
}
- static function _get_cat_title(int $cat_id) {
+ static function _get_cat_title(int $cat_id): string {
switch ($cat_id) {
case 0:
return __("Uncategorized");
@@ -1264,7 +1405,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();
@@ -1281,7 +1422,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();
@@ -1362,7 +1507,7 @@ class Feeds extends Handler_Protected {
$view_query_part = " ";
} else if ($feed != -1) {
- $unread = getFeedUnread($feed, $cat_view);
+ $unread = Feeds::_get_counters($feed, $cat_view, true);
if ($cat_view && $feed > 0 && $include_children)
$unread += self::_get_cat_children_unread($feed);
@@ -1498,7 +1643,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";
@@ -1778,7 +1923,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();
@@ -1795,7 +1943,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();
@@ -1812,7 +1963,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 [];
@@ -1851,24 +2006,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 = [];
@@ -1885,9 +2043,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;
}
}
@@ -1895,11 +2051,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);
@@ -1908,7 +2064,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)
@@ -1932,13 +2088,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)
@@ -1948,7 +2109,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)
@@ -1957,21 +2123,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);
@@ -1987,7 +2155,7 @@ class Feeds extends Handler_Protected {
$owner_uid = $row["owner_uid"];
if (Config::get(Config::FORCE_ARTICLE_PURGE) != 0) {
- Debug::log("purge_feed: FORCE_ARTICLE_PURGE is set, overriding interval to " . Config::get(Config::FORCE_ARTICLE_PURGE), Debug::$LOG_VERBOSE);
+ Debug::log("purge_feed: FORCE_ARTICLE_PURGE is set, overriding interval to " . Config::get(Config::FORCE_ARTICLE_PURGE), Debug::LOG_VERBOSE);
$purge_unread = true;
$purge_interval = Config::get(Config::FORCE_ARTICLE_PURGE);
} else {
@@ -1996,11 +2164,11 @@ class Feeds extends Handler_Protected {
$purge_interval = (int) $purge_interval;
- Debug::log("purge_feed: interval $purge_interval days for feed $feed_id, owner: $owner_uid, purge unread: $purge_unread", Debug::$LOG_VERBOSE);
+ Debug::log("purge_feed: interval $purge_interval days for feed $feed_id, owner: $owner_uid, purge unread: $purge_unread", Debug::LOG_VERBOSE);
if ($purge_interval <= 0) {
- Debug::log("purge_feed: purging disabled for this feed, nothing to do.", Debug::$LOG_VERBOSE);
- return;
+ Debug::log("purge_feed: purging disabled for this feed, nothing to do.", Debug::LOG_VERBOSE);
+ return null;
}
if (!$purge_unread)
@@ -2032,16 +2200,16 @@ class Feeds extends Handler_Protected {
$rows_deleted = $sth->rowCount();
- Debug::log("purge_feed: deleted $rows_deleted articles.", Debug::$LOG_VERBOSE);
+ Debug::log("purge_feed: deleted $rows_deleted articles.", Debug::LOG_VERBOSE);
} else {
- Debug::log("purge_feed: owner of $feed_id not found", Debug::$LOG_VERBOSE);
+ Debug::log("purge_feed: owner of $feed_id not found", Debug::LOG_VERBOSE);
}
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) {
@@ -2054,7 +2222,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();
@@ -2147,7 +2318,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)");
}
@@ -2221,7 +2392,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;
@@ -2231,7 +2405,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":
@@ -2249,7 +2425,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..806c9cfbe 100644
--- a/classes/handler.php
+++ b/classes/handler.php
@@ -1,23 +1,37 @@
<?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;
}
+ /**
+ * @param mixed $p
+ */
+ protected static function _param_to_bool($p): bool {
+ $p = clean($p);
+ return $p && ($p !== "f" && $p !== "false");
+ }
}
diff --git a/classes/handler/administrative.php b/classes/handler/administrative.php
index 52dfed8b7..533cb3630 100644
--- a/classes/handler/administrative.php
+++ b/classes/handler/administrative.php
@@ -1,8 +1,8 @@
<?php
class Handler_Administrative extends Handler_Protected {
- function before($method) {
+ function before(string $method): bool {
if (parent::before($method)) {
- if (($_SESSION["access_level"] ?? 0) >= 10) {
+ 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 4da32e90d..3fef4c2b9 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,7 +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;
}
} else {
@@ -85,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) {
@@ -105,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);
@@ -203,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'];
@@ -243,7 +253,7 @@ class Handler_Public extends Handler {
}
}
- function getUnread() {
+ function getUnread(): void {
$login = clean($_REQUEST["login"]);
$fresh = clean($_REQUEST["fresh"]) == "1";
@@ -261,7 +271,7 @@ class Handler_Public extends Handler {
}
}
- function getProfiles() {
+ function getProfiles(): void {
$login = clean($_REQUEST["login"]);
$rv = [];
@@ -284,20 +294,37 @@ class Handler_Public extends Handler {
print json_encode($rv);
}
- function logout() {
+ function logout(): void {
if (validate_csrf($_POST["csrf_token"])) {
+
+ $login = $_SESSION["name"];
+ $user_id = $_SESSION["uid"];
+
UserHelper::logout();
- header("Location: index.php");
+
+ $redirect_url = "";
+
+ PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_POST_LOGOUT,
+ function ($result) use (&$redirect_url) {
+ if (!empty($result[0]))
+ $redirect_url = UrlHelper::validate($result[0]);
+ },
+ $login, $user_id);
+
+ if (!$redirect_url)
+ $redirect_url = get_self_url_prefix() . "/index.php";
+
+ header("Location: " . $redirect_url);
} else {
header("Content-Type: text/json");
print Errors::to_json(Errors::E_UNAUTHORIZED);
}
}
- function rss() {
+ function rss(): void {
$feed = clean($_REQUEST["id"]);
$key = clean($_REQUEST["key"]);
- $is_cat = clean($_REQUEST["is_cat"] ?? false);
+ $is_cat = self::_param_to_bool($_REQUEST["is_cat"] ?? false);
$limit = (int)clean($_REQUEST["limit"] ?? 0);
$offset = (int)clean($_REQUEST["offset"] ?? 0);
@@ -307,7 +334,7 @@ class Handler_Public extends Handler {
$start_ts = clean($_REQUEST["ts"] ?? "");
$format = clean($_REQUEST['format'] ?? "atom");
- $orig_guid = clean($_REQUEST["orig_guid"] ?? false);
+ $orig_guid = clean($_REQUEST["orig_guid"] ?? "");
if (Config::get(Config::SINGLE_USER_MODE)) {
UserHelper::authenticate("admin", null);
@@ -329,21 +356,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"]);
@@ -399,12 +426,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();
@@ -448,7 +475,7 @@ class Handler_Public extends Handler {
if ($login) {
$user = ORM::for_table('ttrss_users')
- ->select('id', 'resetpass_token')
+ ->select_many('id', 'resetpass_token')
->where_raw('LOWER(login) = LOWER(?)', [$login])
->find_one();
@@ -583,7 +610,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) {
@@ -726,7 +753,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
@@ -742,7 +769,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));
@@ -752,7 +779,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"]));
@@ -784,7 +811,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/ivirtualfeed.php b/classes/ivirtualfeed.php
new file mode 100644
index 000000000..ccd0680fc
--- /dev/null
+++ b/classes/ivirtualfeed.php
@@ -0,0 +1,11 @@
+<?php
+interface IVirtualFeed {
+ function get_unread(int $feed_id) : int;
+ function get_total(int $feed_id) : int;
+ /**
+ * @param int $feed_id
+ * @param array<string,int|string|bool> $options
+ * @return array<int,int|string>
+ */
+ function get_headlines(int $feed_id, array $options) : array;
+}
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 784ebef31..5f3c67852 100755
--- a/classes/logger/sql.php
+++ b/classes/logger/sql.php
@@ -1,8 +1,6 @@
<?php
class Logger_SQL implements Logger_Adapter {
- private $pdo;
-
function __construct() {
$conn = get_class($this);
@@ -12,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 2cfc890fa..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,15 +45,11 @@ class OPML extends Handler_Protected {
</form>";
print "</div></body></html>";
-
-
}
// Export
- private function opml_export_category($owner_uid, $cat_id, $hide_private_feeds = false, $include_settings = true) {
-
- $cat_id = (int) $cat_id;
+ 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 = '')";
@@ -126,7 +125,10 @@ class OPML extends Handler_Protected {
return $out;
}
- function opml_export($filename, $owner_uid, $hide_private_feeds = false, $include_settings = true, $file_output = 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 opml_export(string $filename, int $owner_uid, bool $hide_private_feeds = false, bool $include_settings = true, bool $file_output = false) {
if (!$owner_uid) return;
if (!$file_output)
@@ -189,7 +191,7 @@ class OPML extends Handler_Protected {
WHERE owner_uid = ? ORDER BY id");
$sth->execute([$owner_uid]);
- while ($line = $sth->fetch()) {
+ while ($line = $sth->fetch(PDO::FETCH_ASSOC)) {
$line["rules"] = array();
$line["actions"] = array();
@@ -204,36 +206,36 @@ class OPML extends Handler_Protected {
$cat_filter = $tmp_line["cat_filter"];
if (!$tmp_line["match_on"]) {
- if ($cat_filter && $tmp_line["cat_id"] || $tmp_line["feed_id"]) {
- $tmp_line["feed"] = Feeds::_get_title(
- $cat_filter ? $tmp_line["cat_id"] : $tmp_line["feed_id"],
- $cat_filter);
- } else {
- $tmp_line["feed"] = "";
- }
- } else {
- $match = [];
- foreach (json_decode($tmp_line["match_on"], true) as $feed_id) {
-
- if (strpos($feed_id, "CAT:") === 0) {
- $feed_id = (int)substr($feed_id, 4);
- if ($feed_id) {
- array_push($match, [Feeds::_get_cat_title($feed_id), true, false]);
- } else {
- array_push($match, [0, true, true]);
- }
- } else {
- if ($feed_id) {
- array_push($match, [Feeds::_get_title((int)$feed_id), false, false]);
- } else {
- array_push($match, [0, false, true]);
- }
- }
- }
-
- $tmp_line["match"] = $match;
- unset($tmp_line["match_on"]);
- }
+ if ($cat_filter && $tmp_line["cat_id"] || $tmp_line["feed_id"]) {
+ $tmp_line["feed"] = Feeds::_get_title(
+ $cat_filter ? $tmp_line["cat_id"] : $tmp_line["feed_id"],
+ $cat_filter);
+ } else {
+ $tmp_line["feed"] = "";
+ }
+ } else {
+ $match = [];
+ foreach (json_decode($tmp_line["match_on"], true) as $feed_id) {
+
+ if (strpos($feed_id, "CAT:") === 0) {
+ $feed_id = (int)substr($feed_id, 4);
+ if ($feed_id) {
+ array_push($match, [Feeds::_get_cat_title($feed_id), true, false]);
+ } else {
+ array_push($match, [0, true, true]);
+ }
+ } else {
+ if ($feed_id) {
+ array_push($match, [Feeds::_get_title((int)$feed_id), false, false]);
+ } else {
+ array_push($match, [0, false, true]);
+ }
+ }
+ }
+
+ $tmp_line["match"] = $match;
+ unset($tmp_line["match_on"]);
+ }
unset($tmp_line["feed_id"]);
unset($tmp_line["cat_id"]);
@@ -292,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($node, $cat_id, $owner_uid) {
+ 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);
@@ -318,7 +321,7 @@ class OPML extends Handler_Protected {
if (!$sth->fetch()) {
#$this->opml_notice("[FEED] [$feed_title/$feed_url] dst_CAT=$cat_id");
- $this->opml_notice(T_sprintf("Adding feed: %s", $feed_title == '[Unknown]' ? $feed_url : $feed_title));
+ $this->opml_notice(T_sprintf("Adding feed: %s", $feed_title == '[Unknown]' ? $feed_url : $feed_title), $nest);
if (!$cat_id) $cat_id = null;
@@ -338,12 +341,12 @@ class OPML extends Handler_Protected {
$sth->execute([$feed_title, $feed_url, $owner_uid, $cat_id, $site_url, $order_id, $update_interval, $purge_interval]);
} else {
- $this->opml_notice(T_sprintf("Duplicate feed: %s", $feed_title == '[Unknown]' ? $feed_url : $feed_title));
+ $this->opml_notice(T_sprintf("Duplicate feed: %s", $feed_title == '[Unknown]' ? $feed_url : $feed_title), $nest);
}
}
}
- private function opml_import_label($node, $owner_uid) {
+ private function opml_import_label(DOMNode $node, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$label_name = $attrs->getNamedItem('label-name')->nodeValue;
@@ -351,16 +354,16 @@ class OPML extends Handler_Protected {
$fg_color = $attrs->getNamedItem('label-fg-color')->nodeValue;
$bg_color = $attrs->getNamedItem('label-bg-color')->nodeValue;
- if (!Labels::find_id($label_name, $_SESSION['uid'])) {
- $this->opml_notice(T_sprintf("Adding label %s", htmlspecialchars($label_name)));
+ if (!Labels::find_id($label_name, $owner_uid)) {
+ $this->opml_notice(T_sprintf("Adding label %s", htmlspecialchars($label_name)), $nest);
Labels::create($label_name, $fg_color, $bg_color, $owner_uid);
} else {
- $this->opml_notice(T_sprintf("Duplicate label: %s", htmlspecialchars($label_name)));
+ $this->opml_notice(T_sprintf("Duplicate label: %s", htmlspecialchars($label_name)), $nest);
}
}
}
- private function opml_import_preference($node) {
+ private function opml_import_preference(DOMNode $node, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$pref_name = $attrs->getNamedItem('pref-name')->nodeValue;
@@ -368,13 +371,13 @@ class OPML extends Handler_Protected {
$pref_value = $attrs->getNamedItem('value')->nodeValue;
$this->opml_notice(T_sprintf("Setting preference key %s to %s",
- $pref_name, $pref_value));
+ $pref_name, $pref_value), $nest);
- set_pref($pref_name, $pref_value);
+ set_pref($pref_name, $pref_value, $owner_uid);
}
}
- private function opml_import_filter($node) {
+ private function opml_import_filter(DOMNode $node, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$filter_type = $attrs->getNamedItem('filter-type')->nodeValue;
@@ -393,47 +396,58 @@ class OPML extends Handler_Protected {
$sth = $this->pdo->prepare("INSERT INTO ttrss_filters2 (match_any_rule,enabled,inverse,title,owner_uid)
VALUES (?, ?, ?, ?, ?)");
- $sth->execute([$match_any_rule, $enabled, $inverse, $title, $_SESSION['uid']]);
+ $sth->execute([$match_any_rule, $enabled, $inverse, $title, $owner_uid]);
$sth = $this->pdo->prepare("SELECT MAX(id) AS id FROM ttrss_filters2 WHERE
owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
+ $sth->execute([$owner_uid]);
$row = $sth->fetch();
$filter_id = $row['id'];
if ($filter_id) {
- $this->opml_notice(T_sprintf("Adding filter %s...", $title));
+ $this->opml_notice(T_sprintf("Adding filter %s...", $title), $nest);
+ //$this->opml_notice(json_encode($filter));
foreach ($filter["rules"] as $rule) {
$feed_id = null;
$cat_id = null;
- if ($rule["match"]) {
+ if ($rule["match"] ?? false) {
- $match_on = [];
+ $match_on = [];
- foreach ($rule["match"] as $match) {
- list ($name, $is_cat, $is_id) = $match;
+ foreach ($rule["match"] as $match) {
+ list ($name, $is_cat, $is_id) = $match;
- if ($is_id) {
- array_push($match_on, ($is_cat ? "CAT:" : "") . $name);
- } else {
+ if ($is_id) {
+ array_push($match_on, ($is_cat ? "CAT:" : "") . $name);
+ } else {
- if (!$is_cat) {
- $tsth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
- WHERE title = ? AND owner_uid = ?");
+ $match_id = Feeds::_find_by_title($name, $is_cat, $owner_uid);
- $tsth->execute([$name, $_SESSION['uid']]);
+ if ($match_id) {
+ if ($is_cat) {
+ array_push($match_on, "CAT:$match_id");
+ } else {
+ array_push($match_on, $match_id);
+ }
+ }
+
+ /*if (!$is_cat) {
+ $tsth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
+ WHERE title = ? AND owner_uid = ?");
+
+ $tsth->execute([$name, $_SESSION['uid']]);
- if ($row = $tsth->fetch()) {
- $match_id = $row['id'];
+ if ($row = $tsth->fetch()) {
+ $match_id = $row['id'];
array_push($match_on, $match_id);
- }
- } else {
- $tsth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories
- WHERE title = ? AND owner_uid = ?");
+ }
+ } else {
+ $tsth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories
+ WHERE title = ? AND owner_uid = ?");
$tsth->execute([$name, $_SESSION['uid']]);
if ($row = $tsth->fetch()) {
@@ -441,54 +455,64 @@ class OPML extends Handler_Protected {
array_push($match_on, "CAT:$match_id");
}
- }
- }
- }
+ } */
+ }
+ }
- $reg_exp = $rule["reg_exp"];
- $filter_type = (int)$rule["filter_type"];
- $inverse = bool_to_sql_bool($rule["inverse"]);
- $match_on = json_encode($match_on);
+ $reg_exp = $rule["reg_exp"];
+ $filter_type = (int)$rule["filter_type"];
+ $inverse = bool_to_sql_bool($rule["inverse"]);
+ $match_on = json_encode($match_on);
- $usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_rules
+ $usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_rules
(feed_id,cat_id,match_on,filter_id,filter_type,reg_exp,cat_filter,inverse)
- VALUES
- (NULL, NULL, ?, ?, ?, ?, false, ?)");
- $usth->execute([$match_on, $filter_id, $filter_type, $reg_exp, $inverse]);
+ VALUES
+ (NULL, NULL, ?, ?, ?, ?, false, ?)");
+ $usth->execute([$match_on, $filter_id, $filter_type, $reg_exp, $inverse]);
+
+ } else {
- } else {
+ $match_id = Feeds::_find_by_title($rule['feed'] ?? "", $rule['cat_filter'], $owner_uid);
- if (!$rule["cat_filter"]) {
- $tsth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
- WHERE title = ? AND owner_uid = ?");
+ if ($match_id) {
+ if ($rule['cat_filter']) {
+ $cat_id = $match_id;
+ } else {
+ $feed_id = $match_id;
+ }
+ }
+
+ /*if (!$rule["cat_filter"]) {
+ $tsth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
+ WHERE title = ? AND owner_uid = ?");
- $tsth->execute([$rule['feed'], $_SESSION['uid']]);
+ $tsth->execute([$rule['feed'], $_SESSION['uid']]);
- if ($row = $tsth->fetch()) {
- $feed_id = $row['id'];
- }
- } else {
+ if ($row = $tsth->fetch()) {
+ $feed_id = $row['id'];
+ }
+ } else {
$tsth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories
- WHERE title = ? AND owner_uid = ?");
+ WHERE title = ? AND owner_uid = ?");
$tsth->execute([$rule['feed'], $_SESSION['uid']]);
if ($row = $tsth->fetch()) {
$feed_id = $row['id'];
}
- }
+ } */
- $cat_filter = bool_to_sql_bool($rule["cat_filter"]);
- $reg_exp = $rule["reg_exp"];
- $filter_type = (int)$rule["filter_type"];
- $inverse = bool_to_sql_bool($rule["inverse"]);
+ $cat_filter = bool_to_sql_bool($rule["cat_filter"]);
+ $reg_exp = $rule["reg_exp"];
+ $filter_type = (int)$rule["filter_type"];
+ $inverse = bool_to_sql_bool($rule["inverse"]);
- $usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_rules
+ $usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_rules
(feed_id,cat_id,filter_id,filter_type,reg_exp,cat_filter,inverse)
- VALUES
- (?, ?, ?, ?, ?, ?, ?)");
- $usth->execute([$feed_id, $cat_id, $filter_id, $filter_type, $reg_exp, $cat_filter, $inverse]);
- }
+ VALUES
+ (?, ?, ?, ?, ?, ?, ?)");
+ $usth->execute([$feed_id, $cat_id, $filter_id, $filter_type, $reg_exp, $cat_filter, $inverse]);
+ }
}
foreach ($filter["actions"] as $action) {
@@ -507,8 +531,8 @@ class OPML extends Handler_Protected {
}
}
- private function opml_import_category($doc, $root_node, $owner_uid, $parent_id) {
- $default_cat_id = (int) $this->get_feed_category('Imported feeds', false);
+ 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) {
$cat_title = mb_substr($root_node->attributes->getNamedItem('text')->nodeValue, 0, 250);
@@ -517,13 +541,13 @@ class OPML extends Handler_Protected {
$cat_title = mb_substr($root_node->attributes->getNamedItem('title')->nodeValue, 0, 250);
if (!in_array($cat_title, array("tt-rss-filters", "tt-rss-labels", "tt-rss-prefs"))) {
- $cat_id = $this->get_feed_category($cat_title, $parent_id);
+ $cat_id = $this->get_feed_category($cat_title, $owner_uid, $parent_id);
- if ($cat_id === false) {
+ if ($cat_id === 0) {
$order_id = (int) $root_node->attributes->getNamedItem('ttrssSortOrder')->nodeValue;
- Feeds::_add_cat($cat_title, $_SESSION['uid'], $parent_id ? $parent_id : null, (int)$order_id);
- $cat_id = $this->get_feed_category($cat_title, $parent_id);
+ Feeds::_add_cat($cat_title, $owner_uid, $parent_id ? $parent_id : null, (int)$order_id);
+ $cat_id = $this->get_feed_category($cat_title, $owner_uid, $parent_id);
}
} else {
@@ -540,21 +564,21 @@ class OPML extends Handler_Protected {
$cat_title = false;
}
- #$this->opml_notice("[CAT] $cat_title id: $cat_id P_id: $parent_id");
- $this->opml_notice(T_sprintf("Processing category: %s", $cat_title ? $cat_title : __("Uncategorized")));
+ //$this->opml_notice("[CAT] $cat_title id: $cat_id P_id: $parent_id");
+ $this->opml_notice(T_sprintf("Processing category: %s", $cat_title ? $cat_title : __("Uncategorized")), $nest);
foreach ($outlines as $node) {
if ($node->hasAttributes() && strtolower($node->tagName) == "outline") {
$attrs = $node->attributes;
- $node_cat_title = $attrs->getNamedItem('text')->nodeValue;
+ $node_cat_title = $attrs->getNamedItem('text') ? $attrs->getNamedItem('text')->nodeValue : false;
if (!$node_cat_title)
- $node_cat_title = $attrs->getNamedItem('title')->nodeValue;
+ $node_cat_title = $attrs->getNamedItem('title') ? $attrs->getNamedItem('title')->nodeValue : false;
- $node_feed_url = $attrs->getNamedItem('xmlUrl')->nodeValue;
+ $node_feed_url = $attrs->getNamedItem('xmlUrl') ? $attrs->getNamedItem('xmlUrl')->nodeValue : false;
if ($node_cat_title && !$node_feed_url) {
- $this->opml_import_category($doc, $node, $owner_uid, $cat_id);
+ $this->opml_import_category($doc, $node, $owner_uid, $cat_id, $nest+1);
} else {
if (!$cat_id) {
@@ -565,91 +589,115 @@ class OPML extends Handler_Protected {
switch ($cat_title) {
case "tt-rss-prefs":
- $this->opml_import_preference($node);
+ $this->opml_import_preference($node, $owner_uid, $nest+1);
break;
case "tt-rss-labels":
- $this->opml_import_label($node, $owner_uid);
+ $this->opml_import_label($node, $owner_uid, $nest+1);
break;
case "tt-rss-filters":
- $this->opml_import_filter($node);
+ $this->opml_import_filter($node, $owner_uid, $nest+1);
break;
default:
- $this->opml_import_feed($node, $dst_cat_id, $owner_uid);
+ $this->opml_import_feed($node, $dst_cat_id, $owner_uid, $nest+1);
}
}
}
}
}
- function opml_import($owner_uid) {
+ /** $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;
$doc = false;
- if ($_FILES['opml_file']['error'] != 0) {
- print_error(T_sprintf("Upload failed with error code %d",
- $_FILES['opml_file']['error']));
- return;
- }
+ if (!$filename) {
+ if ($_FILES['opml_file']['error'] != 0) {
+ print_error(T_sprintf("Upload failed with error code %d",
+ $_FILES['opml_file']['error']));
+ return false;
+ }
- if (is_uploaded_file($_FILES['opml_file']['tmp_name'])) {
- $tmp_file = (string)tempnam(Config::get(Config::CACHE_DIR) . '/upload', 'opml');
+ if (is_uploaded_file($_FILES['opml_file']['tmp_name'])) {
+ $tmp_file = (string)tempnam(Config::get(Config::CACHE_DIR) . '/upload', 'opml');
- $result = move_uploaded_file($_FILES['opml_file']['tmp_name'],
- $tmp_file);
+ $result = move_uploaded_file($_FILES['opml_file']['tmp_name'],
+ $tmp_file);
- if (!$result) {
- print_error(__("Unable to move uploaded file."));
- return;
+ if (!$result) {
+ print_error(__("Unable to move uploaded file."));
+ return false;
+ }
+ } else {
+ print_error(__('Error: please upload OPML file.'));
+ return false;
}
} else {
- print_error(__('Error: please upload OPML file.'));
- return;
+ $tmp_file = $filename;
+ }
+
+ if (!is_readable($tmp_file)) {
+ $this->opml_notice(T_sprintf("Error: file is not readable: %s", $filename));
+ return false;
}
$loaded = false;
- if (is_file($tmp_file)) {
- $doc = new DOMDocument();
+ $doc = new DOMDocument();
+
+ if (version_compare(PHP_VERSION, '8.0.0', '<')) {
libxml_disable_entity_loader(false);
- $loaded = $doc->load($tmp_file);
+ }
+
+ $loaded = $doc->load($tmp_file);
+
+ if (version_compare(PHP_VERSION, '8.0.0', '<')) {
libxml_disable_entity_loader(true);
- unlink($tmp_file);
- } else if (empty($doc)) {
- print_error(__('Error: unable to find moved OPML file.'));
- return;
}
+ // only remove temporary i.e. HTTP uploaded files
+ if (!$filename)
+ unlink($tmp_file);
+
if ($loaded) {
- $this->pdo->beginTransaction();
- $this->opml_import_category($doc, false, $owner_uid, false);
- $this->pdo->commit();
+ // we're using ORM while importing so we can't transaction-lock things anymore
+ //$this->pdo->beginTransaction();
+ $this->opml_import_category($doc, null, $owner_uid, 0, 0);
+ //$this->pdo->commit();
} else {
- print_error(__('Error while parsing document.'));
+ $this->opml_notice(__('Error while parsing document.'));
+ return false;
}
- }
- private function opml_notice($msg) {
- print "$msg<br/>";
+ return true;
}
- function get_feed_category($feed_cat, $parent_cat_id = false) {
+ private function opml_notice(string $msg, int $prefix_length = 0): void {
+ if (php_sapi_name() == "cli") {
+ Debug::log(str_repeat(" ", $prefix_length) . $msg);
+ } else {
+ // TODO: use better separator i.e. CSS-defined span of certain width or something
+ print str_repeat("&nbsp;&nbsp;&nbsp;", $prefix_length) . $msg . "<br/>";
+ }
+ }
- $parent_cat_id = (int) $parent_cat_id;
+ function get_feed_category(string $feed_cat, int $owner_uid, int $parent_cat_id) : int {
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories
WHERE title = :title
AND (parent_cat = :parent OR (:parent = 0 AND parent_cat IS NULL))
AND owner_uid = :uid");
- $sth->execute([':title' => $feed_cat, ':parent' => $parent_cat_id, ':uid' => $_SESSION['uid']]);
+ $sth->execute([':title' => $feed_cat, ':parent' => $parent_cat_id, ':uid' => $owner_uid]);
if ($row = $sth->fetch()) {
return $row['id'];
} else {
- return false;
+ return 0;
}
}
-
}
diff --git a/classes/plugin.php b/classes/plugin.php
index ecafa7888..39af6a9a1 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,602 @@ 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. Generated markup must be valid XML.
+ * @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. Generated markup must be valid XML.
+ * @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 [];
+ }
+
+ /** This is run periodically by the update daemon when idle (available both to user and system plugins)
+ * @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
+ * @deprecated removed, see Plugin::hook_headline_toolbar_select_menu_item2()
+ * @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 "";
+ }
+
+ /** Allows adding custom elements to headlines Select... select dropdown (<option> format)
+ * @param int $feed_id
+ * @param int $is_cat
+ * @return string
+ * @see PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2
+ */
+ function hook_headline_toolbar_select_menu_item2($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;
+ }
+
+ /** Invoked after user logout, may override built-in behavior (redirect back to login page)
+ * @param string $login
+ * @param int $user_id
+ * @return array<mixed> - [0] - if set, url to redirect to
+ */
+ function hook_post_logout($login, $user_id) {
+ return [""];
+ }
}
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 ee4107ae7..952d4df77 100755
--- a/classes/pluginhost.php
+++ b/classes/pluginhost.php
@@ -1,90 +1,219 @@
<?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 $last_registered;
- 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
-
- const HOOK_ARTICLE_BUTTON = "hook_article_button"; // hook_article_button($line)
- const HOOK_ARTICLE_FILTER = "hook_article_filter"; // hook_article_filter($article)
- const HOOK_PREFS_TAB = "hook_prefs_tab"; // hook_prefs_tab($tab)
- const HOOK_PREFS_TAB_SECTION = "hook_prefs_tab_section"; // hook_prefs_tab_section($section)
- const HOOK_PREFS_TABS = "hook_prefs_tabs"; // hook_prefs_tabs()
- const HOOK_FEED_PARSED = "hook_feed_parsed"; // hook_feed_parsed($parser, $feed_id)
- const HOOK_UPDATE_TASK = "hook_update_task"; //*1 // GLOBAL: hook_update_task($cli_options)
- const HOOK_AUTH_USER = "hook_auth_user"; // hook_auth_user($login, $password, $service) (byref)
- const HOOK_HOTKEY_MAP = "hook_hotkey_map"; // hook_hotkey_map($hotkeys) (byref)
- const HOOK_RENDER_ARTICLE = "hook_render_article"; // hook_render_article($article)
- const HOOK_RENDER_ARTICLE_CDM = "hook_render_article_cdm"; // hook_render_article_cdm($article)
- const HOOK_FEED_FETCHED = "hook_feed_fetched"; // hook_feed_fetched($feed_data, $fetch_url, $owner_uid, $feed) (byref)
- const HOOK_SANITIZE = "hook_sanitize"; // hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) (byref)
- const HOOK_RENDER_ARTICLE_API = "hook_render_article_api"; // hook_render_article_api($params)
- const HOOK_TOOLBAR_BUTTON = "hook_toolbar_button"; // hook_toolbar_button()
- const HOOK_ACTION_ITEM = "hook_action_item"; // hook_action_item()
- const HOOK_HEADLINE_TOOLBAR_BUTTON = "hook_headline_toolbar_button"; // hook_headline_toolbar_button($feed_id, $is_cat)
- const HOOK_HOTKEY_INFO = "hook_hotkey_info"; // hook_hotkey_info($hotkeys) (byref)
- const HOOK_ARTICLE_LEFT_BUTTON = "hook_article_left_button"; // hook_article_left_button($row)
- const HOOK_PREFS_EDIT_FEED = "hook_prefs_edit_feed"; // hook_prefs_edit_feed($feed_id)
- const HOOK_PREFS_SAVE_FEED = "hook_prefs_save_feed"; // hook_prefs_save_feed($feed_id)
- const HOOK_FETCH_FEED = "hook_fetch_feed"; // hook_fetch_feed($feed_data, $fetch_url, $owner_uid, $feed, $last_article_timestamp, $auth_login, $auth_pass) (byref)
- const HOOK_QUERY_HEADLINES = "hook_query_headlines"; // hook_query_headlines($row) (byref)
- const HOOK_HOUSE_KEEPING = "hook_house_keeping"; //*1 // GLOBAL: hook_house_keeping()
- const HOOK_SEARCH = "hook_search"; // hook_search($query)
- const HOOK_FORMAT_ENCLOSURES = "hook_format_enclosures"; // hook__format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images) (byref)
- const HOOK_SUBSCRIBE_FEED = "hook_subscribe_feed"; // hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) (byref)
- const HOOK_HEADLINES_BEFORE = "hook_headlines_before"; // hook_headlines_before($feed, $is_cat, $qfh_ret)
- const HOOK_RENDER_ENCLOSURE = "hook_render_enclosure"; // hook_render_enclosure($entry, $id, $rv)
- const HOOK_ARTICLE_FILTER_ACTION = "hook_article_filter_action"; // hook_article_filter_action($article, $action)
- const HOOK_ARTICLE_EXPORT_FEED = "hook_article_export_feed"; // hook_article_export_feed($line, $feed, $is_cat, $owner_uid) (byref)
- const HOOK_MAIN_TOOLBAR_BUTTON = "hook_main_toolbar_button"; // hook_main_toolbar_button()
- const HOOK_ENCLOSURE_ENTRY = "hook_enclosure_entry"; // hook_enclosure_entry($entry, $id, $rv) (byref)
- const HOOK_FORMAT_ARTICLE = "hook_format_article"; // hook_format_article($html, $row)
- const HOOK_FORMAT_ARTICLE_CDM = "hook_format_article_cdm"; /* RIP */
- const HOOK_FEED_BASIC_INFO = "hook_feed_basic_info"; // hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed_id, $auth_login, $auth_pass) (byref)
- const HOOK_SEND_LOCAL_FILE = "hook_send_local_file"; // hook_send_local_file($filename)
- const HOOK_UNSUBSCRIBE_FEED = "hook_unsubscribe_feed"; // hook_unsubscribe_feed($feed_id, $owner_uid)
- const HOOK_SEND_MAIL = "hook_send_mail"; // hook_send_mail(Mailer $mailer, $params)
- const HOOK_FILTER_TRIGGERED = "hook_filter_triggered"; // hook_filter_triggered($feed_id, $owner_uid, $article, $matched_filters, $matched_rules, $article_filters)
- const HOOK_GET_FULL_TEXT = "hook_get_full_text"; // hook_get_full_text($url)
- const HOOK_ARTICLE_IMAGE = "hook_article_image"; // hook_article_image($enclosures, $content, $site_url)
- const HOOK_FEED_TREE = "hook_feed_tree"; // hook_feed_tree()
- const HOOK_IFRAME_WHITELISTED = "hook_iframe_whitelisted"; // hook_iframe_whitelisted($url)
- const HOOK_ENCLOSURE_IMPORTED = "hook_enclosure_imported"; // hook_enclosure_imported($enclosure, $feed)
- const HOOK_HEADLINES_CUSTOM_SORT_MAP = "hook_headlines_custom_sort_map"; // hook_headlines_custom_sort_map()
+ /** @see Plugin::hook_article_button() */
+ const HOOK_ARTICLE_BUTTON = "hook_article_button";
+
+ /** @see Plugin::hook_article_filter() */
+ const HOOK_ARTICLE_FILTER = "hook_article_filter";
+
+ /** @see Plugin::hook_prefs_tab() */
+ const HOOK_PREFS_TAB = "hook_prefs_tab";
+
+ /** @see Plugin::hook_prefs_tab_section() */
+ const HOOK_PREFS_TAB_SECTION = "hook_prefs_tab_section";
+
+ /** @see Plugin::hook_prefs_tabs() */
+ const HOOK_PREFS_TABS = "hook_prefs_tabs";
+
+ /** @see Plugin::hook_feed_parsed() */
+ const HOOK_FEED_PARSED = "hook_feed_parsed";
+
+ /** @see Plugin::hook_update_task() */
+ const HOOK_UPDATE_TASK = "hook_update_task"; //*1
+
+ /** @see Plugin::hook_auth_user() */
+ const HOOK_AUTH_USER = "hook_auth_user";
+
+ /** @see Plugin::hook_hotkey_map() */
+ const HOOK_HOTKEY_MAP = "hook_hotkey_map";
+
+ /** @see Plugin::hook_render_article() */
+ const HOOK_RENDER_ARTICLE = "hook_render_article";
+
+ /** @see Plugin::hook_render_article_cdm() */
+ const HOOK_RENDER_ARTICLE_CDM = "hook_render_article_cdm";
+
+ /** @see Plugin::hook_feed_fetched() */
+ const HOOK_FEED_FETCHED = "hook_feed_fetched";
+
+ /** @see Plugin::hook_sanitize() */
+ const HOOK_SANITIZE = "hook_sanitize";
+
+ /** @see Plugin::hook_render_article_api() */
+ const HOOK_RENDER_ARTICLE_API = "hook_render_article_api";
+
+ /** @see Plugin::hook_toolbar_button() */
+ const HOOK_TOOLBAR_BUTTON = "hook_toolbar_button";
+
+ /** @see Plugin::hook_action_item() */
+ const HOOK_ACTION_ITEM = "hook_action_item";
+
+ /** @see Plugin::hook_headline_toolbar_button() */
+ const HOOK_HEADLINE_TOOLBAR_BUTTON = "hook_headline_toolbar_button";
+
+ /** @see Plugin::hook_hotkey_info() */
+ const HOOK_HOTKEY_INFO = "hook_hotkey_info";
+
+ /** @see Plugin::hook_article_left_button() */
+ const HOOK_ARTICLE_LEFT_BUTTON = "hook_article_left_button";
+
+ /** @see Plugin::hook_prefs_edit_feed() */
+ const HOOK_PREFS_EDIT_FEED = "hook_prefs_edit_feed";
+
+ /** @see Plugin::hook_prefs_save_feed() */
+ const HOOK_PREFS_SAVE_FEED = "hook_prefs_save_feed";
+
+ /** @see Plugin::hook_fetch_feed() */
+ const HOOK_FETCH_FEED = "hook_fetch_feed";
+
+ /** @see Plugin::hook_query_headlines() */
+ const HOOK_QUERY_HEADLINES = "hook_query_headlines";
+
+ /** @see Plugin::hook_house_keeping() */
+ const HOOK_HOUSE_KEEPING = "hook_house_keeping"; //*1
+
+ /** @see Plugin::hook_search() */
+ const HOOK_SEARCH = "hook_search";
+
+ /** @see Plugin::hook_format_enclosures() */
+ const HOOK_FORMAT_ENCLOSURES = "hook_format_enclosures";
+
+ /** @see Plugin::hook_subscribe_feed() */
+ const HOOK_SUBSCRIBE_FEED = "hook_subscribe_feed";
+
+ /** @see Plugin::hook_headlines_before() */
+ const HOOK_HEADLINES_BEFORE = "hook_headlines_before";
+
+ /** @see Plugin::hook_render_enclosure() */
+ const HOOK_RENDER_ENCLOSURE = "hook_render_enclosure";
+
+ /** @see Plugin::hook_article_filter_action() */
+ const HOOK_ARTICLE_FILTER_ACTION = "hook_article_filter_action";
+
+ /** @see Plugin::hook_article_export_feed() */
+ const HOOK_ARTICLE_EXPORT_FEED = "hook_article_export_feed";
+
+ /** @see Plugin::hook_main_toolbar_button() */
+ const HOOK_MAIN_TOOLBAR_BUTTON = "hook_main_toolbar_button";
+
+ /** @see Plugin::hook_enclosure_entry() */
+ const HOOK_ENCLOSURE_ENTRY = "hook_enclosure_entry";
+
+ /** @see Plugin::hook_format_article() */
+ const HOOK_FORMAT_ARTICLE = "hook_format_article";
+
+ /** @see Plugin::hook_format_article_cdm() */
+ const HOOK_FORMAT_ARTICLE_CDM = "hook_format_article_cdm";
+
+ /** @see Plugin::hook_feed_basic_info() */
+ const HOOK_FEED_BASIC_INFO = "hook_feed_basic_info";
+
+ /** @see Plugin::hook_send_local_file() */
+ const HOOK_SEND_LOCAL_FILE = "hook_send_local_file";
+
+ /** @see Plugin::hook_unsubscribe_feed() */
+ const HOOK_UNSUBSCRIBE_FEED = "hook_unsubscribe_feed";
+
+ /** @see Plugin::hook_send_mail() */
+ const HOOK_SEND_MAIL = "hook_send_mail";
+
+ /** @see Plugin::hook_filter_triggered() */
+ const HOOK_FILTER_TRIGGERED = "hook_filter_triggered";
+
+ /** @see Plugin::hook_get_full_text() */
+ const HOOK_GET_FULL_TEXT = "hook_get_full_text";
+
+ /** @see Plugin::hook_article_image() */
+ const HOOK_ARTICLE_IMAGE = "hook_article_image";
+
+ /** @see Plugin::hook_feed_tree() */
+ const HOOK_FEED_TREE = "hook_feed_tree";
+
+ /** @see Plugin::hook_iframe_whitelisted() */
+ const HOOK_IFRAME_WHITELISTED = "hook_iframe_whitelisted";
+
+ /** @see Plugin::hook_enclosure_imported() */
+ const HOOK_ENCLOSURE_IMPORTED = "hook_enclosure_imported";
+
+ /** @see Plugin::hook_headlines_custom_sort_map() */
+ const HOOK_HEADLINES_CUSTOM_SORT_MAP = "hook_headlines_custom_sort_map";
+
+ /** @see Plugin::hook_headlines_custom_sort_override() */
const HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE = "hook_headlines_custom_sort_override";
- // hook_headlines_custom_sort_override($order)
+
+ /** @see Plugin::hook_headline_toolbar_select_menu_item()
+ * @deprecated removed, see PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2
+ */
const HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM = "hook_headline_toolbar_select_menu_item";
- // hook_headline_toolbar_select_menu_item($feed_id, $is_cat)
+
+ /** @see Plugin::hook_headline_toolbar_select_menu_item() */
+ const HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2 = "hook_headline_toolbar_select_menu_item2";
+
+ /** @see Plugin::hook_pre_subscribe() */
+ const HOOK_PRE_SUBSCRIBE = "hook_pre_subscribe";
+
+ /** @see Plugin::hook_post_logout() */
+ const HOOK_POST_LOGOUT = "hook_post_logout";
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() {
@@ -98,18 +227,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() {
+ /** needed for compatibility with API 1 */
+ function get_link(): bool {
return false;
}
- // needed for compatibility with API 2 (?)
- function get_dbh() {
+ /** needed for compatibility with API 2 (?) */
+ function get_dbh(): bool {
return false;
}
@@ -117,8 +246,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));
@@ -127,18 +259,26 @@ 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) {
- $method = strtolower($hook);
+ /**
+ * @param PluginHost::HOOK_* $hook
+ * @param mixed $args
+ */
+ function run_hooks(string $hook, ...$args): void {
- foreach ($this->get_hooks($hook) as $plugin) {
+ $method = strtolower((string)$hook);
+
+ foreach ($this->get_hooks((string)$hook) as $plugin) {
//Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE);
try {
@@ -151,10 +291,15 @@ class PluginHost {
}
}
- function run_hooks_until(string $hook, $check, ...$args) {
- $method = strtolower($hook);
+ /**
+ * @param PluginHost::HOOK_* $hook
+ * @param mixed $args
+ * @param mixed $check
+ */
+ function run_hooks_until(string $hook, $check, ...$args): bool {
+ $method = strtolower((string)$hook);
- foreach ($this->get_hooks($hook) as $plugin) {
+ foreach ($this->get_hooks((string)$hook) as $plugin) {
try {
$result = $plugin->$method(...$args);
@@ -171,10 +316,14 @@ class PluginHost {
return false;
}
- function run_hooks_callback(string $hook, Closure $callback, ...$args) {
- $method = strtolower($hook);
+ /**
+ * @param PluginHost::HOOK_* $hook
+ * @param mixed $args
+ */
+ function run_hooks_callback(string $hook, Closure $callback, ...$args): void {
+ $method = strtolower((string)$hook);
- foreach ($this->get_hooks($hook) as $plugin) {
+ foreach ($this->get_hooks((string)$hook) as $plugin) {
//Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE);
try {
@@ -188,10 +337,14 @@ class PluginHost {
}
}
- function chain_hooks_callback(string $hook, Closure $callback, &...$args) {
- $method = strtolower($hook);
+ /**
+ * @param PluginHost::HOOK_* $hook
+ * @param mixed $args
+ */
+ function chain_hooks_callback(string $hook, Closure $callback, &...$args): void {
+ $method = strtolower((string)$hook);
- foreach ($this->get_hooks($hook) as $plugin) {
+ foreach ($this->get_hooks((string)$hook) as $plugin) {
//Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE);
try {
@@ -205,10 +358,13 @@ 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))) {
+ if (!method_exists($sender, strtolower((string)$type))) {
user_error(
sprintf("Plugin %s tried to register a hook without implementation: %s",
get_class($sender), $type),
@@ -229,7 +385,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]);
@@ -241,6 +400,10 @@ class PluginHost {
}
}
+ /**
+ * @param PluginHost::HOOK_* $type
+ * @return array<int, Plugin>
+ */
function get_hooks(string $type) {
if (isset($this->hooks[$type])) {
$tmp = [];
@@ -250,11 +413,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");
@@ -262,10 +428,13 @@ class PluginHost {
asort($plugins);
- $this->load(join(",", $plugins), $kind, $owner_uid, $skip_init);
+ $this->load(join(",", $plugins), (int)$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;
@@ -285,8 +454,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 {
- if (file_exists($file)) require_once $file;
+ $_SESSION["plugin_blacklist"][$class] = 1;
+ require_once $file;
+ unset($_SESSION["plugin_blacklist"][$class]);
+
} catch (Error $err) {
user_error($err, E_USER_WARNING);
continue;
@@ -307,8 +496,6 @@ class PluginHost {
_bind_textdomain_codeset($class, "UTF-8");
}
- $this->last_registered = $class;
-
try {
switch ($kind) {
case $this::KIND_SYSTEM:
@@ -340,27 +527,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);
@@ -369,7 +556,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);
@@ -384,7 +574,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,
@@ -393,27 +583,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);
@@ -422,7 +619,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 = ?");
@@ -436,7 +633,7 @@ class PluginHost {
}
}
- private function save_data(string $plugin) {
+ private function save_data(string $plugin): void {
if ($this->owner_uid) {
if (!$this->pdo_data)
@@ -449,7 +646,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]);
@@ -469,8 +666,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) {
@@ -488,26 +689,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;
@@ -515,7 +722,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;
@@ -535,6 +747,10 @@ class PluginHost {
}
}
+ /**
+ * @param mixed $default_value
+ * @return mixed
+ */
function get(Plugin $sender, string $name, $default_value = false) {
$idx = get_class($sender);
@@ -547,6 +763,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);
@@ -555,13 +775,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);
@@ -576,7 +799,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] = [];
@@ -589,12 +812,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) {
@@ -602,46 +828,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(
@@ -664,8 +898,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(
@@ -674,18 +912,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 5f7635736..b90ce49b4 100755
--- a/classes/pref/feeds.php
+++ b/classes/pref/feeds.php
@@ -5,29 +5,25 @@ 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() {
- $rv = [];
-
- if (Config::get(Config::DB_TYPE) == "pgsql") {
- $dbh = Db::pdo();
-
- $res = $dbh->query("SELECT cfgname FROM pg_ts_config");
-
- while ($row = $res->fetch()) {
- array_push($rv, ucfirst($row['cfgname']));
- }
+ /**
+ * @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'));
}
- return $rv;
+ return [];
}
- function renameCat() {
+ function renameCat(): void {
$cat = ORM::for_table("ttrss_feed_categories")
->where("owner_uid", $_SESSION["uid"])
->find_one($_REQUEST['id']);
@@ -40,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"] ?? "";
@@ -48,74 +47,76 @@ class Pref_Feeds extends Handler_Protected {
$search = "";
// first one is set by API
- $show_empty_cats = clean($_REQUEST['force_show_empty'] ?? false) ||
+ $show_empty_cats = self::_param_to_bool($_REQUEST['force_show_empty'] ?? false) ||
(clean($_REQUEST['mode'] ?? 0) != 2 && !$search);
- $items = array();
-
- $sth = $this->pdo->prepare("SELECT id, title FROM ttrss_feed_categories
- WHERE owner_uid = ? AND parent_cat = ? ORDER BY order_id, title");
- $sth->execute([$_SESSION['uid'], $cat_id]);
-
- while ($line = $sth->fetch()) {
-
- $cat = array();
- $cat['id'] = 'CAT:' . $line['id'];
- $cat['bare_id'] = (int)$line['id'];
- $cat['name'] = $line['title'];
- $cat['items'] = array();
- $cat['checkbox'] = false;
- $cat['type'] = 'category';
- $cat['unread'] = -1;
- $cat['child_unread'] = -1;
- $cat['auxcounter'] = -1;
- $cat['parent_id'] = $cat_id;
-
- $cat['items'] = $this->get_category_items($line['id']);
+ $items = [];
+
+ $feed_categories = ORM::for_table('ttrss_feed_categories')
+ ->select_many('id', 'title')
+ ->where(['owner_uid' => $_SESSION['uid'], 'parent_cat' => $cat_id])
+ ->order_by_asc('order_id')
+ ->order_by_asc('title')
+ ->find_many();
+
+ foreach ($feed_categories as $feed_category) {
+ $cat = [
+ 'id' => 'CAT:' . $feed_category->id,
+ 'bare_id' => (int)$feed_category->id,
+ 'name' => $feed_category->title,
+ 'items' => $this->get_category_items($feed_category->id),
+ 'checkbox' => false,
+ 'type' => 'category',
+ 'unread' => -1,
+ 'child_unread' => -1,
+ 'auxcounter' => -1,
+ 'parent_id' => $cat_id,
+ ];
$num_children = $this->calculate_children_count($cat);
$cat['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', (int) $num_children), $num_children);
if ($num_children > 0 || $show_empty_cats)
array_push($items, $cat);
+ }
+
+ $feeds_obj = ORM::for_table('ttrss_feeds')
+ ->select_many('id', 'title', 'last_error', 'update_interval')
+ ->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated')
+ ->where(['cat_id' => $cat_id, 'owner_uid' => $_SESSION['uid']])
+ ->order_by_asc('order_id')
+ ->order_by_asc('title');
+ if ($search) {
+ $feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]);
}
- $fsth = $this->pdo->prepare("SELECT id, title, last_error,
- ".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated, update_interval
- FROM ttrss_feeds
- WHERE cat_id = :cat AND
- owner_uid = :uid AND
- (:search = '' OR (LOWER(title) LIKE :search OR LOWER(feed_url) LIKE :search))
- ORDER BY order_id, title");
-
- $fsth->execute([":cat" => $cat_id, ":uid" => $_SESSION['uid'], ":search" => $search ? "%$search%" : ""]);
-
- while ($feed_line = $fsth->fetch()) {
- $feed = array();
- $feed['id'] = 'FEED:' . $feed_line['id'];
- $feed['bare_id'] = (int)$feed_line['id'];
- $feed['auxcounter'] = -1;
- $feed['name'] = $feed_line['title'];
- $feed['checkbox'] = false;
- $feed['unread'] = -1;
- $feed['error'] = $feed_line['last_error'];
- $feed['icon'] = Feeds::_get_icon($feed_line['id']);
- $feed['param'] = TimeHelper::make_local_datetime(
- $feed_line['last_updated'], true);
- $feed['updates_disabled'] = (int)($feed_line['update_interval'] < 0);
-
- array_push($items, $feed);
+ foreach ($feeds_obj->find_many() as $feed) {
+ array_push($items, [
+ 'id' => 'FEED:' . $feed->id,
+ 'bare_id' => (int) $feed->id,
+ 'auxcounter' => -1,
+ 'name' => $feed->title,
+ 'checkbox' => false,
+ 'unread' => -1,
+ 'error' => $feed->last_error,
+ 'icon' => Feeds::_get_icon($feed->id),
+ 'param' => TimeHelper::make_local_datetime($feed->last_updated, true),
+ 'updates_disabled' => (int)($feed->update_interval < 0),
+ ]);
}
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"] ?? "";
@@ -181,24 +182,23 @@ class Pref_Feeds extends Handler_Protected {
if (get_pref(Prefs::ENABLE_FEED_CATS)) {
$cat = $this->feedlist_init_cat(-2);
} else {
- $cat['items'] = array();
+ $cat['items'] = [];
}
- $num_labels = 0;
- while ($line = $sth->fetch()) {
- ++$num_labels;
-
- $label_id = Labels::label_to_feed_id($line['id']);
-
- $feed = $this->feedlist_init_feed($label_id, false, 0);
-
- $feed['fg_color'] = $line['fg_color'];
- $feed['bg_color'] = $line['bg_color'];
-
- array_push($cat['items'], $feed);
- }
+ $labels = ORM::for_table('ttrss_labels2')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->order_by_asc('caption')
+ ->find_many();
+
+ if (count($labels)) {
+ foreach ($labels as $label) {
+ $label_id = Labels::label_to_feed_id($label->id);
+ $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);
+ }
- if ($num_labels) {
if ($enable_cats) {
array_push($root['items'], $cat);
} else {
@@ -208,26 +208,29 @@ class Pref_Feeds extends Handler_Protected {
}
if ($enable_cats) {
- $show_empty_cats = clean($_REQUEST['force_show_empty'] ?? false) ||
+ $show_empty_cats = self::_param_to_bool($_REQUEST['force_show_empty'] ?? false) ||
(clean($_REQUEST['mode'] ?? 0) != 2 && !$search);
- $sth = $this->pdo->prepare("SELECT id, title FROM ttrss_feed_categories
- WHERE owner_uid = ? AND parent_cat IS NULL ORDER BY order_id, title");
- $sth->execute([$_SESSION['uid']]);
-
- while ($line = $sth->fetch()) {
- $cat = array();
- $cat['id'] = 'CAT:' . $line['id'];
- $cat['bare_id'] = (int)$line['id'];
- $cat['auxcounter'] = -1;
- $cat['name'] = $line['title'];
- $cat['items'] = array();
- $cat['checkbox'] = false;
- $cat['type'] = 'category';
- $cat['unread'] = -1;
- $cat['child_unread'] = -1;
-
- $cat['items'] = $this->get_category_items($line['id']);
+ $feed_categories = ORM::for_table('ttrss_feed_categories')
+ ->select_many('id', 'title')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->where_null('parent_cat')
+ ->order_by_asc('order_id')
+ ->order_by_asc('title')
+ ->find_many();
+
+ foreach ($feed_categories as $feed_category) {
+ $cat = [
+ 'id' => 'CAT:' . $feed_category->id,
+ 'bare_id' => (int) $feed_category->id,
+ 'auxcounter' => -1,
+ 'name' => $feed_category->title,
+ 'items' => $this->get_category_items($feed_category->id),
+ 'checkbox' => false,
+ 'type' => 'category',
+ 'unread' => -1,
+ 'child_unread' => -1,
+ ];
$num_children = $this->calculate_children_count($cat);
$cat['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', (int) $num_children), $num_children);
@@ -235,47 +238,48 @@ class Pref_Feeds extends Handler_Protected {
if ($num_children > 0 || $show_empty_cats)
array_push($root['items'], $cat);
- $root['param'] += count($cat['items']);
+ //$root['param'] += count($cat['items']);
}
/* Uncategorized is a special case */
+ $cat = [
+ 'id' => 'CAT:0',
+ 'bare_id' => 0,
+ 'auxcounter' => -1,
+ 'name' => __('Uncategorized'),
+ 'items' => [],
+ 'type' => 'category',
+ 'checkbox' => false,
+ 'unread' => -1,
+ 'child_unread' => -1,
+ ];
+
+ $feeds_obj = ORM::for_table('ttrss_feeds')
+ ->select_many('id', 'title', 'last_error', 'update_interval')
+ ->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->where_null('cat_id')
+ ->order_by_asc('order_id')
+ ->order_by_asc('title');
- $cat = array();
- $cat['id'] = 'CAT:0';
- $cat['bare_id'] = 0;
- $cat['auxcounter'] = -1;
- $cat['name'] = __("Uncategorized");
- $cat['items'] = array();
- $cat['type'] = 'category';
- $cat['checkbox'] = false;
- $cat['unread'] = -1;
- $cat['child_unread'] = -1;
-
- $fsth = $this->pdo->prepare("SELECT id, title,last_error,
- ".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated, update_interval
- FROM ttrss_feeds
- WHERE cat_id IS NULL AND
- owner_uid = :uid AND
- (:search = '' OR (LOWER(title) LIKE :search OR LOWER(feed_url) LIKE :search))
- ORDER BY order_id, title");
- $fsth->execute([":uid" => $_SESSION['uid'], ":search" => $search ? "%$search%" : ""]);
-
- while ($feed_line = $fsth->fetch()) {
- $feed = array();
- $feed['id'] = 'FEED:' . $feed_line['id'];
- $feed['bare_id'] = (int)$feed_line['id'];
- $feed['auxcounter'] = -1;
- $feed['name'] = $feed_line['title'];
- $feed['checkbox'] = false;
- $feed['error'] = $feed_line['last_error'];
- $feed['icon'] = Feeds::_get_icon($feed_line['id']);
- $feed['param'] = TimeHelper::make_local_datetime(
- $feed_line['last_updated'], true);
- $feed['unread'] = -1;
- $feed['type'] = 'feed';
- $feed['updates_disabled'] = (int)($feed_line['update_interval'] < 0);
-
- array_push($cat['items'], $feed);
+ if ($search) {
+ $feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]);
+ }
+
+ foreach ($feeds_obj->find_many() as $feed) {
+ array_push($cat['items'], [
+ 'id' => 'FEED:' . $feed->id,
+ 'bare_id' => (int) $feed->id,
+ 'auxcounter' => -1,
+ 'name' => $feed->title,
+ 'checkbox' => false,
+ 'error' => $feed->last_error,
+ 'icon' => Feeds::_get_icon($feed->id),
+ 'param' => TimeHelper::make_local_datetime($feed->last_updated, true),
+ 'unread' => -1,
+ 'type' => 'feed',
+ 'updates_disabled' => (int)($feed->update_interval < 0),
+ ]);
}
$cat['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', count($cat['items'])), count($cat['items']));
@@ -287,61 +291,59 @@ class Pref_Feeds extends Handler_Protected {
$root['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', (int) $num_children), $num_children);
} else {
- $fsth = $this->pdo->prepare("SELECT id, title, last_error,
- ".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated, update_interval
- FROM ttrss_feeds
- WHERE owner_uid = :uid AND
- (:search = '' OR (LOWER(title) LIKE :search OR LOWER(feed_url) LIKE :search))
- ORDER BY order_id, title");
- $fsth->execute([":uid" => $_SESSION['uid'], ":search" => $search ? "%$search%" : ""]);
-
- while ($feed_line = $fsth->fetch()) {
- $feed = array();
- $feed['id'] = 'FEED:' . $feed_line['id'];
- $feed['bare_id'] = (int)$feed_line['id'];
- $feed['auxcounter'] = -1;
- $feed['name'] = $feed_line['title'];
- $feed['checkbox'] = false;
- $feed['error'] = $feed_line['last_error'];
- $feed['icon'] = Feeds::_get_icon($feed_line['id']);
- $feed['param'] = TimeHelper::make_local_datetime(
- $feed_line['last_updated'], true);
- $feed['unread'] = -1;
- $feed['type'] = 'feed';
- $feed['updates_disabled'] = (int)($feed_line['update_interval'] < 0);
-
- array_push($root['items'], $feed);
- }
+ $feeds_obj = ORM::for_table('ttrss_feeds')
+ ->select_many('id', 'title', 'last_error', 'update_interval')
+ ->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->order_by_asc('order_id')
+ ->order_by_asc('title');
- $root['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', count($root['items'])), count($root['items']));
- }
+ if ($search) {
+ $feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]);
+ }
- $fl = array();
- $fl['identifier'] = 'id';
- $fl['label'] = 'name';
+ foreach ($feeds_obj->find_many() as $feed) {
+ array_push($root['items'], [
+ 'id' => 'FEED:' . $feed->id,
+ 'bare_id' => (int) $feed->id,
+ 'auxcounter' => -1,
+ 'name' => $feed->title,
+ 'checkbox' => false,
+ 'error' => $feed->last_error,
+ 'icon' => Feeds::_get_icon($feed->id),
+ 'param' => TimeHelper::make_local_datetime($feed->last_updated, true),
+ 'unread' => -1,
+ 'type' => 'feed',
+ 'updates_disabled' => (int)($feed->update_interval < 0),
+ ]);
+ }
- if (clean($_REQUEST['mode'] ?? 0) != 2) {
- $fl['items'] = array($root);
- } else {
- $fl['items'] = $root['items'];
+ $root['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', count($root['items'])), count($root['items']));
}
- return $fl;
+ return [
+ 'identifier' => 'id',
+ 'label' => 'name',
+ 'items' => clean($_REQUEST['mode'] ?? 0) != 2 ? [$root] : $root['items'],
+ ];
}
- 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++)
@@ -359,10 +361,14 @@ class Pref_Feeds extends Handler_Protected {
$parent_qpart = null;
}
- $sth = $this->pdo->prepare("UPDATE ttrss_feed_categories
- SET parent_cat = ? WHERE id = ? AND
- owner_uid = ?");
- $sth->execute([$parent_qpart, $bare_item_id, $_SESSION['uid']]);
+ $feed_category = ORM::for_table('ttrss_feed_categories')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->find_one($bare_item_id);
+
+ if ($feed_category) {
+ $feed_category->parent_cat = $parent_qpart;
+ $feed_category->save();
+ }
}
$order_id = 1;
@@ -380,22 +386,27 @@ class Pref_Feeds extends Handler_Protected {
if (strpos($id, "FEED") === 0) {
- $cat_id = ($item_id != "root") ? $bare_item_id : null;
-
- $sth = $this->pdo->prepare("UPDATE ttrss_feeds
- SET order_id = ?, cat_id = ?
- WHERE id = ? AND owner_uid = ?");
-
- $sth->execute([$order_id, $cat_id ? $cat_id : null, $bare_id, $_SESSION['uid']]);
+ $feed = ORM::for_table('ttrss_feeds')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->find_one($bare_id);
+ if ($feed) {
+ $feed->order_id = $order_id;
+ $feed->cat_id = ($item_id != "root" && $bare_item_id) ? $bare_item_id : null;
+ $feed->save();
+ }
} else if (strpos($id, "CAT:") === 0) {
$this->process_category_order($data_map, $item['_reference'], $item_id,
$nest_level+1);
- $sth = $this->pdo->prepare("UPDATE ttrss_feed_categories
- SET order_id = ? WHERE id = ? AND
- owner_uid = ?");
- $sth->execute([$order_id, $bare_id, $_SESSION['uid']]);
+ $feed_category = ORM::for_table('ttrss_feed_categories')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->find_one($bare_id);
+
+ if ($feed_category) {
+ $feed_category->order_id = $order_id;
+ $feed_category->save();
+ }
}
}
@@ -404,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']));
@@ -418,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) {
@@ -440,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";
@@ -460,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');
@@ -503,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;
@@ -539,6 +551,8 @@ class Pref_Feeds extends Handler_Protected {
$local_purge_intervals = [ T_nsprintf('%d day', '%d days', $purge_interval, $purge_interval) ];
}
+ $user = ORM::for_table("ttrss_users")->find_one($_SESSION["uid"]);
+
print json_encode([
"feed" => $row,
"cats" => [
@@ -551,6 +565,9 @@ class Pref_Feeds extends Handler_Protected {
"update" => $local_update_intervals,
"purge" => $local_purge_intervals,
],
+ "user" => [
+ "access_level" => $user->access_level
+ ],
"lang" => [
"enabled" => Config::get(Config::DB_TYPE) == "pgsql",
"default" => get_pref(Prefs::DEFAULT_SEARCH_LANGUAGE),
@@ -560,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;
@@ -673,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"]);
@@ -770,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":
@@ -786,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';
}
@@ -837,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()'>".
@@ -980,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'>
@@ -1016,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>
@@ -1036,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'>
@@ -1075,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;
+ $unread = Feeds::_get_counters($feed_id, false, true);
+
+ 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'";
@@ -1120,44 +1134,41 @@ class Pref_Feeds extends Handler_Protected {
$interval_qpart = "DATE_SUB(NOW(), INTERVAL 3 MONTH)";
}
- $sth = $this->pdo->prepare("SELECT ttrss_feeds.title, ttrss_feeds.site_url,
- ttrss_feeds.feed_url, ttrss_feeds.id, MAX(updated) AS last_article
- FROM ttrss_feeds, ttrss_entries, ttrss_user_entries WHERE
- (SELECT MAX(updated) FROM ttrss_entries, ttrss_user_entries WHERE
- ttrss_entries.id = ref_id AND
- ttrss_user_entries.feed_id = ttrss_feeds.id) < $interval_qpart
- AND ttrss_feeds.owner_uid = ? AND
- ttrss_user_entries.feed_id = ttrss_feeds.id AND
- ttrss_entries.id = ref_id
- GROUP BY ttrss_feeds.title, ttrss_feeds.id, ttrss_feeds.site_url, ttrss_feeds.feed_url
- ORDER BY last_article");
- $sth->execute([$_SESSION['uid']]);
-
- $rv = [];
-
- while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
- $row['last_article'] = TimeHelper::make_local_datetime($row['last_article'], false);
- array_push($rv, $row);
+ $inactive_feeds = ORM::for_table('ttrss_feeds')
+ ->table_alias('f')
+ ->select_many('f.id', 'f.title', 'f.site_url', 'f.feed_url')
+ ->select_expr('MAX(e.updated)', 'last_article')
+ ->join('ttrss_user_entries', [ 'ue.feed_id', '=', 'f.id'], 'ue')
+ ->join('ttrss_entries', ['e.id', '=', 'ue.ref_id'], 'e')
+ ->where('f.owner_uid', $_SESSION['uid'])
+ ->where_raw(
+ "(SELECT MAX(ttrss_entries.updated)
+ FROM ttrss_entries
+ JOIN ttrss_user_entries ON ttrss_entries.id = ttrss_user_entries.ref_id
+ WHERE ttrss_user_entries.feed_id = f.id) < $interval_qpart")
+ ->group_by('f.title')
+ ->group_by('f.id')
+ ->group_by('f.site_url')
+ ->group_by('f.feed_url')
+ ->order_by_asc('last_article')
+ ->find_array();
+
+ foreach ($inactive_feeds as $inactive_feed) {
+ $inactive_feed['last_article'] = TimeHelper::make_local_datetime($inactive_feed['last_article'], false);
}
- print json_encode($rv);
+ print json_encode($inactive_feeds);
}
- function feedsWithErrors() {
- $sth = $this->pdo->prepare("SELECT id,title,feed_url,last_error,site_url
- FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
-
- $rv = [];
-
- while ($row = $sth->fetch()) {
- array_push($rv, $row);
- }
-
- print json_encode($rv);
+ 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', '')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->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;
@@ -1198,19 +1209,26 @@ 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']);
$pass = clean($_REQUEST['pass']);
+ $user = ORM::for_table('ttrss_users')->find_one($_SESSION["uid"]);
+
+ // TODO: we should return some kind of error code to frontend here
+ if ($user->access_level == UserHelper::ACCESS_LEVEL_READONLY) {
+ return;
+ }
+
$csth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
WHERE feed_url = ? AND owner_uid = ?");
@@ -1236,22 +1254,22 @@ 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']);
+ $is_cat = self::_param_to_bool($_REQUEST['is_cat'] ?? false);
$new_key = Feeds::_update_access_key($feed_id, $is_cat, $_SESSION["uid"]);
print json_encode(["link" => $new_key]);
}
- function getSharedURL() {
+ function getSharedURL(): void {
$feed_id = clean($_REQUEST['id']);
- $is_cat = clean($_REQUEST['is_cat']) == "true";
+ $is_cat = self::_param_to_bool($_REQUEST['is_cat'] ?? false);
$search = clean($_REQUEST['search']);
$link = Config::get_self_url() . "/public.php?" . http_build_query([
@@ -1268,11 +1286,14 @@ 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) {
- if ($child['type'] ?? '' == 'category') {
+ if (($child['type'] ?? '') == 'category') {
$c += $this->calculate_children_count($child);
} else {
$c += 1;
diff --git a/classes/pref/filters.php b/classes/pref/filters.php
index 29d309dbb..04178f1a6 100755
--- a/classes/pref/filters.php
+++ b/classes/pref/filters.php
@@ -1,20 +1,28 @@
<?php
class Pref_Filters extends Handler_Protected {
- function csrf_ignore($method) {
+ const ACTION_TAG = 4;
+ const ACTION_SCORE = 6;
+ const ACTION_LABEL = 7;
+ const ACTION_PLUGIN = 9;
+ const ACTION_REMOVE_TAG = 10;
+
+ const PARAM_ACTIONS = [self::ACTION_TAG, self::ACTION_SCORE,
+ self::ACTION_LABEL, self::ACTION_PLUGIN, self::ACTION_REMOVE_TAG];
+
+ 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 +48,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 +65,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 +75,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 +86,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 +175,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,
@@ -203,7 +216,7 @@ class Pref_Filters extends Handler_Protected {
} else {
$where = $line["cat_filter"] ?
- Feeds::_get_cat_title($line["cat_id"]) :
+ Feeds::_get_cat_title($line["cat_id"] ?? 0) :
($line["feed_id"] ?
Feeds::_get_title($line["feed_id"]) : __("All feeds"));
}
@@ -222,7 +235,7 @@ class Pref_Filters extends Handler_Protected {
return $rv;
}
- function getfiltertree() {
+ function getfiltertree(): void {
$root = array();
$root['id'] = 'root';
$root['name'] = __('Filters');
@@ -270,7 +283,7 @@ class Pref_Filters extends Handler_Protected {
}
}
- if ($line['action_id'] == 7) {
+ if ($line['action_id'] == self::ACTION_LABEL) {
$label_sth = $this->pdo->prepare("SELECT fg_color, bg_color
FROM ttrss_labels2 WHERE caption = ? AND
owner_uid = ?");
@@ -307,10 +320,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 +418,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 +461,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"]]);
@@ -461,11 +483,7 @@ class Pref_Filters extends Handler_Protected {
$title = __($row["description"]);
- if ($action["action_id"] == 4 || $action["action_id"] == 6 ||
- $action["action_id"] == 7)
- $title .= ": " . $action["action_param"];
-
- if ($action["action_id"] == 9) {
+ if ($action["action_id"] == self::ACTION_PLUGIN) {
list ($pfclass, $pfaction) = explode(":", $action["action_param"]);
$filter_actions = PluginHost::getInstance()->get_filter_actions();
@@ -478,18 +496,20 @@ class Pref_Filters extends Handler_Protected {
}
}
}
+ } else if (in_array($action["action_id"], self::PARAM_ACTIONS)) {
+ $title .= ": " . $action["action_param"];
}
}
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 +530,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 +540,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]);
@@ -583,21 +603,26 @@ class Pref_Filters extends Handler_Protected {
$action_param = $action["action_param"];
$action_param_label = $action["action_param_label"];
- if ($action_id == 7) {
+ if ($action_id == self::ACTION_LABEL) {
$action_param = $action_param_label;
}
- if ($action_id == 6) {
+ if ($action_id == self::ACTION_SCORE) {
$action_param = (int)str_replace("+", "", $action_param);
}
+ if (in_array($action_id, [self::ACTION_TAG, self::ACTION_REMOVE_TAG])) {
+ $action_param = implode(", ", FeedItem_Common::normalize_categories(
+ explode(",", $action_param)));
+ }
+
$asth->execute([$filter_id, $action_id, $action_param]);
}
}
}
}
- 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 +650,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 +716,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 +725,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 +774,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 +805,7 @@ class Pref_Filters extends Handler_Protected {
}
}
- private function _optimize($id) {
+ private function _optimize(int $id): void {
$this->pdo->beginTransaction();
@@ -830,9 +860,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 2cdb919ce..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,9 +157,9 @@ class Pref_Labels extends Handler_Protected {
}
- function add() {
+ function add(): void {
$caption = clean($_REQUEST["caption"]);
- $output = clean($_REQUEST["output"]);
+ $output = clean($_REQUEST["output"] ?? false);
if ($caption) {
if (Labels::create($caption)) {
@@ -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 cb666e945..2d72a7732 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(string $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)));
@@ -813,10 +826,10 @@ class Pref_Prefs extends Handler_Protected {
usort($rv, function($a, $b) { return strcmp($a["name"], $b["name"]); });
- print json_encode(['plugins' => $rv, 'is_admin' => $_SESSION['access_level'] >= 10]);
+ 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">
@@ -890,7 +903,7 @@ class Pref_Prefs extends Handler_Protected {
<?= \Controls\button_tag(\Controls\icon("refresh"), "", ["title" => __("Reload"), "onclick" => "Helpers.Plugins.reload()"]) ?>
- <?php if ($_SESSION["access_level"] >= 10) { ?>
+ <?php if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) { ?>
<?php if (Config::get(Config::CHECK_FOR_UPDATES) && Config::get(Config::CHECK_FOR_PLUGIN_UPDATES)) { ?>
<button class='alt-warning' dojoType='dijit.form.Button' onclick="Helpers.Plugins.update()">
@@ -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,10 +1174,12 @@ class Pref_Prefs extends Handler_Protected {
}
}
}
+
+ return "";
}
- function uninstallPlugin() {
- if ($_SESSION["access_level"] >= 10) {
+ function uninstallPlugin(): void {
+ if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) {
$plugin_name = basename(clean($_REQUEST['plugin']));
$status = 0;
@@ -1166,8 +1193,8 @@ class Pref_Prefs extends Handler_Protected {
}
}
- function installPlugin() {
- if ($_SESSION["access_level"] >= 10 && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
+ 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();
$plugin_dir = dirname(dirname(__DIR__)) . "/plugins.local";
@@ -1251,47 +1278,59 @@ class Pref_Prefs extends Handler_Protected {
}
}
- private function _get_available_plugins() {
- if ($_SESSION["access_level"] >= 10 && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
- return json_decode(UrlHelper::fetch(['url' => 'https://tt-rss.org/plugins.json']), true);
+ /**
+ * @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)) {
+ $content = json_decode(UrlHelper::fetch(['url' => 'https://tt-rss.org/plugins.json']), true);
+
+ if ($content) {
+ return $content;
+ }
}
+
+ return [];
}
- function getAvailablePlugins() {
- if ($_SESSION["access_level"] >= 10) {
+
+ function getAvailablePlugins(): void {
+ if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) {
print json_encode($this->_get_available_plugins());
+ } else {
+ print "[]";
}
}
- function checkForPluginUpdates() {
- if ($_SESSION["access_level"] >= 10 && Config::get(Config::CHECK_FOR_UPDATES) && Config::get(Config::CHECK_FOR_PLUGIN_UPDATES)) {
+ 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() {
- if ($_SESSION["access_level"] >= 10) {
+ 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,21 +1350,21 @@ 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() {
- $id = (int) $_REQUEST['id'] ?? 0;
+ function activateprofile(): void {
+ $id = (int) ($_REQUEST['id'] ?? 0);
$profile = ORM::for_table('ttrss_settings_profiles')
->where('owner_uid', $_SESSION['uid'])
@@ -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..806291c72 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,12 +133,12 @@ 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);
while ($line = $sth->fetch()) {
- foreach ($line as $k => $v) { $line[$k] = htmlspecialchars($v); }
+ foreach ($line as $k => $v) { $line[$k] = htmlspecialchars($v ?? ''); }
?>
<tr>
<td class='errno'>
@@ -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 76a879efd..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
@@ -167,7 +167,7 @@ class Pref_Users extends Handler_Administrative {
$user->created = Db::NOW();
$user->save();
- if ($new_uid = UserHelper::find_user_by_login($login)) {
+ if (!is_null(UserHelper::find_user_by_login($login))) {
print T_sprintf("Added user %s with password %s",
$login, $new_password);
} else {
@@ -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 23a45d951..dbb54cec5 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,11 +105,11 @@ 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"];
- $label_id_count = (int)$_REQUEST["label_id_count"];
+ $feed_id_count = (int) ($_REQUEST["feed_id_count"] ?? -1);
+ $label_id_count = (int) ($_REQUEST["label_id_count"] ?? -1);
// it seems impossible to distinguish empty array [] from a null - both become unset in $_REQUEST
// so, count is >= 0 means we had an array, -1 means null
@@ -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,8 +172,8 @@ class RPC extends Handler_Protected {
"feeds" => Article::_feeds_of($ids)]);
}
- function sanityCheck() {
- $_SESSION["hasSandbox"] = clean($_REQUEST["hasSandbox"]) === "true";
+ function sanityCheck(): void {
+ $_SESSION["hasSandbox"] = self::_param_to_bool($_REQUEST["hasSandbox"] ?? false);
$_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]);
$client_location = $_REQUEST["clientLocation"];
@@ -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";
+ $is_cat = self::_param_to_bool($_REQUEST['is_cat'] ?? false);
$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);
@@ -299,7 +302,8 @@ class RPC extends Handler_Protected {
ttrss_feeds f, ttrss_users u LEFT JOIN ttrss_user_prefs2 p ON
(p.owner_uid = u.id AND profile IS NULL AND pref_name = 'DEFAULT_UPDATE_INTERVAL')
WHERE
- f.owner_uid = u.id
+ f.owner_uid = u.id AND
+ u.access_level NOT IN (".sprintf("%d, %d", UserHelper::ACCESS_LEVEL_DISABLED, UserHelper::ACCESS_LEVEL_READONLY).")
$owner_check_qpart
$update_limit_qpart
$updstart_thresh_qpart
@@ -335,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 = ?");
@@ -360,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 = ?");
@@ -381,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);
@@ -395,7 +405,7 @@ class RPC extends Handler_Protected {
}
}
- function checkforupdates() {
+ function checkforupdates(): void {
$rv = ["changeset" => [], "plugins" => []];
$version = Config::get_version(false);
@@ -403,7 +413,7 @@ class RPC extends Handler_Protected {
$git_timestamp = $version["timestamp"] ?? false;
$git_commit = $version["commit"] ?? false;
- if (Config::get(Config::CHECK_FOR_UPDATES) && $_SESSION["access_level"] >= 10 && $git_timestamp) {
+ if (Config::get(Config::CHECK_FOR_UPDATES) && $_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && $git_timestamp) {
$content = @UrlHelper::fetch(["url" => "https://tt-rss.org/version.json"]);
if ($content) {
@@ -424,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,
@@ -480,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);
@@ -492,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();
@@ -510,7 +526,7 @@ class RPC extends Handler_Protected {
$data['cdm_expanded'] = get_pref(Prefs::CDM_EXPANDED);
$data["labels"] = Labels::get_all($_SESSION["uid"]);
- if (Config::get(Config::LOG_DESTINATION) == 'sql' && $_SESSION['access_level'] >= 10) {
+ if (Config::get(Config::LOG_DESTINATION) == 'sql' && $_SESSION['access_level'] >= UserHelper::ACCESS_LEVEL_ADMIN) {
if (Config::get(Config::DB_TYPE) == 'pgsql') {
$log_interval = "created_at > NOW() - interval '1 hour'";
} else {
@@ -522,6 +538,7 @@ class RPC extends Handler_Protected {
WHERE
errno NOT IN (".E_USER_NOTICE.", ".E_USER_DEPRECATED.") AND
$log_interval AND
+ errstr NOT LIKE '%Returning bool from comparison function is deprecated%' AND
errstr NOT LIKE '%imagecreatefromstring(): Data is not in a recognized format%'");
$sth->execute();
@@ -560,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"),
@@ -640,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",
@@ -726,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 216792a0e..9995b0e43 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) {
@@ -123,7 +129,8 @@ class RSSUtils {
ttrss_feeds f, ttrss_users u LEFT JOIN ttrss_user_prefs2 p ON
(p.owner_uid = u.id AND profile IS NULL AND pref_name = 'DEFAULT_UPDATE_INTERVAL')
WHERE
- f.owner_uid = u.id
+ f.owner_uid = u.id AND
+ u.access_level NOT IN (".sprintf("%d, %d", UserHelper::ACCESS_LEVEL_DISABLED, UserHelper::ACCESS_LEVEL_READONLY).")
$login_thresh_qpart
$update_limit_qpart
$updstart_thresh_qpart
@@ -163,7 +170,8 @@ class RSSUtils {
FROM ttrss_feeds f, ttrss_users u LEFT JOIN ttrss_user_prefs2 p ON
(p.owner_uid = u.id AND profile IS NULL AND pref_name = 'DEFAULT_UPDATE_INTERVAL')
WHERE
- f.owner_uid = u.id
+ f.owner_uid = u.id AND
+ u.access_level NOT IN (".sprintf("%d, %d", UserHelper::ACCESS_LEVEL_DISABLED, UserHelper::ACCESS_LEVEL_READONLY).")
AND feed_url = :feed
$login_thresh_qpart
$update_limit_qpart
@@ -270,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);
@@ -352,6 +360,19 @@ class RSSUtils {
if (!$feed_language) $feed_language = mb_strtolower(get_pref(Prefs::DEFAULT_SEARCH_LANGUAGE, $feed_obj->owner_uid));
if (!$feed_language) $feed_language = 'simple';
+ $user = ORM::for_table('ttrss_users')->find_one($feed_obj->owner_uid);
+
+ if ($user) {
+ if ($user->access_level == UserHelper::ACCESS_LEVEL_READONLY) {
+ Debug::log("error: denied update for $feed: permission denied by owner access level");
+ return false;
+ }
+ } else {
+ // this would indicate database corruption of some kind
+ Debug::log("error: owner not found for feed: $feed");
+ return false;
+ }
+
} else {
Debug::log("error: feeds table record not found for feed: $feed");
return false;
@@ -536,7 +557,7 @@ class RSSUtils {
Debug::log("language: $feed_language", Debug::LOG_VERBOSE);
Debug::log("processing feed data...", Debug::LOG_VERBOSE);
- $site_url = mb_substr(rewrite_relative_url($feed_obj->feed_url, clean($rss->get_link())), 0, 245);
+ $site_url = mb_substr(UrlHelper::rewrite_relative($feed_obj->feed_url, clean($rss->get_link())), 0, 245);
Debug::log("site_url: $site_url", Debug::LOG_VERBOSE);
Debug::log("feed_title: {$rss->get_title()}", Debug::LOG_VERBOSE);
@@ -646,7 +667,7 @@ class RSSUtils {
$entry_title = strip_tags($item->get_title());
- $entry_link = rewrite_relative_url($site_url, clean($item->get_link()));
+ $entry_link = UrlHelper::rewrite_relative($site_url, clean($item->get_link()), "a", "href");
$entry_language = mb_substr(trim($item->get_language()), 0, 2);
@@ -666,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);
@@ -713,8 +734,9 @@ 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),
+ UrlHelper::rewrite_relative($site_url, $e->link, "", "", $e->type),
$e->type, $e->length, $e->title, $e->width, $e->height);
// Yet another episode of "mysql utf8_general_ci is gimped"
@@ -764,13 +786,13 @@ class RSSUtils {
// dupes when the entry gets purged and reinserted again e.g.
// in the case of SLOW SLOW OMG SLOW updating feeds
+ $pdo->commit();
+
$entry_obj = ORM::for_table('ttrss_entries')
->find_one($base_entry_id)
->set('date_updated', Db::NOW())
->save();
- $pdo->commit();
-
continue;
}
@@ -825,7 +847,8 @@ class RSSUtils {
if (count($matched_filter_ids) > 0) {
$filter_objs = ORM::for_table('ttrss_filters2')
->where('owner_uid', $feed_obj->owner_uid)
- ->where_in('id', $matched_filter_ids);
+ ->where_in('id', $matched_filter_ids)
+ ->find_many();
foreach ($filter_objs as $filter_obj) {
$filter_obj->set('last_triggered', Db::NOW());
@@ -898,7 +921,7 @@ class RSSUtils {
$entry_timestamp = time();
}
- $entry_timestamp_fmt = strftime("%Y/%m/%d %H:%M:%S", $entry_timestamp);
+ $entry_timestamp_fmt = date("Y/m/d H:i:s", $entry_timestamp);
Debug::log("date: $entry_timestamp ($entry_timestamp_fmt)", Debug::LOG_VERBOSE);
Debug::log("num_comments: $num_comments", Debug::LOG_VERBOSE);
@@ -1142,32 +1165,30 @@ class RSSUtils {
}
// check for manual tags (we have to do it here since they're loaded from filters)
-
foreach ($article_filters as $f) {
if ($f["type"] == "tag") {
+ $entry_tags = array_merge($entry_tags,
+ FeedItem_Common::normalize_categories(explode(",", $f["param"])));
+ }
+ }
- $manual_tags = array_map('trim', explode(",", mb_strtolower($f["param"])));
-
- foreach ($manual_tags as $tag) {
- array_push($entry_tags, $tag);
- }
+ // like boring tags, but filter-based
+ foreach ($article_filters as $f) {
+ if ($f["type"] == "ignore-tag") {
+ $entry_tags = array_diff($entry_tags,
+ FeedItem_Common::normalize_categories(explode(",", $f["param"])));
}
}
// Skip boring tags
-
- $boring_tags = array_map('trim',
- explode(",", mb_strtolower(
- get_pref(Prefs::BLACKLISTED_TAGS, $feed_obj->owner_uid))));
-
$entry_tags = FeedItem_Common::normalize_categories(
- array_unique(
- array_diff($entry_tags, $boring_tags)));
+ array_diff($entry_tags,
+ FeedItem_Common::normalize_categories(explode(",",
+ get_pref(Prefs::BLACKLISTED_TAGS, $feed_obj->owner_uid)))));
- Debug::log("filtered tags: " . implode(", ", $entry_tags), Debug::LOG_VERBOSE);
+ Debug::log("resulting article tags: " . implode(", ", $entry_tags), Debug::LOG_VERBOSE);
// Save article tags in the database
-
if (count($entry_tags) > 0) {
$tsth = $pdo->prepare("SELECT id FROM ttrss_tags
@@ -1250,15 +1271,21 @@ 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()) {
foreach ($enclosures as $enc) {
if (preg_match("/(image|audio|video)/", $enc[1])) {
- $src = rewrite_relative_url($site_url, $enc[0]);
+ $src = UrlHelper::rewrite_relative($site_url, $enc[0]);
$local_filename = sha1($src);
@@ -1283,8 +1310,8 @@ class RSSUtils {
}
/* TODO: move to DiskCache? */
- static function cache_media_url($cache, $url, $site_url) {
- $url = rewrite_relative_url($site_url, $url);
+ static function cache_media_url(DiskCache $cache, string $url, string $site_url): void {
+ $url = UrlHelper::rewrite_relative($site_url, $url);
$local_filename = sha1($url);
Debug::log("cache_media: checking $url", Debug::LOG_VERBOSE);
@@ -1307,7 +1334,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()) {
@@ -1336,7 +1363,7 @@ class RSSUtils {
}
}
- static function expire_error_log() {
+ static function expire_error_log(): void {
Debug::log("Removing old error log entries...");
$pdo = Db::pdo();
@@ -1350,14 +1377,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;
@@ -1398,7 +1427,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) {
@@ -1482,16 +1519,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) {
@@ -1502,7 +1549,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) {
@@ -1513,7 +1563,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;
@@ -1523,7 +1578,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"])) {
@@ -1533,20 +1592,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();
@@ -1584,7 +1643,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);
@@ -1592,7 +1651,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();
@@ -1608,6 +1667,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";
@@ -1672,11 +1734,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();
@@ -1794,7 +1859,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) {
@@ -1808,14 +1873,14 @@ class RSSUtils {
$base = $xpath->query('/html/head/base[@href]');
foreach ($base as $b) {
- $url = rewrite_relative_url($url, $b->getAttribute("href"));
+ $url = UrlHelper::rewrite_relative($url, $b->getAttribute("href"));
break;
}
$entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
if (count($entries) > 0) {
foreach ($entries as $entry) {
- $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
+ $favicon_url = UrlHelper::rewrite_relative($url, $entry->getAttribute("href"));
break;
}
}
@@ -1823,13 +1888,17 @@ class RSSUtils {
}
if (!$favicon_url)
- $favicon_url = rewrite_relative_url($url, "/favicon.ico");
+ $favicon_url = UrlHelper::rewrite_relative($url, "/favicon.ico");
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(
@@ -1847,7 +1916,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) {
@@ -1857,7 +1929,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 0a444a296..e2055930b 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"];
@@ -68,7 +77,7 @@ class Sanitizer {
// $rewrite_base_url = $site_url ? $site_url : Config::get_self_url();
$rewrite_base_url = $site_url ? $site_url : "http://domain.invalid/";
- $entries = $xpath->query('(//a[@href]|//img[@src]|//source[@srcset|@src])');
+ $entries = $xpath->query('(//a[@href]|//img[@src]|//source[@srcset|@src]|//video[@poster])');
foreach ($entries as $entry) {
@@ -100,6 +109,11 @@ class Sanitizer {
$entry->setAttribute("srcset", RSSUtils::encode_srcset($matches));
}
+ if ($entry->hasAttribute('poster')) {
+ $entry->setAttribute('poster',
+ UrlHelper::rewrite_relative($rewrite_base_url, $entry->getAttribute('poster'), $entry->tagName, "poster"));
+ }
+
if ($entry->hasAttribute('src') &&
($owner && get_pref(Prefs::STRIP_IMAGES, $owner)) || $force_remove_images || ($_SESSION["bw_limit"] ?? false)) {
@@ -178,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 b2c1331b6..83f66a810 100644
--- a/classes/urlhelper.php
+++ b/classes/urlhelper.php
@@ -6,16 +6,40 @@ class UrlHelper {
"tel"
];
+ const EXTRA_SCHEMES_BY_CONTENT_TYPE = [
+ "application/x-bittorrent" => [ "magnet" ],
+ ];
+
+ // 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'];
@@ -33,13 +57,31 @@ class UrlHelper {
* @param string $rel_url Possibly relative URL in the document
* @param string $owner_element Owner element tag name (i.e. "a") (optional)
* @param string $owner_attribute Owner attribute (i.e. "href") (optional)
+ * @param string $content_type URL content type as specified by enclosures, etc.
*
- * @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 = "") {
+ public static function rewrite_relative($base_url,
+ $rel_url,
+ string $owner_element = "",
+ string $owner_attribute = "",
+ string $content_type = "") {
$rel_parts = parse_url($rel_url);
+ if (!$rel_url) return $base_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);
@@ -51,8 +93,13 @@ class UrlHelper {
$owner_element == "a" &&
$owner_attribute == "href") {
return $rel_url;
+ // allow some extra schemes for links with feed-specified content type i.e. enclosures
+ } else if ($content_type &&
+ isset(self::EXTRA_SCHEMES_BY_CONTENT_TYPE[$content_type]) &&
+ in_array($rel_parts["scheme"], self::EXTRA_SCHEMES_BY_CONTENT_TYPE[$content_type])) {
+ return $rel_url;
// allow limited subset of inline base64-encoded images for IMG elements
- } else if ($rel_parts["scheme"] ?? "" == "data" &&
+ } else if (($rel_parts["scheme"] ?? "") == "data" &&
preg_match('%^image/(webp|gif|jpg|png|svg);base64,%', $rel_parts["path"]) &&
$owner_element == "img" &&
$owner_attribute == "src") {
@@ -60,28 +107,36 @@ class UrlHelper {
} else {
$base_parts = parse_url($base_url);
- $rel_parts['host'] = $base_parts['host'];
- $rel_parts['scheme'] = $base_parts['scheme'];
+ $rel_parts['host'] = $base_parts['host'] ?? "";
+ $rel_parts['scheme'] = $base_parts['scheme'] ?? "";
+
+ if ($rel_parts['path'] ?? "") {
- if (isset($rel_parts['path'])) {
+ // we append dirname() of base path to relative URL path as per RFC 3986 section 5.2.2
+ $base_path = with_trailing_slash(dirname($base_parts['path'] ?? ""));
- // experimental: if relative url path is not absolute (i.e. starting with /) concatenate it using base url path
- // (i'm not sure if it's a good idea)
+ // 1. absolute relative path (/test.html) = no-op, proceed as is
- if (strpos($rel_parts['path'], '/') !== 0) {
- $rel_parts['path'] = with_trailing_slash($base_parts['path'] ?? "") . $rel_parts['path'];
+ // 2. dotslash relative URI (./test.html) - strip "./", append base path
+ if (strpos($rel_parts['path'], './') === 0) {
+ $rel_parts['path'] = $base_path . substr($rel_parts['path'], 2);
+ // 3. anything else relative (test.html) - append dirname() of base path
+ } else if (strpos($rel_parts['path'], '/') !== 0) {
+ $rel_parts['path'] = $base_path . $rel_parts['path'];
}
- $rel_parts['path'] = str_replace("/./", "/", $rel_parts['path']);
- $rel_parts['path'] = str_replace("//", "/", $rel_parts['path']);
+ //$rel_parts['path'] = str_replace("/./", "/", $rel_parts['path']);
+ //$rel_parts['path'] = str_replace("//", "/", $rel_parts['path']);
}
return self::validate(self::build_url($rel_parts));
}
}
- // 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 +162,11 @@ class UrlHelper {
} else {
$tokens['host'] = idn_to_ascii($tokens['host']);
}
+
+ // if `idn_to_ascii` failed
+ if ($tokens['host'] === false) {
+ return false;
+ }
}
}
@@ -138,7 +198,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)
@@ -162,8 +225,12 @@ class UrlHelper {
$context = stream_context_create($context_options);
+ // PHP 8 changed the second param from int to bool, but we still support PHP >= 7.1.0
+ // @phpstan-ignore-next-line
$headers = get_headers($url, 0, $context);
} else {
+ // PHP 8 changed the second param from int to bool, but we still support PHP >= 7.1.0
+ // @phpstan-ignore-next-line
$headers = get_headers($url, 0);
}
@@ -185,12 +252,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 = "";
@@ -239,6 +310,8 @@ class UrlHelper {
$url = ltrim($url, ' ');
$url = str_replace(' ', '%20', $url);
+ Debug::log("[UrlHelper] fetching: $url", Debug::LOG_EXTENDED);
+
$url = self::validate($url, true);
if (!$url) {
@@ -275,15 +348,15 @@ class UrlHelper {
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout ? $timeout : Config::get(Config::FILE_FETCH_CONNECT_TIMEOUT));
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ? $timeout : Config::get(Config::FILE_FETCH_TIMEOUT));
- curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("open_basedir") && $followlocation);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $followlocation);
curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
- curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent :
- SELF_USER_AGENT);
+ curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent : Config::get_user_agent());
curl_setopt($ch, CURLOPT_ENCODING, "");
+ curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
if ($http_referrer)
curl_setopt($ch, CURLOPT_REFERER, $http_referrer);
@@ -298,7 +371,7 @@ class UrlHelper {
//Debug::log("[curl progressfunction] $downloaded $max_size", Debug::$LOG_EXTENDED);
if ($downloaded > $max_size) {
- Debug::log("curl: reached max size of $max_size bytes requesting $url, aborting.", Debug::LOG_VERBOSE);
+ Debug::log("[UrlHelper] fetch error: curl reached max size of $max_size bytes downloading $url, aborting.", Debug::LOG_VERBOSE);
return 1;
}
@@ -307,10 +380,6 @@ class UrlHelper {
}
- if (!ini_get("open_basedir")) {
- curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
- }
-
if (Config::get(Config::HTTP_PROXY)) {
curl_setopt($ch, CURLOPT_PROXY, Config::get(Config::HTTP_PROXY));
}
@@ -374,6 +443,8 @@ class UrlHelper {
if (curl_errno($ch) != 0) {
self::$fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
+ } else {
+ self::$fetch_last_error = "HTTP Code: $http_code ";
}
self::$fetch_last_error_content = $contents;
@@ -510,7 +581,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 1cdd320a1..91e40665d 100644
--- a/classes/userhelper.php
+++ b/classes/userhelper.php
@@ -17,7 +17,22 @@ class UserHelper {
self::HASH_ALGO_SHA1
];
- static function authenticate(string $login = null, string $password = null, bool $check_only = false, string $service = null) {
+ /** forbidden to login */
+ const ACCESS_LEVEL_DISABLED = -2;
+
+ /** can't subscribe to new feeds, feeds are not updated */
+ const ACCESS_LEVEL_READONLY = -1;
+
+ /** no restrictions, regular user */
+ const ACCESS_LEVEL_USER = 0;
+
+ /** not used, same as regular user */
+ const ACCESS_LEVEL_POWERUSER = 5;
+
+ /** has administrator permissions */
+ const ACCESS_LEVEL_ADMIN = 10;
+
+ 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;
@@ -41,7 +56,7 @@ class UserHelper {
$user = ORM::for_table('ttrss_users')->find_one($user_id);
- if ($user) {
+ if ($user && $user->access_level != self::ACCESS_LEVEL_DISABLED) {
$_SESSION["uid"] = $user_id;
$_SESSION["auth_module"] = $auth_module;
$_SESSION["name"] = $user->login;
@@ -53,6 +68,8 @@ class UserHelper {
$user->last_login = Db::NOW();
$user->save();
+ $_SESSION["last_login_update"] = time();
+
return true;
}
@@ -68,7 +85,7 @@ class UserHelper {
$_SESSION["uid"] = 1;
$_SESSION["name"] = "admin";
- $_SESSION["access_level"] = 10;
+ $_SESSION["access_level"] = self::ACCESS_LEVEL_ADMIN;
$_SESSION["hide_hello"] = true;
$_SESSION["hide_logout"] = true;
@@ -84,7 +101,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();
@@ -99,7 +116,7 @@ class UserHelper {
}
}
- static function login_sequence() {
+ static function login_sequence(): void {
$pdo = Db::pdo();
if (Config::get(Config::SINGLE_USER_MODE)) {
@@ -144,7 +161,7 @@ class UserHelper {
}
}
- static function print_user_stylesheet() {
+ static function print_user_stylesheet(): void {
$value = get_pref(Prefs::USER_STYLESHEET);
if ($value) {
@@ -155,7 +172,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];
@@ -164,7 +181,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);
@@ -174,7 +191,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();
@@ -185,7 +202,7 @@ class UserHelper {
return null;
}
- static function logout() {
+ static function logout(): void {
if (session_status() === PHP_SESSION_ACTIVE)
session_destroy();
@@ -196,11 +213,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 = "";
@@ -283,7 +300,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) {
@@ -318,7 +335,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 &&
@@ -330,10 +349,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) {