summaryrefslogtreecommitdiff
path: root/classes
diff options
context:
space:
mode:
Diffstat (limited to 'classes')
-rwxr-xr-xclasses/api.php2
-rwxr-xr-xclasses/article.php4
-rw-r--r--classes/config.php14
-rw-r--r--classes/counters.php38
-rwxr-xr-xclasses/db.php8
-rw-r--r--classes/db/migrations.php49
-rw-r--r--classes/debug.php47
-rw-r--r--classes/digest.php4
-rw-r--r--classes/diskcache.php6
-rwxr-xr-xclasses/feeditem/atom.php8
-rwxr-xr-xclasses/feeditem/common.php2
-rwxr-xr-xclasses/feeditem/rss.php2
-rw-r--r--classes/feedparser.php14
-rwxr-xr-xclasses/feeds.php42
-rw-r--r--classes/handler.php6
-rwxr-xr-xclasses/handler/public.php26
-rw-r--r--classes/mailer.php15
-rw-r--r--classes/plugin.php14
-rwxr-xr-xclasses/pluginhost.php75
-rwxr-xr-xclasses/pref/feeds.php16
-rwxr-xr-xclasses/pref/filters.php15
-rw-r--r--classes/pref/prefs.php42
-rw-r--r--classes/prefs.php6
-rwxr-xr-xclasses/rpc.php13
-rwxr-xr-xclasses/rssutils.php39
-rw-r--r--classes/urlhelper.php103
-rw-r--r--classes/userhelper.php160
27 files changed, 435 insertions, 335 deletions
diff --git a/classes/api.php b/classes/api.php
index b17114693..0e873856f 100755
--- a/classes/api.php
+++ b/classes/api.php
@@ -286,7 +286,7 @@ class API extends Handler {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
$field = $set_to $additional_fields
WHERE ref_id IN ($article_qmarks) AND owner_uid = ?");
- $sth->execute(array_merge($article_ids, [$_SESSION['uid']]));
+ $sth->execute([...$article_ids, $_SESSION['uid']]);
$num_updated = $sth->rowCount();
diff --git a/classes/article.php b/classes/article.php
index e113ed219..17a40ca4f 100755
--- a/classes/article.php
+++ b/classes/article.php
@@ -177,7 +177,7 @@ class Article extends Handler_Protected {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
score = ? WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
- $sth->execute(array_merge([$score], $ids, [$_SESSION['uid']]));
+ $sth->execute([$score, ...$ids, $_SESSION['uid']]);
print json_encode(["id" => $ids, "score" => $score]);
}
@@ -507,7 +507,7 @@ class Article extends Handler_Protected {
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
}
- $sth->execute(array_merge($ids, [$owner_uid]));
+ $sth->execute([...$ids, $owner_uid]);
}
/**
diff --git a/classes/config.php b/classes/config.php
index cc089b7ba..a4a42a60a 100644
--- a/classes/config.php
+++ b/classes/config.php
@@ -6,7 +6,7 @@ class Config {
const T_STRING = 2;
const T_INT = 3;
- const SCHEMA_VERSION = 146;
+ const SCHEMA_VERSION = 147;
/** override default values, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX:
*
@@ -189,6 +189,9 @@ class Config {
/** http user agent (changing this is not recommended) */
const HTTP_USER_AGENT = "HTTP_USER_AGENT";
+ /** delay updates for this feed if received HTTP 429 (Too Many Requests) for this amount of seconds (base value, actual delay is base...base*2) */
+ const HTTP_429_THROTTLE_INTERVAL = "HTTP_429_THROTTLE_INTERVAL";
+
/** default values for all global configuration options */
private const _DEFAULTS = [
Config::DB_TYPE => [ "pgsql", Config::T_STRING ],
@@ -245,6 +248,7 @@ class Config {
Config::AUTH_MIN_INTERVAL => [ 5, Config::T_INT ],
Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://tt-rss.org/)',
Config::T_STRING ],
+ Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ],
];
/** @var Config|null */
@@ -461,9 +465,11 @@ class Config {
/** generates reference self_url_path (no trailing slash) */
static function make_self_url() : string {
$proto = self::is_server_https() ? 'https' : 'http';
- $self_url_path = $proto . '://' . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];
+
+ $self_url_path = $proto . '://' . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$self_url_path = preg_replace("/\w+\.php(\?.*$)?$/", "", $self_url_path);
+ #$self_url_path = preg_replace("/(\?.*$)?$/", "", $self_url_path);
if (substr($self_url_path, -1) === "/") {
return substr($self_url_path, 0, -1);
@@ -518,8 +524,8 @@ class Config {
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.4.0', '<')) {
+ array_push($errors, "PHP version 7.4.0 or newer required. You're using " . PHP_VERSION . ".");
}
if (!class_exists("UConverter")) {
diff --git a/classes/counters.php b/classes/counters.php
index 8756b5acf..bc4d2d4a3 100644
--- a/classes/counters.php
+++ b/classes/counters.php
@@ -5,13 +5,13 @@ class Counters {
* @return array<int, array<string, int|string>>
*/
static function get_all(): array {
- return array_merge(
- self::get_global(),
- self::get_virt(),
- self::get_labels(),
- self::get_feeds(),
- self::get_cats()
- );
+ return [
+ ...self::get_global(),
+ ...self::get_virt(),
+ ...self::get_labels(),
+ ...self::get_feeds(),
+ ...self::get_cats(),
+ ];
}
/**
@@ -20,13 +20,13 @@ class Counters {
* @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(),
- self::get_labels($label_ids),
- self::get_feeds($feed_ids),
- self::get_cats(is_array($feed_ids) ? Feeds::_cats_of($feed_ids, $_SESSION["uid"], true) : null)
- );
+ return [
+ ...self::get_global(),
+ ...self::get_virt(),
+ ...self::get_labels($label_ids),
+ ...self::get_feeds($feed_ids),
+ ...self::get_cats(is_array($feed_ids) ? Feeds::_cats_of($feed_ids, $_SESSION["uid"], true) : null)
+ ];
}
/**
@@ -93,11 +93,7 @@ class Counters {
ue.feed_id = f.id AND
ue.owner_uid = ?");
- $sth->execute(array_merge(
- [$_SESSION['uid']],
- $cat_ids,
- [$_SESSION['uid']]
- ));
+ $sth->execute([$_SESSION['uid'], ...$cat_ids, $_SESSION['uid']]);
} else {
$sth = $pdo->prepare("SELECT fc.id,
@@ -170,7 +166,7 @@ class Counters {
WHERE f.id = ue.feed_id AND ue.owner_uid = ? AND f.id IN ($feed_ids_qmarks)
GROUP BY f.id");
- $sth->execute(array_merge([$_SESSION['uid']], $feed_ids));
+ $sth->execute([$_SESSION['uid'], ...$feed_ids]);
} else {
$sth = $pdo->prepare("SELECT f.id,
f.title,
@@ -319,7 +315,7 @@ class Counters {
LEFT JOIN ttrss_user_entries AS u1 ON u1.ref_id = article_id AND u1.owner_uid = ?
WHERE ttrss_labels2.owner_uid = ? AND ttrss_labels2.id IN ($label_ids_qmarks)
GROUP BY ttrss_labels2.id, ttrss_labels2.caption");
- $sth->execute(array_merge([$_SESSION["uid"], $_SESSION["uid"]], $label_ids));
+ $sth->execute([$_SESSION["uid"], $_SESSION["uid"], ...$label_ids]);
} else {
$sth = $pdo->prepare("SELECT id,
caption,
diff --git a/classes/db.php b/classes/db.php
index 2cc89f5ba..4331b662e 100755
--- a/classes/db.php
+++ b/classes/db.php
@@ -17,8 +17,12 @@ class Db
}
}
- static function NOW(): string {
- return date("Y-m-d H:i:s", time());
+ /**
+ * @param int $delta adjust generated timestamp by this value in seconds (either positive or negative)
+ * @return string
+ */
+ static function NOW(int $delta = 0): string {
+ return date("Y-m-d H:i:s", time() + $delta);
}
private function __clone() {
diff --git a/classes/db/migrations.php b/classes/db/migrations.php
index aecd9186c..ffce2af5f 100644
--- a/classes/db/migrations.php
+++ b/classes/db/migrations.php
@@ -1,33 +1,15 @@
<?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;
-
- /** @var int */
- private $cached_version = 0;
-
- /** @var int */
- private $cached_max_version = 0;
-
- /** @var int */
- private $max_version_override;
+ private string $base_filename = "schema.sql";
+ private string $base_path;
+ private string $migrations_path;
+ private string $migrations_table;
+ private bool $base_is_latest;
+ private PDO $pdo;
+ private int $cached_version = 0;
+ private int $cached_max_version = 0;
+ private int $max_version_override;
function __construct() {
$this->pdo = Db::pdo();
@@ -207,14 +189,11 @@ class Db_Migrations {
$filename = "{$this->base_path}/{$this->base_filename}";
if (file_exists($filename)) {
- $lines = array_filter(preg_split("/[\r\n]/", file_get_contents($filename)),
- function ($line) {
- return strlen(trim($line)) > 0 && strpos($line, "--") !== 0;
- });
-
- return array_filter(explode(";", implode("", $lines)), function ($line) {
- return strlen(trim($line)) > 0 && !in_array(strtolower($line), ["begin", "commit"]);
- });
+ $lines = array_filter(preg_split("/[\r\n]/", file_get_contents($filename)),
+ fn($line) => strlen(trim($line)) > 0 && strpos($line, "--") !== 0);
+
+ return array_filter(explode(";", implode("", $lines)),
+ fn($line) => strlen(trim($line)) > 0 && !in_array(strtolower($line), ["begin", "commit"]));
} else {
user_error("Requested schema file ${filename} not found.", E_USER_ERROR);
diff --git a/classes/debug.php b/classes/debug.php
index fbdf260e0..40fa27377 100644
--- a/classes/debug.php
+++ b/classes/debug.php
@@ -12,44 +12,31 @@ class Debug {
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;
+ public static int $LOG_DISABLED = self::LOG_DISABLED;
/**
* @deprecated
- * @var int
*/
- public static $LOG_NORMAL = self::LOG_NORMAL;
+ public static int $LOG_NORMAL = self::LOG_NORMAL;
/**
* @deprecated
- * @var int
*/
- public static $LOG_VERBOSE = self::LOG_VERBOSE;
+ public static int $LOG_VERBOSE = self::LOG_VERBOSE;
/**
* @deprecated
- * @var int
*/
- public static $LOG_EXTENDED = self::LOG_EXTENDED;
+ public static int $LOG_EXTENDED = self::LOG_EXTENDED;
- /** @var bool */
- private static $enabled = false;
+ private static bool $enabled = false;
+ private static bool $quiet = false;
+ private static ?string $logfile = null;
- /** @var bool */
- private static $quiet = false;
-
- /** @var string|null */
- private static $logfile = null;
-
- /**
- * @var int Debug::LOG_*
- */
- private static $loglevel = self::LOG_NORMAL;
+ private static int $loglevel = self::LOG_NORMAL;
public static function set_logfile(string $logfile): void {
self::$logfile = $logfile;
@@ -68,7 +55,7 @@ class Debug {
}
/**
- * @param int $level Debug::LOG_*
+ * @param Debug::LOG_* $level
*/
public static function set_loglevel(int $level): void {
self::$loglevel = $level;
@@ -82,7 +69,21 @@ class Debug {
}
/**
- * @param int $level Debug::LOG_*
+ * @param int $level integer loglevel value
+ * @return Debug::LOG_* if valid, warn and return LOG_DISABLED otherwise
+ */
+ public static function map_loglevel(int $level) : int {
+ if (in_array($level, self::ALL_LOG_LEVELS)) {
+ /** @phpstan-ignore-next-line */
+ return $level;
+ } else {
+ user_error("Passed invalid debug log level: $level", E_USER_WARNING);
+ return self::LOG_DISABLED;
+ }
+ }
+
+ /**
+ * @param Debug::LOG_* $level log level
*/
public static function log(string $message, int $level = Debug::LOG_NORMAL): bool {
diff --git a/classes/digest.php b/classes/digest.php
index 15203166b..3e943e6dd 100644
--- a/classes/digest.php
+++ b/classes/digest.php
@@ -163,9 +163,7 @@ class Digest
$article_labels_formatted = "";
if (is_array($article_labels) && count($article_labels) > 0) {
- $article_labels_formatted = implode(", ", array_map(function($a) {
- return $a[1];
- }, $article_labels));
+ $article_labels_formatted = implode(", ", array_map(fn($a) => $a[1], $article_labels));
}
$tpl->setVariable('FEED_TITLE', $line["feed_title"]);
diff --git a/classes/diskcache.php b/classes/diskcache.php
index 34bba25f1..01c713b99 100644
--- a/classes/diskcache.php
+++ b/classes/diskcache.php
@@ -1,15 +1,13 @@
<?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;
+ private string $dir;
/**
* https://stackoverflow.com/a/53662733
*
* @var array<string, string>
*/
- private $mimeMap = [
+ private array $mimeMap = [
'video/3gpp2' => '3g2',
'video/3gp' => '3gp',
'video/3gpp' => '3gp',
diff --git a/classes/feeditem/atom.php b/classes/feeditem/atom.php
index cac6d8c54..59bf403b3 100755
--- a/classes/feeditem/atom.php
+++ b/classes/feeditem/atom.php
@@ -43,7 +43,8 @@ class FeedItem_Atom extends FeedItem_Common {
$links = $this->elem->getElementsByTagName("link");
foreach ($links as $link) {
- if ($link && $link->hasAttribute("href") &&
+ /** @phpstan-ignore-next-line */
+ if ($link->hasAttribute("href") &&
(!$link->hasAttribute("rel")
|| $link->getAttribute("rel") == "alternate"
|| $link->getAttribute("rel") == "standout")) {
@@ -180,7 +181,8 @@ class FeedItem_Atom extends FeedItem_Common {
$encs = [];
foreach ($links as $link) {
- if ($link && $link->hasAttribute("href") && $link->hasAttribute("rel")) {
+ /** @phpstan-ignore-next-line */
+ if ($link->hasAttribute("href") && $link->hasAttribute("rel")) {
$base = $this->xpath->evaluate("string(ancestor-or-self::*[@xml:base][1]/@xml:base)", $link);
if ($link->getAttribute("rel") == "enclosure") {
@@ -199,7 +201,7 @@ class FeedItem_Atom extends FeedItem_Common {
}
}
- $encs = array_merge($encs, parent::get_enclosures());
+ array_push($encs, ...parent::get_enclosures());
return $encs;
}
diff --git a/classes/feeditem/common.php b/classes/feeditem/common.php
index 6a9be8aca..fde481179 100755
--- a/classes/feeditem/common.php
+++ b/classes/feeditem/common.php
@@ -189,7 +189,7 @@ abstract class FeedItem_Common extends FeedItem {
$tmp = [];
foreach ($cats as $rawcat) {
- $tmp = array_merge($tmp, explode(",", $rawcat));
+ array_push($tmp, ...explode(",", $rawcat));
}
$tmp = array_map(function($srccat) {
diff --git a/classes/feeditem/rss.php b/classes/feeditem/rss.php
index 7017d04e9..132eabff5 100755
--- a/classes/feeditem/rss.php
+++ b/classes/feeditem/rss.php
@@ -153,7 +153,7 @@ class FeedItem_RSS extends FeedItem_Common {
array_push($encs, $enc);
}
- $encs = array_merge($encs, parent::get_enclosures());
+ array_push($encs, ...parent::get_enclosures());
return $encs;
}
diff --git a/classes/feedparser.php b/classes/feedparser.php
index fc2489e2d..4b9c63f56 100644
--- a/classes/feedparser.php
+++ b/classes/feedparser.php
@@ -43,10 +43,8 @@ class FeedParser {
foreach (libxml_get_errors() as $error) {
if ($error->level == LIBXML_ERR_FATAL) {
// 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->error ??= Errors::format_libxml_error($error);
+ $this->libxml_errors[] = Errors::format_libxml_error($error);
}
}
}
@@ -87,9 +85,7 @@ class FeedParser {
$this->type = $this::FEED_ATOM;
break;
default:
- if (!isset($this->error)) {
- $this->error = "Unknown/unsupported feed type";
- }
+ $this->error ??= "Unknown/unsupported feed type";
return;
}
}
@@ -186,9 +182,7 @@ class FeedParser {
if ($this->link) $this->link = trim($this->link);
} else {
- if (!isset($this->error)) {
- $this->error = "Unknown/unsupported feed type";
- }
+ $this->error ??= "Unknown/unsupported feed type";
return;
}
}
diff --git a/classes/feeds.php b/classes/feeds.php
index a06486883..afcc97d81 100755
--- a/classes/feeds.php
+++ b/classes/feeds.php
@@ -122,7 +122,7 @@ class Feeds extends Handler_Protected {
$feed_title = $qfh_ret[1];
$feed_site_url = $qfh_ret[2];
$last_error = $qfh_ret[3];
- $last_updated = strpos($qfh_ret[4], '1970-') === false ?
+ $last_updated = strpos($qfh_ret[4] ?? "", '1970-') === false ?
TimeHelper::make_local_datetime($qfh_ret[4], false) : __("Never");
$highlight_words = $qfh_ret[5];
$reply['first_id'] = $qfh_ret[6];
@@ -248,11 +248,12 @@ class Feeds extends Handler_Protected {
function ($result, $plugin) use (&$line, &$button_doc) {
if ($result && $button_doc->loadXML($result)) {
- /** @var DOMElement|null */
+ /** @var DOMElement|null $child */
$child = $button_doc->firstChild;
if ($child) {
do {
+ /** @var DOMElement|null $child */
$child->setAttribute('data-plugin-name', get_class($plugin));
} while ($child = $child->nextSibling);
@@ -271,11 +272,12 @@ class Feeds extends Handler_Protected {
function ($result, $plugin) use (&$line, &$button_doc) {
if ($result && $button_doc->loadXML($result)) {
- /** @var DOMElement|null */
+ /** @var DOMElement|null $child */
$child = $button_doc->firstChild;
if ($child) {
do {
+ /** @var DOMElement|null $child */
$child->setAttribute('data-plugin-name', get_class($plugin));
} while ($child = $child->nextSibling);
@@ -665,7 +667,7 @@ class Feeds extends Handler_Protected {
}
Debug::set_enabled(true);
- Debug::set_loglevel($xdebug);
+ Debug::set_loglevel(Debug::map_loglevel($xdebug));
$feed_id = (int)$_REQUEST["feed_id"];
$do_update = ($_REQUEST["action"] ?? "") == "do_update";
@@ -1936,8 +1938,8 @@ class Feeds extends Handler_Protected {
$sth->execute([$cat, $owner_uid]);
while ($line = $sth->fetch()) {
- array_push($rv, (int)$line["parent_cat"]);
- $rv = array_merge($rv, self::_get_parent_cats($line["parent_cat"], $owner_uid));
+ $cat = (int) $line["parent_cat"];
+ array_push($rv, $cat, ...self::_get_parent_cats($cat, $owner_uid));
}
return $rv;
@@ -1956,8 +1958,7 @@ class Feeds extends Handler_Protected {
$sth->execute([$cat, $owner_uid]);
while ($line = $sth->fetch()) {
- array_push($rv, $line["id"]);
- $rv = array_merge($rv, self::_get_child_cats($line["id"], $owner_uid));
+ array_push($rv, $line["id"], ...self::_get_child_cats($line["id"], $owner_uid));
}
return $rv;
@@ -1978,16 +1979,18 @@ class Feeds extends Handler_Protected {
$sth = $pdo->prepare("SELECT DISTINCT cat_id, fc.parent_cat FROM ttrss_feeds f LEFT JOIN ttrss_feed_categories fc
ON (fc.id = f.cat_id)
WHERE f.owner_uid = ? AND f.id IN ($feeds_qmarks)");
- $sth->execute(array_merge([$owner_uid], $feeds));
+ $sth->execute([$owner_uid, ...$feeds]);
$rv = [];
if ($row = $sth->fetch()) {
+ $cat_id = (int) $row["cat_id"];
+ $rv[] = $cat_id;
array_push($rv, (int)$row["cat_id"]);
- if ($with_parents && $row["parent_cat"])
- $rv = array_merge($rv,
- self::_get_parent_cats($row["cat_id"], $owner_uid));
+ if ($with_parents && $row["parent_cat"]) {
+ array_push($rv, ...self::_get_parent_cats($cat_id, $owner_uid));
+ }
}
$rv = array_unique($rv);
@@ -2357,8 +2360,11 @@ class Feeds extends Handler_Protected {
$k = mb_strtolower($k);
array_push($search_query_leftover, $not ? "!$k" : $k);
} else {
- array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
- OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
+ $k = mb_strtolower($k);
+ array_push($search_query_leftover, $not ? "-$k" : $k);
+
+ //array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
+ // OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
}
if (!$not) array_push($search_words, $k);
@@ -2380,12 +2386,16 @@ class Feeds extends Handler_Protected {
array_push($query_keywords,
"(tsvector_combined @@ to_tsquery($search_language, $tsquery))");
- }
+ } else {
+ $ft_query = $pdo->quote(implode(" ", $search_query_leftover));
+ array_push($query_keywords,
+ "MATCH (ttrss_entries.title, ttrss_entries.content) AGAINST ($ft_query IN BOOLEAN MODE)");
+ }
}
if (count($query_keywords) > 0)
- $search_query_part = implode("AND", $query_keywords);
+ $search_query_part = implode("AND ", $query_keywords);
else
$search_query_part = "false";
diff --git a/classes/handler.php b/classes/handler.php
index 806c9cfbe..5b54570d8 100644
--- a/classes/handler.php
+++ b/classes/handler.php
@@ -1,11 +1,9 @@
<?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;
+ protected PDO $pdo;
/** @var array<int|string, mixed> */
- protected $args;
+ protected array $args;
/**
* @param array<int|string, mixed> $args
diff --git a/classes/handler/public.php b/classes/handler/public.php
index 3fef4c2b9..ea0972f6b 100755
--- a/classes/handler/public.php
+++ b/classes/handler/public.php
@@ -76,7 +76,7 @@ class Handler_Public extends Handler {
"/public.php?op=rss&id=$feed&key=" .
Feeds::_get_access_key($feed, false, $owner_uid);
- if (!$feed_site_url) $feed_site_url = get_self_url_prefix();
+ if (!$feed_site_url) $feed_site_url = Config::get_self_url();
if ($format == 'atom') {
$tpl = new Templator();
@@ -87,7 +87,7 @@ class Handler_Public extends Handler {
$tpl->setVariable('VERSION', Config::get_version(), true);
$tpl->setVariable('FEED_URL', htmlspecialchars($feed_self_url), true);
- $tpl->setVariable('SELF_URL', htmlspecialchars(get_self_url_prefix()), true);
+ $tpl->setVariable('SELF_URL', htmlspecialchars(Config::get_self_url()), true);
while ($line = $result->fetch()) {
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
@@ -134,7 +134,7 @@ class Handler_Public extends Handler {
$tpl->setVariable('ARTICLE_AUTHOR', htmlspecialchars($line['author']), true);
- $tpl->setVariable('ARTICLE_SOURCE_LINK', htmlspecialchars($line['site_url'] ? $line["site_url"] : get_self_url_prefix()), true);
+ $tpl->setVariable('ARTICLE_SOURCE_LINK', htmlspecialchars($line['site_url'] ? $line["site_url"] : Config::get_self_url()), true);
$tpl->setVariable('ARTICLE_SOURCE_TITLE', htmlspecialchars($line['feed_title'] ?? $feed_title), true);
foreach ($line["tags"] as $tag) {
@@ -219,6 +219,11 @@ class Handler_Public extends Handler {
if (!empty($line['note'])) $article['note'] = $line['note'];
if (!empty($line['author'])) $article['author'] = $line['author'];
+ $article['source'] = [
+ 'link' => $line['site_url'] ? $line["site_url"] : Config::get_self_url(),
+ 'title' => $line['feed_title'] ?? $feed_title
+ ];
+
if (count($line["tags"]) > 0) {
$article['tags'] = array();
@@ -312,7 +317,7 @@ class Handler_Public extends Handler {
$login, $user_id);
if (!$redirect_url)
- $redirect_url = get_self_url_prefix() . "/index.php";
+ $redirect_url = Config::get_self_url() . "/index.php";
header("Location: " . $redirect_url);
} else {
@@ -389,11 +394,11 @@ class Handler_Public extends Handler {
if (UserHelper::authenticate($login, $password)) {
$_POST["password"] = "";
- if (get_schema_version() >= 120) {
+ if (Config::get_schema_version() >= 120) {
$_SESSION["language"] = get_pref(Prefs::USER_LANGUAGE, $_SESSION["uid"]);
}
- $_SESSION["ref_schema_version"] = get_schema_version();
+ $_SESSION["ref_schema_version"] = Config::get_schema_version();
$_SESSION["bw_limit"] = !!clean($_POST["bw_limit"] ?? false);
$_SESSION["safe_mode"] = $safe_mode;
@@ -412,8 +417,7 @@ class Handler_Public extends Handler {
if (session_status() != PHP_SESSION_ACTIVE)
session_start();
- if (!isset($_SESSION["login_error_msg"]))
- $_SESSION["login_error_msg"] = __("Incorrect username or password");
+ $_SESSION["login_error_msg"] ??= __("Incorrect username or password");
}
$return = clean($_REQUEST['return']);
@@ -563,7 +567,7 @@ class Handler_Public extends Handler {
print_notice("Password reset instructions are being sent to your email address.");
$resetpass_token = sha1(get_random_bytes(128));
- $resetpass_link = get_self_url_prefix() . "/public.php?op=forgotpass&hash=" . $resetpass_token .
+ $resetpass_link = Config::get_self_url() . "/public.php?op=forgotpass&hash=" . $resetpass_token .
"&login=" . urlencode($login);
$tpl = new Templator();
@@ -785,7 +789,7 @@ class Handler_Public extends Handler {
$plugin_name = basename(clean($_REQUEST["plugin"]));
$method = clean($_REQUEST["pmethod"]);
- $host->load($plugin_name, PluginHost::KIND_USER, 0);
+ $host->load($plugin_name, PluginHost::KIND_ALL, 0);
//$host->load_data();
$plugin = $host->get_plugin($plugin_name);
@@ -807,7 +811,7 @@ class Handler_Public extends Handler {
} else {
user_error("PluginHandler[PUBLIC]: Requested method '$method' of unknown plugin '$plugin_name'.", E_USER_WARNING);
header("Content-Type: text/json");
- print Errors::to_json(Errors::E_UNKNOWN_PLUGIN);
+ print Errors::to_json(Errors::E_UNKNOWN_PLUGIN, ['plugin' => $plugin_name]);
}
}
diff --git a/classes/mailer.php b/classes/mailer.php
index a15c8546b..ac5c641eb 100644
--- a/classes/mailer.php
+++ b/classes/mailer.php
@@ -1,8 +1,6 @@
<?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 = "";
+ private string $last_error = "";
/**
* @param array<string, mixed> $params
@@ -45,16 +43,9 @@ class Mailer {
++$hooks_tried;
}
- $headers = [ "From: $from_combined" ];
+ $headers = [ "From: $from_combined", "Content-Type: text/plain; charset=UTF-8" ];
- if ($message_html) {
- $headers[] = "MIME-Version: 1.0";
- $headers[] = "Content-Type: text/html; charset=UTF-8";
- } else {
- $headers[] = "Content-Type: text/plain; charset=UTF-8";
- }
-
- $rc = mail($to_combined, $subject, $message, implode("\r\n", array_merge($headers, $additional_headers)));
+ $rc = mail($to_combined, $subject, $message, implode("\r\n", [...$headers, ...$additional_headers]));
if (!$rc) {
$this->set_error(error_get_last()['message'] ?? T_sprintf("Unknown error while sending mail. Hooks tried: %d.", $hooks_tried));
diff --git a/classes/plugin.php b/classes/plugin.php
index 39af6a9a1..f47ab1882 100644
--- a/classes/plugin.php
+++ b/classes/plugin.php
@@ -50,6 +50,11 @@ abstract class Plugin {
}
/** @return string */
+ function get_login_js() {
+ return "";
+ }
+
+ /** @return string */
function get_css() {
return "";
}
@@ -690,6 +695,15 @@ abstract class Plugin {
* @return array<mixed> - [0] - if set, url to redirect to
*/
function hook_post_logout($login, $user_id) {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+
return [""];
}
+
+ /** Adds buttons to the right of default Login button
+ * @return void
+ */
+ function hook_loginform_additional_buttons() {
+ user_error("Dummy method invoked.", E_USER_ERROR);
+ }
}
diff --git a/classes/pluginhost.php b/classes/pluginhost.php
index 952d4df77..4b85bc216 100755
--- a/classes/pluginhost.php
+++ b/classes/pluginhost.php
@@ -1,49 +1,42 @@
<?php
class PluginHost {
- // 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;
+ private ?PDO $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;
+ private ?PDO $pdo_data = null;
/** @var array<string, array<int, array<int, Plugin>>> hook types -> priority levels -> Plugins */
- private $hooks = [];
+ private array $hooks = [];
/** @var array<string, Plugin> */
- private $plugins = [];
+ private array $plugins = [];
/** @var array<string, array<string, Plugin>> handler type -> method type -> Plugin */
- private $handlers = [];
+ private array $handlers = [];
/** @var array<string, array{'description': string, 'suffix': string, 'arghelp': string, 'class': Plugin}> command type -> details array */
- private $commands = [];
+ private array $commands = [];
/** @var array<string, array<string, mixed>> plugin name -> (potential profile array) -> key -> value */
- private $storage = [];
+ private array $storage = [];
/** @var array<int, array<int, array{'id': int, 'title': string, 'sender': Plugin, 'icon': string}>> */
- private $feeds = [];
+ private array $feeds = [];
/** @var array<string, Plugin> API method name, Plugin sender */
- private $api_methods = [];
+ private array $api_methods = [];
/** @var array<string, array<int, array{'action': string, 'description': string, 'sender': Plugin}>> */
- private $plugin_actions = [];
+ private array $plugin_actions = [];
- /** @var int|null */
- private $owner_uid = null;
+ private ?int $owner_uid = null;
- /** @var bool */
- private $data_loaded = false;
+ private bool $data_loaded = false;
- /** @var PluginHost|null */
- private static $instance = null;
+ private static ?PluginHost $instance = null;
const API_VERSION = 2;
const PUBLIC_METHOD_DELIMITER = "--";
@@ -203,6 +196,9 @@ class PluginHost {
/** @see Plugin::hook_post_logout() */
const HOOK_POST_LOGOUT = "hook_post_logout";
+ /** @see Plugin::hook_loginform_additional_buttons() */
+ const HOOK_LOGINFORM_ADDITIONAL_BUTTONS = "hook_loginform_additional_buttons";
+
const KIND_ALL = 1;
const KIND_SYSTEM = 2;
const KIND_USER = 3;
@@ -409,7 +405,7 @@ class PluginHost {
$tmp = [];
foreach (array_keys($this->hooks[$type]) as $prio) {
- $tmp = array_merge($tmp, $this->hooks[$type][$prio]);
+ array_push($tmp, ...$this->hooks[$type][$prio]);
}
return $tmp;
@@ -422,7 +418,7 @@ class PluginHost {
*/
function load_all(int $kind, int $owner_uid = null, bool $skip_init = false): void {
- $plugins = array_merge(glob("plugins/*"), glob("plugins.local/*"));
+ $plugins = [...(glob("plugins/*") ?: []), ...(glob("plugins.local/*") ?: [])];
$plugins = array_filter($plugins, "is_dir");
$plugins = array_map("basename", $plugins);
@@ -539,10 +535,7 @@ class PluginHost {
$method = strtolower($method);
if ($this->is_system($sender)) {
- if (!isset($this->handlers[$handler])) {
- $this->handlers[$handler] = [];
- }
-
+ $this->handlers[$handler] ??= [];
$this->handlers[$handler][$method] = $sender;
}
}
@@ -645,8 +638,7 @@ class PluginHost {
owner_uid= ? AND name = ?");
$sth->execute([$this->owner_uid, $plugin]);
- if (!isset($this->storage[$plugin]))
- $this->storage[$plugin] = [];
+ $this->storage[$plugin] ??= [];
$content = serialize($this->storage[$plugin]);
@@ -677,14 +669,8 @@ class PluginHost {
if ($profile_id) {
$idx = get_class($sender);
- if (!isset($this->storage[$idx])) {
- $this->storage[$idx] = [];
- }
-
- if (!isset($this->storage[$idx][$profile_id])) {
- $this->storage[$idx][$profile_id] = [];
- }
-
+ $this->storage[$idx] ??= [];
+ $this->storage[$idx][$profile_id] ??= [];
$this->storage[$idx][$profile_id][$name] = $value;
$this->save_data(get_class($sender));
@@ -699,9 +685,7 @@ class PluginHost {
function set(Plugin $sender, string $name, $value): void {
$idx = get_class($sender);
- if (!isset($this->storage[$idx]))
- $this->storage[$idx] = [];
-
+ $this->storage[$idx] ??= [];
$this->storage[$idx][$name] = $value;
$this->save_data(get_class($sender));
@@ -713,8 +697,7 @@ class PluginHost {
function set_array(Plugin $sender, array $params): void {
$idx = get_class($sender);
- if (!isset($this->storage[$idx]))
- $this->storage[$idx] = [];
+ $this->storage[$idx] ??= [];
foreach ($params as $name => $value)
$this->storage[$idx][$name] = $value;
@@ -852,11 +835,13 @@ class PluginHost {
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] = [];
+ $this->plugin_actions[$sender_class] ??= [];
- array_push($this->plugin_actions[$sender_class],
- array("action" => $action_name, "description" => $action_desc, "sender" => $sender));
+ $this->plugin_actions[$sender_class][] = [
+ "action" => $action_name,
+ "description" => $action_desc,
+ "sender" => $sender,
+ ];
}
/**
diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php
index 03b70580b..6cf979b0a 100755
--- a/classes/pref/feeds.php
+++ b/classes/pref/feeds.php
@@ -172,7 +172,7 @@ class Pref_Feeds extends Handler_Protected {
if ($enable_cats) {
array_push($root['items'], $cat);
} else {
- $root['items'] = array_merge($root['items'], $cat['items']);
+ array_push($root['items'], ...$cat['items']);
}
$sth = $this->pdo->prepare("SELECT * FROM
@@ -202,7 +202,7 @@ class Pref_Feeds extends Handler_Protected {
if ($enable_cats) {
array_push($root['items'], $cat);
} else {
- $root['items'] = array_merge($root['items'], $cat['items']);
+ array_push($root['items'], ...$cat['items']);
}
}
}
@@ -848,7 +848,7 @@ class Pref_Feeds extends Handler_Protected {
if ($qpart) {
$sth = $this->pdo->prepare("UPDATE ttrss_feeds SET $qpart WHERE id IN ($feed_ids_qmarks)
AND owner_uid = ?");
- $sth->execute(array_merge($feed_ids, [$_SESSION['uid']]));
+ $sth->execute([...$feed_ids, $_SESSION['uid']]);
}
}
@@ -973,16 +973,6 @@ class Pref_Feeds extends Handler_Protected {
persist="true"
model="feedModel"
openOnClick="false">
- <script type="dojo/method" event="onClick" args="item">
- var id = String(item.id);
- var bare_id = id.substr(id.indexOf(':')+1);
-
- if (id.match('FEED:')) {
- CommonDialogs.editFeed(bare_id);
- } else if (id.match('CAT:')) {
- dijit.byId('feedTree').editCategory(bare_id, item);
- }
- </script>
</div>
</div>
</div>
diff --git a/classes/pref/filters.php b/classes/pref/filters.php
index 79dd78993..19ec8d39e 100755
--- a/classes/pref/filters.php
+++ b/classes/pref/filters.php
@@ -115,6 +115,7 @@ class Pref_Filters extends Handler_Protected {
$glue = $filter['match_any_rule'] ? " OR " : " AND ";
$scope_qpart = join($glue, $scope_qparts);
+ /** @phpstan-ignore-next-line */
if (!$scope_qpart) $scope_qpart = "true";
$rv = array();
@@ -537,7 +538,7 @@ class Pref_Filters extends Handler_Protected {
$sth = $this->pdo->prepare("DELETE FROM ttrss_filters2 WHERE id IN ($ids_qmarks)
AND owner_uid = ?");
- $sth->execute(array_merge($ids, [$_SESSION['uid']]));
+ $sth->execute([...$ids, $_SESSION['uid']]);
}
private function _save_rules_and_actions(int $filter_id): void {
@@ -703,14 +704,6 @@ class Pref_Filters extends Handler_Protected {
</div>
<div dojoType="fox.PrefFilterTree" id="filterTree" dndController="dijit.tree.dndSource"
betweenThreshold="5" model="filterModel" openOnClick="true">
- <script type="dojo/method" event="onClick" args="item">
- var id = String(item.id);
- var bare_id = id.substr(id.indexOf(':')+1);
-
- if (id.match('FILTER:')) {
- Filters.edit(bare_id);
- }
- </script>
</div>
</div>
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefFilters") ?>
@@ -788,11 +781,11 @@ class Pref_Filters extends Handler_Protected {
$sth = $this->pdo->prepare("UPDATE ttrss_filters2_rules
SET filter_id = ? WHERE filter_id IN ($ids_qmarks)");
- $sth->execute(array_merge([$base_id], $ids));
+ $sth->execute([$base_id, ...$ids]);
$sth = $this->pdo->prepare("UPDATE ttrss_filters2_actions
SET filter_id = ? WHERE filter_id IN ($ids_qmarks)");
- $sth->execute(array_merge([$base_id], $ids));
+ $sth->execute([$base_id, ...$ids]);
$sth = $this->pdo->prepare("DELETE FROM ttrss_filters2 WHERE id IN ($ids_qmarks)");
$sth->execute($ids);
diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php
index 8c044b49f..3285ce200 100644
--- a/classes/pref/prefs.php
+++ b/classes/pref/prefs.php
@@ -2,18 +2,17 @@
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 = [];
+ private array $pref_help = [];
/** @var array<string, array<int, string>> pref items are Prefs::*|Pref_Prefs::BLOCK_SEPARATOR (PHPStan was complaining) */
- private $pref_item_map = [];
+ private array $pref_item_map = [];
/** @var array<string, string> */
- private $pref_help_bottom = [];
+ private array $pref_help_bottom = [];
/** @var array<int, string> */
- private $pref_blacklist = [];
+ private array $pref_blacklist = [];
private const BLOCK_SEPARATOR = 'BLOCK_SEPARATOR';
@@ -26,7 +25,6 @@ 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";
- /** @param string $method */
function csrf_ignore(string $method) : bool {
$csrf_ignored = array("index", "updateself", "otpqrcode");
@@ -187,7 +185,7 @@ class Pref_Prefs extends Handler_Protected {
$boolean_prefs = explode(",", clean($_POST["boolean_prefs"]));
foreach ($boolean_prefs as $pref) {
- if (!isset($_POST[$pref])) $_POST[$pref] = 'false';
+ $_POST[$pref] ??= 'false';
}
$need_reload = false;
@@ -633,10 +631,11 @@ class Pref_Prefs extends Handler_Protected {
} else if ($pref_name == Prefs::USER_CSS_THEME) {
- $theme_files = array_map("basename",
- array_merge(glob("themes/*.php"),
- glob("themes/*.css"),
- glob("themes.local/*.css")));
+ $theme_files = array_map("basename", [
+ ...glob("themes/*.php") ?: [],
+ ...glob("themes/*.css") ?: [],
+ ...glob("themes.local/*.css") ?: [],
+ ]);
asort($theme_files);
@@ -869,18 +868,19 @@ class Pref_Prefs extends Handler_Protected {
$feed_handler_whitelist = [ "Af_Comics" ];
- $feed_handlers = array_merge(
- PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FEED_FETCHED),
- PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FEED_PARSED),
- PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FETCH_FEED));
+ $feed_handlers = [
+ ...PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FEED_FETCHED),
+ ...PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FEED_PARSED),
+ ...PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FETCH_FEED),
+ ];
- $feed_handlers = array_filter($feed_handlers, function($plugin) use ($feed_handler_whitelist) {
- return in_array(get_class($plugin), $feed_handler_whitelist) === false; });
+ $feed_handlers = array_filter($feed_handlers,
+ fn($plugin) => in_array(get_class($plugin), $feed_handler_whitelist) === false);
if (count($feed_handlers) > 0) {
print_error(
T_sprintf("The following plugins use per-feed content hooks. This may cause excessive data usage and origin server load resulting in a ban of your instance: <b>%s</b>" ,
- implode(", ", array_map(function($plugin) { return get_class($plugin); }, $feed_handlers))
+ implode(", ", array_map(fn($plugin) => get_class($plugin), $feed_handlers))
) . " (<a href='https://tt-rss.org/wiki/FeedHandlerPlugins' target='_blank'>".__("More info...")."</a>)"
);
}
@@ -1024,7 +1024,7 @@ class Pref_Prefs extends Handler_Protected {
}
function setplugins(): void {
- $plugins = array_filter($_REQUEST["plugins"], 'clean') ?? [];
+ $plugins = array_filter($_REQUEST["plugins"] ?? [], 'clean');
set_pref(Prefs::_ENABLED_PLUGINS, implode(",", $plugins));
}
@@ -1069,9 +1069,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
- $rv = array_values(array_filter($rv, function ($item) {
- return $item["rv"]["need_update"];
- }));
+ $rv = array_values(array_filter($rv, fn($item) => $item["rv"]["need_update"]));
return $rv;
}
diff --git a/classes/prefs.php b/classes/prefs.php
index 7e6033f4d..378fea293 100644
--- a/classes/prefs.php
+++ b/classes/prefs.php
@@ -230,7 +230,7 @@ class Prefs {
}
}
- if (get_schema_version() >= 141) {
+ if (Config::get_schema_version() >= 141) {
// fill in any overrides from the database
$sth = $this->pdo->prepare("SELECT pref_name, value FROM ttrss_user_prefs2
WHERE owner_uid = :uid AND
@@ -265,7 +265,7 @@ class Prefs {
if ($this->_is_cached($pref_name, $owner_uid, $profile_id)) {
$cached_value = $this->_get_cache($pref_name, $owner_uid, $profile_id);
return Config::cast_to($cached_value, $type_hint);
- } else if (get_schema_version() >= 141) {
+ } else if (Config::get_schema_version() >= 141) {
$sth = $this->pdo->prepare("SELECT value FROM ttrss_user_prefs2
WHERE pref_name = :name AND owner_uid = :uid AND
(profile = :profile OR (:profile IS NULL AND profile IS NULL))");
@@ -390,7 +390,7 @@ class Prefs {
}
function migrate(int $owner_uid, ?int $profile_id): void {
- if (get_schema_version() < 141)
+ if (Config::get_schema_version() < 141)
return;
if (!$profile_id) $profile_id = null;
diff --git a/classes/rpc.php b/classes/rpc.php
index dbb54cec5..ef2cdfc49 100755
--- a/classes/rpc.php
+++ b/classes/rpc.php
@@ -77,7 +77,7 @@ class RPC extends Handler_Protected {
$sth = $this->pdo->prepare("DELETE FROM ttrss_user_entries
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
- $sth->execute(array_merge($ids, [$_SESSION['uid']]));
+ $sth->execute([...$ids, $_SESSION['uid']]);
Article::_purge_orphans();
@@ -364,7 +364,7 @@ class RPC extends Handler_Protected {
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
}
- $sth->execute(array_merge($ids, [$_SESSION['uid']]));
+ $sth->execute([...$ids, $_SESSION['uid']]);
}
/**
@@ -388,7 +388,7 @@ class RPC extends Handler_Protected {
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
}
- $sth->execute(array_merge($ids, [$_SESSION['uid']]));
+ $sth->execute([...$ids, $_SESSION['uid']]);
}
function log(): void {
@@ -753,12 +753,11 @@ class RPC extends Handler_Protected {
function hotkeyHelp(): void {
$info = self::get_hotkeys_info();
$imap = self::get_hotkeys_map();
- $omap = array();
+ $omap = [];
foreach ($imap[1] as $sequence => $action) {
- if (!isset($omap[$action])) $omap[$action] = array();
-
- array_push($omap[$action], $sequence);
+ $omap[$action] ??= [];
+ $omap[$action][] = $sequence;
}
?>
diff --git a/classes/rssutils.php b/classes/rssutils.php
index aec17d538..fe295417a 100755
--- a/classes/rssutils.php
+++ b/classes/rssutils.php
@@ -39,7 +39,7 @@ class RSSUtils {
// check icon files once every Config::get(Config::CACHE_MAX_DAYS) days
$icon_files = array_filter(glob(Config::get(Config::ICONS_DIR) . "/*.ico"),
- function($f) { return filemtime($f) < time() - 86400 * Config::get(Config::CACHE_MAX_DAYS); });
+ fn(string $f) => filemtime($f) < time() - 86400 * Config::get(Config::CACHE_MAX_DAYS));
foreach ($icon_files as $icon) {
$feed_id = basename($icon, ".ico");
@@ -447,7 +447,7 @@ class RSSUtils {
Debug::log("not using CURL due to open_basedir restrictions", Debug::LOG_VERBOSE);
}
- if (time() - strtotime($feed_obj->last_unconditional) > Config::get(Config::MAX_CONDITIONAL_INTERVAL)) {
+ if (time() - strtotime($feed_obj->last_unconditional ?? "") > Config::get(Config::MAX_CONDITIONAL_INTERVAL)) {
Debug::log("maximum allowed interval for conditional requests exceeded, forcing refetch", Debug::LOG_VERBOSE);
$force_refetch = true;
@@ -492,7 +492,7 @@ class RSSUtils {
// If-Modified-Since
if (UrlHelper::$fetch_last_error_code == 304) {
- Debug::log("source claims data not modified, nothing to do.", Debug::LOG_VERBOSE);
+ Debug::log("source claims data not modified (304), nothing to do.", Debug::LOG_VERBOSE);
$error_message = "";
$feed_obj->set([
@@ -503,6 +503,24 @@ class RSSUtils {
$feed_obj->save();
+ } else if (UrlHelper::$fetch_last_error_code == 429) {
+
+ // randomize interval using Config::HTTP_429_THROTTLE_INTERVAL as a base value (1-2x)
+ $http_429_throttle_interval = rand(Config::get(Config::HTTP_429_THROTTLE_INTERVAL),
+ Config::get(Config::HTTP_429_THROTTLE_INTERVAL)*2);
+
+ $error_message = UrlHelper::$fetch_last_error;
+
+ Debug::log("source claims we're requesting too often (429), throttling updates for $http_429_throttle_interval seconds.",
+ Debug::LOG_VERBOSE);
+
+ $feed_obj->set([
+ 'last_error' => $error_message . " (updates throttled for $http_429_throttle_interval seconds.)",
+ 'last_successful_update' => Db::NOW($http_429_throttle_interval),
+ 'last_updated' => Db::NOW($http_429_throttle_interval),
+ ]);
+
+ $feed_obj->save();
} else {
$error_message = UrlHelper::$fetch_last_error;
@@ -644,7 +662,7 @@ class RSSUtils {
print_r($item);
}
- if (ini_get("max_execution_time") > 0 && time() - $tstart >= ini_get("max_execution_time") * 0.7) {
+ if (ini_get("max_execution_time") > 0 && time() - $tstart >= ((float)ini_get("max_execution_time") * 0.7)) {
Debug::log("looks like there's too many articles to process at once, breaking out.", Debug::LOG_VERBOSE);
$pdo->commit();
break;
@@ -715,7 +733,7 @@ class RSSUtils {
$article_labels = Article::_get_labels($base_entry_id, $feed_obj->owner_uid);
$existing_tags = Article::_get_tags($base_entry_id, $feed_obj->owner_uid);
- $entry_tags = array_unique(array_merge($entry_tags, $existing_tags));
+ $entry_tags = array_unique([...$entry_tags, ...$existing_tags]);
} else {
$base_entry_id = false;
$entry_stored_hash = "";
@@ -847,7 +865,7 @@ class RSSUtils {
$pluginhost->run_hooks(PluginHost::HOOK_FILTER_TRIGGERED,
$feed, $feed_obj->owner_uid, $article, $matched_filters, $matched_rules, $article_filters);
- $matched_filter_ids = array_map(function($f) { return $f['id']; }, $matched_filters);
+ $matched_filter_ids = array_map(fn(array $f) => $f['id'], $matched_filters);
if (count($matched_filter_ids) > 0) {
$filter_objs = ORM::for_table('ttrss_filters2')
@@ -1172,8 +1190,7 @@ 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"])));
+ $entry_tags = [...$entry_tags, ...FeedItem_Common::normalize_categories(explode(",", $f["param"]))];
}
}
@@ -1778,12 +1795,10 @@ class RSSUtils {
owner_uid = ? AND enabled = true ORDER BY order_id, title");
$sth->execute([$owner_uid]);
- $check_cats = array_merge(
- Feeds::_get_parent_cats($cat_id, $owner_uid),
- [$cat_id]);
+ $check_cats = [...Feeds::_get_parent_cats($cat_id, $owner_uid), $cat_id];
$check_cats_str = join(",", $check_cats);
- $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
+ $check_cats_fullids = array_map(fn(int $a) => "CAT:$a", $check_cats);
while ($line = $sth->fetch()) {
$filter_id = $line["id"];
diff --git a/classes/urlhelper.php b/classes/urlhelper.php
index bb51f5d06..dc47f5ad8 100644
--- a/classes/urlhelper.php
+++ b/classes/urlhelper.php
@@ -10,31 +10,14 @@ class UrlHelper {
"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 string $fetch_last_error;
+ static int $fetch_last_error_code;
+ static string $fetch_last_error_content;
+ static string $fetch_last_content_type;
+ static string $fetch_last_modified;
+ static string $fetch_effective_url;
+ static string $fetch_effective_ip_addr;
+ static bool $fetch_curl_used;
/**
* @param array<string, string|int> $parts
@@ -207,32 +190,26 @@ class UrlHelper {
if ($nest > 10)
return false;
- if (version_compare(PHP_VERSION, '7.1.0', '>=')) {
- $context_options = array(
- 'http' => array(
- 'header' => array(
- 'Connection: close'
- ),
- 'method' => 'HEAD',
- 'timeout' => $timeout,
- 'protocol_version'=> 1.1)
- );
-
- if (Config::get(Config::HTTP_PROXY)) {
- $context_options['http']['request_fulluri'] = true;
- $context_options['http']['proxy'] = Config::get(Config::HTTP_PROXY);
- }
+ $context_options = array(
+ 'http' => array(
+ 'header' => array(
+ 'Connection: close'
+ ),
+ 'method' => 'HEAD',
+ 'timeout' => $timeout,
+ 'protocol_version'=> 1.1)
+ );
+
+ if (Config::get(Config::HTTP_PROXY)) {
+ $context_options['http']['request_fulluri'] = true;
+ $context_options['http']['proxy'] = Config::get(Config::HTTP_PROXY);
+ }
- $context = stream_context_create($context_options);
+ $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);
- }
+ // PHP 8 changed the second param from int to bool, but we still support PHP >= 7.4.0
+ // @phpstan-ignore-next-line
+ $headers = get_headers($url, 0, $context);
if (is_array($headers)) {
$headers = array_reverse($headers); // last one is the correct one
@@ -462,7 +439,11 @@ class UrlHelper {
}
if (!$contents) {
- self::$fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
+ if (curl_errno($ch) === 0) {
+ self::$fetch_last_error = 'Successful response, but no content was received.';
+ } else {
+ self::$fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
+ }
curl_close($ch);
return false;
}
@@ -543,6 +524,11 @@ class UrlHelper {
$data = @file_get_contents($url, false, $context);
+ if ($data === false) {
+ self::$fetch_last_error = "'file_get_contents' failed.";
+ return false;
+ }
+
foreach ($http_response_header as $header) {
if (strstr($header, ": ") !== false) {
list ($key, $value) = explode(": ", $header);
@@ -578,15 +564,20 @@ class UrlHelper {
return false;
}
- $is_gzipped = RSSUtils::is_gzipped($data);
+ if ($data) {
+ $is_gzipped = RSSUtils::is_gzipped($data);
- if ($is_gzipped && $data) {
- $tmp = @gzdecode($data);
+ if ($is_gzipped) {
+ $tmp = @gzdecode($data);
- if ($tmp) $data = $tmp;
- }
+ if ($tmp) $data = $tmp;
+ }
- return $data;
+ return $data;
+ } else {
+ self::$fetch_last_error = 'Successful response, but no content was received.';
+ return false;
+ }
}
}
diff --git a/classes/userhelper.php b/classes/userhelper.php
index 91e40665d..228bb14fb 100644
--- a/classes/userhelper.php
+++ b/classes/userhelper.php
@@ -17,6 +17,15 @@ class UserHelper {
self::HASH_ALGO_SHA1
];
+ const ACCESS_LEVELS = [
+ self::ACCESS_LEVEL_DISABLED,
+ self::ACCESS_LEVEL_READONLY,
+ self::ACCESS_LEVEL_USER,
+ self::ACCESS_LEVEL_POWERUSER,
+ self::ACCESS_LEVEL_ADMIN,
+ self::ACCESS_LEVEL_KEEP_CURRENT
+ ];
+
/** forbidden to login */
const ACCESS_LEVEL_DISABLED = -2;
@@ -32,6 +41,23 @@ class UserHelper {
/** has administrator permissions */
const ACCESS_LEVEL_ADMIN = 10;
+ /** used by self::user_modify() to keep current access level */
+ const ACCESS_LEVEL_KEEP_CURRENT = -1024;
+
+ /**
+ * @param int $level integer loglevel value
+ * @return UserHelper::ACCESS_LEVEL_* if valid, warn and return ACCESS_LEVEL_KEEP_CURRENT otherwise
+ */
+ public static function map_access_level(int $level) : int {
+ if (in_array($level, self::ACCESS_LEVELS)) {
+ /** @phpstan-ignore-next-line */
+ return $level;
+ } else {
+ user_error("Passed invalid user access level: $level", E_USER_WARNING);
+ return self::ACCESS_LEVEL_KEEP_CURRENT;
+ }
+ }
+
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;
@@ -133,7 +159,7 @@ class UserHelper {
if (empty($_SESSION["uid"])) {
if (Config::get(Config::AUTH_AUTO_LOGIN) && self::authenticate(null, null)) {
- $_SESSION["ref_schema_version"] = get_schema_version();
+ $_SESSION["ref_schema_version"] = Config::get_schema_version();
} else {
self::authenticate(null, null, true);
}
@@ -217,6 +243,7 @@ class UserHelper {
return substr(bin2hex(get_random_bytes(125)), 0, 250);
}
+ /** TODO: this should invoke UserHelper::user_modify() */
static function reset_password(int $uid, bool $format_output = false, string $new_password = ""): void {
$user = ORM::for_table('ttrss_users')->find_one($uid);
@@ -335,18 +362,14 @@ class UserHelper {
return null;
}
- 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 &&
- method_exists($authenticator, "check_password") &&
- $authenticator->check_password($_SESSION["uid"], "password")) {
-
- return true;
- }
- return false;
+ /**
+ * @param null|int $owner_uid if null, checks current user via session-specific auth module, if set works on internal database only
+ * @return bool
+ * @throws PDOException
+ * @throws Exception
+ */
+ static function is_default_password(?int $owner_uid = null): bool {
+ return self::user_has_password($owner_uid, 'password');
}
/**
@@ -380,4 +403,115 @@ class UserHelper {
else
return false;
}
+
+ /**
+ * @param string $login Login for new user (case-insensitive)
+ * @param string $password Password for new user (may not be blank)
+ * @param UserHelper::ACCESS_LEVEL_* $access_level Access level for new user
+ * @return bool true if user has been created
+ */
+ static function user_add(string $login, string $password, int $access_level) : bool {
+ $login = clean($login);
+
+ if ($login &&
+ $password &&
+ !self::find_user_by_login($login) &&
+ self::map_access_level((int)$access_level) != self::ACCESS_LEVEL_KEEP_CURRENT) {
+
+ $user = ORM::for_table('ttrss_users')->create();
+
+ $user->salt = self::get_salt();
+ $user->login = mb_strtolower($login);
+ $user->pwd_hash = self::hash_password($password, $user->salt);
+ $user->access_level = $access_level;
+ $user->created = Db::NOW();
+
+ return $user->save();
+ }
+
+ return false;
+ }
+
+ /**
+ * @param int $uid User ID to modify
+ * @param string $new_password set password to this value if its not blank
+ * @param UserHelper::ACCESS_LEVEL_* $access_level set user access level to this value if it is set (default ACCESS_LEVEL_KEEP_CURRENT)
+ * @return bool true if user record has been saved
+ *
+ * NOTE: $access_level is of mixed type because of intellephense
+ */
+ static function user_modify(int $uid, string $new_password = '', $access_level = self::ACCESS_LEVEL_KEEP_CURRENT) : bool {
+ $user = ORM::for_table('ttrss_users')->find_one($uid);
+
+ if ($user) {
+ if ($new_password != '') {
+ $new_salt = self::get_salt();
+ $pwd_hash = self::hash_password($new_password, $new_salt, self::HASH_ALGOS[0]);
+
+ $user->pwd_hash = $pwd_hash;
+ $user->salt = $new_salt;
+ }
+
+ if ($access_level != self::ACCESS_LEVEL_KEEP_CURRENT) {
+ $user->access_level = (int)$access_level;
+ }
+
+ return $user->save();
+ }
+
+ return false;
+ }
+
+ /**
+ * @param int $uid user ID to delete (this won't delete built-in admin user with UID 1)
+ * @return bool true if user has been deleted
+ */
+ static function user_delete(int $uid) : bool {
+ if ($uid != 1) {
+
+ $user = ORM::for_table('ttrss_users')->find_one($uid);
+
+ if ($user) {
+ // TODO: is it still necessary to split those queries?
+
+ ORM::for_table('ttrss_tags')
+ ->where('owner_uid', $uid)
+ ->delete_many();
+
+ ORM::for_table('ttrss_feeds')
+ ->where('owner_uid', $uid)
+ ->delete_many();
+
+ return $user->delete();
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param null|int $owner_uid if null, checks current user via session-specific auth module, if set works on internal database only
+ * @param string $password password to compare hash against
+ * @return bool
+ */
+ static function user_has_password(?int $owner_uid, string $password) : bool {
+ if ($owner_uid) {
+ $authenticator = new Auth_Internal();
+
+ return $authenticator->check_password($owner_uid, $password);
+ } else {
+ /** @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 &&
+ method_exists($authenticator, "check_password") &&
+ $authenticator->check_password($_SESSION["uid"], $password)) {
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
}