diff options
Diffstat (limited to 'classes/feeds.php')
-rwxr-xr-x | classes/feeds.php | 1761 |
1 files changed, 729 insertions, 1032 deletions
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"] = "— " . 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"] = "— " . 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 |