From 9ad4cbeecaed32e4106a7fef30bbe3d14195f78a Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Tue, 2 Mar 2021 15:16:38 +0300 Subject: [PATCH] wip separate handlers --- api/index.php | 6 +- classes/article.php | 169 +-- classes/feeds.php | 1777 ++++++++++------------- classes/{ => handler}/api.php | 2 +- classes/handler/article.php | 166 +++ classes/handler/feeds.php | 305 ++++ classes/{ => handler}/opml.php | 2 +- classes/{ => handler}/pluginhandler.php | 2 +- classes/handler/rpc.php | 786 ++++++++++ classes/rpc.php | 786 +--------- 10 files changed, 2005 insertions(+), 1996 deletions(-) rename classes/{ => handler}/api.php (99%) mode change 100755 => 100644 create mode 100644 classes/handler/article.php create mode 100644 classes/handler/feeds.php rename classes/{ => handler}/opml.php (99%) rename classes/{ => handler}/pluginhandler.php (94%) create mode 100644 classes/handler/rpc.php diff --git a/api/index.php b/api/index.php index 750a95721..3bcbc7f92 100644 --- a/api/index.php +++ b/api/index.php @@ -36,8 +36,8 @@ print json_encode([ "seq" => -1, - "status" => API::STATUS_ERR, - "content" => [ "error" => API::E_NOT_LOGGED_IN ] + "status" => Handler_API::STATUS_ERR, + "content" => [ "error" => Handler_API::E_NOT_LOGGED_IN ] ]); return; @@ -48,7 +48,7 @@ $method = strtolower($_REQUEST["op"] ?? ""); - $handler = new API($_REQUEST); + $handler = new Handler_API($_REQUEST); if ($handler->before($method)) { if ($method && method_exists($handler, $method)) { diff --git a/classes/article.php b/classes/article.php index 648b1e2c1..b4dff4b33 100755 --- a/classes/article.php +++ b/classes/article.php @@ -1,29 +1,9 @@ table_alias('e') - ->join('ttrss_user_entries', [ 'ref_id', '=', 'e.id'], 'ue') - ->where('ue.owner_uid', $_SESSION['uid']) - ->find_one((int)$_REQUEST['id']); - - if ($article) { - $article_url = UrlHelper::validate($article->link); - - if ($article_url) { - header("Location: $article_url"); - return; - } - } - - header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); - print "Article not found or has an empty URL."; - } - static function _create_published_article($title, $url, $content, $labels_str, $owner_uid) { @@ -158,137 +138,6 @@ class Article extends Handler_Protected { return $rc; } - function printArticleTags() { - $id = (int) clean($_REQUEST['id'] ?? 0); - - print json_encode(["id" => $id, - "tags" => self::_get_tags($id)]); - } - - function setScore() { - $ids = array_map("intval", clean($_REQUEST['ids'] ?? [])); - $score = (int)clean($_REQUEST['score']); - - $ids_qmarks = arr_qmarks($ids); - - $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET - score = ? WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?"); - - $sth->execute(array_merge([$score], $ids, [$_SESSION['uid']])); - - print json_encode(["id" => $ids, "score" => $score]); - } - - function setArticleTags() { - - $id = clean($_REQUEST["id"]); - - //$tags_str = clean($_REQUEST["tags_str"]); - //$tags = array_unique(array_map('trim', explode(",", $tags_str))); - - $tags = FeedItem_Common::normalize_categories(explode(",", clean($_REQUEST["tags_str"]))); - - $this->pdo->beginTransaction(); - - $sth = $this->pdo->prepare("SELECT int_id FROM ttrss_user_entries WHERE - ref_id = ? AND owner_uid = ? LIMIT 1"); - $sth->execute([$id, $_SESSION['uid']]); - - if ($row = $sth->fetch()) { - - $tags_to_cache = array(); - - $int_id = $row['int_id']; - - $dsth = $this->pdo->prepare("DELETE FROM ttrss_tags WHERE - post_int_id = ? AND owner_uid = ?"); - $dsth->execute([$int_id, $_SESSION['uid']]); - - $csth = $this->pdo->prepare("SELECT post_int_id FROM ttrss_tags - WHERE post_int_id = ? AND owner_uid = ? AND tag_name = ?"); - - $usth = $this->pdo->prepare("INSERT INTO ttrss_tags - (post_int_id, owner_uid, tag_name) - VALUES (?, ?, ?)"); - - foreach ($tags as $tag) { - $csth->execute([$int_id, $_SESSION['uid'], $tag]); - - if (!$csth->fetch()) { - $usth->execute([$int_id, $_SESSION['uid'], $tag]); - } - - array_push($tags_to_cache, $tag); - } - - /* update tag cache */ - - $tags_str = join(",", $tags_to_cache); - - $sth = $this->pdo->prepare("UPDATE ttrss_user_entries - SET tag_cache = ? WHERE ref_id = ? AND owner_uid = ?"); - $sth->execute([$tags_str, $id, $_SESSION['uid']]); - } - - $this->pdo->commit(); - - // get latest tags from the database, original $tags is sometimes JSON-encoded as a hash ({}) - ??? - print json_encode(["id" => (int)$id, "tags" => $this->_get_tags($id)]); - } - - - /*function completeTags() { - $search = clean($_REQUEST["search"]); - - $sth = $this->pdo->prepare("SELECT DISTINCT tag_name FROM ttrss_tags - WHERE owner_uid = ? AND - tag_name LIKE ? ORDER BY tag_name - LIMIT 10"); - - $sth->execute([$_SESSION['uid'], "$search%"]); - - print ""; - }*/ - - function assigntolabel() { - return $this->_label_ops(true); - } - - function removefromlabel() { - return $this->_label_ops(false); - } - - private function _label_ops($assign) { - $reply = array(); - - $ids = explode(",", clean($_REQUEST["ids"])); - $label_id = clean($_REQUEST["lid"]); - - $label = Labels::find_caption($label_id, $_SESSION["uid"]); - - $reply["labels-for"] = []; - - if ($label) { - foreach ($ids as $id) { - if ($assign) - Labels::add_article($id, $label, $_SESSION["uid"]); - else - Labels::remove_article($id, $label, $_SESSION["uid"]); - - array_push($reply["labels-for"], - ["id" => (int)$id, "labels" => $this->_get_labels($id)]); - } - } - - $reply["message"] = "UPDATE_COUNTERS"; - - print json_encode($reply); - } - static function _format_enclosures($id, $always_display_enclosures, $article_content, @@ -416,19 +265,6 @@ class Article extends Handler_Protected { return $tags; } - function getmetadatabyid() { - $article = ORM::for_table('ttrss_entries') - ->join('ttrss_user_entries', ['ref_id', '=', 'id'], 'ue') - ->where('ue.owner_uid', $_SESSION['uid']) - ->find_one((int)$_REQUEST['id']); - - if ($article) { - echo json_encode(["link" => $article->link, "title" => $article->title]); - } else { - echo json_encode([]); - } - } - static function _get_enclosures($id) { $encs = ORM::for_table('ttrss_enclosures') ->where('post_id', $id) @@ -666,4 +502,5 @@ class Article extends Handler_Protected { return array_unique($rv); } -} + +} \ No newline at end of file diff --git a/classes/feeds.php b/classes/feeds.php index c67edbc51..5470b23ac 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -1,978 +1,12 @@ _mark_timestamp("init"); - - $reply = []; - $rgba_cache = []; - $topmost_article_ids = []; - - if (!$offset) $offset = 0; - if ($method == "undefined") $method = ""; - - $method_split = explode(":", $method); - - if ($method == "ForceUpdate" && $feed > 0 && is_numeric($feed)) { - $sth = $this->pdo->prepare("UPDATE ttrss_feeds - SET last_updated = '1970-01-01', last_update_started = '1970-01-01' - WHERE id = ?"); - $sth->execute([$feed]); - } - - if ($method_split[0] == "MarkAllReadGR") { - $this->_catchup($method_split[1], false); - } - - // FIXME: might break tag display? - - if (is_numeric($feed) && $feed > 0 && !$cat_view) { - $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ? LIMIT 1"); - $sth->execute([$feed]); - - if (!$sth->fetch()) { - $reply['content'] = "
".__('Feed not found.')."
"; - } - } - - $search = $_REQUEST["query"] ?? ""; - $search_language = $_REQUEST["search_language"] ?? ""; // PGSQL only - - if ($search) { - $disable_cache = true; - } - - $qfh_ret = []; - - if (!$cat_view && is_numeric($feed) && $feed < PLUGIN_FEED_BASE_INDEX && $feed > LABEL_BASE_INDEX) { - $handler = PluginHost::getInstance()->get_feed_handler( - PluginHost::feed_to_pfeed_id($feed)); - - if ($handler) { - $options = array( - "limit" => $limit, - "view_mode" => $view_mode, - "cat_view" => $cat_view, - "search" => $search, - "override_order" => $override_order, - "offset" => $offset, - "owner_uid" => $_SESSION["uid"], - "filter" => false, - "since_id" => 0, - "include_children" => $include_children, - "order_by" => $order_by); - - $qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id($feed), - $options); - } - - } else { - - $params = array( - "feed" => $feed, - "limit" => $limit, - "view_mode" => $view_mode, - "cat_view" => $cat_view, - "search" => $search, - "search_language" => $search_language, - "override_order" => $override_order, - "offset" => $offset, - "include_children" => $include_children, - "check_first_id" => $check_first_id, - "skip_first_id_check" => $skip_first_id_check, - "order_by" => $order_by - ); - - $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); - - $result = $qfh_ret[0]; // this could be either a PDO query result or a -1 if first id changed - $feed_title = $qfh_ret[1]; - $feed_site_url = $qfh_ret[2]; - $last_error = $qfh_ret[3]; - $last_updated = strpos($qfh_ret[4], '1970-') === false ? - TimeHelper::make_local_datetime($qfh_ret[4], false) : __("Never"); - $highlight_words = $qfh_ret[5]; - $reply['first_id'] = $qfh_ret[6]; - $reply['is_vfeed'] = $qfh_ret[7]; - $query_error_override = $qfh_ret[8]; - - $reply['search_query'] = [$search, $search_language]; - $reply['vfeed_group_enabled'] = $vfeed_group_enabled; - - $plugin_menu_items = ""; - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM, - function ($result) use (&$plugin_menu_items) { - $plugin_menu_items .= $result; - }, - $feed, $cat_view); - - $plugin_buttons = ""; - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_BUTTON, - function ($result) use (&$plugin_buttons) { - $plugin_buttons .= $result; - }, - $feed, $cat_view); - - $reply['toolbar'] = [ - 'site_url' => $feed_site_url, - 'title' => strip_tags($feed_title), - 'error' => $last_error, - 'last_updated' => $last_updated, - 'plugin_menu_items' => $plugin_menu_items, - 'plugin_buttons' => $plugin_buttons, - ]; - - $reply['content'] = []; - - if ($offset == 0) - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINES_BEFORE, - function ($result) use (&$reply) { - $reply['content'] .= $result; - }, - $feed, $cat_view, $qfh_ret); - - $this->_mark_timestamp("object header"); - - $headlines_count = 0; - - if ($result instanceof PDOStatement) { - while ($line = $result->fetch(PDO::FETCH_ASSOC)) { - $this->_mark_timestamp("article start: " . $line["id"] . " " . $line["title"]); - - ++$headlines_count; - - if (!get_pref(Prefs::SHOW_CONTENT_PREVIEW)) { - $line["content_preview"] = ""; - } else { - $line["content_preview"] = "— " . truncate_string(strip_tags($line["content"]), 250); - - $max_excerpt_length = 250; - - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES, - function ($result) use (&$line) { - $line = $result; - }, - $line, $max_excerpt_length); - } - - $this->_mark_timestamp(" hook_query_headlines"); - - $id = $line["id"]; - - // frontend doesn't expect pdo returning booleans as strings on mysql - if (Config::get(Config::DB_TYPE) == "mysql") { - foreach (["unread", "marked", "published"] as $k) { - $line[$k] = $line[$k] === "1"; - } - } - - // normalize archived feed - if ($line['feed_id'] === null) { - $line['feed_id'] = 0; - $line["feed_title"] = __("Archived articles"); - } - - $feed_id = $line["feed_id"]; - - if ($line["num_labels"] > 0) { - $label_cache = $line["label_cache"]; - $labels = false; - - if ($label_cache) { - $label_cache = json_decode($label_cache, true); - - if ($label_cache) { - if ($label_cache["no-labels"] ?? 0 == 1) - $labels = []; - else - $labels = $label_cache; - } - } else { - $labels = Article::_get_labels($id); - } - - $line["labels"] = $labels; - } else { - $line["labels"] = []; - } - - if (count($topmost_article_ids) < 3) { - array_push($topmost_article_ids, $id); - } - - $this->_mark_timestamp(" labels"); - - $line["feed_title"] = $line["feed_title"] ?? ""; - - $line["buttons_left"] = ""; - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_LEFT_BUTTON, - function ($result) use (&$line) { - $line["buttons_left"] .= $result; - }, - $line); - - $line["buttons"] = ""; - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_BUTTON, - function ($result) use (&$line) { - $line["buttons"] .= $result; - }, - $line); - - $this->_mark_timestamp(" pre-sanitize"); - - $line["content"] = Sanitizer::sanitize($line["content"], - $line['hide_images'], false, $line["site_url"], $highlight_words, $line["id"]); - - $this->_mark_timestamp(" sanitize"); - - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_CDM, - function ($result, $plugin) use (&$line) { - $line = $result; - $this->_mark_timestamp(" hook_render_cdm: " . get_class($plugin)); - }, - $line); - - $this->_mark_timestamp(" hook_render_cdm"); - - $line['content'] = DiskCache::rewrite_urls($line['content']); - - $this->_mark_timestamp(" disk_cache_rewrite"); - - $this->_mark_timestamp(" note"); - - if (!get_pref(Prefs::CDM_EXPANDED)) { - $line["cdm_excerpt"] = " - remove_circle"; - - if (get_pref(Prefs::SHOW_CONTENT_PREVIEW)) { - $line["cdm_excerpt"] .= "" . $line["content_preview"] . ""; - } - } - - $this->_mark_timestamp(" pre-enclosures"); - - if ($line["num_enclosures"] > 0) { - $line["enclosures"] = Article::_format_enclosures($id, - $line["always_display_enclosures"], - $line["content"], - $line["hide_images"]); - } else { - $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, false, 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 - $tags = []; - - $line["tags"] = $tags; - - //$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"); - - require_once "colors.php"; - - if (!isset($rgba_cache[$feed_id])) { - if ($fav_color && $fav_color != 'fail') { - $rgba_cache[$feed_id] = \Colors\_color_unpack($fav_color); - } else { - $rgba_cache[$feed_id] = \Colors\_color_unpack($this->_color_of($line['feed_title'])); - } - } - - if (isset($rgba_cache[$feed_id])) { - $line['feed_bg_color'] = 'rgba(' . implode(",", $rgba_cache[$feed_id]) . ',0.3)'; - } - - $this->_mark_timestamp(" color"); - - /* we don't need those */ - - foreach (["date_entered", "guid", "last_published", "last_marked", "tag_cache", "favicon_avg_color", - "uuid", "label_cache", "yyiw", "num_enclosures"] as $k) - unset($line[$k]); - - array_push($reply['content'], $line); - - $this->_mark_timestamp("article end"); - } - } - - $this->_mark_timestamp("end of articles"); - - if (!$headlines_count) { - - if ($result instanceof PDOStatement) { - - if ($query_error_override) { - $message = $query_error_override; - } else { - switch ($view_mode) { - case "unread": - $message = __("No unread articles found to display."); - break; - case "updated": - $message = __("No updated articles found to display."); - break; - case "marked": - $message = __("No starred articles found to display."); - break; - default: - if ($feed < LABEL_BASE_INDEX) { - $message = __("No articles found to display. You can assign articles to labels manually from article header context menu (applies to all selected articles) or use a filter."); - } else { - $message = __("No articles found to display."); - } - } - } - - if (!$offset && $message) { - $reply['content'] = "
$message"; - - $reply['content'] .= "

"; - - $sth = $this->pdo->prepare("SELECT " . SUBSTRING_FOR_DATE . "(MAX(last_updated), 1, 19) AS last_updated FROM ttrss_feeds - WHERE owner_uid = ?"); - $sth->execute([$_SESSION['uid']]); - $row = $sth->fetch(); - - $last_updated = TimeHelper::make_local_datetime($row["last_updated"], false); - - $reply['content'] .= sprintf(__("Feeds last updated at %s"), $last_updated); - - $sth = $this->pdo->prepare("SELECT COUNT(id) AS num_errors - FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?"); - $sth->execute([$_SESSION['uid']]); - $row = $sth->fetch(); - - $num_errors = $row["num_errors"]; - - if ($num_errors > 0) { - $reply['content'] .= "
"; - $reply['content'] .= "" . - __('Some feeds have update errors (click for details)') . ""; - } - $reply['content'] .= "

"; - - } - } else if (is_numeric($result) && $result == -1) { - $reply['first_id_changed'] = true; - } - } - - $this->_mark_timestamp("end"); - - return array($topmost_article_ids, $headlines_count, $feed, $disable_cache, $reply); - } - - function catchupAll() { - $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET - last_read = NOW(), unread = false WHERE unread = true AND owner_uid = ?"); - $sth->execute([$_SESSION['uid']]); - - print json_encode(array("message" => "UPDATE_COUNTERS")); - } - - function view() { - $reply = array(); - - $feed = $_REQUEST["feed"]; - $method = $_REQUEST["m"] ?? ""; - $view_mode = $_REQUEST["view_mode"] ?? ""; - $limit = 30; - $cat_view = $_REQUEST["cat"] == "true"; - $next_unread_feed = $_REQUEST["nuf"] ?? 0; - $offset = $_REQUEST["skip"] ?? 0; - $order_by = $_REQUEST["order_by"] ?? ""; - $check_first_id = $_REQUEST["fid"] ?? 0; - - if (is_numeric($feed)) $feed = (int) $feed; - - /* Feed -5 is a special case: it is used to display auxiliary information - * when there's nothing to load - e.g. no stuff in fresh feed */ - - if ($feed == -5) { - print json_encode($this->_generate_dashboard_feed()); - return; - } - - $sth = false; - if ($feed < LABEL_BASE_INDEX) { - - $label_feed = Labels::feed_to_label_id($feed); - - $sth = $this->pdo->prepare("SELECT id FROM ttrss_labels2 WHERE - id = ? AND owner_uid = ?"); - $sth->execute([$label_feed, $_SESSION['uid']]); - - } else if (!$cat_view && is_numeric($feed) && $feed > 0) { - - $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE - id = ? AND owner_uid = ?"); - $sth->execute([$feed, $_SESSION['uid']]); - - } else if ($cat_view && is_numeric($feed) && $feed > 0) { - - $sth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories WHERE - id = ? AND owner_uid = ?"); - - $sth->execute([$feed, $_SESSION['uid']]); - } - - if ($sth && !$sth->fetch()) { - print json_encode($this->_generate_error_feed(__("Feed not found."))); - return; - } - - set_pref(Prefs::_DEFAULT_VIEW_MODE, $view_mode); - set_pref(Prefs::_DEFAULT_VIEW_ORDER_BY, $order_by); - - /* bump login timestamp if needed */ - if (time() - $_SESSION["last_login_update"] > 3600) { - $user = ORM::for_table('ttrss_users')->find_one($_SESSION["uid"]); - $user->last_login = Db::NOW(); - $user->save(); - - $_SESSION["last_login_update"] = time(); - } - - if (!$cat_view && is_numeric($feed) && $feed > 0) { - $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET last_viewed = NOW() - WHERE id = ? AND owner_uid = ?"); - $sth->execute([$feed, $_SESSION['uid']]); - } - - $reply['headlines'] = []; - - list($override_order, $skip_first_id_check) = self::_order_to_override_query($order_by); - - $ret = $this->_format_headlines_list($feed, $method, - $view_mode, $limit, $cat_view, $offset, - $override_order, true, $check_first_id, $skip_first_id_check, $order_by); - - $headlines_count = $ret[1]; - $disable_cache = $ret[3]; - $reply['headlines'] = $ret[4]; - - if (!$next_unread_feed) - $reply['headlines']['id'] = $feed; - else - $reply['headlines']['id'] = $next_unread_feed; - - $reply['headlines']['is_cat'] = $cat_view; - - $reply['headlines-info'] = ["count" => (int) $headlines_count, - "disable_cache" => (bool) $disable_cache]; - - // this is parsed by handleRpcJson() on first viewfeed() to set cdm expanded, etc - $reply['runtime-info'] = RPC::_make_runtime_info(); - - print json_encode($reply); - } - - private function _generate_dashboard_feed() { - $reply = array(); - - $reply['headlines']['id'] = -5; - $reply['headlines']['is_cat'] = false; - - $reply['headlines']['toolbar'] = ''; - - $reply['headlines']['content'] = "
".__('No feed selected.'); - - $reply['headlines']['content'] .= "

"; - - $sth = $this->pdo->prepare("SELECT ".SUBSTRING_FOR_DATE."(MAX(last_updated), 1, 19) AS last_updated FROM ttrss_feeds - WHERE owner_uid = ?"); - $sth->execute([$_SESSION['uid']]); - $row = $sth->fetch(); - - $last_updated = TimeHelper::make_local_datetime($row["last_updated"], false); - - $reply['headlines']['content'] .= sprintf(__("Feeds last updated at %s"), $last_updated); - - $sth = $this->pdo->prepare("SELECT COUNT(id) AS num_errors - FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?"); - $sth->execute([$_SESSION['uid']]); - $row = $sth->fetch(); - - $num_errors = $row["num_errors"]; - - if ($num_errors > 0) { - $reply['headlines']['content'] .= "
"; - $reply['headlines']['content'] .= "". - __('Some feeds have update errors (click for details)').""; - } - $reply['headlines']['content'] .= "

"; - - $reply['headlines-info'] = array("count" => 0, - "unread" => 0, - "disable_cache" => true); - - return $reply; - } - - private function _generate_error_feed($error) { - $reply = array(); - - $reply['headlines']['id'] = -7; - $reply['headlines']['is_cat'] = false; - - $reply['headlines']['toolbar'] = ''; - $reply['headlines']['content'] = "
". $error . "
"; - - $reply['headlines-info'] = array("count" => 0, - "unread" => 0, - "disable_cache" => true); - - return $reply; - } - - function subscribeToFeed() { - print json_encode([ - "cat_select" => \Controls\select_feeds_cats("cat") - ]); - } - - function search() { - print json_encode([ - "show_language" => Config::get(Config::DB_TYPE) == "pgsql", - "show_syntax_help" => count(PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH)) == 0, - "all_languages" => Pref_Feeds::get_ts_languages(), - "default_language" => get_pref(Prefs::DEFAULT_SEARCH_LANGUAGE) - ]); - } - - function updatedebugger() { - header("Content-type: text/html"); - - $xdebug = isset($_REQUEST["xdebug"]) ? (int)$_REQUEST["xdebug"] : 1; - - Debug::set_enabled(true); - Debug::set_loglevel($xdebug); - - $feed_id = (int)$_REQUEST["feed_id"]; - $do_update = ($_REQUEST["action"] ?? "") == "do_update"; - $csrf_token = $_POST["csrf_token"]; - - $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ? AND owner_uid = ?"); - $sth->execute([$feed_id, $_SESSION['uid']]); - - if (!$sth->fetch()) { - print "Access denied."; - return; - } - ?> - - - - Feed Debugger - - - - - - - - - - -
-

Feed Debugger: _get_title($feed_id) ?>

-
-
- - - - - - -
- -
- -
- -
- -
- -
- - -
- -
- -
-
-
- - - chain_hooks_callback(PluginHost::HOOK_SEARCH, - function ($result) use (&$search_qpart, &$search_words) { - if (!empty($result)) { - list($search_qpart, $search_words) = $result; - return true; - } - }, - $search[0]); - - // fall back in case of no plugins - if (empty($search_qpart)) { - list($search_qpart, $search_words) = self::_search_to_sql($search[0], $search[1], $owner_uid); - } - } else { - $search_qpart = "true"; - } - - // TODO: all this interval stuff needs some generic generator function - - switch ($mode) { - case "1day": - if (Config::get(Config::DB_TYPE) == "pgsql") { - $date_qpart = "date_entered < NOW() - INTERVAL '1 day' "; - } else { - $date_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 1 DAY) "; - } - break; - case "1week": - if (Config::get(Config::DB_TYPE) == "pgsql") { - $date_qpart = "date_entered < NOW() - INTERVAL '1 week' "; - } else { - $date_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 1 WEEK) "; - } - break; - case "2week": - if (Config::get(Config::DB_TYPE) == "pgsql") { - $date_qpart = "date_entered < NOW() - INTERVAL '2 week' "; - } else { - $date_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 2 WEEK) "; - } - break; - default: - $date_qpart = "true"; - } - - if (is_numeric($feed)) { - if ($cat_view) { - - if ($feed >= 0) { - - if ($feed > 0) { - $children = self::_get_child_cats($feed, $owner_uid); - array_push($children, $feed); - $children = array_map("intval", $children); - - $children = join(",", $children); - - $cat_qpart = "cat_id IN ($children)"; - } else { - $cat_qpart = "cat_id IS NULL"; - } - - $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 IN - (SELECT id FROM ttrss_feeds WHERE $cat_qpart) AND $date_qpart AND $search_qpart) as tmp)"); - $sth->execute([$owner_uid]); - - } else if ($feed == -2) { - - $sth = $pdo->prepare("UPDATE ttrss_user_entries - SET unread = false,last_read = NOW() WHERE (SELECT COUNT(*) - FROM ttrss_user_labels2, ttrss_entries WHERE article_id = ref_id AND id = ref_id AND $date_qpart AND $search_qpart) > 0 - AND unread = true AND owner_uid = ?"); - $sth->execute([$owner_uid]); - } - - } else if ($feed > 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]); - - } else if ($feed < 0 && $feed > LABEL_BASE_INDEX) { // special, like starred - - if ($feed == -1) { - $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 marked = true AND $date_qpart AND $search_qpart) as tmp)"); - $sth->execute([$owner_uid]); - } - - if ($feed == -2) { - $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 published = true AND $date_qpart AND $search_qpart) as tmp)"); - $sth->execute([$owner_uid]); - } - - if ($feed == -3) { - - $intl = (int) get_pref(Prefs::FRESH_ARTICLE_MAX_AGE); - - if (Config::get(Config::DB_TYPE) == "pgsql") { - $match_part = "date_entered > NOW() - INTERVAL '$intl hour' "; - } else { - $match_part = "date_entered > DATE_SUB(NOW(), - INTERVAL $intl HOUR) "; - } - - $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 score >= 0 AND unread = true AND $date_qpart AND $match_part AND $search_qpart) as tmp)"); - $sth->execute([$owner_uid]); - } - - if ($feed == -4) { - $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 $date_qpart AND $search_qpart) as tmp)"); - $sth->execute([$owner_uid]); - } - - } else if ($feed < LABEL_BASE_INDEX) { // label - - $label_id = Labels::feed_to_label_id($feed); - - $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_user_labels2 WHERE ref_id = id - 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 - $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]); - - } - } - - static function _get_counters($feed, $is_cat = false, $unread_only = false, - $owner_uid = false) { - - $n_feed = (int) $feed; - $need_entries = false; - - $pdo = Db::pdo(); - - if (!$owner_uid) $owner_uid = $_SESSION["uid"]; - - if ($unread_only) { - $unread_qpart = "unread = true"; - } else { - $unread_qpart = "true"; - } - - $match_part = ""; - - if ($is_cat) { - return self::_get_cat_unread($n_feed, $owner_uid); - } else if ($n_feed == -6) { - return 0; - } else if ($feed != "0" && $n_feed == 0) { - - $sth = $pdo->prepare("SELECT SUM((SELECT COUNT(int_id) - FROM ttrss_user_entries,ttrss_entries WHERE int_id = post_int_id - AND ref_id = id AND $unread_qpart)) AS count FROM ttrss_tags - WHERE owner_uid = ? AND tag_name = ?"); - - $sth->execute([$owner_uid, $feed]); - $row = $sth->fetch(); - - return $row["count"]; - - } else if ($n_feed == -1) { - $match_part = "marked = true"; - } else if ($n_feed == -2) { - $match_part = "published = true"; - } else if ($n_feed == -3) { - $match_part = "unread = true AND score >= 0"; - - $intl = (int) get_pref(Prefs::FRESH_ARTICLE_MAX_AGE, $owner_uid); - - if (Config::get(Config::DB_TYPE) == "pgsql") { - $match_part .= " AND date_entered > NOW() - INTERVAL '$intl hour' "; - } else { - $match_part .= " AND date_entered > DATE_SUB(NOW(), INTERVAL $intl HOUR) "; - } - - $need_entries = true; - - } else if ($n_feed == -4) { - $match_part = "true"; - } else if ($n_feed >= 0) { - - if ($n_feed != 0) { - $match_part = sprintf("feed_id = %d", $n_feed); - } else { - $match_part = "feed_id IS NULL"; - } - - } else if ($feed < LABEL_BASE_INDEX) { - - $label_id = Labels::feed_to_label_id($feed); - - return self::_get_label_unread($label_id, $owner_uid); - } - - if ($match_part) { - - if ($need_entries) { - $from_qpart = "ttrss_user_entries,ttrss_entries"; - $from_where = "ttrss_entries.id = ttrss_user_entries.ref_id AND"; - } else { - $from_qpart = "ttrss_user_entries"; - $from_where = ""; - } - - $sth = $pdo->prepare("SELECT count(int_id) AS unread - FROM $from_qpart WHERE - $unread_qpart AND $from_where ($match_part) AND ttrss_user_entries.owner_uid = ?"); - $sth->execute([$owner_uid]); - $row = $sth->fetch(); - - return $row["unread"]; - - } else { - - $sth = $pdo->prepare("SELECT COUNT(post_int_id) AS unread - FROM ttrss_tags,ttrss_user_entries,ttrss_entries - WHERE tag_name = ? AND post_int_id = int_id AND ref_id = ttrss_entries.id - AND $unread_qpart AND ttrss_tags.owner_uid = ,"); - - $sth->execute([$feed, $owner_uid]); - $row = $sth->fetch(); - - return $row["unread"]; - } - } - - function add() { - $feed = clean($_REQUEST['feed']); - $cat = clean($_REQUEST['cat'] ?? ''); - $need_auth = isset($_REQUEST['need_auth']); - $login = $need_auth ? clean($_REQUEST['login']) : ''; - $pass = $need_auth ? clean($_REQUEST['pass']) : ''; - - $rc = Feeds::_subscribe($feed, $cat, $login, $pass); - - print json_encode(array("result" => $rc)); - } + private $viewfeed_timestamp; + private $viewfeed_timestamp_last; /** * @return array (code => Status code, message => error message if available) @@ -1266,24 +300,7 @@ class Feeds extends Handler_Protected { } } - private static function _get_label_unread($label_id, $owner_uid = false) { - if (!$owner_uid) $owner_uid = $_SESSION["uid"]; - - $pdo = Db::pdo(); - - $sth = $pdo->prepare("SELECT COUNT(ref_id) AS unread FROM ttrss_user_entries, ttrss_user_labels2 - WHERE owner_uid = ? AND unread = true AND label_id = ? AND article_id = ref_id"); - - $sth->execute([$owner_uid, $label_id]); - - if ($row = $sth->fetch()) { - return $row["unread"]; - } else { - return 0; - } - } - - static function _get_headlines($params) { + static function _get_headlines($params) { $pdo = Db::pdo(); @@ -1972,6 +989,56 @@ class Feeds extends Handler_Protected { } } + private static function _get_label_unread($label_id, $owner_uid = false) { + if (!$owner_uid) $owner_uid = $_SESSION["uid"]; + + $pdo = Db::pdo(); + + $sth = $pdo->prepare("SELECT COUNT(ref_id) AS unread FROM ttrss_user_entries, ttrss_user_labels2 + WHERE owner_uid = ? AND unread = true AND label_id = ? AND article_id = ref_id"); + + $sth->execute([$owner_uid, $label_id]); + + if ($row = $sth->fetch()) { + return $row["unread"]; + } else { + return 0; + } + } + + private static function _get_purge_interval($feed_id) { + $feed = ORM::for_table('ttrss_feeds')->find_one($feed_id); + + if ($feed) { + + if ($feed->purge_interval != 0) + return $feed->purge_interval; + else + return get_pref(Prefs::PURGE_OLD_DAYS, $feed->owner_uid); + + } else { + return -1; + } + } + + private function _mark_timestamp($label) { + + 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; + } + static function _purge($feed_id, $purge_interval) { if (!$purge_interval) $purge_interval = self::_get_purge_interval($feed_id); @@ -2042,18 +1109,302 @@ class Feeds extends Handler_Protected { return $rows_deleted; } - private static function _get_purge_interval($feed_id) { - $feed = ORM::for_table('ttrss_feeds')->find_one($feed_id); + static function _order_to_override_query($order) { + $query = ""; + $skip_first_id = false; - if ($feed) { + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE, + function ($result) use (&$query, &$skip_first_id) { + list ($query, $skip_first_id) = $result; + }, + $order); - if ($feed->purge_interval != 0) - return $feed->purge_interval; - else - return get_pref(Prefs::PURGE_OLD_DAYS, $feed->owner_uid); + if ($query) return [$query, $skip_first_id]; + + switch ($order) { + case "title": + $query = "ttrss_entries.title, date_entered, updated"; + break; + case "date_reverse": + $query = "updated"; + $skip_first_id = true; + break; + case "feed_dates": + $query = "updated DESC"; + break; + } + + return [$query, $skip_first_id]; + } + + static function _catchup($feed, $cat_view, $owner_uid = false, $mode = 'all', $search = false) { + + if (!$owner_uid) $owner_uid = $_SESSION['uid']; + + $pdo = Db::pdo(); + + if (is_array($search) && $search[0]) { + $search_qpart = ""; + + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_SEARCH, + function ($result) use (&$search_qpart, &$search_words) { + if (!empty($result)) { + list($search_qpart, $search_words) = $result; + return true; + } + }, + $search[0]); + + // fall back in case of no plugins + if (empty($search_qpart)) { + list($search_qpart, $search_words) = self::_search_to_sql($search[0], $search[1], $owner_uid); + } + } else { + $search_qpart = "true"; + } + + // TODO: all this interval stuff needs some generic generator function + + switch ($mode) { + case "1day": + if (Config::get(Config::DB_TYPE) == "pgsql") { + $date_qpart = "date_entered < NOW() - INTERVAL '1 day' "; + } else { + $date_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 1 DAY) "; + } + break; + case "1week": + if (Config::get(Config::DB_TYPE) == "pgsql") { + $date_qpart = "date_entered < NOW() - INTERVAL '1 week' "; + } else { + $date_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 1 WEEK) "; + } + break; + case "2week": + if (Config::get(Config::DB_TYPE) == "pgsql") { + $date_qpart = "date_entered < NOW() - INTERVAL '2 week' "; + } else { + $date_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 2 WEEK) "; + } + break; + default: + $date_qpart = "true"; + } + + if (is_numeric($feed)) { + if ($cat_view) { + + if ($feed >= 0) { + + if ($feed > 0) { + $children = self::_get_child_cats($feed, $owner_uid); + array_push($children, $feed); + $children = array_map("intval", $children); + + $children = join(",", $children); + + $cat_qpart = "cat_id IN ($children)"; + } else { + $cat_qpart = "cat_id IS NULL"; + } + + $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 IN + (SELECT id FROM ttrss_feeds WHERE $cat_qpart) AND $date_qpart AND $search_qpart) as tmp)"); + $sth->execute([$owner_uid]); + + } else if ($feed == -2) { + + $sth = $pdo->prepare("UPDATE ttrss_user_entries + SET unread = false,last_read = NOW() WHERE (SELECT COUNT(*) + FROM ttrss_user_labels2, ttrss_entries WHERE article_id = ref_id AND id = ref_id AND $date_qpart AND $search_qpart) > 0 + AND unread = true AND owner_uid = ?"); + $sth->execute([$owner_uid]); + } + + } else if ($feed > 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]); + + } else if ($feed < 0 && $feed > LABEL_BASE_INDEX) { // special, like starred + + if ($feed == -1) { + $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 marked = true AND $date_qpart AND $search_qpart) as tmp)"); + $sth->execute([$owner_uid]); + } + + if ($feed == -2) { + $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 published = true AND $date_qpart AND $search_qpart) as tmp)"); + $sth->execute([$owner_uid]); + } + + if ($feed == -3) { + + $intl = (int) get_pref(Prefs::FRESH_ARTICLE_MAX_AGE); + + if (Config::get(Config::DB_TYPE) == "pgsql") { + $match_part = "date_entered > NOW() - INTERVAL '$intl hour' "; + } else { + $match_part = "date_entered > DATE_SUB(NOW(), + INTERVAL $intl HOUR) "; + } + + $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 score >= 0 AND unread = true AND $date_qpart AND $match_part AND $search_qpart) as tmp)"); + $sth->execute([$owner_uid]); + } + + if ($feed == -4) { + $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 $date_qpart AND $search_qpart) as tmp)"); + $sth->execute([$owner_uid]); + } + + } else if ($feed < LABEL_BASE_INDEX) { // label + + $label_id = Labels::feed_to_label_id($feed); + + $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_user_labels2 WHERE ref_id = id + 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 + $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]); + + } + } + + static function _get_counters($feed, $is_cat = false, $unread_only = false, + $owner_uid = false) { + + $n_feed = (int) $feed; + $need_entries = false; + + $pdo = Db::pdo(); + + if (!$owner_uid) $owner_uid = $_SESSION["uid"]; + + if ($unread_only) { + $unread_qpart = "unread = true"; + } else { + $unread_qpart = "true"; + } + + $match_part = ""; + + if ($is_cat) { + return self::_get_cat_unread($n_feed, $owner_uid); + } else if ($n_feed == -6) { + return 0; + } else if ($feed != "0" && $n_feed == 0) { + + $sth = $pdo->prepare("SELECT SUM((SELECT COUNT(int_id) + FROM ttrss_user_entries,ttrss_entries WHERE int_id = post_int_id + AND ref_id = id AND $unread_qpart)) AS count FROM ttrss_tags + WHERE owner_uid = ? AND tag_name = ?"); + + $sth->execute([$owner_uid, $feed]); + $row = $sth->fetch(); + + return $row["count"]; + + } else if ($n_feed == -1) { + $match_part = "marked = true"; + } else if ($n_feed == -2) { + $match_part = "published = true"; + } else if ($n_feed == -3) { + $match_part = "unread = true AND score >= 0"; + + $intl = (int) get_pref(Prefs::FRESH_ARTICLE_MAX_AGE, $owner_uid); + + if (Config::get(Config::DB_TYPE) == "pgsql") { + $match_part .= " AND date_entered > NOW() - INTERVAL '$intl hour' "; + } else { + $match_part .= " AND date_entered > DATE_SUB(NOW(), INTERVAL $intl HOUR) "; + } + + $need_entries = true; + + } else if ($n_feed == -4) { + $match_part = "true"; + } else if ($n_feed >= 0) { + + if ($n_feed != 0) { + $match_part = sprintf("feed_id = %d", $n_feed); + } else { + $match_part = "feed_id IS NULL"; + } + + } else if ($feed < LABEL_BASE_INDEX) { + + $label_id = Labels::feed_to_label_id($feed); + + return self::_get_label_unread($label_id, $owner_uid); + } + + if ($match_part) { + + if ($need_entries) { + $from_qpart = "ttrss_user_entries,ttrss_entries"; + $from_where = "ttrss_entries.id = ttrss_user_entries.ref_id AND"; + } else { + $from_qpart = "ttrss_user_entries"; + $from_where = ""; + } + + $sth = $pdo->prepare("SELECT count(int_id) AS unread + FROM $from_qpart WHERE + $unread_qpart AND $from_where ($match_part) AND ttrss_user_entries.owner_uid = ?"); + $sth->execute([$owner_uid]); + $row = $sth->fetch(); + + return $row["unread"]; } else { - return -1; + + $sth = $pdo->prepare("SELECT COUNT(post_int_id) AS unread + FROM ttrss_tags,ttrss_user_entries,ttrss_entries + WHERE tag_name = ? AND post_int_id = int_id AND ref_id = ttrss_entries.id + AND $unread_qpart AND ttrss_tags.owner_uid = ,"); + + $sth->execute([$feed, $owner_uid]); + $row = $sth->fetch(); + + return $row["unread"]; } } @@ -2225,51 +1576,397 @@ class Feeds extends Handler_Protected { return array($search_query_part, $search_words); } - static function _order_to_override_query($order) { - $query = ""; - $skip_first_id = 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) { - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE, - function ($result) use (&$query, &$skip_first_id) { - list ($query, $skip_first_id) = $result; - }, - $order); + $disable_cache = false; - if ($query) return [$query, $skip_first_id]; + $this->_mark_timestamp("init"); - switch ($order) { - case "title": - $query = "ttrss_entries.title, date_entered, updated"; - break; - case "date_reverse": - $query = "updated"; - $skip_first_id = true; - break; - case "feed_dates": - $query = "updated DESC"; - break; + $reply = []; + $rgba_cache = []; + $topmost_article_ids = []; + + if (!$offset) $offset = 0; + if ($method == "undefined") $method = ""; + + $method_split = explode(":", $method); + + if ($method == "ForceUpdate" && $feed > 0 && is_numeric($feed)) { + $sth = $this->pdo->prepare("UPDATE ttrss_feeds + SET last_updated = '1970-01-01', last_update_started = '1970-01-01' + WHERE id = ?"); + $sth->execute([$feed]); } - return [$query, $skip_first_id]; + if ($method_split[0] == "MarkAllReadGR") { + $this->_catchup($method_split[1], false); + } + + // FIXME: might break tag display? + + if (is_numeric($feed) && $feed > 0 && !$cat_view) { + $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ? LIMIT 1"); + $sth->execute([$feed]); + + if (!$sth->fetch()) { + $reply['content'] = "
".__('Feed not found.')."
"; + } + } + + $search = $_REQUEST["query"] ?? ""; + $search_language = $_REQUEST["search_language"] ?? ""; // PGSQL only + + if ($search) { + $disable_cache = true; + } + + $qfh_ret = []; + + if (!$cat_view && is_numeric($feed) && $feed < PLUGIN_FEED_BASE_INDEX && $feed > LABEL_BASE_INDEX) { + $handler = PluginHost::getInstance()->get_feed_handler( + PluginHost::feed_to_pfeed_id($feed)); + + if ($handler) { + $options = array( + "limit" => $limit, + "view_mode" => $view_mode, + "cat_view" => $cat_view, + "search" => $search, + "override_order" => $override_order, + "offset" => $offset, + "owner_uid" => $_SESSION["uid"], + "filter" => false, + "since_id" => 0, + "include_children" => $include_children, + "order_by" => $order_by); + + $qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id($feed), + $options); + } + + } else { + + $params = array( + "feed" => $feed, + "limit" => $limit, + "view_mode" => $view_mode, + "cat_view" => $cat_view, + "search" => $search, + "search_language" => $search_language, + "override_order" => $override_order, + "offset" => $offset, + "include_children" => $include_children, + "check_first_id" => $check_first_id, + "skip_first_id_check" => $skip_first_id_check, + "order_by" => $order_by + ); + + $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); + + $result = $qfh_ret[0]; // this could be either a PDO query result or a -1 if first id changed + $feed_title = $qfh_ret[1]; + $feed_site_url = $qfh_ret[2]; + $last_error = $qfh_ret[3]; + $last_updated = strpos($qfh_ret[4], '1970-') === false ? + TimeHelper::make_local_datetime($qfh_ret[4], false) : __("Never"); + $highlight_words = $qfh_ret[5]; + $reply['first_id'] = $qfh_ret[6]; + $reply['is_vfeed'] = $qfh_ret[7]; + $query_error_override = $qfh_ret[8]; + + $reply['search_query'] = [$search, $search_language]; + $reply['vfeed_group_enabled'] = $vfeed_group_enabled; + + $plugin_menu_items = ""; + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM, + function ($result) use (&$plugin_menu_items) { + $plugin_menu_items .= $result; + }, + $feed, $cat_view); + + $plugin_buttons = ""; + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_BUTTON, + function ($result) use (&$plugin_buttons) { + $plugin_buttons .= $result; + }, + $feed, $cat_view); + + $reply['toolbar'] = [ + 'site_url' => $feed_site_url, + 'title' => strip_tags($feed_title), + 'error' => $last_error, + 'last_updated' => $last_updated, + 'plugin_menu_items' => $plugin_menu_items, + 'plugin_buttons' => $plugin_buttons, + ]; + + $reply['content'] = []; + + if ($offset == 0) + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINES_BEFORE, + function ($result) use (&$reply) { + $reply['content'] .= $result; + }, + $feed, $cat_view, $qfh_ret); + + $this->_mark_timestamp("object header"); + + $headlines_count = 0; + + if ($result instanceof PDOStatement) { + while ($line = $result->fetch(PDO::FETCH_ASSOC)) { + $this->_mark_timestamp("article start: " . $line["id"] . " " . $line["title"]); + + ++$headlines_count; + + if (!get_pref(Prefs::SHOW_CONTENT_PREVIEW)) { + $line["content_preview"] = ""; + } else { + $line["content_preview"] = "— " . truncate_string(strip_tags($line["content"]), 250); + + $max_excerpt_length = 250; + + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES, + function ($result) use (&$line) { + $line = $result; + }, + $line, $max_excerpt_length); + } + + $this->_mark_timestamp(" hook_query_headlines"); + + $id = $line["id"]; + + // frontend doesn't expect pdo returning booleans as strings on mysql + if (Config::get(Config::DB_TYPE) == "mysql") { + foreach (["unread", "marked", "published"] as $k) { + $line[$k] = $line[$k] === "1"; + } + } + + // normalize archived feed + if ($line['feed_id'] === null) { + $line['feed_id'] = 0; + $line["feed_title"] = __("Archived articles"); + } + + $feed_id = $line["feed_id"]; + + if ($line["num_labels"] > 0) { + $label_cache = $line["label_cache"]; + $labels = false; + + if ($label_cache) { + $label_cache = json_decode($label_cache, true); + + if ($label_cache) { + if ($label_cache["no-labels"] ?? 0 == 1) + $labels = []; + else + $labels = $label_cache; + } + } else { + $labels = Article::_get_labels($id); + } + + $line["labels"] = $labels; + } else { + $line["labels"] = []; + } + + if (count($topmost_article_ids) < 3) { + array_push($topmost_article_ids, $id); + } + + $this->_mark_timestamp(" labels"); + + $line["feed_title"] = $line["feed_title"] ?? ""; + + $line["buttons_left"] = ""; + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_LEFT_BUTTON, + function ($result) use (&$line) { + $line["buttons_left"] .= $result; + }, + $line); + + $line["buttons"] = ""; + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_BUTTON, + function ($result) use (&$line) { + $line["buttons"] .= $result; + }, + $line); + + $this->_mark_timestamp(" pre-sanitize"); + + $line["content"] = Sanitizer::sanitize($line["content"], + $line['hide_images'], false, $line["site_url"], $highlight_words, $line["id"]); + + $this->_mark_timestamp(" sanitize"); + + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_CDM, + function ($result, $plugin) use (&$line) { + $line = $result; + $this->_mark_timestamp(" hook_render_cdm: " . get_class($plugin)); + }, + $line); + + $this->_mark_timestamp(" hook_render_cdm"); + + $line['content'] = DiskCache::rewrite_urls($line['content']); + + $this->_mark_timestamp(" disk_cache_rewrite"); + + $this->_mark_timestamp(" note"); + + if (!get_pref(Prefs::CDM_EXPANDED)) { + $line["cdm_excerpt"] = " + remove_circle"; + + if (get_pref(Prefs::SHOW_CONTENT_PREVIEW)) { + $line["cdm_excerpt"] .= "" . $line["content_preview"] . ""; + } + } + + $this->_mark_timestamp(" pre-enclosures"); + + if ($line["num_enclosures"] > 0) { + $line["enclosures"] = Article::_format_enclosures($id, + $line["always_display_enclosures"], + $line["content"], + $line["hide_images"]); + } else { + $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, false, 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 + $tags = []; + + $line["tags"] = $tags; + + //$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"); + + require_once "colors.php"; + + if (!isset($rgba_cache[$feed_id])) { + if ($fav_color && $fav_color != 'fail') { + $rgba_cache[$feed_id] = \Colors\_color_unpack($fav_color); + } else { + $rgba_cache[$feed_id] = \Colors\_color_unpack($this->_color_of($line['feed_title'])); + } + } + + if (isset($rgba_cache[$feed_id])) { + $line['feed_bg_color'] = 'rgba(' . implode(",", $rgba_cache[$feed_id]) . ',0.3)'; + } + + $this->_mark_timestamp(" color"); + + /* we don't need those */ + + foreach (["date_entered", "guid", "last_published", "last_marked", "tag_cache", "favicon_avg_color", + "uuid", "label_cache", "yyiw", "num_enclosures"] as $k) + unset($line[$k]); + + array_push($reply['content'], $line); + + $this->_mark_timestamp("article end"); + } + } + + $this->_mark_timestamp("end of articles"); + + if (!$headlines_count) { + + if ($result instanceof PDOStatement) { + + if ($query_error_override) { + $message = $query_error_override; + } else { + switch ($view_mode) { + case "unread": + $message = __("No unread articles found to display."); + break; + case "updated": + $message = __("No updated articles found to display."); + break; + case "marked": + $message = __("No starred articles found to display."); + break; + default: + if ($feed < LABEL_BASE_INDEX) { + $message = __("No articles found to display. You can assign articles to labels manually from article header context menu (applies to all selected articles) or use a filter."); + } else { + $message = __("No articles found to display."); + } + } + } + + if (!$offset && $message) { + $reply['content'] = "
$message"; + + $reply['content'] .= "

"; + + $sth = $this->pdo->prepare("SELECT " . SUBSTRING_FOR_DATE . "(MAX(last_updated), 1, 19) AS last_updated FROM ttrss_feeds + WHERE owner_uid = ?"); + $sth->execute([$_SESSION['uid']]); + $row = $sth->fetch(); + + $last_updated = TimeHelper::make_local_datetime($row["last_updated"], false); + + $reply['content'] .= sprintf(__("Feeds last updated at %s"), $last_updated); + + $sth = $this->pdo->prepare("SELECT COUNT(id) AS num_errors + FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?"); + $sth->execute([$_SESSION['uid']]); + $row = $sth->fetch(); + + $num_errors = $row["num_errors"]; + + if ($num_errors > 0) { + $reply['content'] .= "
"; + $reply['content'] .= "" . + __('Some feeds have update errors (click for details)') . ""; + } + $reply['content'] .= "

"; + + } + } else if (is_numeric($result) && $result == -1) { + $reply['first_id_changed'] = true; + } + } + + $this->_mark_timestamp("end"); + + return array($topmost_article_ids, $headlines_count, $feed, $disable_cache, $reply); } - - private function _mark_timestamp($label) { - - 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; - } - -} - +} \ No newline at end of file diff --git a/classes/api.php b/classes/handler/api.php old mode 100755 new mode 100644 similarity index 99% rename from classes/api.php rename to classes/handler/api.php index 991682191..e860584b4 --- a/classes/api.php +++ b/classes/handler/api.php @@ -1,5 +1,5 @@ table_alias('e') + ->join('ttrss_user_entries', [ 'ref_id', '=', 'e.id'], 'ue') + ->where('ue.owner_uid', $_SESSION['uid']) + ->find_one((int)$_REQUEST['id']); + + if ($article) { + $article_url = UrlHelper::validate($article->link); + + if ($article_url) { + header("Location: $article_url"); + return; + } + } + + header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); + print "Article not found or has an empty URL."; + } + + function printArticleTags() { + $id = (int) clean($_REQUEST['id'] ?? 0); + + print json_encode(["id" => $id, + "tags" => Article::_get_tags($id)]); + } + + function setScore() { + $ids = array_map("intval", clean($_REQUEST['ids'] ?? [])); + $score = (int)clean($_REQUEST['score']); + + $ids_qmarks = arr_qmarks($ids); + + $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET + score = ? WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?"); + + $sth->execute(array_merge([$score], $ids, [$_SESSION['uid']])); + + print json_encode(["id" => $ids, "score" => $score]); + } + + function setArticleTags() { + + $id = clean($_REQUEST["id"]); + + //$tags_str = clean($_REQUEST["tags_str"]); + //$tags = array_unique(array_map('trim', explode(",", $tags_str))); + + $tags = FeedItem_Common::normalize_categories(explode(",", clean($_REQUEST["tags_str"]))); + + $this->pdo->beginTransaction(); + + $sth = $this->pdo->prepare("SELECT int_id FROM ttrss_user_entries WHERE + ref_id = ? AND owner_uid = ? LIMIT 1"); + $sth->execute([$id, $_SESSION['uid']]); + + if ($row = $sth->fetch()) { + + $tags_to_cache = array(); + + $int_id = $row['int_id']; + + $dsth = $this->pdo->prepare("DELETE FROM ttrss_tags WHERE + post_int_id = ? AND owner_uid = ?"); + $dsth->execute([$int_id, $_SESSION['uid']]); + + $csth = $this->pdo->prepare("SELECT post_int_id FROM ttrss_tags + WHERE post_int_id = ? AND owner_uid = ? AND tag_name = ?"); + + $usth = $this->pdo->prepare("INSERT INTO ttrss_tags + (post_int_id, owner_uid, tag_name) + VALUES (?, ?, ?)"); + + foreach ($tags as $tag) { + $csth->execute([$int_id, $_SESSION['uid'], $tag]); + + if (!$csth->fetch()) { + $usth->execute([$int_id, $_SESSION['uid'], $tag]); + } + + array_push($tags_to_cache, $tag); + } + + /* update tag cache */ + + $tags_str = join(",", $tags_to_cache); + + $sth = $this->pdo->prepare("UPDATE ttrss_user_entries + SET tag_cache = ? WHERE ref_id = ? AND owner_uid = ?"); + $sth->execute([$tags_str, $id, $_SESSION['uid']]); + } + + $this->pdo->commit(); + + // get latest tags from the database, original $tags is sometimes JSON-encoded as a hash ({}) - ??? + print json_encode(["id" => (int)$id, "tags" => Article::_get_tags($id)]); + } + + + /*function completeTags() { + $search = clean($_REQUEST["search"]); + + $sth = $this->pdo->prepare("SELECT DISTINCT tag_name FROM ttrss_tags + WHERE owner_uid = ? AND + tag_name LIKE ? ORDER BY tag_name + LIMIT 10"); + + $sth->execute([$_SESSION['uid'], "$search%"]); + + print ""; + }*/ + + function assigntolabel() { + return $this->_label_ops(true); + } + + function removefromlabel() { + return $this->_label_ops(false); + } + + private function _label_ops($assign) { + $reply = array(); + + $ids = explode(",", clean($_REQUEST["ids"])); + $label_id = clean($_REQUEST["lid"]); + + $label = Labels::find_caption($label_id, $_SESSION["uid"]); + + $reply["labels-for"] = []; + + if ($label) { + foreach ($ids as $id) { + if ($assign) + Labels::add_article($id, $label, $_SESSION["uid"]); + else + Labels::remove_article($id, $label, $_SESSION["uid"]); + + array_push($reply["labels-for"], + ["id" => (int)$id, "labels" => Article::_get_labels($id)]); + } + } + + $reply["message"] = "UPDATE_COUNTERS"; + + print json_encode($reply); + } + + function getmetadatabyid() { + $article = ORM::for_table('ttrss_entries') + ->join('ttrss_user_entries', ['ref_id', '=', 'id'], 'ue') + ->where('ue.owner_uid', $_SESSION['uid']) + ->find_one((int)$_REQUEST['id']); + + if ($article) { + echo json_encode(["link" => $article->link, "title" => $article->title]); + } else { + echo json_encode([]); + } + } +} diff --git a/classes/handler/feeds.php b/classes/handler/feeds.php new file mode 100644 index 000000000..0d262a554 --- /dev/null +++ b/classes/handler/feeds.php @@ -0,0 +1,305 @@ +pdo->prepare("UPDATE ttrss_user_entries SET + last_read = NOW(), unread = false WHERE unread = true AND owner_uid = ?"); + $sth->execute([$_SESSION['uid']]); + + print json_encode(array("message" => "UPDATE_COUNTERS")); + } + + function view() { + $reply = array(); + + $feed = $_REQUEST["feed"]; + $method = $_REQUEST["m"] ?? ""; + $view_mode = $_REQUEST["view_mode"] ?? ""; + $limit = 30; + $cat_view = $_REQUEST["cat"] == "true"; + $next_unread_feed = $_REQUEST["nuf"] ?? 0; + $offset = $_REQUEST["skip"] ?? 0; + $order_by = $_REQUEST["order_by"] ?? ""; + $check_first_id = $_REQUEST["fid"] ?? 0; + + if (is_numeric($feed)) $feed = (int) $feed; + + /* Feed -5 is a special case: it is used to display auxiliary information + * when there's nothing to load - e.g. no stuff in fresh feed */ + + if ($feed == -5) { + print json_encode($this->_generate_dashboard_feed()); + return; + } + + $sth = false; + if ($feed < LABEL_BASE_INDEX) { + + $label_feed = Labels::feed_to_label_id($feed); + + $sth = $this->pdo->prepare("SELECT id FROM ttrss_labels2 WHERE + id = ? AND owner_uid = ?"); + $sth->execute([$label_feed, $_SESSION['uid']]); + + } else if (!$cat_view && is_numeric($feed) && $feed > 0) { + + $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE + id = ? AND owner_uid = ?"); + $sth->execute([$feed, $_SESSION['uid']]); + + } else if ($cat_view && is_numeric($feed) && $feed > 0) { + + $sth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories WHERE + id = ? AND owner_uid = ?"); + + $sth->execute([$feed, $_SESSION['uid']]); + } + + if ($sth && !$sth->fetch()) { + print json_encode($this->_generate_error_feed(__("Feed not found."))); + return; + } + + set_pref(Prefs::_DEFAULT_VIEW_MODE, $view_mode); + set_pref(Prefs::_DEFAULT_VIEW_ORDER_BY, $order_by); + + /* bump login timestamp if needed */ + if (time() - $_SESSION["last_login_update"] > 3600) { + $user = ORM::for_table('ttrss_users')->find_one($_SESSION["uid"]); + $user->last_login = Db::NOW(); + $user->save(); + + $_SESSION["last_login_update"] = time(); + } + + if (!$cat_view && is_numeric($feed) && $feed > 0) { + $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET last_viewed = NOW() + WHERE id = ? AND owner_uid = ?"); + $sth->execute([$feed, $_SESSION['uid']]); + } + + $reply['headlines'] = []; + + list($override_order, $skip_first_id_check) = Feeds::_order_to_override_query($order_by); + + $ret = Feeds::_format_headlines_list($feed, $method, + $view_mode, $limit, $cat_view, $offset, + $override_order, true, $check_first_id, $skip_first_id_check, $order_by); + + $headlines_count = $ret[1]; + $disable_cache = $ret[3]; + $reply['headlines'] = $ret[4]; + + if (!$next_unread_feed) + $reply['headlines']['id'] = $feed; + else + $reply['headlines']['id'] = $next_unread_feed; + + $reply['headlines']['is_cat'] = $cat_view; + + $reply['headlines-info'] = ["count" => (int) $headlines_count, + "disable_cache" => (bool) $disable_cache]; + + // this is parsed by handleRpcJson() on first viewfeed() to set cdm expanded, etc + $reply['runtime-info'] = RPC::_make_runtime_info(); + + print json_encode($reply); + } + + private function _generate_dashboard_feed() { + $reply = array(); + + $reply['headlines']['id'] = -5; + $reply['headlines']['is_cat'] = false; + + $reply['headlines']['toolbar'] = ''; + + $reply['headlines']['content'] = "
".__('No feed selected.'); + + $reply['headlines']['content'] .= "

"; + + $sth = $this->pdo->prepare("SELECT ".SUBSTRING_FOR_DATE."(MAX(last_updated), 1, 19) AS last_updated FROM ttrss_feeds + WHERE owner_uid = ?"); + $sth->execute([$_SESSION['uid']]); + $row = $sth->fetch(); + + $last_updated = TimeHelper::make_local_datetime($row["last_updated"], false); + + $reply['headlines']['content'] .= sprintf(__("Feeds last updated at %s"), $last_updated); + + $sth = $this->pdo->prepare("SELECT COUNT(id) AS num_errors + FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?"); + $sth->execute([$_SESSION['uid']]); + $row = $sth->fetch(); + + $num_errors = $row["num_errors"]; + + if ($num_errors > 0) { + $reply['headlines']['content'] .= "
"; + $reply['headlines']['content'] .= "". + __('Some feeds have update errors (click for details)').""; + } + $reply['headlines']['content'] .= "

"; + + $reply['headlines-info'] = array("count" => 0, + "unread" => 0, + "disable_cache" => true); + + return $reply; + } + + private function _generate_error_feed($error) { + $reply = array(); + + $reply['headlines']['id'] = -7; + $reply['headlines']['is_cat'] = false; + + $reply['headlines']['toolbar'] = ''; + $reply['headlines']['content'] = "
". $error . "
"; + + $reply['headlines-info'] = array("count" => 0, + "unread" => 0, + "disable_cache" => true); + + return $reply; + } + + function subscribeToFeed() { + print json_encode([ + "cat_select" => \Controls\select_feeds_cats("cat") + ]); + } + + function search() { + print json_encode([ + "show_language" => Config::get(Config::DB_TYPE) == "pgsql", + "show_syntax_help" => count(PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH)) == 0, + "all_languages" => Pref_Feeds::get_ts_languages(), + "default_language" => get_pref(Prefs::DEFAULT_SEARCH_LANGUAGE) + ]); + } + + function updatedebugger() { + header("Content-type: text/html"); + + $xdebug = isset($_REQUEST["xdebug"]) ? (int)$_REQUEST["xdebug"] : 1; + + Debug::set_enabled(true); + Debug::set_loglevel($xdebug); + + $feed_id = (int)$_REQUEST["feed_id"]; + $do_update = ($_REQUEST["action"] ?? "") == "do_update"; + $csrf_token = $_POST["csrf_token"]; + + $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ? AND owner_uid = ?"); + $sth->execute([$feed_id, $_SESSION['uid']]); + + if (!$sth->fetch()) { + print "Access denied."; + return; + } + ?> + + + + Feed Debugger + + + + + + + + + + +
+

Feed Debugger: _get_title($feed_id) ?>

+
+
+ + + + + + +
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+
+
+ + + $rc)); + } + +} + diff --git a/classes/opml.php b/classes/handler/opml.php similarity index 99% rename from classes/opml.php rename to classes/handler/opml.php index 1a223788f..a07e8ebff 100644 --- a/classes/opml.php +++ b/classes/handler/opml.php @@ -1,5 +1,5 @@ total; $i++) { + if (isset($l10n->table_originals[$i * 2 + 2]) && $orig = $l10n->get_original_string($i)) { + if(strpos($orig, "\000") !== false) { // Plural forms + $key = explode(chr(0), $orig); + + $rv[$key[0]] = _ngettext($key[0], $key[1], 1); // Singular + $rv[$key[1]] = _ngettext($key[0], $key[1], 2); // Plural + } else { + $translation = _dgettext($domain,$orig); + $rv[$orig] = $translation; + } + } + } + } + + return $rv; + } + + + function togglepref() { + $key = clean($_REQUEST["key"]); + set_pref($key, !get_pref($key)); + $value = get_pref($key); + + print json_encode(array("param" =>$key, "value" => $value)); + } + + function setpref() { + // set_pref escapes input, so no need to double escape it here + $key = clean($_REQUEST['key']); + $value = $_REQUEST['value']; + + set_pref($key, $value, $_SESSION["uid"], $key != 'USER_STYLESHEET'); + + print json_encode(array("param" =>$key, "value" => $value)); + } + + function mark() { + $mark = clean($_REQUEST["mark"]); + $id = clean($_REQUEST["id"]); + + $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET marked = ?, + last_marked = NOW() + WHERE ref_id = ? AND owner_uid = ?"); + + $sth->execute([$mark, $id, $_SESSION['uid']]); + + print json_encode(array("message" => "UPDATE_COUNTERS")); + } + + function delete() { + $ids = explode(",", clean($_REQUEST["ids"])); + $ids_qmarks = arr_qmarks($ids); + + $sth = $this->pdo->prepare("DELETE FROM ttrss_user_entries + WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?"); + $sth->execute(array_merge($ids, [$_SESSION['uid']])); + + Article::_purge_orphans(); + + print json_encode(array("message" => "UPDATE_COUNTERS")); + } + + function publ() { + $pub = clean($_REQUEST["pub"]); + $id = clean($_REQUEST["id"]); + + $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET + published = ?, last_published = NOW() + WHERE ref_id = ? AND owner_uid = ?"); + + $sth->execute([$pub, $id, $_SESSION['uid']]); + + print json_encode(array("message" => "UPDATE_COUNTERS")); + } + + function getRuntimeInfo() { + $reply = [ + 'runtime-info' => $this->_make_runtime_info() + ]; + + print json_encode($reply); + } + + function getAllCounters() { + @$seq = (int) $_REQUEST['seq']; + + $feed_id_count = (int)$_REQUEST["feed_id_count"]; + $label_id_count = (int)$_REQUEST["label_id_count"]; + + // it seems impossible to distinguish empty array [] from a null - both become unset in $_REQUEST + // so, count is >= 0 means we had an array, -1 means null + // we need null because it means "return all counters"; [] would return nothing + if ($feed_id_count == -1) + $feed_ids = null; + else + $feed_ids = array_map("intval", clean($_REQUEST["feed_ids"] ?? [])); + + if ($label_id_count == -1) + $label_ids = null; + else + $label_ids = array_map("intval", clean($_REQUEST["label_ids"] ?? [])); + + $counters = is_array($feed_ids) && !get_pref(Prefs::DISABLE_CONDITIONAL_COUNTERS) ? + Counters::get_conditional($feed_ids, $label_ids) : Counters::get_all(); + + $reply = [ + 'counters' => $counters, + 'seq' => $seq + ]; + + print json_encode($reply); + } + + /* GET["cmode"] = 0 - mark as read, 1 - as unread, 2 - toggle */ + function catchupSelected() { + $ids = array_map("intval", clean($_REQUEST["ids"] ?? [])); + $cmode = (int)clean($_REQUEST["cmode"]); + + if (count($ids) > 0) + Article::_catchup_by_id($ids, $cmode); + + print json_encode(["message" => "UPDATE_COUNTERS", + "labels" => Article::_labels_of($ids), + "feeds" => Article::_feeds_of($ids)]); + } + + function markSelected() { + $ids = array_map("intval", clean($_REQUEST["ids"] ?? [])); + $cmode = (int)clean($_REQUEST["cmode"]); + + if (count($ids) > 0) + $this->markArticlesById($ids, $cmode); + + print json_encode(["message" => "UPDATE_COUNTERS", + "labels" => Article::_labels_of($ids), + "feeds" => Article::_feeds_of($ids)]); + } + + function publishSelected() { + $ids = array_map("intval", clean($_REQUEST["ids"] ?? [])); + $cmode = (int)clean($_REQUEST["cmode"]); + + if (count($ids) > 0) + $this->publishArticlesById($ids, $cmode); + + print json_encode(["message" => "UPDATE_COUNTERS", + "labels" => Article::_labels_of($ids), + "feeds" => Article::_feeds_of($ids)]); + } + + function sanityCheck() { + $_SESSION["hasSandbox"] = clean($_REQUEST["hasSandbox"]) === "true"; + $_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]); + + $client_location = $_REQUEST["clientLocation"]; + + $error = Errors::E_SUCCESS; + $error_params = []; + + $client_scheme = parse_url($client_location, PHP_URL_SCHEME); + $server_scheme = parse_url(Config::get_self_url(), PHP_URL_SCHEME); + + if (Db_Updater::is_update_required()) { + $error = Errors::E_SCHEMA_MISMATCH; + } else if ($client_scheme != $server_scheme) { + $error = Errors::E_URL_SCHEME_MISMATCH; + $error_params["client_scheme"] = $client_scheme; + $error_params["server_scheme"] = $server_scheme; + $error_params["self_url_path"] = Config::get_self_url(); + } + + if ($error == Errors::E_SUCCESS) { + $reply = []; + + $reply['init-params'] = $this->_make_init_params(); + $reply['runtime-info'] = $this->_make_runtime_info(); + $reply['translations'] = $this->_translations_as_array(); + + print json_encode($reply); + } else { + print Errors::to_json($error, $error_params); + } + } + + /*function completeLabels() { + $search = clean($_REQUEST["search"]); + + $sth = $this->pdo->prepare("SELECT DISTINCT caption FROM + ttrss_labels2 + WHERE owner_uid = ? AND + LOWER(caption) LIKE LOWER(?) ORDER BY caption + LIMIT 5"); + $sth->execute([$_SESSION['uid'], "%$search%"]); + + print "
    "; + while ($line = $sth->fetch()) { + print "
  • " . $line["caption"] . "
  • "; + } + print "
"; + }*/ + + function catchupFeed() { + $feed_id = clean($_REQUEST['feed_id']); + $is_cat = clean($_REQUEST['is_cat']) == "true"; + $mode = clean($_REQUEST['mode'] ?? ''); + $search_query = clean($_REQUEST['search_query']); + $search_lang = clean($_REQUEST['search_lang']); + + Feeds::_catchup($feed_id, $is_cat, false, $mode, [$search_query, $search_lang]); + + // return counters here synchronously so that frontend can figure out next unread feed properly + print json_encode(['counters' => Counters::get_all()]); + + //print json_encode(array("message" => "UPDATE_COUNTERS")); + } + + function setWidescreen() { + $wide = (int) clean($_REQUEST["wide"]); + + set_pref(Prefs::WIDESCREEN_MODE, $wide); + + print json_encode(["wide" => $wide]); + } + + static function updaterandomfeed_real() { + + $default_interval = (int) Prefs::get_default(Prefs::DEFAULT_UPDATE_INTERVAL); + + // Test if the feed need a update (update interval exceded). + if (Config::get(Config::DB_TYPE) == "pgsql") { + $update_limit_qpart = "AND (( + update_interval = 0 + AND (p.value IS NULL OR p.value != '-1') + AND last_updated < NOW() - CAST((COALESCE(p.value, '$default_interval') || ' minutes') AS INTERVAL) + ) OR ( + update_interval > 0 + AND last_updated < NOW() - CAST((update_interval || ' minutes') AS INTERVAL) + ) OR ( + update_interval >= 0 + AND (p.value IS NULL OR p.value != '-1') + AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL) + ))"; + } else { + $update_limit_qpart = "AND (( + update_interval = 0 + AND (p.value IS NULL OR p.value != '-1') + AND last_updated < DATE_SUB(NOW(), INTERVAL CONVERT(COALESCE(p.value, '$default_interval'), SIGNED INTEGER) MINUTE) + ) OR ( + update_interval > 0 + AND last_updated < DATE_SUB(NOW(), INTERVAL update_interval MINUTE) + ) OR ( + update_interval >= 0 + AND (p.value IS NULL OR p.value != '-1') + AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL) + ))"; + } + + // Test if feed is currently being updated by another process. + if (Config::get(Config::DB_TYPE) == "pgsql") { + $updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < NOW() - INTERVAL '5 minutes')"; + } else { + $updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < DATE_SUB(NOW(), INTERVAL 5 MINUTE))"; + } + + $random_qpart = Db::sql_random_function(); + + $pdo = Db::pdo(); + + // we could be invoked from public.php with no active session + if (!empty($_SESSION["uid"])) { + $owner_check_qpart = "AND f.owner_uid = ".$pdo->quote($_SESSION["uid"]); + } else { + $owner_check_qpart = ""; + } + + $query = "SELECT f.feed_url,f.id + FROM + ttrss_feeds f, ttrss_users u LEFT JOIN ttrss_user_prefs2 p ON + (p.owner_uid = u.id AND profile IS NULL AND pref_name = 'DEFAULT_UPDATE_INTERVAL') + WHERE + f.owner_uid = u.id + $owner_check_qpart + $update_limit_qpart + $updstart_thresh_qpart + ORDER BY $random_qpart LIMIT 30"; + + $res = $pdo->query($query); + + $num_updated = 0; + + $tstart = time(); + + while ($line = $res->fetch()) { + $feed_id = $line["id"]; + + if (time() - $tstart < ini_get("max_execution_time") * 0.7) { + RSSUtils::update_rss_feed($feed_id, true); + ++$num_updated; + } else { + break; + } + } + + // Purge orphans and cleanup tags + Article::_purge_orphans(); + //cleanup_tags(14, 50000); + + if ($num_updated > 0) { + print json_encode(array("message" => "UPDATE_COUNTERS", + "num_updated" => $num_updated)); + } else { + print json_encode(array("message" => "NOTHING_TO_UPDATE")); + } + + } + + function updaterandomfeed() { + self::updaterandomfeed_real(); + } + + private function markArticlesById($ids, $cmode) { + + $ids_qmarks = arr_qmarks($ids); + + if ($cmode == 0) { + $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) { + $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET + marked = true, last_marked = NOW() + WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?"); + } else { + $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET + marked = NOT marked,last_marked = NOW() + WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?"); + } + + $sth->execute(array_merge($ids, [$_SESSION['uid']])); + } + + private function publishArticlesById($ids, $cmode) { + + $ids_qmarks = arr_qmarks($ids); + + if ($cmode == 0) { + $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) { + $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET + published = true, last_published = NOW() + WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?"); + } else { + $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET + published = NOT published,last_published = NOW() + WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?"); + } + + $sth->execute(array_merge($ids, [$_SESSION['uid']])); + } + + function log() { + $msg = clean($_REQUEST['msg']); + $file = basename(clean($_REQUEST['file'])); + $line = (int) clean($_REQUEST['line']); + $context = clean($_REQUEST['context']); + + if ($msg) { + Logger::log_error(E_USER_WARNING, + $msg, 'client-js:' . $file, $line, $context); + + echo json_encode(array("message" => "HOST_ERROR_LOGGED")); + } + } + + function checkforupdates() { + $rv = ["changeset" => [], "plugins" => []]; + + $version = Config::get_version(false); + + $git_timestamp = $version["timestamp"] ?? false; + $git_commit = $version["commit"] ?? false; + + if (Config::get(Config::CHECK_FOR_UPDATES) && $_SESSION["access_level"] >= 10 && $git_timestamp) { + $content = @UrlHelper::fetch(["url" => "https://tt-rss.org/version.json"]); + + if ($content) { + $content = json_decode($content, true); + + if ($content && isset($content["changeset"])) { + if ($git_timestamp < (int)$content["changeset"]["timestamp"] && + $git_commit != $content["changeset"]["id"]) { + + $rv["changeset"] = $content["changeset"]; + } + } + } + + $rv["plugins"] = Pref_Prefs::_get_updated_plugins(); + } + + print json_encode($rv); + } + + private function _make_init_params() { + $params = array(); + + foreach ([Prefs::ON_CATCHUP_SHOW_NEXT_FEED, Prefs::HIDE_READ_FEEDS, + Prefs::ENABLE_FEED_CATS, Prefs::FEEDS_SORT_BY_UNREAD, + Prefs::CONFIRM_FEED_CATCHUP, Prefs::CDM_AUTO_CATCHUP, + Prefs::FRESH_ARTICLE_MAX_AGE, Prefs::HIDE_READ_SHOWS_SPECIAL, + Prefs::COMBINED_DISPLAY_MODE, Prefs::DEBUG_HEADLINE_IDS] as $param) { + + $params[strtolower($param)] = (int) get_pref($param); + } + + $params["safe_mode"] = !empty($_SESSION["safe_mode"]); + $params["check_for_updates"] = Config::get(Config::CHECK_FOR_UPDATES); + $params["icons_url"] = Config::get(Config::ICONS_URL); + $params["cookie_lifetime"] = Config::get(Config::SESSION_COOKIE_LIFETIME); + $params["default_view_mode"] = get_pref(Prefs::_DEFAULT_VIEW_MODE); + $params["default_view_limit"] = (int) get_pref(Prefs::_DEFAULT_VIEW_LIMIT); + $params["default_view_order_by"] = get_pref(Prefs::_DEFAULT_VIEW_ORDER_BY); + $params["bw_limit"] = (int) $_SESSION["bw_limit"]; + $params["is_default_pw"] = UserHelper::is_default_password(); + $params["label_base_index"] = LABEL_BASE_INDEX; + + $theme = get_pref(Prefs::USER_CSS_THEME); + $params["theme"] = theme_exists($theme) ? $theme : ""; + + $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names()); + + $params["php_platform"] = PHP_OS; + $params["php_version"] = PHP_VERSION; + + $pdo = Db::pdo(); + + $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM + ttrss_feeds WHERE owner_uid = ?"); + $sth->execute([$_SESSION['uid']]); + $row = $sth->fetch(); + + $max_feed_id = $row["mid"]; + $num_feeds = $row["nf"]; + + $params["self_url_prefix"] = Config::get_self_url(); + $params["max_feed_id"] = (int) $max_feed_id; + $params["num_feeds"] = (int) $num_feeds; + $params["hotkeys"] = $this->get_hotkeys_map(); + $params["widescreen"] = (int) get_pref(Prefs::WIDESCREEN_MODE); + $params['simple_update'] = Config::get(Config::SIMPLE_UPDATE_MODE); + $params["icon_indicator_white"] = $this->image_to_base64("images/indicator_white.gif"); + $params["labels"] = Labels::get_all($_SESSION["uid"]); + + return $params; + } + + private function image_to_base64($filename) { + if (file_exists($filename)) { + $ext = pathinfo($filename, PATHINFO_EXTENSION); + + return "data:image/$ext;base64," . base64_encode((string)file_get_contents($filename)); + } else { + return ""; + } + } + + static function _make_runtime_info() { + $data = array(); + + $pdo = Db::pdo(); + + $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM + ttrss_feeds WHERE owner_uid = ?"); + $sth->execute([$_SESSION['uid']]); + $row = $sth->fetch(); + + $max_feed_id = $row['mid']; + $num_feeds = $row['nf']; + + $data["max_feed_id"] = (int) $max_feed_id; + $data["num_feeds"] = (int) $num_feeds; + $data['cdm_expanded'] = get_pref(Prefs::CDM_EXPANDED); + $data["labels"] = Labels::get_all($_SESSION["uid"]); + + if (Config::get(Config::LOG_DESTINATION) == 'sql' && $_SESSION['access_level'] >= 10) { + if (Config::get(Config::DB_TYPE) == 'pgsql') { + $log_interval = "created_at > NOW() - interval '1 hour'"; + } else { + $log_interval = "created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)"; + } + + $sth = $pdo->prepare("SELECT COUNT(id) AS cid + FROM ttrss_error_log + WHERE + errno NOT IN (".E_USER_NOTICE.", ".E_USER_DEPRECATED.") AND + $log_interval AND + errstr NOT LIKE '%imagecreatefromstring(): Data is not in a recognized format%'"); + $sth->execute(); + + if ($row = $sth->fetch()) { + $data['recent_log_events'] = $row['cid']; + } + } + + if (file_exists(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon.lock")) { + + $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock"); + + if (time() - ($_SESSION["daemon_stamp_check"] ?? 0) > 30) { + + $stamp = (int) @file_get_contents(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon.stamp"); + + if ($stamp) { + $stamp_delta = time() - $stamp; + + if ($stamp_delta > 1800) { + $stamp_check = 0; + } else { + $stamp_check = 1; + $_SESSION["daemon_stamp_check"] = time(); + } + + $data['daemon_stamp_ok'] = $stamp_check; + + $stamp_fmt = date("Y.m.d, G:i", $stamp); + + $data['daemon_stamp'] = $stamp_fmt; + } + } + } + + return $data; + } + + static function get_hotkeys_info() { + $hotkeys = array( + __("Navigation") => array( + "next_feed" => __("Open next feed"), + "prev_feed" => __("Open previous feed"), + "next_article_or_scroll" => __("Open next article (in combined mode, scroll down)"), + "prev_article_or_scroll" => __("Open previous article (in combined mode, scroll up)"), + "next_headlines_page" => __("Scroll headlines by one page down"), + "prev_headlines_page" => __("Scroll headlines by one page up"), + "next_article_noscroll" => __("Open next article"), + "prev_article_noscroll" => __("Open previous article"), + "next_article_noexpand" => __("Move to next article (don't expand)"), + "prev_article_noexpand" => __("Move to previous article (don't expand)"), + "search_dialog" => __("Show search dialog"), + "cancel_search" => __("Cancel active search")), + __("Article") => array( + "toggle_mark" => __("Toggle starred"), + "toggle_publ" => __("Toggle published"), + "toggle_unread" => __("Toggle unread"), + "edit_tags" => __("Edit tags"), + "open_in_new_window" => __("Open in new window"), + "catchup_below" => __("Mark below as read"), + "catchup_above" => __("Mark above as read"), + "article_scroll_down" => __("Scroll down"), + "article_scroll_up" => __("Scroll up"), + "article_page_down" => __("Scroll down page"), + "article_page_up" => __("Scroll up page"), + "select_article_cursor" => __("Select article under cursor"), + "email_article" => __("Email article"), + "close_article" => __("Close/collapse article"), + "toggle_expand" => __("Toggle article expansion (combined mode)"), + "toggle_widescreen" => __("Toggle widescreen mode"), + "toggle_full_text" => __("Toggle full article text via Readability")), + __("Article selection") => array( + "select_all" => __("Select all articles"), + "select_unread" => __("Select unread"), + "select_marked" => __("Select starred"), + "select_published" => __("Select published"), + "select_invert" => __("Invert selection"), + "select_none" => __("Deselect everything")), + __("Feed") => array( + "feed_refresh" => __("Refresh current feed"), + "feed_unhide_read" => __("Un/hide read feeds"), + "feed_subscribe" => __("Subscribe to feed"), + "feed_edit" => __("Edit feed"), + "feed_catchup" => __("Mark as read"), + "feed_reverse" => __("Reverse headlines"), + "feed_toggle_vgroup" => __("Toggle headline grouping"), + "feed_debug_update" => __("Debug feed update"), + "feed_debug_viewfeed" => __("Debug viewfeed()"), + "catchup_all" => __("Mark all feeds as read"), + "cat_toggle_collapse" => __("Un/collapse current category"), + "toggle_cdm_expanded" => __("Toggle auto expand in combined mode"), + "toggle_combined_mode" => __("Toggle combined mode")), + __("Go to") => array( + "goto_all" => __("All articles"), + "goto_fresh" => __("Fresh"), + "goto_marked" => __("Starred"), + "goto_published" => __("Published"), + "goto_read" => __("Recently read"), + "goto_prefs" => __("Preferences")), + __("Other") => array( + "create_label" => __("Create label"), + "create_filter" => __("Create filter"), + "collapse_sidebar" => __("Un/collapse sidebar"), + "help_dialog" => __("Show help dialog")) + ); + + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HOTKEY_INFO, + function ($result) use (&$hotkeys) { + $hotkeys = $result; + }, + $hotkeys); + + return $hotkeys; + } + + // {3} - 3 panel mode only + // {C} - combined mode only + static function get_hotkeys_map() { + $hotkeys = array( + "k" => "next_feed", + "j" => "prev_feed", + "n" => "next_article_noscroll", + "p" => "prev_article_noscroll", + "N" => "article_page_down", + "P" => "article_page_up", + "*(33)|Shift+PgUp" => "article_page_up", + "*(34)|Shift+PgDn" => "article_page_down", + "{3}(38)|Up" => "prev_article_or_scroll", + "{3}(40)|Down" => "next_article_or_scroll", + "*(38)|Shift+Up" => "article_scroll_up", + "*(40)|Shift+Down" => "article_scroll_down", + "^(38)|Ctrl+Up" => "prev_article_noscroll", + "^(40)|Ctrl+Down" => "next_article_noscroll", + "/" => "search_dialog", + "\\" => "cancel_search", + "s" => "toggle_mark", + "S" => "toggle_publ", + "u" => "toggle_unread", + "T" => "edit_tags", + "o" => "open_in_new_window", + "c p" => "catchup_below", + "c n" => "catchup_above", + "a W" => "toggle_widescreen", + "a e" => "toggle_full_text", + "e" => "email_article", + "a q" => "close_article", + "a a" => "select_all", + "a u" => "select_unread", + "a U" => "select_marked", + "a p" => "select_published", + "a i" => "select_invert", + "a n" => "select_none", + "f r" => "feed_refresh", + "f a" => "feed_unhide_read", + "f s" => "feed_subscribe", + "f e" => "feed_edit", + "f q" => "feed_catchup", + "f x" => "feed_reverse", + "f g" => "feed_toggle_vgroup", + "f D" => "feed_debug_update", + "f G" => "feed_debug_viewfeed", + "f C" => "toggle_combined_mode", + "f c" => "toggle_cdm_expanded", + "Q" => "catchup_all", + "x" => "cat_toggle_collapse", + "g a" => "goto_all", + "g f" => "goto_fresh", + "g s" => "goto_marked", + "g p" => "goto_published", + "g r" => "goto_read", + "g P" => "goto_prefs", + "r" => "select_article_cursor", + "c l" => "create_label", + "c f" => "create_filter", + "c s" => "collapse_sidebar", + "?" => "help_dialog", + ); + + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HOTKEY_MAP, + function ($result) use (&$hotkeys) { + $hotkeys = $result; + }, + $hotkeys); + + $prefixes = array(); + + foreach (array_keys($hotkeys) as $hotkey) { + $pair = explode(" ", (string)$hotkey, 2); + + if (count($pair) > 1 && !in_array($pair[0], $prefixes)) { + array_push($prefixes, $pair[0]); + } + } + + return array($prefixes, $hotkeys); + } + + function hotkeyHelp() { + $info = self::get_hotkeys_info(); + $imap = self::get_hotkeys_map(); + $omap = array(); + + foreach ($imap[1] as $sequence => $action) { + if (!isset($omap[$action])) $omap[$action] = array(); + + array_push($omap[$action], $sequence); + } + + ?> +
    + $hotkeys) { + ?> +
  • + $description) { + + if (!empty($omap[$action])) { + foreach ($omap[$action] as $sequence) { + if (strpos($sequence, "|") !== false) { + $sequence = substr($sequence, + strpos($sequence, "|")+1, + strlen($sequence)); + } else { + $keys = explode(" ", $sequence); + + for ($i = 0; $i < count($keys); $i++) { + if (strlen($keys[$i]) > 1) { + $tmp = ''; + foreach (str_split($keys[$i]) as $c) { + switch ($c) { + case '*': + $tmp .= __('Shift') . '+'; + break; + case '^': + $tmp .= __('Ctrl') . '+'; + break; + default: + $tmp .= $c; + } + } + $keys[$i] = $tmp; + } + } + $sequence = join(" ", $keys); + } + + ?> +
  • +
    +
    +
  • + +
+
+ +
+ total; $i++) { - if (isset($l10n->table_originals[$i * 2 + 2]) && $orig = $l10n->get_original_string($i)) { - if(strpos($orig, "\000") !== false) { // Plural forms - $key = explode(chr(0), $orig); - - $rv[$key[0]] = _ngettext($key[0], $key[1], 1); // Singular - $rv[$key[1]] = _ngettext($key[0], $key[1], 2); // Plural - } else { - $translation = _dgettext($domain,$orig); - $rv[$orig] = $translation; - } - } - } - } - - return $rv; - } - - - function togglepref() { - $key = clean($_REQUEST["key"]); - set_pref($key, !get_pref($key)); - $value = get_pref($key); - - print json_encode(array("param" =>$key, "value" => $value)); - } - - function setpref() { - // set_pref escapes input, so no need to double escape it here - $key = clean($_REQUEST['key']); - $value = $_REQUEST['value']; - - set_pref($key, $value, $_SESSION["uid"], $key != 'USER_STYLESHEET'); - - print json_encode(array("param" =>$key, "value" => $value)); - } - - function mark() { - $mark = clean($_REQUEST["mark"]); - $id = clean($_REQUEST["id"]); - - $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET marked = ?, - last_marked = NOW() - WHERE ref_id = ? AND owner_uid = ?"); - - $sth->execute([$mark, $id, $_SESSION['uid']]); - - print json_encode(array("message" => "UPDATE_COUNTERS")); - } - - function delete() { - $ids = explode(",", clean($_REQUEST["ids"])); - $ids_qmarks = arr_qmarks($ids); - - $sth = $this->pdo->prepare("DELETE FROM ttrss_user_entries - WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?"); - $sth->execute(array_merge($ids, [$_SESSION['uid']])); - - Article::_purge_orphans(); - - print json_encode(array("message" => "UPDATE_COUNTERS")); - } - - function publ() { - $pub = clean($_REQUEST["pub"]); - $id = clean($_REQUEST["id"]); - - $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET - published = ?, last_published = NOW() - WHERE ref_id = ? AND owner_uid = ?"); - - $sth->execute([$pub, $id, $_SESSION['uid']]); - - print json_encode(array("message" => "UPDATE_COUNTERS")); - } - - function getRuntimeInfo() { - $reply = [ - 'runtime-info' => $this->_make_runtime_info() - ]; - - print json_encode($reply); - } - - function getAllCounters() { - @$seq = (int) $_REQUEST['seq']; - - $feed_id_count = (int)$_REQUEST["feed_id_count"]; - $label_id_count = (int)$_REQUEST["label_id_count"]; - - // it seems impossible to distinguish empty array [] from a null - both become unset in $_REQUEST - // so, count is >= 0 means we had an array, -1 means null - // we need null because it means "return all counters"; [] would return nothing - if ($feed_id_count == -1) - $feed_ids = null; - else - $feed_ids = array_map("intval", clean($_REQUEST["feed_ids"] ?? [])); - - if ($label_id_count == -1) - $label_ids = null; - else - $label_ids = array_map("intval", clean($_REQUEST["label_ids"] ?? [])); - - $counters = is_array($feed_ids) && !get_pref(Prefs::DISABLE_CONDITIONAL_COUNTERS) ? - Counters::get_conditional($feed_ids, $label_ids) : Counters::get_all(); - - $reply = [ - 'counters' => $counters, - 'seq' => $seq - ]; - - print json_encode($reply); - } - - /* GET["cmode"] = 0 - mark as read, 1 - as unread, 2 - toggle */ - function catchupSelected() { - $ids = array_map("intval", clean($_REQUEST["ids"] ?? [])); - $cmode = (int)clean($_REQUEST["cmode"]); - - if (count($ids) > 0) - Article::_catchup_by_id($ids, $cmode); - - print json_encode(["message" => "UPDATE_COUNTERS", - "labels" => Article::_labels_of($ids), - "feeds" => Article::_feeds_of($ids)]); - } - - function markSelected() { - $ids = array_map("intval", clean($_REQUEST["ids"] ?? [])); - $cmode = (int)clean($_REQUEST["cmode"]); - - if (count($ids) > 0) - $this->markArticlesById($ids, $cmode); - - print json_encode(["message" => "UPDATE_COUNTERS", - "labels" => Article::_labels_of($ids), - "feeds" => Article::_feeds_of($ids)]); - } - - function publishSelected() { - $ids = array_map("intval", clean($_REQUEST["ids"] ?? [])); - $cmode = (int)clean($_REQUEST["cmode"]); - - if (count($ids) > 0) - $this->publishArticlesById($ids, $cmode); - - print json_encode(["message" => "UPDATE_COUNTERS", - "labels" => Article::_labels_of($ids), - "feeds" => Article::_feeds_of($ids)]); - } - - function sanityCheck() { - $_SESSION["hasSandbox"] = clean($_REQUEST["hasSandbox"]) === "true"; - $_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]); - - $client_location = $_REQUEST["clientLocation"]; - - $error = Errors::E_SUCCESS; - $error_params = []; - - $client_scheme = parse_url($client_location, PHP_URL_SCHEME); - $server_scheme = parse_url(Config::get_self_url(), PHP_URL_SCHEME); - - if (Db_Updater::is_update_required()) { - $error = Errors::E_SCHEMA_MISMATCH; - } else if ($client_scheme != $server_scheme) { - $error = Errors::E_URL_SCHEME_MISMATCH; - $error_params["client_scheme"] = $client_scheme; - $error_params["server_scheme"] = $server_scheme; - $error_params["self_url_path"] = Config::get_self_url(); - } - - if ($error == Errors::E_SUCCESS) { - $reply = []; - - $reply['init-params'] = $this->_make_init_params(); - $reply['runtime-info'] = $this->_make_runtime_info(); - $reply['translations'] = $this->_translations_as_array(); - - print json_encode($reply); - } else { - print Errors::to_json($error, $error_params); - } - } - - /*function completeLabels() { - $search = clean($_REQUEST["search"]); - - $sth = $this->pdo->prepare("SELECT DISTINCT caption FROM - ttrss_labels2 - WHERE owner_uid = ? AND - LOWER(caption) LIKE LOWER(?) ORDER BY caption - LIMIT 5"); - $sth->execute([$_SESSION['uid'], "%$search%"]); - - print "
    "; - while ($line = $sth->fetch()) { - print "
  • " . $line["caption"] . "
  • "; - } - print "
"; - }*/ - - function catchupFeed() { - $feed_id = clean($_REQUEST['feed_id']); - $is_cat = clean($_REQUEST['is_cat']) == "true"; - $mode = clean($_REQUEST['mode'] ?? ''); - $search_query = clean($_REQUEST['search_query']); - $search_lang = clean($_REQUEST['search_lang']); - - Feeds::_catchup($feed_id, $is_cat, false, $mode, [$search_query, $search_lang]); - - // return counters here synchronously so that frontend can figure out next unread feed properly - print json_encode(['counters' => Counters::get_all()]); - - //print json_encode(array("message" => "UPDATE_COUNTERS")); - } - - function setWidescreen() { - $wide = (int) clean($_REQUEST["wide"]); - - set_pref(Prefs::WIDESCREEN_MODE, $wide); - - print json_encode(["wide" => $wide]); - } - - static function updaterandomfeed_real() { - - $default_interval = (int) Prefs::get_default(Prefs::DEFAULT_UPDATE_INTERVAL); - - // Test if the feed need a update (update interval exceded). - if (Config::get(Config::DB_TYPE) == "pgsql") { - $update_limit_qpart = "AND (( - update_interval = 0 - AND (p.value IS NULL OR p.value != '-1') - AND last_updated < NOW() - CAST((COALESCE(p.value, '$default_interval') || ' minutes') AS INTERVAL) - ) OR ( - update_interval > 0 - AND last_updated < NOW() - CAST((update_interval || ' minutes') AS INTERVAL) - ) OR ( - update_interval >= 0 - AND (p.value IS NULL OR p.value != '-1') - AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL) - ))"; - } else { - $update_limit_qpart = "AND (( - update_interval = 0 - AND (p.value IS NULL OR p.value != '-1') - AND last_updated < DATE_SUB(NOW(), INTERVAL CONVERT(COALESCE(p.value, '$default_interval'), SIGNED INTEGER) MINUTE) - ) OR ( - update_interval > 0 - AND last_updated < DATE_SUB(NOW(), INTERVAL update_interval MINUTE) - ) OR ( - update_interval >= 0 - AND (p.value IS NULL OR p.value != '-1') - AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL) - ))"; - } - - // Test if feed is currently being updated by another process. - if (Config::get(Config::DB_TYPE) == "pgsql") { - $updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < NOW() - INTERVAL '5 minutes')"; - } else { - $updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < DATE_SUB(NOW(), INTERVAL 5 MINUTE))"; - } - - $random_qpart = Db::sql_random_function(); - - $pdo = Db::pdo(); - - // we could be invoked from public.php with no active session - if (!empty($_SESSION["uid"])) { - $owner_check_qpart = "AND f.owner_uid = ".$pdo->quote($_SESSION["uid"]); - } else { - $owner_check_qpart = ""; - } - - $query = "SELECT f.feed_url,f.id - FROM - ttrss_feeds f, ttrss_users u LEFT JOIN ttrss_user_prefs2 p ON - (p.owner_uid = u.id AND profile IS NULL AND pref_name = 'DEFAULT_UPDATE_INTERVAL') - WHERE - f.owner_uid = u.id - $owner_check_qpart - $update_limit_qpart - $updstart_thresh_qpart - ORDER BY $random_qpart LIMIT 30"; - - $res = $pdo->query($query); - - $num_updated = 0; - - $tstart = time(); - - while ($line = $res->fetch()) { - $feed_id = $line["id"]; - - if (time() - $tstart < ini_get("max_execution_time") * 0.7) { - RSSUtils::update_rss_feed($feed_id, true); - ++$num_updated; - } else { - break; - } - } - - // Purge orphans and cleanup tags - Article::_purge_orphans(); - //cleanup_tags(14, 50000); - - if ($num_updated > 0) { - print json_encode(array("message" => "UPDATE_COUNTERS", - "num_updated" => $num_updated)); - } else { - print json_encode(array("message" => "NOTHING_TO_UPDATE")); - } - - } - - function updaterandomfeed() { - self::updaterandomfeed_real(); - } - - private function markArticlesById($ids, $cmode) { - - $ids_qmarks = arr_qmarks($ids); - - if ($cmode == 0) { - $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) { - $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET - marked = true, last_marked = NOW() - WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?"); - } else { - $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET - marked = NOT marked,last_marked = NOW() - WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?"); - } - - $sth->execute(array_merge($ids, [$_SESSION['uid']])); - } - - private function publishArticlesById($ids, $cmode) { - - $ids_qmarks = arr_qmarks($ids); - - if ($cmode == 0) { - $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) { - $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET - published = true, last_published = NOW() - WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?"); - } else { - $sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET - published = NOT published,last_published = NOW() - WHERE ref_id IN ($ids_qmarks) AND owner_uid = ?"); - } - - $sth->execute(array_merge($ids, [$_SESSION['uid']])); - } - - function log() { - $msg = clean($_REQUEST['msg']); - $file = basename(clean($_REQUEST['file'])); - $line = (int) clean($_REQUEST['line']); - $context = clean($_REQUEST['context']); - - if ($msg) { - Logger::log_error(E_USER_WARNING, - $msg, 'client-js:' . $file, $line, $context); - - echo json_encode(array("message" => "HOST_ERROR_LOGGED")); - } - } - - function checkforupdates() { - $rv = ["changeset" => [], "plugins" => []]; - - $version = Config::get_version(false); - - $git_timestamp = $version["timestamp"] ?? false; - $git_commit = $version["commit"] ?? false; - - if (Config::get(Config::CHECK_FOR_UPDATES) && $_SESSION["access_level"] >= 10 && $git_timestamp) { - $content = @UrlHelper::fetch(["url" => "https://tt-rss.org/version.json"]); - - if ($content) { - $content = json_decode($content, true); - - if ($content && isset($content["changeset"])) { - if ($git_timestamp < (int)$content["changeset"]["timestamp"] && - $git_commit != $content["changeset"]["id"]) { - - $rv["changeset"] = $content["changeset"]; - } - } - } - - $rv["plugins"] = Pref_Prefs::_get_updated_plugins(); - } - - print json_encode($rv); - } - - private function _make_init_params() { - $params = array(); - - foreach ([Prefs::ON_CATCHUP_SHOW_NEXT_FEED, Prefs::HIDE_READ_FEEDS, - Prefs::ENABLE_FEED_CATS, Prefs::FEEDS_SORT_BY_UNREAD, - Prefs::CONFIRM_FEED_CATCHUP, Prefs::CDM_AUTO_CATCHUP, - Prefs::FRESH_ARTICLE_MAX_AGE, Prefs::HIDE_READ_SHOWS_SPECIAL, - Prefs::COMBINED_DISPLAY_MODE, Prefs::DEBUG_HEADLINE_IDS] as $param) { - - $params[strtolower($param)] = (int) get_pref($param); - } - - $params["safe_mode"] = !empty($_SESSION["safe_mode"]); - $params["check_for_updates"] = Config::get(Config::CHECK_FOR_UPDATES); - $params["icons_url"] = Config::get(Config::ICONS_URL); - $params["cookie_lifetime"] = Config::get(Config::SESSION_COOKIE_LIFETIME); - $params["default_view_mode"] = get_pref(Prefs::_DEFAULT_VIEW_MODE); - $params["default_view_limit"] = (int) get_pref(Prefs::_DEFAULT_VIEW_LIMIT); - $params["default_view_order_by"] = get_pref(Prefs::_DEFAULT_VIEW_ORDER_BY); - $params["bw_limit"] = (int) $_SESSION["bw_limit"]; - $params["is_default_pw"] = UserHelper::is_default_password(); - $params["label_base_index"] = LABEL_BASE_INDEX; - - $theme = get_pref(Prefs::USER_CSS_THEME); - $params["theme"] = theme_exists($theme) ? $theme : ""; - - $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names()); - - $params["php_platform"] = PHP_OS; - $params["php_version"] = PHP_VERSION; - - $pdo = Db::pdo(); - - $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM - ttrss_feeds WHERE owner_uid = ?"); - $sth->execute([$_SESSION['uid']]); - $row = $sth->fetch(); - - $max_feed_id = $row["mid"]; - $num_feeds = $row["nf"]; - - $params["self_url_prefix"] = Config::get_self_url(); - $params["max_feed_id"] = (int) $max_feed_id; - $params["num_feeds"] = (int) $num_feeds; - $params["hotkeys"] = $this->get_hotkeys_map(); - $params["widescreen"] = (int) get_pref(Prefs::WIDESCREEN_MODE); - $params['simple_update'] = Config::get(Config::SIMPLE_UPDATE_MODE); - $params["icon_indicator_white"] = $this->image_to_base64("images/indicator_white.gif"); - $params["labels"] = Labels::get_all($_SESSION["uid"]); - - return $params; - } - - private function image_to_base64($filename) { - if (file_exists($filename)) { - $ext = pathinfo($filename, PATHINFO_EXTENSION); - - return "data:image/$ext;base64," . base64_encode((string)file_get_contents($filename)); - } else { - return ""; - } - } - - static function _make_runtime_info() { - $data = array(); - - $pdo = Db::pdo(); - - $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM - ttrss_feeds WHERE owner_uid = ?"); - $sth->execute([$_SESSION['uid']]); - $row = $sth->fetch(); - - $max_feed_id = $row['mid']; - $num_feeds = $row['nf']; - - $data["max_feed_id"] = (int) $max_feed_id; - $data["num_feeds"] = (int) $num_feeds; - $data['cdm_expanded'] = get_pref(Prefs::CDM_EXPANDED); - $data["labels"] = Labels::get_all($_SESSION["uid"]); - - if (Config::get(Config::LOG_DESTINATION) == 'sql' && $_SESSION['access_level'] >= 10) { - if (Config::get(Config::DB_TYPE) == 'pgsql') { - $log_interval = "created_at > NOW() - interval '1 hour'"; - } else { - $log_interval = "created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)"; - } - - $sth = $pdo->prepare("SELECT COUNT(id) AS cid - FROM ttrss_error_log - WHERE - errno NOT IN (".E_USER_NOTICE.", ".E_USER_DEPRECATED.") AND - $log_interval AND - errstr NOT LIKE '%imagecreatefromstring(): Data is not in a recognized format%'"); - $sth->execute(); - - if ($row = $sth->fetch()) { - $data['recent_log_events'] = $row['cid']; - } - } - - if (file_exists(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon.lock")) { - - $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock"); - - if (time() - ($_SESSION["daemon_stamp_check"] ?? 0) > 30) { - - $stamp = (int) @file_get_contents(Config::get(Config::LOCK_DIRECTORY) . "/update_daemon.stamp"); - - if ($stamp) { - $stamp_delta = time() - $stamp; - - if ($stamp_delta > 1800) { - $stamp_check = 0; - } else { - $stamp_check = 1; - $_SESSION["daemon_stamp_check"] = time(); - } - - $data['daemon_stamp_ok'] = $stamp_check; - - $stamp_fmt = date("Y.m.d, G:i", $stamp); - - $data['daemon_stamp'] = $stamp_fmt; - } - } - } - - return $data; - } - - static function get_hotkeys_info() { - $hotkeys = array( - __("Navigation") => array( - "next_feed" => __("Open next feed"), - "prev_feed" => __("Open previous feed"), - "next_article_or_scroll" => __("Open next article (in combined mode, scroll down)"), - "prev_article_or_scroll" => __("Open previous article (in combined mode, scroll up)"), - "next_headlines_page" => __("Scroll headlines by one page down"), - "prev_headlines_page" => __("Scroll headlines by one page up"), - "next_article_noscroll" => __("Open next article"), - "prev_article_noscroll" => __("Open previous article"), - "next_article_noexpand" => __("Move to next article (don't expand)"), - "prev_article_noexpand" => __("Move to previous article (don't expand)"), - "search_dialog" => __("Show search dialog"), - "cancel_search" => __("Cancel active search")), - __("Article") => array( - "toggle_mark" => __("Toggle starred"), - "toggle_publ" => __("Toggle published"), - "toggle_unread" => __("Toggle unread"), - "edit_tags" => __("Edit tags"), - "open_in_new_window" => __("Open in new window"), - "catchup_below" => __("Mark below as read"), - "catchup_above" => __("Mark above as read"), - "article_scroll_down" => __("Scroll down"), - "article_scroll_up" => __("Scroll up"), - "article_page_down" => __("Scroll down page"), - "article_page_up" => __("Scroll up page"), - "select_article_cursor" => __("Select article under cursor"), - "email_article" => __("Email article"), - "close_article" => __("Close/collapse article"), - "toggle_expand" => __("Toggle article expansion (combined mode)"), - "toggle_widescreen" => __("Toggle widescreen mode"), - "toggle_full_text" => __("Toggle full article text via Readability")), - __("Article selection") => array( - "select_all" => __("Select all articles"), - "select_unread" => __("Select unread"), - "select_marked" => __("Select starred"), - "select_published" => __("Select published"), - "select_invert" => __("Invert selection"), - "select_none" => __("Deselect everything")), - __("Feed") => array( - "feed_refresh" => __("Refresh current feed"), - "feed_unhide_read" => __("Un/hide read feeds"), - "feed_subscribe" => __("Subscribe to feed"), - "feed_edit" => __("Edit feed"), - "feed_catchup" => __("Mark as read"), - "feed_reverse" => __("Reverse headlines"), - "feed_toggle_vgroup" => __("Toggle headline grouping"), - "feed_debug_update" => __("Debug feed update"), - "feed_debug_viewfeed" => __("Debug viewfeed()"), - "catchup_all" => __("Mark all feeds as read"), - "cat_toggle_collapse" => __("Un/collapse current category"), - "toggle_cdm_expanded" => __("Toggle auto expand in combined mode"), - "toggle_combined_mode" => __("Toggle combined mode")), - __("Go to") => array( - "goto_all" => __("All articles"), - "goto_fresh" => __("Fresh"), - "goto_marked" => __("Starred"), - "goto_published" => __("Published"), - "goto_read" => __("Recently read"), - "goto_prefs" => __("Preferences")), - __("Other") => array( - "create_label" => __("Create label"), - "create_filter" => __("Create filter"), - "collapse_sidebar" => __("Un/collapse sidebar"), - "help_dialog" => __("Show help dialog")) - ); - - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HOTKEY_INFO, - function ($result) use (&$hotkeys) { - $hotkeys = $result; - }, - $hotkeys); - - return $hotkeys; - } - - // {3} - 3 panel mode only - // {C} - combined mode only - static function get_hotkeys_map() { - $hotkeys = array( - "k" => "next_feed", - "j" => "prev_feed", - "n" => "next_article_noscroll", - "p" => "prev_article_noscroll", - "N" => "article_page_down", - "P" => "article_page_up", - "*(33)|Shift+PgUp" => "article_page_up", - "*(34)|Shift+PgDn" => "article_page_down", - "{3}(38)|Up" => "prev_article_or_scroll", - "{3}(40)|Down" => "next_article_or_scroll", - "*(38)|Shift+Up" => "article_scroll_up", - "*(40)|Shift+Down" => "article_scroll_down", - "^(38)|Ctrl+Up" => "prev_article_noscroll", - "^(40)|Ctrl+Down" => "next_article_noscroll", - "/" => "search_dialog", - "\\" => "cancel_search", - "s" => "toggle_mark", - "S" => "toggle_publ", - "u" => "toggle_unread", - "T" => "edit_tags", - "o" => "open_in_new_window", - "c p" => "catchup_below", - "c n" => "catchup_above", - "a W" => "toggle_widescreen", - "a e" => "toggle_full_text", - "e" => "email_article", - "a q" => "close_article", - "a a" => "select_all", - "a u" => "select_unread", - "a U" => "select_marked", - "a p" => "select_published", - "a i" => "select_invert", - "a n" => "select_none", - "f r" => "feed_refresh", - "f a" => "feed_unhide_read", - "f s" => "feed_subscribe", - "f e" => "feed_edit", - "f q" => "feed_catchup", - "f x" => "feed_reverse", - "f g" => "feed_toggle_vgroup", - "f D" => "feed_debug_update", - "f G" => "feed_debug_viewfeed", - "f C" => "toggle_combined_mode", - "f c" => "toggle_cdm_expanded", - "Q" => "catchup_all", - "x" => "cat_toggle_collapse", - "g a" => "goto_all", - "g f" => "goto_fresh", - "g s" => "goto_marked", - "g p" => "goto_published", - "g r" => "goto_read", - "g P" => "goto_prefs", - "r" => "select_article_cursor", - "c l" => "create_label", - "c f" => "create_filter", - "c s" => "collapse_sidebar", - "?" => "help_dialog", - ); - - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HOTKEY_MAP, - function ($result) use (&$hotkeys) { - $hotkeys = $result; - }, - $hotkeys); - - $prefixes = array(); - - foreach (array_keys($hotkeys) as $hotkey) { - $pair = explode(" ", (string)$hotkey, 2); - - if (count($pair) > 1 && !in_array($pair[0], $prefixes)) { - array_push($prefixes, $pair[0]); - } - } - - return array($prefixes, $hotkeys); - } - - function hotkeyHelp() { - $info = self::get_hotkeys_info(); - $imap = self::get_hotkeys_map(); - $omap = array(); - - foreach ($imap[1] as $sequence => $action) { - if (!isset($omap[$action])) $omap[$action] = array(); - - array_push($omap[$action], $sequence); - } - - ?> -
    - $hotkeys) { - ?> -
  • - $description) { - - if (!empty($omap[$action])) { - foreach ($omap[$action] as $sequence) { - if (strpos($sequence, "|") !== false) { - $sequence = substr($sequence, - strpos($sequence, "|")+1, - strlen($sequence)); - } else { - $keys = explode(" ", $sequence); - - for ($i = 0; $i < count($keys); $i++) { - if (strlen($keys[$i]) > 1) { - $tmp = ''; - foreach (str_split($keys[$i]) as $c) { - switch ($c) { - case '*': - $tmp .= __('Shift') . '+'; - break; - case '^': - $tmp .= __('Ctrl') . '+'; - break; - default: - $tmp .= $c; - } - } - $keys[$i] = $tmp; - } - } - $sequence = join(" ", $keys); - } - - ?> -
  • -
    -
    -
  • - -
-
- -
-