summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Dolgov <[email protected]>2021-03-02 15:16:38 +0300
committerAndrew Dolgov <[email protected]>2021-03-02 15:16:38 +0300
commit9ad4cbeecaed32e4106a7fef30bbe3d14195f78a (patch)
tree687b365d08a17cb2fb737dfc22f78197c62dde1e
parentd6629ed18863f797d34ebdc65815d7af21cb8332 (diff)
wip separate handlersexp-separate-handlers
-rw-r--r--api/index.php6
-rwxr-xr-xclasses/article.php169
-rwxr-xr-xclasses/feeds.php1761
-rw-r--r--[-rwxr-xr-x]classes/handler/api.php (renamed from classes/api.php)2
-rw-r--r--classes/handler/article.php166
-rw-r--r--classes/handler/feeds.php305
-rw-r--r--classes/handler/opml.php (renamed from classes/opml.php)2
-rw-r--r--classes/handler/pluginhandler.php (renamed from classes/pluginhandler.php)2
-rw-r--r--classes/handler/rpc.php786
-rwxr-xr-xclasses/rpc.php786
10 files changed, 1997 insertions, 1988 deletions
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 @@
<?php
-class Article extends Handler_Protected {
+class Article {
const ARTICLE_KIND_ALBUM = 1;
const ARTICLE_KIND_VIDEO = 2;
const ARTICLE_KIND_YOUTUBE = 3;
- function redirect() {
- $article = ORM::for_table('ttrss_entries')
- ->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 "<ul>";
- while ($line = $sth->fetch()) {
- print "<li>" . $line["tag_name"] . "</li>";
- }
- print "</ul>";
- }*/
-
- 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 @@
<?php
-require_once "colors.php";
+class Feeds {
+ const NEVER_GROUP_FEEDS = [ -6, 0 ];
+ const NEVER_GROUP_BY_DATE = [ -2, -1, -3 ];
-class Feeds extends Handler_Protected {
- const NEVER_GROUP_FEEDS = [ -6, 0 ];
- const NEVER_GROUP_BY_DATE = [ -2, -1, -3 ];
+ private $params;
- private $params;
-
- private $viewfeed_timestamp;
- private $viewfeed_timestamp_last;
-
- function csrf_ignore($method) {
- $csrf_ignored = array("index");
-
- return array_search($method, $csrf_ignored) !== false;
- }
-
- private function _format_headlines_list($feed, $method, $view_mode, $limit, $cat_view,
- $offset, $override_order = false, $include_children = false, $check_first_id = false,
- $skip_first_id_check = false, $order_by = false) {
-
- $disable_cache = false;
-
- $this->_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'] = "<div align='center'>".__('Feed not found.')."</div>";
- }
- }
-
- $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"] = "&mdash; " . 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"] = "<span class='collapse'>
- <i class='material-icons' onclick='return Article.cdmUnsetActive(event)'
- title=\"" . __("Collapse article") . "\">remove_circle</i></span>";
-
- if (get_pref(Prefs::SHOW_CONTENT_PREVIEW)) {
- $line["cdm_excerpt"] .= "<span class='excerpt'>" . $line["content_preview"] . "</span>";
- }
- }
-
- $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'] = "<div class='whiteBox'>$message";
-
- $reply['content'] .= "<p><span class=\"text-muted\">";
-
- $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'] .= "<br/>";
- $reply['content'] .= "<a class=\"text-muted\" href=\"#\" onclick=\"CommonDialogs.showFeedsWithErrors()\">" .
- __('Some feeds have update errors (click for details)') . "</a>";
- }
- $reply['content'] .= "</span></p></div>";
-
- }
- } 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'] = "<div class='whiteBox'>".__('No feed selected.');
-
- $reply['headlines']['content'] .= "<p><span class=\"text-muted\">";
-
- $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'] .= "<br/>";
- $reply['headlines']['content'] .= "<a class=\"text-muted\" href=\"#\" onclick=\"CommonDialogs.showFeedsWithErrors()\">".
- __('Some feeds have update errors (click for details)')."</a>";
- }
- $reply['headlines']['content'] .= "</span></p>";
-
- $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'] = "<div class='whiteBox'>". $error . "</div>";
-
- $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;
- }
- ?>
- <!DOCTYPE html>
- <html>
- <head>
- <title>Feed Debugger</title>
- <style type='text/css'>
- @media (prefers-color-scheme: dark) {
- body {
- background : #222;
- }
- }
- body.css_loading * {
- display : none;
- }
- </style>
- <script>
- dojoConfig = {
- async: true,
- cacheBust: "<?= get_scripts_timestamp(); ?>",
- packages: [
- { name: "fox", location: "../../js" },
- ]
- };
- </script>
- <?= javascript_tag("js/utility.js") ?>
- <?= javascript_tag("js/common.js") ?>
- <?= javascript_tag("lib/dojo/dojo.js") ?>
- <?= javascript_tag("lib/dojo/tt-rss-layer.js") ?>
- </head>
- <body class="flat ttrss_utility feed_debugger css_loading">
- <script type="text/javascript">
- require(['dojo/parser', "dojo/ready", 'dijit/form/Button','dijit/form/CheckBox', 'fox/form/Select', 'dijit/form/Form',
- 'dijit/form/Select','dijit/form/TextBox','dijit/form/ValidationTextBox'],function(parser, ready){
- ready(function() {
- parser.parse();
- });
- });
- </script>
-
- <div class="container">
- <h1>Feed Debugger: <?= "$feed_id: " . $this->_get_title($feed_id) ?></h1>
- <div class="content">
- <form method="post" action="" dojoType="dijit.form.Form">
- <?= \Controls\hidden_tag("op", "feeds") ?>
- <?= \Controls\hidden_tag("method", "updatedebugger") ?>
- <?= \Controls\hidden_tag("csrf_token", $csrf_token) ?>
- <?= \Controls\hidden_tag("action", "do_update") ?>
- <?= \Controls\hidden_tag("feed_id", (string)$feed_id) ?>
-
- <fieldset>
- <label>
- <?= \Controls\select_hash("xdebug", $xdebug,
- [Debug::$LOG_VERBOSE => "LOG_VERBOSE", Debug::$LOG_EXTENDED => "LOG_EXTENDED"]);
- ?></label>
- </fieldset>
-
- <fieldset>
- <label class="checkbox"><?= \Controls\checkbox_tag("force_refetch", isset($_REQUEST["force_refetch"])) ?> Force refetch</label>
- </fieldset>
-
- <fieldset class="narrow">
- <label class="checkbox"><?= \Controls\checkbox_tag("force_rehash", isset($_REQUEST["force_rehash"])) ?> Force rehash</label>
- </fieldset>
-
- <?= \Controls\submit_tag("Continue") ?>
- </form>
-
- <hr>
-
- <pre><?php
-
- if ($do_update) {
- RSSUtils::update_rss_feed($feed_id, true);
- }
-
- ?></pre>
- </div>
- </div>
- </body>
- </html>
- <?php
-
- }
-
- 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 {
-
- $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 {
- return -1;
+ $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"];
}
}
@@ -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;
+ $disable_cache = false;
+
+ $this->_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'] = "<div align='center'>".__('Feed not found.')."</div>";
+ }
+ }
+
+ $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;
},
- $order);
+ $feed, $cat_view);
- if ($query) return [$query, $skip_first_id];
+ $plugin_buttons = "";
+ PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_BUTTON,
+ function ($result) use (&$plugin_buttons) {
+ $plugin_buttons .= $result;
+ },
+ $feed, $cat_view);
- 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['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"] = "&mdash; " . 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"] = "<span class='collapse'>
+ <i class='material-icons' onclick='return Article.cdmUnsetActive(event)'
+ title=\"" . __("Collapse article") . "\">remove_circle</i></span>";
+
+ if (get_pref(Prefs::SHOW_CONTENT_PREVIEW)) {
+ $line["cdm_excerpt"] .= "<span class='excerpt'>" . $line["content_preview"] . "</span>";
+ }
+ }
+
+ $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");
+ }
}
- return [$query, $skip_first_id];
- }
+ $this->_mark_timestamp("end of articles");
- private function _mark_timestamp($label) {
+ if (!$headlines_count) {
- if (empty($_REQUEST['timestamps']))
- return;
+ if ($result instanceof PDOStatement) {
- if (!$this->viewfeed_timestamp) $this->viewfeed_timestamp = hrtime(true);
- if (!$this->viewfeed_timestamp_last) $this->viewfeed_timestamp_last = hrtime(true);
+ 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.");
+ }
+ }
+ }
- $timestamp = hrtime(true);
+ if (!$offset && $message) {
+ $reply['content'] = "<div class='whiteBox'>$message";
- printf("[%4d ms, %4d abs] %s\n",
- ($timestamp - $this->viewfeed_timestamp_last) / 1e6,
- ($timestamp - $this->viewfeed_timestamp) / 1e6,
- $label);
+ $reply['content'] .= "<p><span class=\"text-muted\">";
- $this->viewfeed_timestamp_last = $timestamp;
- }
+ $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'] .= "<br/>";
+ $reply['content'] .= "<a class=\"text-muted\" href=\"#\" onclick=\"CommonDialogs.showFeedsWithErrors()\">" .
+ __('Some feeds have update errors (click for details)') . "</a>";
+ }
+ $reply['content'] .= "</span></p></div>";
+
+ }
+ } 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);
+ }
+} \ No newline at end of file
diff --git a/classes/api.php b/classes/handler/api.php
index 991682191..e860584b4 100755..100644
--- a/classes/api.php
+++ b/classes/handler/api.php
@@ -1,5 +1,5 @@
<?php
-class API extends Handler {
+class Handler_API extends Handler {
const API_LEVEL = 15;
diff --git a/classes/handler/article.php b/classes/handler/article.php
new file mode 100644
index 000000000..269fdf75d
--- /dev/null
+++ b/classes/handler/article.php
@@ -0,0 +1,166 @@
+<?php
+class Handler_Article extends Handler_Protected {
+ function redirect() {
+ $article = ORM::for_table('ttrss_entries')
+ ->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 "<ul>";
+ while ($line = $sth->fetch()) {
+ print "<li>" . $line["tag_name"] . "</li>";
+ }
+ print "</ul>";
+ }*/
+
+ 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 @@
+<?php
+require_once "colors.php";
+
+class Handler_Feeds extends Handler_Protected {
+ function csrf_ignore($method) {
+ $csrf_ignored = array("index");
+
+ return array_search($method, $csrf_ignored) !== false;
+ }
+
+ 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) = 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'] = "<div class='whiteBox'>".__('No feed selected.');
+
+ $reply['headlines']['content'] .= "<p><span class=\"text-muted\">";
+
+ $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'] .= "<br/>";
+ $reply['headlines']['content'] .= "<a class=\"text-muted\" href=\"#\" onclick=\"CommonDialogs.showFeedsWithErrors()\">".
+ __('Some feeds have update errors (click for details)')."</a>";
+ }
+ $reply['headlines']['content'] .= "</span></p>";
+
+ $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'] = "<div class='whiteBox'>". $error . "</div>";
+
+ $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;
+ }
+ ?>
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Feed Debugger</title>
+ <style type='text/css'>
+ @media (prefers-color-scheme: dark) {
+ body {
+ background : #222;
+ }
+ }
+ body.css_loading * {
+ display : none;
+ }
+ </style>
+ <script>
+ dojoConfig = {
+ async: true,
+ cacheBust: "<?= get_scripts_timestamp(); ?>",
+ packages: [
+ { name: "fox", location: "../../js" },
+ ]
+ };
+ </script>
+ <?= javascript_tag("js/utility.js") ?>
+ <?= javascript_tag("js/common.js") ?>
+ <?= javascript_tag("lib/dojo/dojo.js") ?>
+ <?= javascript_tag("lib/dojo/tt-rss-layer.js") ?>
+ </head>
+ <body class="flat ttrss_utility feed_debugger css_loading">
+ <script type="text/javascript">
+ require(['dojo/parser', "dojo/ready", 'dijit/form/Button','dijit/form/CheckBox', 'fox/form/Select', 'dijit/form/Form',
+ 'dijit/form/Select','dijit/form/TextBox','dijit/form/ValidationTextBox'],function(parser, ready){
+ ready(function() {
+ parser.parse();
+ });
+ });
+ </script>
+
+ <div class="container">
+ <h1>Feed Debugger: <?= "$feed_id: " . $this->_get_title($feed_id) ?></h1>
+ <div class="content">
+ <form method="post" action="" dojoType="dijit.form.Form">
+ <?= \Controls\hidden_tag("op", "feeds") ?>
+ <?= \Controls\hidden_tag("method", "updatedebugger") ?>
+ <?= \Controls\hidden_tag("csrf_token", $csrf_token) ?>
+ <?= \Controls\hidden_tag("action", "do_update") ?>
+ <?= \Controls\hidden_tag("feed_id", (string)$feed_id) ?>
+
+ <fieldset>
+ <label>
+ <?= \Controls\select_hash("xdebug", $xdebug,
+ [Debug::$LOG_VERBOSE => "LOG_VERBOSE", Debug::$LOG_EXTENDED => "LOG_EXTENDED"]);
+ ?></label>
+ </fieldset>
+
+ <fieldset>
+ <label class="checkbox"><?= \Controls\checkbox_tag("force_refetch", isset($_REQUEST["force_refetch"])) ?> Force refetch</label>
+ </fieldset>
+
+ <fieldset class="narrow">
+ <label class="checkbox"><?= \Controls\checkbox_tag("force_rehash", isset($_REQUEST["force_rehash"])) ?> Force rehash</label>
+ </fieldset>
+
+ <?= \Controls\submit_tag("Continue") ?>
+ </form>
+
+ <hr>
+
+ <pre><?php
+
+ if ($do_update) {
+ RSSUtils::update_rss_feed($feed_id, true);
+ }
+
+ ?></pre>
+ </div>
+ </div>
+ </body>
+ </html>
+ <?php
+
+ }
+
+ 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));
+ }
+
+}
+
diff --git a/classes/opml.php b/classes/handler/opml.php
index 1a223788f..a07e8ebff 100644
--- a/classes/opml.php
+++ b/classes/handler/opml.php
@@ -1,5 +1,5 @@
<?php
-class OPML extends Handler_Protected {
+class Handler_OPML extends Handler_Protected {
function csrf_ignore($method) {
$csrf_ignored = array("export", "import");
diff --git a/classes/pluginhandler.php b/classes/handler/pluginhandler.php
index 75b823822..1bb88c149 100644
--- a/classes/pluginhandler.php
+++ b/classes/handler/pluginhandler.php
@@ -1,5 +1,5 @@
<?php
-class PluginHandler extends Handler_Protected {
+class Handler_PluginHandler extends Handler_Protected {
function csrf_ignore($method) {
return true;
}
diff --git a/classes/handler/rpc.php b/classes/handler/rpc.php
new file mode 100644
index 000000000..6fec9f56f
--- /dev/null
+++ b/classes/handler/rpc.php
@@ -0,0 +1,786 @@
+<?php
+class Handler_RPC extends Handler_Protected {
+
+ /*function csrf_ignore($method) {
+ $csrf_ignored = array("completelabels");
+
+ return array_search($method, $csrf_ignored) !== false;
+ }*/
+
+ private function _translations_as_array() {
+
+ global $text_domains;
+
+ $rv = [];
+
+ foreach (array_keys($text_domains) as $domain) {
+
+ /** @var gettext_reader $l10n */
+ $l10n = _get_reader($domain);
+
+ for ($i = 0; $i < $l10n->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 "<ul>";
+ while ($line = $sth->fetch()) {
+ print "<li>" . $line["caption"] . "</li>";
+ }
+ print "</ul>";
+ }*/
+
+ 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);
+ }
+
+ ?>
+ <ul class='panel panel-scrollable hotkeys-help' style='height : 300px'>
+ <?php
+
+ foreach ($info as $section => $hotkeys) {
+ ?>
+ <li><h3><?= $section ?></h3></li>
+ <?php
+
+ foreach ($hotkeys as $action => $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);
+ }
+
+ ?>
+ <li>
+ <div class='hk'><code><?= $sequence ?></code></div>
+ <div class='desc'><?= $description ?></div>
+ </li>
+ <?php
+ }
+ }
+ }
+ }
+ ?>
+ </ul>
+ <footer class='text-center'>
+ <?= \Controls\submit_tag(__('Close this window')) ?>
+ </footer>
+ <?php
+ }
+}
diff --git a/classes/rpc.php b/classes/rpc.php
index 90c2c762a..04aaf0076 100755
--- a/classes/rpc.php
+++ b/classes/rpc.php
@@ -1,786 +1,4 @@
<?php
-class RPC extends Handler_Protected {
+class RPC {
- /*function csrf_ignore($method) {
- $csrf_ignored = array("completelabels");
-
- return array_search($method, $csrf_ignored) !== false;
- }*/
-
- private function _translations_as_array() {
-
- global $text_domains;
-
- $rv = [];
-
- foreach (array_keys($text_domains) as $domain) {
-
- /** @var gettext_reader $l10n */
- $l10n = _get_reader($domain);
-
- for ($i = 0; $i < $l10n->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 "<ul>";
- while ($line = $sth->fetch()) {
- print "<li>" . $line["caption"] . "</li>";
- }
- print "</ul>";
- }*/
-
- 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);
- }
-
- ?>
- <ul class='panel panel-scrollable hotkeys-help' style='height : 300px'>
- <?php
-
- foreach ($info as $section => $hotkeys) {
- ?>
- <li><h3><?= $section ?></h3></li>
- <?php
-
- foreach ($hotkeys as $action => $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);
- }
-
- ?>
- <li>
- <div class='hk'><code><?= $sequence ?></code></div>
- <div class='desc'><?= $description ?></div>
- </li>
- <?php
- }
- }
- }
- }
- ?>
- </ul>
- <footer class='text-center'>
- <?= \Controls\submit_tag(__('Close this window')) ?>
- </footer>
- <?php
- }
-}
+} \ No newline at end of file