From 4cc3374f9f6cdd6ea05d4d16942d0165d8b2ed90 Mon Sep 17 00:00:00 2001 From: wn_ Date: Wed, 10 Nov 2021 21:38:25 +0000 Subject: [PATCH 001/118] Initial go at PHPStan rule level 6. --- classes/config.php | 41 ++++++++++++++++++++++++++++++-------- classes/db.php | 9 ++++----- classes/errors.php | 6 +++++- classes/logger.php | 13 +++++++----- classes/logger/adapter.php | 4 ++-- classes/logger/sql.php | 2 +- classes/logger/stdout.php | 3 ++- classes/logger/syslog.php | 3 ++- classes/urlhelper.php | 40 ++++++++++++++++++++++++------------- phpstan.neon | 2 +- update.php | 4 ++-- update_daemon2.php | 14 ++++++------- 12 files changed, 93 insertions(+), 48 deletions(-) diff --git a/classes/config.php b/classes/config.php index 53a31b61c..9096a4953 100644 --- a/classes/config.php +++ b/classes/config.php @@ -231,9 +231,13 @@ class Config { Config::T_STRING ], ]; + /** @var Config|null */ private static $instance; + /** @var array> */ private $params = []; + + /** @var array */ 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 + */ static function get_version(bool $as_string = true) { return self::get_instance()->_get_version($as_string); } + /** + * @return array|string + */ private function _get_version(bool $as_string = true) { $root_dir = dirname(__DIR__); @@ -298,7 +308,10 @@ class Config { return $as_string ? $this->version["version"] : $this->version; } - static function get_version_from_git(string $dir) { + /** + * @return array + */ + static function get_version_from_git(string $dir): array { $descriptorspec = [ 1 => ["pipe", "w"], // STDOUT 2 => ["pipe", "w"], // STDERR @@ -364,6 +377,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 +391,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 +453,9 @@ class Config { /* sanity check stuff */ + /** + * @return array> A list of entries identifying tt-rss tables with bad config + */ private static function check_mysql_tables() { $pdo = Db::pdo(); @@ -447,7 +472,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 +646,11 @@ class Config { } } - private static function format_error($msg) { + private static function format_error(string $msg): string { return "
$msg
"; } - 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 +662,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()); } } diff --git a/classes/db.php b/classes/db.php index 7b669cf32..2cc89f5ba 100755 --- a/classes/db.php +++ b/classes/db.php @@ -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()"; } } diff --git a/classes/errors.php b/classes/errors.php index 3599c2639..31be558cf 100644 --- a/classes/errors.php +++ b/classes/errors.php @@ -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 $params + */ + static function to_json(string $code, array $params = []): string { return json_encode(["error" => ["code" => $code, "params" => $params]]); } } diff --git a/classes/logger.php b/classes/logger.php index 42ab4452c..ef6173a42 100755 --- a/classes/logger.php +++ b/classes/logger.php @@ -1,6 +1,9 @@ 'E_USER_DEPRECATED', 32767 => 'E_ALL']; - static function log_error(int $errno, string $errstr, string $file, int $line, $context) { + static function log_error(int $errno, string $errstr, string $file, int $line, string $context): bool { return self::get_instance()->_log_error($errno, $errstr, $file, $line, $context); } - private function _log_error($errno, $errstr, $file, $line, $context) { + private function _log_error(int $errno, string $errstr, string $file, int $line, string $context): bool { //if ($errno == E_NOTICE) return false; if ($this->adapter) @@ -38,11 +41,11 @@ class Logger { return false; } - static function log(int $errno, string $errstr, $context = "") { + static function log(int $errno, string $errstr, string $context = ""): bool { return self::get_instance()->_log($errno, $errstr, $context); } - private function _log(int $errno, string $errstr, $context = "") { + private function _log(int $errno, string $errstr, string $context = ""): bool { if ($this->adapter) return $this->adapter->log_error($errno, $errstr, '', 0, $context); else @@ -65,7 +68,7 @@ class Logger { $this->adapter = new Logger_Stdout(); break; default: - $this->adapter = false; + $this->adapter = null; } if ($this->adapter && !implements_interface($this->adapter, "Logger_Adapter")) diff --git a/classes/logger/adapter.php b/classes/logger/adapter.php index 79f641441..b0287b5fa 100644 --- a/classes/logger/adapter.php +++ b/classes/logger/adapter.php @@ -1,4 +1,4 @@ 117) { diff --git a/classes/logger/stdout.php b/classes/logger/stdout.php index e906853ce..b15649028 100644 --- a/classes/logger/stdout.php +++ b/classes/logger/stdout.php @@ -1,7 +1,7 @@ $parts + */ + static function build_url(array $parts): string { $tmp = $parts['scheme'] . "://" . $parts['host']; if (isset($parts['path'])) $tmp .= $parts['path']; @@ -81,7 +84,10 @@ class UrlHelper { } // extended filtering involves validation for safe ports and loopback - static function validate($url, $extended_filtering = false) { + /** + * @return bool|string false if something went wrong, otherwise the URL string + */ + static function validate(string $url, bool $extended_filtering = false) { $url = clean($url); @@ -138,7 +144,10 @@ class UrlHelper { return $url; } - static function resolve_redirects($url, $timeout, $nest = 0) { + /** + * @return bool|string + */ + static function resolve_redirects(string $url, int $timeout, int $nest = 0) { // too many redirects if ($nest > 10) @@ -189,12 +198,15 @@ class UrlHelper { return false; } - // TODO: max_size currently only works for CURL transfers + /** + * @return bool|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 +522,7 @@ class UrlHelper { } } - public static function url_to_youtube_vid($url) { + public static function url_to_youtube_vid(string $url) { # : bool|string $url = str_replace("youtube.com", "youtube-nocookie.com", $url); $regexps = [ diff --git a/phpstan.neon b/phpstan.neon index 7fe02b07e..8f0447e80 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 5 + level: 6 ignoreErrors: - '#Constant.*\b(SUBSTRING_FOR_DATE|SCHEMA_VERSION|SELF_USER_AGENT|LABEL_BASE_INDEX|PLUGIN_FEED_BASE_INDEX)\b.*not found#' - '#Comparison operation ">" between int<1, max> and 0 is always true.#' diff --git a/update.php b/update.php index b10dde400..36c66b06c 100755 --- a/update.php +++ b/update.php @@ -12,7 +12,7 @@ Config::sanity_check(); - function make_stampfile($filename) { + function make_stampfile(string $filename): bool { $fp = fopen(Config::get(Config::LOCK_DIRECTORY) . "/$filename", "w"); if (flock($fp, LOCK_EX | LOCK_NB)) { @@ -25,7 +25,7 @@ } } - function cleanup_tags($days = 14, $limit = 1000) { + function cleanup_tags(int $days = 14, int $limit = 1000): int { $days = (int) $days; diff --git a/update_daemon2.php b/update_daemon2.php index 8931813ff..46f7450a6 100755 --- a/update_daemon2.php +++ b/update_daemon2.php @@ -37,7 +37,7 @@ /** * @SuppressWarnings(unused) */ - function reap_children() { + function reap_children(): int { global $children; global $ctimes; @@ -64,7 +64,7 @@ return count($tmp); } - function check_ctimes() { + function check_ctimes(): void { global $ctimes; foreach (array_keys($ctimes) as $pid) { @@ -80,7 +80,7 @@ /** * @SuppressWarnings(unused) */ - function sigchld_handler($signal) { + function sigchld_handler(int $signo, mixed $siginfo): void { $running_jobs = reap_children(); Debug::log("Received SIGCHLD, $running_jobs active tasks left."); @@ -88,7 +88,7 @@ pcntl_waitpid(-1, $status, WNOHANG); } - function shutdown($caller_pid) { + function shutdown(int $caller_pid): void { if ($caller_pid == posix_getpid()) { if (file_exists(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon.lock")) { Debug::log("Removing lockfile (master)..."); @@ -97,7 +97,7 @@ } } - function task_shutdown() { + function task_shutdown(): void { $pid = posix_getpid(); if (file_exists(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon-$pid.lock")) { @@ -106,13 +106,13 @@ } } - function sigint_handler() { + function sigint_handler(): void { Debug::log("[MASTER] SIG_INT received, shutting down master process."); shutdown(posix_getpid()); die; } - function task_sigint_handler() { + function task_sigint_handler(): void { Debug::log("[TASK] SIG_INT received, shutting down task."); task_shutdown(); die; From bf53dfa51559fd094338f23acd0f4ec312bfd215 Mon Sep 17 00:00:00 2001 From: wn_ Date: Wed, 10 Nov 2021 21:53:28 +0000 Subject: [PATCH 002/118] Don't use 'mixed' directly (PHP 8+). --- update_daemon2.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/update_daemon2.php b/update_daemon2.php index 46f7450a6..06a31225e 100755 --- a/update_daemon2.php +++ b/update_daemon2.php @@ -79,8 +79,9 @@ /** * @SuppressWarnings(unused) + * @param mixed $siginfo */ - function sigchld_handler(int $signo, mixed $siginfo): void { + function sigchld_handler(int $signo, $siginfo): void { $running_jobs = reap_children(); Debug::log("Received SIGCHLD, $running_jobs active tasks left."); From 7a919a79d7b504c591ba2360b68f2e401675fdae Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 11:08:04 +0000 Subject: [PATCH 003/118] Fix some additional PHPStan warnings in UrlHelper. --- classes/urlhelper.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/classes/urlhelper.php b/classes/urlhelper.php index 5f175af3c..a660af170 100644 --- a/classes/urlhelper.php +++ b/classes/urlhelper.php @@ -16,7 +16,7 @@ class UrlHelper { static bool $fetch_curl_used; /** - * @param array $parts + * @param array $parts */ static function build_url(array $parts): string { $tmp = $parts['scheme'] . "://" . $parts['host']; @@ -113,6 +113,11 @@ class UrlHelper { } else { $tokens['host'] = idn_to_ascii($tokens['host']); } + + // if `idn_to_ascii` failed + if ($tokens['host'] === false) { + return false; + } } } @@ -199,6 +204,7 @@ class UrlHelper { } /** + * @param array|string $options * @return bool|string false if something went wrong, otherwise string contents */ // TODO: max_size currently only works for CURL transfers @@ -522,7 +528,10 @@ class UrlHelper { } } - public static function url_to_youtube_vid(string $url) { # : bool|string + /** + * @return bool|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 = [ From 0f324b77df21631b183c98d67dbb08750be9d2e1 Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 12:11:30 +0000 Subject: [PATCH 004/118] Address PHPStan warning and tweak 'tasks'+'interval' handling in 'update_daemon2.php'. This ensures both are of the expected type (int) and meet a reasonable minimum. --- update_daemon2.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/update_daemon2.php b/update_daemon2.php index 06a31225e..eea790c8b 100755 --- a/update_daemon2.php +++ b/update_daemon2.php @@ -130,7 +130,7 @@ $options = getopt("", $longopts); - if (isset($options["help"]) ) { + if ($options === false || isset($options["help"]) ) { print "Tiny Tiny RSS update daemon.\n\n"; print "Options:\n"; print " --log FILE - log messages to FILE\n"; @@ -161,21 +161,28 @@ if (isset($options["tasks"])) { Debug::log("Set to spawn " . $options["tasks"] . " children."); - $max_jobs = $options["tasks"]; + $max_jobs = (int) $options["tasks"]; } else { $max_jobs = Config::get(Config::DAEMON_MAX_JOBS); } + if ($max_jobs < 1) { + $max_jobs = 1; + Debug::log("Enforced minimum task count of $max_jobs."); + } + if (isset($options["interval"])) { Debug::log("Spawn interval: " . $options["interval"] . " seconds."); - $spawn_interval = $options["interval"]; + $spawn_interval = (int) $options["interval"]; } else { $spawn_interval = Config::get(Config::DAEMON_SLEEP_INTERVAL); } // let's enforce a minimum spawn interval as to not forkbomb the host - $spawn_interval = max(60, $spawn_interval); - Debug::log("Spawn interval: $spawn_interval sec"); + if ($spawn_interval < 60) { + $spawn_interval = 60; + Debug::log("Enforced minimum task spawn interval of $spawn_interval seconds."); + } if (file_is_locked("update_daemon.lock")) { die("error: Can't create lockfile. ". From 14ca0f2ac01c4ff9ffa27387326c6ebe5f002005 Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 12:26:30 +0000 Subject: [PATCH 005/118] Address PHPStan warnings in 'classes/counters.php'. --- classes/counters.php | 45 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/classes/counters.php b/classes/counters.php index 8a8b8bc71..1375a6694 100644 --- a/classes/counters.php +++ b/classes/counters.php @@ -1,7 +1,10 @@ > + */ + 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 $feed_ids + * @param array $label_ids + * @return array> + */ + 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 + */ + 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 $cat_ids + * @return array> + */ + 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 $feed_ids + * @return array> + */ + 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> + */ + 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> + */ + private static function get_virt(): array { $ret = []; @@ -263,7 +288,11 @@ class Counters { return $ret; } - static function get_labels(array $label_ids = null) { + /** + * @param array $label_ids + * @return array> + */ + static function get_labels(array $label_ids = null): array { $ret = []; From bf2bb875ab09c1cb51b7ac0a7bb29a79770e435f Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 15:57:03 +0000 Subject: [PATCH 006/118] Address PHPStan warnings in 'include/sessions.php'. --- include/sessions.php | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/include/sessions.php b/include/sessions.php index 26f4e0bca..48afd0a8b 100644 --- a/include/sessions.php +++ b/include/sessions.php @@ -1,9 +1,9 @@ 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')) { From eb068fbc47de8ce6bb7d1a368cfa1a20e9a2dc90 Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 16:47:51 +0000 Subject: [PATCH 007/118] Address PHPStan warnings in 'classes/prefs.php'. --- classes/prefs.php | 54 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/classes/prefs.php b/classes/prefs.php index 85e7c34db..7e6033f4d 100644 --- a/classes/prefs.php +++ b/classes/prefs.php @@ -141,7 +141,10 @@ class Prefs { Prefs::_PREFS_MIGRATED ]; + /** @var Prefs|null */ private static $instance; + + /** @var array */ 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> + */ static function get_all(int $owner_uid, int $profile_id = null) { return self::get_instance()->_get_all($owner_uid, $profile_id); } + /** + * @return array> + */ 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 From 3f8aaffd3499cd49912c3e2cb663d8572a96851e Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 18:53:52 +0000 Subject: [PATCH 008/118] Address PHPStan warnings in 'classes/rssutils.php'. This also includes a minor tweak in 'update.php' to account for 'getopt()' potentially returning false (indicating failure). --- classes/rssutils.php | 122 +++++++++++++++++++++++++++++++------------ update.php | 2 +- 2 files changed, 91 insertions(+), 33 deletions(-) diff --git a/classes/rssutils.php b/classes/rssutils.php index 927a6c251..3815b3ca1 100755 --- a/classes/rssutils.php +++ b/classes/rssutils.php @@ -1,6 +1,9 @@ $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 $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); @@ -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> $enclosures An array of "enclosure arrays" [string $link, string $type, int $length, string, $title, int $width, int $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> $filters + * @param array $tags + * @param array>|null $matched_rules + * @param array>|null $matched_filters + * + * @return array> 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> $filters An array of filter action arrays with keys "type" and "param" + * + * @return array|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> $filters An array of filter action arrays with keys "type" and "param" + * + * @return array> 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> $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> $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> $filters An array of filter action arrays with keys "type" and "param" + * @param array> $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> 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> 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> $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')))); } diff --git a/update.php b/update.php index 36c66b06c..6cd61a451 100755 --- a/update.php +++ b/update.php @@ -108,7 +108,7 @@ $options = getopt("", array_keys($options_map)); - if (count($options) == 0 || isset($options["help"]) ) { + if ($options === false || count($options) == 0 || isset($options["help"]) ) { print "Tiny Tiny RSS CLI management tool\n"; print "=================================\n"; print "Options:\n\n"; From 03495c11ed69f6311e9c7596cc53c5b15ce82bf6 Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 19:59:25 +0000 Subject: [PATCH 009/118] Address PHPStan warnings in 'classes/sanitizer.php'. This also includes some minor tweaks to things that call 'Sanitizer::sanitize()'. --- classes/api.php | 4 ++-- classes/feeds.php | 2 +- classes/handler/public.php | 4 ++-- classes/sanitizer.php | 19 ++++++++++++++----- include/functions.php | 10 ++++++++-- plugins/share/init.php | 2 +- 6 files changed, 28 insertions(+), 13 deletions(-) diff --git a/classes/api.php b/classes/api.php index 033aa8654..7d6ac174c 100755 --- a/classes/api.php +++ b/classes/api.php @@ -351,7 +351,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; } @@ -746,7 +746,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"]; } diff --git a/classes/feeds.php b/classes/feeds.php index cd2633ffb..20aa9c05d 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -271,7 +271,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"); diff --git a/classes/handler/public.php b/classes/handler/public.php index 14474d0bb..9a9f7b892 100755 --- a/classes/handler/public.php +++ b/classes/handler/public.php @@ -109,7 +109,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 +207,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']; diff --git a/classes/sanitizer.php b/classes/sanitizer.php index 3f6e9504e..2770aece2 100644 --- a/classes/sanitizer.php +++ b/classes/sanitizer.php @@ -1,6 +1,10 @@ $allowed_elements + * @param array $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(DOMNode $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|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 diff --git a/include/functions.php b/include/functions.php index 36519fd44..238cbe7f5 100644 --- a/include/functions.php +++ b/include/functions.php @@ -181,8 +181,14 @@ 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|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); } diff --git a/plugins/share/init.php b/plugins/share/init.php index 359d86802..8da417e52 100644 --- a/plugins/share/init.php +++ b/plugins/share/init.php @@ -133,7 +133,7 @@ class Share extends Plugin { $line["content"] = Sanitizer::sanitize($line["content"], $line['hide_images'], - $owner_uid, $line["site_url"], false, $line["id"]); + $owner_uid, $line["site_url"], null, $line["id"]); PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE, function ($result) use (&$line) { From f704d25ab15c5ffd988403d36b90fe76fb72916e Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 20:12:47 +0000 Subject: [PATCH 010/118] Address PHPStan warnings in 'classes/timehelper.php'. --- classes/feeds.php | 2 +- classes/timehelper.php | 8 ++++---- include/functions.php | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/classes/feeds.php b/classes/feeds.php index 20aa9c05d..529a8e403 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -299,7 +299,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)); diff --git a/classes/timehelper.php b/classes/timehelper.php index 4317f343f..e66f82f90 100644 --- a/classes/timehelper.php +++ b/classes/timehelper.php @@ -1,7 +1,7 @@ Date: Thu, 11 Nov 2021 20:25:13 +0000 Subject: [PATCH 011/118] Address PHPStan warnings in 'classes/userhelper.php'. --- classes/userhelper.php | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/classes/userhelper.php b/classes/userhelper.php index ea714b76b..c09cabb12 100644 --- a/classes/userhelper.php +++ b/classes/userhelper.php @@ -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,7 @@ class UserHelper { return null; } - static function is_default_password() { + static function is_default_password(): bool { $authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]); if ($authenticator && @@ -345,10 +345,12 @@ class UserHelper { return false; } - static function hash_password(string $pass, string $salt, string $algo = "") { - - if (!$algo) $algo = self::HASH_ALGOS[0]; - + /** + * @param UserHelper::HASH_ALGO_* $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) { From d2ccbecea68c0804c7b5b3650a92aa47c90cf29c Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 20:36:37 +0000 Subject: [PATCH 012/118] Address PHPStan warnings in 'include/controls.php'. --- include/controls.php | 70 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/include/controls.php b/include/controls.php index a1a1bc59b..46bcf56a0 100755 --- a/include/controls.php +++ b/include/controls.php @@ -1,7 +1,10 @@ $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 $attributes + */ + function button_tag(string $value, string $type, array $attributes = []): string { return ""; } - function input_tag(string $name, string $value, string $type = "text", array $attributes = [], string $id = "") { + /** + * @param array $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 $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 $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 $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 $attributes + */ + function icon(string $icon, array $attributes = []): string { return "$icon"; } - function select_tag(string $name, $value, array $values, array $attributes = [], string $id = "") { + /** + * @param mixed $value + * @param array $values + * @param array $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 $values + * @param array $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 .= ""; + $rv .= ""; } $rv .= ""; @@ -101,13 +132,19 @@ return $rv; } - function hidden_tag(string $name, string $value, array $attributes = []) { + /** + * @param array $attributes + */ + function hidden_tag(string $name, string $value, array $attributes = []): string { return ""; } - function checkbox_tag(string $name, bool $checked = false, string $value = "", array $attributes = [], string $id = "") { + /** + * @param array $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 $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 = ""; From e4b8e2d0631825ff0d3fa7bd5eb6e88414facd92 Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 20:40:30 +0000 Subject: [PATCH 013/118] Address PHPStan warnings in 'include/controls_compat.php'. --- include/controls_compat.php | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/include/controls_compat.php b/include/controls_compat.php index d1c2c12b5..5a2a9f2ba 100644 --- a/include/controls_compat.php +++ b/include/controls_compat.php @@ -1,7 +1,9 @@ $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 "\n"; } -function javascript_tag($filename, $attributes = []) { +/** + * @param array $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 "\n"; } -function format_warning($msg, $id = "") { +function format_warning(string $msg, string $id = ""): string { return "
$msg
"; } -function format_notice($msg, $id = "") { +function format_notice(string $msg, string $id = ""): string { return "
$msg
"; } -function format_error($msg, $id = "") { +function format_error(string $msg, string $id = ""): string { return "
$msg
"; } -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); } From 00b86bac39f63bc8d23bba470c8f53d5b7c97e6a Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 20:46:42 +0000 Subject: [PATCH 014/118] Address PHPStan warnings in 'include/errorhandler.php'. --- include/errorhandler.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/include/errorhandler.php b/include/errorhandler.php index 3211599ba..09d6bd7bc 100644 --- a/include/errorhandler.php +++ b/include/errorhandler.php @@ -1,5 +1,8 @@ > $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) { From 58ea0d43390ce85db10f6b616ff8775b06815dd4 Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 21:02:06 +0000 Subject: [PATCH 015/118] Address PHPStan warnings in 'classes/debug.php'. --- classes/debug.php | 49 +++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/classes/debug.php b/classes/debug.php index 2ae81e41a..f7c23cf1c 100644 --- a/classes/debug.php +++ b/classes/debug.php @@ -6,47 +6,60 @@ class Debug { const LOG_EXTENDED = 2; /** @deprecated */ - public static $LOG_DISABLED = self::LOG_DISABLED; + public static int $LOG_DISABLED = self::LOG_DISABLED; /** @deprecated */ - public static $LOG_NORMAL = self::LOG_NORMAL; + public static int $LOG_NORMAL = self::LOG_NORMAL; /** @deprecated */ - public static $LOG_VERBOSE = self::LOG_VERBOSE; + public static int $LOG_VERBOSE = self::LOG_VERBOSE; /** @deprecated */ - public static $LOG_EXTENDED = self::LOG_EXTENDED; + public static int $LOG_EXTENDED = self::LOG_EXTENDED; - private static $enabled = false; - private static $quiet = false; - private static $logfile = false; - private static $loglevel = self::LOG_NORMAL; + private static bool $enabled = false; + private static bool $quiet = false; + private static ?string $logfile = null; - public static function set_logfile($logfile) { + /** + * @var Debug::LOG_* + */ + private static int $loglevel = self::LOG_NORMAL; + + 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 Debug::LOG_* $level + */ + public static function set_loglevel($level): void { self::$loglevel = $level; } - public static function get_loglevel() { + /** + * @return Debug::LOG_* + */ + public static function get_loglevel(): int { return self::$loglevel; } - public static function log($message, int $level = 0) { + /** + * @param Debug::LOG_* $level + */ + public static function log(string $message, int $level = Debug::LOG_NORMAL): bool { if (!self::$enabled || self::$loglevel < $level) return false; @@ -73,7 +86,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 +99,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 +107,7 @@ class Debug { } print "[$ts] $message\n"; + + return true; } } From 728a71150a6e63dafeae311e591c5b43ce4e317e Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 21:33:12 +0000 Subject: [PATCH 016/118] Fix 'TimeHelper::make_local_datetime()' (null is allowed). --- classes/timehelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/timehelper.php b/classes/timehelper.php index e66f82f90..453ee0cee 100644 --- a/classes/timehelper.php +++ b/classes/timehelper.php @@ -21,7 +21,7 @@ class TimeHelper { } } - static function make_local_datetime(string $timestamp, bool $long, int $owner_uid = null, + 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']; From cc220058e075602ed689eb77b402ad4c9a623c79 Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 21:37:34 +0000 Subject: [PATCH 017/118] Address PHPStan warnings in 'include/functions.php'. --- include/functions.php | 134 ++++++++++++++++++++++++++++++------------ 1 file changed, 96 insertions(+), 38 deletions(-) diff --git a/include/functions.php b/include/functions.php index 8b112e3fb..af4819f00 100644 --- a/include/functions.php +++ b/include/functions.php @@ -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 + */ + 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,23 +170,27 @@ /* compat shims */ - /** function is @deprecated by Config::get_version() */ + /** + * @deprecated by Config::get_version() + * + * @return array|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) { - Debug::log($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"]); } @@ -192,23 +205,42 @@ 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 $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); } @@ -224,13 +256,19 @@ // 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)); @@ -249,7 +287,7 @@ } } - function make_password($length = 12) { + function make_password(int $length = 12): string { $password = ""; $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ*%+^"; @@ -274,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 = '…') { + function truncate_string(string $str, int $max_len, string $suffix = '…'): string { if (mb_strlen($str, "utf-8") > $max_len) { return mb_substr($str, 0, $max_len, "utf-8") . $suffix; } else { @@ -286,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"); @@ -295,7 +333,7 @@ return $out; } - function truncate_middle($str, $max_len, $suffix = '…') { + function truncate_middle(string $str, int $max_len, string $suffix = '…'): string { if (mb_strlen($str) > $max_len) { return mb_substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len); } else { @@ -303,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"); @@ -333,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)) { @@ -357,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')) { @@ -403,7 +450,7 @@ } } - function read_stdin() { + function read_stdin(): ?string { $fp = fopen("php://stdin", "r"); if ($fp) { @@ -415,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; @@ -429,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 $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; From 50997df57a8128fb72e15e0a6ca50401928d3900 Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 21:46:44 +0000 Subject: [PATCH 018/118] Address PHPStan warnings in 'inclasses/digest.php'. --- classes/digest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/classes/digest.php b/classes/digest.php index 94e5cd1fc..7adf9b449 100644 --- a/classes/digest.php +++ b/classes/digest.php @@ -1,7 +1,7 @@ , 3: string} + */ static function prepare_headlines_digest(int $user_id, int $days = 1, int $limit = 1000) { $tpl = new Templator(); From 2d5603b196047188bb543573cdf725fb10ec0401 Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 22:07:32 +0000 Subject: [PATCH 019/118] Address PHPStan warnings in 'classes/diskcache.php'. --- classes/diskcache.php | 79 ++++++++++++++++++++++++++++--------------- classes/urlhelper.php | 2 +- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/classes/diskcache.php b/classes/diskcache.php index d7ea26d3b..9fa043aee 100644 --- a/classes/diskcache.php +++ b/classes/diskcache.php @@ -1,9 +1,13 @@ + */ + private array $mimeMap = [ 'video/3gpp2' => '3g2', 'video/3gp' => '3gp', 'video/3gpp' => '3gp', @@ -190,21 +194,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 +220,55 @@ 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) { + 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 +277,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 +291,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 +299,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 +356,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 +380,19 @@ class DiskCache { } } - /* this is essentially a wrapper for readfile() which allows plugins to hook - output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else - - hook function should return true if request was handled (or at least attempted to) - - note that this can be called without user context so the plugin to handle this - should be loaded systemwide in config.php */ - function send_local_file($filename) { + /* */ + /** + * this is essentially a wrapper for readfile() which allows plugins to hook + * output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else + * + * hook function should return true if request was handled (or at least attempted to) + * + * note that this can be called without user context so the plugin to handle this + * should be loaded systemwide in config.php + * + * @return bool|int false if the file doesn't exist (or unreadable) or isn't audio/video, true if a plugin handled, otherwise int of bytes sent + */ + function send_local_file(string $filename) { if (file_exists($filename)) { if (is_writable($filename)) touch($filename); diff --git a/classes/urlhelper.php b/classes/urlhelper.php index a660af170..0592bf28c 100644 --- a/classes/urlhelper.php +++ b/classes/urlhelper.php @@ -205,7 +205,7 @@ class UrlHelper { /** * @param array|string $options - * @return bool|string false if something went wrong, otherwise string contents + * @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 From 95277fd099987f8173287c9565494633157a0ac8 Mon Sep 17 00:00:00 2001 From: wn_ Date: Thu, 11 Nov 2021 22:28:13 +0000 Subject: [PATCH 020/118] Address PHPStan warnings in 'classes/labels.php'. --- classes/labels.php | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/classes/labels.php b/classes/labels.php index 570f24f4f..b9c480f82 100644 --- a/classes/labels.php +++ b/classes/labels.php @@ -1,15 +1,15 @@ 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> + */ + 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> An array of label detail arrays + */ + static function get_all(int $owner_uid) { $rv = array(); $pdo = Db::pdo(); @@ -64,7 +70,12 @@ class Labels return $rv; } - static function update_cache($owner_uid, $id, $labels = false, $force = false) { + /** + * @param array>|null $labels + * + * @see Article::_get_labels() + */ + static function update_cache(int $owner_uid, int $id, ?array $labels = null, bool $force = false): void { $pdo = Db::pdo(); if ($force) @@ -81,7 +92,7 @@ class Labels } - static function clear_cache($id) { + static function clear_cache(int $id): void { $pdo = Db::pdo(); @@ -91,7 +102,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 +120,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 +149,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 +193,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']; From a0f37c3206fce8a1fb5a2d82d3d3206990ca1e9c Mon Sep 17 00:00:00 2001 From: wn_ Date: Fri, 12 Nov 2021 00:06:00 +0000 Subject: [PATCH 021/118] Address PHPStan warnings in 'classes/pluginhost.php'. --- classes/pluginhost.php | 238 ++++++++++++++++++++++++++++------------- 1 file changed, 164 insertions(+), 74 deletions(-) diff --git a/classes/pluginhost.php b/classes/pluginhost.php index b506a957a..36e050377 100755 --- a/classes/pluginhost.php +++ b/classes/pluginhost.php @@ -1,20 +1,38 @@ >> hook types -> priority levels -> Plugins */ + private array $hooks = []; + + /** @var array */ + private array $plugins = []; + + /** @var array> handler type -> method type -> Plugin */ + private array $handlers = []; + + /** @var array command type -> details array */ + private array $commands = []; + + /** @var array> plugin name -> (potential profile array) -> key -> value */ + private array $storage = []; + + /** @var array> */ + private array $feeds = []; + + /** @var array API method name, Plugin sender */ + private array $api_methods = []; + + /** @var array> */ + private array $plugin_actions = []; + + private ?int $owner_uid = null; + private bool $data_loaded = false; + private static ?PluginHost $instance = null; const API_VERSION = 2; const PUBLIC_METHOD_DELIMITER = "--"; @@ -174,13 +192,13 @@ class PluginHost { 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 +212,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 +231,11 @@ class PluginHost { return $this->pdo; } - function get_plugin_names() { - $names = array(); + /** + * @return array + */ + function get_plugin_names(): array { + $names = []; foreach ($this->plugins as $p) { array_push($names, get_class($p)); @@ -223,15 +244,21 @@ class PluginHost { return $names; } - function get_plugins() { + /** + * @return array + */ + 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 mixed $args + */ + function run_hooks(string $hook, ...$args): void { $method = strtolower($hook); foreach ($this->get_hooks($hook) as $plugin) { @@ -247,7 +274,11 @@ class PluginHost { } } - function run_hooks_until(string $hook, $check, ...$args) { + /** + * @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 +298,10 @@ class PluginHost { return false; } - function run_hooks_callback(string $hook, Closure $callback, ...$args) { + /** + * @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 +318,10 @@ class PluginHost { } } - function chain_hooks_callback(string $hook, Closure $callback, &...$args) { + /** + * @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 +338,7 @@ class PluginHost { } } - function add_hook(string $type, Plugin $sender, int $priority = 50) { + function add_hook(string $type, Plugin $sender, int $priority = 50): void { $priority = (int) $priority; if (!method_exists($sender, strtolower($type))) { @@ -325,7 +362,7 @@ class PluginHost { ksort($this->hooks[$type]); } - function del_hook(string $type, Plugin $sender) { + 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 +374,9 @@ class PluginHost { } } + /** + * @return array + */ function get_hooks(string $type) { if (isset($this->hooks[$type])) { $tmp = []; @@ -346,11 +386,10 @@ class PluginHost { } return $tmp; - } else { - return []; } + return []; } - function load_all(int $kind, int $owner_uid = null, bool $skip_init = false) { + 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 +400,7 @@ 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) { + function load(string $classlist, int $kind, int $owner_uid = null, bool $skip_init = false): void { $plugins = explode(",", $classlist); $this->owner_uid = (int) $owner_uid; @@ -434,27 +473,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 +502,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 +520,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 +529,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> command type -> details array */ function get_commands() { return $this->commands; } - function run_commands(array $args) { + /** + * @param array $args + */ + function run_commands(array $args): void { foreach ($this->get_commands() as $command => $data) { if (isset($args[$command])) { $command = str_replace("-", "", $command); @@ -516,7 +565,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 +579,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 +592,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 +612,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 +635,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 $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 +668,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 +693,10 @@ class PluginHost { } } + /** + * @param mixed $default_value + * @return mixed + */ function get(Plugin $sender, string $name, $default_value = false) { $idx = get_class($sender); @@ -641,6 +709,10 @@ class PluginHost { } } + /** + * @param array $default_value + * @return array + */ function get_array(Plugin $sender, string $name, array $default_value = []) { $tmp = $this->get($sender, $name); @@ -649,13 +721,16 @@ class PluginHost { return $tmp; } - function get_all($sender) { + /** + * @return array + */ + 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 +745,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 +758,15 @@ class PluginHost { return $id; } + /** + * @return array + */ 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 +774,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> + */ 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 $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 +844,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 $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 +858,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"; } From 57bf56f7945b639d0e39f4c7ad9da161a4a18888 Mon Sep 17 00:00:00 2001 From: wn_ Date: Fri, 12 Nov 2021 01:50:40 +0000 Subject: [PATCH 022/118] Address PHPStan warnings in 'classes/article.php'. Also related changes in some other classes. --- classes/article.php | 92 ++++++++++++++++++++++++++++++--------------- classes/digest.php | 2 +- classes/feeds.php | 4 +- classes/labels.php | 4 +- classes/rpc.php | 8 ++-- 5 files changed, 71 insertions(+), 39 deletions(-) diff --git a/classes/article.php b/classes/article.php index 04855ac9d..4af36d1c0 100755 --- a/classes/article.php +++ b/classes/article.php @@ -4,7 +4,11 @@ class Article extends Handler_Protected { const ARTICLE_KIND_VIDEO = 2; const ARTICLE_KIND_YOUTUBE = 3; - function redirect() { + const CATCHUP_MODE_MARK_AS_READ = 0; + const CATCHUP_MODE_MARK_AS_UNREAD = 1; + const CATCHUP_MODE_TOGGLE = 2; + + function redirect(): void { $article = ORM::for_table('ttrss_entries') ->table_alias('e') ->join('ttrss_user_entries', [ 'ref_id', '=', 'e.id'], 'ue') @@ -24,8 +28,7 @@ class Article extends Handler_Protected { print "Article not found or has an empty URL."; } - static function _create_published_article($title, $url, $content, $labels_str, - $owner_uid) { + static function _create_published_article(string $title, string $url, string $content, string $labels_str, int $owner_uid): bool { $guid = 'SHA1:' . sha1("ttshared:" . $url . $owner_uid); // include owner_uid to prevent global GUID clash @@ -158,14 +161,14 @@ class Article extends Handler_Protected { return $rc; } - function printArticleTags() { + function printArticleTags(): void { $id = (int) clean($_REQUEST['id'] ?? 0); print json_encode(["id" => $id, "tags" => self::_get_tags($id)]); } - function setScore() { + function setScore(): void { $ids = array_map("intval", clean($_REQUEST['ids'] ?? [])); $score = (int)clean($_REQUEST['score']); @@ -179,7 +182,7 @@ class Article extends Handler_Protected { print json_encode(["id" => $ids, "score" => $score]); } - function setArticleTags() { + function setArticleTags(): void { $id = clean($_REQUEST["id"]); @@ -254,18 +257,18 @@ class Article extends Handler_Protected { print ""; }*/ - 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", explode(",", clean($_REQUEST["ids"] ?? []))); $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>} + */ + 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 + */ + 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> + */ + 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 $ids + * @param Article::CATCHUP_MODE_* $cmode + */ + 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> + */ + static function _get_labels(int $id, ?int $owner_uid = null): array { $rv = array(); if (!$owner_uid) $owner_uid = $_SESSION["uid"]; @@ -543,6 +560,12 @@ class Article extends Handler_Protected { return $rv; } + /** + * @param array> $enclosures + * @param array $headline + * + * @return array + */ static function _get_image(array $enclosures, string $content, string $site_url, array $headline) { $article_image = ""; @@ -603,14 +626,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 +647,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 $article_ids + * @return array + */ static function _labels_of(array $article_ids) { if (count($article_ids) == 0) return []; @@ -651,6 +679,10 @@ class Article extends Handler_Protected { return array_unique($rv); } + /** + * @param array $article_ids + * @return array + */ static function _feeds_of(array $article_ids) { if (count($article_ids) == 0) return []; diff --git a/classes/digest.php b/classes/digest.php index 7adf9b449..15203166b 100644 --- a/classes/digest.php +++ b/classes/digest.php @@ -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"); diff --git a/classes/feeds.php b/classes/feeds.php index 529a8e403..c9f47463f 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -289,9 +289,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' => [] ]; } diff --git a/classes/labels.php b/classes/labels.php index b9c480f82..d78f92139 100644 --- a/classes/labels.php +++ b/classes/labels.php @@ -71,11 +71,11 @@ class Labels } /** - * @param array>|null $labels + * @param array> $labels * * @see Article::_get_labels() */ - static function update_cache(int $owner_uid, int $id, ?array $labels = null, bool $force = false): void { + static function update_cache(int $owner_uid, int $id, array $labels, bool $force = false): void { $pdo = Db::pdo(); if ($force) diff --git a/classes/rpc.php b/classes/rpc.php index 0432ed2d3..6d56f039e 100755 --- a/classes/rpc.php +++ b/classes/rpc.php @@ -344,11 +344,11 @@ class RPC extends Handler_Protected { $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 = ?"); @@ -365,11 +365,11 @@ class RPC extends Handler_Protected { $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 = ?"); From 5606e38bff619c388c9621dde30f0d54127a21f4 Mon Sep 17 00:00:00 2001 From: wn_ Date: Fri, 12 Nov 2021 02:01:31 +0000 Subject: [PATCH 023/118] Update signature of handler 'csrf_ignore' to include types. --- classes/feeds.php | 2 +- classes/handler.php | 2 +- classes/ihandler.php | 2 +- classes/opml.php | 2 +- classes/pluginhandler.php | 2 +- classes/pref/feeds.php | 2 +- classes/pref/filters.php | 2 +- classes/pref/labels.php | 2 +- classes/pref/prefs.php | 2 +- classes/pref/system.php | 2 +- classes/pref/users.php | 2 +- classes/rpc.php | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/classes/feeds.php b/classes/feeds.php index c9f47463f..951675adb 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -8,7 +8,7 @@ class Feeds extends Handler_Protected { private $viewfeed_timestamp; 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; diff --git a/classes/handler.php b/classes/handler.php index 09557c284..4c79628db 100644 --- a/classes/handler.php +++ b/classes/handler.php @@ -8,7 +8,7 @@ class Handler implements IHandler { $this->args = $args; } - function csrf_ignore($method) { + function csrf_ignore(string $method): bool { return false; } diff --git a/classes/ihandler.php b/classes/ihandler.php index 01c9e3109..8345839c0 100644 --- a/classes/ihandler.php +++ b/classes/ihandler.php @@ -1,6 +1,6 @@ Date: Fri, 12 Nov 2021 04:48:06 +0000 Subject: [PATCH 024/118] Address PHPStan warnings in 'classes/feeds.php'. Also some minor related tweaks in other classes. --- classes/api.php | 4 +- classes/debug.php | 15 ++- classes/feeds.php | 294 ++++++++++++++++++++++++++++------------------ classes/rpc.php | 2 +- 4 files changed, 191 insertions(+), 124 deletions(-) diff --git a/classes/api.php b/classes/api.php index 7d6ac174c..1835d487c 100755 --- a/classes/api.php +++ b/classes/api.php @@ -409,8 +409,8 @@ class API extends Handler { function catchupFeed() { $feed_id = clean($_REQUEST["feed_id"]); - $is_cat = clean($_REQUEST["is_cat"]); - @$mode = clean($_REQUEST["mode"]); + $is_cat = clean($_REQUEST["is_cat"]) == "true"; + $mode = clean($_REQUEST['mode'] ?? ""); if (!in_array($mode, ["all", "1day", "1week", "2week"])) $mode = "all"; diff --git a/classes/debug.php b/classes/debug.php index f7c23cf1c..eca7b31db 100644 --- a/classes/debug.php +++ b/classes/debug.php @@ -5,6 +5,13 @@ class Debug { const LOG_VERBOSE = 1; const LOG_EXTENDED = 2; + const ALL_LOG_LEVELS = [ + Debug::LOG_DISABLED, + Debug::LOG_NORMAL, + Debug::LOG_VERBOSE, + Debug::LOG_EXTENDED, + ]; + /** @deprecated */ public static int $LOG_DISABLED = self::LOG_DISABLED; @@ -43,21 +50,21 @@ class Debug { } /** - * @param Debug::LOG_* $level + * @param int $level Debug::LOG_* */ - public static function set_loglevel($level): void { + public static function set_loglevel(int $level): void { self::$loglevel = $level; } /** - * @return Debug::LOG_* + * @return int Debug::LOG_* */ public static function get_loglevel(): int { return self::$loglevel; } /** - * @param Debug::LOG_* $level + * @param int $level Debug::LOG_* */ public static function log(string $message, int $level = Debug::LOG_NORMAL): bool { diff --git a/classes/feeds.php b/classes/feeds.php index 951675adb..24511c396 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -5,8 +5,11 @@ class Feeds extends Handler_Protected { const NEVER_GROUP_FEEDS = [ -6, 0 ]; const NEVER_GROUP_BY_DATE = [ -2, -1, -3 ]; - private $viewfeed_timestamp; - private $viewfeed_timestamp_last; + /** @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(string $method): bool { $csrf_ignored = array("index"); @@ -14,9 +17,12 @@ class Feeds extends Handler_Protected { 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, 1: int, 2: int, 3: bool, 4: array} $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, bool $check_first_id, + 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); } @@ -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> + */ + 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 + */ + 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); @@ -644,9 +662,9 @@ class Feeds extends Handler_Protected { $sth->execute([$feed_id, $_SESSION['uid']]); if (!$sth->fetch()) { - print "Access denied."; - return; - } + print "Access denied."; + return; + } ?> @@ -731,7 +749,10 @@ class Feeds extends Handler_Protected { } - static function _catchup($feed, $cat_view, $owner_uid = false, $mode = 'all', $search = false) { + /** + * @param array $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 (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"; @@ -1137,7 +1159,7 @@ class Feeds extends Handler_Protected { } else { $icon = self::_get_icon_file($id); - if ($icon && file_exists($icon)) { + if ($icon && file_exists($icon)) { return Config::get(Config::ICONS_URL) . "/" . basename($icon) . "?" . filemtime($icon); } } @@ -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,8 +1213,8 @@ class Feeds extends Handler_Protected { } } - static function _get_title($id, bool $cat = false) { - $pdo = Db::pdo(); + static function _get_title(int $id, bool $cat = false): string { + $pdo = Db::pdo(); if ($cat) { return self::_get_cat_title($id); @@ -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 { - return 0; + $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"]; @@ -1265,14 +1295,15 @@ class Feeds extends Handler_Protected { $sth = $pdo->prepare("SELECT SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS unread FROM ttrss_user_entries WHERE feed_id IN (SELECT id FROM ttrss_feeds - WHERE (cat_id = :cat OR (:cat IS NULL AND cat_id IS NULL)) + WHERE (cat_id = :cat OR (:cat IS NULL AND cat_id IS NULL)) AND owner_uid = :uid) AND owner_uid = :uid"); + $sth->execute(["cat" => $cat ? $cat : null, "uid" => $owner_uid]); - $row = $sth->fetch(); - - return $row["unread"]; + 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 $params + * @return array $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 + */ + 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 + */ + 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 $feeds + * @return array + */ + static function _cats_of(array $feeds, int $owner_uid, bool $with_parents = false): array { if (count($feeds) == 0) return []; @@ -1930,24 +1979,27 @@ class Feeds extends Handler_Protected { } } - private function _color_of($name) { - $colormap = [ "#1cd7d7","#d91111","#1212d7","#8e16e5","#7b7b7b", - "#39f110","#0bbea6","#ec0e0e","#1534f2","#b9e416", - "#479af2","#f36b14","#10c7e9","#1e8fe7","#e22727" ]; + private function _color_of(string $name): string { + $colormap = [ "#1cd7d7","#d91111","#1212d7","#8e16e5","#7b7b7b", + "#39f110","#0bbea6","#ec0e0e","#1534f2","#b9e416", + "#479af2","#f36b14","#10c7e9","#1e8fe7","#e22727" ]; - $sum = 0; + $sum = 0; - for ($i = 0; $i < strlen($name); $i++) { - $sum += ord($name[$i]); - } + for ($i = 0; $i < strlen($name); $i++) { + $sum += ord($name[$i]); + } - $sum %= count($colormap); + $sum %= count($colormap); - return $colormap[$sum]; + return $colormap[$sum]; } - private static function _get_feeds_from_html($url, $content) { - $url = UrlHelper::validate($url); + /** + * @return array array of feed URL -> feed title + */ + private static function _get_feeds_from_html(string $url, string $content): array { + $url = UrlHelper::validate($url); $baseUrl = substr($url, 0, strrpos($url, '/') + 1); $feedUrls = []; @@ -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("/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,13 @@ 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) { + static function _update_access_key(int $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 +2077,7 @@ 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) { + static function _get_access_key(int $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,21 +2086,23 @@ class Feeds extends Handler_Protected { if ($key) { return $key->access_key; - } else { - $key = ORM::for_table('ttrss_access_keys')->create(); - - $key->owner_uid = $owner_uid; - $key->feed_id = $feed_id; - $key->is_cat = $is_cat; - $key->access_key = uniqid_short(); - - if ($key->save()) { - return $key->access_key; - } } + + $key = ORM::for_table('ttrss_access_keys')->create(); + + $key->owner_uid = $owner_uid; + $key->feed_id = $feed_id; + $key->is_cat = $is_cat; + $key->access_key = uniqid_short(); + + 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 +2131,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 +2172,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 +2185,10 @@ class Feeds extends Handler_Protected { } } - private static function _search_to_sql($search, $search_language, $owner_uid) { + /** + * @return array{0: string, 1: array} [$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 +2281,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 +2355,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 +2368,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 +2388,7 @@ class Feeds extends Handler_Protected { return [$query, $skip_first_id]; } - private function _mark_timestamp($label) { + private function _mark_timestamp(string $label): void { if (empty($_REQUEST['timestamps'])) return; diff --git a/classes/rpc.php b/classes/rpc.php index 4024aae2e..60119a605 100755 --- a/classes/rpc.php +++ b/classes/rpc.php @@ -227,7 +227,7 @@ class RPC extends Handler_Protected { $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()]); From 734be4ebd1dd7732d413bb69df33ac4e33460a5e Mon Sep 17 00:00:00 2001 From: wn_ Date: Fri, 12 Nov 2021 04:51:35 +0000 Subject: [PATCH 025/118] Minor PHPStand warning fix in 'update.php'. --- update.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.php b/update.php index 6cd61a451..d62825b98 100755 --- a/update.php +++ b/update.php @@ -97,7 +97,7 @@ ]; foreach (PluginHost::getInstance()->get_commands() as $command => $data) { - $options_map[$command . $data["suffix"]] = [ $data["arghelp"] ?? "", $data["description"] ]; + $options_map[$command . $data["suffix"]] = [ $data["arghelp"], $data["description"] ]; } if (php_sapi_name() != "cli") { From f0ad5881c0da9602195c4ad15a4c576d5ff54d91 Mon Sep 17 00:00:00 2001 From: wn_ Date: Fri, 12 Nov 2021 04:53:53 +0000 Subject: [PATCH 026/118] PHPStan warning fix in 'backend.php'. --- backend.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend.php b/backend.php index cb7daadad..59f3982c5 100644 --- a/backend.php +++ b/backend.php @@ -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)) { From b0eb347839d3350b57bd614eee67e32a574661d5 Mon Sep 17 00:00:00 2001 From: wn_ Date: Fri, 12 Nov 2021 05:04:55 +0000 Subject: [PATCH 027/118] Fix a warning in 'classes/counters.php'. --- classes/counters.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/classes/counters.php b/classes/counters.php index 1375a6694..50d103a5c 100644 --- a/classes/counters.php +++ b/classes/counters.php @@ -273,6 +273,10 @@ class Counters { if (is_array($feeds)) { foreach ($feeds as $feed) { + if (!method_exists($feed['sender'], 'get_unread')) { + continue; + } + $cv = [ "id" => PluginHost::pfeed_to_feed_id($feed['id']), "counter" => $feed['sender']->get_unread($feed['id']) From 011c941e7cdfce21d415eb6fa479c411776c79ce Mon Sep 17 00:00:00 2001 From: wn_ Date: Fri, 12 Nov 2021 05:24:02 +0000 Subject: [PATCH 028/118] Fix some PHPStan warnings in 'classes/db/migrations.php', 'classes/db/prefs.php', and 'classes/debug.php'. --- classes/db/migrations.php | 37 +++++++++++++++++++++++-------------- classes/db/prefs.php | 10 ++++++++-- classes/debug.php | 2 +- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/classes/db/migrations.php b/classes/db/migrations.php index 3008af535..cb74c247a 100644 --- a/classes/db/migrations.php +++ b/classes/db/migrations.php @@ -1,29 +1,29 @@ 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 +31,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}"); @@ -66,11 +66,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 +114,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) { @@ -174,6 +180,9 @@ class Db_Migrations { return !$this->is_migration_needed(); } + /** + * @return array + */ private function get_lines(int $version) : array { if ($version > 0) $filename = "{$this->migrations_path}/${version}.sql"; diff --git a/classes/db/prefs.php b/classes/db/prefs.php index 821216622..209ef58c1 100644 --- a/classes/db/prefs.php +++ b/classes/db/prefs.php @@ -2,11 +2,17 @@ class Db_Prefs { // this class is a stub for the time being (to be removed) - function read($pref_name, $user_id = false, $die_on_error = false) { + /** + * @return bool|int|null|string + */ + function read(string $pref_name, ?int $user_id = null, bool $die_on_error = false) { return get_pref($pref_name, $user_id); } - function write($pref_name, $value, $user_id = false, $strip_tags = true) { + /** + * @param mixed $value + */ + function write(string $pref_name, $value, ?int $user_id = null, bool $strip_tags = true): bool { return set_pref($pref_name, $value, $user_id, $strip_tags); } } diff --git a/classes/debug.php b/classes/debug.php index eca7b31db..6e8c46ed2 100644 --- a/classes/debug.php +++ b/classes/debug.php @@ -29,7 +29,7 @@ class Debug { private static ?string $logfile = null; /** - * @var Debug::LOG_* + * @var int Debug::LOG_* */ private static int $loglevel = self::LOG_NORMAL; From 9db5e402a0283deaae7d06496f410e9ab8deb1b4 Mon Sep 17 00:00:00 2001 From: wn_ Date: Fri, 12 Nov 2021 05:42:55 +0000 Subject: [PATCH 029/118] Address PHPStan warnings in 'classes/rpc.php'. Also a couple minor fixes in 'classes/article.php' and 'classes/labels.php'. --- classes/article.php | 2 +- classes/labels.php | 2 +- classes/rpc.php | 76 +++++++++++++++++++++++++++++---------------- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/classes/article.php b/classes/article.php index 4af36d1c0..b4f28dc28 100755 --- a/classes/article.php +++ b/classes/article.php @@ -483,7 +483,7 @@ class Article extends Handler_Protected { /** * @param array $ids - * @param Article::CATCHUP_MODE_* $cmode + * @param int $cmode Article::CATCHUP_MODE_* */ static function _catchup_by_id($ids, int $cmode, ?int $owner_uid = null): void { diff --git a/classes/labels.php b/classes/labels.php index d78f92139..5a17d665e 100644 --- a/classes/labels.php +++ b/classes/labels.php @@ -71,7 +71,7 @@ class Labels } /** - * @param array> $labels + * @param array>> $labels * * @see Article::_get_labels() */ diff --git a/classes/rpc.php b/classes/rpc.php index 60119a605..75d008b8b 100755 --- a/classes/rpc.php +++ b/classes/rpc.php @@ -7,7 +7,10 @@ class RPC extends Handler_Protected { return array_search($method, $csrf_ignored) !== false; }*/ - private function _translations_as_array() { + /** + * @return array + */ + 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,7 +223,7 @@ class RPC extends Handler_Protected { print ""; }*/ - function catchupFeed() { + function catchupFeed(): void { $feed_id = clean($_REQUEST['feed_id']); $is_cat = clean($_REQUEST['is_cat']) == "true"; $mode = clean($_REQUEST['mode'] ?? ''); @@ -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,11 +339,14 @@ class RPC extends Handler_Protected { } - function updaterandomfeed() { + function updaterandomfeed(): void { self::updaterandomfeed_real(); } - private function markArticlesById($ids, $cmode) { + /** + * @param array $ids + */ + private function markArticlesById(array $ids, int $cmode): void { $ids_qmarks = arr_qmarks($ids); @@ -361,7 +367,10 @@ class RPC extends Handler_Protected { $sth->execute(array_merge($ids, [$_SESSION['uid']])); } - private function publishArticlesById($ids, $cmode) { + /** + * @param array $ids + */ + private function publishArticlesById(array $ids, int $cmode): void { $ids_qmarks = arr_qmarks($ids); @@ -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 + */ + 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 + */ + 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> + */ + 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, 1: array} $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(); From 2c41bc7fbc9013e79e929a31e3824cf040afc54a Mon Sep 17 00:00:00 2001 From: wn_ Date: Fri, 12 Nov 2021 06:16:18 +0000 Subject: [PATCH 030/118] Address PHPStan warnings in 'classes/mailer.php', 'classes/opml.php', and 'classes/pluginhandler.php'. --- classes/mailer.php | 14 ++++++++++---- classes/opml.php | 32 ++++++++++++++++++++------------ classes/pluginhandler.php | 2 +- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/classes/mailer.php b/classes/mailer.php index 8238904ee..4eb13aec8 100644 --- a/classes/mailer.php +++ b/classes/mailer.php @@ -1,8 +1,12 @@ $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"]; @@ -26,6 +30,8 @@ class Mailer { // 4. set error message if needed via passed Mailer instance function set_error() foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEND_MAIL) as $p) { + // Implemented via plugin, so ignore the undefined method 'hook_send_mail'. + // @phpstan-ignore-next-line $rc = $p->hook_send_mail($this, $params); if ($rc == 1) @@ -46,12 +52,12 @@ class Mailer { return $rc; } - function set_error($message) { + function set_error(string $message): void { $this->last_error = $message; user_error("Error sending mail: $message", E_USER_WARNING); } - function error() { + function error(): string { return $this->last_error; } } diff --git a/classes/opml.php b/classes/opml.php index f60918061..b9f5f2eab 100644 --- a/classes/opml.php +++ b/classes/opml.php @@ -7,6 +7,9 @@ class OPML extends Handler_Protected { 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 { "; print ""; - - } // 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; + + 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 { diff --git a/classes/pluginhandler.php b/classes/pluginhandler.php index 9b3772ddc..5c73920e5 100644 --- a/classes/pluginhandler.php +++ b/classes/pluginhandler.php @@ -4,7 +4,7 @@ class PluginHandler extends Handler_Protected { 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"] ?? ""); From d3a81f598b24d6ae4f98415fac9509df6749eaf8 Mon Sep 17 00:00:00 2001 From: wn_ Date: Fri, 12 Nov 2021 21:17:31 +0000 Subject: [PATCH 031/118] Switch class properties from PHP typing to PHPDoc for compatibility with PHP < 7.4.0 --- classes/db/migrations.php | 35 ++++++++++++++++++++-------- classes/debug.php | 48 +++++++++++++++++++++++++++------------ classes/diskcache.php | 4 +++- classes/mailer.php | 4 +++- classes/pluginhost.php | 41 +++++++++++++++++++++------------ classes/urlhelper.php | 32 +++++++++++++++++++------- 6 files changed, 115 insertions(+), 49 deletions(-) diff --git a/classes/db/migrations.php b/classes/db/migrations.php index cb74c247a..6e20ddf7f 100644 --- a/classes/db/migrations.php +++ b/classes/db/migrations.php @@ -1,16 +1,33 @@ pdo = Db::pdo(); diff --git a/classes/debug.php b/classes/debug.php index 6e8c46ed2..e20126b86 100644 --- a/classes/debug.php +++ b/classes/debug.php @@ -1,9 +1,9 @@ $params diff --git a/classes/pluginhost.php b/classes/pluginhost.php index 36e050377..173a75611 100755 --- a/classes/pluginhost.php +++ b/classes/pluginhost.php @@ -1,38 +1,49 @@ >> hook types -> priority levels -> Plugins */ - private array $hooks = []; + private $hooks = []; /** @var array */ - private array $plugins = []; + private $plugins = []; /** @var array> handler type -> method type -> Plugin */ - private array $handlers = []; + private $handlers = []; /** @var array command type -> details array */ - private array $commands = []; + private $commands = []; /** @var array> plugin name -> (potential profile array) -> key -> value */ - private array $storage = []; + private $storage = []; /** @var array> */ - private array $feeds = []; + private $feeds = []; /** @var array API method name, Plugin sender */ - private array $api_methods = []; + private $api_methods = []; /** @var array> */ - private array $plugin_actions = []; + private $plugin_actions = []; - private ?int $owner_uid = null; - private bool $data_loaded = false; - private static ?PluginHost $instance = null; + /** @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 = "--"; diff --git a/classes/urlhelper.php b/classes/urlhelper.php index 0592bf28c..351d66b8d 100644 --- a/classes/urlhelper.php +++ b/classes/urlhelper.php @@ -6,14 +6,30 @@ class UrlHelper { "tel" ]; - static string $fetch_last_error; - static int $fetch_last_error_code; - static string $fetch_last_error_content; - static string $fetch_last_content_type; - static string $fetch_last_modified; - static string $fetch_effective_url; - static string $fetch_effective_ip_addr; - static bool $fetch_curl_used; + // 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; /** * @param array $parts From 25775bb4075e70aa4fad4620d077d4a0e59bb139 Mon Sep 17 00:00:00 2001 From: wn_ Date: Sat, 13 Nov 2021 04:14:18 +0000 Subject: [PATCH 032/118] Fix type of 'check_first_id' in Feeds '_format_headlines_list'. --- classes/feeds.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/feeds.php b/classes/feeds.php index 24511c396..22906cb54 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -21,7 +21,7 @@ class Feeds extends Handler_Protected { * @return array{0: array, 1: int, 2: int, 3: bool, 4: array} $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, bool $check_first_id, + 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; From 1ec003ce352f5bf1418986d7b96c35e75231ffde Mon Sep 17 00:00:00 2001 From: wn_ Date: Sat, 13 Nov 2021 14:05:43 +0000 Subject: [PATCH 033/118] Typing IHandler methods, typing Handler_Public, fix type of $feed_id (might be tag). --- classes/api.php | 2 +- classes/feeds.php | 14 +++++++++-- classes/handler.php | 13 +++++++--- classes/handler/administrative.php | 2 +- classes/handler/protected.php | 2 +- classes/handler/public.php | 40 ++++++++++++++++-------------- classes/ihandler.php | 4 +-- 7 files changed, 49 insertions(+), 28 deletions(-) diff --git a/classes/api.php b/classes/api.php index 1835d487c..125741c73 100755 --- a/classes/api.php +++ b/classes/api.php @@ -27,7 +27,7 @@ class API extends Handler { ]); } - function before($method) { + function before(string $method): bool { if (parent::before($method)) { header("Content-Type: text/json"); diff --git a/classes/feeds.php b/classes/feeds.php index 22906cb54..0c75215c8 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -2067,7 +2067,12 @@ class Feeds extends Handler_Protected { ->delete_many(); } - static function _update_access_key(int $feed_id, bool $is_cat, int $owner_uid): ?string { + /** + * @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) @@ -2077,7 +2082,12 @@ class Feeds extends Handler_Protected { return self::_get_access_key($feed_id, $is_cat, $owner_uid); } - static function _get_access_key(int $feed_id, bool $is_cat, int $owner_uid): ?string { + /** + * @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) diff --git a/classes/handler.php b/classes/handler.php index 4c79628db..3ee42cedb 100644 --- a/classes/handler.php +++ b/classes/handler.php @@ -1,9 +1,16 @@ */ protected $args; - function __construct($args) { + /** + * @param array $args + */ + function __construct(array $args) { $this->pdo = Db::pdo(); $this->args = $args; } @@ -12,11 +19,11 @@ class Handler implements IHandler { return false; } - function before($method) { + function before(string $method): bool { return true; } - function after() { + function after(): bool { return true; } diff --git a/classes/handler/administrative.php b/classes/handler/administrative.php index f2f5b36ba..533cb3630 100644 --- a/classes/handler/administrative.php +++ b/classes/handler/administrative.php @@ -1,6 +1,6 @@ = UserHelper::ACCESS_LEVEL_ADMIN) { return true; diff --git a/classes/handler/protected.php b/classes/handler/protected.php index 8e9e5ca1d..a15fc0956 100644 --- a/classes/handler/protected.php +++ b/classes/handler/protected.php @@ -1,7 +1,7 @@ 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 { @@ -247,7 +251,7 @@ class Handler_Public extends Handler { } } - function getUnread() { + function getUnread(): void { $login = clean($_REQUEST["login"]); $fresh = clean($_REQUEST["fresh"]) == "1"; @@ -265,7 +269,7 @@ class Handler_Public extends Handler { } } - function getProfiles() { + function getProfiles(): void { $login = clean($_REQUEST["login"]); $rv = []; @@ -288,7 +292,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 +302,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 +337,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 +407,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 +591,7 @@ class Handler_Public extends Handler { print ""; } - function dbupdate() { + function dbupdate(): void { startup_gettext(); if (!Config::get(Config::SINGLE_USER_MODE) && ($_SESSION["access_level"] ?? 0) < 10) { @@ -730,7 +734,7 @@ class Handler_Public extends Handler { Date: Sat, 13 Nov 2021 14:15:20 +0000 Subject: [PATCH 034/118] Address PHPStan warnings in 'classes/api.php'. --- classes/api.php | 109 ++++++++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 49 deletions(-) diff --git a/classes/api.php b/classes/api.php index 125741c73..d2668beee 100755 --- a/classes/api.php +++ b/classes/api.php @@ -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 $reply + */ + private function _wrap(int $status, array $reply): void { print json_encode([ "seq" => $this->seq, "status" => $status, @@ -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), - null, $entry->site_url, null, $entry->id); + false, $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|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"]) == "true"; - $mode = clean($_REQUEST['mode'] ?? ""); + $is_cat = clean($_REQUEST["is_cat"]); + $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']); @@ -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 + */ + 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>, 1: array} $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']), - null, $line["site_url"], null, $line["id"]); + false, $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 */ + private function _get_custom_sort_types(): array { $ret = []; PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_HEADLINES_CUSTOM_SORT_MAP, function ($result) use (&$ret) { From 45431170b629908b9fc39d19a87cf64d90bc9faf Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sat, 13 Nov 2021 17:31:13 +0300 Subject: [PATCH 035/118] fix phpstan warnings in classes/db/migrations.php --- classes/db/migrations.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/classes/db/migrations.php b/classes/db/migrations.php index 6e20ddf7f..aecd9186c 100644 --- a/classes/db/migrations.php +++ b/classes/db/migrations.php @@ -21,10 +21,10 @@ class Db_Migrations { private $pdo; /** @var int */ - private $cached_version; + private $cached_version = 0; /** @var int */ - private $cached_max_version; + private $cached_max_version = 0; /** @var int */ private $max_version_override; @@ -65,7 +65,7 @@ class Db_Migrations { } function get_version() : int { - if (isset($this->cached_version)) + if ($this->cached_version) return $this->cached_version; try { @@ -152,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"); From 77b8dc738616723bd891b09731478f7a4a77672e Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sat, 13 Nov 2021 17:48:52 +0300 Subject: [PATCH 036/118] fix phpstan warnings in classes/feedparser.php --- classes/feedparser.php | 58 +++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/classes/feedparser.php b/classes/feedparser.php index daba271fb..05412ef1e 100644 --- a/classes/feedparser.php +++ b/classes/feedparser.php @@ -1,19 +1,35 @@ */ + private $libxml_errors = []; + + /** @var array */ private $items; + + /** @var string */ private $link; + + /** @var string */ private $title; + + /** @var int */ private $type; + + /** @var DOMXPath */ 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,7 +42,7 @@ 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 + if ($this->error) //currently only the first error is reported $this->error = $this->format_error($error); $this->libxml_errors [] = $this->format_error($error); } @@ -37,7 +53,7 @@ class FeedParser { $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'); @@ -69,7 +85,7 @@ class FeedParser { $this->type = $this::FEED_ATOM; break; default: - if( !isset($this->error) ){ + if (!isset($this->error) ){ $this->error = "Unknown/unsupported feed type"; } return; @@ -100,6 +116,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 +138,7 @@ class FeedParser { $this->title = $title->nodeValue; } + /** @var DOMElement|null $link */ $link = $xpath->query("//channel/link")->item(0); if ($link) { @@ -173,39 +191,37 @@ class FeedParser { } } - function format_error($error) { - if ($error) { - return sprintf("LibXML error %s at line %d (column %d): %s", - $error->code, $error->line, $error->column, - $error->message); - } else { - return ""; - } + 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); } // 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 - WARNING: may return invalid unicode data */ + function errors() : array { return $this->libxml_errors; } - function get_link() { + function get_link() : string { return clean($this->link); } - function get_title() { + function get_title() : string { return clean($this->title); } - function get_items() { + /** @return array */ + function get_items() : array { return $this->items; } - function get_links($rel) { + /** @return array */ + function get_links(string $rel) : array { $rv = array(); switch ($this->type) { From a7983d475efb56711382bd320bb0be503dba0c93 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sat, 13 Nov 2021 17:51:26 +0300 Subject: [PATCH 037/118] fix phpstan warnings in classes/api.php --- classes/api.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/classes/api.php b/classes/api.php index d2668beee..764cb04da 100755 --- a/classes/api.php +++ b/classes/api.php @@ -356,7 +356,7 @@ class API extends Handler { $article['content'] = Sanitizer::sanitize( $entry->content, self::_param_to_bool($entry->hide_images), - false, $entry->site_url, null, $entry->id); + null, $entry->site_url, null, $entry->id); } else { $article['content'] = $entry->content; } @@ -485,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; @@ -756,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"], null, $line["id"]); + null, $line["site_url"], null, $line["id"]); } else { $headline_row["content"] = $line["content"]; } From 8a83f061bfa907af167090133738c1dc065dc69d Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sat, 13 Nov 2021 17:52:03 +0300 Subject: [PATCH 038/118] fix phpstan warnings in classes/sanitizer.php --- classes/sanitizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/sanitizer.php b/classes/sanitizer.php index 2770aece2..e0db811c6 100644 --- a/classes/sanitizer.php +++ b/classes/sanitizer.php @@ -44,7 +44,7 @@ class Sanitizer { return $doc; } - public static function iframe_whitelisted(DOMNode $entry): bool { + public static function iframe_whitelisted(DOMElement $entry): bool { $src = parse_url($entry->getAttribute("src"), PHP_URL_HOST); if (!empty($src)) From b381e9579295b238d44532a50edb6422b8c6b4ab Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sat, 13 Nov 2021 18:18:05 +0300 Subject: [PATCH 039/118] experimental: auto-generate and add all plugin hook methods to Plugin class --- classes/plugin.php | 194 +++++++++++++++++++++++ classes/plugin.tpl | 62 ++++++++ utils/generate-plugin-hook-prototypes.sh | 30 ++++ 3 files changed, 286 insertions(+) create mode 100644 classes/plugin.tpl create mode 100644 utils/generate-plugin-hook-prototypes.sh diff --git a/classes/plugin.php b/classes/plugin.php index ecafa7888..1b6702d72 100644 --- a/classes/plugin.php +++ b/classes/plugin.php @@ -58,4 +58,198 @@ abstract class Plugin { return vsprintf($this->__($msgid), $args); } + /* plugin hook methods (auto-generated) */ + + function hook_article_button($line) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_article_filter($article) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_prefs_tab($tab) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_prefs_tab_section($section) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_prefs_tabs() { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_feed_parsed($parser, $feed_id) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_update_task($cli_options) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_auth_user($login, $password, $service) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_hotkey_map($hotkeys) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_render_article($article) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_render_article_cdm($article) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_feed_fetched($feed_data, $fetch_url, $owner_uid, $feed) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_render_article_api($params) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_toolbar_button() { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_action_item() { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_headline_toolbar_button($feed_id, $is_cat) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_hotkey_info($hotkeys) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_article_left_button($row) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_prefs_edit_feed($feed_id) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_prefs_save_feed($feed_id) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + 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); + } + + function hook_query_headlines($row) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_house_keeping() { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_search($query) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_headlines_before($feed, $is_cat, $qfh_ret) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_render_enclosure($entry, $id, $rv) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_article_filter_action($article, $action) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_article_export_feed($line, $feed, $is_cat, $owner_uid) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_main_toolbar_button() { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_enclosure_entry($entry, $id, $rv) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_format_article($html, $row) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + 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); + } + + function hook_send_local_file($filename) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_unsubscribe_feed($feed_id, $owner_uid) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_send_mail(Mailer $mailer, $params) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_filter_triggered($feed_id, $owner_uid, $article, $matched_filters, $matched_rules, $article_filters) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_get_full_text($url) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_article_image($enclosures, $content, $site_url) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_feed_tree() { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_iframe_whitelisted($url) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_enclosure_imported($enclosure, $feed) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_headlines_custom_sort_map() { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_headlines_custom_sort_override($order) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_headline_toolbar_select_menu_item($feed_id, $is_cat) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + + function hook_pre_subscribe($url, $auth_login, $auth_pass) { + user_error("Dummy method invoked.", E_USER_ERROR); + } + } diff --git a/classes/plugin.tpl b/classes/plugin.tpl new file mode 100644 index 000000000..10f5b8ce7 --- /dev/null +++ b/classes/plugin.tpl @@ -0,0 +1,62 @@ +pdo = Db::pdo(); + } + + function flags() { + /* associative array, possible keys: + needs_curl = boolean + */ + return array(); + } + + function is_public_method($method) { + return false; + } + + function csrf_ignore($method) { + return false; + } + + function get_js() { + return ""; + } + + function get_prefs_js() { + return ""; + } + + function api_version() { + return Plugin::API_VERSION_COMPAT; + } + + /* gettext-related helpers */ + + function __($msgid) { + return _dgettext(PluginHost::object_to_domain($this), $msgid); + } + + function _ngettext($singular, $plural, $number) { + return _dngettext(PluginHost::object_to_domain($this), $singular, $plural, $number); + } + + function T_sprintf() { + $args = func_get_args(); + $msgid = array_shift($args); + + return vsprintf($this->__($msgid), $args); + } + + /** AUTO_GENERATED_HOOKS_GO_HERE **/ +} diff --git a/utils/generate-plugin-hook-prototypes.sh b/utils/generate-plugin-hook-prototypes.sh new file mode 100644 index 000000000..586f3f2c6 --- /dev/null +++ b/utils/generate-plugin-hook-prototypes.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +TMPFILE=$(mktemp) + +grep 'hook_.*(' ../classes/pluginhost.php | sed -e 's#[\t ]*/[* ]*##' \ + -e 's# [*]/$##' \ + -e 's# *(byref) *##' \ + -e 's#GLOBAL: ##' | while read F; do + + cat << EOF >> $TMPFILE + function $F { + user_error("Dummy method invoked.", E_USER_ERROR); + } + +EOF +done + +cat ../classes/plugin.tpl | while IFS=\n read L; do + case $L in + *AUTO_GENERATED_HOOKS_GO_HERE* ) + echo "\t/* plugin hook methods (auto-generated) */\n" + cat $TMPFILE + ;; + * ) + echo "$L" + ;; + esac +done > ../classes/plugin.php + +rm -f -- $TMPFILE From 70051742afdd05ab66d9265edb063eb5b6615765 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sat, 13 Nov 2021 18:21:04 +0300 Subject: [PATCH 040/118] experimental: also don't keep base plugin template as a non-analyzed file --- classes/{plugin.tpl => plugin-template.php} | 2 +- utils/generate-plugin-hook-prototypes.sh | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) rename classes/{plugin.tpl => plugin-template.php} (97%) diff --git a/classes/plugin.tpl b/classes/plugin-template.php similarity index 97% rename from classes/plugin.tpl rename to classes/plugin-template.php index 10f5b8ce7..ad6d07ee0 100644 --- a/classes/plugin.tpl +++ b/classes/plugin-template.php @@ -1,5 +1,5 @@ Date: Sat, 13 Nov 2021 18:26:11 +0300 Subject: [PATCH 041/118] fix phpstan warnings in classes/plugin-template.php --- classes/plugin-template.php | 26 +++++++++++++++----------- classes/plugin.php | 26 +++++++++++++++----------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/classes/plugin-template.php b/classes/plugin-template.php index ad6d07ee0..12437525b 100644 --- a/classes/plugin-template.php +++ b/classes/plugin-template.php @@ -5,53 +5,57 @@ abstract class PluginTemplate { /** @var PDO $pdo */ protected $pdo; - abstract function init(PluginHost $host); + abstract function init(PluginHost $host) : void; - abstract function about(); + /** @return array */ + abstract function about() : array; // return array(1.0, "plugin", "No description", "No author", false); function __construct() { $this->pdo = Db::pdo(); } - function flags() { + /** @return array */ + function flags() : array { /* associative array, possible keys: needs_curl = boolean */ return array(); } - function is_public_method($method) { + function is_public_method(string $method) : bool { return false; } - function csrf_ignore($method) { + function csrf_ignore(string $method) : bool { return false; } - function get_js() { + function get_js() : string { return ""; } - function get_prefs_js() { + function get_prefs_js() : string { return ""; } - function api_version() { + function api_version() : int { return Plugin::API_VERSION_COMPAT; } /* gettext-related helpers */ - function __($msgid) { + function __(string $msgid) : string { + /** @var Plugin $this -- this is a strictly template-related hack */ return _dgettext(PluginHost::object_to_domain($this), $msgid); } - function _ngettext($singular, $plural, $number) { + function _ngettext(string $singular, string $plural, int $number) : string { + /** @var Plugin $this -- this is a strictly template-related hack */ return _dngettext(PluginHost::object_to_domain($this), $singular, $plural, $number); } - function T_sprintf() { + function T_sprintf() : string { $args = func_get_args(); $msgid = array_shift($args); diff --git a/classes/plugin.php b/classes/plugin.php index 1b6702d72..08a122023 100644 --- a/classes/plugin.php +++ b/classes/plugin.php @@ -5,53 +5,57 @@ abstract class Plugin { /** @var PDO $pdo */ protected $pdo; - abstract function init(PluginHost $host); + abstract function init(PluginHost $host) : void; - abstract function about(); + /** @return array */ + abstract function about() : array; // return array(1.0, "plugin", "No description", "No author", false); function __construct() { $this->pdo = Db::pdo(); } - function flags() { + /** @return array */ + function flags() : array { /* associative array, possible keys: needs_curl = boolean */ return array(); } - function is_public_method($method) { + function is_public_method(string $method) : bool { return false; } - function csrf_ignore($method) { + function csrf_ignore(string $method) : bool { return false; } - function get_js() { + function get_js() : string { return ""; } - function get_prefs_js() { + function get_prefs_js() : string { return ""; } - function api_version() { + function api_version() : int { return Plugin::API_VERSION_COMPAT; } /* gettext-related helpers */ - function __($msgid) { + function __(string $msgid) : string { + /** @var Plugin $this -- this is a strictly template-related hack */ return _dgettext(PluginHost::object_to_domain($this), $msgid); } - function _ngettext($singular, $plural, $number) { + function _ngettext(string $singular, string $plural, int $number) : string { + /** @var Plugin $this -- this is a strictly template-related hack */ return _dngettext(PluginHost::object_to_domain($this), $singular, $plural, $number); } - function T_sprintf() { + function T_sprintf() : string { $args = func_get_args(); $msgid = array_shift($args); From b37a03fb31cf1c394e36ccf082bb5d3359f3a1fb Mon Sep 17 00:00:00 2001 From: wn_ Date: Sat, 13 Nov 2021 14:41:22 +0000 Subject: [PATCH 042/118] Fix the type of Labels::update_cache() --- classes/article.php | 2 ++ classes/labels.php | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/classes/article.php b/classes/article.php index b4f28dc28..b720971b9 100755 --- a/classes/article.php +++ b/classes/article.php @@ -553,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)); diff --git a/classes/labels.php b/classes/labels.php index 5a17d665e..026e6621f 100644 --- a/classes/labels.php +++ b/classes/labels.php @@ -71,7 +71,8 @@ class Labels } /** - * @param array>> $labels + * @param array{'no-labels': 1}|array> $labels + * [label_id, caption, fg_color, bg_color] * * @see Article::_get_labels() */ From a18473e4c0d2187f1308c7d77fb40d552bb194ed Mon Sep 17 00:00:00 2001 From: wn_ Date: Sat, 13 Nov 2021 15:50:37 +0000 Subject: [PATCH 043/118] Address PHPStan warnings in 'classes/pref/feeds.php'. --- classes/pref/feeds.php | 175 ++++++++++++++++++++++------------------- 1 file changed, 94 insertions(+), 81 deletions(-) diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php index ce1a53bef..ec6abf9dd 100755 --- a/classes/pref/feeds.php +++ b/classes/pref/feeds.php @@ -11,7 +11,10 @@ class Pref_Feeds extends Handler_Protected { return array_search($method, $csrf_ignored) !== false; } - public static function get_ts_languages() { + /** + * @return array + */ + 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> + */ + 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> + */ + 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 $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 */ $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 { 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 */ + $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 = " + ` + }); + + dialog.show(); + }, subscribeToFeed: function() { xhr.json("backend.php", {op: "feeds", method: "subscribeToFeed"}, diff --git a/js/Feeds.js b/js/Feeds.js index 27586ab13..5ef554af0 100644 --- a/js/Feeds.js +++ b/js/Feeds.js @@ -278,19 +278,8 @@ const Feeds = { } if (App.getInitParam("safe_mode")) { - const dialog = new fox.SingleUseDialog({ - title: __("Safe mode"), - content: `
- ${__('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.')} -
-
- -
` - }); - - dialog.show(); + /* global CommonDialogs */ + CommonDialogs.safeModeWarning(); } // bw_limit disables timeout() so we request initial counters separately From 98af46addd0978c0a8d6893f16a9b46d548b06e7 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 16:13:06 +0300 Subject: [PATCH 059/118] prefs: properly report failures when loading plugin list --- js/PrefHelpers.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/js/PrefHelpers.js b/js/PrefHelpers.js index 30a4544fe..ce3046210 100644 --- a/js/PrefHelpers.js +++ b/js/PrefHelpers.js @@ -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 = `
  • ${__("Error while loading plugins list: %s.").replace("%s", e)}
  • `; + }, render_contents: function() { const container = document.querySelector(".prefs-plugin-list"); From c3ffa08807df4a83cd476aca79fc96217acc6c3a Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 16:15:31 +0300 Subject: [PATCH 060/118] deal with phpstan warnings in update.php --- update.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/update.php b/update.php index d62825b98..90251c2f3 100755 --- a/update.php +++ b/update.php @@ -378,7 +378,7 @@ Debug::log("Exporting feeds of user $user to $filename as OPML..."); if ($owner_uid = UserHelper::find_user_by_login($user)) { - $opml = new OPML(""); + $opml = new OPML([]); $rc = $opml->opml_export($filename, $owner_uid, false, true, true); @@ -394,7 +394,7 @@ Debug::log("Importing feeds of user $user from OPML file $filename..."); if ($owner_uid = UserHelper::find_user_by_login($user)) { - $opml = new OPML(""); + $opml = new OPML([]); $rc = $opml->opml_import($owner_uid, $filename); From af2f4460ce94f48aa4c3bb3176c59325b6612b32 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 16:49:10 +0300 Subject: [PATCH 061/118] * deal with some phpstan warnings in base plugin class * arguably better hack for incompatible plugins causing E_COMPILE_ERROR --- classes/plugin.php | 94 ++++++++++++++++++++++++++++++++++++++++++ classes/pluginhost.php | 18 +++++--- classes/pref/prefs.php | 2 +- 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/classes/plugin.php b/classes/plugin.php index 8c14cd78d..b027a05c3 100644 --- a/classes/plugin.php +++ b/classes/plugin.php @@ -140,10 +140,19 @@ abstract class Plugin { user_error("Dummy method invoked.", E_USER_ERROR); } + /** + * @param FeedParser $parser + * @param int $feed_id + * @return void + */ function hook_feed_parsed($parser, $feed_id) { user_error("Dummy method invoked.", E_USER_ERROR); } + /** + * @param array $cli_options + * @return void + */ function hook_update_task($cli_options) { user_error("Dummy method invoked.", E_USER_ERROR); } @@ -170,44 +179,94 @@ abstract class Plugin { return false; } + /** + * @param array $hotkeys + * @return array + */ function hook_hotkey_map($hotkeys) { user_error("Dummy method invoked.", E_USER_ERROR); } + /** + * @param array $article + * @return array + */ function hook_render_article($article) { user_error("Dummy method invoked.", E_USER_ERROR); + + return []; } + /** + * @param array $article + * @return array + */ function hook_render_article_cdm($article) { user_error("Dummy method invoked.", E_USER_ERROR); + + return []; } + /** + * @param string $feed_data + * @param string $fetch_url + * @param int $owner_uid + * @param int $feed + * @return string + */ function hook_feed_fetched($feed_data, $fetch_url, $owner_uid, $feed) { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } function hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) { user_error("Dummy method invoked.", E_USER_ERROR); } + /** + * @param array{'article': array} $params + * @return array + */ function hook_render_article_api($params) { user_error("Dummy method invoked.", E_USER_ERROR); + + return []; } + /** @return string */ function hook_toolbar_button() { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } + /** @return string */ function hook_action_item() { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } + /** + * @param int $feed_id + * @param bool $is_cat + * @return string + */ function hook_headline_toolbar_button($feed_id, $is_cat) { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } + /** + * @param array $hotkeys + * @return array + */ function hook_hotkey_info($hotkeys) { user_error("Dummy method invoked.", E_USER_ERROR); + + return []; } function hook_article_left_button($row) { @@ -230,6 +289,7 @@ abstract class Plugin { user_error("Dummy method invoked.", E_USER_ERROR); } + /** @return void */ function hook_house_keeping() { user_error("Dummy method invoked.", E_USER_ERROR); } @@ -262,6 +322,7 @@ abstract class Plugin { user_error("Dummy method invoked.", E_USER_ERROR); } + /** @return void */ function hook_main_toolbar_button() { user_error("Dummy method invoked.", E_USER_ERROR); } @@ -296,24 +357,57 @@ abstract class Plugin { user_error("Dummy method invoked.", E_USER_ERROR); } + /** NOTE: $article_filters should be renamed $filter_actions because that's what this is + * @param int $feed_id + * @param int $owner_uid + * @param array $article + * @param array $matched_filters + * @param array $matched_rules + * @param array $article_filters + * @return void + */ function hook_filter_triggered($feed_id, $owner_uid, $article, $matched_filters, $matched_rules, $article_filters) { user_error("Dummy method invoked.", E_USER_ERROR); } + /** + * @param string $url + * @return string + */ function hook_get_full_text($url) { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } + /** + * @param array $enclosures + * @param string $content + * @param string $site_url + * @param array $article + * @return string + */ function hook_article_image($enclosures, $content, $site_url, $article) { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } + /** @return string */ function hook_feed_tree() { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } + /** + * @param string $url + * @return bool + */ function hook_iframe_whitelisted($url) { user_error("Dummy method invoked.", E_USER_ERROR); + + return false; } function hook_enclosure_imported($enclosure, $feed) { diff --git a/classes/pluginhost.php b/classes/pluginhost.php index 7688a6d0d..4b0c14a35 100755 --- a/classes/pluginhost.php +++ b/classes/pluginhost.php @@ -434,16 +434,24 @@ class PluginHost { // WIP hack // we can't catch incompatible method signatures via Throwable - // maybe also auto-disable user plugin in this situation? idk -fox - if ($_SESSION["plugin_blacklist.$class"] ?? false) { - user_error("Plugin $class has caused a PHP Fatal Error so it won't be loaded again in this session.", E_USER_NOTICE); + // 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"] = true; + $_SESSION["plugin_blacklist"][$class] = 1; require_once $file; - $_SESSION["plugin_blacklist.$class"] = false; + unset($_SESSION["plugin_blacklist"][$class]); } catch (Error $err) { user_error($err, E_USER_WARNING); diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php index 3a39bf981..025d8fda2 100644 --- a/classes/pref/prefs.php +++ b/classes/pref/prefs.php @@ -17,7 +17,7 @@ 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(string $method): bool { + function csrf_ignore($method) : bool { $csrf_ignored = array("index", "updateself", "otpqrcode"); return array_search($method, $csrf_ignored) !== false; From 55729b4bbd79c6afa913d3a4acc576eef5cfaae1 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 17:07:47 +0300 Subject: [PATCH 062/118] fix HOOK_QUERY_HEADLINES being invoked with different argument lists, add some more phpdoc comments for base plugin class --- classes/handler/public.php | 4 +- classes/plugin.php | 85 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/classes/handler/public.php b/classes/handler/public.php index e28bb5fd2..b5282c222 100755 --- a/classes/handler/public.php +++ b/classes/handler/public.php @@ -93,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) { diff --git a/classes/plugin.php b/classes/plugin.php index b027a05c3..96541d033 100644 --- a/classes/plugin.php +++ b/classes/plugin.php @@ -109,6 +109,8 @@ abstract class Plugin { */ function hook_article_button($line) { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } /** @@ -117,6 +119,8 @@ abstract class Plugin { */ function hook_article_filter($article) { user_error("Dummy method invoked.", E_USER_ERROR); + + return []; } /** @@ -185,6 +189,8 @@ abstract class Plugin { */ function hook_hotkey_map($hotkeys) { user_error("Dummy method invoked.", E_USER_ERROR); + + return []; } /** @@ -269,14 +275,28 @@ abstract class Plugin { return []; } + /** + * @param array $row + * @return string + */ function hook_article_left_button($row) { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } + /** + * @param int $feed_id + * @return void + */ function hook_prefs_edit_feed($feed_id) { user_error("Dummy method invoked.", E_USER_ERROR); } + /** + * @param int $feed_id + * @return void + */ function hook_prefs_save_feed($feed_id) { user_error("Dummy method invoked.", E_USER_ERROR); } @@ -285,8 +305,15 @@ abstract class Plugin { user_error("Dummy method invoked.", E_USER_ERROR); } - function hook_query_headlines($row) { + /** + * @param array $row + * @param int $excerpt_length + * @return array + */ + function hook_query_headlines($row, $excerpt_length) { user_error("Dummy method invoked.", E_USER_ERROR); + + return []; } /** @return void */ @@ -294,23 +321,50 @@ abstract class Plugin { user_error("Dummy method invoked.", E_USER_ERROR); } + /** + * @param string $query + * @return array + */ function hook_search($query) { user_error("Dummy method invoked.", E_USER_ERROR); + + return []; } function hook_format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images) { user_error("Dummy method invoked.", E_USER_ERROR); } + /** + * @param string $contents + * @param string $url + * @param string $auth_login + * @param string $auth_pass + * @return string (possibly mangled feed data) + */ function hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) { user_error("Dummy method invoked.", E_USER_ERROR); } + /** + * @param int $feed + * @param bool $is_cat + * @param array $qfh_ret (headlines object) + * @return string + */ function hook_headlines_before($feed, $is_cat, $qfh_ret) { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } - function hook_render_enclosure($entry, $id, $rv) { + /** + * @param array $entry + * @param int $article_id + * @param array $rv + * @return string + */ + function hook_render_enclosure($entry, $article_id, $rv) { user_error("Dummy method invoked.", E_USER_ERROR); } @@ -318,8 +372,17 @@ abstract class Plugin { user_error("Dummy method invoked.", E_USER_ERROR); } + /** + * @param array $line + * @param int $feed + * @param bool $is_cat + * @param int $owner_uid + * @return array ($line) + */ function hook_article_export_feed($line, $feed, $is_cat, $owner_uid) { user_error("Dummy method invoked.", E_USER_ERROR); + + return []; } /** @return void */ @@ -331,12 +394,19 @@ abstract class Plugin { * @param array $entry * @param int $id * @param array{'formatted': string, 'entries': array>} $rv - * @return array + * @return array ($entry) */ function hook_enclosure_entry($entry, $id, $rv) { user_error("Dummy method invoked.", E_USER_ERROR); + + return []; } + /** + * @param string $html + * @param array $row + * @return string ($html) + */ function hook_format_article($html, $row) { user_error("Dummy method invoked.", E_USER_ERROR); } @@ -353,8 +423,15 @@ abstract class Plugin { user_error("Dummy method invoked.", E_USER_ERROR); } - function hook_send_mail(Mailer $mailer, $params) { + /** + * @param Mailer $mailer + * @param array $params + * @return int + */ + function hook_send_mail($mailer, $params) { user_error("Dummy method invoked.", E_USER_ERROR); + + return -1; } /** NOTE: $article_filters should be renamed $filter_actions because that's what this is From dd7299b6d070d26ff97194ef14be349f08776e2a Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 17:19:35 +0300 Subject: [PATCH 063/118] deal with a few more phpstan warnings re: base plugin class --- classes/plugin.php | 48 ++++++++++++++++++++++++++++++++- plugins/af_readability/init.php | 1 + 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/classes/plugin.php b/classes/plugin.php index 96541d033..ac234f081 100644 --- a/classes/plugin.php +++ b/classes/plugin.php @@ -226,8 +226,18 @@ abstract class Plugin { return ""; } + /** + * @param DOMDocument $doc + * @param string $site_url + * @param array $allowed_elements + * @param array $disallowed_attributes + * @param int $article_id + * @return DOMDocument + */ function hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) { user_error("Dummy method invoked.", E_USER_ERROR); + + return $doc; } /** @@ -301,8 +311,20 @@ abstract class Plugin { user_error("Dummy method invoked.", E_USER_ERROR); } + /** + * @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) + */ 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 ""; } /** @@ -331,8 +353,19 @@ abstract class Plugin { return []; } - function hook_format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images) { + /** + * @param string $enclosures_formatted + * @param array> $enclosures + * @param int $article_id + * @param bool $always_display_enclosures + * @param string $article_content + * @param bool $hide_images + * @return string|array>> ($enclosures_formatted, $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 ""; } /** @@ -344,6 +377,8 @@ abstract class Plugin { */ function hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } /** @@ -366,10 +401,19 @@ abstract class Plugin { */ function hook_render_enclosure($entry, $article_id, $rv) { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } + /** + * @param array $article + * @param string $action + * @return array ($article) + */ function hook_article_filter_action($article, $action) { user_error("Dummy method invoked.", E_USER_ERROR); + + return []; } /** @@ -409,6 +453,8 @@ abstract class Plugin { */ function hook_format_article($html, $row) { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } function hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed_id, $auth_login, $auth_pass) { diff --git a/plugins/af_readability/init.php b/plugins/af_readability/init.php index 05b229eae..8e75d0c2e 100755 --- a/plugins/af_readability/init.php +++ b/plugins/af_readability/init.php @@ -187,6 +187,7 @@ class Af_Readability extends Plugin { case "action_append": return $this->process_article($article, true); } + return $article; } public function extract_content($url) { From 01b39d985c9f194a35c690a18149cbb06fc7b0d3 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 18:00:03 +0300 Subject: [PATCH 064/118] deal with the rest of warnings in plugin.php --- classes/plugin.php | 55 ++++++++++++++++++++++++++++++++++++++ plugins/af_comics/init.php | 1 + 2 files changed, 56 insertions(+) diff --git a/classes/plugin.php b/classes/plugin.php index ac234f081..b20bbcbc2 100644 --- a/classes/plugin.php +++ b/classes/plugin.php @@ -457,16 +457,40 @@ abstract class Plugin { return ""; } + /** + * @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} + */ 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; } + /** + * @param string $filename + * @return bool + */ function hook_send_local_file($filename) { user_error("Dummy method invoked.", E_USER_ERROR); + + return false; } + /** + * @param int $feed_id + * @param int $owner_uid + * @return bool + */ function hook_unsubscribe_feed($feed_id, $owner_uid) { user_error("Dummy method invoked.", E_USER_ERROR); + + return false; } /** @@ -533,23 +557,54 @@ abstract class Plugin { return false; } + /** + * @param array $enclosure + * @param int $feed + * @return array ($enclosure) + */ function hook_enclosure_imported($enclosure, $feed) { user_error("Dummy method invoked.", E_USER_ERROR); + + return $enclosure; } + /** @return array */ function hook_headlines_custom_sort_map() { user_error("Dummy method invoked.", E_USER_ERROR); + + return ["" => ""]; } + /** + * @param string $order + * @return array -- query, skip_first_id + */ function hook_headlines_custom_sort_override($order) { user_error("Dummy method invoked.", E_USER_ERROR); + + return ["", false]; } + /** + * @param int $feed_id + * @param int $is_cat + * @return string + */ function hook_headline_toolbar_select_menu_item($feed_id, $is_cat) { user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; } + /** + * @param string $url + * @param string $auth_login + * @param string $auth_pass + * @return bool + */ function hook_pre_subscribe(&$url, $auth_login, $auth_pass) { user_error("Dummy method invoked.", E_USER_ERROR); + + return false; } } diff --git a/plugins/af_comics/init.php b/plugins/af_comics/init.php index 84d95a2ba..a9a8f3faa 100755 --- a/plugins/af_comics/init.php +++ b/plugins/af_comics/init.php @@ -1,6 +1,7 @@ $filters */ private $filters = array(); function about() { From 1b5c61ac85f1fbc7b437ba018f1acad08553bdeb Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 18:02:20 +0300 Subject: [PATCH 065/118] userhelper: add a phpdoc variable class hint --- classes/userhelper.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/classes/userhelper.php b/classes/userhelper.php index c09cabb12..0217a8927 100644 --- a/classes/userhelper.php +++ b/classes/userhelper.php @@ -334,6 +334,8 @@ class UserHelper { } 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 && From 7988c79bd40356b62d9f1bca03284c0b35a49fcd Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 18:05:31 +0300 Subject: [PATCH 066/118] plugin.php: add some minor method phpdoc corrections --- classes/plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/plugin.php b/classes/plugin.php index b20bbcbc2..290c4cc3b 100644 --- a/classes/plugin.php +++ b/classes/plugin.php @@ -241,7 +241,7 @@ abstract class Plugin { } /** - * @param array{'article': array} $params + * @param array{'article': array|null, 'headline': array|null} $params * @return array */ function hook_render_article_api($params) { @@ -532,7 +532,7 @@ abstract class Plugin { * @param string $content * @param string $site_url * @param array $article - * @return string + * @return string|array */ function hook_article_image($enclosures, $content, $site_url, $article) { user_error("Dummy method invoked.", E_USER_ERROR); From f5c881586bd6eb41036e36625f426c00aa993c4f Mon Sep 17 00:00:00 2001 From: wn_ Date: Sun, 14 Nov 2021 16:59:21 +0000 Subject: [PATCH 067/118] Handle potentially null link, title, etc. in FeedParser. --- classes/feedparser.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/classes/feedparser.php b/classes/feedparser.php index 05412ef1e..abf1545f8 100644 --- a/classes/feedparser.php +++ b/classes/feedparser.php @@ -11,18 +11,18 @@ class FeedParser { private $libxml_errors = []; /** @var array */ - private $items; + private $items = []; - /** @var string */ + /** @var string|null */ private $link; - /** @var string */ + /** @var string|null */ private $title; - /** @var int */ + /** @var FeedParser::FEED_*|null */ private $type; - /** @var DOMXPath */ + /** @var DOMXPath|null */ private $xpath; const FEED_RDF = 0; @@ -49,8 +49,6 @@ class FeedParser { } } libxml_clear_errors(); - - $this->items = array(); } function init() : void { @@ -208,11 +206,11 @@ class FeedParser { } function get_link() : string { - return clean($this->link); + return clean($this->link ?? ''); } function get_title() : string { - return clean($this->title); + return clean($this->title ?? ''); } /** @return array */ From 6bd6a14c207ddaf35256b11dd381093d57ef38a4 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 20:19:12 +0300 Subject: [PATCH 068/118] revise phpdoc annotations for hook_sanitize() --- classes/plugin.php | 4 ++-- plugins/af_youtube_embed/init.php | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/classes/plugin.php b/classes/plugin.php index 290c4cc3b..55ccff9c3 100644 --- a/classes/plugin.php +++ b/classes/plugin.php @@ -232,7 +232,7 @@ abstract class Plugin { * @param array $allowed_elements * @param array $disallowed_attributes * @param int $article_id - * @return DOMDocument + * @return DOMDocument|array> */ function hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) { user_error("Dummy method invoked.", E_USER_ERROR); @@ -519,7 +519,7 @@ abstract class Plugin { /** * @param string $url - * @return string + * @return string|false */ function hook_get_full_text($url) { user_error("Dummy method invoked.", E_USER_ERROR); diff --git a/plugins/af_youtube_embed/init.php b/plugins/af_youtube_embed/init.php index a1be5562a..ff44bb291 100644 --- a/plugins/af_youtube_embed/init.php +++ b/plugins/af_youtube_embed/init.php @@ -32,6 +32,8 @@ class Af_Youtube_Embed extends Plugin { "; } + + return ""; } function api_version() { From afdb4b00729c8589e99e4ba27981450d1433d9b2 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 20:26:05 +0300 Subject: [PATCH 069/118] set phpdoc annotations for auth_base --- classes/auth/base.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/classes/auth/base.php b/classes/auth/base.php index 9950cbf07..d8128400d 100644 --- a/classes/auth/base.php +++ b/classes/auth/base.php @@ -12,8 +12,14 @@ abstract class Auth_Base extends Plugin implements IAuthModule { 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); @@ -41,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); } From d17b79311e9c80576a8bf392b9d1dbee7fa8fbdc Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 20:33:37 +0300 Subject: [PATCH 070/118] set missing annotations in af_comics --- plugins/af_comics/filter_base.php | 18 ++++++++++++++++++ plugins/af_comics/filters/af_comics_tfd.php | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/plugins/af_comics/filter_base.php b/plugins/af_comics/filter_base.php index 5c82bc870..83bc48184 100644 --- a/plugins/af_comics/filter_base.php +++ b/plugins/af_comics/filter_base.php @@ -1,20 +1,38 @@ */ public abstract function supported(); + + /** + * @param array $article + * @return bool + */ public abstract function process(&$article); public function __construct(/*PluginHost $host*/) { } + /** + * @param string $url + * @return string|false + */ public function on_subscribe($url) { return false; } + /** + * @param string $url + * @return array{"title": string, "site_url": string}|false + */ public function on_basic_info($url) { return false; } + /** + * @param string $url + * @return string|false + */ public function on_fetch($url) { return false; } diff --git a/plugins/af_comics/filters/af_comics_tfd.php b/plugins/af_comics/filters/af_comics_tfd.php index 19ca43a24..2010da37e 100644 --- a/plugins/af_comics/filters/af_comics_tfd.php +++ b/plugins/af_comics/filters/af_comics_tfd.php @@ -12,7 +12,7 @@ class Af_Comics_Tfd extends Af_ComicFilter { false, false, 0, "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)"); - if (!$res) return $article; + if (!$res) return false; $doc = new DOMDocument(); From cfc31fc692d34a61ec1974e2c159efebf6b511be Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 20:36:55 +0300 Subject: [PATCH 071/118] set annotations/types in af_psql_trgm --- plugins/af_psql_trgm/init.php | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/plugins/af_psql_trgm/init.php b/plugins/af_psql_trgm/init.php index aea24fea7..89b66e20b 100644 --- a/plugins/af_psql_trgm/init.php +++ b/plugins/af_psql_trgm/init.php @@ -202,7 +202,7 @@ class Af_Psql_Trgm extends Plugin { filter_unknown_feeds( - $this->get_stored_array("enabled_feeds")); + $this->host->get_array($this, "enabled_feeds")); $this->host->set($this, "enabled_feeds", $enabled_feeds); ?> @@ -227,7 +227,7 @@ class Af_Psql_Trgm extends Plugin { } function hook_prefs_edit_feed($feed_id) { - $enabled_feeds = $this->get_stored_array("enabled_feeds"); + $enabled_feeds = $this->host->get_array($this, "enabled_feeds"); ?>
    @@ -244,7 +244,7 @@ class Af_Psql_Trgm extends Plugin { } function hook_prefs_save_feed($feed_id) { - $enabled_feeds = $this->get_stored_array("enabled_feeds"); + $enabled_feeds = $this->host->get_array($this, "enabled_feeds"); $enable = checkbox_to_sql_bool($_POST["trgm_similarity_enabled"] ?? ""); $key = array_search($feed_id, $enabled_feeds); @@ -273,7 +273,7 @@ class Af_Psql_Trgm extends Plugin { if (!$enable_globally && !in_array($article["feed"]["id"], - $this->get_stored_array("enabled_feeds"))) { + $this->host->get_array($this,"enabled_feeds"))) { return $article; } @@ -281,14 +281,14 @@ class Af_Psql_Trgm extends Plugin { $similarity = (float) $this->host->get($this, "similarity", $this->default_similarity); if ($similarity < 0.01) { - Debug::log("af_psql_trgm: similarity is set too low ($similarity)", Debug::$LOG_EXTENDED); + Debug::log("af_psql_trgm: similarity is set too low ($similarity)", Debug::LOG_EXTENDED); return $article; } $min_title_length = (int) $this->host->get($this, "min_title_length", $this->default_min_length); if (mb_strlen($article["title"]) < $min_title_length) { - Debug::log("af_psql_trgm: article title is too short (min: $min_title_length)", Debug::$LOG_EXTENDED); + Debug::log("af_psql_trgm: article title is too short (min: $min_title_length)", Debug::LOG_EXTENDED); return $article; } @@ -327,10 +327,10 @@ class Af_Psql_Trgm extends Plugin { $row = $sth->fetch(); $similarity_result = $row['ms']; - Debug::log("af_psql_trgm: similarity result for $title_escaped: $similarity_result", Debug::$LOG_EXTENDED); + Debug::log("af_psql_trgm: similarity result for $title_escaped: $similarity_result", Debug::LOG_EXTENDED); if ($similarity_result >= $similarity) { - Debug::log("af_psql_trgm: marking article as read ($similarity_result >= $similarity)", Debug::$LOG_EXTENDED); + Debug::log("af_psql_trgm: marking article as read ($similarity_result >= $similarity)", Debug::LOG_EXTENDED); $article["force_catchup"] = true; } @@ -342,7 +342,12 @@ class Af_Psql_Trgm extends Plugin { return 2; } - private function filter_unknown_feeds($enabled_feeds) { + /** + * @param array $enabled_feeds + * @return array + * @throws PDOException + */ + private function filter_unknown_feeds($enabled_feeds) : array { $tmp = array(); foreach ($enabled_feeds as $feed) { @@ -357,14 +362,4 @@ class Af_Psql_Trgm extends Plugin { return $tmp; } - - private function get_stored_array($name) { - $tmp = $this->host->get($this, $name); - - if (!is_array($tmp)) $tmp = []; - - return $tmp; - } - - } From 80291ffe0c3b43858cf9db3e5ffe5470259503ac Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 20:51:22 +0300 Subject: [PATCH 072/118] deal with phpstan warnings in plugins/af_redditimgur.php --- plugins/af_redditimgur/init.php | 78 ++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/plugins/af_redditimgur/init.php b/plugins/af_redditimgur/init.php index bd4032050..aeb75df3a 100755 --- a/plugins/af_redditimgur/init.php +++ b/plugins/af_redditimgur/init.php @@ -3,10 +3,20 @@ class Af_RedditImgur extends Plugin { /** @var PluginHost $host */ private $host; + + /** @var array */ private $domain_blacklist = [ "github.com" ]; + + /** @var bool */ private $dump_json_data = false; + + /** @var array */ private $fallback_preview_urls = []; + + /** @var int */ private $default_max_score = 100; + + /** @var array> */ private $generated_enclosures = []; function about() { @@ -118,7 +128,7 @@ class Af_RedditImgur extends Plugin { $data (this is a huge blob of random crap returned by reddit API) + * @param DOMDocument $doc + * @param DOMXPath $xpath + * @param DOMElement $anchor + * @return bool + */ + private function process_post_media(array $data, DOMDocument $doc, DOMXPath $xpath, DOMElement $anchor) : bool { $found = 0; if (isset($data["media_metadata"])) { @@ -242,14 +259,21 @@ class Af_RedditImgur extends Plugin { } } - return $found; + return $found > 0; } /* function score_convert(int $value, int $from1, int $from2, int $to1, int $to2) { return ($value - $from1) / ($from2 - $from1) * ($to2 - $to1) + $to1; } */ - private function inline_stuff(&$article, &$doc, $xpath) { + /** + * @param array $article + * @param DOMDocument $doc + * @param DOMXPath $xpath + * @return bool + * @throws PDOException + */ + private function inline_stuff(array &$article, DOMDocument &$doc, DOMXpath $xpath) : bool { $max_score = (int) $this->host->get($this, "max_score", $this->default_max_score); $import_score = (bool) $this->host->get($this, "import_score", $this->default_max_score); @@ -263,7 +287,7 @@ class Af_RedditImgur extends Plugin { $this->generated_enclosures = []; - // embed anchor element, before reddit post layout + /** @var DOMElement|false $anchor -- embed anchor element, before reddit
    post layout */ $anchor = $xpath->query('//body/*')->item(0); // deal with json-provided media content first @@ -583,7 +607,7 @@ class Af_RedditImgur extends Plugin { if ($found) $this->remove_post_thumbnail($doc, $xpath); - return $found; + return $found > 0; } function hook_article_filter($article) { @@ -651,14 +675,14 @@ class Af_RedditImgur extends Plugin { return 2; } - private function remove_post_thumbnail($doc, $xpath) { + private function remove_post_thumbnail(DOMDocument $doc, DOMXpath $xpath) : void { $thumb = $xpath->query("//td/a/img[@src]")->item(0); if ($thumb) $thumb->parentNode->parentNode->removeChild($thumb->parentNode); } - private function handle_as_image($doc, $entry, $image_url, $link_url = false) { + private function handle_as_image(DOMDocument $doc, DOMElement $entry, string $image_url, string $link_url = "") : void { $img = $doc->createElement("img"); $img->setAttribute("src", $image_url); @@ -677,7 +701,7 @@ class Af_RedditImgur extends Plugin { $entry->parentNode->insertBefore($p, $entry); } - private function handle_as_video($doc, $entry, $source_stream, $poster_url = false) { + private function handle_as_video(DOMDocument $doc, DOMElement $entry, string $source_stream, string $poster_url = "") : void { Debug::log("handle_as_video: $source_stream", Debug::LOG_VERBOSE); @@ -709,7 +733,7 @@ class Af_RedditImgur extends Plugin { return $method === "testurl"; } - function testurl() { + function testurl() : void { $url = clean($_POST["url"] ?? ""); $article_url = clean($_POST["article_url"] ?? ""); @@ -804,8 +828,8 @@ class Af_RedditImgur extends Plugin { } /** $useragent defaults to Config::get_user_agent() */ - private function get_header($url, $header, $useragent = false) { - $ret = false; + private function get_header(string $url, int $header, string $useragent = "") : string { + $ret = ""; if (function_exists("curl_init")) { $ch = curl_init($url); @@ -823,16 +847,24 @@ class Af_RedditImgur extends Plugin { return $ret; } - private function get_content_type($url, $useragent = false) { + private function get_content_type(string $url, string $useragent = "") : string { return $this->get_header($url, CURLINFO_CONTENT_TYPE, $useragent); } - // @phpstan-ignore-next-line - private function get_location($url, $useragent = false) { + /*private function get_location(string $url, string $useragent = "") : string { return $this->get_header($url, CURLINFO_EFFECTIVE_URL, $useragent); - } + }*/ - private function readability($article, $url, $doc, $xpath, $debug = false) { + /** + * @param array $article + * @param string $url + * @param DOMDocument $doc + * @param DOMXPath $xpath + * @param bool $debug + * @return array + * @throws PDOException + */ + private function readability(array $article, string $url, DOMDocument $doc, DOMXpath $xpath, bool $debug = false) : array { if (function_exists("curl_init") && $this->host->get($this, "enable_readability") && mb_strlen(strip_tags($article["content"])) <= 150) { @@ -864,7 +896,12 @@ class Af_RedditImgur extends Plugin { return $article; } - private function is_blacklisted($src, $also_blacklist = []) { + /** + * @param string $src + * @param array $also_blacklist + * @return bool + */ + private function is_blacklisted(string $src, array $also_blacklist = []) : bool { $src_domain = parse_url($src, PHP_URL_HOST); foreach (array_merge($this->domain_blacklist, $also_blacklist) as $domain) { @@ -880,7 +917,7 @@ class Af_RedditImgur extends Plugin { return $this->hook_render_article_cdm($article); } - private function rewrite_to_teddit($str) { + private function rewrite_to_teddit(string $str) : string { if (strpos($str, "reddit.com") !== false) { return preg_replace("/https?:\/\/([a-z]+\.)?reddit\.com/", "https://teddit.net", $str); } @@ -888,7 +925,7 @@ class Af_RedditImgur extends Plugin { return $str; } - private function rewrite_to_reddit($str) { + private function rewrite_to_reddit(string $str) : string { if (strpos($str, "teddit.net") !== false) { $str = preg_replace("/https?:\/\/teddit.net/", "https://reddit.com", $str); @@ -899,7 +936,6 @@ class Af_RedditImgur extends Plugin { return $str; } - function hook_render_article_cdm($article) { if ($this->host->get($this, "reddit_to_teddit")) { $need_saving = false; From 931a7533ce70f68b6890368220214e8d3f566180 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 20:53:30 +0300 Subject: [PATCH 073/118] adjust some return types in urlhelper --- classes/urlhelper.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/classes/urlhelper.php b/classes/urlhelper.php index 351d66b8d..5d0d80a41 100644 --- a/classes/urlhelper.php +++ b/classes/urlhelper.php @@ -99,9 +99,8 @@ class UrlHelper { } } - // extended filtering involves validation for safe ports and loopback - /** - * @return bool|string false if something went wrong, otherwise the URL string + /** 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) { @@ -166,7 +165,7 @@ class UrlHelper { } /** - * @return bool|string + * @return false|string */ static function resolve_redirects(string $url, int $timeout, int $nest = 0) { @@ -545,7 +544,7 @@ class UrlHelper { } /** - * @return bool|string false if the provided URL didn't match expected patterns, otherwise the video ID string + * @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); From 91c9a735327ba04fd1e10f81dfca288ae11d779d Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 20:59:49 +0300 Subject: [PATCH 074/118] deal with phpstan warnings in plugins/cache_starred_images.php --- plugins/cache_starred_images/init.php | 31 +++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/plugins/cache_starred_images/init.php b/plugins/cache_starred_images/init.php index 36e8b73f0..731007b5b 100755 --- a/plugins/cache_starred_images/init.php +++ b/plugins/cache_starred_images/init.php @@ -1,11 +1,14 @@ host->get_owner_uid() . "..."); @@ -53,7 +56,7 @@ class Cache_Starred_Images extends Plugin { $usth = $this->pdo->prepare("UPDATE ttrss_entries SET plugin_data = ? WHERE id = ?"); while ($line = $sth->fetch()) { - Debug::log("processing article " . $line["title"], Debug::$LOG_VERBOSE); + Debug::log("processing article " . $line["title"], Debug::LOG_VERBOSE); if ($line["site_url"]) { $success = $this->cache_article_images($line["content"], $line["site_url"], $line["owner_uid"], $line["id"]); @@ -115,7 +118,7 @@ class Cache_Starred_Images extends Plugin { foreach ($entries as $entry) { if ($entry->hasAttribute('src')) { - $src = rewrite_relative_url($site_url, $entry->getAttribute('src')); + $src = UrlHelper::rewrite_relative($site_url, $entry->getAttribute('src')); $local_filename = $article_id . "-" . sha1($src); @@ -130,11 +133,11 @@ class Cache_Starred_Images extends Plugin { return $doc; } - private function cache_url($article_id, $url) { + private function cache_url(int $article_id, string $url) : bool { $local_filename = $article_id . "-" . sha1($url); if (!$this->cache->exists($local_filename)) { - Debug::log("cache_images: downloading: $url to $local_filename", Debug::$LOG_VERBOSE); + Debug::log("cache_images: downloading: $url to $local_filename", Debug::LOG_VERBOSE); $data = UrlHelper::fetch(["url" => $url, "max_size" => Config::get(Config::MAX_CACHE_FILE_SIZE)]); @@ -150,16 +153,16 @@ class Cache_Starred_Images extends Plugin { return false; } - private function cache_article_images($content, $site_url, $owner_uid, $article_id) { + private function cache_article_images(string $content, string $site_url, int $owner_uid, int $article_id) : bool { $status_filename = $article_id . "-" . sha1($site_url) . ".status"; /* housekeeping might run as a separate user, in this case status/media might not be writable */ if (!$this->cache->is_writable($status_filename)) { - Debug::log("status not writable: $status_filename", Debug::$LOG_VERBOSE); + Debug::log("status not writable: $status_filename", Debug::LOG_VERBOSE); return false; } - Debug::log("status: $status_filename", Debug::$LOG_VERBOSE); + Debug::log("status: $status_filename", Debug::LOG_VERBOSE); if ($this->cache->exists($status_filename)) $status = json_decode($this->cache->get($status_filename), true); @@ -170,7 +173,7 @@ class Cache_Starred_Images extends Plugin { // only allow several download attempts for article if ($status["attempt"] > $this->max_cache_attempts) { - Debug::log("too many attempts for $site_url", Debug::$LOG_VERBOSE); + Debug::log("too many attempts for $site_url", Debug::LOG_VERBOSE); return false; } @@ -194,7 +197,7 @@ class Cache_Starred_Images extends Plugin { $has_images = true; - $src = rewrite_relative_url($site_url, $entry->getAttribute('src')); + $src = UrlHelper::rewrite_relative($site_url, $entry->getAttribute('src')); if ($this->cache_url($article_id, $src)) { $success = true; @@ -210,7 +213,7 @@ class Cache_Starred_Images extends Plugin { while ($enc = $esth->fetch()) { $has_images = true; - $url = rewrite_relative_url($site_url, $enc["content_url"]); + $url = UrlHelper::rewrite_relative($site_url, $enc["content_url"]); if ($this->cache_url($article_id, $url)) { $success = true; From 812f5f532e73fe2a9a9aabc4c5bec97b71f5993a Mon Sep 17 00:00:00 2001 From: wn_ Date: Sun, 14 Nov 2021 17:57:17 +0000 Subject: [PATCH 075/118] Address PHPStan warning in 'classes/mailer.php'. --- classes/mailer.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/classes/mailer.php b/classes/mailer.php index a0f232800..339b2895a 100644 --- a/classes/mailer.php +++ b/classes/mailer.php @@ -32,8 +32,6 @@ class Mailer { // 4. set error message if needed via passed Mailer instance function set_error() foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEND_MAIL) as $p) { - // Implemented via plugin, so ignore the undefined method 'hook_send_mail'. - // @phpstan-ignore-next-line $rc = $p->hook_send_mail($this, $params); if ($rc == 1) From 9326ed605f409366baa0488b8c86abf7343a6281 Mon Sep 17 00:00:00 2001 From: wn_ Date: Sun, 14 Nov 2021 17:59:02 +0000 Subject: [PATCH 076/118] Address PHPStan warning in 'classes/pref/filters.php'. --- classes/pref/filters.php | 44 ++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/classes/pref/filters.php b/classes/pref/filters.php index 4faa435aa..6e6e3d9ee 100755 --- a/classes/pref/filters.php +++ b/classes/pref/filters.php @@ -56,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 */ + $filter_types = []; + while ($line = $res->fetch()) { $filter_types[$line["id"]] = $line["name"]; } @@ -64,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, 'name': string}|null */ $rule = json_decode($r, true); if ($rule && $rctr < 5) { @@ -72,19 +77,21 @@ class Pref_Filters extends Handler_Protected { unset($rule["filter_type"]); $scope_inner_qparts = []; + + /** @var int|string $feed_id may be a category string (e.g. 'CAT:7') or feed ID int */ foreach ($rule["feed_id"] as $feed_id) { - if (strpos($feed_id, "CAT:") === 0) { - $cat_id = (int) substr($feed_id, 4); - array_push($scope_inner_qparts, "cat_id = " . $this->pdo->quote($cat_id)); - } else if ($feed_id > 0) { - array_push($scope_inner_qparts, "feed_id = " . $this->pdo->quote($feed_id)); - } - } + if (strpos("$feed_id", "CAT:") === 0) { + $cat_id = (int) substr("$feed_id", 4); + array_push($scope_inner_qparts, "cat_id = " . $cat_id); + } else if (is_numeric($feed_id) && $feed_id > 0) { + array_push($scope_inner_qparts, "feed_id = " . (int)$feed_id); + } + } - if (count($scope_inner_qparts) > 0) { - array_push($scope_qparts, "(" . implode(" OR ", $scope_inner_qparts) . ")"); - } + if (count($scope_inner_qparts) > 0) { + array_push($scope_qparts, "(" . implode(" OR ", $scope_inner_qparts) . ")"); + } array_push($filter["rules"], $rule); @@ -495,7 +502,7 @@ class Pref_Filters extends Handler_Protected { } function editSave(): void { - $filter_id = clean($_REQUEST["id"]); + $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)); @@ -526,7 +533,7 @@ class Pref_Filters extends Handler_Protected { $sth->execute(array_merge($ids, [$_SESSION['uid']])); } - private function _save_rules_and_actions($filter_id): void { + 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]); @@ -698,7 +705,8 @@ class Pref_Filters extends Handler_Protected { } function editrule(): void { - $feed_ids = array_map("intval", explode(",", clean($_REQUEST["ids"]))); + /** @var array */ + $feed_ids = explode(",", clean($_REQUEST["ids"])); print json_encode([ "multiselect" => $this->_feed_multi_select("feed_id", $feed_ids, 'required="1" style="width : 100%; height : 300px" dojoType="fox.form.ValidationMultiSelect"') @@ -840,9 +848,11 @@ class Pref_Filters extends Handler_Protected { $this->pdo->commit(); } - private function _feed_multi_select(string $id, $default_ids = [], - $attributes = "", $include_all_feeds = true, - $root_id = null, $nest_level = 0): string { + /** + * @param array $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(); From f537502fce498118543d47b3d1cb463104f25b1d Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 21:09:53 +0300 Subject: [PATCH 077/118] deal with (most of) phpstan warnings in auth_internal and auth_remote --- plugins/auth_internal/init.php | 22 +++++++++++++++++++--- plugins/auth_remote/init.php | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/plugins/auth_internal/init.php b/plugins/auth_internal/init.php index b66f7719b..1bf3d6a24 100644 --- a/plugins/auth_internal/init.php +++ b/plugins/auth_internal/init.php @@ -130,7 +130,7 @@ class Auth_Internal extends Auth_Base { } if ($login) { - $try_user_id = $this->find_user_by_login($login); + $try_user_id = UserHelper::find_user_by_login($login); if ($try_user_id) { return $this->check_password($try_user_id, $password); @@ -140,6 +140,14 @@ class Auth_Internal extends Auth_Base { return false; } + /** + * @param int $owner_uid + * @param string $password + * @param string $service + * @return int|false (false if failed, user id otherwise) + * @throws PDOException + * @throws Exception + */ function check_password(int $owner_uid, string $password, string $service = '') { $user = ORM::for_table('ttrss_users')->find_one($owner_uid); @@ -203,7 +211,7 @@ class Auth_Internal extends Auth_Base { return false; } - function change_password($owner_uid, $old_password, $new_password) { + function change_password(int $owner_uid, string $old_password, string $new_password) : string { if ($this->check_password($owner_uid, $old_password)) { @@ -246,7 +254,15 @@ class Auth_Internal extends Auth_Base { } } - private function check_app_password($login, $password, $service) { + /** + * @param string $login + * @param string $password + * @param string $service + * @return false|int (false if failed, user id otherwise) + * @throws PDOException + * @throws Exception + */ + private function check_app_password(string $login, string $password, string $service) { $sth = $this->pdo->prepare("SELECT p.id, p.pwd_hash, u.id AS uid FROM ttrss_app_passwords p, ttrss_users u WHERE p.owner_uid = u.id AND LOWER(u.login) = LOWER(?) AND service = ?"); diff --git a/plugins/auth_remote/init.php b/plugins/auth_remote/init.php index 35ee9e31d..9c15d3368 100644 --- a/plugins/auth_remote/init.php +++ b/plugins/auth_remote/init.php @@ -12,7 +12,7 @@ class Auth_Remote extends Auth_Base { $host->add_hook($host::HOOK_AUTH_USER, $this); } - function get_login_by_ssl_certificate() { + function get_login_by_ssl_certificate() : string { $cert_serial = Pref_Prefs::_get_ssl_certificate_id(); if ($cert_serial) { From 5f808051b2bdb2e43f16693ba19b20940944d556 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 21:14:21 +0300 Subject: [PATCH 078/118] deal with phpstan warnings in auto_assign_labels and bookmarklets --- plugins/auto_assign_labels/init.php | 7 ++++++- plugins/bookmarklets/init.php | 7 ++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/auto_assign_labels/init.php b/plugins/auto_assign_labels/init.php index 84fce8d64..b2e5718ea 100755 --- a/plugins/auto_assign_labels/init.php +++ b/plugins/auto_assign_labels/init.php @@ -11,7 +11,12 @@ class Auto_Assign_Labels extends Plugin { $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); } - function get_all_labels_filter_format($owner_uid) { + /** + * @param int $owner_uid + * @return array> + * @throws PDOException + */ + private function get_all_labels_filter_format(int $owner_uid) : array { $rv = array(); // TODO: use Labels::get_all() diff --git a/plugins/bookmarklets/init.php b/plugins/bookmarklets/init.php index 4bd527623..72aeb2c38 100644 --- a/plugins/bookmarklets/init.php +++ b/plugins/bookmarklets/init.php @@ -1,5 +1,7 @@ Date: Sun, 14 Nov 2021 21:20:59 +0300 Subject: [PATCH 079/118] deal with phpstan warnings in plugins/note, nsfw, and share --- plugins/note/init.php | 4 ++-- plugins/nsfw/init.php | 11 +++++++++-- plugins/share/init.php | 5 +---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/plugins/note/init.php b/plugins/note/init.php index bc3df64b1..28d3c8c3a 100644 --- a/plugins/note/init.php +++ b/plugins/note/init.php @@ -21,7 +21,7 @@ class Note extends Plugin { style='cursor : pointer' title='".__('Edit article note')."'>note"; } - function edit() { + function edit() : void { $id = clean($_REQUEST['id']); $sth = $this->pdo->prepare("SELECT note FROM ttrss_user_entries WHERE @@ -49,7 +49,7 @@ class Note extends Plugin { $article + * @return array + * @throws PDOException + */ + private function rewrite_contents(array $article) : array { $tags = explode(",", $this->host->get($this, "tags")); $article_tags = $article["tags"]; @@ -101,7 +108,7 @@ class NSFW extends Plugin { "> - - - - + From c3fbf56984477393669727a296ec229c8d303fbf Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 21:25:56 +0300 Subject: [PATCH 080/118] deal with most of warnings in plugins/af_readability --- plugins/af_readability/init.php | 41 +++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/plugins/af_readability/init.php b/plugins/af_readability/init.php index 8e75d0c2e..a13f2a5b2 100755 --- a/plugins/af_readability/init.php +++ b/plugins/af_readability/init.php @@ -190,7 +190,11 @@ class Af_Readability extends Plugin { return $article; } - public function extract_content($url) { + /** + * @param string $url + * @return string|false + */ + public function extract_content(string $url) { $tmp = UrlHelper::fetch([ "url" => $url, @@ -224,13 +228,13 @@ class Af_Readability extends Plugin { foreach ($entries as $entry) { if ($entry->hasAttribute("href")) { $entry->setAttribute("href", - rewrite_relative_url(UrlHelper::$fetch_effective_url, $entry->getAttribute("href"))); + UrlHelper::rewrite_relative(UrlHelper::$fetch_effective_url, $entry->getAttribute("href"))); } if ($entry->hasAttribute("src")) { $entry->setAttribute("src", - rewrite_relative_url(UrlHelper::$fetch_effective_url, $entry->getAttribute("src"))); + UrlHelper::rewrite_relative(UrlHelper::$fetch_effective_url, $entry->getAttribute("src"))); } } @@ -246,7 +250,13 @@ class Af_Readability extends Plugin { return false; } - function process_article($article, $append_mode) { + /** + * @param array $article + * @param bool $append_mode + * @return array + * @throws PDOException + */ + function process_article(array $article, bool $append_mode) : array { $extracted_content = $this->extract_content($article["link"]); @@ -263,12 +273,14 @@ class Af_Readability extends Plugin { return $article; } - private function get_stored_array($name) { - $tmp = $this->host->get($this, $name); - - if (!is_array($tmp)) $tmp = []; - - return $tmp; + /** + * @param string $name + * @return array + * @throws PDOException + * @deprecated + */ + private function get_stored_array(string $name) : array { + return $this->host->get_array($this, $name); } function hook_article_filter($article) { @@ -306,7 +318,12 @@ class Af_Readability extends Plugin { return 2; } - private function filter_unknown_feeds($enabled_feeds) { + /** + * @param array $enabled_feeds + * @return array + * @throws PDOException + */ + private function filter_unknown_feeds(array $enabled_feeds) : array { $tmp = array(); foreach ($enabled_feeds as $feed) { @@ -322,7 +339,7 @@ class Af_Readability extends Plugin { return $tmp; } - function embed() { + function embed() : void { $article_id = (int) $_REQUEST["id"]; $sth = $this->pdo->prepare("SELECT link FROM ttrss_entries WHERE id = ?"); From 56cf425e457dbf6cb75a899c67f527b2efe340d7 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Nov 2021 23:03:25 +0300 Subject: [PATCH 081/118] revise prototype for hook_enclosure_imported --- classes/plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/plugin.php b/classes/plugin.php index 55ccff9c3..333f605ca 100644 --- a/classes/plugin.php +++ b/classes/plugin.php @@ -558,9 +558,9 @@ abstract class Plugin { } /** - * @param array $enclosure + * @param object $enclosure * @param int $feed - * @return array ($enclosure) + * @return object ($enclosure) */ function hook_enclosure_imported($enclosure, $feed) { user_error("Dummy method invoked.", E_USER_ERROR); From b8f0627a0e5480d872eb1d64224cdfbfcd92b1b8 Mon Sep 17 00:00:00 2001 From: wn_ Date: Sun, 14 Nov 2021 20:03:57 +0000 Subject: [PATCH 082/118] Address PHPStan warning in 'classes/pref/labels.php'. --- classes/pref/labels.php | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/classes/pref/labels.php b/classes/pref/labels.php index 81b85481b..a50a85a66 100644 --- a/classes/pref/labels.php +++ b/classes/pref/labels.php @@ -7,7 +7,7 @@ class Pref_Labels extends Handler_Protected { 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 */ + $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 { ?>
    From abab2a94e8443f205719e9a66c66e3f00a371195 Mon Sep 17 00:00:00 2001 From: wn_ Date: Sun, 14 Nov 2021 20:12:37 +0000 Subject: [PATCH 083/118] Address PHPStan warning in 'classes/pref/prefs.php'. Also update 'select_hash' and 'select_tag' values param, which can have int or string keys. --- classes/pref/prefs.php | 245 ++++++++++++++++++++++++----------------- include/controls.php | 6 +- 2 files changed, 145 insertions(+), 106 deletions(-) diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php index 025d8fda2..d3a5a1370 100644 --- a/classes/pref/prefs.php +++ b/classes/pref/prefs.php @@ -2,12 +2,21 @@ use chillerlan\QRCode; class Pref_Prefs extends Handler_Protected { - + // TODO: class properties can be switched to PHP typing if/when the minimum PHP_VERSION is raised to 7.4.0+ + /** @var array> */ private $pref_help = []; + + /** @var array> pref items are Prefs::*|Pref_Prefs::BLOCK_SEPARATOR (PHPStan was complaining) */ private $pref_item_map = []; + + /** @var array */ private $pref_help_bottom = []; + + /** @var array */ 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,6 +26,7 @@ class Pref_Prefs extends Handler_Protected { const PI_ERR_PLUGIN_NOT_FOUND = "PI_ERR_PLUGIN_NOT_FOUND"; const PI_ERR_NO_WORKDIR = "PI_ERR_NO_WORKDIR"; + /** @param string $method */ function csrf_ignore($method) : bool { $csrf_ignored = array("index", "updateself", "otpqrcode"); @@ -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 { 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 {
    @@ -534,35 +544,38 @@ class Pref_Prefs extends Handler_Protected { */ $prefs_available = []; + + /** @var array */ $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 "
    "; 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 ",postMixInProperties:function(){if(!this.value&&this.srcNodeRef){this.value=this.srcNodeRef.value;}this.inherited(arguments);},buildRendering:function(){this.inherited(arguments);if(has("ie")&&this.cols){_34b.add(this.textbox,"dijitTextAreaCols");}},filter:function(_34d){if(_34d){_34d=_34d.replace(/\r/g,"");}return this.inherited(arguments);},_onInput:function(e){if(this.maxLength){var _34e=parseInt(this.maxLength);var _34f=this.textbox.value.replace(/\r/g,"");var _350=_34f.length-_34e;if(_350>0){var _351=this.textbox;if(_351.selectionStart){var pos=_351.selectionStart;var cr=0;if(has("opera")){cr=(this.textbox.value.substring(0,pos).match(/\r/g)||[]).length;}this.textbox.value=_34f.substring(0,pos-_350-cr)+_34f.substring(pos-cr);_351.setSelectionRange(pos-_350,pos-_350);}else{if(this.ownerDocument.selection){_351.focus();var _352=this.ownerDocument.selection.createRange();_352.moveStart("character",-_350);_352.text="";_352.select();}}}}this.inherited(arguments);}});});},"dijit/_base/window":function(){define(["dojo/window","../main"],function(_353,_354){_354.getDocumentWindow=function(doc){return _353.get(doc);};});},"dijit/PopupMenuItem":function(){define(["dojo/_base/declare","dojo/dom-style","dojo/_base/lang","dojo/query","./popup","./registry","./MenuItem","./hccss"],function(_355,_356,lang,_357,pm,_358,_359){return _355("dijit.PopupMenuItem",_359,{baseClass:"dijitMenuItem dijitPopupMenuItem",_fillContent:function(){if(this.srcNodeRef){var _35a=_357("*",this.srcNodeRef);this.inherited(arguments,[_35a[0]]);this.dropDownContainer=this.srcNodeRef;}},_openPopup:function(_35b,_35c){var _35d=this.popup;pm.open(lang.delegate(_35b,{popup:this.popup,around:this.domNode}));if(_35c&&_35d.focus){_35d.focus();}},_closePopup:function(){pm.close(this.popup);this.popup.parentMenu=null;},startup:function(){if(this._started){return;}this.inherited(arguments);if(!this.popup){var node=_357("[widgetId]",this.dropDownContainer)[0];this.popup=_358.byNode(node);}this.ownerDocumentBody.appendChild(this.popup.domNode);this.popup.domNode.setAttribute("aria-labelledby",this.containerNode.id);this.popup.startup();this.popup.domNode.style.display="none";if(this.arrowWrapper){_356.set(this.arrowWrapper,"visibility","");}this.focusNode.setAttribute("aria-haspopup","true");},destroyDescendants:function(_35e){if(this.popup){if(!this.popup._destroyed){this.popup.destroyRecursive(_35e);}delete this.popup;}this.inherited(arguments);}});});},"dojo/hccss":function(){define(["require","./_base/config","./dom-class","./dom-style","./has","./domReady","./_base/window"],function(_35f,_360,_361,_362,has,_363,win){has.add("highcontrast",function(){var div=win.doc.createElement("div");try{div.style.cssText="border: 1px solid; border-color:red green; position: absolute; height: 5px; top: -999px;"+"background-image: url(\""+(_360.blankGif||_35f.toUrl("./resources/blank.gif"))+"\");";win.body().appendChild(div);var cs=_362.getComputedStyle(div),_364=cs.backgroundImage;return cs.borderTopColor==cs.borderRightColor||(_364&&(_364=="none"||_364=="url(invalid-url:)"));}catch(e){console.warn("hccss: exception detecting high-contrast mode, document is likely hidden: "+e.toString());return false;}finally{if(has("ie")<=8){div.outerHTML="";}else{win.body().removeChild(div);}}});_363(function(){if(has("highcontrast")){_361.add(win.body(),"dj_a11y");}});return has;});},"dijit/form/RadioButton":function(){define(["dojo/_base/declare","./CheckBox","./_RadioButtonMixin"],function(_365,_366,_367){return _365("dijit.form.RadioButton",[_366,_367],{baseClass:"dijitRadio"});});},"dijit/main":function(){define(["dojo/_base/kernel"],function(dojo){return dojo.dijit;});},"dijit/_OnDijitClickMixin":function(){define(["dojo/on","dojo/_base/array","dojo/keys","dojo/_base/declare","dojo/has","./a11yclick"],function(on,_368,keys,_369,has,_36a){var ret=_369("dijit._OnDijitClickMixin",null,{connect:function(obj,_36b,_36c){return this.inherited(arguments,[obj,_36b=="ondijitclick"?_36a:_36b,_36c]);}});ret.a11yclick=_36a;return ret;});},"dijit/InlineEditBox":function(){define(["require","dojo/_base/array","dojo/aspect","dojo/_base/declare","dojo/dom-attr","dojo/dom-class","dojo/dom-construct","dojo/dom-style","dojo/i18n","dojo/_base/kernel","dojo/keys","dojo/_base/lang","dojo/on","dojo/sniff","dojo/when","./a11yclick","./focus","./_Widget","./_TemplatedMixin","./_WidgetsInTemplateMixin","./_Container","./form/Button","./form/_TextBoxMixin","./form/TextBox","dojo/text!./templates/InlineEditBox.html","dojo/i18n!./nls/common"],function(_36d,_36e,_36f,_370,_371,_372,_373,_374,i18n,_375,keys,lang,on,has,when,_376,fm,_377,_378,_379,_37a,_37b,_37c,_37d,_37e){var _37f=_370("dijit._InlineEditor",[_377,_378,_379],{templateString:_37e,contextRequire:_36d,postMixInProperties:function(){this.inherited(arguments);this.messages=i18n.getLocalization("dijit","common",this.lang);_36e.forEach(["buttonSave","buttonCancel"],function(prop){if(!this[prop]){this[prop]=this.messages[prop];}},this);},buildRendering:function(){this.inherited(arguments);var Cls=typeof this.editor=="string"?(lang.getObject(this.editor)||_36d(this.editor)):this.editor;var _380=this.sourceStyle,_381="line-height:"+_380.lineHeight+";",_382=_374.getComputedStyle(this.domNode);_36e.forEach(["Weight","Family","Size","Style"],function(prop){var _383=_380["font"+prop],_384=_382["font"+prop];if(_384!=_383){_381+="font-"+prop+":"+_380["font"+prop]+";";}},this);_36e.forEach(["marginTop","marginBottom","marginLeft","marginRight","position","left","top","right","bottom","float","clear","display"],function(prop){this.domNode.style[prop]=_380[prop];},this);var _385=this.inlineEditBox.width;if(_385=="100%"){_381+="width:100%;";this.domNode.style.display="block";}else{_381+="width:"+(_385+(Number(_385)==_385?"px":""))+";";}var _386=lang.delegate(this.inlineEditBox.editorParams,{style:_381,dir:this.dir,lang:this.lang,textDir:this.textDir});this.editWidget=new Cls(_386,this.editorPlaceholder);if(this.inlineEditBox.autoSave){this.saveButton.destroy();this.cancelButton.destroy();this.saveButton=this.cancelButton=null;_373.destroy(this.buttonContainer);}},postCreate:function(){this.inherited(arguments);var ew=this.editWidget;if(this.inlineEditBox.autoSave){this.own(_36f.after(ew,"onChange",lang.hitch(this,"_onChange"),true),on(ew,"keydown",lang.hitch(this,"_onKeyDown")));}else{if("intermediateChanges" in ew){ew.set("intermediateChanges",true);this.own(_36f.after(ew,"onChange",lang.hitch(this,"_onIntermediateChange"),true));this.saveButton.set("disabled",true);}}},startup:function(){this.editWidget.startup();this.inherited(arguments);},_onIntermediateChange:function(){this.saveButton.set("disabled",(this.getValue()==this._resetValue)||!this.enableSave());},destroy:function(){this.editWidget.destroy(true);this.inherited(arguments);},getValue:function(){var ew=this.editWidget;return String(ew.get(("displayedValue" in ew||"_getDisplayedValueAttr" in ew)?"displayedValue":"value"));},_onKeyDown:function(e){if(this.inlineEditBox.autoSave&&this.inlineEditBox.editing){if(e.altKey||e.ctrlKey){return;}if(e.keyCode==keys.ESCAPE){e.stopPropagation();e.preventDefault();this.cancel(true);}else{if(e.keyCode==keys.ENTER&&e.target.tagName=="INPUT"){e.stopPropagation();e.preventDefault();this._onChange();}}}},_onBlur:function(){this.inherited(arguments);if(this.inlineEditBox.autoSave&&this.inlineEditBox.editing){if(this.getValue()==this._resetValue){this.cancel(false);}else{if(this.enableSave()){this.save(false);}}}},_onChange:function(){if(this.inlineEditBox.autoSave&&this.inlineEditBox.editing&&this.enableSave()){fm.focus(this.inlineEditBox.displayNode);}},enableSave:function(){return this.editWidget.isValid?this.editWidget.isValid():true;},focus:function(){this.editWidget.focus();if(this.editWidget.focusNode){fm._onFocusNode(this.editWidget.focusNode);if(this.editWidget.focusNode.tagName=="INPUT"){this.defer(function(){_37c.selectInputText(this.editWidget.focusNode);});}}}});var _387=_370("dijit.InlineEditBox"+(has("dojo-bidi")?"_NoBidi":""),_377,{editing:false,autoSave:true,buttonSave:"",buttonCancel:"",renderAsHtml:false,editor:_37d,editorWrapper:_37f,editorParams:{},disabled:false,onChange:function(){},onCancel:function(){},width:"100%",value:"",noValueIndicator:has("ie")<=6?"    ✍    ":"    ✍    ",constructor:function(){this.editorParams={};},postMixInProperties:function(){this.inherited(arguments);this.displayNode=this.srcNodeRef;this.own(on(this.displayNode,_376,lang.hitch(this,"_onClick")),on(this.displayNode,"mouseover, focus",lang.hitch(this,"_onMouseOver")),on(this.displayNode,"mouseout, blur",lang.hitch(this,"_onMouseOut")));this.displayNode.setAttribute("role","button");if(!this.displayNode.getAttribute("tabIndex")){this.displayNode.setAttribute("tabIndex",0);}if(!this.value&&!("value" in this.params)){this.value=lang.trim(this.renderAsHtml?this.displayNode.innerHTML:(this.displayNode.innerText||this.displayNode.textContent||""));}if(!this.value){this.displayNode.innerHTML=this.noValueIndicator;}_372.add(this.displayNode,"dijitInlineEditBoxDisplayMode");},setDisabled:function(_388){_375.deprecated("dijit.InlineEditBox.setDisabled() is deprecated. Use set('disabled', bool) instead.","","2.0");this.set("disabled",_388);},_setDisabledAttr:function(_389){this.domNode.setAttribute("aria-disabled",_389?"true":"false");if(_389){this.displayNode.removeAttribute("tabIndex");}else{this.displayNode.setAttribute("tabIndex",0);}_372.toggle(this.displayNode,"dijitInlineEditBoxDisplayModeDisabled",_389);this._set("disabled",_389);},_onMouseOver:function(){if(!this.disabled){_372.add(this.displayNode,"dijitInlineEditBoxDisplayModeHover");}},_onMouseOut:function(){_372.remove(this.displayNode,"dijitInlineEditBoxDisplayModeHover");},_onClick:function(e){if(this.disabled){return;}if(e){e.stopPropagation();e.preventDefault();}this._onMouseOut();this.defer("edit");},edit:function(){if(this.disabled||this.editing){return;}this._set("editing",true);this._savedTabIndex=_371.get(this.displayNode,"tabIndex")||"0";if(!this.wrapperWidget){var _38a=_373.create("span",null,this.domNode,"before");var Ewc=typeof this.editorWrapper=="string"?lang.getObject(this.editorWrapper):this.editorWrapper;this.wrapperWidget=new Ewc({value:this.value,buttonSave:this.buttonSave,buttonCancel:this.buttonCancel,dir:this.dir,lang:this.lang,tabIndex:this._savedTabIndex,editor:this.editor,inlineEditBox:this,sourceStyle:_374.getComputedStyle(this.displayNode),save:lang.hitch(this,"save"),cancel:lang.hitch(this,"cancel"),textDir:this.textDir},_38a);if(!this.wrapperWidget._started){this.wrapperWidget.startup();}if(!this._started){this.startup();}}var ww=this.wrapperWidget;_372.add(this.displayNode,"dijitOffScreen");_372.remove(ww.domNode,"dijitOffScreen");_374.set(ww.domNode,{visibility:"visible"});_371.set(this.displayNode,"tabIndex","-1");var ew=ww.editWidget;var self=this;when(ew.onLoadDeferred,lang.hitch(ww,function(){ew.set(("displayedValue" in ew||"_setDisplayedValueAttr" in ew)?"displayedValue":"value",self.value);this.defer(function(){if(ww.saveButton){ww.saveButton.set("disabled","intermediateChanges" in ew);}this.focus();this._resetValue=this.getValue();});}));},_onBlur:function(){this.inherited(arguments);if(!this.editing){}},destroy:function(){if(this.wrapperWidget&&!this.wrapperWidget._destroyed){this.wrapperWidget.destroy();delete this.wrapperWidget;}this.inherited(arguments);},_showText:function(_38b){var ww=this.wrapperWidget;_374.set(ww.domNode,{visibility:"hidden"});_372.add(ww.domNode,"dijitOffScreen");_372.remove(this.displayNode,"dijitOffScreen");_371.set(this.displayNode,"tabIndex",this._savedTabIndex);if(_38b){fm.focus(this.displayNode);}},save:function(_38c){if(this.disabled||!this.editing){return;}this._set("editing",false);var ww=this.wrapperWidget;var _38d=ww.getValue();this.set("value",_38d);this._showText(_38c);},setValue:function(val){_375.deprecated("dijit.InlineEditBox.setValue() is deprecated. Use set('value', ...) instead.","","2.0");return this.set("value",val);},_setValueAttr:function(val){val=lang.trim(val);var _38e=this.renderAsHtml?val:val.replace(/&/gm,"&").replace(//gm,">").replace(/"/gm,""").replace(/\n/g,"
    ");if(this.editorParams&&this.editorParams.type==="password"){this.displayNode.innerHTML="********";}else{this.displayNode.innerHTML=_38e||this.noValueIndicator;}this._set("value",val);if(this._started){this.defer(function(){this.onChange(val);});}},getValue:function(){_375.deprecated("dijit.InlineEditBox.getValue() is deprecated. Use get('value') instead.","","2.0");return this.get("value");},cancel:function(_38f){if(this.disabled||!this.editing){return;}this._set("editing",false);this.defer("onCancel");this._showText(_38f);}});if(has("dojo-bidi")){_387=_370("dijit.InlineEditBox",_387,{_setValueAttr:function(){this.inherited(arguments);this.applyTextDir(this.displayNode);}});}_387._InlineEditor=_37f;return _387;});},"dojo/selector/acme":function(){define(["../dom","../sniff","../_base/array","../_base/lang","../_base/window"],function(dom,has,_390,lang,win){var trim=lang.trim;var each=_390.forEach;var _391=function(){return win.doc;};var _392=(_391().compatMode)=="BackCompat";var _393=">~+";var _394=false;var _395=function(){return true;};var _396=function(_397){if(_393.indexOf(_397.slice(-1))>=0){_397+=" * ";}else{_397+=" ";}var ts=function(s,e){return trim(_397.slice(s,e));};var _398=[];var _399=-1,_39a=-1,_39b=-1,_39c=-1,_39d=-1,inId=-1,_39e=-1,_39f,lc="",cc="",_3a0;var x=0,ql=_397.length,_3a1=null,_3a2=null;var _3a3=function(){if(_39e>=0){var tv=(_39e==x)?null:ts(_39e,x);_3a1[(_393.indexOf(tv)<0)?"tag":"oper"]=tv;_39e=-1;}};var _3a4=function(){if(inId>=0){_3a1.id=ts(inId,x).replace(/\\/g,"");inId=-1;}};var _3a5=function(){if(_39d>=0){_3a1.classes.push(ts(_39d+1,x).replace(/\\/g,""));_39d=-1;}};var _3a6=function(){_3a4();_3a3();_3a5();};var _3a7=function(){_3a6();if(_39c>=0){_3a1.pseudos.push({name:ts(_39c+1,x)});}_3a1.loops=(_3a1.pseudos.length||_3a1.attrs.length||_3a1.classes.length);_3a1.oquery=_3a1.query=ts(_3a0,x);_3a1.otag=_3a1.tag=(_3a1["oper"])?null:(_3a1.tag||"*");if(_3a1.tag){_3a1.tag=_3a1.tag.toUpperCase();}if(_398.length&&(_398[_398.length-1].oper)){_3a1.infixOper=_398.pop();_3a1.query=_3a1.infixOper.query+" "+_3a1.query;}_398.push(_3a1);_3a1=null;};for(;lc=cc,cc=_397.charAt(x),x=0){if(cc=="]"){if(!_3a2.attr){_3a2.attr=ts(_399+1,x);}else{_3a2.matchFor=ts((_39b||_399+1),x);}var cmf=_3a2.matchFor;if(cmf){if((cmf.charAt(0)=="\"")||(cmf.charAt(0)=="'")){_3a2.matchFor=cmf.slice(1,-1);}}if(_3a2.matchFor){_3a2.matchFor=_3a2.matchFor.replace(/\\/g,"");}_3a1.attrs.push(_3a2);_3a2=null;_399=_39b=-1;}else{if(cc=="="){var _3a8=("|~^$*".indexOf(lc)>=0)?lc:"";_3a2.type=_3a8+cc;_3a2.attr=ts(_399+1,x-_3a8.length);_39b=x+1;}}}else{if(_39a>=0){if(cc==")"){if(_39c>=0){_3a2.value=ts(_39a+1,x);}_39c=_39a=-1;}}else{if(cc=="#"){_3a6();inId=x+1;}else{if(cc=="."){_3a6();_39d=x;}else{if(cc==":"){_3a6();_39c=x;}else{if(cc=="["){_3a6();_399=x;_3a2={};}else{if(cc=="("){if(_39c>=0){_3a2={name:ts(_39c+1,x),value:null};_3a1.pseudos.push(_3a2);}_39a=x;}else{if((cc==" ")&&(lc!=cc)){_3a7();}}}}}}}}}return _398;};var _3a9=function(_3aa,_3ab){if(!_3aa){return _3ab;}if(!_3ab){return _3aa;}return function(){return _3aa.apply(window,arguments)&&_3ab.apply(window,arguments);};};var _3ac=function(i,arr){var r=arr||[];if(i){r.push(i);}return r;};var _3ad=function(n){return (1==n.nodeType);};var _3ae="";var _3af=function(elem,attr){if(!elem){return _3ae;}if(attr=="class"){return elem.className||_3ae;}if(attr=="for"){return elem.htmlFor||_3ae;}if(attr=="style"){return elem.style.cssText||_3ae;}return (_394?elem.getAttribute(attr):elem.getAttribute(attr,2))||_3ae;};var _3b0={"*=":function(attr,_3b1){return function(elem){return (_3af(elem,attr).indexOf(_3b1)>=0);};},"^=":function(attr,_3b2){return function(elem){return (_3af(elem,attr).indexOf(_3b2)==0);};},"$=":function(attr,_3b3){return function(elem){var ea=" "+_3af(elem,attr);var _3b4=ea.lastIndexOf(_3b3);return _3b4>-1&&(_3b4==(ea.length-_3b3.length));};},"~=":function(attr,_3b5){var tval=" "+_3b5+" ";return function(elem){var ea=" "+_3af(elem,attr)+" ";return (ea.indexOf(tval)>=0);};},"|=":function(attr,_3b6){var _3b7=_3b6+"-";return function(elem){var ea=_3af(elem,attr);return ((ea==_3b6)||(ea.indexOf(_3b7)==0));};},"=":function(attr,_3b8){return function(elem){return (_3af(elem,attr)==_3b8);};}};var _3b9=_391().documentElement;var _3ba=!(_3b9.nextElementSibling||"nextElementSibling" in _3b9);var _3bb=!_3ba?"nextElementSibling":"nextSibling";var _3bc=!_3ba?"previousElementSibling":"previousSibling";var _3bd=(_3ba?_3ad:_395);var _3be=function(node){while(node=node[_3bc]){if(_3bd(node)){return false;}}return true;};var _3bf=function(node){while(node=node[_3bb]){if(_3bd(node)){return false;}}return true;};var _3c0=function(node){var root=node.parentNode;root=root.nodeType!=7?root:root.nextSibling;var i=0,tret=root.children||root.childNodes,ci=(node["_i"]||node.getAttribute("_i")||-1),cl=(root["_l"]||(typeof root.getAttribute!=="undefined"?root.getAttribute("_l"):-1));if(!tret){return -1;}var l=tret.length;if(cl==l&&ci>=0&&cl>=0){return ci;}if(has("ie")&&typeof root.setAttribute!=="undefined"){root.setAttribute("_l",l);}else{root["_l"]=l;}ci=-1;for(var te=root["firstElementChild"]||root["firstChild"];te;te=te[_3bb]){if(_3bd(te)){if(has("ie")){te.setAttribute("_i",++i);}else{te["_i"]=++i;}if(node===te){ci=i;}}}return ci;};var _3c1=function(elem){return !((_3c0(elem))%2);};var _3c2=function(elem){return ((_3c0(elem))%2);};var _3c3={"checked":function(name,_3c4){return function(elem){return !!("checked" in elem?elem.checked:elem.selected);};},"disabled":function(name,_3c5){return function(elem){return elem.disabled;};},"enabled":function(name,_3c6){return function(elem){return !elem.disabled;};},"first-child":function(){return _3be;},"last-child":function(){return _3bf;},"only-child":function(name,_3c7){return function(node){return _3be(node)&&_3bf(node);};},"empty":function(name,_3c8){return function(elem){var cn=elem.childNodes;var cnl=elem.childNodes.length;for(var x=cnl-1;x>=0;x--){var nt=cn[x].nodeType;if((nt===1)||(nt==3)){return false;}}return true;};},"contains":function(name,_3c9){var cz=_3c9.charAt(0);if(cz=="\""||cz=="'"){_3c9=_3c9.slice(1,-1);}return function(elem){return (elem.innerHTML.indexOf(_3c9)>=0);};},"not":function(name,_3ca){var p=_396(_3ca)[0];var _3cb={el:1};if(p.tag!="*"){_3cb.tag=1;}if(!p.classes.length){_3cb.classes=1;}var ntf=_3cc(p,_3cb);return function(elem){return (!ntf(elem));};},"nth-child":function(name,_3cd){var pi=parseInt;if(_3cd=="odd"){return _3c2;}else{if(_3cd=="even"){return _3c1;}}if(_3cd.indexOf("n")!=-1){var _3ce=_3cd.split("n",2);var pred=_3ce[0]?((_3ce[0]=="-")?-1:pi(_3ce[0])):1;var idx=_3ce[1]?pi(_3ce[1]):0;var lb=0,ub=-1;if(pred>0){if(idx<0){idx=(idx%pred)&&(pred+(idx%pred));}else{if(idx>0){if(idx>=pred){lb=idx-idx%pred;}idx=idx%pred;}}}else{if(pred<0){pred*=-1;if(idx>0){ub=idx;idx=idx%pred;}}}if(pred>0){return function(elem){var i=_3c0(elem);return (i>=lb)&&(ub<0||i<=ub)&&((i%pred)==idx);};}else{_3cd=idx;}}var _3cf=pi(_3cd);return function(elem){return (_3c0(elem)==_3cf);};}};var _3d0=(has("ie")<9||has("ie")==9&&has("quirks"))?function(cond){var clc=cond.toLowerCase();if(clc=="class"){cond="className";}return function(elem){return (_394?elem.getAttribute(cond):elem[cond]||elem[clc]);};}:function(cond){return function(elem){return (elem&&elem.getAttribute&&elem.hasAttribute(cond));};};var _3cc=function(_3d1,_3d2){if(!_3d1){return _395;}_3d2=_3d2||{};var ff=null;if(!("el" in _3d2)){ff=_3a9(ff,_3ad);}if(!("tag" in _3d2)){if(_3d1.tag!="*"){ff=_3a9(ff,function(elem){return (elem&&((_394?elem.tagName:elem.tagName.toUpperCase())==_3d1.getTag()));});}}if(!("classes" in _3d2)){each(_3d1.classes,function(_3d3,idx,arr){var re=new RegExp("(?:^|\\s)"+_3d3+"(?:\\s|$)");ff=_3a9(ff,function(elem){return re.test(elem.className);});ff.count=idx;});}if(!("pseudos" in _3d2)){each(_3d1.pseudos,function(_3d4){var pn=_3d4.name;if(_3c3[pn]){ff=_3a9(ff,_3c3[pn](pn,_3d4.value));}});}if(!("attrs" in _3d2)){each(_3d1.attrs,function(attr){var _3d5;var a=attr.attr;if(attr.type&&_3b0[attr.type]){_3d5=_3b0[attr.type](a,attr.matchFor);}else{if(a.length){_3d5=_3d0(a);}}if(_3d5){ff=_3a9(ff,_3d5);}});}if(!("id" in _3d2)){if(_3d1.id){ff=_3a9(ff,function(elem){return (!!elem&&(elem.id==_3d1.id));});}}if(!ff){if(!("default" in _3d2)){ff=_395;}}return ff;};var _3d6=function(_3d7){return function(node,ret,bag){while(node=node[_3bb]){if(_3ba&&(!_3ad(node))){continue;}if((!bag||_3d8(node,bag))&&_3d7(node)){ret.push(node);}break;}return ret;};};var _3d9=function(_3da){return function(root,ret,bag){var te=root[_3bb];while(te){if(_3bd(te)){if(bag&&!_3d8(te,bag)){break;}if(_3da(te)){ret.push(te);}}te=te[_3bb];}return ret;};};var _3db=function(_3dc,_3dd){var _3de=function(_3df){var _3e0=[];try{_3e0=Array.prototype.slice.call(_3df);}catch(e){for(var i=0,len=_3df.length;i"==oper){_3e5=_3db(_3e6);}}}}return _3e2[_3e4.query]=_3e5;};var _3ed=function(root,_3ee){var _3ef=_3ac(root),qp,x,te,qpl=_3ee.length,bag,ret;for(var i=0;i0){bag={};ret.nozip=true;}var gef=_3e3(qp);for(var j=0;(te=_3ef[j]);j++){gef(te,ret,bag);}if(!ret.length){break;}_3ef=ret;}return ret;};var _3f0={},_3f1={};var _3f2=function(_3f3){var _3f4=_396(trim(_3f3));if(_3f4.length==1){var tef=_3e3(_3f4[0]);return function(root){var r=tef(root,[]);if(r){r.nozip=true;}return r;};}return function(root){return _3ed(root,_3f4);};};var _3f5=has("ie")?"commentStrip":"nozip";var qsa="querySelectorAll";var _3f6=!!_391()[qsa];var _3f7=/\\[>~+]|n\+\d|([^ \\])?([>~+])([^ =])?/g;var _3f8=function(_3f9,pre,ch,post){return ch?(pre?pre+" ":"")+ch+(post?" "+post:""):_3f9;};var _3fa=/([^[]*)([^\]]*])?/g;var _3fb=function(_3fc,_3fd,att){return _3fd.replace(_3f7,_3f8)+(att||"");};var _3fe=function(_3ff,_400){_3ff=_3ff.replace(_3fa,_3fb);if(_3f6){var _401=_3f1[_3ff];if(_401&&!_400){return _401;}}var _402=_3f0[_3ff];if(_402){return _402;}var qcz=_3ff.charAt(0);var _403=(-1==_3ff.indexOf(" "));if((_3ff.indexOf("#")>=0)&&(_403)){_400=true;}var _404=(_3f6&&(!_400)&&(_393.indexOf(qcz)==-1)&&(!has("ie")||(_3ff.indexOf(":")==-1))&&(!(_392&&(_3ff.indexOf(".")>=0)))&&(_3ff.indexOf(":contains")==-1)&&(_3ff.indexOf(":checked")==-1)&&(_3ff.indexOf("|=")==-1));if(_404){var tq=(_393.indexOf(_3ff.charAt(_3ff.length-1))>=0)?(_3ff+" *"):_3ff;return _3f1[_3ff]=function(root){if(9==root.nodeType||_403){try{var r=root[qsa](tq);r[_3f5]=true;return r;}catch(e){}}return _3fe(_3ff,true)(root);};}else{var _405=_3ff.match(/([^\s,](?:"(?:\\.|[^"])+"|'(?:\\.|[^'])+'|[^,])*)/g);return _3f0[_3ff]=((_405.length<2)?_3f2(_3ff):function(root){var _406=0,ret=[],tp;while((tp=_405[_406++])){ret=ret.concat(_3f2(tp)(root));}return ret;});}};var _407=0;var _408=has("ie")?function(node){if(_394){return (node.getAttribute("_uid")||node.setAttribute("_uid",++_407)||_407);}else{return node.uniqueID;}}:function(node){return (node._uid||(node._uid=++_407));};var _3d8=function(node,bag){if(!bag){return 1;}var id=_408(node);if(!bag[id]){return bag[id]=1;}return 0;};var _409="_zipIdx";var _40a=function(arr){if(arr&&arr.nozip){return arr;}if(!arr||!arr.length){return [];}if(arr.length<2){return [arr[0]];}var ret=[];_407++;var x,te;if(has("ie")&&_394){var _40b=_407+"";for(x=0;xv.w-_416.H_TRIGGER_AUTOSCROLL){dx=Math.min(_416.H_AUTOSCROLL_VALUE,_419-html.scrollLeft);}}if(e.clientY<_416.V_TRIGGER_AUTOSCROLL){dy=-_416.V_AUTOSCROLL_VALUE;}else{if(e.clientY>v.h-_416.V_TRIGGER_AUTOSCROLL){dy=Math.min(_416.V_AUTOSCROLL_VALUE,_418-html.scrollTop);}}window.scrollBy(dx,dy);};_416._validNodes={"div":1,"p":1,"td":1};_416._validOverflow={"auto":1,"scroll":1};_416.autoScrollNodes=function(e){var b,t,w,h,rx,ry,dx=0,dy=0,_41a,_41b;for(var n=e.target;n;){if(n.nodeType==1&&(n.tagName.toLowerCase() in _416._validNodes)){var s=_414.getComputedStyle(n),_41c=(s.overflowX.toLowerCase() in _416._validOverflow),_41d=(s.overflowY.toLowerCase() in _416._validOverflow);if(_41c||_41d){b=_413.getContentBox(n,s);t=_413.position(n,true);}if(_41c){w=Math.min(_416.H_TRIGGER_AUTOSCROLL,b.w/2);rx=e.pageX-t.x;if(has("webkit")||has("opera")){rx+=win.body().scrollLeft;}dx=0;if(rx>0&&rxb.w-w){dx=w;}}_41a=n.scrollLeft;n.scrollLeft=n.scrollLeft+dx;}}if(_41d){h=Math.min(_416.V_TRIGGER_AUTOSCROLL,b.h/2);ry=e.pageY-t.y;if(has("webkit")||has("opera")){ry+=win.body().scrollTop;}dy=0;if(ry>0&&ryb.h-h){dy=h;}}_41b=n.scrollTop;n.scrollTop=n.scrollTop+dy;}}if(dx||dy){return;}}try{n=n.parentNode;}catch(x){n=null;}}_416.autoScroll(e);};return _416;});},"dijit/form/_RadioButtonMixin":function(){define(["dojo/_base/array","dojo/_base/declare","dojo/dom-attr","dojo/_base/lang","dojo/query!css2","../registry"],function(_41e,_41f,_420,lang,_421,_422){return _41f("dijit.form._RadioButtonMixin",null,{type:"radio",_getRelatedWidgets:function(){var ary=[];_421("input[type=radio]",this.focusNode.form||this.ownerDocument).forEach(lang.hitch(this,function(_423){if(_423.name==this.name&&_423.form==this.focusNode.form){var _424=_422.getEnclosingWidget(_423);if(_424){ary.push(_424);}}}));return ary;},_setCheckedAttr:function(_425){this.inherited(arguments);if(!this._created){return;}if(_425){_41e.forEach(this._getRelatedWidgets(),lang.hitch(this,function(_426){if(_426!=this&&_426.checked){_426.set("checked",false);}}));}},_getSubmitValue:function(_427){return _427==null?"on":_427;},_onClick:function(e){if(this.checked||this.disabled){e.stopPropagation();e.preventDefault();return false;}if(this.readOnly){e.stopPropagation();e.preventDefault();_41e.forEach(this._getRelatedWidgets(),lang.hitch(this,function(_428){_420.set(this.focusNode||this.domNode,"checked",_428.checked);}));return false;}var _429=false;var _42a;_41e.some(this._getRelatedWidgets(),function(_42b){if(_42b.checked){_42a=_42b;return true;}return false;});this.checked=true;_42a&&(_42a.checked=false);if(this.onClick(e)===false||e.defaultPrevented){_429=true;}this.checked=false;_42a&&(_42a.checked=true);if(_429){e.preventDefault();}else{this.set("checked",true);}return !_429;}});});},"dojo/data/ItemFileWriteStore":function(){define(["../_base/lang","../_base/declare","../_base/array","../_base/json","../_base/kernel","./ItemFileReadStore","../date/stamp"],function(lang,_42c,_42d,_42e,_42f,_430,_431){return _42c("dojo.data.ItemFileWriteStore",_430,{constructor:function(_432){this._features["dojo.data.api.Write"]=true;this._features["dojo.data.api.Notification"]=true;this._pending={_newItems:{},_modifiedItems:{},_deletedItems:{}};if(!this._datatypeMap["Date"].serialize){this._datatypeMap["Date"].serialize=function(obj){return _431.toISOString(obj,{zulu:true});};}if(_432&&(_432.referenceIntegrity===false)){this.referenceIntegrity=false;}this._saveInProgress=false;},referenceIntegrity:true,_assert:function(_433){if(!_433){throw new Error("assertion failed in ItemFileWriteStore");}},_getIdentifierAttribute:function(){return this.getFeatures()["dojo.data.api.Identity"];},newItem:function(_434,_435){this._assert(!this._saveInProgress);if(!this._loadFinished){this._forceLoad();}if(typeof _434!="object"&&typeof _434!="undefined"){throw new Error("newItem() was passed something other than an object");}var _436=null;var _437=this._getIdentifierAttribute();if(_437===Number){_436=this._arrayOfAllItems.length;}else{_436=_434[_437];if(typeof _436==="undefined"){throw new Error("newItem() was not passed an identity for the new item");}if(lang.isArray(_436)){throw new Error("newItem() was not passed an single-valued identity");}}if(this._itemsByIdentity){this._assert(typeof this._itemsByIdentity[_436]==="undefined");}this._assert(typeof this._pending._newItems[_436]==="undefined");this._assert(typeof this._pending._deletedItems[_436]==="undefined");var _438={};_438[this._storeRefPropName]=this;_438[this._itemNumPropName]=this._arrayOfAllItems.length;if(this._itemsByIdentity){this._itemsByIdentity[_436]=_438;_438[_437]=[_436];}this._arrayOfAllItems.push(_438);var _439=null;if(_435&&_435.parent&&_435.attribute){_439={item:_435.parent,attribute:_435.attribute,oldValue:undefined};var _43a=this.getValues(_435.parent,_435.attribute);if(_43a&&_43a.length>0){var _43b=_43a.slice(0,_43a.length);if(_43a.length===1){_439.oldValue=_43a[0];}else{_439.oldValue=_43a.slice(0,_43a.length);}_43b.push(_438);this._setValueOrValues(_435.parent,_435.attribute,_43b,false);_439.newValue=this.getValues(_435.parent,_435.attribute);}else{this._setValueOrValues(_435.parent,_435.attribute,_438,false);_439.newValue=_438;}}else{_438[this._rootItemPropName]=true;this._arrayOfTopLevelItems.push(_438);}this._pending._newItems[_436]=_438;for(var key in _434){if(key===this._storeRefPropName||key===this._itemNumPropName){throw new Error("encountered bug in ItemFileWriteStore.newItem");}var _43c=_434[key];if(!lang.isArray(_43c)){_43c=[_43c];}_438[key]=_43c;if(this.referenceIntegrity){for(var i=0;i<_43c.length;i++){var val=_43c[i];if(this.isItem(val)){this._addReferenceToMap(val,_438,key);}}}}this.onNew(_438,_439);return _438;},_removeArrayElement:function(_43d,_43e){var _43f=_42d.indexOf(_43d,_43e);if(_43f!=-1){_43d.splice(_43f,1);return true;}return false;},deleteItem:function(item){this._assert(!this._saveInProgress);this._assertIsItem(item);var _440=item[this._itemNumPropName];var _441=this.getIdentity(item);if(this.referenceIntegrity){var _442=this.getAttributes(item);if(item[this._reverseRefMap]){item["backup_"+this._reverseRefMap]=lang.clone(item[this._reverseRefMap]);}_42d.forEach(_442,function(_443){_42d.forEach(this.getValues(item,_443),function(_444){if(this.isItem(_444)){if(!item["backupRefs_"+this._reverseRefMap]){item["backupRefs_"+this._reverseRefMap]=[];}item["backupRefs_"+this._reverseRefMap].push({id:this.getIdentity(_444),attr:_443});this._removeReferenceFromMap(_444,item,_443);}},this);},this);var _445=item[this._reverseRefMap];if(_445){for(var _446 in _445){var _447=null;if(this._itemsByIdentity){_447=this._itemsByIdentity[_446];}else{_447=this._arrayOfAllItems[_446];}if(_447){for(var _448 in _445[_446]){var _449=this.getValues(_447,_448)||[];var _44a=_42d.filter(_449,function(_44b){return !(this.isItem(_44b)&&this.getIdentity(_44b)==_441);},this);this._removeReferenceFromMap(item,_447,_448);if(_44a.length<_449.length){this._setValueOrValues(_447,_448,_44a,true);}}}}}}this._arrayOfAllItems[_440]=null;item[this._storeRefPropName]=null;if(this._itemsByIdentity){delete this._itemsByIdentity[_441];}this._pending._deletedItems[_441]=item;if(item[this._rootItemPropName]){this._removeArrayElement(this._arrayOfTopLevelItems,item);}this.onDelete(item);return true;},setValue:function(item,_44c,_44d){return this._setValueOrValues(item,_44c,_44d,true);},setValues:function(item,_44e,_44f){return this._setValueOrValues(item,_44e,_44f,true);},unsetAttribute:function(item,_450){return this._setValueOrValues(item,_450,[],true);},_setValueOrValues:function(item,_451,_452,_453){this._assert(!this._saveInProgress);this._assertIsItem(item);this._assert(lang.isString(_451));this._assert(typeof _452!=="undefined");var _454=this._getIdentifierAttribute();if(_451==_454){throw new Error("ItemFileWriteStore does not have support for changing the value of an item's identifier.");}var _455=this._getValueOrValues(item,_451);var _456=this.getIdentity(item);if(!this._pending._modifiedItems[_456]){var _457={};for(var key in item){if((key===this._storeRefPropName)||(key===this._itemNumPropName)||(key===this._rootItemPropName)){_457[key]=item[key];}else{if(key===this._reverseRefMap){_457[key]=lang.clone(item[key]);}else{_457[key]=item[key].slice(0,item[key].length);}}}this._pending._modifiedItems[_456]=_457;}var _458=false;if(lang.isArray(_452)&&_452.length===0){_458=delete item[_451];_452=undefined;if(this.referenceIntegrity&&_455){var _459=_455;if(!lang.isArray(_459)){_459=[_459];}for(var i=0;i<_459.length;i++){var _45a=_459[i];if(this.isItem(_45a)){this._removeReferenceFromMap(_45a,item,_451);}}}}else{var _45b;if(lang.isArray(_452)){_45b=_452.slice(0,_452.length);}else{_45b=[_452];}if(this.referenceIntegrity){if(_455){var _459=_455;if(!lang.isArray(_459)){_459=[_459];}var map={};_42d.forEach(_459,function(_45c){if(this.isItem(_45c)){var id=this.getIdentity(_45c);map[id.toString()]=true;}},this);_42d.forEach(_45b,function(_45d){if(this.isItem(_45d)){var id=this.getIdentity(_45d);if(map[id.toString()]){delete map[id.toString()];}else{this._addReferenceToMap(_45d,item,_451);}}},this);for(var rId in map){var _45e;if(this._itemsByIdentity){_45e=this._itemsByIdentity[rId];}else{_45e=this._arrayOfAllItems[rId];}this._removeReferenceFromMap(_45e,item,_451);}}else{for(var i=0;i<_45b.length;i++){var _45a=_45b[i];if(this.isItem(_45a)){this._addReferenceToMap(_45a,item,_451);}}}}item[_451]=_45b;_458=true;}if(_453){this.onSet(item,_451,_455,_452);}return _458;},_addReferenceToMap:function(_45f,_460,_461){var _462=this.getIdentity(_460);var _463=_45f[this._reverseRefMap];if(!_463){_463=_45f[this._reverseRefMap]={};}var _464=_463[_462];if(!_464){_464=_463[_462]={};}_464[_461]=true;},_removeReferenceFromMap:function(_465,_466,_467){var _468=this.getIdentity(_466);var _469=_465[this._reverseRefMap];var _46a;if(_469){for(_46a in _469){if(_46a==_468){delete _469[_46a][_467];if(this._isEmpty(_469[_46a])){delete _469[_46a];}}}if(this._isEmpty(_469)){delete _465[this._reverseRefMap];}}},_dumpReferenceMap:function(){var i;for(i=0;i0){_477=false;}}}return _477;},save:function(_478){this._assert(!this._saveInProgress);this._saveInProgress=true;var self=this;var _479=function(){self._pending={_newItems:{},_modifiedItems:{},_deletedItems:{}};self._saveInProgress=false;if(_478&&_478.onComplete){var _47a=_478.scope||_42f.global;_478.onComplete.call(_47a);}};var _47b=function(err){self._saveInProgress=false;if(_478&&_478.onError){var _47c=_478.scope||_42f.global;_478.onError.call(_47c,err);}};if(this._saveEverything){var _47d=this._getNewFileContentString();this._saveEverything(_479,_47b,_47d);}if(this._saveCustom){this._saveCustom(_479,_47b);}if(!this._saveEverything&&!this._saveCustom){_479();}},revert:function(){this._assert(!this._saveInProgress);var _47e;for(_47e in this._pending._modifiedItems){var _47f=this._pending._modifiedItems[_47e];var _480=null;if(this._itemsByIdentity){_480=this._itemsByIdentity[_47e];}else{_480=this._arrayOfAllItems[_47e];}_47f[this._storeRefPropName]=this;for(var key in _480){delete _480[key];}lang.mixin(_480,_47f);}var _481;for(_47e in this._pending._deletedItems){_481=this._pending._deletedItems[_47e];_481[this._storeRefPropName]=this;var _482=_481[this._itemNumPropName];if(_481["backup_"+this._reverseRefMap]){_481[this._reverseRefMap]=_481["backup_"+this._reverseRefMap];delete _481["backup_"+this._reverseRefMap];}this._arrayOfAllItems[_482]=_481;if(this._itemsByIdentity){this._itemsByIdentity[_47e]=_481;}if(_481[this._rootItemPropName]){this._arrayOfTopLevelItems.push(_481);}}for(_47e in this._pending._deletedItems){_481=this._pending._deletedItems[_47e];if(_481["backupRefs_"+this._reverseRefMap]){_42d.forEach(_481["backupRefs_"+this._reverseRefMap],function(_483){var _484;if(this._itemsByIdentity){_484=this._itemsByIdentity[_483.id];}else{_484=this._arrayOfAllItems[_483.id];}this._addReferenceToMap(_484,_481,_483.attr);},this);delete _481["backupRefs_"+this._reverseRefMap];}}for(_47e in this._pending._newItems){var _485=this._pending._newItems[_47e];_485[this._storeRefPropName]=null;this._arrayOfAllItems[_485[this._itemNumPropName]]=null;if(_485[this._rootItemPropName]){this._removeArrayElement(this._arrayOfTopLevelItems,_485);}if(this._itemsByIdentity){delete this._itemsByIdentity[_47e];}}this._pending={_newItems:{},_modifiedItems:{},_deletedItems:{}};return true;},isDirty:function(item){if(item){var _486=this.getIdentity(item);return new Boolean(this._pending._newItems[_486]||this._pending._modifiedItems[_486]||this._pending._deletedItems[_486]).valueOf();}else{return !this._isEmpty(this._pending._newItems)||!this._isEmpty(this._pending._modifiedItems)||!this._isEmpty(this._pending._deletedItems);}},onSet:function(item,_487,_488,_489){},onNew:function(_48a,_48b){},onDelete:function(_48c){},close:function(_48d){if(this.clearOnClose){if(!this.isDirty()){this.inherited(arguments);}else{throw new Error("dojo.data.ItemFileWriteStore: There are unsaved changes present in the store. Please save or revert the changes before invoking close.");}}}});});},"dojo/dnd/TimedMoveable":function(){define(["../_base/declare","./Moveable"],function(_48e,_48f){var _490=_48f.prototype.onMove;return _48e("dojo.dnd.TimedMoveable",_48f,{timeout:40,constructor:function(node,_491){if(!_491){_491={};}if(_491.timeout&&typeof _491.timeout=="number"&&_491.timeout>=0){this.timeout=_491.timeout;}},onMoveStop:function(_492){if(_492._timer){clearTimeout(_492._timer);_490.call(this,_492,_492._leftTop);}_48f.prototype.onMoveStop.apply(this,arguments);},onMove:function(_493,_494){_493._leftTop=_494;if(!_493._timer){var _495=this;_493._timer=setTimeout(function(){_493._timer=null;_490.call(_495,_493,_493._leftTop);},this.timeout);}}});});},"dojo/NodeList-fx":function(){define(["./query","./_base/lang","./aspect","./_base/fx","./fx"],function(_496,lang,_497,_498,_499){var _49a=_496.NodeList;lang.extend(_49a,{_anim:function(obj,_49b,args){args=args||{};var a=_499.combine(this.map(function(item){var _49c={node:item};lang.mixin(_49c,args);return obj[_49b](_49c);}));return args.auto?a.play()&&this:a;},wipeIn:function(args){return this._anim(_499,"wipeIn",args);},wipeOut:function(args){return this._anim(_499,"wipeOut",args);},slideTo:function(args){return this._anim(_499,"slideTo",args);},fadeIn:function(args){return this._anim(_498,"fadeIn",args);},fadeOut:function(args){return this._anim(_498,"fadeOut",args);},animateProperty:function(args){return this._anim(_498,"animateProperty",args);},anim:function(_49d,_49e,_49f,_4a0,_4a1){var _4a2=_499.combine(this.map(function(item){return _498.animateProperty({node:item,properties:_49d,duration:_49e||350,easing:_49f});}));if(_4a0){_497.after(_4a2,"onEnd",_4a0,true);}return _4a2.play(_4a1||0);}});return _49a;});},"dijit/form/_ListMouseMixin":function(){define(["dojo/_base/declare","dojo/on","dojo/touch","./_ListBase"],function(_4a3,on,_4a4,_4a5){return _4a3("dijit.form._ListMouseMixin",_4a5,{postCreate:function(){this.inherited(arguments);this.domNode.dojoClick=true;this._listConnect("click","_onClick");this._listConnect("mousedown","_onMouseDown");this._listConnect("mouseup","_onMouseUp");this._listConnect("mouseover","_onMouseOver");this._listConnect("mouseout","_onMouseOut");},_onClick:function(evt,_4a6){this._setSelectedAttr(_4a6,false);if(this._deferredClick){this._deferredClick.remove();}this._deferredClick=this.defer(function(){this._deferredClick=null;this.onClick(_4a6);});},_onMouseDown:function(evt,_4a7){if(this._hoveredNode){this.onUnhover(this._hoveredNode);this._hoveredNode=null;}this._isDragging=true;this._setSelectedAttr(_4a7,false);},_onMouseUp:function(evt,_4a8){this._isDragging=false;var _4a9=this.selected;var _4aa=this._hoveredNode;if(_4a9&&_4a8==_4a9){this.defer(function(){this._onClick(evt,_4a9);});}else{if(_4aa){this.defer(function(){this._onClick(evt,_4aa);});}}},_onMouseOut:function(evt,_4ab){if(this._hoveredNode){this.onUnhover(this._hoveredNode);this._hoveredNode=null;}if(this._isDragging){this._cancelDrag=(new Date()).getTime()+1000;}},_onMouseOver:function(evt,_4ac){if(this._cancelDrag){var time=(new Date()).getTime();if(time>this._cancelDrag){this._isDragging=false;}this._cancelDrag=null;}this._hoveredNode=_4ac;this.onHover(_4ac);if(this._isDragging){this._setSelectedAttr(_4ac,false);}}});});},"dojo/cookie":function(){define(["./_base/kernel","./regexp"],function(dojo,_4ad){dojo.cookie=function(name,_4ae,_4af){var c=document.cookie,ret;if(arguments.length==1){var _4b0=c.match(new RegExp("(?:^|; )"+_4ad.escapeString(name)+"=([^;]*)"));ret=_4b0?decodeURIComponent(_4b0[1]):undefined;}else{_4af=_4af||{};var exp=_4af.expires;if(typeof exp=="number"){var d=new Date();d.setTime(d.getTime()+exp*24*60*60*1000);exp=_4af.expires=d;}if(exp&&exp.toUTCString){_4af.expires=exp.toUTCString();}_4ae=encodeURIComponent(_4ae);var _4b1=name+"="+_4ae,_4b2;for(_4b2 in _4af){_4b1+="; "+_4b2;var _4b3=_4af[_4b2];if(_4b3!==true){_4b1+="="+_4b3;}}document.cookie=_4b1;}return ret;};dojo.cookie.isSupported=function(){if(!("cookieEnabled" in navigator)){this("__djCookieTest__","CookiesAllowed");navigator.cookieEnabled=this("__djCookieTest__")=="CookiesAllowed";if(navigator.cookieEnabled){this("__djCookieTest__","",{expires:-1});}}return navigator.cookieEnabled;};return dojo.cookie;});},"dojo/cache":function(){define(["./_base/kernel","./text"],function(dojo){return dojo.cache;});},"dijit/ProgressBar":function(){define(["require","dojo/_base/declare","dojo/dom-class","dojo/_base/lang","dojo/number","./_Widget","./_TemplatedMixin","dojo/text!./templates/ProgressBar.html"],function(_4b4,_4b5,_4b6,lang,_4b7,_4b8,_4b9,_4ba){return _4b5("dijit.ProgressBar",[_4b8,_4b9],{progress:"0",value:"",maximum:100,places:0,indeterminate:false,label:"",name:"",templateString:_4ba,_indeterminateHighContrastImagePath:_4b4.toUrl("./themes/a11y/indeterminate_progress.gif"),postMixInProperties:function(){this.inherited(arguments);if(!(this.params&&"value" in this.params)){this.value=this.indeterminate?Infinity:this.progress;}},buildRendering:function(){this.inherited(arguments);this.indeterminateHighContrastImage.setAttribute("src",this._indeterminateHighContrastImagePath.toString());this.update();},_setDirAttr:function(val){var rtl=val.toLowerCase()=="rtl";_4b6.toggle(this.domNode,"dijitProgressBarRtl",rtl);_4b6.toggle(this.domNode,"dijitProgressBarIndeterminateRtl",this.indeterminate&&rtl);this.inherited(arguments);},update:function(_4bb){lang.mixin(this,_4bb||{});var tip=this.internalProgress,ap=this.domNode;var _4bc=1;if(this.indeterminate){ap.removeAttribute("aria-valuenow");}else{if(String(this.progress).indexOf("%")!=-1){_4bc=Math.min(parseFloat(this.progress)/100,1);this.progress=_4bc*this.maximum;}else{this.progress=Math.min(this.progress,this.maximum);_4bc=this.maximum?this.progress/this.maximum:0;}ap.setAttribute("aria-valuenow",this.progress);}ap.setAttribute("aria-labelledby",this.labelNode.id);ap.setAttribute("aria-valuemin",0);ap.setAttribute("aria-valuemax",this.maximum);this.labelNode.innerHTML=this.report(_4bc);_4b6.toggle(this.domNode,"dijitProgressBarIndeterminate",this.indeterminate);_4b6.toggle(this.domNode,"dijitProgressBarIndeterminateRtl",this.indeterminate&&!this.isLeftToRight());tip.style.width=(_4bc*100)+"%";this.onChange();},_setValueAttr:function(v){this._set("value",v);if(v==Infinity){this.update({indeterminate:true});}else{this.update({indeterminate:false,progress:v});}},_setLabelAttr:function(_4bd){this._set("label",_4bd);this.update();},_setIndeterminateAttr:function(_4be){this._set("indeterminate",_4be);this.update();},report:function(_4bf){return this.label?this.label:(this.indeterminate?" ":_4b7.format(_4bf,{type:"percent",places:this.places,locale:this.lang}));},onChange:function(){}});});},"dijit/_base/popup":function(){define(["dojo/dom-class","dojo/_base/window","../popup","../BackgroundIframe"],function(_4c0,win,_4c1){var _4c2=_4c1._createWrapper;_4c1._createWrapper=function(_4c3){if(!_4c3.declaredClass){_4c3={_popupWrapper:(_4c3.parentNode&&_4c0.contains(_4c3.parentNode,"dijitPopup"))?_4c3.parentNode:null,domNode:_4c3,destroy:function(){},ownerDocument:_4c3.ownerDocument,ownerDocumentBody:win.body(_4c3.ownerDocument)};}return _4c2.call(this,_4c3);};var _4c4=_4c1.open;_4c1.open=function(args){if(args.orient&&typeof args.orient!="string"&&!("length" in args.orient)){var ary=[];for(var key in args.orient){ary.push({aroundCorner:key,corner:args.orient[key]});}args.orient=ary;}return _4c4.call(this,args);};return _4c1;});},"dojo/promise/all":function(){define(["../_base/array","../_base/lang","../Deferred","../when"],function(_4c5,lang,_4c6,when){"use strict";var some=_4c5.some;return function all(_4c7){var _4c8,_4c5;if(lang.isArray(_4c7)){_4c5=_4c7;}else{if(_4c7&&typeof _4c7==="object"){_4c8=_4c7;}}var _4c9;var _4ca=[];if(_4c8){_4c5=[];for(var key in _4c8){if(Object.hasOwnProperty.call(_4c8,key)){_4ca.push(key);_4c5.push(_4c8[key]);}}_4c9={};}else{if(_4c5){_4c9=[];}}if(!_4c5||!_4c5.length){return new _4c6().resolve(_4c9);}var _4cb=new _4c6();_4cb.promise.always(function(){_4c9=_4ca=null;});var _4cc=_4c5.length;some(_4c5,function(_4cd,_4ce){if(!_4c8){_4ca.push(_4ce);}when(_4cd,function(_4cf){if(!_4cb.isFulfilled()){_4c9[_4ca[_4ce]]=_4cf;if(--_4cc===0){_4cb.resolve(_4c9);}}},_4cb.reject);return _4cb.isFulfilled();});return _4cb.promise;};});},"dijit/ColorPalette":function(){define(["require","dojo/text!./templates/ColorPalette.html","./_Widget","./_TemplatedMixin","./_PaletteMixin","./hccss","dojo/i18n","dojo/_base/Color","dojo/_base/declare","dojo/dom-construct","dojo/string","dojo/i18n!dojo/nls/colors","dojo/colors"],function(_4d0,_4d1,_4d2,_4d3,_4d4,has,i18n,_4d5,_4d6,_4d7,_4d8){var _4d9=_4d6("dijit.ColorPalette",[_4d2,_4d3,_4d4],{palette:"7x10",_palettes:{"7x10":[["white","seashell","cornsilk","lemonchiffon","lightyellow","palegreen","paleturquoise","lightcyan","lavender","plum"],["lightgray","pink","bisque","moccasin","khaki","lightgreen","lightseagreen","lightskyblue","cornflowerblue","violet"],["silver","lightcoral","sandybrown","orange","palegoldenrod","chartreuse","mediumturquoise","skyblue","mediumslateblue","orchid"],["gray","red","orangered","darkorange","yellow","limegreen","darkseagreen","royalblue","slateblue","mediumorchid"],["dimgray","crimson","chocolate","coral","gold","forestgreen","seagreen","blue","blueviolet","darkorchid"],["darkslategray","firebrick","saddlebrown","sienna","olive","green","darkcyan","mediumblue","darkslateblue","darkmagenta"],["black","darkred","maroon","brown","darkolivegreen","darkgreen","midnightblue","navy","indigo","purple"]],"3x4":[["white","lime","green","blue"],["silver","yellow","fuchsia","navy"],["gray","red","purple","black"]]},templateString:_4d1,baseClass:"dijitColorPalette",_dyeFactory:function(_4da,row,col,_4db){return new this._dyeClass(_4da,row,col,_4db);},buildRendering:function(){this.inherited(arguments);this._dyeClass=_4d6(_4d9._Color,{palette:this.palette});this._preparePalette(this._palettes[this.palette],i18n.getLocalization("dojo","colors",this.lang));}});_4d9._Color=_4d6("dijit._Color",_4d5,{template:""+"${alt}"+"",hcTemplate:""+"${alt}"+"",_imagePaths:{"7x10":_4d0.toUrl("./themes/a11y/colors7x10.png"),"3x4":_4d0.toUrl("./themes/a11y/colors3x4.png")},constructor:function(_4dc,row,col,_4dd){this._title=_4dd;this._row=row;this._col=col;this.setColor(_4d5.named[_4dc]);},getValue:function(){return this.toHex();},fillCell:function(cell,_4de){var html=_4d8.substitute(has("highcontrast")?this.hcTemplate:this.template,{color:this.toHex(),blankGif:_4de,alt:this._title,title:this._title,image:this._imagePaths[this.palette].toString(),left:this._col*-20-5,top:this._row*-20-5,size:this.palette=="7x10"?"height: 145px; width: 206px":"height: 64px; width: 86px"});_4d7.place(html,cell);}});return _4d9;});},"dojo/_base/url":function(){define(["./kernel"],function(dojo){var ore=new RegExp("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?$"),ire=new RegExp("^((([^\\[:]+):)?([^@]+)@)?(\\[([^\\]]+)\\]|([^\\[:]*))(:([0-9]+))?$"),_4df=function(){var n=null,_4e0=arguments,uri=[_4e0[0]];for(var i=1;i<_4e0.length;i++){if(!_4e0[i]){continue;}var _4e1=new _4df(_4e0[i]+""),_4e2=new _4df(uri[0]+"");if(_4e1.path==""&&!_4e1.scheme&&!_4e1.authority&&!_4e1.query){if(_4e1.fragment!=n){_4e2.fragment=_4e1.fragment;}_4e1=_4e2;}else{if(!_4e1.scheme){_4e1.scheme=_4e2.scheme;if(!_4e1.authority){_4e1.authority=_4e2.authority;if(_4e1.path.charAt(0)!="/"){var path=_4e2.path.substring(0,_4e2.path.lastIndexOf("/")+1)+_4e1.path;var segs=path.split("/");for(var j=0;j0&&!(j==1&&segs[0]=="")&&segs[j]==".."&&segs[j-1]!=".."){if(j==(segs.length-1)){segs.splice(j,1);segs[j-1]="";}else{segs.splice(j-1,2);j-=2;}}}}_4e1.path=segs.join("/");}}}}uri=[];if(_4e1.scheme){uri.push(_4e1.scheme,":");}if(_4e1.authority){uri.push("//",_4e1.authority);}uri.push(_4e1.path);if(_4e1.query){uri.push("?",_4e1.query);}if(_4e1.fragment){uri.push("#",_4e1.fragment);}}this.uri=uri.join("");var r=this.uri.match(ore);this.scheme=r[2]||(r[1]?"":n);this.authority=r[4]||(r[3]?"":n);this.path=r[5];this.query=r[7]||(r[6]?"":n);this.fragment=r[9]||(r[8]?"":n);if(this.authority!=n){r=this.authority.match(ire);this.user=r[3]||n;this.password=r[4]||n;this.host=r[6]||r[7];this.port=r[9]||n;}};_4df.prototype.toString=function(){return this.uri;};return dojo._Url=_4df;});},"dojo/text":function(){define(["./_base/kernel","require","./has","./request"],function(dojo,_4e3,has,_4e4){var _4e5;if(1){_4e5=function(url,sync,load){_4e4(url,{sync:!!sync,headers:{"X-Requested-With":null}}).then(load);};}else{if(_4e3.getText){_4e5=_4e3.getText;}else{console.error("dojo/text plugin failed to load because loader does not support getText");}}var _4e6={},_4e7=function(text){if(text){text=text.replace(/^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,"");var _4e8=text.match(/]*>\s*([\s\S]+)\s*<\/body>/im);if(_4e8){text=_4e8[1];}}else{text="";}return text;},_4e9={},_4ea={};dojo.cache=function(_4eb,url,_4ec){var key;if(typeof _4eb=="string"){if(/\//.test(_4eb)){key=_4eb;_4ec=url;}else{key=_4e3.toUrl(_4eb.replace(/\./g,"/")+(url?("/"+url):""));}}else{key=_4eb+"";_4ec=url;}var val=(_4ec!=undefined&&typeof _4ec!="string")?_4ec.value:_4ec,_4ed=_4ec&&_4ec.sanitize;if(typeof val=="string"){_4e6[key]=val;return _4ed?_4e7(val):val;}else{if(val===null){delete _4e6[key];return null;}else{if(!(key in _4e6)){_4e5(key,true,function(text){_4e6[key]=text;});}return _4ed?_4e7(_4e6[key]):_4e6[key];}}};return {dynamic:true,normalize:function(id,_4ee){var _4ef=id.split("!"),url=_4ef[0];return (/^\./.test(url)?_4ee(url):url)+(_4ef[1]?"!"+_4ef[1]:"");},load:function(id,_4f0,load){var _4f1=id.split("!"),_4f2=_4f1.length>1,_4f3=_4f1[0],url=_4f0.toUrl(_4f1[0]),_4f4="url:"+url,text=_4e9,_4f5=function(text){load(_4f2?_4e7(text):text);};if(_4f3 in _4e6){text=_4e6[_4f3];}else{if(_4f0.cache&&_4f4 in _4f0.cache){text=_4f0.cache[_4f4];}else{if(url in _4e6){text=_4e6[url];}}}if(text===_4e9){if(_4ea[url]){_4ea[url].push(_4f5);}else{var _4f6=_4ea[url]=[_4f5];_4e5(url,!_4f0.async,function(text){_4e6[_4f3]=_4e6[url]=text;for(var i=0;i<_4f6.length;){_4f6[i++](text);}delete _4ea[url];});}}else{_4f5(text);}}};});},"dijit/layout/LayoutContainer":function(){define(["dojo/_base/array","dojo/_base/declare","dojo/dom-class","dojo/dom-style","dojo/_base/lang","../_WidgetBase","./_LayoutWidget","./utils"],function(_4f7,_4f8,_4f9,_4fa,lang,_4fb,_4fc,_4fd){var _4fe=_4f8("dijit.layout.LayoutContainer",_4fc,{design:"headline",baseClass:"dijitLayoutContainer",startup:function(){if(this._started){return;}_4f7.forEach(this.getChildren(),this._setupChild,this);this.inherited(arguments);},_setupChild:function(_4ff){this.inherited(arguments);var _500=_4ff.region;if(_500){_4f9.add(_4ff.domNode,this.baseClass+"Pane");}},_getOrderedChildren:function(){var _501=_4f7.map(this.getChildren(),function(_502,idx){return {pane:_502,weight:[_502.region=="center"?Infinity:0,_502.layoutPriority,(this.design=="sidebar"?1:-1)*(/top|bottom/.test(_502.region)?1:-1),idx]};},this);_501.sort(function(a,b){var aw=a.weight,bw=b.weight;for(var i=0;i>1)-pos.y+"px";this.connectorNode.style.left="";}else{if(pos.corner.charAt(1)=="M"&&pos.aroundCorner.charAt(1)=="M"){this.connectorNode.style.left=_524.x+((_524.w-this.connectorNode.offsetWidth)>>1)-pos.x+"px";}else{this.connectorNode.style.left="";this.connectorNode.style.top="";}}_513.set(this.domNode,"opacity",0);this.fadeIn.play();this.isShowingNow=true;this.aroundNode=_51e;this.onMouseEnter=_521||noop;this.onMouseLeave=_522||noop;},orient:function(node,_525,_526,_527,_528){this.connectorNode.style.top="";var _529=_527.h,_52a=_527.w;node.className="dijitTooltip "+{"MR-ML":"dijitTooltipRight","ML-MR":"dijitTooltipLeft","TM-BM":"dijitTooltipAbove","BM-TM":"dijitTooltipBelow","BL-TL":"dijitTooltipBelow dijitTooltipABLeft","TL-BL":"dijitTooltipAbove dijitTooltipABLeft","BR-TR":"dijitTooltipBelow dijitTooltipABRight","TR-BR":"dijitTooltipAbove dijitTooltipABRight","BR-BL":"dijitTooltipRight","BL-BR":"dijitTooltipLeft"}[_525+"-"+_526];this.domNode.style.width="auto";var size=_512.position(this.domNode);if(has("ie")||has("trident")){size.w+=2;}var _52b=Math.min((Math.max(_52a,1)),size.w);_512.setMarginBox(this.domNode,{w:_52b});if(_526.charAt(0)=="B"&&_525.charAt(0)=="B"){var bb=_512.position(node);var _52c=this.connectorNode.offsetHeight;if(bb.h>_529){var _52d=_529-((_528.h+_52c)>>1);this.connectorNode.style.top=_52d+"px";this.connectorNode.style.bottom="";}else{this.connectorNode.style.bottom=Math.min(Math.max(_528.h/2-_52c/2,0),bb.h-_52c)+"px";this.connectorNode.style.top="";}}else{this.connectorNode.style.top="";this.connectorNode.style.bottom="";}return Math.max(0,size.w-_52a);},_onShow:function(){if(has("ie")){this.domNode.style.filter="";}},hide:function(_52e){if(this._onDeck&&this._onDeck[1]==_52e){this._onDeck=null;}else{if(this.aroundNode===_52e){this.fadeIn.stop();this.isShowingNow=false;this.aroundNode=null;this.fadeOut.play();}else{}}this.onMouseEnter=this.onMouseLeave=noop;},_onHide:function(){this.domNode.style.cssText="";this.containerNode.innerHTML="";if(this._onDeck){this.show.apply(this,this._onDeck);this._onDeck=null;}}});if(has("dojo-bidi")){_51c.extend({_setAutoTextDir:function(node){this.applyTextDir(node);_50f.forEach(node.children,function(_52f){this._setAutoTextDir(_52f);},this);},_setTextDirAttr:function(_530){this._set("textDir",_530);if(_530=="auto"){this._setAutoTextDir(this.containerNode);}else{this.containerNode.dir=this.textDir;}}});}_51b.showTooltip=function(_531,_532,_533,rtl,_534,_535,_536){if(_533){_533=_50f.map(_533,function(val){return {after:"after-centered",before:"before-centered"}[val]||val;});}if(!_523._masterTT){_51b._masterTT=_523._masterTT=new _51c();}return _523._masterTT.show(_531,_532,_533,rtl,_534,_535,_536);};_51b.hideTooltip=function(_537){return _523._masterTT&&_523._masterTT.hide(_537);};var _538="DORMANT",_539="SHOW TIMER",_53a="SHOWING",_53b="HIDE TIMER";function noop(){};var _523=_510("dijit.Tooltip",_517,{label:"",showDelay:400,hideDelay:400,connectId:[],position:[],selector:"",_setConnectIdAttr:function(_53c){_50f.forEach(this._connections||[],function(_53d){_50f.forEach(_53d,function(_53e){_53e.remove();});},this);this._connectIds=_50f.filter(lang.isArrayLike(_53c)?_53c:(_53c?[_53c]:[]),function(id){return dom.byId(id,this.ownerDocument);},this);this._connections=_50f.map(this._connectIds,function(id){var node=dom.byId(id,this.ownerDocument),_53f=this.selector,_540=_53f?function(_541){return on.selector(_53f,_541);}:function(_542){return _542;},self=this;return [on(node,_540(_514.enter),function(){self._onHover(this);}),on(node,_540("focusin"),function(){self._onHover(this);}),on(node,_540(_514.leave),lang.hitch(self,"_onUnHover")),on(node,_540("focusout"),lang.hitch(self,"set","state",_538))];},this);this._set("connectId",_53c);},addTarget:function(node){var id=node.id||node;if(_50f.indexOf(this._connectIds,id)==-1){this.set("connectId",this._connectIds.concat(id));}},removeTarget:function(node){var id=node.id||node,idx=_50f.indexOf(this._connectIds,id);if(idx>=0){this._connectIds.splice(idx,1);this.set("connectId",this._connectIds);}},buildRendering:function(){this.inherited(arguments);_511.add(this.domNode,"dijitTooltipData");},startup:function(){this.inherited(arguments);var ids=this.connectId;_50f.forEach(lang.isArrayLike(ids)?ids:[ids],this.addTarget,this);},getContent:function(node){return this.label||this.domNode.innerHTML;},state:_538,_setStateAttr:function(val){if(this.state==val||(val==_539&&this.state==_53a)||(val==_53b&&this.state==_538)){return;}if(this._hideTimer){this._hideTimer.remove();delete this._hideTimer;}if(this._showTimer){this._showTimer.remove();delete this._showTimer;}switch(val){case _538:if(this._connectNode){_523.hide(this._connectNode);delete this._connectNode;this.onHide();}break;case _539:if(this.state!=_53a){this._showTimer=this.defer(function(){this.set("state",_53a);},this.showDelay);}break;case _53a:var _543=this.getContent(this._connectNode);if(!_543){this.set("state",_538);return;}_523.show(_543,this._connectNode,this.position,!this.isLeftToRight(),this.textDir,lang.hitch(this,"set","state",_53a),lang.hitch(this,"set","state",_53b));this.onShow(this._connectNode,this.position);break;case _53b:this._hideTimer=this.defer(function(){this.set("state",_538);},this.hideDelay);break;}this._set("state",val);},_onHover:function(_544){if(this._connectNode&&_544!=this._connectNode){this.set("state",_538);}this._connectNode=_544;this.set("state",_539);},_onUnHover:function(_545){this.set("state",_53b);},open:function(_546){this.set("state",_538);this._connectNode=_546;this.set("state",_53a);},close:function(){this.set("state",_538);},onShow:function(){},onHide:function(){},destroy:function(){this.set("state",_538);_50f.forEach(this._connections||[],function(_547){_50f.forEach(_547,function(_548){_548.remove();});},this);this.inherited(arguments);}});_523._MasterTooltip=_51c;_523.show=_51b.showTooltip;_523.hide=_51b.hideTooltip;_523.defaultPosition=["after-centered","before-centered"];return _523;});},"dojo/string":function(){define(["./_base/kernel","./_base/lang"],function(_549,lang){var _54a=/[&<>'"\/]/g;var _54b={"&":"&","<":"<",">":">","\"":""","'":"'","/":"/"};var _54c={};lang.setObject("dojo.string",_54c);_54c.escape=function(str){if(!str){return "";}return str.replace(_54a,function(c){return _54b[c];});};_54c.rep=function(str,num){if(num<=0||!str){return "";}var buf=[];for(;;){if(num&1){buf.push(str);}if(!(num>>=1)){break;}str+=str;}return buf.join("");};_54c.pad=function(text,size,ch,end){if(!ch){ch="0";}var out=String(text),pad=_54c.rep(ch,Math.ceil((size-out.length)/ch.length));return end?out+pad:pad+out;};_54c.substitute=function(_54d,map,_54e,_54f){_54f=_54f||_549.global;_54e=_54e?lang.hitch(_54f,_54e):function(v){return v;};return _54d.replace(/\$\{([^\s\:\}]*)(?:\:([^\s\:\}]+))?\}/g,function(_550,key,_551){if(key==""){return "$";}var _552=lang.getObject(key,false,map);if(_551){_552=lang.getObject(_551,false,_54f).call(_54f,_552,key);}var _553=_54e(_552,key);if(typeof _553==="undefined"){throw new Error("string.substitute could not find key \""+key+"\" in template");}return _553.toString();});};_54c.trim=String.prototype.trim?lang.trim:function(str){str=str.replace(/^\s+/,"");for(var i=str.length-1;i>=0;i--){if(/\S/.test(str.charAt(i))){str=str.substring(0,i+1);break;}}return str;};return _54c;});},"dijit/layout/AccordionPane":function(){define(["dojo/_base/declare","dojo/_base/kernel","./ContentPane"],function(_554,_555,_556){return _554("dijit.layout.AccordionPane",_556,{constructor:function(){_555.deprecated("dijit.layout.AccordionPane deprecated, use ContentPane instead","","2.0");},onSelected:function(){}});});},"dijit/dijit":function(){define(["./main","./_base","dojo/parser","./_Widget","./_TemplatedMixin","./_Container","./layout/_LayoutWidget","./form/_FormWidget","./form/_FormValueWidget"],function(_557){return _557;});},"dijit/form/DropDownButton":function(){define(["dojo/_base/declare","dojo/_base/kernel","dojo/_base/lang","dojo/query","../registry","../popup","./Button","../_Container","../_HasDropDown","dojo/text!./templates/DropDownButton.html","../a11yclick"],function(_558,_559,lang,_55a,_55b,_55c,_55d,_55e,_55f,_560){return _558("dijit.form.DropDownButton",[_55d,_55e,_55f],{baseClass:"dijitDropDownButton",templateString:_560,_fillContent:function(){var _561=this.srcNodeRef;var dest=this.containerNode;if(_561&&dest){while(_561.hasChildNodes()){var _562=_561.firstChild;if(_562.hasAttribute&&(_562.hasAttribute("data-dojo-type")||_562.hasAttribute("dojoType")||_562.hasAttribute("data-"+_559._scopeName+"-type")||_562.hasAttribute(_559._scopeName+"Type"))){this.dropDownContainer=this.ownerDocument.createElement("div");this.dropDownContainer.appendChild(_562);}else{dest.appendChild(_562);}}}},startup:function(){if(this._started){return;}if(!this.dropDown&&this.dropDownContainer){this.dropDown=_55b.byNode(this.dropDownContainer.firstChild);delete this.dropDownContainer;}if(this.dropDown){_55c.hide(this.dropDown);}this.inherited(arguments);},isLoaded:function(){var _563=this.dropDown;return (!!_563&&(!_563.href||_563.isLoaded));},loadDropDown:function(_564){var _565=this.dropDown;var _566=_565.on("load",lang.hitch(this,function(){_566.remove();_564();}));_565.refresh();},isFocusable:function(){return this.inherited(arguments)&&!this._mouseDown;}});});},"dijit/form/_FormValueMixin":function(){define(["dojo/_base/declare","dojo/dom-attr","dojo/keys","dojo/_base/lang","dojo/on","dojo/sniff","./_FormWidgetMixin"],function(_567,_568,keys,lang,on,has,_569){return _567("dijit.form._FormValueMixin",_569,{readOnly:false,_setReadOnlyAttr:function(_56a){if(has("trident")&&"disabled" in this){_568.set(this.focusNode,"readOnly",_56a||this.disabled);}else{_568.set(this.focusNode,"readOnly",_56a);}this._set("readOnly",_56a);},postCreate:function(){this.inherited(arguments);if(this._resetValue===undefined){this._lastValueReported=this._resetValue=this.value;}},_setValueAttr:function(_56b,_56c){this._handleOnChange(_56b,_56c);},_handleOnChange:function(_56d,_56e){this._set("value",_56d);this.inherited(arguments);},undo:function(){this._setValueAttr(this._lastValueReported,false);},reset:function(){this._hasBeenBlurred=false;this._setValueAttr(this._resetValue,true);}});});},"dijit/form/_FormWidgetMixin":function(){define(["dojo/_base/array","dojo/_base/declare","dojo/dom-attr","dojo/dom-style","dojo/_base/lang","dojo/mouse","dojo/on","dojo/sniff","dojo/window","../a11y"],function(_56f,_570,_571,_572,lang,_573,on,has,_574,a11y){return _570("dijit.form._FormWidgetMixin",null,{name:"",alt:"",value:"",type:"text","aria-label":"focusNode",tabIndex:"0",_setTabIndexAttr:"focusNode",disabled:false,intermediateChanges:false,scrollOnFocus:true,_setIdAttr:"focusNode",_setDisabledAttr:function(_575){this._set("disabled",_575);if(/^(button|input|select|textarea|optgroup|option|fieldset)$/i.test(this.focusNode.tagName)){_571.set(this.focusNode,"disabled",_575);if(has("trident")&&"readOnly" in this){_571.set(this.focusNode,"readonly",_575||this.readOnly);}}else{this.focusNode.setAttribute("aria-disabled",_575?"true":"false");}if(this.valueNode){_571.set(this.valueNode,"disabled",_575);}if(_575){this._set("hovering",false);this._set("active",false);var _576="tabIndex" in this.attributeMap?this.attributeMap.tabIndex:("_setTabIndexAttr" in this)?this._setTabIndexAttr:"focusNode";_56f.forEach(lang.isArray(_576)?_576:[_576],function(_577){var node=this[_577];if(has("webkit")||a11y.hasDefaultTabStop(node)){node.setAttribute("tabIndex","-1");}else{node.removeAttribute("tabIndex");}},this);}else{if(this.tabIndex!=""){this.set("tabIndex",this.tabIndex);}}},_onFocus:function(by){if(by=="mouse"&&this.isFocusable()){var _578=this.own(on(this.focusNode,"focus",function(){_579.remove();_578.remove();}))[0];var _57a=has("pointer-events")?"pointerup":has("MSPointer")?"MSPointerUp":has("touch-events")?"touchend, mouseup":"mouseup";var _579=this.own(on(this.ownerDocumentBody,_57a,lang.hitch(this,function(evt){_579.remove();_578.remove();if(this.focused){if(evt.type=="touchend"){this.defer("focus");}else{this.focus();}}})))[0];}if(this.scrollOnFocus){this.defer(function(){_574.scrollIntoView(this.domNode);});}this.inherited(arguments);},isFocusable:function(){return !this.disabled&&this.focusNode&&(_572.get(this.domNode,"display")!="none");},focus:function(){if(!this.disabled&&this.focusNode.focus){try{this.focusNode.focus();}catch(e){}}},compare:function(val1,val2){if(typeof val1=="number"&&typeof val2=="number"){return (isNaN(val1)&&isNaN(val2))?0:val1-val2;}else{if(val1>val2){return 1;}else{if(val1 *",this.containerNode).some(function(node){var _59f=_599.byNode(node);if(_59f&&_59f.resize){_59d.push(_59f);}else{if(!/script|link|style/i.test(node.nodeName)&&node.offsetHeight){_59e=true;}}});this._singleChild=_59d.length==1&&!_59e?_59d[0]:null;_595.toggle(this.containerNode,this.baseClass+"SingleChild",!!this._singleChild);},resize:function(_5a0,_5a1){this._resizeCalled=true;this._scheduleLayout(_5a0,_5a1);},_scheduleLayout:function(_5a2,_5a3){if(this._isShown()){this._layout(_5a2,_5a3);}else{this._needLayout=true;this._changeSize=_5a2;this._resultSize=_5a3;}},_layout:function(_5a4,_5a5){delete this._needLayout;if(!this._wasShown&&this.open!==false){this._onShow();}if(_5a4){_596.setMarginBox(this.domNode,_5a4);}var cn=this.containerNode;if(cn===this.domNode){var mb=_5a5||{};lang.mixin(mb,_5a4||{});if(!("h" in mb)||!("w" in mb)){mb=lang.mixin(_596.getMarginBox(cn),mb);}this._contentBox=_59b.marginBox2contentBox(cn,mb);}else{this._contentBox=_596.getContentBox(cn);}this._layoutChildren();},_layoutChildren:function(){this._checkIfSingleChild();if(this._singleChild&&this._singleChild.resize){var cb=this._contentBox||_596.getContentBox(this.containerNode);this._singleChild.resize({w:cb.w,h:cb.h});}else{var _5a6=this.getChildren(),_5a7,i=0;while(_5a7=_5a6[i++]){if(_5a7.resize){_5a7.resize();}}}},_isShown:function(){if(this._childOfLayoutWidget){if(this._resizeCalled&&"open" in this){return this.open;}return this._resizeCalled;}else{if("open" in this){return this.open;}else{var node=this.domNode,_5a8=this.domNode.parentNode;return (node.style.display!="none")&&(node.style.visibility!="hidden")&&!_595.contains(node,"dijitHidden")&&_5a8&&_5a8.style&&(_5a8.style.display!="none");}}},_onShow:function(){this._wasShown=true;if(this._needLayout){this._layout(this._changeSize,this._resultSize);}this.inherited(arguments);}});});},"dijit/WidgetSet":function(){define(["dojo/_base/array","dojo/_base/declare","dojo/_base/kernel","./registry"],function(_5a9,_5aa,_5ab,_5ac){var _5ad=_5aa("dijit.WidgetSet",null,{constructor:function(){this._hash={};this.length=0;},add:function(_5ae){if(this._hash[_5ae.id]){throw new Error("Tried to register widget with id=="+_5ae.id+" but that id is already registered");}this._hash[_5ae.id]=_5ae;this.length++;},remove:function(id){if(this._hash[id]){delete this._hash[id];this.length--;}},forEach:function(func,_5af){_5af=_5af||_5ab.global;var i=0,id;for(id in this._hash){func.call(_5af,this._hash[id],i++,this._hash);}return this;},filter:function(_5b0,_5b1){_5b1=_5b1||_5ab.global;var res=new _5ad(),i=0,id;for(id in this._hash){var w=this._hash[id];if(_5b0.call(_5b1,w,i++,this._hash)){res.add(w);}}return res;},byId:function(id){return this._hash[id];},byClass:function(cls){var res=new _5ad(),id,_5b2;for(id in this._hash){_5b2=this._hash[id];if(_5b2.declaredClass==cls){res.add(_5b2);}}return res;},toArray:function(){var ar=[];for(var id in this._hash){ar.push(this._hash[id]);}return ar;},map:function(func,_5b3){return _5a9.map(this.toArray(),func,_5b3);},every:function(func,_5b4){_5b4=_5b4||_5ab.global;var x=0,i;for(i in this._hash){if(!func.call(_5b4,this._hash[i],x++,this._hash)){return false;}}return true;},some:function(func,_5b5){_5b5=_5b5||_5ab.global;var x=0,i;for(i in this._hash){if(func.call(_5b5,this._hash[i],x++,this._hash)){return true;}}return false;}});_5a9.forEach(["forEach","filter","byClass","map","every","some"],function(func){_5ac[func]=_5ad.prototype[func];});return _5ad;});},"dojo/dnd/Moveable":function(){define(["../_base/array","../_base/declare","../_base/lang","../dom","../dom-class","../Evented","../has","../on","../topic","../touch","./common","./Mover","../_base/window"],function(_5b6,_5b7,lang,dom,_5b8,_5b9,has,on,_5ba,_5bb,dnd,_5bc,win){var _5bd;var _5be=function(){};function _5bf(){if("touchAction" in document.body.style){_5bd="touchAction";}else{if("msTouchAction" in document.body.style){_5bd="msTouchAction";}}_5be=function _5be(node,_5c0){node.style[_5bd]=_5c0;};_5be(arguments[0],arguments[1]);};if(has("touch-action")){_5be=_5bf;}var _5c1=_5b7("dojo.dnd.Moveable",[_5b9],{handle:"",delay:0,skip:false,constructor:function(node,_5c2){this.node=dom.byId(node);_5be(this.node,"none");if(!_5c2){_5c2={};}this.handle=_5c2.handle?dom.byId(_5c2.handle):null;if(!this.handle){this.handle=this.node;}this.delay=_5c2.delay>0?_5c2.delay:0;this.skip=_5c2.skip;this.mover=_5c2.mover?_5c2.mover:_5bc;this.events=[on(this.handle,_5bb.press,lang.hitch(this,"onMouseDown")),on(this.handle,"dragstart",lang.hitch(this,"onSelectStart")),on(this.handle,"selectstart",lang.hitch(this,"onSelectStart"))];},markupFactory:function(_5c3,node,Ctor){return new Ctor(node,_5c3);},destroy:function(){_5b6.forEach(this.events,function(_5c4){_5c4.remove();});_5be(this.node,"");this.events=this.node=this.handle=null;},onMouseDown:function(e){if(this.skip&&dnd.isFormElement(e)){return;}if(this.delay){this.events.push(on(this.handle,_5bb.move,lang.hitch(this,"onMouseMove")),on(this.handle.ownerDocument,_5bb.release,lang.hitch(this,"onMouseUp")));this._lastX=e.pageX;this._lastY=e.pageY;}else{this.onDragDetected(e);}e.stopPropagation();e.preventDefault();},onMouseMove:function(e){if(Math.abs(e.pageX-this._lastX)>this.delay||Math.abs(e.pageY-this._lastY)>this.delay){this.onMouseUp(e);this.onDragDetected(e);}e.stopPropagation();e.preventDefault();},onMouseUp:function(e){for(var i=0;i<2;++i){this.events.pop().remove();}e.stopPropagation();e.preventDefault();},onSelectStart:function(e){if(!this.skip||!dnd.isFormElement(e)){e.stopPropagation();e.preventDefault();}},onDragDetected:function(e){new this.mover(this.node,e,this);},onMoveStart:function(_5c5){_5ba.publish("/dnd/move/start",_5c5);_5b8.add(win.body(),"dojoMove");_5b8.add(this.node,"dojoMoveItem");},onMoveStop:function(_5c6){_5ba.publish("/dnd/move/stop",_5c6);_5b8.remove(win.body(),"dojoMove");_5b8.remove(this.node,"dojoMoveItem");},onFirstMove:function(){},onMove:function(_5c7,_5c8){this.onMoving(_5c7,_5c8);var s=_5c7.node.style;s.left=_5c8.l+"px";s.top=_5c8.t+"px";this.onMoved(_5c7,_5c8);},onMoving:function(){},onMoved:function(){}});return _5c1;});},"dijit/TooltipDialog":function(){define(["dojo/_base/declare","dojo/dom-class","dojo/has","dojo/keys","dojo/_base/lang","dojo/on","./focus","./layout/ContentPane","./_DialogMixin","./form/_FormMixin","./_TemplatedMixin","dojo/text!./templates/TooltipDialog.html","./main"],function(_5c9,_5ca,has,keys,lang,on,_5cb,_5cc,_5cd,_5ce,_5cf,_5d0,_5d1){var _5d2=_5c9("dijit.TooltipDialog",[_5cc,_5cf,_5ce,_5cd],{title:"",doLayout:false,autofocus:true,baseClass:"dijitTooltipDialog",_firstFocusItem:null,_lastFocusItem:null,templateString:_5d0,_setTitleAttr:"containerNode",postCreate:function(){this.inherited(arguments);this.own(on(this.domNode,"keydown",lang.hitch(this,"_onKey")));},orient:function(node,_5d3,_5d4){var newC={"MR-ML":"dijitTooltipRight","ML-MR":"dijitTooltipLeft","TM-BM":"dijitTooltipAbove","BM-TM":"dijitTooltipBelow","BL-TL":"dijitTooltipBelow dijitTooltipABLeft","TL-BL":"dijitTooltipAbove dijitTooltipABLeft","BR-TR":"dijitTooltipBelow dijitTooltipABRight","TR-BR":"dijitTooltipAbove dijitTooltipABRight","BR-BL":"dijitTooltipRight","BL-BR":"dijitTooltipLeft","BR-TL":"dijitTooltipBelow dijitTooltipABLeft","BL-TR":"dijitTooltipBelow dijitTooltipABRight","TL-BR":"dijitTooltipAbove dijitTooltipABRight","TR-BL":"dijitTooltipAbove dijitTooltipABLeft"}[_5d3+"-"+_5d4];_5ca.replace(this.domNode,newC,this._currentOrientClass||"");this._currentOrientClass=newC;},focus:function(){this._getFocusItems();_5cb.focus(this._firstFocusItem);},onOpen:function(pos){this.orient(this.domNode,pos.aroundCorner,pos.corner);var _5d5=pos.aroundNodePos;if(pos.corner.charAt(0)=="M"&&pos.aroundCorner.charAt(0)=="M"){this.connectorNode.style.top=_5d5.y+((_5d5.h-this.connectorNode.offsetHeight)>>1)-pos.y+"px";this.connectorNode.style.left="";}else{if(pos.corner.charAt(1)=="M"&&pos.aroundCorner.charAt(1)=="M"){this.connectorNode.style.left=_5d5.x+((_5d5.w-this.connectorNode.offsetWidth)>>1)-pos.x+"px";}}this._onShow();},onClose:function(){this.onHide();},_onKey:function(evt){if(evt.keyCode==keys.ESCAPE){this.defer("onCancel");evt.stopPropagation();evt.preventDefault();}else{if(evt.keyCode==keys.TAB){var node=evt.target;this._getFocusItems();if(this._firstFocusItem==this._lastFocusItem){evt.stopPropagation();evt.preventDefault();}else{if(node==this._firstFocusItem&&evt.shiftKey){_5cb.focus(this._lastFocusItem);evt.stopPropagation();evt.preventDefault();}else{if(node==this._lastFocusItem&&!evt.shiftKey){_5cb.focus(this._firstFocusItem);evt.stopPropagation();evt.preventDefault();}else{evt.stopPropagation();}}}}}}});if(has("dojo-bidi")){_5d2.extend({_setTitleAttr:function(_5d6){this.containerNode.title=(this.textDir&&this.enforceTextDirWithUcc)?this.enforceTextDirWithUcc(null,_5d6):_5d6;this._set("title",_5d6);},_setTextDirAttr:function(_5d7){if(!this._created||this.textDir!=_5d7){this._set("textDir",_5d7);if(this.textDir&&this.title){this.containerNode.title=this.enforceTextDirWithUcc(null,this.title);}}}});}return _5d2;});},"dojo/store/util/SimpleQueryEngine":function(){define(["../../_base/array"],function(_5d8){return function(_5d9,_5da){switch(typeof _5d9){default:throw new Error("Can not query with a "+typeof _5d9);case "object":case "undefined":var _5db=_5d9;_5d9=function(_5dc){for(var key in _5db){var _5dd=_5db[key];if(_5dd&&_5dd.test){if(!_5dd.test(_5dc[key],_5dc)){return false;}}else{if(_5dd!=_5dc[key]){return false;}}}return true;};break;case "string":if(!this[_5d9]){throw new Error("No filter function "+_5d9+" was found in store");}_5d9=this[_5d9];case "function":}function _5de(_5df){var _5e0=_5d8.filter(_5df,_5d9);var _5e1=_5da&&_5da.sort;if(_5e1){_5e0.sort(typeof _5e1=="function"?_5e1:function(a,b){for(var sort,i=0;sort=_5e1[i];i++){var _5e2=a[sort.attribute];var _5e3=b[sort.attribute];_5e2=_5e2!=null?_5e2.valueOf():_5e2;_5e3=_5e3!=null?_5e3.valueOf():_5e3;if(_5e2!=_5e3){return !!sort.descending==(_5e2==null||_5e2>_5e3)?-1:1;}}return 0;});}if(_5da&&(_5da.start||_5da.count)){var _5e4=_5e0.length;_5e0=_5e0.slice(_5da.start||0,(_5da.start||0)+(_5da.count||Infinity));_5e0.total=_5e4;}return _5e0;};_5de.matches=_5d9;return _5de;};});},"dijit/typematic":function(){define(["dojo/_base/array","dojo/_base/connect","dojo/_base/lang","dojo/on","dojo/sniff","./main"],function(_5e5,_5e6,lang,on,has,_5e7){var _5e8=(_5e7.typematic={_fireEventAndReload:function(){this._timer=null;this._callback(++this._count,this._node,this._evt);this._currentTimeout=Math.max(this._currentTimeout<0?this._initialDelay:(this._subsequentDelay>1?this._subsequentDelay:Math.round(this._currentTimeout*this._subsequentDelay)),this._minDelay);this._timer=setTimeout(lang.hitch(this,"_fireEventAndReload"),this._currentTimeout);},trigger:function(evt,_5e9,node,_5ea,obj,_5eb,_5ec,_5ed){if(obj!=this._obj){this.stop();this._initialDelay=_5ec||500;this._subsequentDelay=_5eb||0.9;this._minDelay=_5ed||10;this._obj=obj;this._node=node;this._currentTimeout=-1;this._count=-1;this._callback=lang.hitch(_5e9,_5ea);this._evt={faux:true};for(var attr in evt){if(attr!="layerX"&&attr!="layerY"){var v=evt[attr];if(typeof v!="function"&&typeof v!="undefined"){this._evt[attr]=v;}}}this._fireEventAndReload();}},stop:function(){if(this._timer){clearTimeout(this._timer);this._timer=null;}if(this._obj){this._callback(-1,this._node,this._evt);this._obj=null;}},addKeyListener:function(node,_5ee,_5ef,_5f0,_5f1,_5f2,_5f3){var type="keyCode" in _5ee?"keydown":"charCode" in _5ee?"keypress":_5e6._keypress,attr="keyCode" in _5ee?"keyCode":"charCode" in _5ee?"charCode":"charOrCode";var _5f4=[on(node,type,lang.hitch(this,function(evt){if(evt[attr]==_5ee[attr]&&(_5ee.ctrlKey===undefined||_5ee.ctrlKey==evt.ctrlKey)&&(_5ee.altKey===undefined||_5ee.altKey==evt.altKey)&&(_5ee.metaKey===undefined||_5ee.metaKey==(evt.metaKey||false))&&(_5ee.shiftKey===undefined||_5ee.shiftKey==evt.shiftKey)){evt.stopPropagation();evt.preventDefault();_5e8.trigger(evt,_5ef,node,_5f0,_5ee,_5f1,_5f2,_5f3);}else{if(_5e8._obj==_5ee){_5e8.stop();}}})),on(node,"keyup",lang.hitch(this,function(){if(_5e8._obj==_5ee){_5e8.stop();}}))];return {remove:function(){_5e5.forEach(_5f4,function(h){h.remove();});}};},addMouseListener:function(node,_5f5,_5f6,_5f7,_5f8,_5f9){var _5fa=[on(node,"mousedown",lang.hitch(this,function(evt){evt.preventDefault();_5e8.trigger(evt,_5f5,node,_5f6,node,_5f7,_5f8,_5f9);})),on(node,"mouseup",lang.hitch(this,function(evt){if(this._obj){evt.preventDefault();}_5e8.stop();})),on(node,"mouseout",lang.hitch(this,function(evt){if(this._obj){evt.preventDefault();}_5e8.stop();})),on(node,"dblclick",lang.hitch(this,function(evt){evt.preventDefault();if(has("ie")<9){_5e8.trigger(evt,_5f5,node,_5f6,node,_5f7,_5f8,_5f9);setTimeout(lang.hitch(this,_5e8.stop),50);}}))];return {remove:function(){_5e5.forEach(_5fa,function(h){h.remove();});}};},addListener:function(_5fb,_5fc,_5fd,_5fe,_5ff,_600,_601,_602){var _603=[this.addKeyListener(_5fc,_5fd,_5fe,_5ff,_600,_601,_602),this.addMouseListener(_5fb,_5fe,_5ff,_600,_601,_602)];return {remove:function(){_5e5.forEach(_603,function(h){h.remove();});}};}});return _5e8;});},"dijit/MenuItem":function(){define(["dojo/_base/declare","dojo/dom","dojo/dom-attr","dojo/dom-class","dojo/_base/kernel","dojo/sniff","dojo/_base/lang","./_Widget","./_TemplatedMixin","./_Contained","./_CssStateMixin","dojo/text!./templates/MenuItem.html"],function(_604,dom,_605,_606,_607,has,lang,_608,_609,_60a,_60b,_60c){var _60d=_604("dijit.MenuItem"+(has("dojo-bidi")?"_NoBidi":""),[_608,_609,_60a,_60b],{templateString:_60c,baseClass:"dijitMenuItem",label:"",_setLabelAttr:function(val){this._set("label",val);var _60e="";var text;var ndx=val.search(/{\S}/);if(ndx>=0){_60e=val.charAt(ndx+1);var _60f=val.substr(0,ndx);var _610=val.substr(ndx+3);text=_60f+_60e+_610;val=_60f+""+_60e+""+_610;}else{text=val;}this.domNode.setAttribute("aria-label",text+" "+this.accelKey);this.containerNode.innerHTML=val;this._set("shortcutKey",_60e);},iconClass:"dijitNoIcon",_setIconClassAttr:{node:"iconNode",type:"class"},accelKey:"",disabled:false,_fillContent:function(_611){if(_611&&!("label" in this.params)){this._set("label",_611.innerHTML);}},buildRendering:function(){this.inherited(arguments);var _612=this.id+"_text";_605.set(this.containerNode,"id",_612);if(this.accelKeyNode){_605.set(this.accelKeyNode,"id",this.id+"_accel");}dom.setSelectable(this.domNode,false);},onClick:function(){},focus:function(){try{if(has("ie")==8){this.containerNode.focus();}this.focusNode.focus();}catch(e){}},_setSelected:function(_613){_606.toggle(this.domNode,"dijitMenuItemSelected",_613);},setLabel:function(_614){_607.deprecated("dijit.MenuItem.setLabel() is deprecated. Use set('label', ...) instead.","","2.0");this.set("label",_614);},setDisabled:function(_615){_607.deprecated("dijit.Menu.setDisabled() is deprecated. Use set('disabled', bool) instead.","","2.0");this.set("disabled",_615);},_setDisabledAttr:function(_616){this.focusNode.setAttribute("aria-disabled",_616?"true":"false");this._set("disabled",_616);},_setAccelKeyAttr:function(_617){if(this.accelKeyNode){this.accelKeyNode.style.display=_617?"":"none";this.accelKeyNode.innerHTML=_617;_605.set(this.containerNode,"colSpan",_617?"1":"2");}this._set("accelKey",_617);}});if(has("dojo-bidi")){_60d=_604("dijit.MenuItem",_60d,{_setLabelAttr:function(val){this.inherited(arguments);if(this.textDir==="auto"){this.applyTextDir(this.textDirNode);}}});}return _60d;});},"dijit/layout/TabController":function(){define(["dojo/_base/declare","dojo/dom","dojo/dom-attr","dojo/dom-class","dojo/has","dojo/i18n","dojo/_base/lang","./StackController","../registry","../Menu","../MenuItem","dojo/text!./templates/_TabButton.html","dojo/i18n!../nls/common"],function(_618,dom,_619,_61a,has,i18n,lang,_61b,_61c,Menu,_61d,_61e){var _61f=_618("dijit.layout._TabButton"+(has("dojo-bidi")?"_NoBidi":""),_61b.StackButton,{baseClass:"dijitTab",cssStateNodes:{closeNode:"dijitTabCloseButton"},templateString:_61e,_setNameAttr:"focusNode",scrollOnFocus:false,buildRendering:function(){this.inherited(arguments);dom.setSelectable(this.containerNode,false);},startup:function(){this.inherited(arguments);var n=this.domNode;this.defer(function(){n.className=n.className;},1);},_setCloseButtonAttr:function(disp){this._set("closeButton",disp);_61a.toggle(this.domNode,"dijitClosable",disp);this.closeNode.style.display=disp?"":"none";if(disp){var _620=i18n.getLocalization("dijit","common");if(this.closeNode){_619.set(this.closeNode,"title",_620.itemClose);}}},_setDisabledAttr:function(_621){this.inherited(arguments);if(this.closeNode){if(_621){_619.remove(this.closeNode,"title");}else{var _622=i18n.getLocalization("dijit","common");_619.set(this.closeNode,"title",_622.itemClose);}}},_setLabelAttr:function(_623){this.inherited(arguments);if(!this.showLabel&&!this.params.title){this.iconNode.alt=lang.trim(this.containerNode.innerText||this.containerNode.textContent||"");}}});if(has("dojo-bidi")){_61f=_618("dijit.layout._TabButton",_61f,{_setLabelAttr:function(_624){this.inherited(arguments);this.applyTextDir(this.iconNode,this.iconNode.alt);}});}var _625=_618("dijit.layout.TabController",_61b,{baseClass:"dijitTabController",templateString:"
    ",tabPosition:"top",buttonWidget:_61f,buttonWidgetCloseClass:"dijitTabCloseButton",postCreate:function(){this.inherited(arguments);var _626=new Menu({id:this.id+"_Menu",ownerDocument:this.ownerDocument,dir:this.dir,lang:this.lang,textDir:this.textDir,targetNodeIds:[this.domNode],selector:function(node){return _61a.contains(node,"dijitClosable")&&!_61a.contains(node,"dijitTabDisabled");}});this.own(_626);var _627=i18n.getLocalization("dijit","common"),_628=this;_626.addChild(new _61d({label:_627.itemClose,ownerDocument:this.ownerDocument,dir:this.dir,lang:this.lang,textDir:this.textDir,onClick:function(evt){var _629=_61c.byNode(this.getParent().currentTarget);_628.onCloseButtonClick(_629.page);}}));}});_625.TabButton=_61f;return _625;});},"dijit/ToolbarSeparator":function(){define(["dojo/_base/declare","dojo/dom","./_Widget","./_TemplatedMixin"],function(_62a,dom,_62b,_62c){return _62a("dijit.ToolbarSeparator",[_62b,_62c],{templateString:"
    ",buildRendering:function(){this.inherited(arguments);dom.setSelectable(this.domNode,false);},isFocusable:function(){return false;}});});},"dijit/layout/_LayoutWidget":function(){define(["dojo/_base/lang","../_Widget","../_Container","../_Contained","../Viewport","dojo/_base/declare","dojo/dom-class","dojo/dom-geometry","dojo/dom-style"],function(lang,_62d,_62e,_62f,_630,_631,_632,_633,_634){return _631("dijit.layout._LayoutWidget",[_62d,_62e,_62f],{baseClass:"dijitLayoutContainer",isLayoutContainer:true,_setTitleAttr:null,buildRendering:function(){this.inherited(arguments);_632.add(this.domNode,"dijitContainer");},startup:function(){if(this._started){return;}this.inherited(arguments);var _635=this.getParent&&this.getParent();if(!(_635&&_635.isLayoutContainer)){this.resize();this.own(_630.on("resize",lang.hitch(this,"resize")));}},resize:function(_636,_637){var node=this.domNode;if(_636){_633.setMarginBox(node,_636);}var mb=_637||{};lang.mixin(mb,_636||{});if(!("h" in mb)||!("w" in mb)){mb=lang.mixin(_633.getMarginBox(node),mb);}var cs=_634.getComputedStyle(node);var me=_633.getMarginExtents(node,cs);var be=_633.getBorderExtents(node,cs);var bb=(this._borderBox={w:mb.w-(me.w+be.w),h:mb.h-(me.h+be.h)});var pe=_633.getPadExtents(node,cs);this._contentBox={l:_634.toPixelValue(node,cs.paddingLeft),t:_634.toPixelValue(node,cs.paddingTop),w:bb.w-pe.w,h:bb.h-pe.h};this.layout();},layout:function(){},_setupChild:function(_638){var cls=this.baseClass+"-child "+(_638.baseClass?this.baseClass+"-"+_638.baseClass:"");_632.add(_638.domNode,cls);},addChild:function(_639,_63a){this.inherited(arguments);if(this._started){this._setupChild(_639);}},removeChild:function(_63b){var cls=this.baseClass+"-child"+(_63b.baseClass?" "+this.baseClass+"-"+_63b.baseClass:"");_632.remove(_63b.domNode,cls);this.inherited(arguments);}});});},"dojo/selector/lite":function(){define(["../has","../_base/kernel"],function(has,dojo){"use strict";var _63c=document.createElement("div");var _63d=_63c.matches||_63c.webkitMatchesSelector||_63c.mozMatchesSelector||_63c.msMatchesSelector||_63c.oMatchesSelector;var _63e=_63c.querySelectorAll;var _63f=/([^\s,](?:"(?:\\.|[^"])+"|'(?:\\.|[^'])+'|[^,])*)/g;has.add("dom-matches-selector",!!_63d);has.add("dom-qsa",!!_63e);var _640=function(_641,root){if(_642&&_641.indexOf(",")>-1){return _642(_641,root);}var doc=root?root.ownerDocument||root:dojo.doc||document,_643=(_63e?/^([\w]*)#([\w\-]+$)|^(\.)([\w\-\*]+$)|^(\w+$)/:/^([\w]*)#([\w\-]+)(?:\s+(.*))?$|(?:^|(>|.+\s+))([\w\-\*]+)(\S*$)/).exec(_641);root=root||doc;if(_643){var _644=has("ie")===8&&has("quirks")?root.nodeType===doc.nodeType:root.parentNode!==null&&root.nodeType!==9&&root.parentNode===doc;if(_643[2]&&_644){var _645=dojo.byId?dojo.byId(_643[2],doc):doc.getElementById(_643[2]);if(!_645||(_643[1]&&_643[1]!=_645.tagName.toLowerCase())){return [];}if(root!=doc){var _646=_645;while(_646!=root){_646=_646.parentNode;if(!_646){return [];}}}return _643[3]?_640(_643[3],_645):[_645];}if(_643[3]&&root.getElementsByClassName){return root.getElementsByClassName(_643[4]);}var _645;if(_643[5]){_645=root.getElementsByTagName(_643[5]);if(_643[4]||_643[6]){_641=(_643[4]||"")+_643[6];}else{return _645;}}}if(_63e){if(root.nodeType===1&&root.nodeName.toLowerCase()!=="object"){return _647(root,_641,root.querySelectorAll);}else{return root.querySelectorAll(_641);}}else{if(!_645){_645=root.getElementsByTagName("*");}}var _648=[];for(var i=0,l=_645.length;i-1&&(" "+node.className+" ").indexOf(_655)>-1;};},"#":function(id){return function(node){return node.id==id;};}};var _656={"^=":function(_657,_658){return _657.indexOf(_658)==0;},"*=":function(_659,_65a){return _659.indexOf(_65a)>-1;},"$=":function(_65b,_65c){return _65b.substring(_65b.length-_65c.length,_65b.length)==_65c;},"~=":function(_65d,_65e){return (" "+_65d+" ").indexOf(" "+_65e+" ")>-1;},"|=":function(_65f,_660){return (_65f+"-").indexOf(_660+"-")==0;},"=":function(_661,_662){return _661==_662;},"":function(_663,_664){return true;}};function attr(name,_665,type){var _666=_665.charAt(0);if(_666=="\""||_666=="'"){_665=_665.slice(1,-1);}_665=_665.replace(/\\/g,"");var _667=_656[type||""];return function(node){var _668=node.getAttribute(name);return _668&&_667(_668,_665);};};function _669(_66a){return function(node,root){while((node=node.parentNode)!=root){if(_66a(node,root)){return true;}}};};function _66b(_66c){return function(node,root){node=node.parentNode;return _66c?node!=root&&_66c(node,root):node==root;};};var _66d={};function and(_66e,next){return _66e?function(node,root){return next(node)&&_66e(node,root);}:next;};return function(node,_66f,root){var _670=_66d[_66f];if(!_670){if(_66f.replace(/(?:\s*([> ])\s*)|(#|\.)?((?:\\.|[\w-])+)|\[\s*([\w-]+)\s*(.?=)?\s*("(?:\\.|[^"])+"|'(?:\\.|[^'])+'|(?:\\.|[^\]])*)\s*\]/g,function(t,_671,type,_672,_673,_674,_675){if(_672){_670=and(_670,_652[type||""](_672.replace(/\\/g,"")));}else{if(_671){_670=(_671==" "?_669:_66b)(_670);}else{if(_673){_670=and(_670,attr(_673,_675,_674));}}}return "";})){throw new Error("Syntax error in query");}if(!_670){return true;}_66d[_66f]=_670;}return _670(node,root);};})();}if(!has("dom-qsa")){var _642=function(_676,root){var _677=_676.match(_63f);var _678=[];for(var i=0;i<_677.length;i++){_676=new String(_677[i].replace(/\s*$/,""));_676.indexOf=escape;var _679=_640(_676,root);for(var j=0,l=_679.length;j0&&_694[pi].parent===_694[pi-1].widget;pi--){}return _694[pi];},open:function(args){var _695=this._stack,_696=args.popup,node=_696.domNode,_697=args.orient||["below","below-alt","above","above-alt"],ltr=args.parent?args.parent.isLeftToRight():_682.isBodyLtr(_696.ownerDocument),_698=args.around,id=(args.around&&args.around.id)?(args.around.id+"_dropdown"):("popup_"+this._idGen++);while(_695.length&&(!args.parent||!dom.isDescendant(args.parent.domNode,_695[_695.length-1].widget.domNode))){this.close(_695[_695.length-1].widget);}var _699=this.moveOffScreen(_696);if(_696.startup&&!_696._started){_696.startup();}var _69a,_69b=_682.position(node);if("maxHeight" in args&&args.maxHeight!=-1){_69a=args.maxHeight||Infinity;}else{var _69c=_686.getEffectiveBox(this.ownerDocument),_69d=_698?_682.position(_698,false):{y:args.y-(args.padding||0),h:(args.padding||0)*2};_69a=Math.floor(Math.max(_69d.y,_69c.h-(_69d.y+_69d.h)));}if(_69b.h>_69a){var cs=_683.getComputedStyle(node),_69e=cs.borderLeftWidth+" "+cs.borderLeftStyle+" "+cs.borderLeftColor;_683.set(_699,{overflowY:"scroll",height:_69a+"px",border:_69e});node._originalStyle=node.style.cssText;node.style.border="none";}_680.set(_699,{id:id,style:{zIndex:this._beginZIndex+_695.length},"class":"dijitPopup "+(_696.baseClass||_696["class"]||"").split(" ")[0]+"Popup",dijitPopupParent:args.parent?args.parent.id:""});if(_695.length==0&&_698){this._firstAroundNode=_698;this._firstAroundPosition=_682.position(_698,true);this._aroundMoveListener=setTimeout(lang.hitch(this,"_repositionAll"),50);}if(has("config-bgIframe")&&!_696.bgIframe){_696.bgIframe=new _685(_699);}var _69f=_696.orient?lang.hitch(_696,"orient"):null,best=_698?_684.around(_699,_698,_697,ltr,_69f):_684.at(_699,args,_697=="R"?["TR","BR","TL","BL"]:["TL","BL","TR","BR"],args.padding,_69f);_699.style.visibility="visible";node.style.visibility="visible";var _6a0=[];_6a0.push(on(_699,"keydown",lang.hitch(this,function(evt){if(evt.keyCode==keys.ESCAPE&&args.onCancel){evt.stopPropagation();evt.preventDefault();args.onCancel();}else{if(evt.keyCode==keys.TAB){evt.stopPropagation();evt.preventDefault();var _6a1=this.getTopPopup();if(_6a1&&_6a1.onCancel){_6a1.onCancel();}}}})));if(_696.onCancel&&args.onCancel){_6a0.push(_696.on("cancel",args.onCancel));}_6a0.push(_696.on(_696.onExecute?"execute":"change",lang.hitch(this,function(){var _6a2=this.getTopPopup();if(_6a2&&_6a2.onExecute){_6a2.onExecute();}})));_695.push({widget:_696,wrapper:_699,parent:args.parent,onExecute:args.onExecute,onCancel:args.onCancel,onClose:args.onClose,handlers:_6a0});if(_696.onOpen){_696.onOpen(best);}return best;},close:function(_6a3){var _6a4=this._stack;while((_6a3&&_67d.some(_6a4,function(elem){return elem.widget==_6a3;}))||(!_6a3&&_6a4.length)){var top=_6a4.pop(),_6a5=top.widget,_6a6=top.onClose;if(_6a5.bgIframe){_6a5.bgIframe.destroy();delete _6a5.bgIframe;}if(_6a5.onClose){_6a5.onClose();}var h;while(h=top.handlers.pop()){h.remove();}if(_6a5&&_6a5.domNode){this.hide(_6a5);}if(_6a6){_6a6();}}if(_6a4.length==0&&this._aroundMoveListener){clearTimeout(this._aroundMoveListener);this._firstAroundNode=this._firstAroundPosition=this._aroundMoveListener=null;}}});return (_687.popup=new _689());});},"dijit/_base/manager":function(){define(["dojo/_base/array","dojo/_base/config","dojo/_base/lang","../registry","../main"],function(_6a7,_6a8,lang,_6a9,_6aa){var _6ab={};_6a7.forEach(["byId","getUniqueId","findWidgets","_destroyAll","byNode","getEnclosingWidget"],function(name){_6ab[name]=_6a9[name];});lang.mixin(_6ab,{defaultDuration:_6a8["defaultDuration"]||200});lang.mixin(_6aa,_6ab);return _6aa;});},"dijit/layout/StackController":function(){define(["dojo/_base/array","dojo/_base/declare","dojo/dom-class","dojo/dom-construct","dojo/keys","dojo/_base/lang","dojo/on","dojo/topic","../focus","../registry","../_Widget","../_TemplatedMixin","../_Container","../form/ToggleButton","dojo/touch"],function(_6ac,_6ad,_6ae,_6af,keys,lang,on,_6b0,_6b1,_6b2,_6b3,_6b4,_6b5,_6b6){var _6b7=_6ad("dijit.layout._StackButton",_6b6,{tabIndex:"-1",closeButton:false,_aria_attr:"aria-selected",buildRendering:function(evt){this.inherited(arguments);(this.focusNode||this.domNode).setAttribute("role","tab");}});var _6b8=_6ad("dijit.layout.StackController",[_6b3,_6b4,_6b5],{baseClass:"dijitStackController",templateString:"",containerId:"",buttonWidget:_6b7,buttonWidgetCloseClass:"dijitStackCloseButton",pane2button:function(id){return _6b2.byId(this.id+"_"+id);},postCreate:function(){this.inherited(arguments);this.own(_6b0.subscribe(this.containerId+"-startup",lang.hitch(this,"onStartup")),_6b0.subscribe(this.containerId+"-addChild",lang.hitch(this,"onAddChild")),_6b0.subscribe(this.containerId+"-removeChild",lang.hitch(this,"onRemoveChild")),_6b0.subscribe(this.containerId+"-selectChild",lang.hitch(this,"onSelectChild")),_6b0.subscribe(this.containerId+"-containerKeyDown",lang.hitch(this,"onContainerKeyDown")));this.containerNode.dojoClick=true;this.own(on(this.containerNode,"click",lang.hitch(this,function(evt){var _6b9=_6b2.getEnclosingWidget(evt.target);if(_6b9!=this.containerNode&&!_6b9.disabled&&_6b9.page){for(var _6ba=evt.target;_6ba!==this.containerNode;_6ba=_6ba.parentNode){if(_6ae.contains(_6ba,this.buttonWidgetCloseClass)){this.onCloseButtonClick(_6b9.page);break;}else{if(_6ba==_6b9.domNode){this.onButtonClick(_6b9.page);break;}}}}})));},onStartup:function(info){this.textDir=info.textDir;_6ac.forEach(info.children,this.onAddChild,this);if(info.selected){this.onSelectChild(info.selected);}var _6bb=_6b2.byId(this.containerId).containerNode,_6bc=lang.hitch(this,"pane2button"),_6bd={"title":"label","showtitle":"showLabel","iconclass":"iconClass","closable":"closeButton","tooltip":"title","disabled":"disabled","textdir":"textdir"},_6be=function(attr,_6bf){return on(_6bb,"attrmodified-"+attr,function(evt){var _6c0=_6bc(evt.detail&&evt.detail.widget&&evt.detail.widget.id);if(_6c0){_6c0.set(_6bf,evt.detail.newValue);}});};for(var attr in _6bd){this.own(_6be(attr,_6bd[attr]));}},destroy:function(_6c1){this.destroyDescendants(_6c1);this.inherited(arguments);},onAddChild:function(page,_6c2){var Cls=lang.isString(this.buttonWidget)?lang.getObject(this.buttonWidget):this.buttonWidget;var _6c3=new Cls({id:this.id+"_"+page.id,name:this.id+"_"+page.id,label:page.title,disabled:page.disabled,ownerDocument:this.ownerDocument,dir:page.dir,lang:page.lang,textDir:page.textDir||this.textDir,showLabel:page.showTitle,iconClass:page.iconClass,closeButton:page.closable,title:page.tooltip,page:page});this.addChild(_6c3,_6c2);page.controlButton=_6c3;if(!this._currentChild){this.onSelectChild(page);}var _6c4=page._wrapper.getAttribute("aria-labelledby")?page._wrapper.getAttribute("aria-labelledby")+" "+_6c3.id:_6c3.id;page._wrapper.removeAttribute("aria-label");page._wrapper.setAttribute("aria-labelledby",_6c4);},onRemoveChild:function(page){if(this._currentChild===page){this._currentChild=null;}var _6c5=this.pane2button(page.id);if(_6c5){this.removeChild(_6c5);_6c5.destroy();}delete page.controlButton;},onSelectChild:function(page){if(!page){return;}if(this._currentChild){var _6c6=this.pane2button(this._currentChild.id);_6c6.set("checked",false);_6c6.focusNode.setAttribute("tabIndex","-1");}var _6c7=this.pane2button(page.id);_6c7.set("checked",true);this._currentChild=page;_6c7.focusNode.setAttribute("tabIndex","0");var _6c8=_6b2.byId(this.containerId);},onButtonClick:function(page){var _6c9=this.pane2button(page.id);_6b1.focus(_6c9.focusNode);if(this._currentChild&&this._currentChild.id===page.id){_6c9.set("checked",true);}var _6ca=_6b2.byId(this.containerId);_6ca.selectChild(page);},onCloseButtonClick:function(page){var _6cb=_6b2.byId(this.containerId);_6cb.closeChild(page);if(this._currentChild){var b=this.pane2button(this._currentChild.id);if(b){_6b1.focus(b.focusNode||b.domNode);}}},adjacent:function(_6cc){if(!this.isLeftToRight()&&(!this.tabPosition||/top|bottom/.test(this.tabPosition))){_6cc=!_6cc;}var _6cd=this.getChildren();var idx=_6ac.indexOf(_6cd,this.pane2button(this._currentChild.id)),_6ce=_6cd[idx];var _6cf;do{idx=(idx+(_6cc?1:_6cd.length-1))%_6cd.length;_6cf=_6cd[idx];}while(_6cf.disabled&&_6cf!=_6ce);return _6cf;},onkeydown:function(e,_6d0){if(this.disabled||e.altKey){return;}var _6d1=null;if(e.ctrlKey||!e._djpage){switch(e.keyCode){case keys.LEFT_ARROW:case keys.UP_ARROW:if(!e._djpage){_6d1=false;}break;case keys.PAGE_UP:if(e.ctrlKey){_6d1=false;}break;case keys.RIGHT_ARROW:case keys.DOWN_ARROW:if(!e._djpage){_6d1=true;}break;case keys.PAGE_DOWN:if(e.ctrlKey){_6d1=true;}break;case keys.HOME:var _6d2=this.getChildren();for(var idx=0;idx<_6d2.length;idx++){var _6d3=_6d2[idx];if(!_6d3.disabled){this.onButtonClick(_6d3.page);break;}}e.stopPropagation();e.preventDefault();break;case keys.END:var _6d2=this.getChildren();for(var idx=_6d2.length-1;idx>=0;idx--){var _6d3=_6d2[idx];if(!_6d3.disabled){this.onButtonClick(_6d3.page);break;}}e.stopPropagation();e.preventDefault();break;case keys.DELETE:case "W".charCodeAt(0):if(this._currentChild.closable&&(e.keyCode==keys.DELETE||e.ctrlKey)){this.onCloseButtonClick(this._currentChild);e.stopPropagation();e.preventDefault();}break;case keys.TAB:if(e.ctrlKey){this.onButtonClick(this.adjacent(!e.shiftKey).page);e.stopPropagation();e.preventDefault();}break;}if(_6d1!==null){this.onButtonClick(this.adjacent(_6d1).page);e.stopPropagation();e.preventDefault();}}},onContainerKeyDown:function(info){info.e._djpage=info.page;this.onkeydown(info.e);}});_6b8.StackButton=_6b7;return _6b8;});},"dojo/dnd/Mover":function(){define(["../_base/array","../_base/declare","../_base/lang","../sniff","../_base/window","../dom","../dom-geometry","../dom-style","../Evented","../on","../touch","./common","./autoscroll"],function(_6d4,_6d5,lang,has,win,dom,_6d6,_6d7,_6d8,on,_6d9,dnd,_6da){return _6d5("dojo.dnd.Mover",[_6d8],{constructor:function(node,e,host){this.node=dom.byId(node);this.marginBox={l:e.pageX,t:e.pageY};this.mouseButton=e.button;var h=(this.host=host),d=node.ownerDocument;function _6db(e){e.preventDefault();e.stopPropagation();};this.events=[on(d,_6d9.move,lang.hitch(this,"onFirstMove")),on(d,_6d9.move,lang.hitch(this,"onMouseMove")),on(d,_6d9.release,lang.hitch(this,"onMouseUp")),on(d,"dragstart",_6db),on(d.body,"selectstart",_6db)];_6da.autoScrollStart(d);if(h&&h.onMoveStart){h.onMoveStart(this);}},onMouseMove:function(e){_6da.autoScroll(e);var m=this.marginBox;this.host.onMove(this,{l:m.l+e.pageX,t:m.t+e.pageY},e);e.preventDefault();e.stopPropagation();},onMouseUp:function(e){if(has("webkit")&&has("mac")&&this.mouseButton==2?e.button==0:this.mouseButton==e.button){this.destroy();}e.preventDefault();e.stopPropagation();},onFirstMove:function(e){var s=this.node.style,l,t,h=this.host;switch(s.position){case "relative":case "absolute":l=Math.round(parseFloat(s.left))||0;t=Math.round(parseFloat(s.top))||0;break;default:s.position="absolute";var m=_6d6.getMarginBox(this.node);var b=win.doc.body;var bs=_6d7.getComputedStyle(b);var bm=_6d6.getMarginBox(b,bs);var bc=_6d6.getContentBox(b,bs);l=m.l-(bc.l-bm.l);t=m.t-(bc.t-bm.t);break;}this.marginBox.l=l-this.marginBox.l;this.marginBox.t=t-this.marginBox.t;if(h&&h.onFirstMove){h.onFirstMove(this,e);}this.events.shift().remove();},destroy:function(){_6d4.forEach(this.events,function(_6dc){_6dc.remove();});var h=this.host;if(h&&h.onMoveStop){h.onMoveStop(this);}this.events=this.node=this.host=null;}});});},"dojo/request/default":function(){define(["exports","require","../has"],function(_6dd,_6de,has){var _6df=has("config-requestProvider"),_6e0;if(1||has("host-webworker")){_6e0="./xhr";}else{if(0){_6e0="./node";}}if(!_6df){_6df=_6e0;}_6dd.getPlatformDefaultId=function(){return _6e0;};_6dd.load=function(id,_6e1,_6e2,_6e3){_6de([id=="platform"?_6e0:_6df],function(_6e4){_6e2(_6e4);});};});},"dijit/layout/TabContainer":function(){define(["dojo/_base/lang","dojo/_base/declare","./_TabContainerBase","./TabController","./ScrollingTabController"],function(lang,_6e5,_6e6,_6e7,_6e8){return _6e5("dijit.layout.TabContainer",_6e6,{useMenu:true,useSlider:true,controllerWidget:"",_makeController:function(_6e9){var cls=this.baseClass+"-tabs"+(this.doLayout?"":" dijitTabNoLayout"),_6e7=typeof this.controllerWidget=="string"?lang.getObject(this.controllerWidget):this.controllerWidget;return new _6e7({id:this.id+"_tablist",ownerDocument:this.ownerDocument,dir:this.dir,lang:this.lang,textDir:this.textDir,tabPosition:this.tabPosition,doLayout:this.doLayout,containerId:this.id,"class":cls,nested:this.nested,useMenu:this.useMenu,useSlider:this.useSlider,tabStripClass:this.tabStrip?this.baseClass+(this.tabStrip?"":"No")+"Strip":null},_6e9);},postMixInProperties:function(){this.inherited(arguments);if(!this.controllerWidget){this.controllerWidget=(this.tabPosition=="top"||this.tabPosition=="bottom")&&!this.nested?_6e8:_6e7;}}});});},"dijit/BackgroundIframe":function(){define(["require","./main","dojo/_base/config","dojo/dom-construct","dojo/dom-style","dojo/_base/lang","dojo/on","dojo/sniff"],function(_6ea,_6eb,_6ec,_6ed,_6ee,lang,on,has){has.add("config-bgIframe",(has("ie")||has("trident"))&&!/IEMobile\/10\.0/.test(navigator.userAgent));var _6ef=new function(){var _6f0=[];this.pop=function(){var _6f1;if(_6f0.length){_6f1=_6f0.pop();_6f1.style.display="";}else{if(has("ie")<9){var burl=_6ec["dojoBlankHtmlUrl"]||_6ea.toUrl("dojo/resources/blank.html")||"javascript:\"\"";var html="