1
0
mirror of https://git.tt-rss.org/git/tt-rss.git synced 2025-12-13 01:46:00 +00:00

Merge branch 'wip-phpstan-level6'

This commit is contained in:
Andrew Dolgov
2021-11-18 07:32:28 +03:00
697 changed files with 6687 additions and 3692 deletions

View File

@@ -124,7 +124,7 @@
$handler = $reflection->newInstanceWithoutConstructor();
}
if ($handler && implements_interface($handler, 'IHandler')) {
if (implements_interface($handler, 'IHandler')) {
$handler->__construct($_REQUEST);
if (validate_csrf($csrf_token) || $handler->csrf_ignore($method)) {

View File

@@ -13,13 +13,20 @@ class API extends Handler {
const E_UNKNOWN_METHOD = "UNKNOWN_METHOD";
const E_OPERATION_FAILED = "E_OPERATION_FAILED";
/** @var int|null */
private $seq;
private static function _param_to_bool($p) {
/**
* @param mixed $p
*/
private static function _param_to_bool($p): bool {
return $p && ($p !== "f" && $p !== "false");
}
private function _wrap($status, $reply) {
/**
* @param array<int|string, mixed> $reply
*/
private function _wrap(int $status, array $reply): void {
print json_encode([
"seq" => $this->seq,
"status" => $status,
@@ -27,7 +34,7 @@ class API extends Handler {
]);
}
function before($method) {
function before(string $method): bool {
if (parent::before($method)) {
header("Content-Type: text/json");
@@ -48,17 +55,17 @@ class API extends Handler {
return false;
}
function getVersion() {
function getVersion(): void {
$rv = array("version" => Config::get_version());
$this->_wrap(self::STATUS_OK, $rv);
}
function getApiLevel() {
function getApiLevel(): void {
$rv = array("level" => self::API_LEVEL);
$this->_wrap(self::STATUS_OK, $rv);
}
function login() {
function login(): void {
if (session_status() == PHP_SESSION_ACTIVE) {
session_destroy();
@@ -87,22 +94,20 @@ class API extends Handler {
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_API_DISABLED));
}
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
return;
}
$this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
}
function logout() {
function logout(): void {
UserHelper::logout();
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
function isLoggedIn() {
function isLoggedIn(): void {
$this->_wrap(self::STATUS_OK, array("status" => $_SESSION["uid"] != ''));
}
function getUnread() {
function getUnread(): void {
$feed_id = clean($_REQUEST["feed_id"] ?? "");
$is_cat = clean($_REQUEST["is_cat"] ?? "");
@@ -114,12 +119,12 @@ class API extends Handler {
}
/* Method added for ttrss-reader for Android */
function getCounters() {
function getCounters(): void {
$this->_wrap(self::STATUS_OK, Counters::get_all());
}
function getFeeds() {
$cat_id = clean($_REQUEST["cat_id"]);
function getFeeds(): void {
$cat_id = (int) clean($_REQUEST["cat_id"]);
$unread_only = self::_param_to_bool(clean($_REQUEST["unread_only"] ?? 0));
$limit = (int) clean($_REQUEST["limit"] ?? 0);
$offset = (int) clean($_REQUEST["offset"] ?? 0);
@@ -130,7 +135,7 @@ class API extends Handler {
$this->_wrap(self::STATUS_OK, $feeds);
}
function getCategories() {
function getCategories(): void {
$unread_only = self::_param_to_bool(clean($_REQUEST["unread_only"] ?? false));
$enable_nested = self::_param_to_bool(clean($_REQUEST["enable_nested"] ?? false));
$include_empty = self::_param_to_bool(clean($_REQUEST['include_empty'] ?? false));
@@ -186,11 +191,11 @@ class API extends Handler {
$this->_wrap(self::STATUS_OK, $cats);
}
function getHeadlines() {
function getHeadlines(): void {
$feed_id = clean($_REQUEST["feed_id"]);
if ($feed_id !== "") {
if ($feed_id !== "" && is_numeric($feed_id)) {
if (is_numeric($feed_id)) $feed_id = (int) $feed_id;
$feed_id = (int) $feed_id;
$limit = (int)clean($_REQUEST["limit"] ?? 0 );
@@ -237,7 +242,7 @@ class API extends Handler {
}
}
function updateArticle() {
function updateArticle(): void {
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
$mode = (int) clean($_REQUEST["mode"]);
$data = clean($_REQUEST["data"] ?? "");
@@ -303,7 +308,7 @@ class API extends Handler {
}
function getArticle() {
function getArticle(): void {
$article_ids = explode(',', clean($_REQUEST['article_id'] ?? ''));
$sanitize_content = self::_param_to_bool($_REQUEST['sanitize'] ?? true);
@@ -351,7 +356,7 @@ class API extends Handler {
$article['content'] = Sanitizer::sanitize(
$entry->content,
self::_param_to_bool($entry->hide_images),
false, $entry->site_url, false, $entry->id);
null, $entry->site_url, null, $entry->id);
} else {
$article['content'] = $entry->content;
}
@@ -375,7 +380,10 @@ class API extends Handler {
}
}
private function _get_config() {
/**
* @return array<string, array<string, string>|bool|int|string>
*/
private function _get_config(): array {
$config = [
"icons_dir" => Config::get(Config::ICONS_DIR),
"icons_url" => Config::get(Config::ICONS_URL)
@@ -391,13 +399,13 @@ class API extends Handler {
return $config;
}
function getConfig() {
function getConfig(): void {
$config = $this->_get_config();
$this->_wrap(self::STATUS_OK, $config);
}
function updateFeed() {
function updateFeed(): void {
$feed_id = (int) clean($_REQUEST["feed_id"]);
if (!ini_get("open_basedir")) {
@@ -407,10 +415,10 @@ class API extends Handler {
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
function catchupFeed() {
function catchupFeed(): void {
$feed_id = clean($_REQUEST["feed_id"]);
$is_cat = clean($_REQUEST["is_cat"]);
@$mode = clean($_REQUEST["mode"]);
$mode = clean($_REQUEST["mode"] ?? "");
if (!in_array($mode, ["all", "1day", "1week", "2week"]))
$mode = "all";
@@ -420,13 +428,13 @@ class API extends Handler {
$this->_wrap(self::STATUS_OK, array("status" => "OK"));
}
function getPref() {
function getPref(): void {
$pref_name = clean($_REQUEST["pref_name"]);
$this->_wrap(self::STATUS_OK, array("value" => get_pref($pref_name)));
}
function getLabels() {
function getLabels(): void {
$article_id = (int)clean($_REQUEST['article_id'] ?? -1);
$rv = [];
@@ -462,7 +470,7 @@ class API extends Handler {
$this->_wrap(self::STATUS_OK, $rv);
}
function setArticleLabel() {
function setArticleLabel(): void {
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
$label_id = (int) clean($_REQUEST['label_id']);
@@ -477,9 +485,9 @@ class API extends Handler {
foreach ($article_ids as $id) {
if ($assign)
Labels::add_article($id, $label, $_SESSION["uid"]);
Labels::add_article((int)$id, $label, $_SESSION["uid"]);
else
Labels::remove_article($id, $label, $_SESSION["uid"]);
Labels::remove_article((int)$id, $label, $_SESSION["uid"]);
++$num_updated;
@@ -491,7 +499,7 @@ class API extends Handler {
}
function index($method) {
function index(string $method): void {
$plugin = PluginHost::getInstance()->get_api_method(strtolower($method));
if ($plugin && method_exists($plugin, $method)) {
@@ -504,7 +512,7 @@ class API extends Handler {
}
}
function shareToPublished() {
function shareToPublished(): void {
$title = strip_tags(clean($_REQUEST["title"]));
$url = strip_tags(clean($_REQUEST["url"]));
$content = strip_tags(clean($_REQUEST["content"]));
@@ -516,13 +524,12 @@ class API extends Handler {
}
}
private static function _api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested = false) {
/**
* @return array<int, array{'id': int, 'title': string, 'unread': int, 'cat_id': int}>
*/
private static function _api_get_feeds(int $cat_id, bool $unread_only, int $limit, int $offset, bool $include_nested = false): array {
$feeds = [];
$limit = (int) $limit;
$offset = (int) $offset;
$cat_id = (int) $cat_id;
/* Labels */
/* API only: -4 All feeds, including virtual feeds */
@@ -632,13 +639,16 @@ class API extends Handler {
return $feeds;
}
private static function _api_get_headlines($feed_id, $limit, $offset,
$filter, $is_cat, $show_excerpt, $show_content, $view_mode, $order,
$include_attachments, $since_id,
$search = "", $include_nested = false, $sanitize_content = true,
$force_update = false, $excerpt_length = 100, $check_first_id = false, $skip_first_id_check = false) {
/**
* @return array{0: array<int, array<string, mixed>>, 1: array<string, mixed>} $headlines, $headlines_header
*/
private static function _api_get_headlines(int $feed_id, int $limit, int $offset,
string $filter, bool $is_cat, bool $show_excerpt, bool $show_content, ?string $view_mode, string $order,
bool $include_attachments, int $since_id, string $search = "", bool $include_nested = false,
bool $sanitize_content = true, bool $force_update = false, int $excerpt_length = 100, ?int $check_first_id = null,
bool $skip_first_id_check = false): array {
if ($force_update && $feed_id > 0 && is_numeric($feed_id)) {
if ($force_update && is_numeric($feed_id) && $feed_id > 0) {
// Update the feed if required with some basic flood control
$feed = ORM::for_table('ttrss_feeds')
@@ -746,7 +756,7 @@ class API extends Handler {
$headline_row["content"] = Sanitizer::sanitize(
$line["content"],
self::_param_to_bool($line['hide_images']),
false, $line["site_url"], false, $line["id"]);
null, $line["site_url"], null, $line["id"]);
} else {
$headline_row["content"] = $line["content"];
}
@@ -803,7 +813,7 @@ class API extends Handler {
return array($headlines, $headlines_header);
}
function unsubscribeFeed() {
function unsubscribeFeed(): void {
$feed_id = (int) clean($_REQUEST["feed_id"]);
$feed_exists = ORM::for_table('ttrss_feeds')
@@ -818,7 +828,7 @@ class API extends Handler {
}
}
function subscribeToFeed() {
function subscribeToFeed(): void {
$feed_url = clean($_REQUEST["feed_url"]);
$category_id = (int) clean($_REQUEST["category_id"]);
$login = clean($_REQUEST["login"]);
@@ -833,7 +843,7 @@ class API extends Handler {
}
}
function getFeedTree() {
function getFeedTree(): void {
$include_empty = self::_param_to_bool(clean($_REQUEST['include_empty']));
$pf = new Pref_Feeds($_REQUEST);
@@ -846,7 +856,7 @@ class API extends Handler {
}
// only works for labels or uncategorized for the time being
private function _is_cat_empty($id) {
private function _is_cat_empty(int $id): bool {
if ($id == -2) {
$label_count = ORM::for_table('ttrss_labels2')
->where('owner_uid', $_SESSION['uid'])
@@ -865,7 +875,8 @@ class API extends Handler {
return false;
}
private function _get_custom_sort_types() {
/** @return array<string, string> */
private function _get_custom_sort_types(): array {
$ret = [];
PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_HEADLINES_CUSTOM_SORT_MAP, function ($result) use (&$ret) {

View File

@@ -4,7 +4,11 @@ class Article extends Handler_Protected {
const ARTICLE_KIND_VIDEO = 2;
const ARTICLE_KIND_YOUTUBE = 3;
function redirect() {
const CATCHUP_MODE_MARK_AS_READ = 0;
const CATCHUP_MODE_MARK_AS_UNREAD = 1;
const CATCHUP_MODE_TOGGLE = 2;
function redirect(): void {
$article = ORM::for_table('ttrss_entries')
->table_alias('e')
->join('ttrss_user_entries', [ 'ref_id', '=', 'e.id'], 'ue')
@@ -24,8 +28,7 @@ class Article extends Handler_Protected {
print "Article not found or has an empty URL.";
}
static function _create_published_article($title, $url, $content, $labels_str,
$owner_uid) {
static function _create_published_article(string $title, string $url, string $content, string $labels_str, int $owner_uid): bool {
$guid = 'SHA1:' . sha1("ttshared:" . $url . $owner_uid); // include owner_uid to prevent global GUID clash
@@ -158,14 +161,14 @@ class Article extends Handler_Protected {
return $rc;
}
function printArticleTags() {
function printArticleTags(): void {
$id = (int) clean($_REQUEST['id'] ?? 0);
print json_encode(["id" => $id,
"tags" => self::_get_tags($id)]);
}
function setScore() {
function setScore(): void {
$ids = array_map("intval", clean($_REQUEST['ids'] ?? []));
$score = (int)clean($_REQUEST['score']);
@@ -179,14 +182,14 @@ class Article extends Handler_Protected {
print json_encode(["id" => $ids, "score" => $score]);
}
function setArticleTags() {
function setArticleTags(): void {
$id = clean($_REQUEST["id"]);
//$tags_str = clean($_REQUEST["tags_str"]);
//$tags = array_unique(array_map('trim', explode(",", $tags_str)));
$tags = FeedItem_Common::normalize_categories(explode(",", clean($_REQUEST["tags_str"])));
$tags = FeedItem_Common::normalize_categories(explode(",", clean($_REQUEST["tags_str"] ?? "")));
$this->pdo->beginTransaction();
@@ -254,18 +257,18 @@ class Article extends Handler_Protected {
print "</ul>";
}*/
function assigntolabel() {
return $this->_label_ops(true);
function assigntolabel(): void {
$this->_label_ops(true);
}
function removefromlabel() {
return $this->_label_ops(false);
function removefromlabel(): void {
$this->_label_ops(false);
}
private function _label_ops($assign) {
private function _label_ops(bool $assign): void {
$reply = array();
$ids = explode(",", clean($_REQUEST["ids"]));
$ids = array_map("intval", array_filter(explode(",", clean($_REQUEST["ids"] ?? "")), "strlen"));
$label_id = clean($_REQUEST["lid"]);
$label = Labels::find_caption($label_id, $_SESSION["uid"]);
@@ -289,11 +292,10 @@ class Article extends Handler_Protected {
print json_encode($reply);
}
static function _format_enclosures($id,
$always_display_enclosures,
$article_content,
$hide_images = false) {
/**
* @return array{'formatted': string, 'entries': array<int, array<string, mixed>>}
*/
static function _format_enclosures(int $id, bool $always_display_enclosures, string $article_content, bool $hide_images = false): array {
$enclosures = self::_get_enclosures($id);
$enclosures_formatted = "";
@@ -366,7 +368,10 @@ class Article extends Handler_Protected {
return $rv;
}
static function _get_tags($id, $owner_uid = 0, $tag_cache = false) {
/**
* @return array<int, string>
*/
static function _get_tags(int $id, int $owner_uid = 0, ?string $tag_cache = null): array {
$a_id = $id;
@@ -383,12 +388,14 @@ class Article extends Handler_Protected {
/* check cache first */
if ($tag_cache === false) {
if (!$tag_cache) {
$csth = $pdo->prepare("SELECT tag_cache FROM ttrss_user_entries
WHERE ref_id = ? AND owner_uid = ?");
$csth->execute([$id, $owner_uid]);
if ($row = $csth->fetch()) $tag_cache = $row["tag_cache"];
if ($row = $csth->fetch()) {
$tag_cache = $row["tag_cache"];
}
}
if ($tag_cache) {
@@ -416,7 +423,7 @@ class Article extends Handler_Protected {
return $tags;
}
function getmetadatabyid() {
function getmetadatabyid(): void {
$article = ORM::for_table('ttrss_entries')
->join('ttrss_user_entries', ['ref_id', '=', 'id'], 'ue')
->where('ue.owner_uid', $_SESSION['uid'])
@@ -429,7 +436,10 @@ class Article extends Handler_Protected {
}
}
static function _get_enclosures($id) {
/**
* @return array<int, array<string, mixed>>
*/
static function _get_enclosures(int $id): array {
$encs = ORM::for_table('ttrss_enclosures')
->where('post_id', $id)
->find_many();
@@ -452,7 +462,7 @@ class Article extends Handler_Protected {
}
static function _purge_orphans() {
static function _purge_orphans(): void {
// purge orphaned posts in main content table
@@ -471,7 +481,11 @@ class Article extends Handler_Protected {
}
}
static function _catchup_by_id($ids, $cmode, $owner_uid = false) {
/**
* @param array<int, int> $ids
* @param int $cmode Article::CATCHUP_MODE_*
*/
static function _catchup_by_id($ids, int $cmode, ?int $owner_uid = null): void {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
@@ -479,11 +493,11 @@ class Article extends Handler_Protected {
$ids_qmarks = arr_qmarks($ids);
if ($cmode == 1) {
if ($cmode == Article::CATCHUP_MODE_MARK_AS_UNREAD) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET
unread = true
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else if ($cmode == 2) {
} else if ($cmode == Article::CATCHUP_MODE_TOGGLE) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET
unread = NOT unread,last_read = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
@@ -496,7 +510,10 @@ class Article extends Handler_Protected {
$sth->execute(array_merge($ids, [$owner_uid]));
}
static function _get_labels($id, $owner_uid = false) {
/**
* @return array<int, array<int, int|string>>
*/
static function _get_labels(int $id, ?int $owner_uid = null): array {
$rv = array();
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
@@ -536,6 +553,8 @@ class Article extends Handler_Protected {
}
if (count($rv) > 0)
// PHPStan has issues with the shape of $rv for some reason (array vs non-empty-array).
// @phpstan-ignore-next-line
Labels::update_cache($owner_uid, $id, $rv);
else
Labels::update_cache($owner_uid, $id, array("no-labels" => 1));
@@ -543,6 +562,12 @@ class Article extends Handler_Protected {
return $rv;
}
/**
* @param array<int, array<string, mixed>> $enclosures
* @param array<string, mixed> $headline
*
* @return array<int, Article::ARTICLE_KIND_*|string>
*/
static function _get_image(array $enclosures, string $content, string $site_url, array $headline) {
$article_image = "";
@@ -577,6 +602,7 @@ class Article extends Handler_Protected {
} else if ($e->nodeName == "video") {
$article_image = $e->getAttribute("poster");
/** @var DOMElement|null $src */
$src = $tmpxpath->query("//source[@src]", $e)->item(0);
if ($src) {
@@ -603,14 +629,14 @@ class Article extends Handler_Protected {
}
if ($article_image) {
$article_image = rewrite_relative_url($site_url, $article_image);
$article_image = UrlHelper::rewrite_relative($site_url, $article_image);
if (!$article_kind && (count($enclosures) > 1 || (isset($elems) && $elems->length > 1)))
$article_kind = Article::ARTICLE_KIND_ALBUM;
}
if ($article_stream)
$article_stream = rewrite_relative_url($site_url, $article_stream);
$article_stream = UrlHelper::rewrite_relative($site_url, $article_stream);
}
$cache = new DiskCache("images");
@@ -624,7 +650,12 @@ class Article extends Handler_Protected {
return [$article_image, $article_stream, $article_kind];
}
// only cached, returns label ids (not label feed ids)
/**
* only cached, returns label ids (not label feed ids)
*
* @param array<int, int> $article_ids
* @return array<int, int>
*/
static function _labels_of(array $article_ids) {
if (count($article_ids) == 0)
return [];
@@ -651,6 +682,10 @@ class Article extends Handler_Protected {
return array_unique($rv);
}
/**
* @param array<int, int> $article_ids
* @return array<int, int>
*/
static function _feeds_of(array $article_ids) {
if (count($article_ids) == 0)
return [];

View File

@@ -8,13 +8,18 @@ abstract class Auth_Base extends Plugin implements IAuthModule {
$this->pdo = Db::pdo();
}
// compatibility wrapper, because of how pluginhost works (hook name == method name)
function hook_auth_user(...$args) {
return $this->authenticate(...$args);
function hook_auth_user($login, $password, $service = '') {
return $this->authenticate($login, $password, $service);
}
// Auto-creates specified user if allowed by system configuration
// Can be used instead of find_user_by_login() by external auth modules
/** Auto-creates specified user if allowed by system configuration.
* Can be used instead of find_user_by_login() by external auth modules
* @param string $login
* @param string|false $password
* @return null|int
* @throws Exception
* @throws PDOException
*/
function auto_create_user(string $login, $password = false) {
if ($login && Config::get(Config::AUTH_AUTO_CREATE)) {
$user_id = UserHelper::find_user_by_login($login);
@@ -42,7 +47,12 @@ abstract class Auth_Base extends Plugin implements IAuthModule {
return UserHelper::find_user_by_login($login);
}
// @deprecated
/** replaced with UserHelper::find_user_by_login()
* @param string $login
* @return null|int
* @deprecated
*/
function find_user_by_login(string $login) {
return UserHelper::find_user_by_login($login);
}

View File

@@ -231,9 +231,13 @@ class Config {
Config::T_STRING ],
];
/** @var Config|null */
private static $instance;
/** @var array<string, array<bool|int|string>> */
private $params = [];
/** @var array<string, mixed> */
private $version = [];
/** @var Db_Migrations|null $migrations */
@@ -268,10 +272,16 @@ class Config {
directory, its contents are displayed instead of git commit-based version, this could be generated
based on source git tree commit used when creating the package */
/**
* @return array<string, mixed>|string
*/
static function get_version(bool $as_string = true) {
return self::get_instance()->_get_version($as_string);
}
/**
* @return array<string, mixed>|string
*/
private function _get_version(bool $as_string = true) {
$root_dir = dirname(__DIR__);
@@ -289,7 +299,10 @@ class Config {
user_error("Unable to determine version: " . $this->version["version"], E_USER_WARNING);
$this->version["version"] = "UNKNOWN (Unsupported, Git error)";
} else if (!getenv("TTRSS_SELF_URL_PATH") || !file_exists("/.dockerenv")) {
$this->version["version"] .= " (Unsupported)";
}
} else {
$this->version["version"] = "UNKNOWN (Unsupported)";
}
@@ -298,7 +311,10 @@ class Config {
return $as_string ? $this->version["version"] : $this->version;
}
static function get_version_from_git(string $dir) {
/**
* @return array<string, int|string>
*/
static function get_version_from_git(string $dir): array {
$descriptorspec = [
1 => ["pipe", "w"], // STDOUT
2 => ["pipe", "w"], // STDERR
@@ -364,6 +380,9 @@ class Config {
return self::get_migrations()->get_version();
}
/**
* @return bool|int|string
*/
static function cast_to(string $value, int $type_hint) {
switch ($type_hint) {
case self::T_BOOL:
@@ -375,24 +394,30 @@ class Config {
}
}
/**
* @return bool|int|string
*/
private function _get(string $param) {
list ($value, $type_hint) = $this->params[$param];
return $this->cast_to($value, $type_hint);
}
private function _add(string $param, string $default, int $type_hint) {
private function _add(string $param, string $default, int $type_hint): void {
$override = getenv(self::_ENVVAR_PREFIX . $param);
$this->params[$param] = [ self::cast_to($override !== false ? $override : $default, $type_hint), $type_hint ];
}
static function add(string $param, string $default, int $type_hint = Config::T_STRING) {
static function add(string $param, string $default, int $type_hint = Config::T_STRING): void {
$instance = self::get_instance();
return $instance->_add($param, $default, $type_hint);
$instance->_add($param, $default, $type_hint);
}
/**
* @return bool|int|string
*/
static function get(string $param) {
$instance = self::get_instance();
@@ -431,6 +456,9 @@ class Config {
/* sanity check stuff */
/**
* @return array<int, array<string, string>> A list of entries identifying tt-rss tables with bad config
*/
private static function check_mysql_tables() {
$pdo = Db::pdo();
@@ -447,7 +475,7 @@ class Config {
return $bad_tables;
}
static function sanity_check() {
static function sanity_check(): void {
/*
we don't actually need the DB object right now but some checks below might use ORM which won't be initialized
@@ -621,11 +649,11 @@ class Config {
}
}
private static function format_error($msg) {
private static function format_error(string $msg): string {
return "<div class=\"alert alert-danger\">$msg</div>";
}
static function get_override_links() {
static function get_override_links(): string {
$rv = "";
$local_css = get_theme_path(self::get(self::LOCAL_OVERRIDE_STYLESHEET));
@@ -637,7 +665,7 @@ class Config {
return $rv;
}
static function get_user_agent() {
static function get_user_agent(): string {
return sprintf(self::get(self::HTTP_USER_AGENT), self::get_version());
}
}

View File

@@ -1,7 +1,10 @@
<?php
class Counters {
static function get_all() {
/**
* @return array<int, array<string, int|string>>
*/
static function get_all(): array {
return array_merge(
self::get_global(),
self::get_virt(),
@@ -11,7 +14,12 @@ class Counters {
);
}
static function get_conditional(array $feed_ids = null, array $label_ids = null) {
/**
* @param array<int> $feed_ids
* @param array<int> $label_ids
* @return array<int, array<string, int|string>>
*/
static function get_conditional(array $feed_ids = null, array $label_ids = null): array {
return array_merge(
self::get_global(),
self::get_virt(),
@@ -21,7 +29,10 @@ class Counters {
);
}
static private function get_cat_children(int $cat_id, int $owner_uid) {
/**
* @return array<int, int>
*/
static private function get_cat_children(int $cat_id, int $owner_uid): array {
$unread = 0;
$marked = 0;
@@ -40,7 +51,11 @@ class Counters {
return [$unread, $marked];
}
private static function get_cats(array $cat_ids = null) {
/**
* @param array<int> $cat_ids
* @return array<int, array<string, int|string>>
*/
private static function get_cats(array $cat_ids = null): array {
$ret = [];
/* Labels category */
@@ -129,7 +144,11 @@ class Counters {
return $ret;
}
private static function get_feeds(array $feed_ids = null) {
/**
* @param array<int> $feed_ids
* @return array<int, array<string, int|string>>
*/
private static function get_feeds(array $feed_ids = null): array {
$ret = [];
@@ -199,7 +218,10 @@ class Counters {
return $ret;
}
private static function get_global() {
/**
* @return array<int, array<string, int|string>>
*/
private static function get_global(): array {
$ret = [
[
"id" => "global-unread",
@@ -219,7 +241,10 @@ class Counters {
return $ret;
}
private static function get_virt() {
/**
* @return array<int, array<string, int|string>>
*/
private static function get_virt(): array {
$ret = [];
@@ -248,6 +273,11 @@ class Counters {
if (is_array($feeds)) {
foreach ($feeds as $feed) {
/** @var IVirtualFeed $feed['sender'] */
if (!implements_interface($feed['sender'], 'IVirtualFeed'))
continue;
$cv = [
"id" => PluginHost::pfeed_to_feed_id($feed['id']),
"counter" => $feed['sender']->get_unread($feed['id'])
@@ -263,7 +293,11 @@ class Counters {
return $ret;
}
static function get_labels(array $label_ids = null) {
/**
* @param array<int> $label_ids
* @return array<int, array<string, int|string>>
*/
static function get_labels(array $label_ids = null): array {
$ret = [];

View File

@@ -17,7 +17,7 @@ class Db
}
}
static function NOW() {
static function NOW(): string {
return date("Y-m-d H:i:s", time());
}
@@ -25,7 +25,7 @@ class Db
//
}
public static function get_dsn() {
public static function get_dsn(): string {
$db_port = Config::get(Config::DB_PORT) ? ';port=' . Config::get(Config::DB_PORT) : '';
$db_host = Config::get(Config::DB_HOST) ? ';host=' . Config::get(Config::DB_HOST) : '';
if (Config::get(Config::DB_TYPE) == "mysql" && Config::get(Config::MYSQL_CHARSET)) {
@@ -88,12 +88,11 @@ class Db
return self::$instance->pdo;
}
public static function sql_random_function() {
public static function sql_random_function(): string {
if (Config::get(Config::DB_TYPE) == "mysql") {
return "RAND()";
} else {
return "RANDOM()";
}
return "RANDOM()";
}
}

View File

@@ -1,29 +1,46 @@
<?php
class Db_Migrations {
// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
/** @var string */
private $base_filename = "schema.sql";
/** @var string */
private $base_path;
/** @var string */
private $migrations_path;
/** @var string */
private $migrations_table;
/** @var bool */
private $base_is_latest;
/** @var PDO */
private $pdo;
private $cached_version;
private $cached_max_version;
/** @var int */
private $cached_version = 0;
/** @var int */
private $cached_max_version = 0;
/** @var int */
private $max_version_override;
function __construct() {
$this->pdo = Db::pdo();
}
function initialize_for_plugin(Plugin $plugin, bool $base_is_latest = true, string $schema_suffix = "sql") {
function initialize_for_plugin(Plugin $plugin, bool $base_is_latest = true, string $schema_suffix = "sql"): void {
$plugin_dir = PluginHost::getInstance()->get_plugin_dir($plugin);
$this->initialize($plugin_dir . "/${schema_suffix}",
strtolower("ttrss_migrations_plugin_" . get_class($plugin)),
$base_is_latest);
}
function initialize(string $root_path, string $migrations_table, bool $base_is_latest = true, int $max_version_override = 0) {
function initialize(string $root_path, string $migrations_table, bool $base_is_latest = true, int $max_version_override = 0): void {
$this->base_path = "$root_path/" . Config::get(Config::DB_TYPE);
$this->migrations_path = $this->base_path . "/migrations";
$this->migrations_table = $migrations_table;
@@ -31,7 +48,7 @@ class Db_Migrations {
$this->max_version_override = $max_version_override;
}
private function set_version(int $version) {
private function set_version(int $version): void {
Debug::log("Updating table {$this->migrations_table} with version ${version}...", Debug::LOG_EXTENDED);
$sth = $this->pdo->query("SELECT * FROM {$this->migrations_table}");
@@ -48,7 +65,7 @@ class Db_Migrations {
}
function get_version() : int {
if (isset($this->cached_version))
if ($this->cached_version)
return $this->cached_version;
try {
@@ -66,11 +83,15 @@ class Db_Migrations {
}
}
private function create_migrations_table() {
private function create_migrations_table(): void {
$this->pdo->query("CREATE TABLE IF NOT EXISTS {$this->migrations_table} (schema_version integer not null)");
}
private function migrate_to(int $version) {
/**
* @throws PDOException
* @return bool false if the migration failed, otherwise true (or an exception)
*/
private function migrate_to(int $version): bool {
try {
if ($version <= $this->get_version()) {
Debug::log("Refusing to apply version $version: current version is higher", Debug::LOG_VERBOSE);
@@ -110,8 +131,10 @@ class Db_Migrations {
Debug::log("Migration finished, current version: " . $this->get_version(), Debug::LOG_VERBOSE);
Logger::log(E_USER_NOTICE, "Applied migration to version $version for {$this->migrations_table}");
return true;
} else {
Debug::log("Migration failed: schema file is empty or missing.", Debug::LOG_VERBOSE);
return false;
}
} catch (PDOException $e) {
@@ -129,7 +152,7 @@ class Db_Migrations {
if ($this->max_version_override > 0)
return $this->max_version_override;
if (isset($this->cached_max_version))
if ($this->cached_max_version)
return $this->cached_max_version;
$migrations = glob("{$this->migrations_path}/*.sql");
@@ -174,6 +197,9 @@ class Db_Migrations {
return !$this->is_migration_needed();
}
/**
* @return array<int, string>
*/
private function get_lines(int $version) : array {
if ($version > 0)
$filename = "{$this->migrations_path}/${version}.sql";

View File

@@ -2,11 +2,17 @@
class Db_Prefs {
// this class is a stub for the time being (to be removed)
function read($pref_name, $user_id = false, $die_on_error = false) {
/**
* @return bool|int|null|string
*/
function read(string $pref_name, ?int $user_id = null, bool $die_on_error = false) {
return get_pref($pref_name, $user_id);
}
function write($pref_name, $value, $user_id = false, $strip_tags = true) {
/**
* @param mixed $value
*/
function write(string $pref_name, $value, ?int $user_id = null, bool $strip_tags = true): bool {
return set_pref($pref_name, $value, $user_id, $strip_tags);
}
}

View File

@@ -5,48 +5,86 @@ class Debug {
const LOG_VERBOSE = 1;
const LOG_EXTENDED = 2;
/** @deprecated */
const ALL_LOG_LEVELS = [
Debug::LOG_DISABLED,
Debug::LOG_NORMAL,
Debug::LOG_VERBOSE,
Debug::LOG_EXTENDED,
];
// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
/**
* @deprecated
* @var int
*/
public static $LOG_DISABLED = self::LOG_DISABLED;
/** @deprecated */
/**
* @deprecated
* @var int
*/
public static $LOG_NORMAL = self::LOG_NORMAL;
/** @deprecated */
/**
* @deprecated
* @var int
*/
public static $LOG_VERBOSE = self::LOG_VERBOSE;
/** @deprecated */
/**
* @deprecated
* @var int
*/
public static $LOG_EXTENDED = self::LOG_EXTENDED;
/** @var bool */
private static $enabled = false;
/** @var bool */
private static $quiet = false;
private static $logfile = false;
/** @var string|null */
private static $logfile = null;
/**
* @var int Debug::LOG_*
*/
private static $loglevel = self::LOG_NORMAL;
public static function set_logfile($logfile) {
public static function set_logfile(string $logfile): void {
self::$logfile = $logfile;
}
public static function enabled() {
public static function enabled(): bool {
return self::$enabled;
}
public static function set_enabled($enable) {
public static function set_enabled(bool $enable): void {
self::$enabled = $enable;
}
public static function set_quiet($quiet) {
public static function set_quiet(bool $quiet): void {
self::$quiet = $quiet;
}
public static function set_loglevel($level) {
/**
* @param int $level Debug::LOG_*
*/
public static function set_loglevel(int $level): void {
self::$loglevel = $level;
}
public static function get_loglevel() {
/**
* @return int Debug::LOG_*
*/
public static function get_loglevel(): int {
return self::$loglevel;
}
public static function log($message, int $level = 0) {
/**
* @param int $level Debug::LOG_*
*/
public static function log(string $message, int $level = Debug::LOG_NORMAL): bool {
if (!self::$enabled || self::$loglevel < $level) return false;
@@ -73,7 +111,7 @@ class Debug {
if (!$locked) {
fclose($fp);
user_error("Unable to lock debugging log file: " . self::$logfile, E_USER_WARNING);
return;
return false;
}
}
@@ -86,7 +124,7 @@ class Debug {
fclose($fp);
if (self::$quiet)
return;
return false;
} else {
user_error("Unable to open debugging log file: " . self::$logfile, E_USER_WARNING);
@@ -94,5 +132,7 @@ class Debug {
}
print "[$ts] $message\n";
return true;
}
}

View File

@@ -1,7 +1,7 @@
<?php
class Digest
{
static function send_headlines_digests() {
static function send_headlines_digests(): void {
$user_limit = 15; // amount of users to process (e.g. emails to send out)
$limit = 1000; // maximum amount of headlines to include
@@ -62,7 +62,7 @@ class Digest
if ($rc && $do_catchup) {
Debug::log("Marking affected articles as read...");
Article::_catchup_by_id($affected_ids, 0, $line["id"]);
Article::_catchup_by_id($affected_ids, Article::CATCHUP_MODE_MARK_AS_READ, $line["id"]);
}
} else {
Debug::log("No headlines");
@@ -78,6 +78,9 @@ class Digest
Debug::log("All done.");
}
/**
* @return array{0: string, 1: int, 2: array<int>, 3: string}
*/
static function prepare_headlines_digest(int $user_id, int $days = 1, int $limit = 1000) {
$tpl = new Templator();

View File

@@ -1,9 +1,15 @@
<?php
class DiskCache {
// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
/** @var string */
private $dir;
// https://stackoverflow.com/a/53662733
private $mimeMap = [
/**
* https://stackoverflow.com/a/53662733
*
* @var array<string, string>
*/
private array $mimeMap = [
'video/3gpp2' => '3g2',
'video/3gp' => '3gp',
'video/3gpp' => '3gp',
@@ -190,21 +196,22 @@ class DiskCache {
'text/x-scriptzsh' => 'zsh'
];
public function __construct($dir) {
public function __construct(string $dir) {
$this->dir = Config::get(Config::CACHE_DIR) . "/" . basename(clean($dir));
}
public function get_dir() {
public function get_dir(): string {
return $this->dir;
}
public function make_dir() {
public function make_dir(): bool {
if (!is_dir($this->dir)) {
return mkdir($this->dir);
}
return false;
}
public function is_writable($filename = "") {
public function is_writable(?string $filename = null): bool {
if ($filename) {
if (file_exists($this->get_full_path($filename)))
return is_writable($this->get_full_path($filename));
@@ -215,44 +222,75 @@ class DiskCache {
}
}
public function exists($filename) {
public function exists(string $filename): bool {
return file_exists($this->get_full_path($filename));
}
public function get_size($filename) {
/**
* @return int|false -1 if the file doesn't exist, false if an error occurred, size in bytes otherwise
*/
public function get_size(string $filename) {
if ($this->exists($filename))
return filesize($this->get_full_path($filename));
else
return -1;
}
public function get_full_path($filename) {
public function get_full_path(string $filename): string {
return $this->dir . "/" . basename(clean($filename));
}
public function put($filename, $data) {
/**
* @param mixed $data
*
* @return int|false Bytes written or false if an error occurred.
*/
public function put(string $filename, $data) {
return file_put_contents($this->get_full_path($filename), $data);
}
public function touch($filename) {
public function touch(string $filename): bool {
return touch($this->get_full_path($filename));
}
public function get($filename) {
/** Downloads $url to cache as $local_filename if its missing (unless $force-ed)
* @param string $url
* @param string $local_filename
* @param array<string,string|int|false> $options (additional params to UrlHelper::fetch())
* @param bool $force
* @return bool
*/
public function download(string $url, string $local_filename, array $options = [], bool $force = false) : bool {
if ($this->exists($local_filename) && !$force)
return true;
$data = UrlHelper::fetch(array_merge(["url" => $url,
"max_size" => Config::get(Config::MAX_CACHE_FILE_SIZE)], $options));
if ($data)
return $this->put($local_filename, $data) > 0;
return false;
}
public function get(string $filename): ?string {
if ($this->exists($filename))
return file_get_contents($this->get_full_path($filename));
else
return null;
}
public function get_mime_type($filename) {
/**
* @return false|null|string false if detection failed, null if the file doesn't exist, string mime content type otherwise
*/
public function get_mime_type(string $filename) {
if ($this->exists($filename))
return mime_content_type($this->get_full_path($filename));
else
return null;
}
public function get_fake_extension($filename) {
public function get_fake_extension(string $filename): string {
$mimetype = $this->get_mime_type($filename);
if ($mimetype)
@@ -261,7 +299,10 @@ class DiskCache {
return "";
}
public function send($filename) {
/**
* @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent
*/
public function send(string $filename) {
$fake_extension = $this->get_fake_extension($filename);
if ($fake_extension)
@@ -272,7 +313,7 @@ class DiskCache {
return $this->send_local_file($this->get_full_path($filename));
}
public function get_url($filename) {
public function get_url(string $filename): string {
return Config::get_self_url() . "/public.php?op=cached&file=" . basename($this->dir) . "/" . basename($filename);
}
@@ -280,8 +321,7 @@ class DiskCache {
// this is called separately after sanitize() and plugin render article hooks to allow
// plugins work on original source URLs used before caching
// NOTE: URLs should be already absolutized because this is called after sanitize()
static public function rewrite_urls($str)
{
static public function rewrite_urls(string $str): string {
$res = trim($str);
if (!$res) return '';
@@ -338,7 +378,7 @@ class DiskCache {
return $res;
}
static function expire() {
static function expire(): void {
$dirs = array_filter(glob(Config::get(Config::CACHE_DIR) . "/*"), "is_dir");
foreach ($dirs as $cache_dir) {
@@ -362,14 +402,19 @@ class DiskCache {
}
}
/* this is essentially a wrapper for readfile() which allows plugins to hook
output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
hook function should return true if request was handled (or at least attempted to)
note that this can be called without user context so the plugin to handle this
should be loaded systemwide in config.php */
function send_local_file($filename) {
/* */
/**
* this is essentially a wrapper for readfile() which allows plugins to hook
* output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
*
* hook function should return true if request was handled (or at least attempted to)
*
* note that this can be called without user context so the plugin to handle this
* should be loaded systemwide in config.php
*
* @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent
*/
function send_local_file(string $filename) {
if (file_exists($filename)) {
if (is_writable($filename)) touch($filename);

View File

@@ -7,7 +7,11 @@ class Errors {
const E_SCHEMA_MISMATCH = "E_SCHEMA_MISMATCH";
const E_URL_SCHEME_MISMATCH = "E_URL_SCHEME_MISMATCH";
static function to_json(string $code, array $params = []) {
/**
* @param Errors::E_* $code
* @param array<string, string> $params
*/
static function to_json(string $code, array $params = []): string {
return json_encode(["error" => ["code" => $code, "params" => $params]]);
}
}

View File

@@ -1,10 +1,21 @@
<?php
class FeedEnclosure {
/** @var string */
public $link;
/** @var string */
public $type;
/** @var string */
public $length;
/** @var string */
public $title;
/** @var string */
public $height;
/** @var string */
public $width;
}

View File

@@ -1,16 +1,24 @@
<?php
abstract class FeedItem {
abstract function get_id();
abstract function get_id(): string;
/** @return int|false a timestamp on success, false otherwise */
abstract function get_date();
abstract function get_link();
abstract function get_title();
abstract function get_description();
abstract function get_content();
abstract function get_comments_url();
abstract function get_comments_count();
abstract function get_categories();
abstract function get_enclosures();
abstract function get_author();
abstract function get_language();
abstract function get_link(): string;
abstract function get_title(): string;
abstract function get_description(): string;
abstract function get_content(): string;
abstract function get_comments_url(): string;
abstract function get_comments_count(): int;
/** @return array<int, string> */
abstract function get_categories(): array;
/** @return array<int, FeedEnclosure> */
abstract function get_enclosures(): array;
abstract function get_author(): string;
abstract function get_language(): string;
}

View File

@@ -2,7 +2,7 @@
class FeedItem_Atom extends FeedItem_Common {
const NS_XML = "http://www.w3.org/XML/1998/namespace";
function get_id() {
function get_id(): string {
$id = $this->elem->getElementsByTagName("id")->item(0);
if ($id) {
@@ -12,6 +12,9 @@ class FeedItem_Atom extends FeedItem_Common {
}
}
/**
* @return int|false a timestamp on success, false otherwise
*/
function get_date() {
$updated = $this->elem->getElementsByTagName("updated")->item(0);
@@ -30,10 +33,13 @@ class FeedItem_Atom extends FeedItem_Common {
if ($date) {
return strtotime($date->nodeValue);
}
// consistent with strtotime failing to parse
return false;
}
function get_link() {
function get_link(): string {
$links = $this->elem->getElementsByTagName("link");
foreach ($links as $link) {
@@ -44,24 +50,27 @@ class FeedItem_Atom extends FeedItem_Common {
$base = $this->xpath->evaluate("string(ancestor-or-self::*[@xml:base][1]/@xml:base)", $link);
if ($base)
return rewrite_relative_url($base, clean(trim($link->getAttribute("href"))));
return UrlHelper::rewrite_relative($base, clean(trim($link->getAttribute("href"))));
else
return clean(trim($link->getAttribute("href")));
}
}
}
function get_title() {
return '';
}
function get_title(): string {
$title = $this->elem->getElementsByTagName("title")->item(0);
if ($title) {
return clean(trim($title->nodeValue));
}
return $title ? clean(trim($title->nodeValue)) : '';
}
/** $base is optional (returns $content if $base is null), $content is an HTML string */
private function rewrite_content_to_base($base, $content) {
/**
* @param string|null $base optional (returns $content if $base is null)
* @param string $content an HTML string
*
* @return string the rewritten XML or original $content
*/
private function rewrite_content_to_base(?string $base = null, string $content) {
if (!empty($base) && !empty($content)) {
@@ -81,14 +90,17 @@ class FeedItem_Atom extends FeedItem_Common {
}
}
return $tmpdoc->saveXML();
// Fall back to $content if saveXML somehow fails (i.e. returns false)
$modified_content = $tmpdoc->saveXML();
return $modified_content !== false ? $modified_content : $content;
}
}
return $content;
}
function get_content() {
function get_content(): string {
/** @var DOMElement|null */
$content = $this->elem->getElementsByTagName("content")->item(0);
if ($content) {
@@ -108,10 +120,13 @@ class FeedItem_Atom extends FeedItem_Common {
return $this->rewrite_content_to_base($base, $this->subtree_or_text($content));
}
return '';
}
// TODO: duplicate code should be merged with get_content()
function get_description() {
function get_description(): string {
/** @var DOMElement|null */
$content = $this->elem->getElementsByTagName("summary")->item(0);
if ($content) {
@@ -132,9 +147,13 @@ class FeedItem_Atom extends FeedItem_Common {
return $this->rewrite_content_to_base($base, $this->subtree_or_text($content));
}
return '';
}
function get_categories() {
/**
* @return array<int, string>
*/
function get_categories(): array {
$categories = $this->elem->getElementsByTagName("category");
$cats = [];
@@ -152,7 +171,10 @@ class FeedItem_Atom extends FeedItem_Common {
return $this->normalize_categories($cats);
}
function get_enclosures() {
/**
* @return array<int, FeedEnclosure>
*/
function get_enclosures(): array {
$links = $this->elem->getElementsByTagName("link");
$encs = [];
@@ -182,7 +204,7 @@ class FeedItem_Atom extends FeedItem_Common {
return $encs;
}
function get_language() {
function get_language(): string {
$lang = $this->elem->getAttributeNS(self::NS_XML, "lang");
if (!empty($lang)) {
@@ -195,5 +217,6 @@ class FeedItem_Atom extends FeedItem_Common {
}
}
}
return '';
}
}

View File

@@ -1,16 +1,20 @@
<?php
abstract class FeedItem_Common extends FeedItem {
/** @var DOMElement */
protected $elem;
protected $xpath;
/** @var DOMDocument */
protected $doc;
function __construct($elem, $doc, $xpath) {
/** @var DOMXPath */
protected $xpath;
function __construct(DOMElement $elem, DOMDocument $doc, DOMXPath $xpath) {
$this->elem = $elem;
$this->xpath = $xpath;
$this->doc = $doc;
try {
$source = $elem->getElementsByTagName("source")->item(0);
// we don't need <source> element
@@ -21,11 +25,12 @@ abstract class FeedItem_Common extends FeedItem {
}
}
function get_element() {
function get_element(): DOMElement {
return $this->elem;
}
function get_author() {
function get_author(): string {
/** @var DOMElement|null */
$author = $this->elem->getElementsByTagName("author")->item(0);
if ($author) {
@@ -51,7 +56,7 @@ abstract class FeedItem_Common extends FeedItem {
return implode(", ", $authors);
}
function get_comments_url() {
function get_comments_url(): string {
//RSS only. Use a query here to avoid namespace clashes (e.g. with slash).
//might give a wrong result if a default namespace was declared (possible with XPath 2.0)
$com_url = $this->xpath->query("comments", $this->elem)->item(0);
@@ -65,20 +70,28 @@ abstract class FeedItem_Common extends FeedItem {
if ($com_url)
return clean($com_url->nodeValue);
return '';
}
function get_comments_count() {
function get_comments_count(): int {
//also query for ATE stuff here
$query = "slash:comments|thread:total|atom:link[@rel='replies']/@thread:count";
$comments = $this->xpath->query($query, $this->elem)->item(0);
if ($comments) {
return clean($comments->nodeValue);
}
if ($comments && is_numeric($comments->nodeValue)) {
return (int) clean($comments->nodeValue);
}
// this is common for both Atom and RSS types and deals with various media: elements
function get_enclosures() {
return 0;
}
/**
* this is common for both Atom and RSS types and deals with various 'media:' elements
*
* @return array<int, FeedEnclosure>
*/
function get_enclosures(): array {
$encs = [];
$enclosures = $this->xpath->query("media:content", $this->elem);
@@ -108,6 +121,7 @@ abstract class FeedItem_Common extends FeedItem {
foreach ($enclosures as $enclosure) {
$enc = new FeedEnclosure();
/** @var DOMElement|null */
$content = $this->xpath->query("media:content", $enclosure)->item(0);
if ($content) {
@@ -150,11 +164,14 @@ abstract class FeedItem_Common extends FeedItem {
return $encs;
}
function count_children($node) {
function count_children(DOMElement $node): int {
return $node->getElementsByTagName("*")->length;
}
function subtree_or_text($node) {
/**
* @return false|string false on failure, otherwise string contents
*/
function subtree_or_text(DOMElement $node) {
if ($this->count_children($node) == 0) {
return $node->nodeValue;
} else {
@@ -162,7 +179,12 @@ abstract class FeedItem_Common extends FeedItem {
}
}
static function normalize_categories($cats) {
/**
* @param array<int, string> $cats
*
* @return array<int, string>
*/
static function normalize_categories(array $cats): array {
$tmp = [];

View File

@@ -1,6 +1,6 @@
<?php
class FeedItem_RSS extends FeedItem_Common {
function get_id() {
function get_id(): string {
$id = $this->elem->getElementsByTagName("guid")->item(0);
if ($id) {
@@ -10,6 +10,9 @@ class FeedItem_RSS extends FeedItem_Common {
}
}
/**
* @return int|false a timestamp on success, false otherwise
*/
function get_date() {
$pubDate = $this->elem->getElementsByTagName("pubDate")->item(0);
@@ -22,9 +25,12 @@ class FeedItem_RSS extends FeedItem_Common {
if ($date) {
return strtotime($date->nodeValue);
}
// consistent with strtotime failing to parse
return false;
}
function get_link() {
function get_link(): string {
$links = $this->xpath->query("atom:link", $this->elem);
foreach ($links as $link) {
@@ -37,6 +43,7 @@ class FeedItem_RSS extends FeedItem_Common {
}
}
/** @var DOMElement|null */
$link = $this->elem->getElementsByTagName("guid")->item(0);
if ($link && $link->hasAttributes() && $link->getAttribute("isPermaLink") == "true") {
@@ -48,9 +55,11 @@ class FeedItem_RSS extends FeedItem_Common {
if ($link) {
return clean(trim($link->nodeValue));
}
return '';
}
function get_title() {
function get_title(): string {
$title = $this->xpath->query("title", $this->elem)->item(0);
if ($title) {
@@ -64,10 +73,15 @@ class FeedItem_RSS extends FeedItem_Common {
if ($title) {
return clean(trim($title->nodeValue));
}
return '';
}
function get_content() {
function get_content(): string {
/** @var DOMElement|null */
$contentA = $this->xpath->query("content:encoded", $this->elem)->item(0);
/** @var DOMElement|null */
$contentB = $this->elem->getElementsByTagName("description")->item(0);
if ($contentA && !$contentB) {
@@ -85,17 +99,24 @@ class FeedItem_RSS extends FeedItem_Common {
return mb_strlen($resultA) > mb_strlen($resultB) ? $resultA : $resultB;
}
return '';
}
function get_description() {
function get_description(): string {
$summary = $this->elem->getElementsByTagName("description")->item(0);
if ($summary) {
return $summary->nodeValue;
}
return '';
}
function get_categories() {
/**
* @return array<int, string>
*/
function get_categories(): array {
$categories = $this->elem->getElementsByTagName("category");
$cats = [];
@@ -112,7 +133,10 @@ class FeedItem_RSS extends FeedItem_Common {
return $this->normalize_categories($cats);
}
function get_enclosures() {
/**
* @return array<int, FeedEnclosure>
*/
function get_enclosures(): array {
$enclosures = $this->elem->getElementsByTagName("enclosure");
$encs = array();
@@ -134,7 +158,7 @@ class FeedItem_RSS extends FeedItem_Common {
return $encs;
}
function get_language() {
function get_language(): string {
$languages = $this->doc->getElementsByTagName('language');
if (count($languages) == 0) {
@@ -143,5 +167,4 @@ class FeedItem_RSS extends FeedItem_Common {
return clean($languages[0]->textContent);
}
}

View File

@@ -1,19 +1,35 @@
<?php
class FeedParser {
/** @var DOMDocument */
private $doc;
private $error;
private $libxml_errors = array();
private $items;
/** @var string|null */
private $error = null;
/** @var array<string> */
private $libxml_errors = [];
/** @var array<FeedItem> */
private $items = [];
/** @var string|null */
private $link;
/** @var string|null */
private $title;
/** @var FeedParser::FEED_*|null */
private $type;
/** @var DOMXPath|null */
private $xpath;
const FEED_RDF = 0;
const FEED_RSS = 1;
const FEED_ATOM = 2;
function __construct($data) {
function __construct(string $data) {
libxml_use_internal_errors(true);
libxml_clear_errors();
$this->doc = new DOMDocument();
@@ -26,18 +42,18 @@ class FeedParser {
if ($error) {
foreach (libxml_get_errors() as $error) {
if ($error->level == LIBXML_ERR_FATAL) {
if(!isset($this->error)) //currently only the first error is reported
// currently only the first error is reported
if (!isset($this->error)) {
$this->error = $this->format_error($error);
}
$this->libxml_errors[] = $this->format_error($error);
}
}
}
libxml_clear_errors();
$this->items = array();
}
function init() {
function init() : void {
$root = $this->doc->firstChild;
$xpath = new DOMXPath($this->doc);
$xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
@@ -51,10 +67,12 @@ class FeedParser {
$this->xpath = $xpath;
$root = $xpath->query("(//atom03:feed|//atom:feed|//channel|//rdf:rdf|//rdf:RDF)");
$root_list = $xpath->query("(//atom03:feed|//atom:feed|//channel|//rdf:rdf|//rdf:RDF)");
if (!empty($root) && $root->length > 0) {
$root = $root->item(0);
if (!empty($root_list) && $root_list->length > 0) {
/** @var DOMElement|null $root */
$root = $root_list->item(0);
if ($root) {
switch (mb_strtolower($root->tagName)) {
@@ -100,6 +118,7 @@ class FeedParser {
if (!$link)
$link = $xpath->query("//atom03:feed/atom03:link[@rel='alternate']")->item(0);
/** @var DOMElement|null $link */
if ($link && $link->hasAttributes()) {
$this->link = $link->getAttribute("href");
}
@@ -121,6 +140,7 @@ class FeedParser {
$this->title = $title->nodeValue;
}
/** @var DOMElement|null $link */
$link = $xpath->query("//channel/link")->item(0);
if ($link) {
@@ -173,39 +193,37 @@ class FeedParser {
}
}
function format_error($error) {
if ($error) {
function format_error(LibXMLError $error) : string {
return sprintf("LibXML error %s at line %d (column %d): %s",
$error->code, $error->line, $error->column,
$error->message);
} else {
return "";
}
}
// libxml may have invalid unicode data in error messages
function error() {
function error() : string {
return UConverter::transcode($this->error, 'UTF-8', 'UTF-8');
}
// WARNING: may return invalid unicode data
function errors() {
/** @return array<string> - WARNING: may return invalid unicode data */
function errors() : array {
return $this->libxml_errors;
}
function get_link() {
return clean($this->link);
function get_link() : string {
return clean($this->link ?? '');
}
function get_title() {
return clean($this->title);
function get_title() : string {
return clean($this->title ?? '');
}
function get_items() {
/** @return array<FeedItem> */
function get_items() : array {
return $this->items;
}
function get_links($rel) {
/** @return array<string> */
function get_links(string $rel) : array {
$rv = array();
switch ($this->type) {

View File

@@ -5,18 +5,24 @@ class Feeds extends Handler_Protected {
const NEVER_GROUP_FEEDS = [ -6, 0 ];
const NEVER_GROUP_BY_DATE = [ -2, -1, -3 ];
/** @var int|float int on 64-bit, float on 32-bit */
private $viewfeed_timestamp;
/** @var int|float int on 64-bit, float on 32-bit */
private $viewfeed_timestamp_last;
function csrf_ignore($method) {
function csrf_ignore(string $method): bool {
$csrf_ignored = array("index");
return array_search($method, $csrf_ignored) !== false;
}
private function _format_headlines_list($feed, $method, $view_mode, $limit, $cat_view,
$offset, $override_order = false, $include_children = false, $check_first_id = false,
$skip_first_id_check = false, $order_by = false) {
/**
* @return array{0: array<int, int>, 1: int, 2: int, 3: bool, 4: array<string, mixed>} $topmost_article_ids, $headlines_count, $feed, $disable_cache, $reply
*/
private function _format_headlines_list(int $feed, string $method, string $view_mode, int $limit, bool $cat_view,
int $offset, string $override_order, bool $include_children, ?int $check_first_id = null,
bool $skip_first_id_check, string $order_by): array {
$disable_cache = false;
@@ -80,6 +86,8 @@ class Feeds extends Handler_Protected {
"include_children" => $include_children,
"order_by" => $order_by);
// Implemented by a plugin, so ignore the undefined 'get_headlines' method.
// @phpstan-ignore-next-line
$qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id($feed),
$options);
}
@@ -271,7 +279,7 @@ class Feeds extends Handler_Protected {
$this->_mark_timestamp(" pre-sanitize");
$line["content"] = Sanitizer::sanitize($line["content"],
$line['hide_images'], false, $line["site_url"], $highlight_words, $line["id"]);
$line['hide_images'], null, $line["site_url"], $highlight_words, $line["id"]);
$this->_mark_timestamp(" sanitize");
@@ -289,9 +297,9 @@ class Feeds extends Handler_Protected {
if ($line["num_enclosures"] > 0) {
$line["enclosures"] = Article::_format_enclosures($id,
$line["always_display_enclosures"],
sql_bool_to_bool($line["always_display_enclosures"]),
$line["content"],
$line["hide_images"]);
sql_bool_to_bool($line["hide_images"]));
} else {
$line["enclosures"] = [ 'formatted' => '', 'entries' => [] ];
}
@@ -299,7 +307,7 @@ class Feeds extends Handler_Protected {
$this->_mark_timestamp(" enclosures");
$line["updated_long"] = TimeHelper::make_local_datetime($line["updated"],true);
$line["updated"] = TimeHelper::make_local_datetime($line["updated"], false, false, false, true);
$line["updated"] = TimeHelper::make_local_datetime($line["updated"], false, null, false, true);
$line['imported'] = T_sprintf("Imported at %s",
TimeHelper::make_local_datetime($line["date_entered"], false));
@@ -433,7 +441,7 @@ class Feeds extends Handler_Protected {
return array($topmost_article_ids, $headlines_count, $feed, $disable_cache, $reply);
}
function catchupAll() {
function catchupAll(): void {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
last_read = NOW(), unread = false WHERE unread = true AND owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
@@ -441,7 +449,7 @@ class Feeds extends Handler_Protected {
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function view() {
function view(): void {
$reply = array();
$feed = $_REQUEST["feed"];
@@ -450,7 +458,7 @@ class Feeds extends Handler_Protected {
$limit = 30;
$cat_view = $_REQUEST["cat"] == "true";
$next_unread_feed = $_REQUEST["nuf"] ?? 0;
$offset = $_REQUEST["skip"] ?? 0;
$offset = (int) ($_REQUEST["skip"] ?? 0);
$order_by = $_REQUEST["order_by"] ?? "";
$check_first_id = $_REQUEST["fid"] ?? 0;
@@ -538,7 +546,10 @@ class Feeds extends Handler_Protected {
print json_encode($reply);
}
private function _generate_dashboard_feed() {
/**
* @return array<string, array<string, mixed>>
*/
private function _generate_dashboard_feed(): array {
$reply = array();
$reply['headlines']['id'] = -5;
@@ -580,7 +591,10 @@ class Feeds extends Handler_Protected {
return $reply;
}
private function _generate_error_feed($error) {
/**
* @return array<string, mixed>
*/
private function _generate_error_feed(string $error): array {
$reply = array();
$reply['headlines']['id'] = -7;
@@ -596,13 +610,13 @@ class Feeds extends Handler_Protected {
return $reply;
}
function subscribeToFeed() {
function subscribeToFeed(): void {
print json_encode([
"cat_select" => \Controls\select_feeds_cats("cat")
]);
}
function search() {
function search(): void {
print json_encode([
"show_language" => Config::get(Config::DB_TYPE) == "pgsql",
"show_syntax_help" => count(PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH)) == 0,
@@ -611,7 +625,7 @@ class Feeds extends Handler_Protected {
]);
}
function opensite() {
function opensite(): void {
$feed = ORM::for_table('ttrss_feeds')
->find_one((int)$_REQUEST['feed_id']);
@@ -628,10 +642,14 @@ class Feeds extends Handler_Protected {
print "Feed not found or has an empty site URL.";
}
function updatedebugger() {
function updatedebugger(): void {
header("Content-type: text/html");
$xdebug = isset($_REQUEST["xdebug"]) ? (int)$_REQUEST["xdebug"] : 1;
$xdebug = isset($_REQUEST["xdebug"]) ? (int)$_REQUEST["xdebug"] : Debug::LOG_VERBOSE;
if (!in_array($xdebug, Debug::ALL_LOG_LEVELS)) {
$xdebug = Debug::LOG_VERBOSE;
}
Debug::set_enabled(true);
Debug::set_loglevel($xdebug);
@@ -731,7 +749,10 @@ class Feeds extends Handler_Protected {
}
static function _catchup($feed, $cat_view, $owner_uid = false, $mode = 'all', $search = false) {
/**
* @param array<int, string> $search
*/
static function _catchup(string $feed_id_or_tag_name, bool $cat_view, ?int $owner_uid = null, string $mode = 'all', ?array $search = null): void {
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
@@ -785,14 +806,16 @@ class Feeds extends Handler_Protected {
$date_qpart = "true";
}
if (is_numeric($feed)) {
if (is_numeric($feed_id_or_tag_name)) {
$feed_id = (int) $feed_id_or_tag_name;
if ($cat_view) {
if ($feed >= 0) {
if ($feed_id >= 0) {
if ($feed > 0) {
$children = self::_get_child_cats($feed, $owner_uid);
array_push($children, $feed);
if ($feed_id > 0) {
$children = self::_get_child_cats($feed_id, $owner_uid);
array_push($children, $feed_id);
$children = array_map("intval", $children);
$children = join(",", $children);
@@ -810,7 +833,7 @@ class Feeds extends Handler_Protected {
(SELECT id FROM ttrss_feeds WHERE $cat_qpart) AND $date_qpart AND $search_qpart) as tmp)");
$sth->execute([$owner_uid]);
} else if ($feed == -2) {
} else if ($feed_id == -2) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false,last_read = NOW() WHERE (SELECT COUNT(*)
@@ -819,18 +842,18 @@ class Feeds extends Handler_Protected {
$sth->execute([$owner_uid]);
}
} else if ($feed > 0) {
} else if ($feed_id > 0) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false, last_read = NOW() WHERE ref_id IN
(SELECT id FROM
(SELECT DISTINCT id FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id
AND owner_uid = ? AND unread = true AND feed_id = ? AND $date_qpart AND $search_qpart) as tmp)");
$sth->execute([$owner_uid, $feed]);
$sth->execute([$owner_uid, $feed_id]);
} else if ($feed < 0 && $feed > LABEL_BASE_INDEX) { // special, like starred
} else if ($feed_id < 0 && $feed_id > LABEL_BASE_INDEX) { // special, like starred
if ($feed == -1) {
if ($feed_id == -1) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false, last_read = NOW() WHERE ref_id IN
(SELECT id FROM
@@ -839,7 +862,7 @@ class Feeds extends Handler_Protected {
$sth->execute([$owner_uid]);
}
if ($feed == -2) {
if ($feed_id == -2) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false, last_read = NOW() WHERE ref_id IN
(SELECT id FROM
@@ -848,7 +871,7 @@ class Feeds extends Handler_Protected {
$sth->execute([$owner_uid]);
}
if ($feed == -3) {
if ($feed_id == -3) {
$intl = (int) get_pref(Prefs::FRESH_ARTICLE_MAX_AGE);
@@ -867,7 +890,7 @@ class Feeds extends Handler_Protected {
$sth->execute([$owner_uid]);
}
if ($feed == -4) {
if ($feed_id == -4) {
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false, last_read = NOW() WHERE ref_id IN
(SELECT id FROM
@@ -875,10 +898,9 @@ class Feeds extends Handler_Protected {
AND owner_uid = ? AND unread = true AND $date_qpart AND $search_qpart) as tmp)");
$sth->execute([$owner_uid]);
}
} else if ($feed_id < LABEL_BASE_INDEX) { // label
} else if ($feed < LABEL_BASE_INDEX) { // label
$label_id = Labels::feed_to_label_id($feed);
$label_id = Labels::feed_to_label_id($feed_id);
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false, last_read = NOW() WHERE ref_id IN
@@ -887,23 +909,21 @@ class Feeds extends Handler_Protected {
AND label_id = ? AND ref_id = article_id
AND owner_uid = ? AND unread = true AND $date_qpart AND $search_qpart) as tmp)");
$sth->execute([$label_id, $owner_uid]);
}
} else { // tag
$tag_name = $feed_id_or_tag_name;
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET unread = false, last_read = NOW() WHERE ref_id IN
(SELECT id FROM
(SELECT DISTINCT ttrss_entries.id FROM ttrss_entries, ttrss_user_entries, ttrss_tags WHERE ref_id = ttrss_entries.id
AND post_int_id = int_id AND tag_name = ?
AND ttrss_user_entries.owner_uid = ? AND unread = true AND $date_qpart AND $search_qpart) as tmp)");
$sth->execute([$feed, $owner_uid]);
$sth->execute([$tag_name, $owner_uid]);
}
}
static function _get_counters($feed, $is_cat = false, $unread_only = false,
$owner_uid = false) {
static function _get_counters(int $feed, bool $is_cat = false, bool $unread_only = false, ?int $owner_uid = null): int {
$n_feed = (int) $feed;
$need_entries = false;
@@ -1002,7 +1022,7 @@ class Feeds extends Handler_Protected {
}
}
function add() {
function add(): void {
$feed = clean($_REQUEST['feed']);
$cat = clean($_REQUEST['cat'] ?? '');
$need_auth = isset($_REQUEST['need_auth']);
@@ -1015,7 +1035,7 @@ class Feeds extends Handler_Protected {
}
/**
* @return array (code => Status code, message => error message if available)
* @return array<string, mixed> (code => Status code, message => error message if available)
*
* 0 - OK, Feed already exists
* 1 - OK, Feed added
@@ -1029,8 +1049,7 @@ class Feeds extends Handler_Protected {
* 7 - Error while creating feed database entry.
* 8 - Permission denied (ACCESS_LEVEL_READONLY).
*/
static function _subscribe($url, $cat_id = 0,
$auth_login = '', $auth_pass = '') : array {
static function _subscribe(string $url, int $cat_id = 0, string $auth_login = '', string $auth_pass = ''): array {
$user = ORM::for_table("ttrss_users")->find_one($_SESSION['uid']);
@@ -1109,15 +1128,18 @@ class Feeds extends Handler_Protected {
}
}
static function _get_icon_file($feed_id) {
static function _get_icon_file(int $feed_id): string {
return Config::get(Config::ICONS_DIR) . "/$feed_id.ico";
}
static function _has_icon($id) {
static function _has_icon(int $id): bool {
return is_file(Config::get(Config::ICONS_DIR) . "/$id.ico") && filesize(Config::get(Config::ICONS_DIR) . "/$id.ico") > 0;
}
static function _get_icon($id) {
/**
* @return false|string false if the icon ID was unrecognized, otherwise, the icon identifier string
*/
static function _get_icon(int $id) {
switch ($id) {
case 0:
return "archive";
@@ -1147,6 +1169,9 @@ class Feeds extends Handler_Protected {
return false;
}
/**
* @return false|int false if the feed couldn't be found by URL+owner, otherwise the feed ID
*/
static function _find_by_url(string $feed_url, int $owner_uid) {
$feed = ORM::for_table('ttrss_feeds')
->where('owner_uid', $owner_uid)
@@ -1160,7 +1185,11 @@ class Feeds extends Handler_Protected {
}
}
/** $owner_uid defaults to $_SESSION['uid] */
/**
* $owner_uid defaults to $_SESSION['uid']
*
* @return false|int false if the category/feed couldn't be found by title, otherwise its ID
*/
static function _find_by_title(string $title, bool $cat = false, int $owner_uid = 0) {
$res = false;
@@ -1184,7 +1213,7 @@ class Feeds extends Handler_Protected {
}
}
static function _get_title($id, bool $cat = false) {
static function _get_title(int $id, bool $cat = false): string {
$pdo = Db::pdo();
if ($cat) {
@@ -1197,7 +1226,7 @@ class Feeds extends Handler_Protected {
return __("Fresh articles");
} else if ($id == -4) {
return __("All articles");
} else if ($id === 0 || $id === "0") {
} else if ($id === 0) {
return __("Archived articles");
} else if ($id == -6) {
return __("Recently read");
@@ -1226,12 +1255,12 @@ class Feeds extends Handler_Protected {
}
} else {
return $id;
return "$id";
}
}
// only real cats
static function _get_cat_marked(int $cat, int $owner_uid = 0) {
static function _get_cat_marked(int $cat, int $owner_uid = 0): int {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
@@ -1245,16 +1274,17 @@ class Feeds extends Handler_Protected {
WHERE (cat_id = :cat OR (:cat IS NULL AND cat_id IS NULL))
AND owner_uid = :uid)
AND owner_uid = :uid");
$sth->execute(["cat" => $cat ? $cat : null, "uid" => $owner_uid]);
$row = $sth->fetch();
return $row["marked"];
} else {
$sth->execute(["cat" => $cat ? $cat : null, "uid" => $owner_uid]);
if ($row = $sth->fetch()) {
return (int) $row["marked"];
}
}
return 0;
}
}
static function _get_cat_unread(int $cat, int $owner_uid = 0) {
static function _get_cat_unread(int $cat, int $owner_uid = 0): int {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
@@ -1268,11 +1298,12 @@ class Feeds extends Handler_Protected {
WHERE (cat_id = :cat OR (:cat IS NULL AND cat_id IS NULL))
AND owner_uid = :uid)
AND owner_uid = :uid");
$sth->execute(["cat" => $cat ? $cat : null, "uid" => $owner_uid]);
$row = $sth->fetch();
return $row["unread"];
if ($row = $sth->fetch()) {
return (int) $row["unread"];
}
} else if ($cat == -1) {
return 0;
} else if ($cat == -2) {
@@ -1280,15 +1311,19 @@ class Feeds extends Handler_Protected {
$sth = $pdo->prepare("SELECT COUNT(DISTINCT article_id) AS unread
FROM ttrss_user_entries ue, ttrss_user_labels2 l
WHERE article_id = ref_id AND unread IS true AND ue.owner_uid = :uid");
$sth->execute(["uid" => $owner_uid]);
$row = $sth->fetch();
return $row["unread"];
$sth->execute(["uid" => $owner_uid]);
if ($row = $sth->fetch()) {
return (int) $row["unread"];
}
}
return 0;
}
// only accepts real cats (>= 0)
static function _get_cat_children_unread(int $cat, int $owner_uid = 0) {
static function _get_cat_children_unread(int $cat, int $owner_uid = 0): int {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
@@ -1307,7 +1342,7 @@ class Feeds extends Handler_Protected {
return $unread;
}
static function _get_global_unread(int $user_id = 0) {
static function _get_global_unread(int $user_id = 0): int {
if (!$user_id) $user_id = $_SESSION["uid"];
@@ -1323,7 +1358,7 @@ class Feeds extends Handler_Protected {
return $row["count"];
}
static function _get_cat_title(int $cat_id) {
static function _get_cat_title(int $cat_id): string {
switch ($cat_id) {
case 0:
return __("Uncategorized");
@@ -1343,7 +1378,7 @@ class Feeds extends Handler_Protected {
}
}
private static function _get_label_unread($label_id, int $owner_uid = 0) {
private static function _get_label_unread(int $label_id, ?int $owner_uid = null): int {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
@@ -1360,7 +1395,11 @@ class Feeds extends Handler_Protected {
}
}
static function _get_headlines($params) {
/**
* @param array<string, mixed> $params
* @return array<int, mixed> $result, $feed_title, $feed_site_url, $last_error, $last_updated, $highlight_words, $first_id, $is_vfeed, $query_error_override
*/
static function _get_headlines($params): array {
$pdo = Db::pdo();
@@ -1577,7 +1616,7 @@ class Feeds extends Handler_Protected {
} else if ($feed <= LABEL_BASE_INDEX) { // labels
$label_id = Labels::feed_to_label_id($feed);
$query_strategy_part = "label_id = ".$pdo->quote($label_id)." AND
$query_strategy_part = "label_id = $label_id AND
ttrss_labels2.id = ttrss_user_labels2.label_id AND
ttrss_user_labels2.article_id = ref_id";
@@ -1857,7 +1896,10 @@ class Feeds extends Handler_Protected {
}
static function _get_parent_cats(int $cat, int $owner_uid) {
/**
* @return array<int, int>
*/
static function _get_parent_cats(int $cat, int $owner_uid): array {
$rv = array();
$pdo = Db::pdo();
@@ -1874,7 +1916,10 @@ class Feeds extends Handler_Protected {
return $rv;
}
static function _get_child_cats(int $cat, int $owner_uid) {
/**
* @return array<int, int>
*/
static function _get_child_cats(int $cat, int $owner_uid): array {
$rv = array();
$pdo = Db::pdo();
@@ -1891,7 +1936,11 @@ class Feeds extends Handler_Protected {
return $rv;
}
static function _cats_of(array $feeds, int $owner_uid, bool $with_parents = false) {
/**
* @param array<int, int> $feeds
* @return array<int, int>
*/
static function _cats_of(array $feeds, int $owner_uid, bool $with_parents = false): array {
if (count($feeds) == 0)
return [];
@@ -1930,7 +1979,7 @@ class Feeds extends Handler_Protected {
}
}
private function _color_of($name) {
private function _color_of(string $name): string {
$colormap = [ "#1cd7d7","#d91111","#1212d7","#8e16e5","#7b7b7b",
"#39f110","#0bbea6","#ec0e0e","#1534f2","#b9e416",
"#479af2","#f36b14","#10c7e9","#1e8fe7","#e22727" ];
@@ -1946,7 +1995,10 @@ class Feeds extends Handler_Protected {
return $colormap[$sum];
}
private static function _get_feeds_from_html($url, $content) {
/**
* @return array<string, string> array of feed URL -> feed title
*/
private static function _get_feeds_from_html(string $url, string $content): array {
$url = UrlHelper::validate($url);
$baseUrl = substr($url, 0, strrpos($url, '/') + 1);
@@ -1964,9 +2016,7 @@ class Feeds extends Handler_Protected {
if ($title == '') {
$title = $entry->getAttribute('type');
}
$feedUrl = rewrite_relative_url(
$baseUrl, $entry->getAttribute('href')
);
$feedUrl = UrlHelper::rewrite_relative($baseUrl, $entry->getAttribute('href'));
$feedUrls[$feedUrl] = $title;
}
}
@@ -1974,11 +2024,11 @@ class Feeds extends Handler_Protected {
return $feedUrls;
}
static function _is_html($content) {
static function _is_html(string $content): bool {
return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 8192)) !== 0;
}
static function _remove_cat(int $id, int $owner_uid) {
static function _remove_cat(int $id, int $owner_uid): void {
$cat = ORM::for_table('ttrss_feed_categories')
->where('owner_uid', $owner_uid)
->find_one($id);
@@ -1987,7 +2037,7 @@ class Feeds extends Handler_Protected {
$cat->delete();
}
static function _add_cat(string $title, int $owner_uid, int $parent_cat = null, int $order_id = 0) {
static function _add_cat(string $title, int $owner_uid, int $parent_cat = null, int $order_id = 0): bool {
$cat = ORM::for_table('ttrss_feed_categories')
->where('owner_uid', $owner_uid)
@@ -2011,13 +2061,18 @@ class Feeds extends Handler_Protected {
return false;
}
static function _clear_access_keys(int $owner_uid) {
static function _clear_access_keys(int $owner_uid): void {
$key = ORM::for_table('ttrss_access_keys')
->where('owner_uid', $owner_uid)
->delete_many();
}
static function _update_access_key(string $feed_id, bool $is_cat, int $owner_uid) {
/**
* @param string $feed_id may be a feed ID or tag
*
* @see Handler_Public#generate_syndicated_feed()
*/
static function _update_access_key(string $feed_id, bool $is_cat, int $owner_uid): ?string {
$key = ORM::for_table('ttrss_access_keys')
->where('owner_uid', $owner_uid)
->where('feed_id', $feed_id)
@@ -2027,7 +2082,12 @@ class Feeds extends Handler_Protected {
return self::_get_access_key($feed_id, $is_cat, $owner_uid);
}
static function _get_access_key(string $feed_id, bool $is_cat, int $owner_uid) {
/**
* @param string $feed_id may be a feed ID or tag
*
* @see Handler_Public#generate_syndicated_feed()
*/
static function _get_access_key(string $feed_id, bool $is_cat, int $owner_uid): ?string {
$key = ORM::for_table('ttrss_access_keys')
->where('owner_uid', $owner_uid)
->where('feed_id', $feed_id)
@@ -2036,7 +2096,8 @@ class Feeds extends Handler_Protected {
if ($key) {
return $key->access_key;
} else {
}
$key = ORM::for_table('ttrss_access_keys')->create();
$key->owner_uid = $owner_uid;
@@ -2047,10 +2108,11 @@ class Feeds extends Handler_Protected {
if ($key->save()) {
return $key->access_key;
}
}
return null;
}
static function _purge(int $feed_id, int $purge_interval) {
static function _purge(int $feed_id, int $purge_interval): ?int {
if (!$purge_interval) $purge_interval = self::_get_purge_interval($feed_id);
@@ -2079,7 +2141,7 @@ class Feeds extends Handler_Protected {
if ($purge_interval <= 0) {
Debug::log("purge_feed: purging disabled for this feed, nothing to do.", Debug::$LOG_VERBOSE);
return;
return null;
}
if (!$purge_unread)
@@ -2120,7 +2182,7 @@ class Feeds extends Handler_Protected {
return $rows_deleted;
}
private static function _get_purge_interval(int $feed_id) {
private static function _get_purge_interval(int $feed_id): int {
$feed = ORM::for_table('ttrss_feeds')->find_one($feed_id);
if ($feed) {
@@ -2133,7 +2195,10 @@ class Feeds extends Handler_Protected {
}
}
private static function _search_to_sql($search, $search_language, $owner_uid) {
/**
* @return array{0: string, 1: array<int, string>} [$search_query_part, $search_words]
*/
private static function _search_to_sql(string $search, string $search_language, int $owner_uid): array {
$keywords = str_getcsv(preg_replace('/(-?\w+)\:"(\w+)/', '"${1}:${2}', trim($search)), ' ');
$query_keywords = array();
$search_words = array();
@@ -2226,7 +2291,7 @@ class Feeds extends Handler_Protected {
array_push($query_keywords, "($not
(ttrss_entries.id IN (
SELECT article_id FROM ttrss_user_labels2 WHERE
label_id = ".$pdo->quote($label_id).")))");
label_id = $label_id)))");
} else {
array_push($query_keywords, "(false)");
}
@@ -2300,7 +2365,10 @@ class Feeds extends Handler_Protected {
return array($search_query_part, $search_words);
}
static function _order_to_override_query($order) {
/**
* @return array{0: string, 1: bool}
*/
static function _order_to_override_query(string $order): array {
$query = "";
$skip_first_id = false;
@@ -2310,7 +2378,9 @@ class Feeds extends Handler_Protected {
},
$order);
if ($query) return [$query, $skip_first_id];
if (is_string($query) && $query !== "") {
return [$query, $skip_first_id];
}
switch ($order) {
case "title":
@@ -2328,7 +2398,7 @@ class Feeds extends Handler_Protected {
return [$query, $skip_first_id];
}
private function _mark_timestamp($label) {
private function _mark_timestamp(string $label): void {
if (empty($_REQUEST['timestamps']))
return;

View File

@@ -1,22 +1,29 @@
<?php
class Handler implements IHandler {
// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
/** @var PDO */
protected $pdo;
/** @var array<int|string, mixed> */
protected $args;
function __construct($args) {
/**
* @param array<int|string, mixed> $args
*/
function __construct(array $args) {
$this->pdo = Db::pdo();
$this->args = $args;
}
function csrf_ignore($method) {
function csrf_ignore(string $method): bool {
return false;
}
function before($method) {
function before(string $method): bool {
return true;
}
function after() {
function after(): bool {
return true;
}

View File

@@ -1,6 +1,6 @@
<?php
class Handler_Administrative extends Handler_Protected {
function before($method) {
function before(string $method): bool {
if (parent::before($method)) {
if (($_SESSION["access_level"] ?? 0) >= UserHelper::ACCESS_LEVEL_ADMIN) {
return true;

View File

@@ -1,7 +1,7 @@
<?php
class Handler_Protected extends Handler {
function before($method) {
function before(string $method): bool {
return parent::before($method) && !empty($_SESSION['uid']);
}
}

View File

@@ -1,10 +1,12 @@
<?php
class Handler_Public extends Handler {
// $feed may be a tag
/**
* @param string $feed may be a feed ID or tag
*/
private function generate_syndicated_feed(int $owner_uid, string $feed, bool $is_cat,
int $limit, int $offset, string $search, string $view_mode = "",
string $format = 'atom', string $order = "", string $orig_guid = "", string $start_ts = "") {
string $format = 'atom', string $order = "", string $orig_guid = "", string $start_ts = ""): void {
$note_style = "background-color : #fff7d5;
border-width : 1px; ".
@@ -52,11 +54,13 @@ class Handler_Public extends Handler {
PluginHost::feed_to_pfeed_id((int)$feed));
if ($handler) {
// 'get_headlines' is implemented by the plugin.
// @phpstan-ignore-next-line
$qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id((int)$feed), $params);
} else {
user_error("Failed to find handler for plugin feed ID: $feed", E_USER_ERROR);
return false;
return;
}
} else {
@@ -89,11 +93,13 @@ class Handler_Public extends Handler {
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
$line["tags"] = Article::_get_tags($line["id"], $owner_uid);
$max_excerpt_length = 250;
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
function ($result) use (&$line) {
$line = $result;
},
$line);
$line, $max_excerpt_length);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_EXPORT_FEED,
function ($result) use (&$line) {
@@ -109,7 +115,7 @@ class Handler_Public extends Handler {
$tpl->setVariable('ARTICLE_EXCERPT', $line["content_preview"], true);
$content = Sanitizer::sanitize($line["content"], false, $owner_uid,
$feed_site_url, false, $line["id"]);
$feed_site_url, null, $line["id"]);
$content = DiskCache::rewrite_urls($content);
@@ -207,7 +213,7 @@ class Handler_Public extends Handler {
$article['link'] = $line['link'];
$article['title'] = $line['title'];
$article['excerpt'] = $line["content_preview"];
$article['content'] = Sanitizer::sanitize($line["content"], false, $owner_uid, $feed_site_url, false, $line["id"]);
$article['content'] = Sanitizer::sanitize($line["content"], false, $owner_uid, $feed_site_url, null, $line["id"]);
$article['updated'] = date('c', strtotime($line["updated"]));
if (!empty($line['note'])) $article['note'] = $line['note'];
@@ -247,7 +253,7 @@ class Handler_Public extends Handler {
}
}
function getUnread() {
function getUnread(): void {
$login = clean($_REQUEST["login"]);
$fresh = clean($_REQUEST["fresh"]) == "1";
@@ -265,7 +271,7 @@ class Handler_Public extends Handler {
}
}
function getProfiles() {
function getProfiles(): void {
$login = clean($_REQUEST["login"]);
$rv = [];
@@ -288,7 +294,7 @@ class Handler_Public extends Handler {
print json_encode($rv);
}
function logout() {
function logout(): void {
if (validate_csrf($_POST["csrf_token"])) {
UserHelper::logout();
header("Location: index.php");
@@ -298,7 +304,7 @@ class Handler_Public extends Handler {
}
}
function rss() {
function rss(): void {
$feed = clean($_REQUEST["id"]);
$key = clean($_REQUEST["key"]);
$is_cat = clean($_REQUEST["is_cat"] ?? false);
@@ -333,21 +339,21 @@ class Handler_Public extends Handler {
header('HTTP/1.1 403 Forbidden');
}
function updateTask() {
function updateTask(): void {
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_UPDATE_TASK);
}
function housekeepingTask() {
function housekeepingTask(): void {
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
}
function globalUpdateFeeds() {
function globalUpdateFeeds(): void {
RPC::updaterandomfeed_real();
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_UPDATE_TASK);
}
function login() {
function login(): void {
if (!Config::get(Config::SINGLE_USER_MODE)) {
$login = clean($_POST["login"]);
@@ -403,12 +409,12 @@ class Handler_Public extends Handler {
}
}
function index() {
function index(): void {
header("Content-Type: text/plain");
print Errors::to_json(Errors::E_UNKNOWN_METHOD);
}
function forgotpass() {
function forgotpass(): void {
startup_gettext();
session_start();
@@ -587,7 +593,7 @@ class Handler_Public extends Handler {
print "</html>";
}
function dbupdate() {
function dbupdate(): void {
startup_gettext();
if (!Config::get(Config::SINGLE_USER_MODE) && ($_SESSION["access_level"] ?? 0) < 10) {
@@ -730,7 +736,7 @@ class Handler_Public extends Handler {
<?php
}
function cached() {
function cached(): void {
list ($cache_dir, $filename) = explode("/", $_GET["file"], 2);
// we do not allow files with extensions at the moment
@@ -746,7 +752,7 @@ class Handler_Public extends Handler {
}
}
private function _make_article_tag_uri($id, $timestamp) {
private function _make_article_tag_uri(int $id, string $timestamp): string {
$timestamp = date("Y-m-d", strtotime($timestamp));
@@ -756,7 +762,7 @@ class Handler_Public extends Handler {
// this should be used very carefully because this endpoint is exposed to unauthenticated users
// plugin data is not loaded because there's no user context and owner_uid/session may or may not be available
// in general, don't do anything user-related in here and do not modify $_SESSION
public function pluginhandler() {
public function pluginhandler(): void {
$host = new PluginHost();
$plugin_name = basename(clean($_REQUEST["plugin"]));
@@ -788,7 +794,7 @@ class Handler_Public extends Handler {
}
}
static function _render_login_form(string $return_to = "") {
static function _render_login_form(string $return_to = ""): void {
header('Cache-Control: public');
if ($return_to)

View File

@@ -1,5 +1,18 @@
<?php
interface IAuthModule {
function authenticate($login, $password); // + optional third parameter: $service
function hook_auth_user(...$args); // compatibility wrapper due to how hooks work
/**
* @param string $login
* @param string $password
* @param string $service
* @return int|false user_id
*/
function authenticate($login, $password, $service = '');
/** this is a pluginhost compatibility wrapper that invokes $this->authenticate(...$args) (Auth_Base)
* @param string $login
* @param string $password
* @param string $service
* @return int|false user_id
*/
function hook_auth_user($login, $password, $service = '');
}

View File

@@ -1,6 +1,6 @@
<?php
interface IHandler {
function csrf_ignore($method);
function before($method);
function after();
function csrf_ignore(string $method): bool;
function before(string $method): bool;
function after(): bool;
}

View File

@@ -1,15 +1,15 @@
<?php
class Labels
{
static function label_to_feed_id($label) {
static function label_to_feed_id(int $label): int {
return LABEL_BASE_INDEX - 1 - abs($label);
}
static function feed_to_label_id($feed) {
static function feed_to_label_id(int $feed): int {
return LABEL_BASE_INDEX - 1 + abs($feed);
}
static function find_id($label, $owner_uid) {
static function find_id(string $label, int $owner_uid): int {
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT id FROM ttrss_labels2 WHERE LOWER(caption) = LOWER(?)
@@ -23,7 +23,7 @@ class Labels
}
}
static function find_caption($label, $owner_uid) {
static function find_caption(int $label, int $owner_uid): string {
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT caption FROM ttrss_labels2 WHERE id = ?
@@ -37,18 +37,24 @@ class Labels
}
}
static function get_as_hash($owner_uid) {
/**
* @return array<int, array<string, string>>
*/
static function get_as_hash(int $owner_uid): array {
$rv = [];
$labels = Labels::get_all($owner_uid);
foreach ($labels as $i => $label) {
$rv[$label["id"]] = $labels[$i];
$rv[(int)$label["id"]] = $labels[$i];
}
return $rv;
}
static function get_all($owner_uid) {
/**
* @return array<int, array<string, string>> An array of label detail arrays
*/
static function get_all(int $owner_uid) {
$rv = array();
$pdo = Db::pdo();
@@ -64,7 +70,13 @@ class Labels
return $rv;
}
static function update_cache($owner_uid, $id, $labels = false, $force = false) {
/**
* @param array{'no-labels': 1}|array<int, array<int, array{0: int, 1: string, 2: string, 3: string}>> $labels
* [label_id, caption, fg_color, bg_color]
*
* @see Article::_get_labels()
*/
static function update_cache(int $owner_uid, int $id, array $labels, bool $force = false): void {
$pdo = Db::pdo();
if ($force)
@@ -81,7 +93,7 @@ class Labels
}
static function clear_cache($id) {
static function clear_cache(int $id): void {
$pdo = Db::pdo();
@@ -91,7 +103,7 @@ class Labels
}
static function remove_article($id, $label, $owner_uid) {
static function remove_article(int $id, string $label, int $owner_uid): void {
$label_id = self::find_id($label, $owner_uid);
@@ -109,7 +121,7 @@ class Labels
self::clear_cache($id);
}
static function add_article($id, $label, $owner_uid) {
static function add_article(int $id, string $label, int $owner_uid): void {
$label_id = self::find_id($label, $owner_uid);
@@ -138,7 +150,7 @@ class Labels
}
static function remove($id, $owner_uid) {
static function remove(int $id, int $owner_uid): void {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
@@ -182,7 +194,10 @@ class Labels
if (!$tr_in_progress) $pdo->commit();
}
static function create($caption, $fg_color = '', $bg_color = '', $owner_uid = false) {
/**
* @return false|int false if the check for an existing label failed, otherwise the number of rows inserted (1 on success)
*/
static function create(string $caption, ?string $fg_color = '', ?string $bg_color = '', ?int $owner_uid = null) {
if (!$owner_uid) $owner_uid = $_SESSION['uid'];

View File

@@ -1,6 +1,9 @@
<?php
class Logger {
/** @var Logger|null */
private static $instance;
/** @var Logger_Adapter|null */
private $adapter;
const LOG_DEST_SQL = "sql";
@@ -25,11 +28,11 @@ class Logger {
16384 => 'E_USER_DEPRECATED',
32767 => 'E_ALL'];
static function log_error(int $errno, string $errstr, string $file, int $line, $context) {
static function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
return self::get_instance()->_log_error($errno, $errstr, $file, $line, $context);
}
private function _log_error($errno, $errstr, $file, $line, $context) {
private function _log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
//if ($errno == E_NOTICE) return false;
if ($this->adapter)
@@ -38,11 +41,11 @@ class Logger {
return false;
}
static function log(int $errno, string $errstr, $context = "") {
static function log(int $errno, string $errstr, string $context = ""): bool {
return self::get_instance()->_log($errno, $errstr, $context);
}
private function _log(int $errno, string $errstr, $context = "") {
private function _log(int $errno, string $errstr, string $context = ""): bool {
if ($this->adapter)
return $this->adapter->log_error($errno, $errstr, '', 0, $context);
else
@@ -65,7 +68,7 @@ class Logger {
$this->adapter = new Logger_Stdout();
break;
default:
$this->adapter = false;
$this->adapter = null;
}
if ($this->adapter && !implements_interface($this->adapter, "Logger_Adapter"))

View File

@@ -1,4 +1,4 @@
<?php
interface Logger_Adapter {
function log_error(int $errno, string $errstr, string $file, int $line, $context);
function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool;
}

View File

@@ -10,7 +10,7 @@ class Logger_SQL implements Logger_Adapter {
ORM::configure('return_result_sets', true, $conn);
}
function log_error(int $errno, string $errstr, string $file, int $line, $context) {
function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
if (Config::get_schema_version() > 117) {

View File

@@ -1,7 +1,7 @@
<?php
class Logger_Stdout implements Logger_Adapter {
function log_error(int $errno, string $errstr, string $file, int $line, $context) {
function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
switch ($errno) {
case E_ERROR:
@@ -25,6 +25,7 @@ class Logger_Stdout implements Logger_Adapter {
print "[EEE] $priority $errname ($file:$line) $errstr\n";
return true;
}
}

View File

@@ -1,7 +1,7 @@
<?php
class Logger_Syslog implements Logger_Adapter {
function log_error(int $errno, string $errstr, string $file, int $line, $context) {
function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool {
switch ($errno) {
case E_ERROR:
@@ -25,6 +25,7 @@ class Logger_Syslog implements Logger_Adapter {
syslog($priority, "[tt-rss] $errname ($file:$line) $errstr");
return true;
}
}

View File

@@ -1,8 +1,14 @@
<?php
class Mailer {
// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
/** @var string */
private $last_error = "";
function mail($params) {
/**
* @param array<string, mixed> $params
* @return bool|int bool if the default mail function handled the request, otherwise an int as described in Mailer#mail()
*/
function mail(array $params) {
$to_name = $params["to_name"] ?? "";
$to_address = $params["to_address"];
@@ -25,6 +31,8 @@ class Mailer {
// 3. any other return value will allow cycling to the next handler and, eventually, to default mail() function
// 4. set error message if needed via passed Mailer instance function set_error()
$hooks_tried = 0;
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEND_MAIL) as $p) {
$rc = $p->hook_send_mail($this, $params);
@@ -33,6 +41,8 @@ class Mailer {
if ($rc == -1)
return 0;
++$hooks_tried;
}
$headers = [ "From: $from_combined", "Content-Type: text/plain; charset=UTF-8" ];
@@ -40,18 +50,18 @@ class Mailer {
$rc = mail($to_combined, $subject, $message, implode("\r\n", array_merge($headers, $additional_headers)));
if (!$rc) {
$this->set_error(error_get_last()['message']);
$this->set_error(error_get_last()['message'] ?? T_sprintf("Unknown error while sending mail. Hooks tried: %d.", $hooks_tried));
}
return $rc;
}
function set_error($message) {
function set_error(string $message): void {
$this->last_error = $message;
user_error("Error sending mail: $message", E_USER_WARNING);
}
function error() {
function error(): string {
return $this->last_error;
}
}

View File

@@ -1,12 +1,15 @@
<?php
class OPML extends Handler_Protected {
function csrf_ignore($method) {
function csrf_ignore(string $method): bool {
$csrf_ignored = array("export", "import");
return array_search($method, $csrf_ignored) !== false;
}
/**
* @return bool|int|void false if writing the file failed, true if printing succeeded, int if bytes were written to a file, or void if $owner_uid is missing
*/
function export() {
$output_name = sprintf("tt-rss_%s_%s.opml", $_SESSION["name"], date("Y-m-d"));
$include_settings = $_REQUEST["include_settings"] == "1";
@@ -17,7 +20,7 @@ class OPML extends Handler_Protected {
return $rc;
}
function import() {
function import(): void {
$owner_uid = $_SESSION["uid"];
header('Content-Type: text/html; charset=utf-8');
@@ -42,13 +45,11 @@ class OPML extends Handler_Protected {
</form>";
print "</div></body></html>";
}
// Export
private function opml_export_category(int $owner_uid, int $cat_id, bool $hide_private_feeds = false, bool $include_settings = true) {
private function opml_export_category(int $owner_uid, int $cat_id, bool $hide_private_feeds = false, bool $include_settings = true): string {
if ($hide_private_feeds)
$hide_qpart = "(private IS false AND auth_login = '' AND auth_pass = '')";
@@ -124,6 +125,9 @@ class OPML extends Handler_Protected {
return $out;
}
/**
* @return bool|int|void false if writing the file failed, true if printing succeeded, int if bytes were written to a file, or void if $owner_uid is missing
*/
function opml_export(string $filename, int $owner_uid, bool $hide_private_feeds = false, bool $include_settings = true, bool $file_output = false) {
if (!$owner_uid) return;
@@ -290,13 +294,14 @@ class OPML extends Handler_Protected {
if ($file_output)
return file_put_contents($filename, $res) > 0;
else
print $res;
return true;
}
// Import
private function opml_import_feed(DOMNode $node, int $cat_id, int $owner_uid, int $nest) {
private function opml_import_feed(DOMNode $node, int $cat_id, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$feed_title = mb_substr($attrs->getNamedItem('text')->nodeValue, 0, 250);
@@ -341,7 +346,7 @@ class OPML extends Handler_Protected {
}
}
private function opml_import_label(DOMNode $node, int $owner_uid, int $nest) {
private function opml_import_label(DOMNode $node, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$label_name = $attrs->getNamedItem('label-name')->nodeValue;
@@ -358,7 +363,7 @@ class OPML extends Handler_Protected {
}
}
private function opml_import_preference(DOMNode $node, int $owner_uid, int $nest) {
private function opml_import_preference(DOMNode $node, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$pref_name = $attrs->getNamedItem('pref-name')->nodeValue;
@@ -372,7 +377,7 @@ class OPML extends Handler_Protected {
}
}
private function opml_import_filter(DOMNode $node, int $owner_uid, int $nest) {
private function opml_import_filter(DOMNode $node, int $owner_uid, int $nest): void {
$attrs = $node->attributes;
$filter_type = $attrs->getNamedItem('filter-type')->nodeValue;
@@ -526,7 +531,7 @@ class OPML extends Handler_Protected {
}
}
private function opml_import_category(DOMDocument $doc, ?DOMNode $root_node, int $owner_uid, int $parent_id, int $nest) {
private function opml_import_category(DOMDocument $doc, ?DOMNode $root_node, int $owner_uid, int $parent_id, int $nest): void {
$default_cat_id = (int) $this->get_feed_category('Imported feeds', $owner_uid, 0);
if ($root_node) {
@@ -601,6 +606,9 @@ class OPML extends Handler_Protected {
}
/** $filename is optional; assumes HTTP upload with $_FILES otherwise */
/**
* @return bool|void false on failure, true if successful, void if $owner_uid is missing
*/
function opml_import(int $owner_uid, string $filename = "") {
if (!$owner_uid) return;
@@ -667,7 +675,7 @@ class OPML extends Handler_Protected {
return true;
}
private function opml_notice(string $msg, int $prefix_length = 0) {
private function opml_notice(string $msg, int $prefix_length = 0): void {
if (php_sapi_name() == "cli") {
Debug::log(str_repeat(" ", $prefix_length) . $msg);
} else {

View File

@@ -5,8 +5,14 @@ abstract class Plugin {
/** @var PDO $pdo */
protected $pdo;
abstract function init(PluginHost $host);
/**
* @param PluginHost $host
*
* @return void
* */
abstract function init($host);
/** @return array<null|float|string|bool> */
abstract function about();
// return array(1.0, "plugin", "No description", "No author", false);
@@ -14,6 +20,7 @@ abstract class Plugin {
$this->pdo = Db::pdo();
}
/** @return array<string,bool> */
function flags() {
/* associative array, possible keys:
needs_curl = boolean
@@ -21,36 +28,65 @@ abstract class Plugin {
return array();
}
/**
* @param string $method
*
* @return bool */
function is_public_method($method) {
return false;
}
/**
* @param string $method
*
* @return bool */
function csrf_ignore($method) {
return false;
}
/** @return string */
function get_js() {
return "";
}
/** @return string */
function get_css() {
return "";
}
/** @return string */
function get_prefs_js() {
return "";
}
/** @return int */
function api_version() {
return Plugin::API_VERSION_COMPAT;
}
/* gettext-related helpers */
/**
* @param string $msgid
*
* @return string */
function __($msgid) {
/** @var Plugin $this -- this is a strictly template-related hack */
return _dgettext(PluginHost::object_to_domain($this), $msgid);
}
/**
* @param string $singular
* @param string $plural
* @param int $number
*
* @return string */
function _ngettext($singular, $plural, $number) {
/** @var Plugin $this -- this is a strictly template-related hack */
return _dngettext(PluginHost::object_to_domain($this), $singular, $plural, $number);
}
/** @return string */
function T_sprintf() {
$args = func_get_args();
$msgid = array_shift($args);
@@ -58,4 +94,580 @@ abstract class Plugin {
return vsprintf($this->__($msgid), $args);
}
/* plugin hook methods */
/* GLOBAL hooks are invoked in global context, only available to system plugins (loaded via .env for all users) */
/** Adds buttons for article (on the right) - e.g. mail, share, add note.
* @param array<string,mixed> $line
* @return string
* @see PluginHost::HOOK_ARTICLE_BUTTON
* @see Plugin::hook_article_left_button()
*/
function hook_article_button($line) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Allows plugins to alter article data as gathered from feed XML, i.e. embed images, get full text content, etc.
* @param array<string,mixed> $article
* @return array<string,mixed>
* @see PluginHost::HOOK_ARTICLE_FILTER
*/
function hook_article_filter($article) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Allow adding new UI elements (e.g. accordion panes) to (top) tab contents in Preferences
* @param string $tab
* @return void
* @see PluginHost::HOOK_PREFS_TAB
*/
function hook_prefs_tab($tab) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Allow adding new content to various sections of preferences UI (i.e. OPML import/export pane)
* @param string $section
* @return void
* @see PluginHost::HOOK_PREFS_TAB_SECTION
*/
function hook_prefs_tab_section($section) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Allows adding new (top) tabs in preferences UI
* @return void
* @see PluginHost::HOOK_PREFS_TABS
*/
function hook_prefs_tabs() {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Invoked when feed XML is processed by FeedParser class
* @param FeedParser $parser
* @param int $feed_id
* @return void
* @see PluginHost::HOOK_FEED_PARSED
*/
function hook_feed_parsed($parser, $feed_id) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** GLOBAL: Invoked when a feed update task finishes
* @param array<string,string> $cli_options
* @return void
* @see PluginHost::HOOK_UPDATE_TASK
*/
function hook_update_task($cli_options) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** This is a pluginhost compatibility wrapper that invokes $this->authenticate(...$args) (Auth_Base)
* @param string $login
* @param string $password
* @param string $service
* @return int|false user_id
* @see PluginHost::HOOK_AUTH_USER
*/
function hook_auth_user($login, $password, $service = '') {
user_error("Dummy method invoked.", E_USER_ERROR);
return false;
}
/** IAuthModule only
* @param string $login
* @param string $password
* @param string $service
* @return int|false user_id
*/
function authenticate($login, $password, $service = '') {
user_error("Dummy method invoked.", E_USER_ERROR);
return false;
}
/** Allows plugins to modify global hotkey map (hotkey sequence -> action)
* @param array<string, string> $hotkeys
* @return array<string, string>
* @see PluginHost::HOOK_HOTKEY_MAP
* @see Plugin::hook_hotkey_info()
*/
function hook_hotkey_map($hotkeys) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Invoked when article is rendered by backend (before it gets passed to frontent JS code) - three panel mode
* @param array<string, mixed> $article
* @return array<string, mixed>
* @see PluginHost::HOOK_RENDER_ARTICLE
*/
function hook_render_article($article) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Invoked when article is rendered by backend (before it gets passed to frontent JS code) - combined mode
* @param array<string, mixed> $article
* @return array<string, mixed>
* @see PluginHost::HOOK_RENDER_ARTICLE_CDM
*/
function hook_render_article_cdm($article) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Invoked when raw feed XML data has been successfully downloaded (but not parsed yet)
* @param string $feed_data
* @param string $fetch_url
* @param int $owner_uid
* @param int $feed
* @return string
* @see PluginHost::HOOK_FEED_FETCHED
*/
function hook_feed_fetched($feed_data, $fetch_url, $owner_uid, $feed) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked on article content when it is sanitized (i.e. potentially harmful tags removed)
* @param DOMDocument $doc
* @param string $site_url
* @param array<string> $allowed_elements
* @param array<string> $disallowed_attributes
* @param int $article_id
* @return DOMDocument|array<int,DOMDocument|array<string>>
* @see PluginHost::HOOK_SANITIZE
*/
function hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) {
user_error("Dummy method invoked.", E_USER_ERROR);
return $doc;
}
/** Invoked when article is rendered by backend (before it gets passed to frontent JS code) - exclusive to API clients
* @param array{'article': array<string,mixed>|null, 'headline': array<string,mixed>|null} $params
* @return array<string, string>
* @see PluginHost::HOOK_RENDER_ARTICLE_API
*/
function hook_render_article_api($params) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Allows adding new UI elements to tt-rss main toolbar (to the right, before Actions... dropdown)
* @return string
* @see PluginHost::HOOK_TOOLBAR_BUTTON
*/
function hook_toolbar_button() {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Allows adding new items to tt-rss main Actions... dropdown menu
* @return string
* @see PluginHost::HOOK_ACTION_ITEM
*/
function hook_action_item() {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Allows adding new UI elements to the toolbar area related to currently loaded feed headlines
* @param int $feed_id
* @param bool $is_cat
* @return string
* @see PluginHost::HOOK_HEADLINE_TOOLBAR_BUTTON
*/
function hook_headline_toolbar_button($feed_id, $is_cat) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Allows adding new hotkey action names and descriptions
* @param array<string, array<string, string>> $hotkeys
* @return array<string, array<string, string>>
* @see PluginHost::HOOK_HOTKEY_INFO
* @see Plugin::hook_hotkey_map()
*/
function hook_hotkey_info($hotkeys) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Adds per-article buttons on the left side
* @param array<string,mixed> $row
* @return string
* @see PluginHost::HOOK_ARTICLE_LEFT_BUTTON
* @see Plugin::hook_article_button()
*/
function hook_article_left_button($row) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Allows adding new UI elements to the "Plugins" tab of the feed editor UI
* @param int $feed_id
* @return void
* @see PluginHost::HOOK_PREFS_EDIT_FEED
*/
function hook_prefs_edit_feed($feed_id) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Invoked when data is saved in the feed editor
* @param int $feed_id
* @return void
* @see PluginHost::HOOK_PREFS_SAVE_FEED
*/
function hook_prefs_save_feed($feed_id) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Allows overriding built-in fetching mechanism for feeds, substituting received data if necessary
* (i.e. origin site doesn't actually provide any RSS feeds), or XML is invalid
* @param string $feed_data
* @param string $fetch_url
* @param int $owner_uid
* @param int $feed
* @param int $last_article_timestamp
* @param string $auth_login
* @param string $auth_pass
* @return string (possibly mangled feed data)
* @see PluginHost::HOOK_FETCH_FEED
*/
function hook_fetch_feed($feed_data, $fetch_url, $owner_uid, $feed, $last_article_timestamp, $auth_login, $auth_pass) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked when headlines data ($row) has been retrieved from the database
* @param array<string,mixed> $row
* @param int $excerpt_length
* @return array<string,mixed>
* @see PluginHost::HOOK_QUERY_HEADLINES
*/
function hook_query_headlines($row, $excerpt_length) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** GLOBAL: This is run periodically by the update daemon when idle
* @return void
* @see PluginHost::HOOK_HOUSE_KEEPING */
function hook_house_keeping() {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Allows overriding built-in article search
* @param string $query
* @return array<int, string|array<string>> - list(SQL search query, highlight keywords)
* @see PluginHost::HOOK_SEARCH
*/
function hook_search($query) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Invoked when enclosures are rendered to HTML (when article itself is rendered)
* @param string $enclosures_formatted
* @param array<int, array<string, mixed>> $enclosures
* @param int $article_id
* @param bool $always_display_enclosures
* @param string $article_content
* @param bool $hide_images
* @return string|array<string,array<int, array<string, mixed>>> ($enclosures_formatted, $enclosures)
* @see PluginHost::HOOK_FORMAT_ENCLOSURES
*/
function hook_format_enclosures($enclosures_formatted, $enclosures, $article_id, $always_display_enclosures, $article_content, $hide_images) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked during feed subscription (after data has been fetched)
* @param string $contents
* @param string $url
* @param string $auth_login
* @param string $auth_pass
* @return string (possibly mangled feed data)
* @see PluginHost::HOOK_SUBSCRIBE_FEED
*/
function hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/**
* @param int $feed
* @param bool $is_cat
* @param array<string,mixed> $qfh_ret (headlines object)
* @return string
* @see PluginHost::HOOK_HEADLINES_BEFORE
*/
function hook_headlines_before($feed, $is_cat, $qfh_ret) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/**
* @param array<string,mixed> $entry
* @param int $article_id
* @param array<string,mixed> $rv
* @return string
* @see PluginHost::HOOK_RENDER_ENCLOSURE
*/
function hook_render_enclosure($entry, $article_id, $rv) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/**
* @param array<string,mixed> $article
* @param string $action
* @return array<string,mixed> ($article)
* @see PluginHost::HOOK_ARTICLE_FILTER_ACTION
*/
function hook_article_filter_action($article, $action) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/**
* @param array<string,mixed> $line
* @param int $feed
* @param bool $is_cat
* @param int $owner_uid
* @return array<string,mixed> ($line)
* @see PluginHost::HOOK_ARTICLE_EXPORT_FEED
*/
function hook_article_export_feed($line, $feed, $is_cat, $owner_uid) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Allows adding custom buttons to tt-rss main toolbar (left side)
* @return void
* @see PluginHost::HOOK_MAIN_TOOLBAR_BUTTON
*/
function hook_main_toolbar_button() {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Invoked for every enclosure entry as article is being rendered
* @param array<string,string> $entry
* @param int $id
* @param array{'formatted': string, 'entries': array<int, array<string, mixed>>} $rv
* @return array<string,string> ($entry)
* @see PluginHost::HOOK_ENCLOSURE_ENTRY
*/
function hook_enclosure_entry($entry, $id, $rv) {
user_error("Dummy method invoked.", E_USER_ERROR);
return [];
}
/** Share plugins run this when article is being rendered as HTML for sharing
* @param string $html
* @param array<string,mixed> $row
* @return string ($html)
* @see PluginHost::HOOK_FORMAT_ARTICLE
*/
function hook_format_article($html, $row) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked when basic feed information (title, site_url) is being collected, useful to override default if feed doesn't provide anything (or feed itself is synthesized)
* @param array{"title": string, "site_url": string} $basic_info
* @param string $fetch_url
* @param int $owner_uid
* @param int $feed_id
* @param string $auth_login
* @param string $auth_pass
* @return array{"title": string, "site_url": string}
* @see PluginHost::HOOK_FEED_BASIC_INFO
*/
function hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed_id, $auth_login, $auth_pass) {
user_error("Dummy method invoked.", E_USER_ERROR);
return $basic_info;
}
/** Invoked when file (e.g. cache entry, static data) is being sent to client, may override default mechanism
* using faster httpd-specific implementation (see nginx_xaccel)
* @param string $filename
* @return bool
* @see PluginHost::HOOK_SEND_LOCAL_FILE
*/
function hook_send_local_file($filename) {
user_error("Dummy method invoked.", E_USER_ERROR);
return false;
}
/** Invoked when user tries to unsubscribe from a feed, returning true would prevent any further default actions
* @param int $feed_id
* @param int $owner_uid
* @return bool
* @see PluginHost::HOOK_UNSUBSCRIBE_FEED
*/
function hook_unsubscribe_feed($feed_id, $owner_uid) {
user_error("Dummy method invoked.", E_USER_ERROR);
return false;
}
/** Invoked when mail is being sent (if no hooks are registered, tt-rss uses PHP mail() as a fallback)
* @param Mailer $mailer
* @param array<string,mixed> $params
* @return int
* @see PluginHost::HOOK_SEND_MAIL
*/
function hook_send_mail($mailer, $params) {
user_error("Dummy method invoked.", E_USER_ERROR);
return -1;
}
/** Invoked when filter is triggered on an article, may be used to implement logging for filters
* NOTE: $article_filters should be renamed $filter_actions because that's what this is
* @param int $feed_id
* @param int $owner_uid
* @param array<string,mixed> $article
* @param array<string,mixed> $matched_filters
* @param array<string,string|bool|int> $matched_rules
* @param array<string,string> $article_filters
* @return void
* @see PluginHost::HOOK_FILTER_TRIGGERED
*/
function hook_filter_triggered($feed_id, $owner_uid, $article, $matched_filters, $matched_rules, $article_filters) {
user_error("Dummy method invoked.", E_USER_ERROR);
}
/** Plugins may provide this to allow getting full article text (af_readbility implements this)
* @param string $url
* @return string|false
* @see PluginHost::HOOK_GET_FULL_TEXT
*/
function hook_get_full_text($url) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked when article flavor image is being determined, allows overriding default selection logic
* @param array<string,string> $enclosures
* @param string $content
* @param string $site_url
* @param array<string,mixed> $article
* @return string|array<int,string>
* @see PluginHost::HOOK_ARTICLE_IMAGE
*/
function hook_article_image($enclosures, $content, $site_url, $article) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Allows adding arbitrary elements before feed tree
* @return string HTML
* @see PluginHost::HOOK_FEED_TREE
* */
function hook_feed_tree() {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked for every iframe to determine if it is allowed to be displayed
* @param string $url
* @return bool
* @see PluginHost::HOOK_IFRAME_WHITELISTED
*/
function hook_iframe_whitelisted($url) {
user_error("Dummy method invoked.", E_USER_ERROR);
return false;
}
/**
* @param object $enclosure
* @param int $feed
* @return object ($enclosure)
* @see PluginHost::HOOK_ENCLOSURE_IMPORTED
*/
function hook_enclosure_imported($enclosure, $feed) {
user_error("Dummy method invoked.", E_USER_ERROR);
return $enclosure;
}
/** Allows adding custom elements to headline sort dropdown (name -> caption)
* @return array<string,string>
* @see PluginHost::HOOK_HEADLINES_CUSTOM_SORT_MAP
*/
function hook_headlines_custom_sort_map() {
user_error("Dummy method invoked.", E_USER_ERROR);
return ["" => ""];
}
/** Allows overriding headline sorting (or provide custom sort methods)
* @param string $order
* @return array<int, string|bool> -- (query, skip_first_id)
* @see PluginHost::HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE
*/
function hook_headlines_custom_sort_override($order) {
user_error("Dummy method invoked.", E_USER_ERROR);
return ["", false];
}
/** Allows adding custom elements to headlines Select... dropdown
* @param int $feed_id
* @param int $is_cat
* @return string
* @see PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM
*/
function hook_headline_toolbar_select_menu_item($feed_id, $is_cat) {
user_error("Dummy method invoked.", E_USER_ERROR);
return "";
}
/** Invoked when user tries to subscribe to feed, may override information (i.e. feed URL) used afterwards
* @param string $url
* @param string $auth_login
* @param string $auth_pass
* @return bool
* @see PluginHost::HOOK_PRE_SUBSCRIBE
*/
function hook_pre_subscribe(&$url, $auth_login, $auth_pass) {
user_error("Dummy method invoked.", E_USER_ERROR);
return false;
}
}

View File

@@ -1,10 +1,10 @@
<?php
class PluginHandler extends Handler_Protected {
function csrf_ignore($method) {
function csrf_ignore(string $method): bool {
return true;
}
function catchall($method) {
function catchall(string $method): void {
$plugin_name = clean($_REQUEST["plugin"]);
$plugin = PluginHost::getInstance()->get_plugin($plugin_name);
$csrf_token = ($_POST["csrf_token"] ?? "");

View File

@@ -1,186 +1,211 @@
<?php
class PluginHost {
private $pdo;
/* separate handle for plugin data so transaction while saving wouldn't clash with possible main
tt-rss code transactions; only initialized when first needed */
private $pdo_data;
private $hooks = array();
private $plugins = array();
private $handlers = array();
private $commands = array();
private $storage = array();
private $feeds = array();
private $api_methods = array();
private $plugin_actions = array();
private $owner_uid;
private $data_loaded;
private static $instance;
// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
/** @var PDO|null */
private $pdo = null;
/**
* separate handle for plugin data so transaction while saving wouldn't clash with possible main
* tt-rss code transactions; only initialized when first needed
*
* @var PDO|null
*/
private $pdo_data = null;
/** @var array<string, array<int, array<int, Plugin>>> hook types -> priority levels -> Plugins */
private $hooks = [];
/** @var array<string, Plugin> */
private $plugins = [];
/** @var array<string, array<string, Plugin>> handler type -> method type -> Plugin */
private $handlers = [];
/** @var array<string, array{'description': string, 'suffix': string, 'arghelp': string, 'class': Plugin}> command type -> details array */
private $commands = [];
/** @var array<string, array<string, mixed>> plugin name -> (potential profile array) -> key -> value */
private $storage = [];
/** @var array<int, array<int, array{'id': int, 'title': string, 'sender': Plugin, 'icon': string}>> */
private $feeds = [];
/** @var array<string, Plugin> API method name, Plugin sender */
private $api_methods = [];
/** @var array<string, array<int, array{'action': string, 'description': string, 'sender': Plugin}>> */
private $plugin_actions = [];
/** @var int|null */
private $owner_uid = null;
/** @var bool */
private $data_loaded = false;
/** @var PluginHost|null */
private static $instance = null;
const API_VERSION = 2;
const PUBLIC_METHOD_DELIMITER = "--";
// Hooks marked with *1 are run in global context and available
// to plugins loaded in config.php only
/** hook_article_button($line) */
/** @see Plugin::hook_article_button() */
const HOOK_ARTICLE_BUTTON = "hook_article_button";
/** hook_article_filter($article) */
/** @see Plugin::hook_article_filter() */
const HOOK_ARTICLE_FILTER = "hook_article_filter";
/** hook_prefs_tab($tab) */
/** @see Plugin::hook_prefs_tab() */
const HOOK_PREFS_TAB = "hook_prefs_tab";
/** hook_prefs_tab_section($section) */
/** @see Plugin::hook_prefs_tab_section() */
const HOOK_PREFS_TAB_SECTION = "hook_prefs_tab_section";
/** hook_prefs_tabs() */
/** @see Plugin::hook_prefs_tabs() */
const HOOK_PREFS_TABS = "hook_prefs_tabs";
/** hook_feed_parsed($parser, $feed_id) */
/** @see Plugin::hook_feed_parsed() */
const HOOK_FEED_PARSED = "hook_feed_parsed";
/** GLOBAL: hook_update_task($cli_options) */
/** @see Plugin::hook_update_task() */
const HOOK_UPDATE_TASK = "hook_update_task"; //*1
/** hook_auth_user($login, $password, $service) (byref) */
/** @see Plugin::hook_auth_user() */
const HOOK_AUTH_USER = "hook_auth_user";
/** hook_hotkey_map($hotkeys) (byref) */
/** @see Plugin::hook_hotkey_map() */
const HOOK_HOTKEY_MAP = "hook_hotkey_map";
/** hook_render_article($article) */
/** @see Plugin::hook_render_article() */
const HOOK_RENDER_ARTICLE = "hook_render_article";
/** hook_render_article_cdm($article) */
/** @see Plugin::hook_render_article_cdm() */
const HOOK_RENDER_ARTICLE_CDM = "hook_render_article_cdm";
/** hook_feed_fetched($feed_data, $fetch_url, $owner_uid, $feed) (byref) */
/** @see Plugin::hook_feed_fetched() */
const HOOK_FEED_FETCHED = "hook_feed_fetched";
/** hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) (byref) */
/** @see Plugin::hook_sanitize() */
const HOOK_SANITIZE = "hook_sanitize";
/** hook_render_article_api($params) */
/** @see Plugin::hook_render_article_api() */
const HOOK_RENDER_ARTICLE_API = "hook_render_article_api";
/** hook_toolbar_button() */
/** @see Plugin::hook_toolbar_button() */
const HOOK_TOOLBAR_BUTTON = "hook_toolbar_button";
/** hook_action_item() */
/** @see Plugin::hook_action_item() */
const HOOK_ACTION_ITEM = "hook_action_item";
/** hook_headline_toolbar_button($feed_id, $is_cat) */
/** @see Plugin::hook_headline_toolbar_button() */
const HOOK_HEADLINE_TOOLBAR_BUTTON = "hook_headline_toolbar_button";
/** hook_hotkey_info($hotkeys) (byref) */
/** @see Plugin::hook_hotkey_info() */
const HOOK_HOTKEY_INFO = "hook_hotkey_info";
/** hook_article_left_button($row) */
/** @see Plugin::hook_article_left_button() */
const HOOK_ARTICLE_LEFT_BUTTON = "hook_article_left_button";
/** hook_prefs_edit_feed($feed_id) */
/** @see Plugin::hook_prefs_edit_feed() */
const HOOK_PREFS_EDIT_FEED = "hook_prefs_edit_feed";
/** hook_prefs_save_feed($feed_id) */
/** @see Plugin::hook_prefs_save_feed() */
const HOOK_PREFS_SAVE_FEED = "hook_prefs_save_feed";
/** hook_fetch_feed($feed_data, $fetch_url, $owner_uid, $feed, $last_article_timestamp, $auth_login, $auth_pass) (byref) */
/** @see Plugin::hook_fetch_feed() */
const HOOK_FETCH_FEED = "hook_fetch_feed";
/** hook_query_headlines($row) (byref) */
/** @see Plugin::hook_query_headlines() */
const HOOK_QUERY_HEADLINES = "hook_query_headlines";
/** GLOBAL: hook_house_keeping() */
/** @see Plugin::hook_house_keeping() */
const HOOK_HOUSE_KEEPING = "hook_house_keeping"; //*1
/** hook_search($query) */
/** @see Plugin::hook_search() */
const HOOK_SEARCH = "hook_search";
/** hook_format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images) (byref) */
/** @see Plugin::hook_format_enclosures() */
const HOOK_FORMAT_ENCLOSURES = "hook_format_enclosures";
/** hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) (byref) */
/** @see Plugin::hook_subscribe_feed() */
const HOOK_SUBSCRIBE_FEED = "hook_subscribe_feed";
/** hook_headlines_before($feed, $is_cat, $qfh_ret) */
/** @see Plugin::hook_headlines_before() */
const HOOK_HEADLINES_BEFORE = "hook_headlines_before";
/** hook_render_enclosure($entry, $id, $rv) */
/** @see Plugin::hook_render_enclosure() */
const HOOK_RENDER_ENCLOSURE = "hook_render_enclosure";
/** hook_article_filter_action($article, $action) */
/** @see Plugin::hook_article_filter_action() */
const HOOK_ARTICLE_FILTER_ACTION = "hook_article_filter_action";
/** hook_article_export_feed($line, $feed, $is_cat, $owner_uid) (byref) */
/** @see Plugin::hook_article_export_feed() */
const HOOK_ARTICLE_EXPORT_FEED = "hook_article_export_feed";
/** hook_main_toolbar_button() */
/** @see Plugin::hook_main_toolbar_button() */
const HOOK_MAIN_TOOLBAR_BUTTON = "hook_main_toolbar_button";
/** hook_enclosure_entry($entry, $id, $rv) (byref) */
/** @see Plugin::hook_enclosure_entry() */
const HOOK_ENCLOSURE_ENTRY = "hook_enclosure_entry";
/** hook_format_article($html, $row) */
/** @see Plugin::hook_format_article() */
const HOOK_FORMAT_ARTICLE = "hook_format_article";
/** @deprecated removed, do not use */
/** @see Plugin::hook_format_article_cdm() */
const HOOK_FORMAT_ARTICLE_CDM = "hook_format_article_cdm";
/** hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed_id, $auth_login, $auth_pass) (byref) */
/** @see Plugin::hook_feed_basic_info() */
const HOOK_FEED_BASIC_INFO = "hook_feed_basic_info";
/** hook_send_local_file($filename) */
/** @see Plugin::hook_send_local_file() */
const HOOK_SEND_LOCAL_FILE = "hook_send_local_file";
/** hook_unsubscribe_feed($feed_id, $owner_uid) */
/** @see Plugin::hook_unsubscribe_feed() */
const HOOK_UNSUBSCRIBE_FEED = "hook_unsubscribe_feed";
/** hook_send_mail(Mailer $mailer, $params) */
/** @see Plugin::hook_send_mail() */
const HOOK_SEND_MAIL = "hook_send_mail";
/** hook_filter_triggered($feed_id, $owner_uid, $article, $matched_filters, $matched_rules, $article_filters) */
/** @see Plugin::hook_filter_triggered() */
const HOOK_FILTER_TRIGGERED = "hook_filter_triggered";
/** hook_get_full_text($url) */
/** @see Plugin::hook_get_full_text() */
const HOOK_GET_FULL_TEXT = "hook_get_full_text";
/** hook_article_image($enclosures, $content, $site_url) */
/** @see Plugin::hook_article_image() */
const HOOK_ARTICLE_IMAGE = "hook_article_image";
/** hook_feed_tree() */
/** @see Plugin::hook_feed_tree() */
const HOOK_FEED_TREE = "hook_feed_tree";
/** hook_iframe_whitelisted($url) */
/** @see Plugin::hook_iframe_whitelisted() */
const HOOK_IFRAME_WHITELISTED = "hook_iframe_whitelisted";
/** hook_enclosure_imported($enclosure, $feed) */
/** @see Plugin::hook_enclosure_imported() */
const HOOK_ENCLOSURE_IMPORTED = "hook_enclosure_imported";
/** hook_headlines_custom_sort_map() */
/** @see Plugin::hook_headlines_custom_sort_map() */
const HOOK_HEADLINES_CUSTOM_SORT_MAP = "hook_headlines_custom_sort_map";
/** hook_headlines_custom_sort_override($order) */
/** @see Plugin::hook_headlines_custom_sort_override() */
const HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE = "hook_headlines_custom_sort_override";
/** hook_headline_toolbar_select_menu_item($feed_id, $is_cat) */
/** @see Plugin::hook_headline_toolbar_select_menu_item() */
const HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM = "hook_headline_toolbar_select_menu_item";
/** hook_pre_subscribe($url, $auth_login, $auth_pass) (byref) */
/** @see Plugin::hook_pre_subscribe() */
const HOOK_PRE_SUBSCRIBE = "hook_pre_subscribe";
const KIND_ALL = 1;
const KIND_SYSTEM = 2;
const KIND_USER = 3;
static function object_to_domain(Plugin $plugin) {
static function object_to_domain(Plugin $plugin): string {
return strtolower(get_class($plugin));
}
function __construct() {
$this->pdo = Db::pdo();
$this->storage = array();
$this->storage = [];
}
private function __clone() {
@@ -194,18 +219,18 @@ class PluginHost {
return self::$instance;
}
private function register_plugin(string $name, Plugin $plugin) {
private function register_plugin(string $name, Plugin $plugin): void {
//array_push($this->plugins, $plugin);
$this->plugins[$name] = $plugin;
}
/** needed for compatibility with API 1 */
function get_link() {
function get_link(): bool {
return false;
}
/** needed for compatibility with API 2 (?) */
function get_dbh() {
function get_dbh(): bool {
return false;
}
@@ -213,8 +238,11 @@ class PluginHost {
return $this->pdo;
}
function get_plugin_names() {
$names = array();
/**
* @return array<int, string>
*/
function get_plugin_names(): array {
$names = [];
foreach ($this->plugins as $p) {
array_push($names, get_class($p));
@@ -223,15 +251,22 @@ class PluginHost {
return $names;
}
function get_plugins() {
/**
* @return array<Plugin>
*/
function get_plugins(): array {
return $this->plugins;
}
function get_plugin(string $name) {
function get_plugin(string $name): ?Plugin {
return $this->plugins[strtolower($name)] ?? null;
}
function run_hooks(string $hook, ...$args) {
/**
* @param PluginHost::HOOK_* $hook
* @param mixed $args
*/
function run_hooks(string $hook, ...$args): void {
$method = strtolower($hook);
foreach ($this->get_hooks($hook) as $plugin) {
@@ -247,7 +282,12 @@ class PluginHost {
}
}
function run_hooks_until(string $hook, $check, ...$args) {
/**
* @param PluginHost::HOOK_* $hook
* @param mixed $args
* @param mixed $check
*/
function run_hooks_until(string $hook, $check, ...$args): bool {
$method = strtolower($hook);
foreach ($this->get_hooks($hook) as $plugin) {
@@ -267,7 +307,11 @@ class PluginHost {
return false;
}
function run_hooks_callback(string $hook, Closure $callback, ...$args) {
/**
* @param PluginHost::HOOK_* $hook
* @param mixed $args
*/
function run_hooks_callback(string $hook, Closure $callback, ...$args): void {
$method = strtolower($hook);
foreach ($this->get_hooks($hook) as $plugin) {
@@ -284,7 +328,11 @@ class PluginHost {
}
}
function chain_hooks_callback(string $hook, Closure $callback, &...$args) {
/**
* @param PluginHost::HOOK_* $hook
* @param mixed $args
*/
function chain_hooks_callback(string $hook, Closure $callback, &...$args): void {
$method = strtolower($hook);
foreach ($this->get_hooks($hook) as $plugin) {
@@ -301,7 +349,10 @@ class PluginHost {
}
}
function add_hook(string $type, Plugin $sender, int $priority = 50) {
/**
* @param PluginHost::HOOK_* $type
*/
function add_hook(string $type, Plugin $sender, int $priority = 50): void {
$priority = (int) $priority;
if (!method_exists($sender, strtolower($type))) {
@@ -325,7 +376,10 @@ class PluginHost {
ksort($this->hooks[$type]);
}
function del_hook(string $type, Plugin $sender) {
/**
* @param PluginHost::HOOK_* $type
*/
function del_hook(string $type, Plugin $sender): void {
if (is_array($this->hooks[$type])) {
foreach (array_keys($this->hooks[$type]) as $prio) {
$key = array_search($sender, $this->hooks[$type][$prio]);
@@ -337,6 +391,10 @@ class PluginHost {
}
}
/**
* @param PluginHost::HOOK_* $type
* @return array<int, Plugin>
*/
function get_hooks(string $type) {
if (isset($this->hooks[$type])) {
$tmp = [];
@@ -346,11 +404,14 @@ class PluginHost {
}
return $tmp;
} else {
}
return [];
}
}
function load_all(int $kind, int $owner_uid = null, bool $skip_init = false) {
/**
* @param PluginHost::KIND_* $kind
*/
function load_all(int $kind, int $owner_uid = null, bool $skip_init = false): void {
$plugins = array_merge(glob("plugins/*"), glob("plugins.local/*"));
$plugins = array_filter($plugins, "is_dir");
@@ -361,7 +422,10 @@ class PluginHost {
$this->load(join(",", $plugins), $kind, $owner_uid, $skip_init);
}
function load(string $classlist, int $kind, int $owner_uid = null, bool $skip_init = false) {
/**
* @param PluginHost::KIND_* $kind
*/
function load(string $classlist, int $kind, int $owner_uid = null, bool $skip_init = false): void {
$plugins = explode(",", $classlist);
$this->owner_uid = (int) $owner_uid;
@@ -381,8 +445,28 @@ class PluginHost {
}
if (!isset($this->plugins[$class])) {
// WIP hack
// we can't catch incompatible method signatures via Throwable
// this also enables global tt-rss safe mode in case there are more plugins like this
if (($_SESSION["plugin_blacklist"][$class] ?? 0)) {
// only report once per-plugin per-session
if ($_SESSION["plugin_blacklist"][$class] < 2) {
user_error("Plugin $class has caused a PHP fatal error so it won't be loaded again in this session.", E_USER_WARNING);
$_SESSION["plugin_blacklist"][$class] = 2;
}
$_SESSION["safe_mode"] = 1;
continue;
}
try {
$_SESSION["plugin_blacklist"][$class] = 1;
require_once $file;
unset($_SESSION["plugin_blacklist"][$class]);
} catch (Error $err) {
user_error($err, E_USER_WARNING);
continue;
@@ -434,27 +518,27 @@ class PluginHost {
$this->load_data();
}
function is_system(Plugin $plugin) {
function is_system(Plugin $plugin): bool {
$about = $plugin->about();
return $about[3] ?? false;
return ($about[3] ?? false) === true;
}
// only system plugins are allowed to modify routing
function add_handler(string $handler, $method, Plugin $sender) {
function add_handler(string $handler, string $method, Plugin $sender): void {
$handler = str_replace("-", "_", strtolower($handler));
$method = strtolower($method);
if ($this->is_system($sender)) {
if (!isset($this->handlers[$handler])) {
$this->handlers[$handler] = array();
$this->handlers[$handler] = [];
}
$this->handlers[$handler][$method] = $sender;
}
}
function del_handler(string $handler, $method, Plugin $sender) {
function del_handler(string $handler, string $method, Plugin $sender): void {
$handler = str_replace("-", "_", strtolower($handler));
$method = strtolower($method);
@@ -463,7 +547,10 @@ class PluginHost {
}
}
function lookup_handler($handler, $method) {
/**
* @return false|Plugin false if the handler couldn't be found, otherwise the Plugin/handler
*/
function lookup_handler(string $handler, string $method) {
$handler = str_replace("-", "_", strtolower($handler));
$method = strtolower($method);
@@ -478,7 +565,7 @@ class PluginHost {
return false;
}
function add_command(string $command, string $description, Plugin $sender, string $suffix = "", string $arghelp = "") {
function add_command(string $command, string $description, Plugin $sender, string $suffix = "", string $arghelp = ""): void {
$command = str_replace("-", "_", strtolower($command));
$this->commands[$command] = array("description" => $description,
@@ -487,27 +574,34 @@ class PluginHost {
"class" => $sender);
}
function del_command(string $command) {
function del_command(string $command): void {
$command = "-" . strtolower($command);
unset($this->commands[$command]);
}
function lookup_command($command) {
/**
* @return false|Plugin false if the command couldn't be found, otherwise the registered Plugin
*/
function lookup_command(string $command) {
$command = "-" . strtolower($command);
if (is_array($this->commands[$command])) {
if (array_key_exists($command, $this->commands) && is_array($this->commands[$command])) {
return $this->commands[$command]["class"];
} else {
return false;
}
}
/** @return array<string, array{'description': string, 'suffix': string, 'arghelp': string, 'class': Plugin}>> command type -> details array */
function get_commands() {
return $this->commands;
}
function run_commands(array $args) {
/**
* @param array<string, mixed> $args
*/
function run_commands(array $args): void {
foreach ($this->get_commands() as $command => $data) {
if (isset($args[$command])) {
$command = str_replace("-", "", $command);
@@ -516,7 +610,7 @@ class PluginHost {
}
}
private function load_data() {
private function load_data(): void {
if ($this->owner_uid && !$this->data_loaded && get_schema_version() > 100) {
$sth = $this->pdo->prepare("SELECT name, content FROM ttrss_plugin_storage
WHERE owner_uid = ?");
@@ -530,7 +624,7 @@ class PluginHost {
}
}
private function save_data(string $plugin) {
private function save_data(string $plugin): void {
if ($this->owner_uid) {
if (!$this->pdo_data)
@@ -543,7 +637,7 @@ class PluginHost {
$sth->execute([$this->owner_uid, $plugin]);
if (!isset($this->storage[$plugin]))
$this->storage[$plugin] = array();
$this->storage[$plugin] = [];
$content = serialize($this->storage[$plugin]);
@@ -563,8 +657,12 @@ class PluginHost {
}
}
// same as set(), but sets data to current preference profile
function profile_set(Plugin $sender, string $name, $value) {
/**
* same as set(), but sets data to current preference profile
*
* @param mixed $value
*/
function profile_set(Plugin $sender, string $name, $value): void {
$profile_id = $_SESSION["profile"] ?? null;
if ($profile_id) {
@@ -582,26 +680,32 @@ class PluginHost {
$this->save_data(get_class($sender));
} else {
return $this->set($sender, $name, $value);
$this->set($sender, $name, $value);
}
}
function set(Plugin $sender, string $name, $value) {
/**
* @param mixed $value
*/
function set(Plugin $sender, string $name, $value): void {
$idx = get_class($sender);
if (!isset($this->storage[$idx]))
$this->storage[$idx] = array();
$this->storage[$idx] = [];
$this->storage[$idx][$name] = $value;
$this->save_data(get_class($sender));
}
function set_array(Plugin $sender, array $params) {
/**
* @param array<int|string, mixed> $params
*/
function set_array(Plugin $sender, array $params): void {
$idx = get_class($sender);
if (!isset($this->storage[$idx]))
$this->storage[$idx] = array();
$this->storage[$idx] = [];
foreach ($params as $name => $value)
$this->storage[$idx][$name] = $value;
@@ -609,7 +713,12 @@ class PluginHost {
$this->save_data(get_class($sender));
}
// same as get(), but sets data to current preference profile
/**
* same as get(), but sets data to current preference profile
*
* @param mixed $default_value
* @return mixed
*/
function profile_get(Plugin $sender, string $name, $default_value = false) {
$profile_id = $_SESSION["profile"] ?? null;
@@ -629,6 +738,10 @@ class PluginHost {
}
}
/**
* @param mixed $default_value
* @return mixed
*/
function get(Plugin $sender, string $name, $default_value = false) {
$idx = get_class($sender);
@@ -641,6 +754,10 @@ class PluginHost {
}
}
/**
* @param array<int|string, mixed> $default_value
* @return array<int|string, mixed>
*/
function get_array(Plugin $sender, string $name, array $default_value = []) {
$tmp = $this->get($sender, $name);
@@ -649,13 +766,16 @@ class PluginHost {
return $tmp;
}
function get_all($sender) {
/**
* @return array<string, mixed>
*/
function get_all(Plugin $sender) {
$idx = get_class($sender);
return $this->storage[$idx] ?? [];
}
function clear_data(Plugin $sender) {
function clear_data(Plugin $sender): void {
if ($this->owner_uid) {
$idx = get_class($sender);
@@ -670,7 +790,7 @@ class PluginHost {
// Plugin feed functions are *EXPERIMENTAL*!
// cat_id: only -1 is supported (Special)
function add_feed(int $cat_id, string $title, string $icon, Plugin $sender) {
function add_feed(int $cat_id, string $title, string $icon, Plugin $sender): int {
if (empty($this->feeds[$cat_id]))
$this->feeds[$cat_id] = [];
@@ -683,12 +803,15 @@ class PluginHost {
return $id;
}
/**
* @return array<int, array{'id': int, 'title': string, 'sender': Plugin, 'icon': string}>
*/
function get_feeds(int $cat_id) {
return $this->feeds[$cat_id] ?? [];
}
// convert feed_id (e.g. -129) to pfeed_id first
function get_feed_handler(int $pfeed_id) {
function get_feed_handler(int $pfeed_id): ?Plugin {
foreach ($this->feeds as $cat) {
foreach ($cat as $feed) {
if ($feed['id'] == $pfeed_id) {
@@ -696,46 +819,54 @@ class PluginHost {
}
}
}
return null;
}
static function pfeed_to_feed_id(int $pfeed) {
static function pfeed_to_feed_id(int $pfeed): int {
return PLUGIN_FEED_BASE_INDEX - 1 - abs($pfeed);
}
static function feed_to_pfeed_id(int $feed) {
static function feed_to_pfeed_id(int $feed): int {
return PLUGIN_FEED_BASE_INDEX - 1 + abs($feed);
}
function add_api_method(string $name, Plugin $sender) {
function add_api_method(string $name, Plugin $sender): void {
if ($this->is_system($sender)) {
$this->api_methods[strtolower($name)] = $sender;
}
}
function get_api_method(string $name) {
return $this->api_methods[$name];
function get_api_method(string $name): ?Plugin {
return $this->api_methods[$name] ?? null;
}
function add_filter_action(Plugin $sender, string $action_name, string $action_desc) {
function add_filter_action(Plugin $sender, string $action_name, string $action_desc): void {
$sender_class = get_class($sender);
if (!isset($this->plugin_actions[$sender_class]))
$this->plugin_actions[$sender_class] = array();
$this->plugin_actions[$sender_class] = [];
array_push($this->plugin_actions[$sender_class],
array("action" => $action_name, "description" => $action_desc, "sender" => $sender));
}
/**
* @return array<string, array<int, array{'action': string, 'description': string, 'sender': Plugin}>>
*/
function get_filter_actions() {
return $this->plugin_actions;
}
function get_owner_uid() {
function get_owner_uid(): ?int {
return $this->owner_uid;
}
// handled by classes/pluginhandler.php, requires valid session
function get_method_url(Plugin $sender, string $method, $params = []) {
/**
* handled by classes/pluginhandler.php, requires valid session
*
* @param array<int|string, mixed> $params
*/
function get_method_url(Plugin $sender, string $method, array $params = []): string {
return Config::get_self_url() . "/backend.php?" .
http_build_query(
array_merge(
@@ -758,8 +889,12 @@ class PluginHost {
$params));
} */
// WARNING: endpoint in public.php, exposed to unauthenticated users
function get_public_method_url(Plugin $sender, string $method, $params = []) {
/**
* WARNING: endpoint in public.php, exposed to unauthenticated users
*
* @param array<int|string, mixed> $params
*/
function get_public_method_url(Plugin $sender, string $method, array $params = []): ?string {
if ($sender->is_public_method($method)) {
return Config::get_self_url() . "/public.php?" .
http_build_query(
@@ -768,18 +903,18 @@ class PluginHost {
"op" => strtolower(get_class($sender) . self::PUBLIC_METHOD_DELIMITER . $method),
],
$params));
} else {
user_error("get_public_method_url: requested method '$method' of '" . get_class($sender) . "' is private.");
}
user_error("get_public_method_url: requested method '$method' of '" . get_class($sender) . "' is private.");
return null;
}
function get_plugin_dir(Plugin $plugin) {
function get_plugin_dir(Plugin $plugin): string {
$ref = new ReflectionClass(get_class($plugin));
return dirname($ref->getFileName());
}
// TODO: use get_plugin_dir()
function is_local(Plugin $plugin) {
function is_local(Plugin $plugin): bool {
$ref = new ReflectionClass(get_class($plugin));
return basename(dirname(dirname($ref->getFileName()))) == "plugins.local";
}

View File

@@ -5,13 +5,16 @@ class Pref_Feeds extends Handler_Protected {
const E_ICON_UPLOAD_FAILED = 'E_ICON_UPLOAD_FAILED';
const E_ICON_UPLOAD_SUCCESS = 'E_ICON_UPLOAD_SUCCESS';
function csrf_ignore($method) {
function csrf_ignore(string $method): bool {
$csrf_ignored = array("index", "getfeedtree", "savefeedorder");
return array_search($method, $csrf_ignored) !== false;
}
public static function get_ts_languages() {
/**
* @return array<int, string>
*/
public static function get_ts_languages(): array {
if (Config::get(Config::DB_TYPE) == 'pgsql') {
return array_map('ucfirst',
array_column(ORM::for_table('pg_ts_config')->select('cfgname')->find_array(), 'cfgname'));
@@ -20,7 +23,7 @@ class Pref_Feeds extends Handler_Protected {
return [];
}
function renameCat() {
function renameCat(): void {
$cat = ORM::for_table("ttrss_feed_categories")
->where("owner_uid", $_SESSION["uid"])
->find_one($_REQUEST['id']);
@@ -33,7 +36,10 @@ class Pref_Feeds extends Handler_Protected {
}
}
private function get_category_items($cat_id) {
/**
* @return array<int, array<string, bool|int|string>>
*/
private function get_category_items(int $cat_id): array {
if (clean($_REQUEST['mode'] ?? 0) != 2)
$search = $_SESSION["prefs_feed_search"] ?? "";
@@ -103,11 +109,14 @@ class Pref_Feeds extends Handler_Protected {
return $items;
}
function getfeedtree() {
function getfeedtree(): void {
print json_encode($this->_makefeedtree());
}
function _makefeedtree() {
/**
* @return array<string, array<int|string, mixed>|string>
*/
function _makefeedtree(): array {
if (clean($_REQUEST['mode'] ?? 0) != 2)
$search = $_SESSION["prefs_feed_search"] ?? "";
@@ -184,7 +193,7 @@ class Pref_Feeds extends Handler_Protected {
if (count($labels)) {
foreach ($labels as $label) {
$label_id = Labels::label_to_feed_id($label->id);
$feed = $this->feedlist_init_feed($label_id, false, 0);
$feed = $this->feedlist_init_feed($label_id, null, false);
$feed['fg_color'] = $label->fg_color;
$feed['bg_color'] = $label->bg_color;
array_push($cat['items'], $feed);
@@ -319,19 +328,22 @@ class Pref_Feeds extends Handler_Protected {
];
}
function catsortreset() {
function catsortreset(): void {
$sth = $this->pdo->prepare("UPDATE ttrss_feed_categories
SET order_id = 0 WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
}
function feedsortreset() {
function feedsortreset(): void {
$sth = $this->pdo->prepare("UPDATE ttrss_feeds
SET order_id = 0 WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
}
private function process_category_order(&$data_map, $item_id, $parent_id = false, $nest_level = 0) {
/**
* @param array<string, mixed> $data_map
*/
private function process_category_order(array &$data_map, string $item_id = '', string $parent_id = '', int $nest_level = 0): void {
$prefix = "";
for ($i = 0; $i < $nest_level; $i++)
@@ -403,7 +415,7 @@ class Pref_Feeds extends Handler_Protected {
}
}
function savefeedorder() {
function savefeedorder(): void {
$data = json_decode($_POST['payload'], true);
#file_put_contents("/tmp/saveorder.json", clean($_POST['payload']));
@@ -417,8 +429,9 @@ class Pref_Feeds extends Handler_Protected {
if (is_array($data) && is_array($data['items'])) {
# $cat_order_id = 0;
/** @var array<int, mixed> */
$data_map = array();
$root_item = false;
$root_item = '';
foreach ($data['items'] as $item) {
@@ -439,7 +452,7 @@ class Pref_Feeds extends Handler_Protected {
}
}
function removeIcon() {
function removeIcon(): void {
$feed_id = (int) $_REQUEST["feed_id"];
$icon_file = Config::get(Config::ICONS_DIR) . "/$feed_id.ico";
@@ -459,7 +472,7 @@ class Pref_Feeds extends Handler_Protected {
}
}
function uploadIcon() {
function uploadIcon(): void {
$feed_id = (int) $_REQUEST['feed_id'];
$tmp_file = tempnam(Config::get(Config::CACHE_DIR) . '/upload', 'icon');
@@ -502,7 +515,7 @@ class Pref_Feeds extends Handler_Protected {
print json_encode(['rc' => $rc, 'icon_url' => Feeds::_get_icon($feed_id)]);
}
function editfeed() {
function editfeed(): void {
global $purge_intervals;
global $update_intervals;
@@ -564,12 +577,12 @@ class Pref_Feeds extends Handler_Protected {
}
}
private function _batch_toggle_checkbox($name) {
private function _batch_toggle_checkbox(string $name): string {
return \Controls\checkbox_tag("", false, "",
["data-control-for" => $name, "title" => __("Check to enable field"), "onchange" => "App.dialogOf(this).toggleField(this)"]);
}
function editfeeds() {
function editfeeds(): void {
global $purge_intervals;
global $update_intervals;
@@ -677,15 +690,15 @@ class Pref_Feeds extends Handler_Protected {
<?php
}
function batchEditSave() {
return $this->editsaveops(true);
function batchEditSave(): void {
$this->editsaveops(true);
}
function editSave() {
return $this->editsaveops(false);
function editSave(): void {
$this->editsaveops(false);
}
private function editsaveops($batch) {
private function editsaveops(bool $batch): void {
$feed_title = clean($_POST["title"]);
$feed_url = clean($_POST["feed_url"]);
@@ -774,11 +787,11 @@ class Pref_Feeds extends Handler_Protected {
break;
case "update_interval":
$qpart = "update_interval = " . $this->pdo->quote($upd_intl);
$qpart = "update_interval = " . $upd_intl; // made int above
break;
case "purge_interval":
$qpart = "purge_interval =" . $this->pdo->quote($purge_intl);
$qpart = "purge_interval = " . $purge_intl; // made int above
break;
case "auth_login":
@@ -790,33 +803,33 @@ class Pref_Feeds extends Handler_Protected {
break;
case "private":
$qpart = "private = " . $this->pdo->quote($private);
$qpart = "private = " . $private; // made int above
break;
case "include_in_digest":
$qpart = "include_in_digest = " . $this->pdo->quote($include_in_digest);
$qpart = "include_in_digest = " . $include_in_digest; // made int above
break;
case "always_display_enclosures":
$qpart = "always_display_enclosures = " . $this->pdo->quote($always_display_enclosures);
$qpart = "always_display_enclosures = " . $always_display_enclosures; // made int above
break;
case "mark_unread_on_update":
$qpart = "mark_unread_on_update = " . $this->pdo->quote($mark_unread_on_update);
$qpart = "mark_unread_on_update = " . $mark_unread_on_update; // made int above
break;
case "cache_images":
$qpart = "cache_images = " . $this->pdo->quote($cache_images);
$qpart = "cache_images = " . $cache_images; // made int above
break;
case "hide_images":
$qpart = "hide_images = " . $this->pdo->quote($hide_images);
$qpart = "hide_images = " . $hide_images; // made int above
break;
case "cat_id":
if (get_pref(Prefs::ENABLE_FEED_CATS)) {
if ($cat_id) {
$qpart = "cat_id = " . $this->pdo->quote($cat_id);
$qpart = "cat_id = " . $cat_id; // made int above
} else {
$qpart = 'cat_id = NULL';
}
@@ -841,39 +854,36 @@ class Pref_Feeds extends Handler_Protected {
$this->pdo->commit();
}
return;
}
function remove() {
$ids = explode(",", clean($_REQUEST["ids"]));
function remove(): void {
/** @var array<int, int> */
$ids = array_map('intval', explode(",", clean($_REQUEST["ids"])));
foreach ($ids as $id) {
self::remove_feed($id, $_SESSION["uid"]);
}
return;
}
function removeCat() {
function removeCat(): void {
$ids = explode(",", clean($_REQUEST["ids"]));
foreach ($ids as $id) {
Feeds::_remove_cat((int)$id, $_SESSION["uid"]);
}
}
function addCat() {
function addCat(): void {
$feed_cat = clean($_REQUEST["cat"]);
Feeds::_add_cat($feed_cat, $_SESSION['uid']);
}
function importOpml() {
function importOpml(): void {
$opml = new OPML($_REQUEST);
$opml->opml_import($_SESSION["uid"]);
}
private function index_feeds() {
private function index_feeds(): void {
$error_button = "<button dojoType='dijit.form.Button'
id='pref_feeds_errors_btn' style='display : none'
onclick='CommonDialogs.showFeedsWithErrors()'>".
@@ -984,7 +994,7 @@ class Pref_Feeds extends Handler_Protected {
}
private function index_opml() {
private function index_opml(): void {
?>
<form id='opml_import_form' method='post' enctype='multipart/form-data'>
@@ -1020,7 +1030,7 @@ class Pref_Feeds extends Handler_Protected {
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefFeedsOPML");
}
private function index_shared() {
private function index_shared(): void {
?>
<?= format_notice('Published articles can be subscribed by anyone who knows the following URL:') ?></h3>
@@ -1040,7 +1050,7 @@ class Pref_Feeds extends Handler_Protected {
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefFeedsPublishedGenerated");
}
function index() {
function index(): void {
?>
<div dojoType='dijit.layout.TabContainer' tabPosition='left-h'>
@@ -1079,44 +1089,44 @@ class Pref_Feeds extends Handler_Protected {
<?php
}
private function feedlist_init_cat($cat_id) {
$obj = array();
$cat_id = (int) $cat_id;
$obj['id'] = 'CAT:' . $cat_id;
$obj['items'] = array();
$obj['name'] = Feeds::_get_cat_title($cat_id);
$obj['type'] = 'category';
$obj['unread'] = -1; //(int) Feeds::_get_cat_unread($cat_id);
$obj['bare_id'] = $cat_id;
return $obj;
/**
* @return array<string, mixed>
*/
private function feedlist_init_cat(int $cat_id): array {
return [
'id' => 'CAT:' . $cat_id,
'items' => array(),
'name' => Feeds::_get_cat_title($cat_id),
'type' => 'category',
'unread' => -1, //(int) Feeds::_get_cat_unread($cat_id);
'bare_id' => $cat_id,
];
}
private function feedlist_init_feed($feed_id, $title = false, $unread = false, $error = '', $updated = '') {
$obj = array();
$feed_id = (int) $feed_id;
/**
* @return array<string, mixed>
*/
private function feedlist_init_feed(int $feed_id, ?string $title = null, bool $unread = false, string $error = '', string $updated = ''): array {
if (!$title)
$title = Feeds::_get_title($feed_id, false);
if ($unread === false)
$unread = getFeedUnread($feed_id, false);
$obj['id'] = 'FEED:' . $feed_id;
$obj['name'] = $title;
$obj['unread'] = (int) $unread;
$obj['type'] = 'feed';
$obj['error'] = $error;
$obj['updated'] = $updated;
$obj['icon'] = Feeds::_get_icon($feed_id);
$obj['bare_id'] = $feed_id;
$obj['auxcounter'] = 0;
return $obj;
return [
'id' => 'FEED:' . $feed_id,
'name' => $title,
'unread' => (int) $unread,
'type' => 'feed',
'error' => $error,
'updated' => $updated,
'icon' => Feeds::_get_icon($feed_id),
'bare_id' => $feed_id,
'auxcounter' => 0,
];
}
function inactiveFeeds() {
function inactiveFeeds(): void {
if (Config::get(Config::DB_TYPE) == "pgsql") {
$interval_qpart = "NOW() - INTERVAL '3 months'";
@@ -1150,7 +1160,7 @@ class Pref_Feeds extends Handler_Protected {
print json_encode($inactive_feeds);
}
function feedsWithErrors() {
function feedsWithErrors(): void {
print json_encode(ORM::for_table('ttrss_feeds')
->select_many('id', 'title', 'feed_url', 'last_error', 'site_url')
->where_not_equal('last_error', '')
@@ -1158,7 +1168,7 @@ class Pref_Feeds extends Handler_Protected {
->find_array());
}
static function remove_feed($id, $owner_uid) {
static function remove_feed(int $id, int $owner_uid): void {
if (PluginHost::getInstance()->run_hooks_until(PluginHost::HOOK_UNSUBSCRIBE_FEED, true, $id, $owner_uid))
return;
@@ -1199,14 +1209,14 @@ class Pref_Feeds extends Handler_Protected {
}
}
function batchSubscribe() {
function batchSubscribe(): void {
print json_encode([
"enable_cats" => (int)get_pref(Prefs::ENABLE_FEED_CATS),
"cat_select" => \Controls\select_feeds_cats("cat")
]);
}
function batchAddFeeds() {
function batchAddFeeds(): void {
$cat_id = clean($_REQUEST['cat']);
$feeds = explode("\n", clean($_REQUEST['feeds']));
$login = clean($_REQUEST['login']);
@@ -1216,7 +1226,7 @@ class Pref_Feeds extends Handler_Protected {
// TODO: we should return some kind of error code to frontend here
if ($user->access_level == UserHelper::ACCESS_LEVEL_READONLY) {
return false;
return;
}
$csth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
@@ -1244,11 +1254,11 @@ class Pref_Feeds extends Handler_Protected {
}
}
function clearKeys() {
return Feeds::_clear_access_keys($_SESSION['uid']);
function clearKeys(): void {
Feeds::_clear_access_keys($_SESSION['uid']);
}
function regenFeedKey() {
function regenFeedKey(): void {
$feed_id = clean($_REQUEST['id']);
$is_cat = clean($_REQUEST['is_cat']);
@@ -1257,7 +1267,7 @@ class Pref_Feeds extends Handler_Protected {
print json_encode(["link" => $new_key]);
}
function getSharedURL() {
function getSharedURL(): void {
$feed_id = clean($_REQUEST['id']);
$is_cat = clean($_REQUEST['is_cat']) == "true";
$search = clean($_REQUEST['search']);
@@ -1276,7 +1286,10 @@ class Pref_Feeds extends Handler_Protected {
]);
}
private function calculate_children_count($cat) {
/**
* @param array<string, mixed> $cat
*/
private function calculate_children_count(array $cat): int {
$c = 0;
foreach ($cat['items'] ?? [] as $child) {

View File

@@ -1,20 +1,19 @@
<?php
class Pref_Filters extends Handler_Protected {
function csrf_ignore($method) {
function csrf_ignore(string $method): bool {
$csrf_ignored = array("index", "getfiltertree", "savefilterorder");
return array_search($method, $csrf_ignored) !== false;
}
function filtersortreset() {
function filtersortreset(): void {
$sth = $this->pdo->prepare("UPDATE ttrss_filters2
SET order_id = 0 WHERE owner_uid = ?");
$sth->execute([$_SESSION['uid']]);
return;
}
function savefilterorder() {
function savefilterorder(): void {
$data = json_decode($_POST['payload'], true);
#file_put_contents("/tmp/saveorder.json", clean($_POST['payload']));
@@ -40,11 +39,9 @@ class Pref_Filters extends Handler_Protected {
}
}
}
return;
}
function testFilterDo() {
function testFilterDo(): void {
$offset = (int) clean($_REQUEST["offset"]);
$limit = (int) clean($_REQUEST["limit"]);
@@ -59,7 +56,9 @@ class Pref_Filters extends Handler_Protected {
$res = $this->pdo->query("SELECT id,name FROM ttrss_filter_types");
$filter_types = array();
/** @var array<int, string> */
$filter_types = [];
while ($line = $res->fetch()) {
$filter_types[$line["id"]] = $line["name"];
}
@@ -67,7 +66,10 @@ class Pref_Filters extends Handler_Protected {
$scope_qparts = array();
$rctr = 0;
/** @var string $r */
foreach (clean($_REQUEST["rule"]) AS $r) {
/** @var array{'reg_exp': string, 'filter_type': int, 'feed_id': array<int, int|string>, 'name': string}|null */
$rule = json_decode($r, true);
if ($rule && $rctr < 5) {
@@ -75,13 +77,15 @@ class Pref_Filters extends Handler_Protected {
unset($rule["filter_type"]);
$scope_inner_qparts = [];
/** @var int|string $feed_id may be a category string (e.g. 'CAT:7') or feed ID int */
foreach ($rule["feed_id"] as $feed_id) {
if (strpos($feed_id, "CAT:") === 0) {
$cat_id = (int) substr($feed_id, 4);
array_push($scope_inner_qparts, "cat_id = " . $this->pdo->quote($cat_id));
} else if ($feed_id > 0) {
array_push($scope_inner_qparts, "feed_id = " . $this->pdo->quote($feed_id));
if (strpos("$feed_id", "CAT:") === 0) {
$cat_id = (int) substr("$feed_id", 4);
array_push($scope_inner_qparts, "cat_id = " . $cat_id);
} else if (is_numeric($feed_id) && $feed_id > 0) {
array_push($scope_inner_qparts, "feed_id = " . (int)$feed_id);
}
}
@@ -162,7 +166,7 @@ class Pref_Filters extends Handler_Protected {
print json_encode($rv);
}
private function _get_rules_list($filter_id) {
private function _get_rules_list(int $filter_id): string {
$sth = $this->pdo->prepare("SELECT reg_exp,
inverse,
match_on,
@@ -222,7 +226,7 @@ class Pref_Filters extends Handler_Protected {
return $rv;
}
function getfiltertree() {
function getfiltertree(): void {
$root = array();
$root['id'] = 'root';
$root['name'] = __('Filters');
@@ -307,10 +311,9 @@ class Pref_Filters extends Handler_Protected {
$fl['items'] = array($root);
print json_encode($fl);
return;
}
function edit() {
function edit(): void {
$filter_id = (int) clean($_REQUEST["id"] ?? 0);
@@ -406,7 +409,10 @@ class Pref_Filters extends Handler_Protected {
}
}
private function _get_rule_name($rule) {
/**
* @param array<string, mixed>|null $rule
*/
private function _get_rule_name(?array $rule = null): string {
if (!$rule) $rule = json_decode(clean($_REQUEST["rule"]), true);
$feeds = $rule["feed_id"];
@@ -446,11 +452,18 @@ class Pref_Filters extends Handler_Protected {
"<span class='field'>$filter_type</span>", "<span class='feed'>$feed</span>", isset($rule["inverse"]) ? __("(inverse)") : "") . "</span>";
}
function printRuleName() {
function printRuleName(): void {
print $this->_get_rule_name(json_decode(clean($_REQUEST["rule"]), true));
}
private function _get_action_name($action) {
/**
* @param array<string, mixed>|null $action
*/
private function _get_action_name(?array $action = null): string {
if (!$action) {
return "";
}
$sth = $this->pdo->prepare("SELECT description FROM
ttrss_filter_actions WHERE id = ?");
$sth->execute([(int)$action["action_id"]]);
@@ -484,12 +497,12 @@ class Pref_Filters extends Handler_Protected {
return $title;
}
function printActionName() {
print $this->_get_action_name(json_decode(clean($_REQUEST["action"]), true));
function printActionName(): void {
print $this->_get_action_name(json_decode(clean($_REQUEST["action"] ?? ""), true));
}
function editSave() {
$filter_id = clean($_REQUEST["id"]);
function editSave(): void {
$filter_id = (int) clean($_REQUEST["id"]);
$enabled = checkbox_to_sql_bool(clean($_REQUEST["enabled"] ?? false));
$match_any_rule = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"] ?? false));
$inverse = checkbox_to_sql_bool(clean($_REQUEST["inverse"] ?? false));
@@ -510,7 +523,7 @@ class Pref_Filters extends Handler_Protected {
$this->pdo->commit();
}
function remove() {
function remove(): void {
$ids = explode(",", clean($_REQUEST["ids"]));
$ids_qmarks = arr_qmarks($ids);
@@ -520,7 +533,7 @@ class Pref_Filters extends Handler_Protected {
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
}
private function _save_rules_and_actions($filter_id) {
private function _save_rules_and_actions(int $filter_id): void {
$sth = $this->pdo->prepare("DELETE FROM ttrss_filters2_rules WHERE filter_id = ?");
$sth->execute([$filter_id]);
@@ -597,7 +610,7 @@ class Pref_Filters extends Handler_Protected {
}
}
function add () {
function add(): void {
$enabled = checkbox_to_sql_bool(clean($_REQUEST["enabled"] ?? false));
$match_any_rule = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"] ?? false));
$title = clean($_REQUEST["title"]);
@@ -625,7 +638,7 @@ class Pref_Filters extends Handler_Protected {
$this->pdo->commit();
}
function index() {
function index(): void {
if (array_key_exists("search", $_REQUEST)) {
$filter_search = clean($_REQUEST["search"]);
$_SESSION["prefs_filter_search"] = $filter_search;
@@ -691,7 +704,8 @@ class Pref_Filters extends Handler_Protected {
<?php
}
function editrule() {
function editrule(): void {
/** @var array<int, int|string> */
$feed_ids = explode(",", clean($_REQUEST["ids"]));
print json_encode([
@@ -699,7 +713,10 @@ class Pref_Filters extends Handler_Protected {
]);
}
private function _get_name($id) {
/**
* @return array<int, string>
*/
private function _get_name(int $id): array {
$sth = $this->pdo->prepare(
"SELECT title,match_any_rule,f.inverse AS inverse,COUNT(DISTINCT r.id) AS num_rules,COUNT(DISTINCT a.id) AS num_actions
@@ -745,8 +762,9 @@ class Pref_Filters extends Handler_Protected {
return [];
}
function join() {
$ids = explode(",", clean($_REQUEST["ids"]));
function join(): void {
/** @var array<int, int> */
$ids = array_map("intval", explode(",", clean($_REQUEST["ids"])));
if (count($ids) > 1) {
$base_id = array_shift($ids);
@@ -775,7 +793,7 @@ class Pref_Filters extends Handler_Protected {
}
}
private function _optimize($id) {
private function _optimize(int $id): void {
$this->pdo->beginTransaction();
@@ -830,9 +848,11 @@ class Pref_Filters extends Handler_Protected {
$this->pdo->commit();
}
private function _feed_multi_select($id, $default_ids = [],
$attributes = "", $include_all_feeds = true,
$root_id = null, $nest_level = 0) {
/**
* @param array<int, int|string> $default_ids
*/
private function _feed_multi_select(string $id, array $default_ids = [], string $attributes = "",
bool $include_all_feeds = true, ?int $root_id = null, int $nest_level = 0): string {
$pdo = Db::pdo();

View File

@@ -1,13 +1,13 @@
<?php
class Pref_Labels extends Handler_Protected {
function csrf_ignore($method) {
function csrf_ignore(string $method): bool {
$csrf_ignored = array("index", "getlabeltree");
return array_search($method, $csrf_ignored) !== false;
}
function edit() {
function edit(): void {
$label = ORM::for_table('ttrss_labels2')
->where('owner_uid', $_SESSION['uid'])
->find_one($_REQUEST['id']);
@@ -17,7 +17,7 @@ class Pref_Labels extends Handler_Protected {
}
}
function getlabeltree() {
function getlabeltree(): void {
$root = array();
$root['id'] = 'root';
$root['name'] = __('Labels');
@@ -48,10 +48,9 @@ class Pref_Labels extends Handler_Protected {
$fl['items'] = array($root);
print json_encode($fl);
return;
}
function colorset() {
function colorset(): void {
$kind = clean($_REQUEST["kind"]);
$ids = explode(',', clean($_REQUEST["ids"]));
$color = clean($_REQUEST["color"]);
@@ -84,7 +83,7 @@ class Pref_Labels extends Handler_Protected {
}
}
function colorreset() {
function colorreset(): void {
$ids = explode(',', clean($_REQUEST["ids"]));
foreach ($ids as $id) {
@@ -101,7 +100,7 @@ class Pref_Labels extends Handler_Protected {
}
}
function save() {
function save(): void {
$id = clean($_REQUEST["id"]);
$caption = clean($_REQUEST["caption"]);
@@ -148,9 +147,9 @@ class Pref_Labels extends Handler_Protected {
}
function remove() {
$ids = explode(",", clean($_REQUEST["ids"]));
function remove(): void {
/** @var array<int, int> */
$ids = array_map("intval", explode(",", clean($_REQUEST["ids"])));
foreach ($ids as $id) {
Labels::remove($id, $_SESSION["uid"]);
@@ -158,7 +157,7 @@ class Pref_Labels extends Handler_Protected {
}
function add() {
function add(): void {
$caption = clean($_REQUEST["caption"]);
$output = clean($_REQUEST["output"] ?? false);
@@ -171,7 +170,7 @@ class Pref_Labels extends Handler_Protected {
}
}
function index() {
function index(): void {
?>
<div dojoType='dijit.layout.BorderContainer' gutters='false'>
<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='top'>

View File

@@ -2,12 +2,21 @@
use chillerlan\QRCode;
class Pref_Prefs extends Handler_Protected {
// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
/** @var array<Prefs::*, array<int, string>> */
private $pref_help = [];
/** @var array<string, array<int, string>> pref items are Prefs::*|Pref_Prefs::BLOCK_SEPARATOR (PHPStan was complaining) */
private $pref_item_map = [];
/** @var array<string, string> */
private $pref_help_bottom = [];
/** @var array<int, string> */
private $pref_blacklist = [];
private const BLOCK_SEPARATOR = 'BLOCK_SEPARATOR';
const PI_RES_ALREADY_INSTALLED = "PI_RES_ALREADY_INSTALLED";
const PI_RES_SUCCESS = "PI_RES_SUCCESS";
const PI_ERR_NO_CLASS = "PI_ERR_NO_CLASS";
@@ -17,7 +26,8 @@ class Pref_Prefs extends Handler_Protected {
const PI_ERR_PLUGIN_NOT_FOUND = "PI_ERR_PLUGIN_NOT_FOUND";
const PI_ERR_NO_WORKDIR = "PI_ERR_NO_WORKDIR";
function csrf_ignore($method) {
/** @param string $method */
function csrf_ignore($method) : bool {
$csrf_ignored = array("index", "updateself", "otpqrcode");
return array_search($method, $csrf_ignored) !== false;
@@ -30,35 +40,35 @@ class Pref_Prefs extends Handler_Protected {
__('General') => [
Prefs::USER_LANGUAGE,
Prefs::USER_TIMEZONE,
'BLOCK_SEPARATOR',
self::BLOCK_SEPARATOR,
Prefs::USER_CSS_THEME,
'BLOCK_SEPARATOR',
self::BLOCK_SEPARATOR,
Prefs::ENABLE_API_ACCESS,
],
__('Feeds') => [
Prefs::DEFAULT_UPDATE_INTERVAL,
Prefs::FRESH_ARTICLE_MAX_AGE,
Prefs::DEFAULT_SEARCH_LANGUAGE,
'BLOCK_SEPARATOR',
self::BLOCK_SEPARATOR,
Prefs::ENABLE_FEED_CATS,
'BLOCK_SEPARATOR',
self::BLOCK_SEPARATOR,
Prefs::CONFIRM_FEED_CATCHUP,
Prefs::ON_CATCHUP_SHOW_NEXT_FEED,
'BLOCK_SEPARATOR',
self::BLOCK_SEPARATOR,
Prefs::HIDE_READ_FEEDS,
Prefs::HIDE_READ_SHOWS_SPECIAL,
],
__('Articles') => [
Prefs::PURGE_OLD_DAYS,
Prefs::PURGE_UNREAD_ARTICLES,
'BLOCK_SEPARATOR',
self::BLOCK_SEPARATOR,
Prefs::COMBINED_DISPLAY_MODE,
Prefs::CDM_EXPANDED,
Prefs::CDM_ENABLE_GRID,
'BLOCK_SEPARATOR',
self::BLOCK_SEPARATOR,
Prefs::CDM_AUTO_CATCHUP,
Prefs::VFEED_GROUP_BY_FEED,
'BLOCK_SEPARATOR',
self::BLOCK_SEPARATOR,
Prefs::SHOW_CONTENT_PREVIEW,
Prefs::STRIP_IMAGES,
],
@@ -69,12 +79,12 @@ class Pref_Prefs extends Handler_Protected {
],
__('Advanced') => [
Prefs::BLACKLISTED_TAGS,
'BLOCK_SEPARATOR',
self::BLOCK_SEPARATOR,
Prefs::LONG_DATE_FORMAT,
Prefs::SHORT_DATE_FORMAT,
'BLOCK_SEPARATOR',
self::BLOCK_SEPARATOR,
Prefs::SSL_CERT_SERIAL,
'BLOCK_SEPARATOR',
self::BLOCK_SEPARATOR,
Prefs::DISABLE_CONDITIONAL_COUNTERS,
Prefs::HEADLINES_NO_DISTINCT,
],
@@ -127,7 +137,7 @@ class Pref_Prefs extends Handler_Protected {
];
}
function changepassword() {
function changepassword(): void {
if (Config::get(Config::FORBID_PASSWORD_CHANGES)) {
print "ERROR: ".format_error("Access forbidden.");
@@ -173,7 +183,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
function saveconfig() {
function saveconfig(): void {
$boolean_prefs = explode(",", clean($_POST["boolean_prefs"]));
foreach ($boolean_prefs as $pref) {
@@ -223,7 +233,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
function changePersonalData() {
function changePersonalData(): void {
$user = ORM::for_table('ttrss_users')->find_one($_SESSION['uid']);
$new_email = clean($_POST['email']);
@@ -264,13 +274,13 @@ class Pref_Prefs extends Handler_Protected {
print __("Your personal data has been saved.");
}
function resetconfig() {
function resetconfig(): void {
Prefs::reset($_SESSION["uid"], $_SESSION["profile"]);
print "PREFS_NEED_RELOAD";
}
private function index_auth_personal() {
private function index_auth_personal(): void {
$user = ORM::for_table('ttrss_users')->find_one($_SESSION['uid']);
@@ -310,7 +320,7 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
private function index_auth_password() {
private function index_auth_password(): void {
if ($_SESSION["auth_module"]) {
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
} else {
@@ -385,7 +395,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
private function index_auth_app_passwords() {
private function index_auth_app_passwords(): void {
print_notice("Separate passwords used for API clients. Required if you enable OTP.");
?>
@@ -409,7 +419,7 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
private function index_auth_2fa() {
private function index_auth_2fa(): void {
$otp_enabled = UserHelper::is_otp_enabled($_SESSION["uid"]);
if ($_SESSION["auth_module"] == "auth_internal") {
@@ -515,7 +525,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
function index_auth() {
function index_auth(): void {
?>
<div dojoType='dijit.layout.TabContainer'>
<div dojoType='dijit.layout.ContentPane' title="<?= __('Personal data') ?>">
@@ -534,35 +544,38 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
private function index_prefs_list() {
private function index_prefs_list(): void {
$profile = $_SESSION["profile"] ?? null;
if ($profile) {
print_notice(__("Some preferences are only available in default profile."));
}
/** @var array<string, array{'type_hint': Config::T_*, 'value': bool|int|string, 'help_text': string, 'short_desc': string}> */
$prefs_available = [];
/** @var array<int, string> */
$listed_boolean_prefs = [];
foreach (Prefs::get_all($_SESSION["uid"], $profile) as $line) {
foreach (Prefs::get_all($_SESSION["uid"], $profile) as $pref) {
if (in_array($line["pref_name"], $this->pref_blacklist)) {
if (in_array($pref["pref_name"], $this->pref_blacklist)) {
continue;
}
if ($profile && in_array($line["pref_name"], Prefs::_PROFILE_BLACKLIST)) {
if ($profile && in_array($pref["pref_name"], Prefs::_PROFILE_BLACKLIST)) {
continue;
}
$pref_name = $line["pref_name"];
$pref_name = $pref["pref_name"];
$short_desc = $this->_get_short_desc($pref_name);
if (!$short_desc)
continue;
$prefs_available[$pref_name] = [
'type_hint' => $line['type_hint'],
'value' => $line['value'],
'type_hint' => $pref['type_hint'],
'value' => $pref['value'],
'help_text' => $this->_get_help_text($pref_name),
'short_desc' => $short_desc
];
@@ -574,12 +587,12 @@ class Pref_Prefs extends Handler_Protected {
foreach ($this->pref_item_map[$section] as $pref_name) {
if ($pref_name == 'BLOCK_SEPARATOR' && !$profile) {
if ($pref_name == self::BLOCK_SEPARATOR && !$profile) {
print "<hr/>";
continue;
}
if ($pref_name == "DEFAULT_SEARCH_LANGUAGE" && Config::get(Config::DB_TYPE) != "pgsql") {
if ($pref_name == Prefs::DEFAULT_SEARCH_LANGUAGE && Config::get(Config::DB_TYPE) != "pgsql") {
continue;
}
@@ -596,17 +609,17 @@ class Pref_Prefs extends Handler_Protected {
$value = $item['value'];
$type_hint = $item['type_hint'];
if ($pref_name == "USER_LANGUAGE") {
if ($pref_name == Prefs::USER_LANGUAGE) {
print \Controls\select_hash($pref_name, $value, get_translations(),
["style" => 'width : 220px; margin : 0px']);
} else if ($pref_name == "USER_TIMEZONE") {
} else if ($pref_name == Prefs::USER_TIMEZONE) {
$timezones = explode("\n", file_get_contents("lib/timezones.txt"));
print \Controls\select_tag($pref_name, $value, $timezones, ["dojoType" => "dijit.form.FilteringSelect"]);
} else if ($pref_name == "BLACKLISTED_TAGS") { # TODO: other possible <textarea> prefs go here
} else if ($pref_name == Prefs::BLACKLISTED_TAGS) { # TODO: other possible <textarea> prefs go here
print "<div>";
@@ -618,7 +631,7 @@ class Pref_Prefs extends Handler_Protected {
print "</div>";
} else if ($pref_name == "USER_CSS_THEME") {
} else if ($pref_name == Prefs::USER_CSS_THEME) {
$theme_files = array_map("basename",
array_merge(glob("themes/*.php"),
@@ -642,13 +655,13 @@ class Pref_Prefs extends Handler_Protected {
<?php
} else if ($pref_name == "DEFAULT_UPDATE_INTERVAL") {
} else if ($pref_name == Prefs::DEFAULT_UPDATE_INTERVAL) {
global $update_intervals_nodefault;
print \Controls\select_hash($pref_name, $value, $update_intervals_nodefault);
} else if ($pref_name == "DEFAULT_SEARCH_LANGUAGE") {
} else if ($pref_name == Prefs::DEFAULT_SEARCH_LANGUAGE) {
print \Controls\select_tag($pref_name, $value, Pref_Feeds::get_ts_languages());
@@ -656,7 +669,7 @@ class Pref_Prefs extends Handler_Protected {
array_push($listed_boolean_prefs, $pref_name);
if ($pref_name == "PURGE_UNREAD_ARTICLES" && Config::get(Config::FORCE_ARTICLE_PURGE) != 0) {
if ($pref_name == Prefs::PURGE_UNREAD_ARTICLES && Config::get(Config::FORCE_ARTICLE_PURGE) != 0) {
$is_disabled = true;
$is_checked = true;
} else {
@@ -672,10 +685,10 @@ class Pref_Prefs extends Handler_Protected {
['onclick' => 'Helpers.Digest.preview()', 'style' => 'margin-left : 10px']);
}
} else if (in_array($pref_name, ['FRESH_ARTICLE_MAX_AGE',
'PURGE_OLD_DAYS', 'LONG_DATE_FORMAT', 'SHORT_DATE_FORMAT'])) {
} else if (in_array($pref_name, [Prefs::FRESH_ARTICLE_MAX_AGE,
Prefs::PURGE_OLD_DAYS, Prefs::LONG_DATE_FORMAT, Prefs::SHORT_DATE_FORMAT])) {
if ($pref_name == "PURGE_OLD_DAYS" && Config::get(Config::FORCE_ARTICLE_PURGE) != 0) {
if ($pref_name == Prefs::PURGE_OLD_DAYS && Config::get(Config::FORCE_ARTICLE_PURGE) != 0) {
$attributes = ["disabled" => true, "required" => true];
$value = Config::get(Config::FORCE_ARTICLE_PURGE);
} else {
@@ -687,7 +700,7 @@ class Pref_Prefs extends Handler_Protected {
else
print \Controls\input_tag($pref_name, $value, "text", $attributes);
} else if ($pref_name == "SSL_CERT_SERIAL") {
} else if ($pref_name == Prefs::SSL_CERT_SERIAL) {
print \Controls\input_tag($pref_name, $value, "text", ["readonly" => true], "SSL_CERT_SERIAL");
@@ -727,7 +740,7 @@ class Pref_Prefs extends Handler_Protected {
print \Controls\hidden_tag("boolean_prefs", htmlspecialchars(join(",", $listed_boolean_prefs)));
}
private function index_prefs() {
private function index_prefs(): void {
?>
<form dojoType='dijit.form.Form' id='changeSettingsForm'>
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
@@ -783,7 +796,7 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
function getPluginsList() {
function getPluginsList(): void {
$system_enabled = array_map("trim", explode(",", (string)Config::get(Config::PLUGINS)));
$user_enabled = array_map("trim", explode(",", get_pref(Prefs::_ENABLED_PLUGINS)));
@@ -816,7 +829,7 @@ class Pref_Prefs extends Handler_Protected {
print json_encode(['plugins' => $rv, 'is_admin' => $_SESSION['access_level'] >= UserHelper::ACCESS_LEVEL_ADMIN]);
}
function index_plugins() {
function index_plugins(): void {
?>
<form dojoType="dijit.form.Form" id="changePluginsForm">
@@ -912,7 +925,7 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
function index() {
function index(): void {
?>
<div dojoType='dijit.layout.AccordionContainer' region='center'>
<div dojoType='dijit.layout.AccordionPane' title="<i class='material-icons'>person</i> <?= __('Personal data / Authentication')?>">
@@ -937,7 +950,7 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
function _get_otp_qrcode_img() {
function _get_otp_qrcode_img(): ?string {
$secret = UserHelper::get_otp_secret($_SESSION["uid"]);
$login = UserHelper::get_login_by_id($_SESSION["uid"]);
@@ -949,15 +962,16 @@ class Pref_Prefs extends Handler_Protected {
return $qrcode->render($otpurl);
}
return false;
return null;
}
function otpenable() {
function otpenable(): void {
$password = clean($_REQUEST["password"]);
$otp_check = clean($_REQUEST["otp"]);
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
/** @var Auth_Internal|false $authenticator -- this is only here to make check_password() visible to static analyzer */
if ($authenticator->check_password($_SESSION["uid"], $password)) {
if (UserHelper::enable_otp($_SESSION["uid"], $otp_check)) {
print "OK";
@@ -969,9 +983,10 @@ class Pref_Prefs extends Handler_Protected {
}
}
function otpdisable() {
function otpdisable(): void {
$password = clean($_REQUEST["password"]);
/** @var Auth_Internal|false $authenticator -- this is only here to make check_password() visible to static analyzer */
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
if ($authenticator->check_password($_SESSION["uid"], $password)) {
@@ -1008,18 +1023,19 @@ class Pref_Prefs extends Handler_Protected {
}
function setplugins() {
function setplugins(): void {
$plugins = array_filter($_REQUEST["plugins"], 'clean') ?? [];
set_pref(Prefs::_ENABLED_PLUGINS, implode(",", $plugins));
}
function _get_plugin_version(Plugin $plugin) {
function _get_plugin_version(Plugin $plugin): string {
$about = $plugin->about();
if (!empty($about[0])) {
return T_sprintf("v%.2f, by %s", $about[0], $about[2]);
} else {
}
$ref = new ReflectionClass(get_class($plugin));
$plugin_dir = dirname($ref->getFileName());
@@ -1033,13 +1049,16 @@ class Pref_Prefs extends Handler_Protected {
return $ver["status"] == 0 ? T_sprintf("v%s, by %s", $ver["version"], $about[2]) : $ver["version"];
}
}
return "";
}
static function _get_updated_plugins() {
/**
* @return array<int, array{'plugin': string, 'rv': array{'stdout': false|string, 'stderr': false|string, 'git_status': int, 'need_update': bool}|null}>
*/
static function _get_updated_plugins(): array {
$root_dir = dirname(dirname(__DIR__)); # we're in classes/pref/
$plugin_dirs = array_filter(glob("$root_dir/plugins.local/*"), "is_dir");
$rv = [];
foreach ($plugin_dirs as $dir) {
@@ -1057,7 +1076,10 @@ class Pref_Prefs extends Handler_Protected {
return $rv;
}
private static function _plugin_needs_update($root_dir, $plugin_name) {
/**
* @return array{'stdout': false|string, 'stderr': false|string, 'git_status': int, 'need_update': bool}|null
*/
private static function _plugin_needs_update(string $root_dir, string $plugin_name): ?array {
$plugin_dir = "$root_dir/plugins.local/" . basename($plugin_name);
$rv = null;
@@ -1086,7 +1108,10 @@ class Pref_Prefs extends Handler_Protected {
}
private function _update_plugin($root_dir, $plugin_name) {
/**
* @return array{'stdout': false|string, 'stderr': false|string, 'git_status': int}
*/
private function _update_plugin(string $root_dir, string $plugin_name): array {
$plugin_dir = "$root_dir/plugins.local/" . basename($plugin_name);
$rv = [];
@@ -1112,7 +1137,7 @@ class Pref_Prefs extends Handler_Protected {
}
// https://gist.github.com/mindplay-dk/a4aad91f5a4f1283a5e2#gistcomment-2036828
private function _recursive_rmdir(string $dir, bool $keep_root = false) {
private function _recursive_rmdir(string $dir, bool $keep_root = false): bool {
// Handle bad arguments.
if (empty($dir) || !file_exists($dir)) {
return true; // No such file/dir$dir exists.
@@ -1137,7 +1162,7 @@ class Pref_Prefs extends Handler_Protected {
}
// https://stackoverflow.com/questions/7153000/get-class-name-from-file
private function _get_class_name_from_file($file) {
private function _get_class_name_from_file(string $file): string {
$tokens = token_get_all(file_get_contents($file));
for ($i = 0; $i < count($tokens); $i++) {
@@ -1149,9 +1174,11 @@ class Pref_Prefs extends Handler_Protected {
}
}
}
return "";
}
function uninstallPlugin() {
function uninstallPlugin(): void {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) {
$plugin_name = basename(clean($_REQUEST['plugin']));
$status = 0;
@@ -1166,7 +1193,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
function installPlugin() {
function installPlugin(): void {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
$plugin_name = basename(clean($_REQUEST['plugin']));
$all_plugins = $this->_get_available_plugins();
@@ -1251,47 +1278,59 @@ class Pref_Prefs extends Handler_Protected {
}
}
private function _get_available_plugins() {
/**
* @return array<int, array{'name': string, 'description': string, 'topics': array<int, string>, 'html_url': string, 'clone_url': string, 'last_update': string}>
*/
private function _get_available_plugins(): array {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
return json_decode(UrlHelper::fetch(['url' => 'https://tt-rss.org/plugins.json']), true);
$content = json_decode(UrlHelper::fetch(['url' => 'https://tt-rss.org/plugins.json']), true);
if ($content) {
return $content;
}
}
function getAvailablePlugins() {
return [];
}
function getAvailablePlugins(): void {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) {
print json_encode($this->_get_available_plugins());
} else {
print "[]";
}
}
function checkForPluginUpdates() {
function checkForPluginUpdates(): void {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN && Config::get(Config::CHECK_FOR_UPDATES) && Config::get(Config::CHECK_FOR_PLUGIN_UPDATES)) {
$plugin_name = $_REQUEST["name"] ?? "";
$root_dir = dirname(dirname(__DIR__)); # we're in classes/pref/
if (!empty($plugin_name)) {
$rv = [["plugin" => $plugin_name, "rv" => self::_plugin_needs_update($root_dir, $plugin_name)]];
} else {
$rv = self::_get_updated_plugins();
}
$rv = empty($plugin_name) ? self::_get_updated_plugins() : [
["plugin" => $plugin_name, "rv" => self::_plugin_needs_update($root_dir, $plugin_name)],
];
print json_encode($rv);
}
}
function updateLocalPlugins() {
function updateLocalPlugins(): void {
if ($_SESSION["access_level"] >= UserHelper::ACCESS_LEVEL_ADMIN) {
$plugins = explode(",", $_REQUEST["plugins"] ?? "");
if ($plugins !== false) {
$plugins = array_filter($plugins, 'strlen');
}
# we're in classes/pref/
$root_dir = dirname(dirname(__DIR__));
$rv = [];
if (count($plugins) > 0) {
if ($plugins) {
foreach ($plugins as $plugin_name) {
array_push($rv, ["plugin" => $plugin_name, "rv" => $this->_update_plugin($root_dir, $plugin_name)]);
}
// @phpstan-ignore-next-line
} else {
$plugin_dirs = array_filter(glob("$root_dir/plugins.local/*"), "is_dir");
@@ -1301,7 +1340,7 @@ class Pref_Prefs extends Handler_Protected {
$test = self::_plugin_needs_update($root_dir, $plugin_name);
if (!empty($test["o"]))
if (!empty($test["stdout"]))
array_push($rv, ["plugin" => $plugin_name, "rv" => $this->_update_plugin($root_dir, $plugin_name)]);
}
}
@@ -1311,20 +1350,20 @@ class Pref_Prefs extends Handler_Protected {
}
}
function clearplugindata() {
function clearplugindata(): void {
$name = clean($_REQUEST["name"]);
PluginHost::getInstance()->clear_data(PluginHost::getInstance()->get_plugin($name));
}
function customizeCSS() {
function customizeCSS(): void {
$value = get_pref(Prefs::USER_STYLESHEET);
$value = str_replace("<br/>", "\n", $value);
print json_encode(["value" => $value]);
}
function activateprofile() {
function activateprofile(): void {
$id = (int) ($_REQUEST['id'] ?? 0);
$profile = ORM::for_table('ttrss_settings_profiles')
@@ -1338,7 +1377,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
function cloneprofile() {
function cloneprofile(): void {
$old_profile = $_REQUEST["old_profile"] ?? 0;
$new_title = clean($_REQUEST["new_title"]);
@@ -1367,7 +1406,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
function remprofiles() {
function remprofiles(): void {
$ids = $_REQUEST["ids"] ?? [];
ORM::for_table('ttrss_settings_profiles')
@@ -1377,7 +1416,7 @@ class Pref_Prefs extends Handler_Protected {
->delete_many();
}
function addprofile() {
function addprofile(): void {
$title = clean($_REQUEST["title"]);
if ($title) {
@@ -1396,7 +1435,7 @@ class Pref_Prefs extends Handler_Protected {
}
}
function saveprofile() {
function saveprofile(): void {
$id = (int)$_REQUEST["id"];
$title = clean($_REQUEST["value"]);
@@ -1413,7 +1452,7 @@ class Pref_Prefs extends Handler_Protected {
}
// TODO: this maybe needs to be unified with Public::getProfiles()
function getProfiles() {
function getProfiles(): void {
$rv = [];
$profiles = ORM::for_table('ttrss_settings_profiles')
@@ -1442,21 +1481,21 @@ class Pref_Prefs extends Handler_Protected {
print json_encode($rv);
}
private function _get_short_desc($pref_name) {
private function _get_short_desc(string $pref_name): string {
if (isset($this->pref_help[$pref_name][0])) {
return $this->pref_help[$pref_name][0];
}
return "";
}
private function _get_help_text($pref_name) {
private function _get_help_text(string $pref_name): string {
if (isset($this->pref_help[$pref_name][1])) {
return $this->pref_help[$pref_name][1];
}
return "";
}
private function appPasswordList() {
private function appPasswordList(): void {
?>
<div dojoType='fox.Toolbar'>
<div dojoType='fox.form.DropDownButton'>
@@ -1506,7 +1545,7 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
function deleteAppPasswords() {
function deleteAppPasswords(): void {
$passwords = ORM::for_table('ttrss_app_passwords')
->where('owner_uid', $_SESSION['uid'])
->where_in('id', $_REQUEST['ids'] ?? [])
@@ -1515,7 +1554,7 @@ class Pref_Prefs extends Handler_Protected {
$this->appPasswordList();
}
function generateAppPassword() {
function generateAppPassword(): void {
$title = clean($_REQUEST['title']);
$new_password = make_password(16);
$new_salt = UserHelper::get_salt();
@@ -1536,11 +1575,11 @@ class Pref_Prefs extends Handler_Protected {
$this->appPasswordList();
}
function previewDigest() {
function previewDigest(): void {
print json_encode(Digest::prepare_headlines_digest($_SESSION["uid"], 1, 16));
}
static function _get_ssl_certificate_id() {
static function _get_ssl_certificate_id(): string {
if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] ?? false) {
return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
$_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
@@ -1556,7 +1595,7 @@ class Pref_Prefs extends Handler_Protected {
return "";
}
private function format_otp_secret($secret) {
private function format_otp_secret(string $secret): string {
return implode(" ", str_split($secret, 4));
}
}

View File

@@ -2,19 +2,19 @@
class Pref_System extends Handler_Administrative {
private $log_page_limit = 15;
private const LOG_PAGE_LIMIT = 15;
function csrf_ignore($method) {
function csrf_ignore(string $method): bool {
$csrf_ignored = array("index");
return array_search($method, $csrf_ignored) !== false;
}
function clearLog() {
function clearLog(): void {
$this->pdo->query("DELETE FROM ttrss_error_log");
}
function sendTestEmail() {
function sendTestEmail(): void {
$mail_address = clean($_REQUEST["mail_address"]);
$mailer = new Mailer();
@@ -28,7 +28,7 @@ class Pref_System extends Handler_Administrative {
print json_encode(['rc' => $rc, 'error' => $mailer->error()]);
}
function getphpinfo() {
function getphpinfo(): void {
ob_start();
phpinfo();
$info = ob_get_contents();
@@ -37,7 +37,7 @@ class Pref_System extends Handler_Administrative {
print preg_replace( '%^.*<body>(.*)</body>.*$%ms','$1', (string)$info);
}
private function _log_viewer(int $page, int $severity) {
private function _log_viewer(int $page, int $severity): void {
$errno_values = [];
switch ($severity) {
@@ -56,8 +56,7 @@ class Pref_System extends Handler_Administrative {
$errno_filter_qpart = "true";
}
$limit = $this->log_page_limit;
$offset = $limit * $page;
$offset = self::LOG_PAGE_LIMIT * $page;
$sth = $this->pdo->prepare("SELECT
COUNT(id) AS total_pages
@@ -69,7 +68,7 @@ class Pref_System extends Handler_Administrative {
$sth->execute($errno_values);
if ($res = $sth->fetch()) {
$total_pages = (int)($res["total_pages"] / $limit);
$total_pages = (int)($res["total_pages"] / self::LOG_PAGE_LIMIT);
} else {
$total_pages = 0;
}
@@ -134,7 +133,7 @@ class Pref_System extends Handler_Administrative {
$errno_filter_qpart
ORDER BY
ttrss_error_log.id DESC
LIMIT $limit OFFSET $offset");
LIMIT ". self::LOG_PAGE_LIMIT ." OFFSET $offset");
$sth->execute($errno_values);
@@ -159,7 +158,7 @@ class Pref_System extends Handler_Administrative {
<?php
}
function index() {
function index(): void {
$severity = (int) ($_REQUEST["severity"] ?? E_USER_WARNING);
$page = (int) ($_REQUEST["page"] ?? 0);

View File

@@ -1,10 +1,10 @@
<?php
class Pref_Users extends Handler_Administrative {
function csrf_ignore($method) {
function csrf_ignore(string $method): bool {
return $method == "index";
}
function edit() {
function edit(): void {
$user = ORM::for_table('ttrss_users')
->select_expr("id,login,access_level,email,full_name,otp_enabled")
->find_one((int)$_REQUEST["id"])
@@ -20,7 +20,7 @@ class Pref_Users extends Handler_Administrative {
}
}
function userdetails() {
function userdetails(): void {
$id = (int) clean($_REQUEST["id"]);
$sth = $this->pdo->prepare("SELECT login,
@@ -103,7 +103,7 @@ class Pref_Users extends Handler_Administrative {
}
function editSave() {
function editSave(): void {
$id = (int)$_REQUEST['id'];
$password = clean($_REQUEST["password"]);
$user = ORM::for_table('ttrss_users')->find_one($id);
@@ -132,7 +132,7 @@ class Pref_Users extends Handler_Administrative {
}
}
function remove() {
function remove(): void {
$ids = explode(",", clean($_REQUEST["ids"]));
foreach ($ids as $id) {
@@ -149,7 +149,7 @@ class Pref_Users extends Handler_Administrative {
}
}
function add() {
function add(): void {
$login = clean($_REQUEST["login"]);
if (!$login) return; // no blank usernames
@@ -178,11 +178,11 @@ class Pref_Users extends Handler_Administrative {
}
}
function resetPass() {
function resetPass(): void {
UserHelper::reset_password(clean($_REQUEST["id"]));
}
function index() {
function index(): void {
global $access_level_names;

View File

@@ -141,7 +141,10 @@ class Prefs {
Prefs::_PREFS_MIGRATED
];
/** @var Prefs|null */
private static $instance;
/** @var array<string, bool|int|string> */
private $cache = [];
/** @var PDO */
@@ -154,10 +157,13 @@ class Prefs {
return self::$instance;
}
static function is_valid(string $pref_name) {
static function is_valid(string $pref_name): bool {
return isset(self::_DEFAULTS[$pref_name]);
}
/**
* @return bool|int|null|string
*/
static function get_default(string $pref_name) {
if (self::is_valid($pref_name))
return self::_DEFAULTS[$pref_name][0];
@@ -181,10 +187,16 @@ class Prefs {
//
}
/**
* @return array<int, array<string, bool|int|null|string>>
*/
static function get_all(int $owner_uid, int $profile_id = null) {
return self::get_instance()->_get_all($owner_uid, $profile_id);
}
/**
* @return array<int, array<string, bool|int|null|string>>
*/
private function _get_all(int $owner_uid, int $profile_id = null) {
$rv = [];
@@ -205,7 +217,7 @@ class Prefs {
return $rv;
}
private function cache_all(int $owner_uid, $profile_id = null) {
private function cache_all(int $owner_uid, ?int $profile_id): void {
if (!$profile_id) $profile_id = null;
// fill cache with defaults
@@ -232,11 +244,17 @@ class Prefs {
}
}
static function get(string $pref_name, int $owner_uid, int $profile_id = null) {
/**
* @return bool|int|null|string
*/
static function get(string $pref_name, int $owner_uid, ?int $profile_id) {
return self::get_instance()->_get($pref_name, $owner_uid, $profile_id);
}
private function _get(string $pref_name, int $owner_uid, int $profile_id = null) {
/**
* @return bool|int|null|string
*/
private function _get(string $pref_name, int $owner_uid, ?int $profile_id) {
if (isset(self::_DEFAULTS[$pref_name])) {
if (!$profile_id || in_array($pref_name, self::_PROFILE_BLACKLIST)) $profile_id = null;
@@ -274,12 +292,15 @@ class Prefs {
return null;
}
private function _is_cached(string $pref_name, int $owner_uid, int $profile_id = null) {
private function _is_cached(string $pref_name, int $owner_uid, ?int $profile_id): bool {
$cache_key = sprintf("%d/%d/%s", $owner_uid, $profile_id, $pref_name);
return isset($this->cache[$cache_key]);
}
private function _get_cache(string $pref_name, int $owner_uid, int $profile_id = null) {
/**
* @return bool|int|null|string
*/
private function _get_cache(string $pref_name, int $owner_uid, ?int $profile_id) {
$cache_key = sprintf("%d/%d/%s", $owner_uid, $profile_id, $pref_name);
if (isset($this->cache[$cache_key]))
@@ -288,17 +309,23 @@ class Prefs {
return null;
}
private function _set_cache(string $pref_name, $value, int $owner_uid, int $profile_id = null) {
/**
* @param bool|int|string $value
*/
private function _set_cache(string $pref_name, $value, int $owner_uid, ?int $profile_id): void {
$cache_key = sprintf("%d/%d/%s", $owner_uid, $profile_id, $pref_name);
$this->cache[$cache_key] = $value;
}
static function set(string $pref_name, $value, int $owner_uid, int $profile_id = null, bool $strip_tags = true) {
/**
* @param bool|int|string $value
*/
static function set(string $pref_name, $value, int $owner_uid, ?int $profile_id, bool $strip_tags = true): bool {
return self::get_instance()->_set($pref_name, $value, $owner_uid, $profile_id);
}
private function _delete(string $pref_name, int $owner_uid, int $profile_id = null) {
private function _delete(string $pref_name, int $owner_uid, ?int $profile_id): bool {
$sth = $this->pdo->prepare("DELETE FROM ttrss_user_prefs2
WHERE pref_name = :name AND owner_uid = :uid AND
(profile = :profile OR (:profile IS NULL AND profile IS NULL))");
@@ -306,7 +333,10 @@ class Prefs {
return $sth->execute(["uid" => $owner_uid, "profile" => $profile_id, "name" => $pref_name ]);
}
private function _set(string $pref_name, $value, int $owner_uid, int $profile_id = null, bool $strip_tags = true) {
/**
* @param bool|int|string $value
*/
private function _set(string $pref_name, $value, int $owner_uid, ?int $profile_id, bool $strip_tags = true): bool {
if (!$profile_id) $profile_id = null;
if ($profile_id && in_array($pref_name, self::_PROFILE_BLACKLIST))
@@ -359,7 +389,7 @@ class Prefs {
return false;
}
function migrate(int $owner_uid, int $profile_id = null) {
function migrate(int $owner_uid, ?int $profile_id): void {
if (get_schema_version() < 141)
return;
@@ -401,7 +431,7 @@ class Prefs {
}
}
static function reset(int $owner_uid, int $profile_id = null) {
static function reset(int $owner_uid, ?int $profile_id): void {
if (!$profile_id) $profile_id = null;
$sth = Db::pdo()->prepare("DELETE FROM ttrss_user_prefs2

View File

@@ -1,13 +1,16 @@
<?php
class RPC extends Handler_Protected {
/*function csrf_ignore($method) {
/*function csrf_ignore(string $method): bool {
$csrf_ignored = array("completelabels");
return array_search($method, $csrf_ignored) !== false;
}*/
private function _translations_as_array() {
/**
* @return array<string, string>
*/
private function _translations_as_array(): array {
global $text_domains;
@@ -37,7 +40,7 @@ class RPC extends Handler_Protected {
}
function togglepref() {
function togglepref(): void {
$key = clean($_REQUEST["key"]);
set_pref($key, !get_pref($key));
$value = get_pref($key);
@@ -45,7 +48,7 @@ class RPC extends Handler_Protected {
print json_encode(array("param" =>$key, "value" => $value));
}
function setpref() {
function setpref(): void {
// set_pref escapes input, so no need to double escape it here
$key = clean($_REQUEST['key']);
$value = $_REQUEST['value'];
@@ -55,7 +58,7 @@ class RPC extends Handler_Protected {
print json_encode(array("param" =>$key, "value" => $value));
}
function mark() {
function mark(): void {
$mark = clean($_REQUEST["mark"]);
$id = clean($_REQUEST["id"]);
@@ -68,7 +71,7 @@ class RPC extends Handler_Protected {
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function delete() {
function delete(): void {
$ids = explode(",", clean($_REQUEST["ids"]));
$ids_qmarks = arr_qmarks($ids);
@@ -81,7 +84,7 @@ class RPC extends Handler_Protected {
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function publ() {
function publ(): void {
$pub = clean($_REQUEST["pub"]);
$id = clean($_REQUEST["id"]);
@@ -94,7 +97,7 @@ class RPC extends Handler_Protected {
print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function getRuntimeInfo() {
function getRuntimeInfo(): void {
$reply = [
'runtime-info' => $this->_make_runtime_info()
];
@@ -102,7 +105,7 @@ class RPC extends Handler_Protected {
print json_encode($reply);
}
function getAllCounters() {
function getAllCounters(): void {
@$seq = (int) $_REQUEST['seq'];
$feed_id_count = (int)$_REQUEST["feed_id_count"];
@@ -133,7 +136,7 @@ class RPC extends Handler_Protected {
}
/* GET["cmode"] = 0 - mark as read, 1 - as unread, 2 - toggle */
function catchupSelected() {
function catchupSelected(): void {
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
$cmode = (int)clean($_REQUEST["cmode"]);
@@ -145,7 +148,7 @@ class RPC extends Handler_Protected {
"feeds" => Article::_feeds_of($ids)]);
}
function markSelected() {
function markSelected(): void {
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
$cmode = (int)clean($_REQUEST["cmode"]);
@@ -157,7 +160,7 @@ class RPC extends Handler_Protected {
"feeds" => Article::_feeds_of($ids)]);
}
function publishSelected() {
function publishSelected(): void {
$ids = array_map("intval", clean($_REQUEST["ids"] ?? []));
$cmode = (int)clean($_REQUEST["cmode"]);
@@ -169,7 +172,7 @@ class RPC extends Handler_Protected {
"feeds" => Article::_feeds_of($ids)]);
}
function sanityCheck() {
function sanityCheck(): void {
$_SESSION["hasSandbox"] = clean($_REQUEST["hasSandbox"]) === "true";
$_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]);
@@ -220,14 +223,14 @@ class RPC extends Handler_Protected {
print "</ul>";
}*/
function catchupFeed() {
function catchupFeed(): void {
$feed_id = clean($_REQUEST['feed_id']);
$is_cat = clean($_REQUEST['is_cat']) == "true";
$mode = clean($_REQUEST['mode'] ?? '');
$search_query = clean($_REQUEST['search_query']);
$search_lang = clean($_REQUEST['search_lang']);
Feeds::_catchup($feed_id, $is_cat, false, $mode, [$search_query, $search_lang]);
Feeds::_catchup($feed_id, $is_cat, null, $mode, [$search_query, $search_lang]);
// return counters here synchronously so that frontend can figure out next unread feed properly
print json_encode(['counters' => Counters::get_all()]);
@@ -235,7 +238,7 @@ class RPC extends Handler_Protected {
//print json_encode(array("message" => "UPDATE_COUNTERS"));
}
function setWidescreen() {
function setWidescreen(): void {
$wide = (int) clean($_REQUEST["wide"]);
set_pref(Prefs::WIDESCREEN_MODE, $wide);
@@ -243,7 +246,7 @@ class RPC extends Handler_Protected {
print json_encode(["wide" => $wide]);
}
static function updaterandomfeed_real() {
static function updaterandomfeed_real(): void {
$default_interval = (int) Prefs::get_default(Prefs::DEFAULT_UPDATE_INTERVAL);
@@ -336,19 +339,22 @@ class RPC extends Handler_Protected {
}
function updaterandomfeed() {
function updaterandomfeed(): void {
self::updaterandomfeed_real();
}
private function markArticlesById($ids, $cmode) {
/**
* @param array<int, int> $ids
*/
private function markArticlesById(array $ids, int $cmode): void {
$ids_qmarks = arr_qmarks($ids);
if ($cmode == 0) {
if ($cmode == Article::CATCHUP_MODE_MARK_AS_READ) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
marked = false, last_marked = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else if ($cmode == 1) {
} else if ($cmode == Article::CATCHUP_MODE_MARK_AS_UNREAD) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
marked = true, last_marked = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
@@ -361,15 +367,18 @@ class RPC extends Handler_Protected {
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
}
private function publishArticlesById($ids, $cmode) {
/**
* @param array<int, int> $ids
*/
private function publishArticlesById(array $ids, int $cmode): void {
$ids_qmarks = arr_qmarks($ids);
if ($cmode == 0) {
if ($cmode == Article::CATCHUP_MODE_MARK_AS_READ) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
published = false, last_published = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
} else if ($cmode == 1) {
} else if ($cmode == Article::CATCHUP_MODE_MARK_AS_UNREAD) {
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET
published = true, last_published = NOW()
WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?");
@@ -382,7 +391,7 @@ class RPC extends Handler_Protected {
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
}
function log() {
function log(): void {
$msg = clean($_REQUEST['msg'] ?? "");
$file = basename(clean($_REQUEST['file'] ?? ""));
$line = (int) clean($_REQUEST['line'] ?? 0);
@@ -396,7 +405,7 @@ class RPC extends Handler_Protected {
}
}
function checkforupdates() {
function checkforupdates(): void {
$rv = ["changeset" => [], "plugins" => []];
$version = Config::get_version(false);
@@ -425,7 +434,10 @@ class RPC extends Handler_Protected {
print json_encode($rv);
}
private function _make_init_params() {
/**
* @return array<string, mixed>
*/
private function _make_init_params(): array {
$params = array();
foreach ([Prefs::ON_CATCHUP_SHOW_NEXT_FEED, Prefs::HIDE_READ_FEEDS,
@@ -481,7 +493,7 @@ class RPC extends Handler_Protected {
return $params;
}
private function image_to_base64($filename) {
private function image_to_base64(string $filename): string {
if (file_exists($filename)) {
$ext = pathinfo($filename, PATHINFO_EXTENSION);
@@ -493,7 +505,10 @@ class RPC extends Handler_Protected {
}
}
static function _make_runtime_info() {
/**
* @return array<string, mixed>
*/
static function _make_runtime_info(): array {
$data = array();
$pdo = Db::pdo();
@@ -562,7 +577,10 @@ class RPC extends Handler_Protected {
return $data;
}
static function get_hotkeys_info() {
/**
* @return array<string, array<string, string>>
*/
static function get_hotkeys_info(): array {
$hotkeys = array(
__("Navigation") => array(
"next_feed" => __("Open next feed"),
@@ -642,8 +660,12 @@ class RPC extends Handler_Protected {
return $hotkeys;
}
// {3} - 3 panel mode only
// {C} - combined mode only
/**
* {3} - 3 panel mode only
* {C} - combined mode only
*
* @return array{0: array<int, string>, 1: array<string, string>} $prefixes, $hotkeys
*/
static function get_hotkeys_map() {
$hotkeys = array(
"k" => "next_feed",
@@ -728,7 +750,7 @@ class RPC extends Handler_Protected {
return array($prefixes, $hotkeys);
}
function hotkeyHelp() {
function hotkeyHelp(): void {
$info = self::get_hotkeys_info();
$imap = self::get_hotkeys_map();
$omap = array();

View File

@@ -1,6 +1,9 @@
<?php
class RSSUtils {
static function calculate_article_hash($article, $pluginhost) {
/**
* @param array<string, mixed> $article
*/
static function calculate_article_hash(array $article, PluginHost $pluginhost): string {
$tmp = "";
$ignored_fields = [ "feed", "guid", "guid_hashed", "owner_uid", "force_catchup" ];
@@ -21,16 +24,16 @@ class RSSUtils {
}
// Strips utf8mb4 characters (i.e. emoji) for mysql
static function strip_utf8mb4(string $str) {
static function strip_utf8mb4(string $str): string {
return preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $str);
}
static function cleanup_feed_browser() {
static function cleanup_feed_browser(): void {
$pdo = Db::pdo();
$pdo->query("DELETE FROM ttrss_feedbrowser_cache");
}
static function cleanup_feed_icons() {
static function cleanup_feed_icons(): void {
$pdo = Db::pdo();
$sth = $pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ?");
@@ -52,7 +55,10 @@ class RSSUtils {
}
}
static function update_daemon_common(int $limit = 0, array $options = []) {
/**
* @param array<string, false|string> $options
*/
static function update_daemon_common(int $limit = 0, array $options = []): int {
if (!$limit) $limit = Config::get(Config::DAEMON_FEED_LIMIT);
if (Config::get_schema_version() != Config::SCHEMA_VERSION) {
@@ -272,7 +278,7 @@ class RSSUtils {
}
/** this is used when subscribing */
static function update_basic_info(int $feed_id) {
static function update_basic_info(int $feed_id): void {
$feed = ORM::for_table('ttrss_feeds')
->select_many('id', 'owner_uid', 'feed_url', 'auth_pass', 'auth_login', 'title', 'site_url')
->find_one($feed_id);
@@ -681,7 +687,7 @@ class RSSUtils {
}
$entry_comments = mb_substr(strip_tags($item->get_comments_url()), 0, 245);
$num_comments = (int) $item->get_comments_count();
$num_comments = $item->get_comments_count();
$entry_author = strip_tags($item->get_author());
$entry_guid = mb_substr($entry_guid, 0, 245);
@@ -728,6 +734,7 @@ class RSSUtils {
},
$e, $feed);
// TODO: Just use FeedEnclosure (and modify it to cover whatever justified this)?
$e_item = array(
rewrite_relative_url($site_url, $e->link),
$e->type, $e->length, $e->title, $e->width, $e->height);
@@ -1265,8 +1272,14 @@ class RSSUtils {
return true;
}
/* TODO: move to DiskCache? */
static function cache_enclosures($enclosures, $site_url) {
/**
* TODO: move to DiskCache?
*
* @param array<int, array<string>> $enclosures An array of "enclosure arrays" [string $link, string $type, string $length, string, $title, string $width, string $height]
* @see RSSUtils::update_rss_feed()
* @see FeedEnclosure
*/
static function cache_enclosures(array $enclosures, string $site_url): void {
$cache = new DiskCache("images");
if ($cache->is_writable()) {
@@ -1298,7 +1311,7 @@ class RSSUtils {
}
/* TODO: move to DiskCache? */
static function cache_media_url($cache, $url, $site_url) {
static function cache_media_url(DiskCache $cache, string $url, string $site_url): void {
$url = rewrite_relative_url($site_url, $url);
$local_filename = sha1($url);
@@ -1322,7 +1335,7 @@ class RSSUtils {
}
/* TODO: move to DiskCache? */
static function cache_media($html, $site_url) {
static function cache_media(string $html, string $site_url): void {
$cache = new DiskCache("images");
if ($html && $cache->is_writable()) {
@@ -1351,7 +1364,7 @@ class RSSUtils {
}
}
static function expire_error_log() {
static function expire_error_log(): void {
Debug::log("Removing old error log entries...");
$pdo = Db::pdo();
@@ -1365,14 +1378,16 @@ class RSSUtils {
}
}
// deprecated; table not used
static function expire_feed_archive() {
/**
* @deprecated table not used
*/
static function expire_feed_archive(): void {
$pdo = Db::pdo();
$pdo->query("DELETE FROM ttrss_archived_feeds");
}
static function expire_lock_files() {
static function expire_lock_files(): void {
Debug::log("Removing old lock files...", Debug::LOG_VERBOSE);
$num_deleted = 0;
@@ -1413,7 +1428,15 @@ class RSSUtils {
return $params;
} */
static function get_article_filters($filters, $title, $content, $link, $author, $tags, &$matched_rules = false, &$matched_filters = false) {
/**
* @param array<int, array<string, mixed>> $filters
* @param array<int, string> $tags
* @param array<int, array<string, mixed>>|null $matched_rules
* @param array<int, array<string, mixed>>|null $matched_filters
*
* @return array<int, array<string, string>> An array of filter action arrays with keys "type" and "param"
*/
static function get_article_filters(array $filters, string $title, string $content, string $link, string $author, array $tags, array &$matched_rules = null, array &$matched_filters = null): array {
$matches = array();
foreach ($filters as $filter) {
@@ -1497,16 +1520,26 @@ class RSSUtils {
return $matches;
}
static function find_article_filter($filters, $filter_name) {
/**
* @param array<int, array<string, string>> $filters An array of filter action arrays with keys "type" and "param"
*
* @return array<string, string>|null A filter action array with keys "type" and "param"
*/
static function find_article_filter(array $filters, string $filter_name): ?array {
foreach ($filters as $f) {
if ($f["type"] == $filter_name) {
return $f;
};
}
return false;
return null;
}
static function find_article_filters($filters, $filter_name) {
/**
* @param array<int, array<string, string>> $filters An array of filter action arrays with keys "type" and "param"
*
* @return array<int, array<string, string>> An array of filter action arrays with keys "type" and "param"
*/
static function find_article_filters(array $filters, string $filter_name): array {
$results = array();
foreach ($filters as $f) {
@@ -1517,7 +1550,10 @@ class RSSUtils {
return $results;
}
static function calculate_article_score($filters) {
/**
* @param array<int, array<string, string>> $filters An array of filter action arrays with keys "type" and "param"
*/
static function calculate_article_score(array $filters): int {
$score = 0;
foreach ($filters as $f) {
@@ -1528,7 +1564,12 @@ class RSSUtils {
return $score;
}
static function labels_contains_caption($labels, $caption) {
/**
* @param array<int, array<int, int|string>> $labels An array of label arrays like [int $feed_id, string $caption, string $fg_color, string $bg_color]
*
* @see Article::_get_labels()
*/
static function labels_contains_caption(array $labels, string $caption): bool {
foreach ($labels as $label) {
if ($label[1] == $caption) {
return true;
@@ -1538,7 +1579,11 @@ class RSSUtils {
return false;
}
static function assign_article_to_label_filters($id, $filters, $owner_uid, $article_labels) {
/**
* @param array<int, array<string, string>> $filters An array of filter action arrays with keys "type" and "param"
* @param array<int, array<int, int|string>> $article_labels An array of label arrays like [int $feed_id, string $caption, string $fg_color, string $bg_color]
*/
static function assign_article_to_label_filters(int $id, array $filters, int $owner_uid, $article_labels): void {
foreach ($filters as $f) {
if ($f["type"] == "label") {
if (!self::labels_contains_caption($article_labels, $f["param"])) {
@@ -1548,20 +1593,20 @@ class RSSUtils {
}
}
static function make_guid_from_title($title) {
static function make_guid_from_title(string $title): ?string {
return preg_replace("/[ \"\',.:;]/", "-",
mb_strtolower(strip_tags($title), 'utf-8'));
}
/* counter cache is no longer used, if called truncate leftover data */
static function cleanup_counters_cache() {
static function cleanup_counters_cache(): void {
$pdo = Db::pdo();
$pdo->query("DELETE FROM ttrss_counters_cache");
$pdo->query("DELETE FROM ttrss_cat_counters_cache");
}
static function disable_failed_feeds() {
static function disable_failed_feeds(): void {
if (Config::get(Config::DAEMON_UNSUCCESSFUL_DAYS_LIMIT) > 0) {
$pdo = Db::pdo();
@@ -1599,7 +1644,7 @@ class RSSUtils {
}
}
static function housekeeping_user($owner_uid) {
static function housekeeping_user(int $owner_uid): void {
$tmph = new PluginHost();
UserHelper::load_user_plugins($owner_uid, $tmph);
@@ -1607,7 +1652,7 @@ class RSSUtils {
$tmph->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
}
static function housekeeping_common() {
static function housekeeping_common(): void {
DiskCache::expire();
self::expire_lock_files();
@@ -1623,6 +1668,9 @@ class RSSUtils {
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
}
/**
* @return false|string
*/
static function update_favicon(string $site_url, int $feed) {
$icon_file = Config::get(Config::ICONS_DIR) . "/$feed.ico";
@@ -1687,11 +1735,14 @@ class RSSUtils {
return $icon_file;
}
static function is_gzipped($feed_data) {
static function is_gzipped(string $feed_data): bool {
return strpos(substr($feed_data, 0, 3),
"\x1f" . "\x8b" . "\x08", 0) === 0;
}
/**
* @return array<int, array<string, mixed>> An array of filter arrays with keys "id", "match_any_rule", "inverse", "rules", and "actions"
*/
static function load_filters(int $feed_id, int $owner_uid) {
$filters = array();
@@ -1809,7 +1860,7 @@ class RSSUtils {
*
* @param string $url A feed or page URL
* @access public
* @return mixed The favicon URL, or false if none was found.
* @return false|string The favicon URL string, or false if none was found.
*/
static function get_favicon_url(string $url) {
@@ -1843,8 +1894,12 @@ class RSSUtils {
return $favicon_url;
}
// https://community.tt-rss.org/t/problem-with-img-srcset/3519
static function decode_srcset($srcset) {
/**
* @see https://community.tt-rss.org/t/problem-with-img-srcset/3519
*
* @return array<int, array<string, string>> An array of srcset subitem arrays with keys "url" and "size"
*/
static function decode_srcset(string $srcset): array {
$matches = [];
preg_match_all(
@@ -1862,7 +1917,10 @@ class RSSUtils {
return $matches;
}
static function encode_srcset($matches) {
/**
* @param array<int, array<string, string>> $matches An array of srcset subitem arrays with keys "url" and "size"
*/
static function encode_srcset(array $matches): string {
$tokens = [];
foreach ($matches as $m) {
@@ -1872,7 +1930,7 @@ class RSSUtils {
return implode(",", $tokens);
}
static function function_enabled($func) {
static function function_enabled(string $func): bool {
return !in_array($func,
explode(',', str_replace(" ", "", ini_get('disable_functions'))));
}

View File

@@ -1,6 +1,10 @@
<?php
class Sanitizer {
private static function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
/**
* @param array<int, string> $allowed_elements
* @param array<int, string> $disallowed_attributes
*/
private static function strip_harmful_tags(DOMDocument $doc, array $allowed_elements, $disallowed_attributes): DOMDocument {
$xpath = new DOMXPath($doc);
$entries = $xpath->query('//*');
@@ -40,7 +44,7 @@ class Sanitizer {
return $doc;
}
public static function iframe_whitelisted($entry) {
public static function iframe_whitelisted(DOMElement $entry): bool {
$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
if (!empty($src))
@@ -49,11 +53,16 @@ class Sanitizer {
return false;
}
private static function is_prefix_https() {
private static function is_prefix_https(): bool {
return parse_url(Config::get(Config::SELF_URL_PATH), PHP_URL_SCHEME) == 'https';
}
public static function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
/**
* @param array<int, string>|null $highlight_words Words to highlight in the HTML output.
*
* @return false|string The HTML, or false if an error occurred.
*/
public static function sanitize(string $str, bool $force_remove_images = false, int $owner = null, string $site_url = null, array $highlight_words = null, int $article_id = null) {
if (!$owner && isset($_SESSION["uid"]))
$owner = $_SESSION["uid"];
@@ -183,7 +192,7 @@ class Sanitizer {
$div->appendChild($entry);
}
if ($highlight_words && is_array($highlight_words)) {
if (is_array($highlight_words)) {
foreach ($highlight_words as $word) {
// http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph

View File

@@ -1,7 +1,7 @@
<?php
class TimeHelper {
static function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
static function smart_date_time(int $timestamp, int $tz_offset = 0, int $owner_uid = null, bool $eta_min = false): string {
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
@@ -21,8 +21,8 @@ class TimeHelper {
}
}
static function make_local_datetime($timestamp, $long, $owner_uid = false,
$no_smart_dt = false, $eta_min = false) {
static function make_local_datetime(?string $timestamp, bool $long, int $owner_uid = null,
bool $no_smart_dt = false, bool $eta_min = false): string {
if (!$owner_uid) $owner_uid = $_SESSION['uid'];
if (!$timestamp) $timestamp = '1970-01-01 0:00';
@@ -67,7 +67,7 @@ class TimeHelper {
}
}
static function convert_timestamp($timestamp, $source_tz, $dest_tz) {
static function convert_timestamp(int $timestamp, string $source_tz, string $dest_tz): int {
try {
$source_tz = new DateTimeZone($source_tz);

View File

@@ -6,16 +6,35 @@ class UrlHelper {
"tel"
];
// TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+
/** @var string */
static $fetch_last_error;
/** @var int */
static $fetch_last_error_code;
/** @var string */
static $fetch_last_error_content;
/** @var string */
static $fetch_last_content_type;
/** @var string */
static $fetch_last_modified;
/** @var string */
static $fetch_effective_url;
/** @var string */
static $fetch_effective_ip_addr;
/** @var bool */
static $fetch_curl_used;
static function build_url($parts) {
/**
* @param array<string, string|int> $parts
*/
static function build_url(array $parts): string {
$tmp = $parts['scheme'] . "://" . $parts['host'];
if (isset($parts['path'])) $tmp .= $parts['path'];
@@ -34,12 +53,22 @@ class UrlHelper {
* @param string $owner_element Owner element tag name (i.e. "a") (optional)
* @param string $owner_attribute Owner attribute (i.e. "href") (optional)
*
* @return string Absolute URL
* @return false|string Absolute URL or false on failure (either during URL parsing or validation)
*/
public static function rewrite_relative($base_url, $rel_url, string $owner_element = "", string $owner_attribute = "") {
$rel_parts = parse_url($rel_url);
/**
* If parse_url failed to parse $rel_url return false to match the current "invalid thing" behavior
* of UrlHelper::validate().
*
* TODO: There are many places where a string return value is assumed. We should either update those
* to account for the possibility of failure, or look into updating this function's return values.
*/
if ($rel_parts === false) {
return false;
}
if (!empty($rel_parts['host']) && !empty($rel_parts['scheme'])) {
return self::validate($rel_url);
@@ -80,8 +109,10 @@ class UrlHelper {
}
}
// extended filtering involves validation for safe ports and loopback
static function validate($url, $extended_filtering = false) {
/** extended filtering involves validation for safe ports and loopback
* @return false|string false if something went wrong, otherwise the URL string
*/
static function validate(string $url, bool $extended_filtering = false) {
$url = clean($url);
@@ -107,6 +138,11 @@ class UrlHelper {
} else {
$tokens['host'] = idn_to_ascii($tokens['host']);
}
// if `idn_to_ascii` failed
if ($tokens['host'] === false) {
return false;
}
}
}
@@ -138,7 +174,10 @@ class UrlHelper {
return $url;
}
static function resolve_redirects($url, $timeout, $nest = 0) {
/**
* @return false|string
*/
static function resolve_redirects(string $url, int $timeout, int $nest = 0) {
// too many redirects
if ($nest > 10)
@@ -189,12 +228,16 @@ class UrlHelper {
return false;
}
/**
* @param array<string, bool|int|string>|string $options
* @return false|string false if something went wrong, otherwise string contents
*/
// TODO: max_size currently only works for CURL transfers
// TODO: multiple-argument way is deprecated, first parameter is a hash now
public static function fetch($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
self::$fetch_last_error = false;
self::$fetch_last_error = "";
self::$fetch_last_error_code = -1;
self::$fetch_last_error_content = "";
self::$fetch_last_content_type = "";
@@ -510,7 +553,10 @@ class UrlHelper {
}
}
public static function url_to_youtube_vid($url) {
/**
* @return false|string false if the provided URL didn't match expected patterns, otherwise the video ID string
*/
public static function url_to_youtube_vid(string $url) {
$url = str_replace("youtube.com", "youtube-nocookie.com", $url);
$regexps = [

View File

@@ -32,7 +32,7 @@ class UserHelper {
/** has administrator permissions */
const ACCESS_LEVEL_ADMIN = 10;
static function authenticate(string $login = null, string $password = null, bool $check_only = false, string $service = null) {
static function authenticate(string $login = null, string $password = null, bool $check_only = false, string $service = null): bool {
if (!Config::get(Config::SINGLE_USER_MODE)) {
$user_id = false;
$auth_module = false;
@@ -99,7 +99,7 @@ class UserHelper {
}
}
static function load_user_plugins(int $owner_uid, PluginHost $pluginhost = null) {
static function load_user_plugins(int $owner_uid, PluginHost $pluginhost = null): void {
if (!$pluginhost) $pluginhost = PluginHost::getInstance();
@@ -114,7 +114,7 @@ class UserHelper {
}
}
static function login_sequence() {
static function login_sequence(): void {
$pdo = Db::pdo();
if (Config::get(Config::SINGLE_USER_MODE)) {
@@ -159,7 +159,7 @@ class UserHelper {
}
}
static function print_user_stylesheet() {
static function print_user_stylesheet(): void {
$value = get_pref(Prefs::USER_STYLESHEET);
if ($value) {
@@ -170,7 +170,7 @@ class UserHelper {
}
static function get_user_ip() {
static function get_user_ip(): ?string {
foreach (["HTTP_X_REAL_IP", "REMOTE_ADDR"] as $hdr) {
if (isset($_SERVER[$hdr]))
return $_SERVER[$hdr];
@@ -179,7 +179,7 @@ class UserHelper {
return null;
}
static function get_login_by_id(int $id) {
static function get_login_by_id(int $id): ?string {
$user = ORM::for_table('ttrss_users')
->find_one($id);
@@ -189,7 +189,7 @@ class UserHelper {
return null;
}
static function find_user_by_login(string $login) {
static function find_user_by_login(string $login): ?int {
$user = ORM::for_table('ttrss_users')
->where('login', $login)
->find_one();
@@ -200,7 +200,7 @@ class UserHelper {
return null;
}
static function logout() {
static function logout(): void {
if (session_status() === PHP_SESSION_ACTIVE)
session_destroy();
@@ -211,11 +211,11 @@ class UserHelper {
session_commit();
}
static function get_salt() {
static function get_salt(): string {
return substr(bin2hex(get_random_bytes(125)), 0, 250);
}
static function reset_password($uid, $format_output = false, $new_password = "") {
static function reset_password(int $uid, bool $format_output = false, string $new_password = ""): void {
$user = ORM::for_table('ttrss_users')->find_one($uid);
$message = "";
@@ -298,7 +298,7 @@ class UserHelper {
}
}
static function get_otp_secret(int $owner_uid, bool $show_if_enabled = false) {
static function get_otp_secret(int $owner_uid, bool $show_if_enabled = false): ?string {
$user = ORM::for_table('ttrss_users')->find_one($owner_uid);
if ($user) {
@@ -333,7 +333,9 @@ class UserHelper {
return null;
}
static function is_default_password() {
static function is_default_password(): bool {
/** @var Auth_Internal|false $authenticator -- this is only here to make check_password() visible to static analyzer */
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
if ($authenticator &&
@@ -345,10 +347,12 @@ class UserHelper {
return false;
}
static function hash_password(string $pass, string $salt, string $algo = "") {
if (!$algo) $algo = self::HASH_ALGOS[0];
/**
* @param string $algo should be one of UserHelper::HASH_ALGO_*
*
* @return false|string False if the password couldn't be hashed, otherwise the hash string.
*/
static function hash_password(string $pass, string $salt, string $algo = self::HASH_ALGOS[0]) {
$pass_hash = "";
switch ($algo) {

View File

@@ -6,6 +6,6 @@
"j4mie/idiorm": "^1.5"
},
"require-dev": {
"phpstan/phpstan": "^1.0.0"
"phpstan/phpstan": "^1."
}
}

84
composer.lock generated
View File

@@ -4,27 +4,23 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7e359aa56ec6aad30f701009ced5590c",
"content-hash": "e6292f986e0c1bccf460250425d42c19",
"packages": [
{
"name": "beberlei/assert",
"version": "v3.2.7",
"version": "v3.2.2",
"source": {
"type": "git",
"url": "https://github.com/beberlei/assert.git",
"reference": "d63a6943fc4fd1a2aedb65994e3548715105abcf"
"reference": "5547e7d03f8c6be121b8b9db6d6ed5a22ffdcb01"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/beberlei/assert/zipball/d63a6943fc4fd1a2aedb65994e3548715105abcf",
"reference": "d63a6943fc4fd1a2aedb65994e3548715105abcf",
"url": "https://api.github.com/repos/beberlei/assert/zipball/5547e7d03f8c6be121b8b9db6d6ed5a22ffdcb01",
"reference": "5547e7d03f8c6be121b8b9db6d6ed5a22ffdcb01",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"php": "^7"
},
"require-dev": {
@@ -32,9 +28,6 @@
"phpstan/phpstan-shim": "*",
"phpunit/phpunit": ">=6.0.0 <8"
},
"suggest": {
"ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles"
},
"type": "library",
"autoload": {
"psr-4": {
@@ -66,32 +59,29 @@
"assertion",
"validation"
],
"support": {
"issues": "https://github.com/beberlei/assert/issues",
"source": "https://github.com/beberlei/assert/tree/v3"
},
"time": "2019-12-19T17:51:41+00:00"
"time": "2019-08-23T16:04:58+00:00"
},
{
"name": "chillerlan/php-qrcode",
"version": "3.4.0",
"version": "3.4.1",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-qrcode.git",
"reference": "d8bf297e6843a53aeaa8f3285ce04fc349d133d6"
"reference": "468603b687a5fe75c1ff33857a45f1726c7b95a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/d8bf297e6843a53aeaa8f3285ce04fc349d133d6",
"reference": "d8bf297e6843a53aeaa8f3285ce04fc349d133d6",
"url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/468603b687a5fe75c1ff33857a45f1726c7b95a9",
"reference": "468603b687a5fe75c1ff33857a45f1726c7b95a9",
"shasum": ""
},
"require": {
"chillerlan/php-settings-container": "^1.2",
"chillerlan/php-settings-container": "^1.2.2",
"ext-mbstring": "*",
"php": "^7.2"
"php": "^7.2 || ^8.0"
},
"require-dev": {
"phan/phan": "^3.2.2",
"phpunit/phpunit": "^8.5",
"setasign/fpdf": "^1.8.2"
},
@@ -133,10 +123,6 @@
"qrcode",
"qrcode-generator"
],
"support": {
"issues": "https://github.com/chillerlan/php-qrcode/issues",
"source": "https://github.com/chillerlan/php-qrcode/tree/3.4.0"
},
"funding": [
{
"url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4",
@@ -147,28 +133,28 @@
"type": "ko_fi"
}
],
"time": "2020-11-18T20:51:41+00:00"
"time": "2021-09-03T17:54:45+00:00"
},
{
"name": "chillerlan/php-settings-container",
"version": "1.2.1",
"version": "1.2.2",
"source": {
"type": "git",
"url": "https://github.com/chillerlan/php-settings-container.git",
"reference": "b9b0431dffd74102ee92348a63b4c33fc8ba639b"
"reference": "d1b5284d6eb3a767459738bb0b20073f0cb3eeaf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/b9b0431dffd74102ee92348a63b4c33fc8ba639b",
"reference": "b9b0431dffd74102ee92348a63b4c33fc8ba639b",
"url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/d1b5284d6eb3a767459738bb0b20073f0cb3eeaf",
"reference": "d1b5284d6eb3a767459738bb0b20073f0cb3eeaf",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.2"
"php": "^7.2 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^8.3"
"phpunit/phpunit": "^8.4"
},
"type": "library",
"autoload": {
@@ -195,11 +181,17 @@
"container",
"helper"
],
"support": {
"issues": "https://github.com/chillerlan/php-settings-container/issues",
"source": "https://github.com/chillerlan/php-settings-container"
"funding": [
{
"url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4",
"type": "custom"
},
"time": "2019-09-10T00:09:44+00:00"
{
"url": "https://ko-fi.com/codemasher",
"type": "ko_fi"
}
],
"time": "2021-09-03T17:33:25+00:00"
},
{
"name": "j4mie/idiorm",
@@ -595,16 +587,16 @@
"packages-dev": [
{
"name": "phpstan/phpstan",
"version": "1.0.0",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "0d13a99513182e521271d46bde8f28caa4f84d97"
"reference": "bcea0ae85868a89d5789c75f012c93129f842934"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d13a99513182e521271d46bde8f28caa4f84d97",
"reference": "0d13a99513182e521271d46bde8f28caa4f84d97",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/bcea0ae85868a89d5789c75f012c93129f842934",
"reference": "bcea0ae85868a89d5789c75f012c93129f842934",
"shasum": ""
},
"require": {
@@ -633,10 +625,6 @@
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.0.0"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
@@ -655,7 +643,7 @@
"type": "tidelift"
}
],
"time": "2021-11-01T06:38:20+00:00"
"time": "2021-11-09T12:41:09+00:00"
}
],
"aliases": [],
@@ -665,5 +653,5 @@
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.1.0"
"plugin-api-version": "1.1.0"
}

View File

@@ -5,7 +5,7 @@ if (file_exists("lib/floIcon.php")) {
require_once "lib/floIcon.php";
}
function _resolve_htmlcolor($color) {
function _resolve_htmlcolor(string $color): string {
$htmlcolors = array ("aliceblue" => "#f0f8ff",
"antiquewhite" => "#faebd7",
"aqua" => "#00ffff",
@@ -162,8 +162,14 @@ function _resolve_htmlcolor($color) {
return $color;
}
### RGB >> HSL
function _color_rgb2hsl($rgb) {
/**
* RGB >> HSL
*
* @param array{0: int, 1: int, 2: int} $rgb
*
* @return array{0: float, 1: float, 2: float}
*/
function _color_rgb2hsl(array $rgb): array {
$r = $rgb[0]; $g = $rgb[1]; $b = $rgb[2];
$min = min($r, min($g, $b)); $max = max($r, max($g, $b));
$delta = $max - $min; $l = ($min + $max) / 2; $s = 0;
@@ -179,8 +185,14 @@ function _color_rgb2hsl($rgb) {
} return array($h, $s, $l);
}
### HSL >> RGB
function _color_hsl2rgb($hsl) {
/**
* HSL >> RGB
*
* @param array{0: float, 1: float, 2: float} $hsl
*
* @return array{0: int, 1: int, 2: int}
*/
function _color_hsl2rgb($hsl): array {
$h = $hsl[0]; $s = $hsl[1]; $l = $hsl[2];
$m2 = ($l <= 0.5) ? $l * ($s + 1) : $l + $s - $l*$s;
$m1 = $l * 2 - $m2;
@@ -190,16 +202,28 @@ function _color_hsl2rgb($hsl) {
}
### Helper function for _color_hsl2rgb().
function _color_hue2rgb($m1, $m2, $h) {
function _color_hue2rgb(float $m1, float $m2, float $h): int {
$rv = $m1;
$h = ($h < 0) ? $h + 1 : (($h > 1) ? $h - 1 : $h);
if ($h * 6 < 1) return $m1 + ($m2 - $m1) * $h * 6;
if ($h * 2 < 1) return $m2;
if ($h * 3 < 2) return $m1 + ($m2 - $m1) * (0.66666 - $h) * 6;
return $m1;
if ($h * 6 < 1) {
$rv = $m1 + ($m2 - $m1) * $h * 6;
} else if ($h * 2 < 1) {
$rv = $m2;
} else if ($h * 3 < 2) {
$rv = $m1 + ($m2 - $m1) * (0.66666 - $h) * 6;
}
### Convert a hex color into an RGB triplet.
function _color_unpack($hex, $normalize = false) {
return (int) round($rv);
}
/**
* Convert a hex color into an RGB triplet.
*
* @return array{0: int, 1: int, 2: int}
*/
function _color_unpack(string $hex, bool $normalize = false): array {
$hex = strpos($hex, '#') !== 0 ? _resolve_htmlcolor($hex) : substr($hex, 1);
if (strlen($hex) == 4) {
@@ -216,8 +240,12 @@ function _color_unpack($hex, $normalize = false) {
return $out;
}
### Convert an RGB triplet to a hex color.
function _color_pack($rgb, $normalize = false) {
/**
* Convert an RGB triplet to a hex color.
*
* @param array{0: int, 1: int, 2: int} $rgb
*/
function _color_pack(array $rgb, bool $normalize = false): string {
$out = 0;
foreach ($rgb as $k => $v) {
@@ -227,7 +255,12 @@ function _color_pack($rgb, $normalize = false) {
return '#'. str_pad(dechex($out), 6, '0', STR_PAD_LEFT);
}
function rgb2hsl($arr) {
/**
* @param array{0: int, 1: int, 2: int} $arr
*
* @return array{0: float, 1: float, 2: float}
*/
function rgb2hsl(array $arr): array {
$r = $arr[0];
$g = $arr[1];
$b = $arr[2];
@@ -268,7 +301,12 @@ function rgb2hsl($arr) {
return array($h, $s, $v);
}
function hsl2rgb($arr) {
/**
* @param array{0: float, 1: float, 2: float} $arr
*
* @return array{0: int, 1: int, 2: int}
*/
function hsl2rgb(array $arr): array {
$h = $arr[0];
$s = $arr[1];
$v = $arr[2];
@@ -289,42 +327,43 @@ function hsl2rgb($arr) {
else if ($var_i == 4) { $var_R = $var_3 ; $var_G = $var_1 ; $var_B = $v ; }
else { $var_R = $v ; $var_G = $var_1 ; $var_B = $var_2 ; }
$r = $var_R * 255;
$g = $var_G * 255;
$B = $var_B * 255;
$r = (int) round($var_R * 255);
$g = (int) round($var_G * 255);
$B = (int) round($var_B * 255);
}
return array($r, $g, $B);
}
function colorPalette($imageFile, $numColors, $granularity = 5) {
$granularity = max(1, abs((int)$granularity));
$colors = array();
/**
* @return array<int, string>|null
*/
function colorPalette(string $imageFile, int $numColors, int $granularity = 5): ?array {
$granularity = max(1, abs($granularity));
$colors = [];
$size = @getimagesize($imageFile);
$img = null;
// to enable .ico support place floIcon.php into lib/
if (strtolower($size['mime']) == 'image/vnd.microsoft.icon') {
if (class_exists("floIcon")) {
$ico = new \floIcon();
@$ico->readICO($imageFile);
if(count($ico->images)==0)
return false;
else
$img = @$ico->images[count($ico->images)-1]->getImageResource();
if(count($ico->images) == 0) {
return null;
} else {
return false;
$img = @$ico->images[count($ico->images)-1]->getImageResource();
}
}
return null;
} else if ($size[0] > 0 && $size[1] > 0) {
$img = @imagecreatefromstring(file_get_contents($imageFile));
}
if (!$img) return false;
if (!$img) {
return null;
}
for($x = 0; $x < $size[0]; $x += $granularity) {
for($y = 0; $y < $size[1]; $y += $granularity) {
@@ -334,6 +373,7 @@ function hsl2rgb($arr) {
$green = round(round(($rgb['green'] / 0x33)) * 0x33);
$blue = round(round(($rgb['blue'] / 0x33)) * 0x33);
$thisRGB = sprintf('%02X%02X%02X', $red, $green, $blue);
if(array_key_exists($thisRGB, $colors)) {
$colors[$thisRGB]++;
} else {
@@ -346,7 +386,7 @@ function hsl2rgb($arr) {
return array_slice(array_keys($colors), 0, $numColors);
}
function calculate_avg_color($iconFile) {
function calculate_avg_color(string $iconFile): string {
$palette = colorPalette($iconFile, 4, 4);
if (is_array($palette)) {

View File

@@ -1,7 +1,10 @@
<?php
namespace Controls;
function attributes_to_string(array $attributes) {
/**
* @param array<string, mixed> $attributes
*/
function attributes_to_string(array $attributes): string {
$rv = [];
foreach ($attributes as $k => $v) {
@@ -21,21 +24,27 @@
return hidden_tag("op", strtolower(get_class($plugin) . \PluginHost::PUBLIC_METHOD_DELIMITER . $method));
} */
function public_method_tags(\Plugin $plugin, string $method) {
function public_method_tags(\Plugin $plugin, string $method): string {
return hidden_tag("op", strtolower(get_class($plugin) . \PluginHost::PUBLIC_METHOD_DELIMITER . $method));
}
function pluginhandler_tags(\Plugin $plugin, string $method) {
function pluginhandler_tags(\Plugin $plugin, string $method): string {
return hidden_tag("op", "pluginhandler") .
hidden_tag("plugin", strtolower(get_class($plugin))) .
hidden_tag("method", $method);
}
function button_tag(string $value, string $type, array $attributes = []) {
/**
* @param array<string, mixed> $attributes
*/
function button_tag(string $value, string $type, array $attributes = []): string {
return "<button dojoType=\"dijit.form.Button\" ".attributes_to_string($attributes)." type=\"$type\">$value</button>";
}
function input_tag(string $name, string $value, string $type = "text", array $attributes = [], string $id = "") {
/**
* @param array<string, mixed> $attributes
*/
function input_tag(string $name, string $value, string $type = "text", array $attributes = [], string $id = ""): string {
$attributes_str = attributes_to_string($attributes);
$dojo_type = strpos($attributes_str, "dojoType") === false ? "dojoType='dijit.form.TextBox'" : "";
@@ -43,23 +52,40 @@
type=\"$type\" value=\"".htmlspecialchars($value)."\">";
}
function number_spinner_tag(string $name, string $value, array $attributes = [], string $id = "") {
/**
* @param array<string, mixed> $attributes
*/
function number_spinner_tag(string $name, string $value, array $attributes = [], string $id = ""): string {
return input_tag($name, $value, "text", array_merge(["dojoType" => "dijit.form.NumberSpinner"], $attributes), $id);
}
function submit_tag(string $value, array $attributes = []) {
/**
* @param array<string, mixed> $attributes
*/
function submit_tag(string $value, array $attributes = []): string {
return button_tag($value, "submit", array_merge(["class" => "alt-primary"], $attributes));
}
function cancel_dialog_tag(string $value, array $attributes = []) {
/**
* @param array<string, mixed> $attributes
*/
function cancel_dialog_tag(string $value, array $attributes = []): string {
return button_tag($value, "", array_merge(["onclick" => "App.dialogOf(this).hide()"], $attributes));
}
function icon(string $icon, array $attributes = []) {
/**
* @param array<string, mixed> $attributes
*/
function icon(string $icon, array $attributes = []): string {
return "<i class=\"material-icons\" ".attributes_to_string($attributes).">$icon</i>";
}
function select_tag(string $name, $value, array $values, array $attributes = [], string $id = "") {
/**
* @param mixed $value
* @param array<int|string, string> $values
* @param array<string, mixed> $attributes
*/
function select_tag(string $name, $value, array $values, array $attributes = [], string $id = ""): string {
$attributes_str = attributes_to_string($attributes);
$dojo_type = strpos($attributes_str, "dojoType") === false ? "dojoType='fox.form.Select'" : "";
@@ -83,7 +109,12 @@
return select_tag($name, $value, $values, $attributes, $id);
}*/
function select_hash(string $name, $value, array $values, array $attributes = [], string $id = "") {
/**
* @param mixed $value
* @param array<int|string, string> $values
* @param array<string, mixed> $attributes
*/
function select_hash(string $name, $value, array $values, array $attributes = [], string $id = ""): string {
$attributes_str = attributes_to_string($attributes);
$dojo_type = strpos($attributes_str, "dojoType") === false ? "dojoType='fox.form.Select'" : "";
@@ -93,7 +124,7 @@
foreach ($values as $k => $v) {
$is_sel = ($k == $value) ? "selected=\"selected\"" : "";
$rv .= "<option value=\"".htmlspecialchars($k)."\" $is_sel>".htmlspecialchars($v)."</option>";
$rv .= "<option value=\"".htmlspecialchars("$k")."\" $is_sel>".htmlspecialchars($v)."</option>";
}
$rv .= "</select>";
@@ -101,13 +132,19 @@
return $rv;
}
function hidden_tag(string $name, string $value, array $attributes = []) {
/**
* @param array<string, mixed> $attributes
*/
function hidden_tag(string $name, string $value, array $attributes = []): string {
return "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\"
".attributes_to_string($attributes)." name=\"".htmlspecialchars($name)."\"
value=\"".htmlspecialchars($value)."\">";
}
function checkbox_tag(string $name, bool $checked = false, string $value = "", array $attributes = [], string $id = "") {
/**
* @param array<string, mixed> $attributes
*/
function checkbox_tag(string $name, bool $checked = false, string $value = "", array $attributes = [], string $id = ""): string {
$is_checked = $checked ? "checked" : "";
$value_str = $value ? "value=\"".htmlspecialchars($value)."\"" : "";
@@ -115,8 +152,11 @@
$value_str $is_checked ".attributes_to_string($attributes)." id=\"".htmlspecialchars($id)."\">";
}
/**
* @param array<string, mixed> $attributes
*/
function select_feeds_cats(string $name, int $default_id = null, array $attributes = [],
bool $include_all_cats = true, string $root_id = null, int $nest_level = 0, string $id = "") {
bool $include_all_cats = true, string $root_id = null, int $nest_level = 0, string $id = ""): string {
$ret = "";

View File

@@ -1,7 +1,9 @@
<?php
function stylesheet_tag($filename, $attributes = []) {
/**
* @param array<string, mixed> $attributes
*/
function stylesheet_tag(string $filename, array $attributes = []): string {
$attributes_str = \Controls\attributes_to_string(
array_merge(
@@ -16,7 +18,10 @@ function stylesheet_tag($filename, $attributes = []) {
return "<link $attributes_str/>\n";
}
function javascript_tag($filename, $attributes = []) {
/**
* @param array<string, mixed> $attributes
*/
function javascript_tag(string $filename, array $attributes = []): string {
$attributes_str = \Controls\attributes_to_string(
array_merge(
[
@@ -29,26 +34,26 @@ function javascript_tag($filename, $attributes = []) {
return "<script $attributes_str></script>\n";
}
function format_warning($msg, $id = "") {
function format_warning(string $msg, string $id = ""): string {
return "<div class=\"alert\" id=\"$id\">$msg</div>";
}
function format_notice($msg, $id = "") {
function format_notice(string $msg, string $id = ""): string {
return "<div class=\"alert alert-info\" id=\"$id\">$msg</div>";
}
function format_error($msg, $id = "") {
function format_error(string $msg, string $id = ""): string {
return "<div class=\"alert alert-danger\" id=\"$id\">$msg</div>";
}
function print_notice($msg) {
function print_notice(string $msg): string {
return print format_notice($msg);
}
function print_warning($msg) {
function print_warning(string $msg): string {
return print format_warning($msg);
}
function print_error($msg) {
function print_error(string $msg): string {
return print format_error($msg);
}

View File

@@ -1,5 +1,8 @@
<?php
function format_backtrace($trace) {
/**
* @param array<int, array<string, mixed>> $trace
*/
function format_backtrace($trace): string {
$rv = "";
$idx = 1;
@@ -39,7 +42,7 @@ function format_backtrace($trace) {
return $rv;
}
function ttrss_error_handler($errno, $errstr, $file, $line) {
function ttrss_error_handler(int $errno, string $errstr, string $file, int $line): bool {
/*if (version_compare(PHP_VERSION, '8.0.0', '<')) {
if (error_reporting() == 0 || !$errno) return false;
} else {
@@ -59,7 +62,7 @@ function ttrss_error_handler($errno, $errstr, $file, $line) {
return false;
}
function ttrss_fatal_handler() {
function ttrss_fatal_handler(): bool {
$error = error_get_last();
if ($error !== NULL) {

View File

@@ -36,15 +36,24 @@
define('SUBSTRING_FOR_DATE', 'SUBSTRING');
}
/**
* @return bool|int|null|string
*/
function get_pref(string $pref_name, int $owner_uid = null) {
return Prefs::get($pref_name, $owner_uid ? $owner_uid : $_SESSION["uid"], $_SESSION["profile"] ?? null);
}
function set_pref(string $pref_name, $value, int $owner_uid = null, bool $strip_tags = true) {
/**
* @param bool|int|string $value
*/
function set_pref(string $pref_name, $value, int $owner_uid = null, bool $strip_tags = true): bool {
return Prefs::set($pref_name, $value, $owner_uid ? $owner_uid : $_SESSION["uid"], $_SESSION["profile"] ?? null, $strip_tags);
}
function get_translations() {
/**
* @return array<string, string>
*/
function get_translations(): array {
$t = array(
"auto" => __("Detect automatically"),
"ar_SA" => "العربيّة (Arabic)",
@@ -81,7 +90,7 @@
require_once "lib/gettext/gettext.inc.php";
function startup_gettext() {
function startup_gettext(): void {
$selected_locale = "";
@@ -161,70 +170,105 @@
/* compat shims */
/** function is @deprecated by Config::get_version() */
/**
* @deprecated by Config::get_version()
*
* @return array<string, mixed>|string
*/
function get_version() {
return Config::get_version();
}
/** function is @deprecated by Config::get_schema_version() */
function get_schema_version() {
function get_schema_version(): int {
return Config::get_schema_version();
}
/** function is @deprecated by Debug::log() */
function _debug($msg) {
function _debug(string $msg): void {
Debug::log($msg);
}
/** function is @deprecated */
function getFeedUnread($feed, $is_cat = false) {
function getFeedUnread(int $feed, bool $is_cat = false): int {
return Feeds::_get_counters($feed, $is_cat, true, $_SESSION["uid"]);
}
/** function is @deprecated by Sanitizer::sanitize() */
function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
/**
* @deprecated by Sanitizer::sanitize()
*
* @param array<int, string>|null $highlight_words Words to highlight in the HTML output.
*
* @return false|string The HTML, or false if an error occurred.
*/
function sanitize(string $str, bool $force_remove_images = false, int $owner = null, string $site_url = null, array $highlight_words = null, int $article_id = null) {
return Sanitizer::sanitize($str, $force_remove_images, $owner, $site_url, $highlight_words, $article_id);
}
/** function is @deprecated by UrlHelper::fetch() */
/**
* @deprecated by UrlHelper::fetch()
*
* @param array<string, bool|int|string>|string $params
* @return bool|string false if something went wrong, otherwise string contents
*/
function fetch_file_contents($params) {
return UrlHelper::fetch($params);
}
/** function is @deprecated by UrlHelper::rewrite_relative() */
/**
* @deprecated by UrlHelper::rewrite_relative()
*
* Converts a (possibly) relative URL to a absolute one, using provided base URL.
* Provides some exceptions for additional schemes like data: if called with owning element/attribute.
*
* @param string $base_url Base URL (i.e. from where the document is)
* @param string $rel_url Possibly relative URL in the document
*
* @return string Absolute URL
*/
function rewrite_relative_url($base_url, $rel_url) {
return UrlHelper::rewrite_relative($base_url, $rel_url);
}
/** function is @deprecated by UrlHelper::validate() */
function validate_url($url) {
/**
* @deprecated by UrlHelper::validate()
*
* @return bool|string false if something went wrong, otherwise the URL string
*/
function validate_url(string $url) {
return UrlHelper::validate($url);
}
/** function is @deprecated by UserHelper::authenticate() */
function authenticate_user($login, $password, $check_only = false, $service = false) {
function authenticate_user(string $login = null, string $password = null, bool $check_only = false, string $service = null): bool {
return UserHelper::authenticate($login, $password, $check_only, $service);
}
/** function is @deprecated by TimeHelper::smart_date_time() */
function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
function smart_date_time(int $timestamp, int $tz_offset = 0, int $owner_uid = null, bool $eta_min = false): string {
return TimeHelper::smart_date_time($timestamp, $tz_offset, $owner_uid, $eta_min);
}
/** function is @deprecated by TimeHelper::make_local_datetime() */
function make_local_datetime($timestamp, $long, $owner_uid = false, $no_smart_dt = false, $eta_min = false) {
function make_local_datetime(string $timestamp, bool $long, int $owner_uid = null, bool $no_smart_dt = false, bool $eta_min = false): string {
return TimeHelper::make_local_datetime($timestamp, $long, $owner_uid, $no_smart_dt, $eta_min);
}
// this returns Config::SELF_URL_PATH sans ending slash
/** function is @deprecated by Config::get_self_url() */
function get_self_url_prefix() {
function get_self_url_prefix(): string {
return Config::get_self_url();
}
/* end compat shims */
// this is used for user http parameters unless HTML code is actually needed
/**
* This is used for user http parameters unless HTML code is actually needed.
*
* @param mixed $param
*
* @return mixed
*/
function clean($param) {
if (is_array($param)) {
return array_map("trim", array_map("strip_tags", $param));
@@ -243,7 +287,7 @@
}
}
function make_password($length = 12) {
function make_password(int $length = 12): string {
$password = "";
$possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ*%+^";
@@ -268,11 +312,11 @@
return $password;
}
function validate_csrf($csrf_token) {
function validate_csrf(?string $csrf_token): bool {
return isset($csrf_token) && hash_equals($_SESSION['csrf_token'] ?? "", $csrf_token);
}
function truncate_string($str, $max_len, $suffix = '&hellip;') {
function truncate_string(string $str, int $max_len, string $suffix = '&hellip;'): string {
if (mb_strlen($str, "utf-8") > $max_len) {
return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
} else {
@@ -280,7 +324,7 @@
}
}
function mb_substr_replace($original, $replacement, $position, $length) {
function mb_substr_replace(string $original, string $replacement, int $position, int $length): string {
$startString = mb_substr($original, 0, $position, "UTF-8");
$endString = mb_substr($original, $position + $length, mb_strlen($original), "UTF-8");
@@ -289,7 +333,7 @@
return $out;
}
function truncate_middle($str, $max_len, $suffix = '&hellip;') {
function truncate_middle(string $str, int $max_len, string $suffix = '&hellip;'): string {
if (mb_strlen($str) > $max_len) {
return mb_substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
} else {
@@ -297,15 +341,15 @@
}
}
function sql_bool_to_bool($s) {
function sql_bool_to_bool(string $s): bool {
return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
}
function bool_to_sql_bool($s) {
function bool_to_sql_bool(bool $s): int {
return $s ? 1 : 0;
}
function file_is_locked($filename) {
function file_is_locked(string $filename): bool {
if (file_exists(Config::get(Config::LOCK_DIRECTORY) . "/$filename")) {
if (function_exists('flock')) {
$fp = @fopen(Config::get(Config::LOCK_DIRECTORY) . "/$filename", "r");
@@ -327,7 +371,10 @@
}
}
function make_lockfile($filename) {
/**
* @return resource|false A file pointer resource on success, or false on error.
*/
function make_lockfile(string $filename) {
$fp = fopen(Config::get(Config::LOCK_DIRECTORY) . "/$filename", "w");
if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
@@ -351,38 +398,44 @@
}
}
function checkbox_to_sql_bool($val) {
/**
* @param mixed $val
*/
function checkbox_to_sql_bool($val): int {
return ($val == "on") ? 1 : 0;
}
function uniqid_short() {
function uniqid_short(): string {
return uniqid(base_convert((string)rand(), 10, 36));
}
function T_sprintf() {
function T_sprintf(): string {
$args = func_get_args();
return vsprintf(__(array_shift($args)), $args);
}
function T_nsprintf() {
function T_nsprintf(): string {
$args = func_get_args();
return vsprintf(_ngettext(array_shift($args), array_shift($args), array_shift($args)), $args);
}
function init_plugins() {
function init_plugins(): bool {
PluginHost::getInstance()->load(Config::get(Config::PLUGINS), PluginHost::KIND_ALL);
return true;
}
if (!function_exists('gzdecode')) {
function gzdecode($string) { // no support for 2nd argument
/**
* @return false|string The decoded string or false if an error occurred.
*/
function gzdecode(string $string) { // no support for 2nd argument
return file_get_contents('compress.zlib://data:who/cares;base64,'.
base64_encode($string));
}
}
function get_random_bytes($length) {
function get_random_bytes(int $length): string {
if (function_exists('random_bytes')) {
return random_bytes($length);
} else if (function_exists('openssl_random_pseudo_bytes')) {
@@ -397,7 +450,7 @@
}
}
function read_stdin() {
function read_stdin(): ?string {
$fp = fopen("php://stdin", "r");
if ($fp) {
@@ -409,11 +462,19 @@
return null;
}
function implements_interface($class, $interface) {
return in_array($interface, class_implements($class));
/**
* @param object|string $class
*/
function implements_interface($class, string $interface): bool {
$class_implemented_interfaces = class_implements($class);
if ($class_implemented_interfaces) {
return in_array($interface, $class_implemented_interfaces);
}
return false;
}
function get_theme_path($theme) {
function get_theme_path(string $theme): string {
$check = "themes/$theme";
if (file_exists($check)) return $check;
@@ -423,15 +484,18 @@
return "";
}
function theme_exists($theme) {
function theme_exists(string $theme): bool {
return file_exists("themes/$theme") || file_exists("themes.local/$theme");
}
function arr_qmarks($arr) {
/**
* @param array<int, mixed> $arr
*/
function arr_qmarks(array $arr): string {
return str_repeat('?,', count($arr) - 1) . '?';
}
function get_scripts_timestamp() {
function get_scripts_timestamp(): int {
$files = glob("js/*.js");
$ts = 0;

View File

@@ -31,7 +31,7 @@ require_once "autoload.php";
ini_get("session.cookie_secure"),
ini_get("session.cookie_httponly"));
function validate_session() {
function validate_session(): bool {
if (\Config::get(\Config::SINGLE_USER_MODE)) return true;
$pdo = \Db::pdo();
@@ -58,11 +58,11 @@ require_once "autoload.php";
return true;
}
function ttrss_open ($s, $n) {
function ttrss_open(string $savePath, string $sessionName): bool {
return true;
}
function ttrss_read ($id){
function ttrss_read(string $id): string {
global $session_expire;
$sth = \Db::pdo()->prepare("SELECT data FROM ttrss_sessions WHERE id=?");
@@ -84,7 +84,7 @@ require_once "autoload.php";
}
function ttrss_write ($id, $data) {
function ttrss_write(string $id, string $data): bool {
global $session_expire;
$data = base64_encode($data);
@@ -105,18 +105,18 @@ require_once "autoload.php";
return true;
}
function ttrss_close () {
function ttrss_close(): bool {
return true;
}
function ttrss_destroy($id) {
function ttrss_destroy(string $id): bool {
$sth = \Db::pdo()->prepare("DELETE FROM ttrss_sessions WHERE id = ?");
$sth->execute([$id]);
return true;
}
function ttrss_gc ($expire) {
function ttrss_gc(int $lifetime): bool {
\Db::pdo()->query("DELETE FROM ttrss_sessions WHERE expire < " . time());
return true;
@@ -126,7 +126,10 @@ require_once "autoload.php";
session_set_save_handler('\Sessions\ttrss_open',
'\Sessions\ttrss_close', '\Sessions\ttrss_read',
'\Sessions\ttrss_write', '\Sessions\ttrss_destroy',
'\Sessions\ttrss_gc');
'\Sessions\ttrss_gc'); // @phpstan-ignore-line
// PHPStan complains about '\Sessions\ttrss_gc' if its $lifetime param isn't marked as string,
// but the docs say it's an int. If it is actually a string it'll get coerced to an int.
register_shutdown_function('session_write_close');
if (!defined('NO_SESSION_AUTOSTART')) {

View File

@@ -820,6 +820,10 @@ const App = {
App.updateRuntimeInfo();
}, 60 * 1000)
if (App.getInitParam("safe_mode") && this.isPrefs()) {
CommonDialogs.safeModeWarning();
}
console.log("second stage ok");
},

View File

@@ -11,6 +11,21 @@ const CommonDialogs = {
const dialog = dijit.byId("infoBox");
if (dialog) dialog.hide();
},
safeModeWarning: function() {
const dialog = new fox.SingleUseDialog({
title: __("Safe mode"),
content: `<div class='alert alert-info'>
${__('Tiny Tiny RSS is running in safe mode. All themes and plugins are disabled. You will need to log out and back in to disable it.')}
</div>
<footer class='text-center'>
<button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
${__('Close this window')}
</button>
</footer>`
});
dialog.show();
},
subscribeToFeed: function() {
xhr.json("backend.php",
{op: "feeds", method: "subscribeToFeed"},

View File

@@ -278,19 +278,8 @@ const Feeds = {
}
if (App.getInitParam("safe_mode")) {
const dialog = new fox.SingleUseDialog({
title: __("Safe mode"),
content: `<div class='alert alert-info'>
${__('Tiny Tiny RSS is running in safe mode. All themes and plugins are disabled. You will need to log out and back in to disable it.')}
</div>
<footer class='text-center'>
<button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
${__('Close this window')}
</button>
</footer>`
});
dialog.show();
/* global CommonDialogs */
CommonDialogs.safeModeWarning();
}
// bw_limit disables timeout() so we request initial counters separately

View File

@@ -363,8 +363,15 @@ const Helpers = {
xhr.json("backend.php", {op: "pref-prefs", method: "getPluginsList"}, (reply) => {
this._list_of_plugins = reply;
this.render_contents();
}, (e) => {
this.render_error(e);
});
},
render_error: function(e) {
const container = document.querySelector(".prefs-plugin-list");
container.innerHTML = `<li class='text-error'>${__("Error while loading plugins list: %s.").replace("%s", e)}</li>`;
},
render_contents: function() {
const container = document.querySelector(".prefs-plugin-list");

View File

@@ -13,7 +13,7 @@ The text of the AFL and BSD licenses is reproduced below.
The "New" BSD License:
**********************
Copyright (c) 2005-2016, The JS Foundation
Copyright (c) 2005-2018, The JS Foundation
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@@ -4,6 +4,10 @@
Toolkit and provides a framework for building additional widgets as well as a full set of rich user interface widgets
including form, layout and data-aware items.
While still being maintained, new development is primarily focused on modern Dojo.
Checkout the [Dojo framework website](https://dojo.io/) or if you want a more detailed technical status and overview, checkout the [`Dojo roadmap`](https://dojo.io/community/).
## Installing
Installation instructions are available at [dojotoolkit.org/download][download].
@@ -20,7 +24,7 @@ If you are starting out with Dojo and Dijit, the following resources are availab
## License and Copyright
The Dojo Toolkit (including this package) is dual licensed under BSD 3-Clause and AFL. For more information on the
license please see the [License Information][]. The Dojo Toolkit is Copyright (c) 2005-2016, The JS Foundation. All
license please see the [License Information][]. The Dojo Toolkit is Copyright (c) 2005-2018, The JS Foundation. All
rights reserved.
[core]: https://github.com/dojo/dojo

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/_editor/nls/FontChoice",{root:({fontSize:"Size",fontName:"Font",formatBlock:"Format",serif:"serif","sans-serif":"sans-serif",monospace:"monospace",cursive:"cursive",fantasy:"fantasy",noFormat:"None",p:"Paragraph",h1:"Heading",h2:"Subheading",h3:"Sub-subheading",pre:"Pre-formatted",1:"xx-small",2:"x-small",3:"small",4:"medium",5:"large",6:"x-large",7:"xx-large"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true});
define("dijit/_editor/nls/FontChoice",{root:({fontSize:"Size",fontName:"Font",formatBlock:"Format",serif:"serif","sans-serif":"sans-serif",monospace:"monospace",cursive:"cursive",fantasy:"fantasy",noFormat:"None",p:"Paragraph",h1:"Heading",h2:"Subheading",h3:"Sub-subheading",pre:"Pre-formatted",1:"xx-small",2:"x-small",3:"small",4:"medium",5:"large",6:"x-large",7:"xx-large"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true,"al":true});

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/_editor/nls/LinkDialog",{root:({createLinkTitle:"Link Properties",insertImageTitle:"Image Properties",url:"URL:",text:"Description:",target:"Target:",set:"Set",currentWindow:"Current Window",parentWindow:"Parent Window",topWindow:"Topmost Window",newWindow:"New Window"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true});
define("dijit/_editor/nls/LinkDialog",{root:({createLinkTitle:"Link Properties",insertImageTitle:"Image Properties",url:"URL:",text:"Description:",target:"Target:",set:"Set",currentWindow:"Current Window",parentWindow:"Parent Window",topWindow:"Topmost Window",newWindow:"New Window"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true,"al":true});

View File

@@ -0,0 +1,2 @@
//>>built
define("dijit/_editor/nls/al/FontChoice",{fontSize:"Madhësia",fontName:"Shkrimi",formatBlock:"Formati",serif:"serif","sans-serif":"sans-serif",monospace:"monospace",cursive:"cursive",fantasy:"fantasy",noFormat:"Asnjë",p:"Paragraf",h1:"Titull",h2:"Nëntitull",h3:"Nëntitull i Nëntitullit",pre:"I para-formatuar",1:"i stër stër vogel",2:"i stërvogel",3:"vogël",4:"mesatar",5:"madh",6:"shumë i madh",7:"i stërmadh"});

View File

@@ -0,0 +1,2 @@
//>>built
define("dijit/_editor/nls/al/LinkDialog",{createLinkTitle:"Cilesitë e lidhjes",insertImageTitle:"Cilesitë e Imazhit",url:"URL:",text:"Përshkrim:",target:"Objektivi:",set:"Vendos",currentWindow:"Dritarja Aktuale",parentWindow:"Dritarja prind",topWindow:"Dritarja e sipërme",newWindow:"Dritare e re"});

View File

@@ -0,0 +1,2 @@
//>>built
define("dijit/_editor/nls/al/commands",{"bold":"Bold","copy":"Kopjo","cut":"Prit","delete":"Fshij","indent":"Kthehu","insertHorizontalRule":"Rregull horizontale","insertOrderedList":"Lista e numeruar","insertUnorderedList":"Lista me shënja","italic":"I pjerrët","justifyCenter":"Qënderzo","justifyFull":"Justifiko","justifyLeft":"Rradhit nga e majtë","justifyRight":"Rradhit nga e djathta","outdent":"Outdent","paste":"Ngjit","redo":"Rikthe","removeFormat":"Hiq formatin","selectAll":"Zgjidh të gjitha","strikethrough":"Përplasje","subscript":"Subscript","superscript":"Superscript","underline":"Underline","undo":"Anulo","unlink":"Hiq lidhje","createLink":"Krijo lidhje","toggleDir":"Aktivizo / çaktivizo drejtimin","insertImage":"Fut imazhin","insertTable":"Fut / Ndrysho tabelën","toggleTableBorder":"Aktivizo / çaktivizo kufirin e tabelës","deleteTable":"Fshi tabelën","tableProp":"Properties tabela","htmlToggle":"Burimi HTML","foreColor":"Ngjyra e përparme","hiliteColor":"Ngjyra e sfondit","plainFormatBlock":"Stili i paragrafit","formatBlock":"Stili i paragrafit","fontSize":"Madhësia e shkronjave","fontName":"Emri i fontit","tabIndent":"Tabelë e kthimit","fullScreen":"Aktivizo / çaktivizo ekranin e plotë","viewSource":"Shiko burimin HTML","print":"Shtyp","newPage":"Faqja e re","systemShortcut":"Veprimi '${0}' është i disponueshëm vetëm në shfletues përmes një çelësi. Përdorni ${1}. ","CtrlKey":"Ctrl+${0}","appleKey":"⌘${0}"});

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/_editor/nls/commands",{root:({"bold":"Bold","copy":"Copy","cut":"Cut","delete":"Delete","indent":"Indent","insertHorizontalRule":"Horizontal Rule","insertOrderedList":"Numbered List","insertUnorderedList":"Bullet List","italic":"Italic","justifyCenter":"Align Center","justifyFull":"Justify","justifyLeft":"Align Left","justifyRight":"Align Right","outdent":"Outdent","paste":"Paste","redo":"Redo","removeFormat":"Remove Format","selectAll":"Select All","strikethrough":"Strikethrough","subscript":"Subscript","superscript":"Superscript","underline":"Underline","undo":"Undo","unlink":"Remove Link","createLink":"Create Link","toggleDir":"Toggle Direction","insertImage":"Insert Image","insertTable":"Insert/Edit Table","toggleTableBorder":"Toggle Table Border","deleteTable":"Delete Table","tableProp":"Table Property","htmlToggle":"HTML Source","foreColor":"Foreground Color","hiliteColor":"Background Color","plainFormatBlock":"Paragraph Style","formatBlock":"Paragraph Style","fontSize":"Font Size","fontName":"Font Name","tabIndent":"Tab Indent","fullScreen":"Toggle Full Screen","viewSource":"View HTML Source","print":"Print","newPage":"New Page","systemShortcut":"The \"${0}\" action is only available in your browser using a keyboard shortcut. Use ${1}.","ctrlKey":"ctrl+${0}","appleKey":"⌘${0}"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true});
define("dijit/_editor/nls/commands",{root:({"bold":"Bold","copy":"Copy","cut":"Cut","delete":"Delete","indent":"Indent","insertHorizontalRule":"Horizontal Rule","insertOrderedList":"Numbered List","insertUnorderedList":"Bullet List","italic":"Italic","justifyCenter":"Align Center","justifyFull":"Justify","justifyLeft":"Align Left","justifyRight":"Align Right","outdent":"Outdent","paste":"Paste","redo":"Redo","removeFormat":"Remove Format","selectAll":"Select All","strikethrough":"Strikethrough","subscript":"Subscript","superscript":"Superscript","underline":"Underline","undo":"Undo","unlink":"Remove Link","createLink":"Create Link","toggleDir":"Toggle Direction","insertImage":"Insert Image","insertTable":"Insert/Edit Table","toggleTableBorder":"Toggle Table Border","deleteTable":"Delete Table","tableProp":"Table Property","htmlToggle":"HTML Source","foreColor":"Foreground Color","hiliteColor":"Background Color","plainFormatBlock":"Paragraph Style","formatBlock":"Paragraph Style","fontSize":"Font Size","fontName":"Font Name","tabIndent":"Tab Indent","fullScreen":"Toggle Full Screen","viewSource":"View HTML Source","print":"Print","newPage":"New Page","systemShortcut":"The \"${0}\" action is only available in your browser using a keyboard shortcut. Use ${1}.","ctrlKey":"ctrl+${0}","appleKey":"⌘${0}"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true,"al":true});

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/_editor/nls/it/FontChoice",({fontSize:"Dimensione",fontName:"Carattere",formatBlock:"Formato",serif:"serif","sans-serif":"sans-serif",monospace:"spaziatura fissa",cursive:"corsivo",fantasy:"fantasy",noFormat:"Nessuna",p:"Paragrafo",h1:"Intestazione",h2:"Sottointestazione",h3:"Sottointestazione secondaria",pre:"Preformattato",1:"piccolissimo",2:"molto piccolo",3:"piccolo",4:"medio",5:"grande",6:"molto grande",7:"grandissimo"}));
define("dijit/_editor/nls/it/FontChoice",{fontSize:"Dimensione",fontName:"Carattere",formatBlock:"Formato",serif:"serif","sans-serif":"sans-serif",monospace:"spaziatura fissa",cursive:"corsivo",fantasy:"fantasy",noFormat:"Nessuna",p:"Paragrafo",h1:"Intestazione",h2:"Sottointestazione",h3:"Sottointestazione secondaria",pre:"Preformattato",1:"piccolissimo",2:"molto piccolo",3:"piccolo",4:"medio",5:"grande",6:"molto grande",7:"grandissimo"});

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/_editor/nls/it/LinkDialog",({createLinkTitle:"Proprietà collegamento",insertImageTitle:"Proprietà immagine",url:"URL:",text:"Descrizione:",target:"Destinazione:",set:"Imposta",currentWindow:"Finestra corrente",parentWindow:"Finestra padre",topWindow:"Finestra superiore",newWindow:"Nuova finestra"}));
define("dijit/_editor/nls/it/LinkDialog",{createLinkTitle:"Proprietà collegamento",insertImageTitle:"Proprietà immagine",url:"URL:",text:"Descrizione:",target:"Destinazione:",set:"Imposta",currentWindow:"Finestra corrente",parentWindow:"Finestra padre",topWindow:"Finestra superiore",newWindow:"Nuova finestra"});

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/_editor/nls/it/commands",({"bold":"Grassetto","copy":"Copia","cut":"Taglia","delete":"Elimina","indent":"Rientro","insertHorizontalRule":"Righello orizzontale","insertOrderedList":"Elenco numerato","insertUnorderedList":"Elenco puntato","italic":"Corsivo","justifyCenter":"Allinea al centro","justifyFull":"Giustifica","justifyLeft":"Allinea a sinistra","justifyRight":"Allinea a destra","outdent":"Annulla rientro","paste":"Incolla","redo":"Ripristina","removeFormat":"Rimuovi formato","selectAll":"Seleziona tutto","strikethrough":"Barrato","subscript":"Pedice","superscript":"Apice","underline":"Sottolinea","undo":"Annulla","unlink":"Rimuovi collegamento","createLink":"Crea collegamento","toggleDir":"Attiva/Disattiva direzione","insertImage":"Inserisci immagine","insertTable":"Inserisci/Modifica tabella","toggleTableBorder":"Attiva/Disattiva bordo tabella","deleteTable":"Elimina tabella","tableProp":"Proprietà tabella","htmlToggle":"Origine HTML","foreColor":"Colore primo piano","hiliteColor":"Colore sfondo","plainFormatBlock":"Stile paragrafo","formatBlock":"Stile paragrafo","fontSize":"Dimensione carattere","fontName":"Nome carattere","tabIndent":"Rientro tabulazione","fullScreen":"Attiva/Disattiva schermo intero","viewSource":"Visualizza origine HTML","print":"Stampa","newPage":"Nuova pagina","systemShortcut":"La azione \"${0}\" è disponibile solo nel browser tramite un tasto di scelta rapida. Utilizzare ${1}.","ctrlKey":"ctrl+${0}","appleKey":"⌘${0}"}));
define("dijit/_editor/nls/it/commands",{"bold":"Grassetto","copy":"Copia","cut":"Taglia","delete":"Elimina","indent":"Rientro","insertHorizontalRule":"Righello orizzontale","insertOrderedList":"Elenco numerato","insertUnorderedList":"Elenco puntato","italic":"Corsivo","justifyCenter":"Allinea al centro","justifyFull":"Giustifica","justifyLeft":"Allinea a sinistra","justifyRight":"Allinea a destra","outdent":"Annulla rientro","paste":"Incolla","redo":"Ripristina","removeFormat":"Rimuovi formato","selectAll":"Seleziona tutto","strikethrough":"Barrato","subscript":"Pedice","superscript":"Apice","underline":"Sottolinea","undo":"Annulla","unlink":"Rimuovi collegamento","createLink":"Crea collegamento","toggleDir":"Attiva/Disattiva direzione","insertImage":"Inserisci immagine","insertTable":"Inserisci/Modifica tabella","toggleTableBorder":"Attiva/Disattiva bordo tabella","deleteTable":"Elimina tabella","tableProp":"Proprietà tabella","htmlToggle":"Origine HTML","foreColor":"Colore primo piano","hiliteColor":"Colore sfondo","plainFormatBlock":"Stile paragrafo","formatBlock":"Stile paragrafo","fontSize":"Dimensione carattere","fontName":"Nome carattere","tabIndent":"Rientro tabulazione","fullScreen":"Attiva/Disattiva schermo intero","viewSource":"Visualizza origine HTML","print":"Stampa","newPage":"Nuova pagina","systemShortcut":"La azione \"${0}\" è disponibile solo nel browser tramite un tasto di scelta rapida. Utilizzare ${1}.","ctrlKey":"ctrl+${0}","appleKey":"⌘${0}"});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -15,7 +15,7 @@
"url": "https://github.com/dojo/dijit.git"
},
"dependencies": {
"dojo": "1.14.2"
"dojo": "1.16.4"
},
"devDependencies": {
}

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/form/_SearchMixin",["dojo/_base/declare","dojo/keys","dojo/_base/lang","dojo/query","dojo/string","dojo/when","../registry"],function(_1,_2,_3,_4,_5,_6,_7){return _1("dijit.form._SearchMixin",null,{pageSize:Infinity,store:null,fetchProperties:{},query:{},list:"",_setListAttr:function(_8){this._set("list",_8);},searchDelay:200,searchAttr:"name",queryExpr:"${0}*",ignoreCase:true,_patternToRegExp:function(_9){return new RegExp("^"+_9.replace(/(\\.)|(\*)|(\?)|\W/g,function(_a,_b,_c,_d){return _c?".*":_d?".":_b?_b:"\\"+_a;})+"$",this.ignoreCase?"mi":"m");},_abortQuery:function(){if(this.searchTimer){this.searchTimer=this.searchTimer.remove();}if(this._queryDeferHandle){this._queryDeferHandle=this._queryDeferHandle.remove();}if(this._fetchHandle){if(this._fetchHandle.abort){this._cancelingQuery=true;this._fetchHandle.abort();this._cancelingQuery=false;}if(this._fetchHandle.cancel){this._cancelingQuery=true;this._fetchHandle.cancel();this._cancelingQuery=false;}this._fetchHandle=null;}},_processInput:function(_e){if(this.disabled||this.readOnly){return;}var _f=_e.charOrCode;this._prev_key_backspace=false;if(_f==_2.DELETE||_f==_2.BACKSPACE){this._prev_key_backspace=true;this._maskValidSubsetError=true;}if(!this.store){this.onSearch();}else{this.searchTimer=this.defer("_startSearchFromInput",1);}},onSearch:function(){},_startSearchFromInput:function(){this._startSearch(this.focusNode.value);},_startSearch:function(_10){this._abortQuery();var _11=this,_4=_3.clone(this.query),_12={start:0,count:this.pageSize,queryOptions:{ignoreCase:this.ignoreCase,deep:true}},qs=_5.substitute(this.queryExpr,[_10.replace(/([\\\*\?])/g,"\\$1")]),q,_13=function(){var _14=_11._fetchHandle=_11.store.query(_4,_12);if(_11.disabled||_11.readOnly||(q!==_11._lastQuery)){return;}_6(_14,function(res){_11._fetchHandle=null;if(!_11.disabled&&!_11.readOnly&&(q===_11._lastQuery)){_6(_14.total,function(_15){res.total=_15;var _16=_11.pageSize;if(isNaN(_16)||_16>res.total){_16=res.total;}res.nextPage=function(_17){_12.direction=_17=_17!==false;_12.count=_16;if(_17){_12.start+=res.length;if(_12.start>=res.total){_12.count=0;}}else{_12.start-=_16;if(_12.start<0){_12.count=Math.max(_16+_12.start,0);_12.start=0;}}if(_12.count<=0){res.length=0;_11.onSearch(res,_4,_12);}else{_13();}};_11.onSearch(res,_4,_12);});}},function(err){_11._fetchHandle=null;if(!_11._cancelingQuery){console.error(_11.declaredClass+" "+err.toString());}});};_3.mixin(_12,this.fetchProperties);if(this.store._oldAPI){q=qs;}else{q=this._patternToRegExp(qs);q.toString=function(){return qs;};}this._lastQuery=_4[this.searchAttr]=q;this._queryDeferHandle=this.defer(_13,this.searchDelay);},constructor:function(){this.query={};this.fetchProperties={};},postMixInProperties:function(){if(!this.store){var _18=this.list;if(_18){this.store=_7.byId(_18);}}this.inherited(arguments);}});});
define("dijit/form/_SearchMixin",["dojo/_base/declare","dojo/keys","dojo/_base/lang","dojo/query","dojo/string","dojo/when","../registry"],function(_1,_2,_3,_4,_5,_6,_7){return _1("dijit.form._SearchMixin",null,{pageSize:Infinity,store:null,fetchProperties:{},query:{},list:"",_setListAttr:function(_8){this._set("list",_8);},searchDelay:200,searchAttr:"name",queryExpr:"${0}*",ignoreCase:true,_patternToRegExp:function(_9){return new RegExp("^"+_9.replace(/(\\.)|(\*)|(\?)|\W/g,function(_a,_b,_c,_d){return _c?".*":_d?".":_b?_b:"\\"+_a;})+"$",this.ignoreCase?"mi":"m");},_abortQuery:function(){if(this.searchTimer){this.searchTimer=this.searchTimer.remove();}if(this._queryDeferHandle){this._queryDeferHandle=this._queryDeferHandle.remove();}if(this._fetchHandle){if(this._fetchHandle.abort){this._cancelingQuery=true;this._fetchHandle.abort();this._cancelingQuery=false;}if(this._fetchHandle.cancel){this._cancelingQuery=true;this._fetchHandle.cancel();this._cancelingQuery=false;}this._fetchHandle=null;}},_processInput:function(_e){if(this.disabled||this.readOnly){return;}var _f=_e.charOrCode;this._prev_key_backspace=false;if(_f===_2.DELETE||_f===_2.BACKSPACE){this._prev_key_backspace=true;this._maskValidSubsetError=true;}if(!this.store){this.onSearch();}else{this.searchTimer=this.defer("_startSearchFromInput",1);}},onSearch:function(){},_startSearchFromInput:function(){this._startSearch(this.focusNode.value);},_startSearch:function(_10){this._abortQuery();var _11=this,_4=_3.clone(this.query),_12={start:0,count:this.pageSize,queryOptions:{ignoreCase:this.ignoreCase,deep:true}},qs=_5.substitute(this.queryExpr,[_10.replace(/([\\\*\?])/g,"\\$1")]),q,_13=function(){var _14=_11._fetchHandle=_11.store.query(_4,_12);if(_11.disabled||_11.readOnly||(q!==_11._lastQuery)){return;}_6(_14,function(res){_11._fetchHandle=null;if(!_11.disabled&&!_11.readOnly&&(q===_11._lastQuery)){_6(_14.total,function(_15){res.total=_15;var _16=_11.pageSize;if(isNaN(_16)||_16>res.total){_16=res.total;}res.nextPage=function(_17){_12.direction=_17=_17!==false;_12.count=_16;if(_17){_12.start+=res.length;if(_12.start>=res.total){_12.count=0;}}else{_12.start-=_16;if(_12.start<0){_12.count=Math.max(_16+_12.start,0);_12.start=0;}}if(_12.count<=0){res.length=0;_11.onSearch(res,_4,_12);}else{_13();}};_11.onSearch(res,_4,_12);});}},function(err){_11._fetchHandle=null;if(!_11._cancelingQuery){console.error(_11.declaredClass+" "+err.toString());}});};_3.mixin(_12,this.fetchProperties);if(this.store._oldAPI){q=qs;}else{q=this._patternToRegExp(qs);q.toString=function(){return qs;};}this._lastQuery=_4[this.searchAttr]=q;this._queryDeferHandle=this.defer(_13,this.searchDelay);},constructor:function(){this.query={};this.fetchProperties={};},postMixInProperties:function(){if(!this.store){var _18=this.list;if(_18){this.store=_7.byId(_18);}}this.inherited(arguments);}});});

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/form/nls/ComboBox",{root:({previousMessage:"Previous choices",nextMessage:"More choices"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true});
define("dijit/form/nls/ComboBox",{root:({previousMessage:"Previous choices",nextMessage:"More choices"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true,"al":true});

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/form/nls/Textarea",{root:({iframeEditTitle:"edit area",iframeFocusTitle:"edit area frame"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true});
define("dijit/form/nls/Textarea",{root:({iframeEditTitle:"edit area",iframeFocusTitle:"edit area frame"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true,"al":true});

View File

@@ -0,0 +1,2 @@
//>>built
define("dijit/form/nls/al/ComboBox",{previousMessage:"Zgjedhja e mëparshme",nextMessage:"Më tepër zgjedhje"});

View File

@@ -0,0 +1,2 @@
//>>built
define("dijit/form/nls/al/Textarea",{iframeEditTitle:"zona e editimit",iframeFocusTitle:"frame i zonës së editimit"});

View File

@@ -0,0 +1,2 @@
//>>built
define("dijit/form/nls/al/validate",{invalidMessage:"Vlera e vendosur nuk është e saktë",missingMessage:"Kjo vlerë është e detyruar",rangeMessage:"Kjo vlerë është jashtë rradhe"});

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/form/nls/it/ComboBox",({previousMessage:"Scelte precedenti",nextMessage:"Scelte successive"}));
define("dijit/form/nls/it/ComboBox",{previousMessage:"Scelte precedenti",nextMessage:"Scelte successive"});

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/form/nls/it/Textarea",({iframeEditTitle:"modifica area",iframeFocusTitle:"modifica frame area"}));
define({iframeEditTitle:"modifica area",iframeFocusTitle:"modifica frame area"});

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/form/nls/it/validate",({invalidMessage:"Il valore immesso non è valido.",missingMessage:"Questo valore è obbligatorio.",rangeMessage:"Questo valore è fuori dall'intervallo consentito."}));
define("dijit/form/nls/it/validate",{invalidMessage:"Il valore immesso non è valido.",missingMessage:"Questo valore è obbligatorio.",rangeMessage:"Questo valore è fuori dall'intervallo consentito."});

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/form/nls/validate",{root:({invalidMessage:"The value entered is not valid.",missingMessage:"This value is required.",rangeMessage:"This value is out of range."}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true});
define("dijit/form/nls/validate",{root:({invalidMessage:"The value entered is not valid.",missingMessage:"This value is required.",rangeMessage:"This value is out of range."}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true,"al":true});

View File

@@ -0,0 +1,2 @@
//>>built
define("dijit/nls/al/common",{buttonOk:"OK",buttonCancel:"Anullo",buttonSave:"Ruaj",itemClose:"Mbyll"});

View File

@@ -0,0 +1,2 @@
//>>built
define("dijit/nls/al/loading",{loadingState:"Duke u ngarkuar",errorState:"Një gabim ndodhi"});

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/nls/common",{root:({buttonOk:"OK",buttonCancel:"Cancel",buttonSave:"Save",itemClose:"Close"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true});
define("dijit/nls/common",{root:({buttonOk:"OK",buttonCancel:"Cancel",buttonSave:"Save",itemClose:"Close"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true,"al":true});

View File

@@ -1,2 +1,2 @@
//>>built
define("dijit/nls/loading",{root:({loadingState:"Loading...",errorState:"Sorry, an error occurred"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true});
define("dijit/nls/loading",{root:({loadingState:"Loading...",errorState:"Sorry, an error occurred"}),"bs":true,"mk":true,"sr":true,"zh":true,"zh-tw":true,"uk":true,"tr":true,"th":true,"sv":true,"sl":true,"sk":true,"ru":true,"ro":true,"pt":true,"pt-pt":true,"pl":true,"nl":true,"nb":true,"ko":true,"kk":true,"ja":true,"it":true,"id":true,"hu":true,"hr":true,"he":true,"fr":true,"fi":true,"eu":true,"es":true,"el":true,"de":true,"da":true,"cs":true,"ca":true,"bg":true,"az":true,"ar":true,"al":true});

View File

@@ -1,12 +1,12 @@
{
"name": "dijit",
"version": "1.14.2",
"version": "1.16.4",
"directories": {
"lib": "."
},
"main": "main",
"dependencies": {
"dojo": "1.14.2"
"dojo": "1.16.4"
},
"description": "Dijit provides a complete collection of user interface controls based on Dojo, giving you the power to create web applications that are highly optimized for usability, performance, internationalization, accessibility, but above all deliver an incredible user experience.",
"license" : "BSD-3-Clause OR AFL-2.1",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
dijit
dojo
dojox
dijit-themes
release
util
dojo-release-*

View File

@@ -7,7 +7,7 @@
# Dojo requires Java runtime to build. Further information on rebuilding Dojo
# is available here: http://dojotoolkit.org/reference-guide/build/index.html
VERSION=1.14.2
VERSION=1.16.4
# Download and extract dojo src code if it doesn't already exist
if [ ! -d "dojo" ]; then

View File

@@ -13,7 +13,7 @@ The text of the AFL and BSD licenses is reproduced below.
The "New" BSD License:
**********************
Copyright (c) 2005-2017, The JS Foundation
Copyright (c) 2005-2018, The JS Foundation
All rights reserved.
Redistribution and use in source and binary forms, with or without

Some files were not shown because too many files have changed in this diff Show More