diff options
Diffstat (limited to 'classes')
-rwxr-xr-x | classes/api.php | 2 | ||||
-rwxr-xr-x | classes/article.php | 4 | ||||
-rw-r--r-- | classes/config.php | 14 | ||||
-rw-r--r-- | classes/counters.php | 38 | ||||
-rwxr-xr-x | classes/db.php | 8 | ||||
-rw-r--r-- | classes/db/migrations.php | 49 | ||||
-rw-r--r-- | classes/debug.php | 47 | ||||
-rw-r--r-- | classes/digest.php | 4 | ||||
-rw-r--r-- | classes/diskcache.php | 6 | ||||
-rwxr-xr-x | classes/feeditem/atom.php | 8 | ||||
-rwxr-xr-x | classes/feeditem/common.php | 2 | ||||
-rwxr-xr-x | classes/feeditem/rss.php | 2 | ||||
-rw-r--r-- | classes/feedparser.php | 14 | ||||
-rwxr-xr-x | classes/feeds.php | 42 | ||||
-rw-r--r-- | classes/handler.php | 6 | ||||
-rwxr-xr-x | classes/handler/public.php | 26 | ||||
-rw-r--r-- | classes/mailer.php | 15 | ||||
-rw-r--r-- | classes/plugin.php | 14 | ||||
-rwxr-xr-x | classes/pluginhost.php | 75 | ||||
-rwxr-xr-x | classes/pref/feeds.php | 16 | ||||
-rwxr-xr-x | classes/pref/filters.php | 15 | ||||
-rw-r--r-- | classes/pref/prefs.php | 42 | ||||
-rw-r--r-- | classes/prefs.php | 6 | ||||
-rwxr-xr-x | classes/rpc.php | 13 | ||||
-rwxr-xr-x | classes/rssutils.php | 39 | ||||
-rw-r--r-- | classes/urlhelper.php | 103 | ||||
-rw-r--r-- | classes/userhelper.php | 160 |
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; + } + } |