diff options
Diffstat (limited to 'classes')
-rwxr-xr-x | classes/api.php | 2 | ||||
-rwxr-xr-x | classes/article.php | 34 | ||||
-rw-r--r-- | classes/config.php | 27 | ||||
-rw-r--r-- | classes/counters.php | 11 | ||||
-rw-r--r-- | classes/digest.php | 3 | ||||
-rw-r--r-- | classes/diskcache.php | 58 | ||||
-rwxr-xr-x | classes/feeds.php | 82 | ||||
-rwxr-xr-x | classes/handler/public.php | 6 | ||||
-rw-r--r-- | classes/icatchall.php | 4 | ||||
-rwxr-xr-x | classes/pluginhost.php | 39 | ||||
-rwxr-xr-x | classes/pref/feeds.php | 4 | ||||
-rwxr-xr-x | classes/rpc.php | 3 | ||||
-rwxr-xr-x | classes/rssutils.php | 23 | ||||
-rw-r--r-- | classes/sanitizer.php | 3 | ||||
-rw-r--r-- | classes/tracer.php | 69 | ||||
-rw-r--r-- | classes/urlhelper.php | 51 |
16 files changed, 336 insertions, 83 deletions
diff --git a/classes/api.php b/classes/api.php index b482a70eb..255de52d4 100755 --- a/classes/api.php +++ b/classes/api.php @@ -99,7 +99,7 @@ class API extends Handler { } function isLoggedIn(): bool { - return $this->_wrap(self::STATUS_OK, array("status" => $_SESSION["uid"] != '')); + return $this->_wrap(self::STATUS_OK, array("status" => (bool)($_SESSION["uid"] ?? ''))); } function getUnread(): bool { diff --git a/classes/article.php b/classes/article.php index 609ddeebe..63469ccd2 100755 --- a/classes/article.php +++ b/classes/article.php @@ -239,8 +239,7 @@ class Article extends Handler_Protected { print json_encode(["id" => (int)$id, "tags" => $this->_get_tags($id)]); } - - /*function completeTags() { + function completeTags(): void { $search = clean($_REQUEST["search"]); $sth = $this->pdo->prepare("SELECT DISTINCT tag_name FROM ttrss_tags @@ -250,12 +249,14 @@ class Article extends Handler_Protected { $sth->execute([$_SESSION['uid'], "$search%"]); - print "<ul>"; + $results = []; + while ($line = $sth->fetch()) { - print "<li>" . $line["tag_name"] . "</li>"; + array_push($results, $line["tag_name"]); } - print "</ul>"; - }*/ + + print json_encode($results); + } function assigntolabel(): void { $this->_label_ops(true); @@ -297,6 +298,8 @@ class Article extends Handler_Protected { * @return array{'formatted': string, 'entries': array<int, array<string, mixed>>} */ static function _format_enclosures(int $id, bool $always_display_enclosures, string $article_content, bool $hide_images = false): array { + $scope = Tracer::start(__METHOD__); + $enclosures = self::_get_enclosures($id); $enclosures_formatted = ""; @@ -323,6 +326,7 @@ class Article extends Handler_Protected { $enclosures_formatted, $enclosures, $id, $always_display_enclosures, $article_content, $hide_images); if (!empty($enclosures_formatted)) { + $scope->close(); return [ 'formatted' => $enclosures_formatted, 'entries' => [] @@ -366,6 +370,7 @@ class Article extends Handler_Protected { } } + $scope->close(); return $rv; } @@ -373,6 +378,7 @@ class Article extends Handler_Protected { * @return array<int, string> */ static function _get_tags(int $id, int $owner_uid = 0, ?string $tag_cache = null): array { + $scope = Tracer::start(__METHOD__); $a_id = $id; @@ -421,6 +427,7 @@ class Article extends Handler_Protected { $sth->execute([$tags_str, $id, $owner_uid]); } + $scope->close(); return $tags; } @@ -515,6 +522,8 @@ class Article extends Handler_Protected { * @return array<int, array<int, int|string>> */ static function _get_labels(int $id, ?int $owner_uid = null): array { + $scope = Tracer::start(__METHOD__, []); + $rv = array(); if (!$owner_uid) $owner_uid = $_SESSION["uid"]; @@ -560,6 +569,8 @@ class Article extends Handler_Protected { else Labels::update_cache($owner_uid, $id, array("no-labels" => 1)); + $scope->close(); + return $rv; } @@ -570,6 +581,7 @@ class Article extends Handler_Protected { * @return array<int, Article::ARTICLE_KIND_*|string> */ static function _get_image(array $enclosures, string $content, string $site_url, array $headline) { + $scope = Tracer::start(__METHOD__); $article_image = ""; $article_stream = ""; @@ -648,6 +660,8 @@ class Article extends Handler_Protected { if ($article_stream && $cache->exists(sha1($article_stream))) $article_stream = $cache->get_url(sha1($article_stream)); + $scope->close(); + return [$article_image, $article_stream, $article_kind]; } @@ -661,6 +675,8 @@ class Article extends Handler_Protected { if (count($article_ids) == 0) return []; + $scope = Tracer::start(__METHOD__); + $entries = ORM::for_table('ttrss_entries') ->table_alias('e') ->join('ttrss_user_entries', ['ref_id', '=', 'id'], 'ue') @@ -680,6 +696,8 @@ class Article extends Handler_Protected { } } + $scope->close(); + return array_unique($rv); } @@ -691,6 +709,8 @@ class Article extends Handler_Protected { if (count($article_ids) == 0) return []; + $scope = Tracer::start(__METHOD__); + $entries = ORM::for_table('ttrss_entries') ->table_alias('e') ->join('ttrss_user_entries', ['ref_id', '=', 'id'], 'ue') @@ -703,6 +723,8 @@ class Article extends Handler_Protected { array_push($rv, $entry->feed_id); } + $scope->close(); + return array_unique($rv); } } diff --git a/classes/config.php b/classes/config.php index a865266ca..afdb7b8ef 100644 --- a/classes/config.php +++ b/classes/config.php @@ -192,6 +192,12 @@ class Config { /** delay updates for this feed if received HTTP 429 (Too Many Requests) for this amount of seconds (base value, actual delay is base...base*2) */ const HTTP_429_THROTTLE_INTERVAL = "HTTP_429_THROTTLE_INTERVAL"; + /** host running Jaeger collector to receive traces (disabled if empty) */ + const JAEGER_REPORTING_HOST = "JAEGER_REPORTING_HOST"; + + /** Jaeger service name */ + const JAEGER_SERVICE_NAME = "JAEGER_SERVICE_NAME"; + /** default values for all global configuration options */ private const _DEFAULTS = [ Config::DB_TYPE => [ "pgsql", Config::T_STRING ], @@ -249,6 +255,8 @@ class Config { Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://tt-rss.org/)', Config::T_STRING ], Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ], + Config::JAEGER_REPORTING_HOST => [ "", Config::T_STRING ], + Config::JAEGER_SERVICE_NAME => [ "tt-rss", Config::T_STRING ], ]; /** @var Config|null */ @@ -303,7 +311,11 @@ class Config { static function get_version_html() : string { $version = self::get_version(false); - return sprintf("<span title=\"%s\">%s</span>", date("Y-m-d H:i:s", ($version['timestamp'] ?? 0)), $version['version']); + return sprintf("<span title=\"%s\n%s\n%s\">%s</span>", + date("Y-m-d H:i:s", ($version['timestamp'] ?? 0)), + $version['commit'], + $version['branch'] ?? '', + $version['version']); } /** @@ -315,8 +327,16 @@ class Config { if (empty($this->version)) { $this->version["status"] = -1; - if (PHP_OS === "Darwin") { - $ttrss_version["version"] = "UNKNOWN (Unsupported, Darwin)"; + if (getenv("CI_COMMIT_SHORT_SHA") && getenv("CI_COMMIT_TIMESTAMP")) { + + $this->version["branch"] = getenv("CI_COMMIT_BRANCH"); + $this->version["timestamp"] = strtotime(getenv("CI_COMMIT_TIMESTAMP")); + $this->version["version"] = sprintf("%s-%s", date("y.m", $this->version["timestamp"]), getenv("CI_COMMIT_SHORT_SHA")); + $this->version["commit"] = getenv("CI_COMMIT_SHORT_SHA"); + $this->version["status"] = 0; + + } else if (PHP_OS === "Darwin") { + $this->version["version"] = "UNKNOWN (Unsupported, Darwin)"; } else if (file_exists("$root_dir/version_static.txt")) { $this->version["version"] = trim(file_get_contents("$root_dir/version_static.txt")) . " (Unsupported)"; } else if (ini_get("open_basedir")) { @@ -352,6 +372,7 @@ class Config { $rv = [ "status" => -1, "version" => "", + "branch" => "", "commit" => "", "timestamp" => 0, ]; diff --git a/classes/counters.php b/classes/counters.php index 41bb1b9ae..fcf28f938 100644 --- a/classes/counters.php +++ b/classes/counters.php @@ -145,6 +145,7 @@ class Counters { * @return array<int, array<string, int|string>> */ private static function get_feeds(array $feed_ids = null): array { + $scope = Tracer::start(__METHOD__); $ret = []; @@ -211,6 +212,8 @@ class Counters { } + $scope->close(); + return $ret; } @@ -218,6 +221,8 @@ class Counters { * @return array<int, array<string, int|string>> */ private static function get_global(): array { + $scope = Tracer::start(__METHOD__); + $ret = [ [ "id" => "global-unread", @@ -234,6 +239,8 @@ class Counters { "counter" => $subcribed_feeds ]); + $scope->close(); + return $ret; } @@ -241,6 +248,7 @@ class Counters { * @return array<int, array<string, int|string>> */ private static function get_virt(): array { + $scope = Tracer::start(__METHOD__); $ret = []; @@ -287,6 +295,7 @@ class Counters { } } + $scope->close(); return $ret; } @@ -295,6 +304,7 @@ class Counters { * @return array<int, array<string, int|string>> */ static function get_labels(array $label_ids = null): array { + $scope = Tracer::start(__METHOD__); $ret = []; @@ -346,6 +356,7 @@ class Counters { array_push($ret, $cv); } + $scope->close(); return $ret; } } diff --git a/classes/digest.php b/classes/digest.php index b19c37c5f..d77a83b8c 100644 --- a/classes/digest.php +++ b/classes/digest.php @@ -2,6 +2,7 @@ class Digest { static function send_headlines_digests(): void { + $scope = Tracer::start(__METHOD__); $user_limit = 15; // amount of users to process (e.g. emails to send out) $limit = 1000; // maximum amount of headlines to include @@ -75,6 +76,8 @@ class Digest } } } + + $scope->close(); Debug::log("All done."); } diff --git a/classes/diskcache.php b/classes/diskcache.php index 2a3f8c8d7..6a9038289 100644 --- a/classes/diskcache.php +++ b/classes/diskcache.php @@ -221,7 +221,11 @@ class DiskCache implements Cache_Adapter { } public function remove(string $filename): bool { - return $this->adapter->remove($filename); + $scope = Tracer::start(__METHOD__, ['filename' => $filename]); + $rc = $this->adapter->remove($filename); + $scope->close(); + + return $rc; } public function set_dir(string $dir) : void { @@ -244,14 +248,22 @@ class DiskCache implements Cache_Adapter { } public function exists(string $filename): bool { - return $this->adapter->exists(basename($filename)); + $scope = Tracer::start(__METHOD__, ['filename' => $filename]); + $rc = $this->adapter->exists(basename($filename)); + $scope->close(); + + return $rc; } /** * @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) { - return $this->adapter->get_size(basename($filename)); + $scope = Tracer::start(__METHOD__, ['filename' => $filename]); + $rc = $this->adapter->get_size(basename($filename)); + $scope->close(); + + return $rc; } /** @@ -260,7 +272,11 @@ class DiskCache implements Cache_Adapter { * @return int|false Bytes written or false if an error occurred. */ public function put(string $filename, $data) { - return $this->adapter->put(basename($filename), $data); + $scope = Tracer::start(__METHOD__); + $rc = $this->adapter->put(basename($filename), $data); + $scope->close(); + + return $rc; } /** @deprecated we can't assume cached files are local, and other storages @@ -304,11 +320,16 @@ class DiskCache implements Cache_Adapter { } public function send(string $filename) { + $scope = Tracer::start(__METHOD__, ['filename' => $filename]); + $filename = basename($filename); if (!$this->exists($filename)) { header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); echo "File not found."; + + $scope->getSpan()->setTag('error', '404 not found'); + $scope->close(); return false; } @@ -317,6 +338,9 @@ class DiskCache implements Cache_Adapter { if (($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '') == $gmt_modified || ($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') == $file_mtime) { header('HTTP/1.1 304 Not Modified'); + + $scope->getSpan()->setTag('error', '304 not modified'); + $scope->close(); return false; } @@ -334,6 +358,9 @@ class DiskCache implements Cache_Adapter { header("Content-type: text/plain"); print "Stored file has disallowed content type ($mimetype)"; + + $scope->getSpan()->setTag('error', '400 disallowed content type'); + $scope->close(); return false; } @@ -355,7 +382,13 @@ class DiskCache implements Cache_Adapter { header_remove("Pragma"); - return $this->adapter->send($filename); + $scope->getSpan()->setTag('mimetype', $mimetype); + + $rc = $this->adapter->send($filename); + + $scope->close(); + + return $rc; } public function get_full_path(string $filename): string { @@ -384,8 +417,14 @@ class DiskCache implements Cache_Adapter { // 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(string $str): string { + $scope = Tracer::start(__METHOD__); + $res = trim($str); - if (!$res) return ''; + + if (!$res) { + $scope->close(); + return ''; + } $doc = new DOMDocument(); if (@$doc->loadHTML('<?xml encoding="UTF-8">' . $res)) { @@ -397,6 +436,8 @@ class DiskCache implements Cache_Adapter { $need_saving = false; foreach ($entries as $entry) { + $e_scope = Tracer::start('entry', ['tagName' => $entry->tagName]); + foreach (array('src', 'poster') as $attr) { if ($entry->hasAttribute($attr)) { $url = $entry->getAttribute($attr); @@ -428,6 +469,8 @@ class DiskCache implements Cache_Adapter { $entry->setAttribute("srcset", RSSUtils::encode_srcset($matches)); } + + $e_scope->close(); } if ($need_saving) { @@ -437,6 +480,9 @@ class DiskCache implements Cache_Adapter { $res = $doc->saveHTML(); } } + + $scope->close(); + return $res; } } diff --git a/classes/feeds.php b/classes/feeds.php index 002a9eae7..68619302f 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -49,12 +49,6 @@ class Feeds extends Handler_Protected { const NEVER_GROUP_FEEDS = [ Feeds::FEED_RECENTLY_READ, Feeds::FEED_ARCHIVED ]; const NEVER_GROUP_BY_DATE = [ Feeds::FEED_PUBLISHED, Feeds::FEED_STARRED, Feeds::FEED_FRESH ]; - /** @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"); @@ -71,7 +65,7 @@ class Feeds extends Handler_Protected { $disable_cache = false; - $this->_mark_timestamp("init"); + $scope = Tracer::start(__METHOD__, [], func_get_args()); $reply = []; $rgba_cache = []; @@ -157,8 +151,6 @@ class Feeds extends Handler_Protected { $qfh_ret = $this->_get_headlines($params); } - $this->_mark_timestamp("db query"); - $vfeed_group_enabled = get_pref(Prefs::VFEED_GROUP_BY_FEED) && !(in_array($feed, self::NEVER_GROUP_FEEDS) && !$cat_view); @@ -176,6 +168,8 @@ class Feeds extends Handler_Protected { $reply['search_query'] = [$search, $search_language]; $reply['vfeed_group_enabled'] = $vfeed_group_enabled; + $p_scope = Tracer::start('plugin_menu_items'); + $plugin_menu_items = ""; PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2, function ($result) use (&$plugin_menu_items) { @@ -208,13 +202,15 @@ class Feeds extends Handler_Protected { }, $feed, $cat_view, $qfh_ret); - $this->_mark_timestamp("object header"); + $p_scope->close(); + + $a_scope = Tracer::start('articles'); $headlines_count = 0; if ($result instanceof PDOStatement) { while ($line = $result->fetch(PDO::FETCH_ASSOC)) { - $this->_mark_timestamp("article start: " . $line["id"] . " " . $line["title"]); + $aa_scope = Tracer::start('article', ['id' => $line['id']]); ++$headlines_count; @@ -232,8 +228,6 @@ class Feeds extends Handler_Protected { $line, $max_excerpt_length); } - $this->_mark_timestamp(" hook_query_headlines"); - $id = $line["id"]; // frontend doesn't expect pdo returning booleans as strings on mysql @@ -281,8 +275,6 @@ class Feeds extends Handler_Protected { array_push($topmost_article_ids, $id); } - $this->_mark_timestamp(" labels"); - $line["feed_title"] = $line["feed_title"] ?? ""; $button_doc = new DOMDocument(); @@ -312,6 +304,7 @@ class Feeds extends Handler_Protected { $line); $line["buttons"] = ""; + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_BUTTON, function ($result, $plugin) use (&$line, &$button_doc) { if ($result && $button_doc->loadXML($result)) { @@ -335,13 +328,9 @@ class Feeds extends Handler_Protected { }, $line); - $this->_mark_timestamp(" pre-sanitize"); - $line["content"] = Sanitizer::sanitize($line["content"], $line['hide_images'], null, $line["site_url"], $highlight_words, $line["id"]); - $this->_mark_timestamp(" sanitize"); - if (!get_pref(Prefs::CDM_EXPANDED)) { $line["cdm_excerpt"] = "<span class='collapse'> <i class='material-icons' onclick='return Article.cdmUnsetActive(event)' @@ -352,8 +341,6 @@ class Feeds extends Handler_Protected { } } - $this->_mark_timestamp(" pre-enclosures"); - if ($line["num_enclosures"] > 0) { $line["enclosures"] = Article::_format_enclosures($id, sql_bool_to_bool($line["always_display_enclosures"]), @@ -363,16 +350,12 @@ class Feeds extends Handler_Protected { $line["enclosures"] = [ 'formatted' => '', 'entries' => [] ]; } - $this->_mark_timestamp(" enclosures"); - $line["updated_long"] = TimeHelper::make_local_datetime($line["updated"],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)); - $this->_mark_timestamp(" local-datetime"); - if ($line["tag_cache"]) $tags = explode(",", $line["tag_cache"]); else @@ -382,14 +365,12 @@ class Feeds extends Handler_Protected { //$line["tags"] = Article::_get_tags($line["id"], false, $line["tag_cache"]); - $this->_mark_timestamp(" tags"); - $line['has_icon'] = self::_has_icon($feed_id); //setting feed headline background color, needs to change text color based on dark/light $fav_color = $line['favicon_avg_color'] ?? false; - $this->_mark_timestamp(" pre-color"); + $c_scope = Tracer::start('colors'); require_once "colors.php"; @@ -405,22 +386,16 @@ class Feeds extends Handler_Protected { $line['feed_bg_color'] = 'rgba(' . implode(",", $rgba_cache[$feed_id]) . ',0.3)'; } - $this->_mark_timestamp(" color"); - $this->_mark_timestamp(" pre-hook_render_cdm"); + $c_scope->close(); PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_CDM, function ($result, $plugin) use (&$line) { $line = $result; - $this->_mark_timestamp(" hook: " . get_class($plugin)); }, $line); - $this->_mark_timestamp(" hook_render_cdm"); - $line['content'] = DiskCache::rewrite_urls($line['content']); - $this->_mark_timestamp(" disk_cache_rewrite"); - /* we don't need those */ foreach (["date_entered", "guid", "last_published", "last_marked", "tag_cache", "favicon_avg_color", @@ -429,11 +404,11 @@ class Feeds extends Handler_Protected { array_push($reply['content'], $line); - $this->_mark_timestamp("article end"); + $aa_scope->close(); } } - $this->_mark_timestamp("end of articles"); + $a_scope->close(); if (!$headlines_count) { @@ -494,7 +469,7 @@ class Feeds extends Handler_Protected { } } - $this->_mark_timestamp("end"); + $scope->close(); return array($topmost_article_ids, $headlines_count, $feed, $disable_cache, $reply); } @@ -987,6 +962,7 @@ class Feeds extends Handler_Protected { * @throws PDOException */ static function _get_counters($feed, bool $is_cat = false, bool $unread_only = false, ?int $owner_uid = null): int { + $scope = Tracer::start(__METHOD__, [], func_get_args()); $n_feed = (int) $feed; $need_entries = false; @@ -1010,11 +986,14 @@ class Feeds extends Handler_Protected { $handler = PluginHost::getInstance()->get_feed_handler($feed_id); if (implements_interface($handler, 'IVirtualFeed')) { /** @var IVirtualFeed $handler */ + $scope->close(); return $handler->get_unread($feed_id); } else { + $scope->close(); return 0; } } else if ($n_feed == Feeds::FEED_RECENTLY_READ) { + $scope->close(); return 0; // tags } else if ($feed != "0" && $n_feed == 0) { @@ -1028,6 +1007,7 @@ class Feeds extends Handler_Protected { $row = $sth->fetch(); // Handle 'SUM()' returning null if there are no results + $scope->close(); return $row["count"] ?? 0; } else if ($n_feed == Feeds::FEED_STARRED) { @@ -1061,6 +1041,7 @@ class Feeds extends Handler_Protected { $label_id = Labels::feed_to_label_id($feed); + $scope->close(); return self::_get_label_unread($label_id, $owner_uid); } @@ -1080,6 +1061,7 @@ class Feeds extends Handler_Protected { $sth->execute([$owner_uid]); $row = $sth->fetch(); + $scope->close(); return $row["unread"]; } else { @@ -1092,6 +1074,7 @@ class Feeds extends Handler_Protected { $sth->execute([$feed, $owner_uid]); $row = $sth->fetch(); + $scope->close(); return $row["unread"]; } } @@ -1489,6 +1472,8 @@ class Feeds extends Handler_Protected { */ static function _get_headlines($params): array { + $scope = Tracer::start(__METHOD__, [], func_get_args()); + $pdo = Db::pdo(); // WARNING: due to highly dynamic nature of this query its going to quote parameters @@ -1981,8 +1966,9 @@ class Feeds extends Handler_Protected { $res = $pdo->query($query); } - return array($res, $feed_title, $feed_site_url, $last_error, $last_updated, $search_words, $first_id, $vfeed_query_part != "", $query_error_override); + $scope->close(); + return array($res, $feed_title, $feed_site_url, $last_error, $last_updated, $search_words, $first_id, $vfeed_query_part != "", $query_error_override); } /** @@ -2495,23 +2481,5 @@ class Feeds extends Handler_Protected { return [$query, $skip_first_id]; } - private function _mark_timestamp(string $label): void { - - if (empty($_REQUEST['timestamps'])) - return; - - if (!$this->viewfeed_timestamp) $this->viewfeed_timestamp = hrtime(true); - if (!$this->viewfeed_timestamp_last) $this->viewfeed_timestamp_last = hrtime(true); - - $timestamp = hrtime(true); - - printf("[%4d ms, %4d abs] %s\n", - ($timestamp - $this->viewfeed_timestamp_last) / 1e6, - ($timestamp - $this->viewfeed_timestamp) / 1e6, - $label); - - $this->viewfeed_timestamp_last = $timestamp; - } - } diff --git a/classes/handler/public.php b/classes/handler/public.php index d776e27cd..d7a7010fe 100755 --- a/classes/handler/public.php +++ b/classes/handler/public.php @@ -416,10 +416,10 @@ class Handler_Public extends Handler { $_SESSION["login_error_msg"] ??= __("Incorrect username or password"); } - $return = clean($_REQUEST['return']); + $return = clean($_REQUEST['return'] ?? ''); - if ($_REQUEST['return'] && mb_strpos($return, Config::get_self_url()) === 0) { - header("Location: " . clean($_REQUEST['return'])); + if ($return && mb_strpos($return, Config::get_self_url()) === 0) { + header("Location: $return"); } else { header("Location: " . Config::get_self_url()); } diff --git a/classes/icatchall.php b/classes/icatchall.php new file mode 100644 index 000000000..29954d35a --- /dev/null +++ b/classes/icatchall.php @@ -0,0 +1,4 @@ +<?php +interface ICatchall { + function catchall(string $method): void; +} diff --git a/classes/pluginhost.php b/classes/pluginhost.php index ab26780c7..bdbecca13 100755 --- a/classes/pluginhost.php +++ b/classes/pluginhost.php @@ -339,10 +339,13 @@ class PluginHost { */ function chain_hooks_callback(string $hook, Closure $callback, &...$args): void { $method = strtolower((string)$hook); + $scope = Tracer::start(__METHOD__, ['hook' => $hook]); foreach ($this->get_hooks((string)$hook) as $plugin) { //Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE); + $p_scope = Tracer::start("$hook - " . get_class($plugin)); + try { if ($callback($plugin->$method(...$args), $plugin)) break; @@ -351,7 +354,11 @@ class PluginHost { } catch (Error $err) { user_error($err, E_USER_WARNING); } + + $p_scope->close(); } + + $scope->close(); } /** @@ -431,6 +438,8 @@ class PluginHost { * @param PluginHost::KIND_* $kind */ function load(string $classlist, int $kind, int $owner_uid = null, bool $skip_init = false): void { + $scope = Tracer::start(__METHOD__); + $plugins = explode(",", $classlist); $this->owner_uid = (int) $owner_uid; @@ -439,18 +448,21 @@ class PluginHost { $class = trim($class); $class_file = strtolower(basename(clean($class))); + $p_scope = Tracer::start("loading $class_file"); + // try system plugin directory first $file = dirname(__DIR__) . "/plugins/$class_file/init.php"; if (!file_exists($file)) { $file = dirname(__DIR__) . "/plugins.local/$class_file/init.php"; - if (!file_exists($file)) + if (!file_exists($file)) { + $p_scope->close(); continue; + } } if (!isset($this->plugins[$class])) { - // WIP hack // we can't catch incompatible method signatures via Throwable // this also enables global tt-rss safe mode in case there are more plugins like this @@ -464,6 +476,8 @@ class PluginHost { $_SESSION["safe_mode"] = 1; + $p_scope->getSpan()->setTag('error', 'plugin is blacklisted'); + $p_scope->close(); continue; } @@ -474,16 +488,21 @@ class PluginHost { } catch (Error $err) { user_error($err, E_USER_WARNING); + + $p_scope->getSpan()->setTag('error', $err); + $p_scope->close(); continue; } if (class_exists($class) && is_subclass_of($class, "Plugin")) { - $plugin = new $class($this); $plugin_api = $plugin->api_version(); if ($plugin_api < self::API_VERSION) { user_error("Plugin $class is not compatible with current API version (need: " . self::API_VERSION . ", got: $plugin_api)", E_USER_WARNING); + + $p_scope->getSpan()->setTag('error', 'plugin is not compatible with API version'); + $p_scope->close(); continue; } @@ -492,6 +511,8 @@ class PluginHost { _bind_textdomain_codeset($class, "UTF-8"); } + $i_scope = Tracer::start('init and register plugin'); + try { switch ($kind) { case $this::KIND_SYSTEM: @@ -516,11 +537,17 @@ class PluginHost { } catch (Error $err) { user_error($err, E_USER_WARNING); } + + $i_scope->close(); + } } + $p_scope->close(); } $this->load_data(); + + $scope->close(); } function is_system(Plugin $plugin): bool { @@ -613,6 +640,8 @@ class PluginHost { } private function load_data(): void { + $scope = Tracer::start(__METHOD__); + 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 = ?"); @@ -624,10 +653,13 @@ class PluginHost { $this->data_loaded = true; } + + $scope->close(); } private function save_data(string $plugin): void { if ($this->owner_uid) { + $scope = Tracer::start(__METHOD__); if (!$this->pdo_data) $this->pdo_data = Db::instance()->pdo_connect(); @@ -655,6 +687,7 @@ class PluginHost { } $this->pdo_data->commit(); + $scope->close(); } } diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php index ed6560fd3..fb7e01af8 100755 --- a/classes/pref/feeds.php +++ b/classes/pref/feeds.php @@ -1104,12 +1104,16 @@ class Pref_Feeds extends Handler_Protected { * @return array<string, mixed> */ private function feedlist_init_feed(int $feed_id, ?string $title = null, bool $unread = false, string $error = '', string $updated = ''): array { + $scope = Tracer::start(__METHOD__, []); + if (!$title) $title = Feeds::_get_title($feed_id, false); if ($unread === false) $unread = Feeds::_get_counters($feed_id, false, true); + $scope->close(); + return [ 'id' => 'FEED:' . $feed_id, 'name' => $title, diff --git a/classes/rpc.php b/classes/rpc.php index 204b002d5..afd3c0c79 100755 --- a/classes/rpc.php +++ b/classes/rpc.php @@ -106,6 +106,8 @@ class RPC extends Handler_Protected { } function getAllCounters(): void { + $scope = Tracer::start(__METHOD__); + @$seq = (int) $_REQUEST['seq']; $feed_id_count = (int) ($_REQUEST["feed_id_count"] ?? -1); @@ -132,6 +134,7 @@ class RPC extends Handler_Protected { 'seq' => $seq ]; + $scope->close(); print json_encode($reply); } diff --git a/classes/rssutils.php b/classes/rssutils.php index 385ab31e6..d1096cc85 100755 --- a/classes/rssutils.php +++ b/classes/rssutils.php @@ -68,6 +68,8 @@ class RSSUtils { * @param array<string, false|string> $options */ static function update_daemon_common(int $limit = 0, array $options = []): int { + $scope = Tracer::start(__METHOD__); + if (!$limit) $limit = Config::get(Config::DAEMON_FEED_LIMIT); if (Config::get_schema_version() != Config::SCHEMA_VERSION) { @@ -283,6 +285,8 @@ class RSSUtils { // Send feed digests by email if needed. Digest::send_headlines_digests(); + $scope->close(); + return $nf; } @@ -349,6 +353,7 @@ class RSSUtils { static function update_rss_feed(int $feed, bool $no_cache = false) : bool { + $scope = Tracer::start(__METHOD__, [], func_get_args()); Debug::log("start", Debug::LOG_VERBOSE); $pdo = Db::pdo(); @@ -383,16 +388,19 @@ class RSSUtils { if ($user) { if ($user->access_level == UserHelper::ACCESS_LEVEL_READONLY) { Debug::log("error: denied update for $feed: permission denied by owner access level"); + $scope->close(); return false; } } else { // this would indicate database corruption of some kind Debug::log("error: owner not found for feed: $feed"); + $scope->close(); return false; } } else { Debug::log("error: feeds table record not found for feed: $feed"); + $scope->close(); return false; } @@ -550,6 +558,7 @@ class RSSUtils { $feed_obj->save(); } + $scope->close(); return $error_message == ""; } @@ -675,7 +684,7 @@ class RSSUtils { ]); $feed_obj->save(); - + $scope->close(); return true; // no articles } @@ -684,6 +693,8 @@ class RSSUtils { $tstart = time(); foreach ($items as $item) { + $a_scope = Tracer::start('article'); + $pdo->beginTransaction(); Debug::log("=================================================================================================================================", @@ -1276,6 +1287,7 @@ class RSSUtils { Debug::log("article processed.", Debug::LOG_VERBOSE); $pdo->commit(); + $a_scope->close(); } Debug::log("=================================================================================================================================", @@ -1317,10 +1329,12 @@ class RSSUtils { unset($rss); Debug::log("update failed.", Debug::LOG_VERBOSE); + $scope->close(); return false; } Debug::log("update done.", Debug::LOG_VERBOSE); + $scope->close(); return true; } @@ -1485,6 +1499,8 @@ class RSSUtils { * @return array<int, array<string, string>> An array of filter action arrays with keys "type" and "param" */ static function get_article_filters(array $filters, string $title, string $content, string $link, string $author, array $tags, array &$matched_rules = null, array &$matched_filters = null): array { + $scope = Tracer::start(__METHOD__); + $matches = array(); foreach ($filters as $filter) { @@ -1526,6 +1542,9 @@ class RSSUtils { $match = @preg_match("/$reg_exp/iu", $author); break; case "tag": + if (count($tags) == 0) + array_push($tags, ''); // allow matching if there are no tags + foreach ($tags as $tag) { if (@preg_match("/$reg_exp/iu", $tag)) { $match = true; @@ -1565,6 +1584,8 @@ class RSSUtils { } } + $scope->close(); + return $matches; } diff --git a/classes/sanitizer.php b/classes/sanitizer.php index 8b4584a28..28874d16f 100644 --- a/classes/sanitizer.php +++ b/classes/sanitizer.php @@ -63,6 +63,7 @@ class Sanitizer { * @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) { + $scope = Tracer::start(__METHOD__); if (!$owner && isset($_SESSION["uid"])) $owner = $_SESSION["uid"]; @@ -223,6 +224,8 @@ class Sanitizer { $res = $doc->saveHTML(); + $scope->close(); + /* strip everything outside of <body>...</body> */ $res_frag = array(); diff --git a/classes/tracer.php b/classes/tracer.php new file mode 100644 index 000000000..5a23dfeba --- /dev/null +++ b/classes/tracer.php @@ -0,0 +1,69 @@ +<?php +use OpenTracing\GlobalTracer; +use OpenTracing\Scope; + +class Tracer { + /** @var Tracer $instance */ + private static $instance; + + public function __construct() { + $jaeger_host = Config::get(Config::JAEGER_REPORTING_HOST); + + if ($jaeger_host) { + $config = new \Jaeger\Config( + [ + 'sampler' => [ + 'type' => \Jaeger\SAMPLER_TYPE_CONST, + 'param' => true, + ], + 'logging' => true, + "local_agent" => [ + "reporting_host" => $jaeger_host, + "reporting_port" => 6832 + ], + 'dispatch_mode' => \Jaeger\Config::JAEGER_OVER_BINARY_UDP, + ], + Config::get(Config::JAEGER_SERVICE_NAME) + ); + + $config->initializeTracer(); + + register_shutdown_function(function() { + $tracer = GlobalTracer::get(); + $tracer->flush(); + }); + } + } + + /** + * @param string $name + * @param array<string>|array<string, array<string, mixed>> $tags + * @param array<string> $args + * @return Scope + */ + private function _start(string $name, array $tags = [], array $args = []): Scope { + $tracer = GlobalTracer::get(); + + $tags['args'] = json_encode($args); + + return $tracer->startActiveSpan($name, ['tags' => $tags]); + } + + /** + * @param string $name + * @param array<string>|array<string, array<string, mixed>> $tags + * @param array<string> $args + * @return Scope + */ + public static function start(string $name, array $tags = [], array $args = []) : Scope { + return self::get_instance()->_start($name, $tags, $args); + } + + public static function get_instance() : Tracer { + if (self::$instance == null) + self::$instance = new self(); + + return self::$instance; + } + +} diff --git a/classes/urlhelper.php b/classes/urlhelper.php index dc47f5ad8..6300e7346 100644 --- a/classes/urlhelper.php +++ b/classes/urlhelper.php @@ -185,10 +185,14 @@ class UrlHelper { * @return false|string */ static function resolve_redirects(string $url, int $timeout, int $nest = 0) { + $scope = Tracer::start(__METHOD__, ['url' => $url]); // too many redirects - if ($nest > 10) + if ($nest > 10) { + $scope->getSpan()->setTag('error', 'too many redirects'); + $scope->close(); return false; + } $context_options = array( 'http' => array( @@ -222,9 +226,12 @@ class UrlHelper { } } + $scope->close(); return $url; } + $scope->getSpan()->setTag('error', 'request failed'); + $scope->close(); // request failed? return false; } @@ -270,8 +277,10 @@ class UrlHelper { "useragent" => @func_get_arg(7) ); */ } - $url = $options["url"]; + + $scope = Tracer::start(__METHOD__, ['url' => $url]); + $type = isset($options["type"]) ? $options["type"] : false; $login = isset($options["login"]) ? $options["login"] : false; $pass = isset($options["pass"]) ? $options["pass"] : false; @@ -293,6 +302,9 @@ class UrlHelper { if (!$url) { self::$fetch_last_error = "Requested URL failed extended validation."; + + $scope->getSpan()->setTag('error', self::$fetch_last_error); + $scope->close(); return false; } @@ -301,6 +313,9 @@ class UrlHelper { if (!$ip_addr || strpos($ip_addr, "127.") === 0) { self::$fetch_last_error = "URL hostname failed to resolve or resolved to a loopback address ($ip_addr)"; + + $scope->getSpan()->setTag('error', self::$fetch_last_error); + $scope->close(); return false; } @@ -310,7 +325,12 @@ class UrlHelper { $ch = curl_init($url); - if (!$ch) return false; + if (!$ch) { + self::$fetch_last_error = "curl_init() failed"; + $scope->getSpan()->setTag('error', self::$fetch_last_error); + $scope->close(); + return false; + } $curl_http_headers = []; @@ -412,6 +432,8 @@ class UrlHelper { if (!self::validate(self::$fetch_effective_url, true)) { self::$fetch_last_error = "URL received after redirection failed extended validation."; + $scope->getSpan()->setTag('error', self::$fetch_last_error); + $scope->close(); return false; } @@ -420,6 +442,8 @@ class UrlHelper { if (!self::$fetch_effective_ip_addr || strpos(self::$fetch_effective_ip_addr, "127.") === 0) { self::$fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address (".self::$fetch_effective_ip_addr.")"; + $scope->getSpan()->setTag('error', self::$fetch_last_error); + $scope->close(); return false; } @@ -435,6 +459,9 @@ class UrlHelper { self::$fetch_last_error_content = $contents; curl_close($ch); + + $scope->getSpan()->setTag('error', self::$fetch_last_error); + $scope->close(); return false; } @@ -445,6 +472,9 @@ class UrlHelper { self::$fetch_last_error = curl_errno($ch) . " " . curl_error($ch); } curl_close($ch); + + $scope->getSpan()->setTag('error', self::$fetch_last_error); + $scope->close(); return false; } @@ -458,6 +488,8 @@ class UrlHelper { if ($tmp) $contents = $tmp; } + $scope->close(); + return $contents; } else { @@ -511,6 +543,8 @@ class UrlHelper { if (!self::validate(self::$fetch_effective_url, true)) { self::$fetch_last_error = "URL received after redirection failed extended validation."; + $scope->getSpan()->setTag('error', self::$fetch_last_error); + $scope->close(); return false; } @@ -519,6 +553,8 @@ class UrlHelper { if (!self::$fetch_effective_ip_addr || strpos(self::$fetch_effective_ip_addr, "127.") === 0) { self::$fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address (".self::$fetch_effective_ip_addr.")"; + $scope->getSpan()->setTag('error', self::$fetch_last_error); + $scope->close(); return false; } @@ -526,6 +562,9 @@ class UrlHelper { if ($data === false) { self::$fetch_last_error = "'file_get_contents' failed."; + + $scope->getSpan()->setTag('error', self::$fetch_last_error); + $scope->close(); return false; } @@ -561,6 +600,8 @@ class UrlHelper { self::$fetch_last_error_content = $data; + $scope->getSpan()->setTag('error', self::$fetch_last_error); + $scope->close(); return false; } @@ -573,9 +614,13 @@ class UrlHelper { if ($tmp) $data = $tmp; } + $scope->close(); return $data; } else { self::$fetch_last_error = 'Successful response, but no content was received.'; + + $scope->getSpan()->setTag('error', self::$fetch_last_error); + $scope->close(); return false; } } |