summaryrefslogtreecommitdiff
path: root/classes
diff options
context:
space:
mode:
Diffstat (limited to 'classes')
-rwxr-xr-xclasses/api.php80
-rwxr-xr-xclasses/article.php4
-rw-r--r--classes/config.php14
-rw-r--r--classes/counters.php40
-rwxr-xr-xclasses/db.php8
-rw-r--r--classes/db/migrations.php57
-rw-r--r--classes/debug.php47
-rw-r--r--classes/digest.php6
-rw-r--r--classes/diskcache.php8
-rwxr-xr-xclasses/feeditem/atom.php14
-rwxr-xr-xclasses/feeditem/common.php2
-rwxr-xr-xclasses/feeditem/rss.php6
-rw-r--r--classes/feedparser.php14
-rwxr-xr-xclasses/feeds.php57
-rw-r--r--classes/handler.php6
-rwxr-xr-xclasses/handler/public.php34
-rw-r--r--classes/mailer.php15
-rw-r--r--classes/plugin.php14
-rw-r--r--classes/pluginhandler.php6
-rwxr-xr-xclasses/pluginhost.php75
-rwxr-xr-xclasses/pref/feeds.php20
-rwxr-xr-xclasses/pref/filters.php15
-rw-r--r--classes/pref/labels.php2
-rw-r--r--classes/pref/prefs.php44
-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.php185
29 files changed, 554 insertions, 380 deletions
diff --git a/classes/api.php b/classes/api.php
index b17114693..262d5c5bc 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();
@@ -333,7 +333,7 @@ class API extends Handler {
'published' => self::_param_to_bool($entry->published),
'comments' => $entry->comments,
'author' => $entry->author,
- 'updated' => (int) strtotime($entry->updated),
+ 'updated' => (int) strtotime($entry->updated ?? ''),
'feed_id' => $entry->feed_id,
'attachments' => Article::_get_enclosures($entry->id),
'score' => (int) $entry->score,
@@ -544,6 +544,29 @@ class API extends Handler {
/* Virtual feeds */
+ $vfeeds = PluginHost::getInstance()->get_feeds(-1);
+
+ if (is_array($vfeeds)) {
+ foreach ($vfeeds as $feed) {
+ if (!implements_interface($feed['sender'], 'IVirtualFeed'))
+ continue;
+
+ /** @var IVirtualFeed $feed['sender'] */
+ $unread = $feed['sender']->get_unread($feed['id']);
+
+ if ($unread || !$unread_only) {
+ $row = [
+ 'id' => PluginHost::pfeed_to_feed_id($feed['id']),
+ 'title' => $feed['title'],
+ 'unread' => $unread,
+ 'cat_id' => -1,
+ ];
+
+ array_push($feeds, $row);
+ }
+ }
+ }
+
if ($cat_id == -4 || $cat_id == -1) {
foreach ([-1, -2, -3, -4, -6, 0] as $i) {
$unread = Feeds::_get_counters($i, false, true);
@@ -618,7 +641,7 @@ class API extends Handler {
'unread' => (int) $unread,
'has_icon' => $has_icon,
'cat_id' => (int) $feed->cat_id,
- 'last_updated' => (int) strtotime($feed->last_updated),
+ 'last_updated' => (int) strtotime($feed->last_updated ?? ''),
'order_id' => (int) $feed->order_id,
];
@@ -648,7 +671,7 @@ class API extends Handler {
->find_one($feed_id);
if ($feed) {
- $last_updated = strtotime($feed->last_updated);
+ $last_updated = strtotime($feed->last_updated ?? '');
$cache_images = self::_param_to_bool($feed->cache_images);
if (!$cache_images && time() - $last_updated > 120) {
@@ -675,7 +698,50 @@ class API extends Handler {
"skip_first_id_check" => $skip_first_id_check
);
- $qfh_ret = Feeds::_get_headlines($params);
+ $qfh_ret = [];
+
+ if (!$is_cat && is_numeric($feed_id) && $feed_id < PLUGIN_FEED_BASE_INDEX && $feed_id > LABEL_BASE_INDEX) {
+ $pfeed_id = PluginHost::feed_to_pfeed_id($feed_id);
+
+ /** @var IVirtualFeed|false $handler */
+ $handler = PluginHost::getInstance()->get_feed_handler($pfeed_id);
+
+ if ($handler) {
+ $params = array(
+ "feed" => $feed_id,
+ "limit" => $limit,
+ "view_mode" => $view_mode,
+ "cat_view" => $is_cat,
+ "search" => $search,
+ "override_order" => $order,
+ "offset" => $offset,
+ "since_id" => 0,
+ "include_children" => $include_nested,
+ "check_first_id" => $check_first_id,
+ "skip_first_id_check" => $skip_first_id_check
+ );
+
+ $qfh_ret = $handler->get_headlines($pfeed_id, $params);
+ }
+
+ } else {
+
+ $params = array(
+ "feed" => $feed_id,
+ "limit" => $limit,
+ "view_mode" => $view_mode,
+ "cat_view" => $is_cat,
+ "search" => $search,
+ "override_order" => $order,
+ "offset" => $offset,
+ "since_id" => $since_id,
+ "include_children" => $include_nested,
+ "check_first_id" => $check_first_id,
+ "skip_first_id_check" => $skip_first_id_check
+ );
+
+ $qfh_ret = Feeds::_get_headlines($params);
+ }
$result = $qfh_ret[0];
$feed_title = $qfh_ret[1];
@@ -725,7 +791,7 @@ class API extends Handler {
"unread" => self::_param_to_bool($line["unread"]),
"marked" => self::_param_to_bool($line["marked"]),
"published" => self::_param_to_bool($line["published"]),
- "updated" => (int)strtotime($line["updated"]),
+ "updated" => (int)strtotime($line["updated"] ?? ''),
"is_updated" => $is_updated,
"title" => $line["title"],
"link" => $line["link"],
@@ -835,7 +901,7 @@ class API extends Handler {
}
function getFeedTree(): bool {
- $include_empty = self::_param_to_bool(clean($_REQUEST['include_empty']));
+ $include_empty = self::_param_to_bool($_REQUEST['include_empty'] ?? false);
$pf = new Pref_Feeds($_REQUEST);
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..9699cb97c 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,
@@ -197,7 +193,7 @@ class Counters {
}
// hide default un-updated timestamp i.e. 1970-01-01 (?) -fox
- if ((int)date('Y') - (int)date('Y', strtotime($line['last_updated'])) > 2)
+ if ((int)date('Y') - (int)date('Y', strtotime($line['last_updated'] ?? '')) > 2)
$last_updated = '';
$cv = [
@@ -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..d63736987 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();
@@ -35,7 +17,7 @@ class Db_Migrations {
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}",
+ $this->initialize("{$plugin_dir}/{$schema_suffix}",
strtolower("ttrss_migrations_plugin_" . get_class($plugin)),
$base_is_latest);
}
@@ -49,7 +31,7 @@ class Db_Migrations {
}
private function set_version(int $version): void {
- Debug::log("Updating table {$this->migrations_table} with version ${version}...", Debug::LOG_EXTENDED);
+ Debug::log("Updating table {$this->migrations_table} with version {$version}...", Debug::LOG_EXTENDED);
$sth = $this->pdo->query("SELECT * FROM {$this->migrations_table}");
@@ -188,7 +170,7 @@ class Db_Migrations {
try {
$this->migrate_to($i);
} catch (PDOException $e) {
- user_error("Failed to apply migration ${i} for {$this->migrations_table}: " . $e->getMessage(), E_USER_WARNING);
+ user_error("Failed to apply migration {$i} for {$this->migrations_table}: " . $e->getMessage(), E_USER_WARNING);
return false;
//throw $e;
}
@@ -202,22 +184,19 @@ class Db_Migrations {
*/
private function get_lines(int $version) : array {
if ($version > 0)
- $filename = "{$this->migrations_path}/${version}.sql";
+ $filename = "{$this->migrations_path}/{$version}.sql";
else
$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;
- });
+ $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)), function ($line) {
- return strlen(trim($line)) > 0 && !in_array(strtolower($line), ["begin", "commit"]);
- });
+ 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);
+ user_error("Requested schema file {$filename} not found.", E_USER_ERROR);
return [];
}
}
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..a5dbc0945 100644
--- a/classes/digest.php
+++ b/classes/digest.php
@@ -22,7 +22,7 @@ class Digest
while ($line = $res->fetch()) {
if (get_pref(Prefs::DIGEST_ENABLE, $line['id'])) {
- $preferred_ts = strtotime(get_pref(Prefs::DIGEST_PREFERRED_TIME, $line['id']));
+ $preferred_ts = strtotime(get_pref(Prefs::DIGEST_PREFERRED_TIME, $line['id']) ?? '');
// try to send digests within 2 hours of preferred time
if ($preferred_ts && time() >= $preferred_ts &&
@@ -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..1df8daf15 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',
@@ -308,7 +306,7 @@ class DiskCache {
if ($fake_extension)
$fake_extension = ".$fake_extension";
- header("Content-Disposition: inline; filename=\"${filename}${fake_extension}\"");
+ header("Content-Disposition: inline; filename=\"{$filename}{$fake_extension}\"");
return $this->send_local_file($this->get_full_path($filename));
}
diff --git a/classes/feeditem/atom.php b/classes/feeditem/atom.php
index cac6d8c54..f6c96f959 100755
--- a/classes/feeditem/atom.php
+++ b/classes/feeditem/atom.php
@@ -19,19 +19,19 @@ class FeedItem_Atom extends FeedItem_Common {
$updated = $this->elem->getElementsByTagName("updated")->item(0);
if ($updated) {
- return strtotime($updated->nodeValue);
+ return strtotime($updated->nodeValue ?? '');
}
$published = $this->elem->getElementsByTagName("published")->item(0);
if ($published) {
- return strtotime($published->nodeValue);
+ return strtotime($published->nodeValue ?? '');
}
$date = $this->xpath->query("dc:date", $this->elem)->item(0);
if ($date) {
- return strtotime($date->nodeValue);
+ return strtotime($date->nodeValue ?? '');
}
// consistent with strtotime failing to parse
@@ -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..e07fd1d06 100755
--- a/classes/feeditem/rss.php
+++ b/classes/feeditem/rss.php
@@ -17,13 +17,13 @@ class FeedItem_RSS extends FeedItem_Common {
$pubDate = $this->elem->getElementsByTagName("pubDate")->item(0);
if ($pubDate) {
- return strtotime($pubDate->nodeValue);
+ return strtotime($pubDate->nodeValue ?? '');
}
$date = $this->xpath->query("dc:date", $this->elem)->item(0);
if ($date) {
- return strtotime($date->nodeValue);
+ return strtotime($date->nodeValue ?? '');
}
// consistent with strtotime failing to parse
@@ -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..a063b9ed5 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((int)Debug::map_loglevel($xdebug));
$feed_id = (int)$_REQUEST["feed_id"];
$do_update = ($_REQUEST["action"] ?? "") == "do_update";
@@ -963,6 +965,15 @@ class Feeds extends Handler_Protected {
if ($is_cat) {
return self::_get_cat_unread($n_feed, $owner_uid);
+ } else if(is_numeric($feed) && $feed < PLUGIN_FEED_BASE_INDEX && $feed > LABEL_BASE_INDEX) { // virtual Feed
+ $feed_id = PluginHost::feed_to_pfeed_id($feed);
+ $handler = PluginHost::getInstance()->get_feed_handler($feed_id);
+ if (implements_interface($handler, 'IVirtualFeed')) {
+ /** @var IVirtualFeed $handler */
+ return $handler->get_unread($feed_id);
+ } else {
+ return 0;
+ }
} else if ($n_feed == -6) {
return 0;
// tags
@@ -1738,11 +1749,11 @@ class Feeds extends Handler_Protected {
}
if (!$allow_archived) {
- $from_qpart = "${ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id), ttrss_feeds";
+ $from_qpart = "{$ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id), ttrss_feeds";
$feed_check_qpart = "ttrss_user_entries.feed_id = ttrss_feeds.id AND";
} else {
- $from_qpart = "${ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id)
+ $from_qpart = "{$ext_tables_part}ttrss_entries LEFT JOIN ttrss_user_entries ON (ref_id = ttrss_entries.id)
LEFT JOIN ttrss_feeds ON (feed_id = ttrss_feeds.id)";
$feed_check_qpart = "";
}
@@ -1936,8 +1947,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 +1967,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 +1988,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);
@@ -2226,7 +2238,7 @@ class Feeds extends Handler_Protected {
* @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)), ' ');
+ $keywords = str_getcsv(preg_replace('/(-?\w+)\:"(\w+)/', '"{$1}:{$2}', trim($search)), ' ');
$query_keywords = array();
$search_words = array();
$search_query_leftover = array();
@@ -2357,8 +2369,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 +2395,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..08b73b87d 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, '...'));
@@ -128,13 +128,13 @@ class Handler_Public extends Handler {
$tpl->setVariable('ARTICLE_CONTENT', $content, true);
$tpl->setVariable('ARTICLE_UPDATED_ATOM',
- date('c', strtotime($line["updated"])), true);
+ date('c', strtotime($line["updated"] ?? '')), true);
$tpl->setVariable('ARTICLE_UPDATED_RFC822',
- date(DATE_RFC822, strtotime($line["updated"])), true);
+ date(DATE_RFC822, strtotime($line["updated"] ?? '')), true);
$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) {
@@ -214,11 +214,16 @@ class Handler_Public extends Handler {
$article['title'] = $line['title'];
$article['excerpt'] = $line["content_preview"];
$article['content'] = Sanitizer::sanitize($line["content"], false, $owner_uid, $feed_site_url, null, $line["id"]);
- $article['updated'] = date('c', strtotime($line["updated"]));
+ $article['updated'] = date('c', strtotime($line["updated"] ?? ''));
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,7 @@ class Handler_Public extends Handler {
if (UserHelper::authenticate($login, $password)) {
$_POST["password"] = "";
- if (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 +413,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 +563,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 +785,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 +807,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/pluginhandler.php b/classes/pluginhandler.php
index 5c73920e5..a6f0a4965 100644
--- a/classes/pluginhandler.php
+++ b/classes/pluginhandler.php
@@ -14,15 +14,15 @@ class PluginHandler extends Handler_Protected {
if (validate_csrf($csrf_token) || $plugin->csrf_ignore($method)) {
$plugin->$method();
} else {
- user_error("Rejected ${plugin_name}->${method}(): invalid CSRF token.", E_USER_WARNING);
+ user_error("Rejected {$plugin_name}->{$method}(): invalid CSRF token.", E_USER_WARNING);
print Errors::to_json(Errors::E_UNAUTHORIZED);
}
} else {
- user_error("Rejected ${plugin_name}->${method}(): unknown method.", E_USER_WARNING);
+ user_error("Rejected {$plugin_name}->{$method}(): unknown method.", E_USER_WARNING);
print Errors::to_json(Errors::E_UNKNOWN_METHOD);
}
} else {
- user_error("Rejected ${plugin_name}->${method}(): unknown plugin.", E_USER_WARNING);
+ user_error("Rejected {$plugin_name}->{$method}(): unknown plugin.", E_USER_WARNING);
print Errors::to_json(Errors::E_UNKNOWN_PLUGIN);
}
}
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..f2e8e12da 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']);
}
}
}
@@ -373,7 +373,7 @@ class Pref_Feeds extends Handler_Protected {
$order_id = 1;
- $cat = $data_map[$item_id];
+ $cat = ($data_map[$item_id] ?? false);
if ($cat && is_array($cat)) {
foreach ($cat as $item) {
@@ -436,7 +436,7 @@ class Pref_Feeds extends Handler_Protected {
foreach ($data['items'] as $item) {
# if ($item['id'] != 'root') {
- if (is_array($item['items'])) {
+ if (is_array($item['items'] ?? false)) {
if (isset($item['items']['_reference'])) {
$data_map[$item['id']] = array($item['items']);
} else {
@@ -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/labels.php b/classes/pref/labels.php
index a50a85a66..2e128691e 100644
--- a/classes/pref/labels.php
+++ b/classes/pref/labels.php
@@ -61,7 +61,7 @@ class Pref_Labels extends Handler_Protected {
if ($kind == "fg" || $kind == "bg") {
$sth = $this->pdo->prepare("UPDATE ttrss_labels2 SET
- ${kind}_color = ? WHERE id = ?
+ {$kind}_color = ? WHERE id = ?
AND owner_uid = ?");
$sth->execute([$color, $id, $_SESSION['uid']]);
diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php
index 8c044b49f..0c1b90213 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;
@@ -242,7 +240,7 @@ class Pref_Prefs extends Handler_Protected {
$user->full_name = clean($_POST['full_name']);
if ($user->email != $new_email) {
- Logger::log(E_USER_NOTICE, "Email address of user ".$user->login." has been changed to ${new_email}.");
+ Logger::log(E_USER_NOTICE, "Email address of user {$user->login} has been changed to {$new_email}.");
if ($user->email) {
$mailer = new Mailer();
@@ -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..4d9f30548 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;
@@ -57,19 +83,15 @@ class UserHelper {
$user = ORM::for_table('ttrss_users')->find_one($user_id);
if ($user && $user->access_level != self::ACCESS_LEVEL_DISABLED) {
- $_SESSION["uid"] = $user_id;
+ self::set_session_for_user($user_id);
$_SESSION["auth_module"] = $auth_module;
$_SESSION["name"] = $user->login;
$_SESSION["access_level"] = $user->access_level;
- $_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
- $_SESSION["ip_address"] = UserHelper::get_user_ip();
$_SESSION["pwd_hash"] = $user->pwd_hash;
$user->last_login = Db::NOW();
$user->save();
- $_SESSION["last_login_update"] = time();
-
return true;
}
@@ -82,8 +104,7 @@ class UserHelper {
return false;
} else {
-
- $_SESSION["uid"] = 1;
+ self::set_session_for_user(1);
$_SESSION["name"] = "admin";
$_SESSION["access_level"] = self::ACCESS_LEVEL_ADMIN;
@@ -92,12 +113,20 @@ class UserHelper {
$_SESSION["auth_module"] = false;
- if (empty($_SESSION["csrf_token"]))
- $_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
+ return true;
+ }
+ }
- $_SESSION["ip_address"] = UserHelper::get_user_ip();
+ static function set_session_for_user(int $owner_uid): void {
+ $_SESSION["uid"] = $owner_uid;
+ $_SESSION["last_login_update"] = time();
+ $_SESSION["ip_address"] = UserHelper::get_user_ip();
- return true;
+ if (empty($_SESSION["csrf_token"]))
+ $_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
+
+ if (Config::get_schema_version() >= 120) {
+ $_SESSION["language"] = get_pref(Prefs::USER_LANGUAGE, $owner_uid);
}
}
@@ -133,7 +162,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 +246,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 +365,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 +406,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;
+ }
+
}