diff options
Diffstat (limited to 'classes')
-rwxr-xr-x | classes/api.php | 2 | ||||
-rw-r--r-- | classes/config.php | 9 | ||||
-rwxr-xr-x | classes/feeditem/common.php | 1 | ||||
-rwxr-xr-x | classes/feeds.php | 82 | ||||
-rwxr-xr-x | classes/handler/public.php | 6 | ||||
-rw-r--r-- | classes/opml.php | 334 | ||||
-rwxr-xr-x | classes/pluginhost.php | 197 | ||||
-rwxr-xr-x | classes/pref/feeds.php | 410 | ||||
-rwxr-xr-x | classes/pref/filters.php | 2 | ||||
-rw-r--r-- | classes/pref/labels.php | 2 | ||||
-rwxr-xr-x | classes/rpc.php | 1 | ||||
-rwxr-xr-x | classes/rssutils.php | 2 | ||||
-rw-r--r-- | classes/sanitizer.php | 7 | ||||
-rw-r--r-- | classes/urlhelper.php | 5 |
14 files changed, 643 insertions, 417 deletions
diff --git a/classes/api.php b/classes/api.php index 5f825e551..033aa8654 100755 --- a/classes/api.php +++ b/classes/api.php @@ -307,6 +307,7 @@ class API extends Handler { $article_ids = explode(',', clean($_REQUEST['article_id'] ?? '')); $sanitize_content = self::_param_to_bool($_REQUEST['sanitize'] ?? true); + // @phpstan-ignore-next-line if (count($article_ids)) { $entries = ORM::for_table('ttrss_entries') ->table_alias('e') @@ -369,7 +370,6 @@ class API extends Handler { } $this->_wrap(self::STATUS_OK, $articles); - // @phpstan-ignore-next-line } else { $this->_wrap(self::STATUS_ERR, ['error' => self::E_INCORRECT_USAGE]); } diff --git a/classes/config.php b/classes/config.php index 4ae4a2407..be4ecde36 100644 --- a/classes/config.php +++ b/classes/config.php @@ -170,6 +170,9 @@ class Config { const AUTH_MIN_INTERVAL = "AUTH_MIN_INTERVAL"; // minimum amount of seconds required between authentication attempts + const HTTP_USER_AGENT = "HTTP_USER_AGENT"; + // http user agent (changing this is not recommended) + // default values for all of the above: private const _DEFAULTS = [ Config::DB_TYPE => [ "pgsql", Config::T_STRING ], @@ -224,6 +227,8 @@ class Config { Config::CHECK_FOR_PLUGIN_UPDATES => [ "true", Config::T_BOOL ], Config::ENABLE_PLUGIN_INSTALLER => [ "true", Config::T_BOOL ], Config::AUTH_MIN_INTERVAL => [ 5, Config::T_INT ], + Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://tt-rss.org/)', + Config::T_STRING ], ]; private static $instance; @@ -632,4 +637,8 @@ class Config { return $rv; } + + static function get_user_agent() { + return sprintf(self::get(self::HTTP_USER_AGENT), self::get_version()); + } } diff --git a/classes/feeditem/common.php b/classes/feeditem/common.php index 18afeaa94..b4774941f 100755 --- a/classes/feeditem/common.php +++ b/classes/feeditem/common.php @@ -190,6 +190,7 @@ abstract class FeedItem_Common extends FeedItem { }, $tmp); // remove empty values + // @phpstan-ignore-next-line $tmp = array_filter($tmp, 'strlen'); asort($tmp); diff --git a/classes/feeds.php b/classes/feeds.php index 42673ca95..ea9158949 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -230,17 +230,43 @@ class Feeds extends Handler_Protected { $line["feed_title"] = $line["feed_title"] ?? ""; + $button_doc = new DOMDocument(); + $line["buttons_left"] = ""; PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_LEFT_BUTTON, - function ($result) use (&$line) { - $line["buttons_left"] .= $result; + function ($result, $plugin) use (&$line, &$button_doc) { + if ($result && $button_doc->loadXML($result)) { + + /** @var DOMElement|null */ + $child = $button_doc->firstChild; + + if ($child) { + do { + $child->setAttribute('data-plugin-name', get_class($plugin)); + } while ($child = $child->nextSibling); + + $line["buttons_left"] .= $button_doc->saveXML($button_doc->firstChild); + } + } }, $line); $line["buttons"] = ""; PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_BUTTON, - function ($result) use (&$line) { - $line["buttons"] .= $result; + function ($result, $plugin) use (&$line, &$button_doc) { + if ($result && $button_doc->loadXML($result)) { + + /** @var DOMElement|null */ + $child = $button_doc->firstChild; + + if ($child) { + do { + $child->setAttribute('data-plugin-name', get_class($plugin)); + } while ($child = $child->nextSibling); + + $line["buttons"] .= $button_doc->saveXML($button_doc->firstChild); + } + } }, $line); @@ -587,6 +613,23 @@ class Feeds extends Handler_Protected { ]); } + function opensite() { + $feed = ORM::for_table('ttrss_feeds') + ->find_one((int)$_REQUEST['feed_id']); + + if ($feed) { + $site_url = UrlHelper::validate($feed->site_url); + + if ($site_url) { + header("Location: $site_url"); + return; + } + } + + header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); + print "Feed not found or has an empty site URL."; + } + function updatedebugger() { header("Content-type: text/html"); @@ -996,6 +1039,13 @@ class Feeds extends Handler_Protected { if (!$url) return ["code" => 2]; + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_PRE_SUBSCRIBE, + /** @phpstan-ignore-next-line */ + function ($result) use (&$url, &$auth_login, &$auth_pass) { + // arguments are updated inside the hook (if needed) + }, + $url, $auth_login, $auth_pass); + $contents = UrlHelper::fetch($url, false, $auth_login, $auth_pass); PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_SUBSCRIBE_FEED, @@ -1105,6 +1155,30 @@ class Feeds extends Handler_Protected { } } + /** $owner_uid defaults to $_SESSION['uid] */ + static function _find_by_title(string $title, bool $cat = false, int $owner_uid = 0) { + + $res = false; + + if ($cat) { + $res = ORM::for_table('ttrss_feed_categories') + ->where('owner_uid', $owner_uid ? $owner_uid : $_SESSION['uid']) + ->where('title', $title) + ->find_one(); + } else { + $res = ORM::for_table('ttrss_feeds') + ->where('owner_uid', $owner_uid ? $owner_uid : $_SESSION['uid']) + ->where('title', $title) + ->find_one(); + } + + if ($res) { + return $res->id; + } else { + return false; + } + } + static function _get_title($id, bool $cat = false) { $pdo = Db::pdo(); diff --git a/classes/handler/public.php b/classes/handler/public.php index 4da32e90d..14474d0bb 100755 --- a/classes/handler/public.php +++ b/classes/handler/public.php @@ -53,6 +53,10 @@ class Handler_Public extends Handler { if ($handler) { $qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id((int)$feed), $params); + } else { + user_error("Failed to find handler for plugin feed ID: $feed", E_USER_ERROR); + + return false; } } else { @@ -448,7 +452,7 @@ class Handler_Public extends Handler { if ($login) { $user = ORM::for_table('ttrss_users') - ->select('id', 'resetpass_token') + ->select_many('id', 'resetpass_token') ->where_raw('LOWER(login) = LOWER(?)', [$login]) ->find_one(); diff --git a/classes/opml.php b/classes/opml.php index 2cfc890fa..e0dbebcda 100644 --- a/classes/opml.php +++ b/classes/opml.php @@ -48,9 +48,7 @@ class OPML extends Handler_Protected { // Export - private function opml_export_category($owner_uid, $cat_id, $hide_private_feeds = false, $include_settings = true) { - - $cat_id = (int) $cat_id; + private function opml_export_category(int $owner_uid, int $cat_id, bool $hide_private_feeds = false, bool $include_settings = true) { if ($hide_private_feeds) $hide_qpart = "(private IS false AND auth_login = '' AND auth_pass = '')"; @@ -126,7 +124,7 @@ class OPML extends Handler_Protected { return $out; } - function opml_export($filename, $owner_uid, $hide_private_feeds = false, $include_settings = true, $file_output = false) { + function opml_export(string $filename, int $owner_uid, bool $hide_private_feeds = false, bool $include_settings = true, bool $file_output = false) { if (!$owner_uid) return; if (!$file_output) @@ -189,7 +187,7 @@ class OPML extends Handler_Protected { WHERE owner_uid = ? ORDER BY id"); $sth->execute([$owner_uid]); - while ($line = $sth->fetch()) { + while ($line = $sth->fetch(PDO::FETCH_ASSOC)) { $line["rules"] = array(); $line["actions"] = array(); @@ -204,36 +202,36 @@ class OPML extends Handler_Protected { $cat_filter = $tmp_line["cat_filter"]; if (!$tmp_line["match_on"]) { - if ($cat_filter && $tmp_line["cat_id"] || $tmp_line["feed_id"]) { - $tmp_line["feed"] = Feeds::_get_title( - $cat_filter ? $tmp_line["cat_id"] : $tmp_line["feed_id"], - $cat_filter); - } else { - $tmp_line["feed"] = ""; - } - } else { - $match = []; - foreach (json_decode($tmp_line["match_on"], true) as $feed_id) { - - if (strpos($feed_id, "CAT:") === 0) { - $feed_id = (int)substr($feed_id, 4); - if ($feed_id) { - array_push($match, [Feeds::_get_cat_title($feed_id), true, false]); - } else { - array_push($match, [0, true, true]); - } - } else { - if ($feed_id) { - array_push($match, [Feeds::_get_title((int)$feed_id), false, false]); - } else { - array_push($match, [0, false, true]); - } - } - } - - $tmp_line["match"] = $match; - unset($tmp_line["match_on"]); - } + if ($cat_filter && $tmp_line["cat_id"] || $tmp_line["feed_id"]) { + $tmp_line["feed"] = Feeds::_get_title( + $cat_filter ? $tmp_line["cat_id"] : $tmp_line["feed_id"], + $cat_filter); + } else { + $tmp_line["feed"] = ""; + } + } else { + $match = []; + foreach (json_decode($tmp_line["match_on"], true) as $feed_id) { + + if (strpos($feed_id, "CAT:") === 0) { + $feed_id = (int)substr($feed_id, 4); + if ($feed_id) { + array_push($match, [Feeds::_get_cat_title($feed_id), true, false]); + } else { + array_push($match, [0, true, true]); + } + } else { + if ($feed_id) { + array_push($match, [Feeds::_get_title((int)$feed_id), false, false]); + } else { + array_push($match, [0, false, true]); + } + } + } + + $tmp_line["match"] = $match; + unset($tmp_line["match_on"]); + } unset($tmp_line["feed_id"]); unset($tmp_line["cat_id"]); @@ -298,7 +296,7 @@ class OPML extends Handler_Protected { // Import - private function opml_import_feed($node, $cat_id, $owner_uid) { + private function opml_import_feed(DOMNode $node, int $cat_id, int $owner_uid, int $nest) { $attrs = $node->attributes; $feed_title = mb_substr($attrs->getNamedItem('text')->nodeValue, 0, 250); @@ -318,7 +316,7 @@ class OPML extends Handler_Protected { if (!$sth->fetch()) { #$this->opml_notice("[FEED] [$feed_title/$feed_url] dst_CAT=$cat_id"); - $this->opml_notice(T_sprintf("Adding feed: %s", $feed_title == '[Unknown]' ? $feed_url : $feed_title)); + $this->opml_notice(T_sprintf("Adding feed: %s", $feed_title == '[Unknown]' ? $feed_url : $feed_title), $nest); if (!$cat_id) $cat_id = null; @@ -338,12 +336,12 @@ class OPML extends Handler_Protected { $sth->execute([$feed_title, $feed_url, $owner_uid, $cat_id, $site_url, $order_id, $update_interval, $purge_interval]); } else { - $this->opml_notice(T_sprintf("Duplicate feed: %s", $feed_title == '[Unknown]' ? $feed_url : $feed_title)); + $this->opml_notice(T_sprintf("Duplicate feed: %s", $feed_title == '[Unknown]' ? $feed_url : $feed_title), $nest); } } } - private function opml_import_label($node, $owner_uid) { + private function opml_import_label(DOMNode $node, int $owner_uid, int $nest) { $attrs = $node->attributes; $label_name = $attrs->getNamedItem('label-name')->nodeValue; @@ -351,16 +349,16 @@ class OPML extends Handler_Protected { $fg_color = $attrs->getNamedItem('label-fg-color')->nodeValue; $bg_color = $attrs->getNamedItem('label-bg-color')->nodeValue; - if (!Labels::find_id($label_name, $_SESSION['uid'])) { - $this->opml_notice(T_sprintf("Adding label %s", htmlspecialchars($label_name))); + if (!Labels::find_id($label_name, $owner_uid)) { + $this->opml_notice(T_sprintf("Adding label %s", htmlspecialchars($label_name)), $nest); Labels::create($label_name, $fg_color, $bg_color, $owner_uid); } else { - $this->opml_notice(T_sprintf("Duplicate label: %s", htmlspecialchars($label_name))); + $this->opml_notice(T_sprintf("Duplicate label: %s", htmlspecialchars($label_name)), $nest); } } } - private function opml_import_preference($node) { + private function opml_import_preference(DOMNode $node, int $owner_uid, int $nest) { $attrs = $node->attributes; $pref_name = $attrs->getNamedItem('pref-name')->nodeValue; @@ -368,13 +366,13 @@ class OPML extends Handler_Protected { $pref_value = $attrs->getNamedItem('value')->nodeValue; $this->opml_notice(T_sprintf("Setting preference key %s to %s", - $pref_name, $pref_value)); + $pref_name, $pref_value), $nest); - set_pref($pref_name, $pref_value); + set_pref($pref_name, $pref_value, $owner_uid); } } - private function opml_import_filter($node) { + private function opml_import_filter(DOMNode $node, int $owner_uid, int $nest) { $attrs = $node->attributes; $filter_type = $attrs->getNamedItem('filter-type')->nodeValue; @@ -393,47 +391,58 @@ class OPML extends Handler_Protected { $sth = $this->pdo->prepare("INSERT INTO ttrss_filters2 (match_any_rule,enabled,inverse,title,owner_uid) VALUES (?, ?, ?, ?, ?)"); - $sth->execute([$match_any_rule, $enabled, $inverse, $title, $_SESSION['uid']]); + $sth->execute([$match_any_rule, $enabled, $inverse, $title, $owner_uid]); $sth = $this->pdo->prepare("SELECT MAX(id) AS id FROM ttrss_filters2 WHERE owner_uid = ?"); - $sth->execute([$_SESSION['uid']]); + $sth->execute([$owner_uid]); $row = $sth->fetch(); $filter_id = $row['id']; if ($filter_id) { - $this->opml_notice(T_sprintf("Adding filter %s...", $title)); + $this->opml_notice(T_sprintf("Adding filter %s...", $title), $nest); + //$this->opml_notice(json_encode($filter)); foreach ($filter["rules"] as $rule) { $feed_id = null; $cat_id = null; - if ($rule["match"]) { + if ($rule["match"] ?? false) { + + $match_on = []; - $match_on = []; + foreach ($rule["match"] as $match) { + list ($name, $is_cat, $is_id) = $match; - foreach ($rule["match"] as $match) { - list ($name, $is_cat, $is_id) = $match; + if ($is_id) { + array_push($match_on, ($is_cat ? "CAT:" : "") . $name); + } else { - if ($is_id) { - array_push($match_on, ($is_cat ? "CAT:" : "") . $name); - } else { + $match_id = Feeds::_find_by_title($name, $is_cat, $owner_uid); - if (!$is_cat) { - $tsth = $this->pdo->prepare("SELECT id FROM ttrss_feeds - WHERE title = ? AND owner_uid = ?"); + if ($match_id) { + if ($is_cat) { + array_push($match_on, "CAT:$match_id"); + } else { + array_push($match_on, $match_id); + } + } - $tsth->execute([$name, $_SESSION['uid']]); + /*if (!$is_cat) { + $tsth = $this->pdo->prepare("SELECT id FROM ttrss_feeds + WHERE title = ? AND owner_uid = ?"); - if ($row = $tsth->fetch()) { - $match_id = $row['id']; + $tsth->execute([$name, $_SESSION['uid']]); + + if ($row = $tsth->fetch()) { + $match_id = $row['id']; array_push($match_on, $match_id); - } - } else { - $tsth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories - WHERE title = ? AND owner_uid = ?"); + } + } else { + $tsth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories + WHERE title = ? AND owner_uid = ?"); $tsth->execute([$name, $_SESSION['uid']]); if ($row = $tsth->fetch()) { @@ -441,54 +450,64 @@ class OPML extends Handler_Protected { array_push($match_on, "CAT:$match_id"); } - } - } - } + } */ + } + } - $reg_exp = $rule["reg_exp"]; - $filter_type = (int)$rule["filter_type"]; - $inverse = bool_to_sql_bool($rule["inverse"]); - $match_on = json_encode($match_on); + $reg_exp = $rule["reg_exp"]; + $filter_type = (int)$rule["filter_type"]; + $inverse = bool_to_sql_bool($rule["inverse"]); + $match_on = json_encode($match_on); - $usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_rules + $usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_rules (feed_id,cat_id,match_on,filter_id,filter_type,reg_exp,cat_filter,inverse) - VALUES - (NULL, NULL, ?, ?, ?, ?, false, ?)"); - $usth->execute([$match_on, $filter_id, $filter_type, $reg_exp, $inverse]); + VALUES + (NULL, NULL, ?, ?, ?, ?, false, ?)"); + $usth->execute([$match_on, $filter_id, $filter_type, $reg_exp, $inverse]); - } else { + } else { - if (!$rule["cat_filter"]) { - $tsth = $this->pdo->prepare("SELECT id FROM ttrss_feeds - WHERE title = ? AND owner_uid = ?"); + $match_id = Feeds::_find_by_title($rule['feed'] ?? "", $rule['cat_filter'], $owner_uid); - $tsth->execute([$rule['feed'], $_SESSION['uid']]); + if ($match_id) { + if ($rule['cat_filter']) { + $cat_id = $match_id; + } else { + $feed_id = $match_id; + } + } - if ($row = $tsth->fetch()) { - $feed_id = $row['id']; - } - } else { + /*if (!$rule["cat_filter"]) { + $tsth = $this->pdo->prepare("SELECT id FROM ttrss_feeds + WHERE title = ? AND owner_uid = ?"); + + $tsth->execute([$rule['feed'], $_SESSION['uid']]); + + if ($row = $tsth->fetch()) { + $feed_id = $row['id']; + } + } else { $tsth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories - WHERE title = ? AND owner_uid = ?"); + WHERE title = ? AND owner_uid = ?"); $tsth->execute([$rule['feed'], $_SESSION['uid']]); if ($row = $tsth->fetch()) { $feed_id = $row['id']; } - } + } */ - $cat_filter = bool_to_sql_bool($rule["cat_filter"]); - $reg_exp = $rule["reg_exp"]; - $filter_type = (int)$rule["filter_type"]; - $inverse = bool_to_sql_bool($rule["inverse"]); + $cat_filter = bool_to_sql_bool($rule["cat_filter"]); + $reg_exp = $rule["reg_exp"]; + $filter_type = (int)$rule["filter_type"]; + $inverse = bool_to_sql_bool($rule["inverse"]); - $usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_rules + $usth = $this->pdo->prepare("INSERT INTO ttrss_filters2_rules (feed_id,cat_id,filter_id,filter_type,reg_exp,cat_filter,inverse) - VALUES - (?, ?, ?, ?, ?, ?, ?)"); - $usth->execute([$feed_id, $cat_id, $filter_id, $filter_type, $reg_exp, $cat_filter, $inverse]); - } + VALUES + (?, ?, ?, ?, ?, ?, ?)"); + $usth->execute([$feed_id, $cat_id, $filter_id, $filter_type, $reg_exp, $cat_filter, $inverse]); + } } foreach ($filter["actions"] as $action) { @@ -507,8 +526,8 @@ class OPML extends Handler_Protected { } } - private function opml_import_category($doc, $root_node, $owner_uid, $parent_id) { - $default_cat_id = (int) $this->get_feed_category('Imported feeds', false); + private function opml_import_category(DOMDocument $doc, ?DOMNode $root_node, int $owner_uid, int $parent_id, int $nest) { + $default_cat_id = (int) $this->get_feed_category('Imported feeds', $owner_uid, 0); if ($root_node) { $cat_title = mb_substr($root_node->attributes->getNamedItem('text')->nodeValue, 0, 250); @@ -517,13 +536,13 @@ class OPML extends Handler_Protected { $cat_title = mb_substr($root_node->attributes->getNamedItem('title')->nodeValue, 0, 250); if (!in_array($cat_title, array("tt-rss-filters", "tt-rss-labels", "tt-rss-prefs"))) { - $cat_id = $this->get_feed_category($cat_title, $parent_id); + $cat_id = $this->get_feed_category($cat_title, $owner_uid, $parent_id); - if ($cat_id === false) { + if ($cat_id === 0) { $order_id = (int) $root_node->attributes->getNamedItem('ttrssSortOrder')->nodeValue; - Feeds::_add_cat($cat_title, $_SESSION['uid'], $parent_id ? $parent_id : null, (int)$order_id); - $cat_id = $this->get_feed_category($cat_title, $parent_id); + Feeds::_add_cat($cat_title, $owner_uid, $parent_id ? $parent_id : null, (int)$order_id); + $cat_id = $this->get_feed_category($cat_title, $owner_uid, $parent_id); } } else { @@ -540,21 +559,21 @@ class OPML extends Handler_Protected { $cat_title = false; } - #$this->opml_notice("[CAT] $cat_title id: $cat_id P_id: $parent_id"); - $this->opml_notice(T_sprintf("Processing category: %s", $cat_title ? $cat_title : __("Uncategorized"))); + //$this->opml_notice("[CAT] $cat_title id: $cat_id P_id: $parent_id"); + $this->opml_notice(T_sprintf("Processing category: %s", $cat_title ? $cat_title : __("Uncategorized")), $nest); foreach ($outlines as $node) { if ($node->hasAttributes() && strtolower($node->tagName) == "outline") { $attrs = $node->attributes; - $node_cat_title = $attrs->getNamedItem('text')->nodeValue; + $node_cat_title = $attrs->getNamedItem('text') ? $attrs->getNamedItem('text')->nodeValue : false; if (!$node_cat_title) - $node_cat_title = $attrs->getNamedItem('title')->nodeValue; + $node_cat_title = $attrs->getNamedItem('title') ? $attrs->getNamedItem('title')->nodeValue : false; - $node_feed_url = $attrs->getNamedItem('xmlUrl')->nodeValue; + $node_feed_url = $attrs->getNamedItem('xmlUrl') ? $attrs->getNamedItem('xmlUrl')->nodeValue : false; if ($node_cat_title && !$node_feed_url) { - $this->opml_import_category($doc, $node, $owner_uid, $cat_id); + $this->opml_import_category($doc, $node, $owner_uid, $cat_id, $nest+1); } else { if (!$cat_id) { @@ -565,91 +584,112 @@ class OPML extends Handler_Protected { switch ($cat_title) { case "tt-rss-prefs": - $this->opml_import_preference($node); + $this->opml_import_preference($node, $owner_uid, $nest+1); break; case "tt-rss-labels": - $this->opml_import_label($node, $owner_uid); + $this->opml_import_label($node, $owner_uid, $nest+1); break; case "tt-rss-filters": - $this->opml_import_filter($node); + $this->opml_import_filter($node, $owner_uid, $nest+1); break; default: - $this->opml_import_feed($node, $dst_cat_id, $owner_uid); + $this->opml_import_feed($node, $dst_cat_id, $owner_uid, $nest+1); } } } } } - function opml_import($owner_uid) { + /** $filename is optional; assumes HTTP upload with $_FILES otherwise */ + function opml_import(int $owner_uid, string $filename = "") { if (!$owner_uid) return; $doc = false; - if ($_FILES['opml_file']['error'] != 0) { - print_error(T_sprintf("Upload failed with error code %d", - $_FILES['opml_file']['error'])); - return; - } + if (!$filename) { + if ($_FILES['opml_file']['error'] != 0) { + print_error(T_sprintf("Upload failed with error code %d", + $_FILES['opml_file']['error'])); + return false; + } - if (is_uploaded_file($_FILES['opml_file']['tmp_name'])) { - $tmp_file = (string)tempnam(Config::get(Config::CACHE_DIR) . '/upload', 'opml'); + if (is_uploaded_file($_FILES['opml_file']['tmp_name'])) { + $tmp_file = (string)tempnam(Config::get(Config::CACHE_DIR) . '/upload', 'opml'); - $result = move_uploaded_file($_FILES['opml_file']['tmp_name'], - $tmp_file); + $result = move_uploaded_file($_FILES['opml_file']['tmp_name'], + $tmp_file); - if (!$result) { - print_error(__("Unable to move uploaded file.")); - return; + if (!$result) { + print_error(__("Unable to move uploaded file.")); + return false; + } + } else { + print_error(__('Error: please upload OPML file.')); + return false; } } else { - print_error(__('Error: please upload OPML file.')); - return; + $tmp_file = $filename; + } + + if (!is_readable($tmp_file)) { + $this->opml_notice(T_sprintf("Error: file is not readable: %s", $filename)); + return false; } $loaded = false; - if (is_file($tmp_file)) { - $doc = new DOMDocument(); + $doc = new DOMDocument(); + + if (version_compare(PHP_VERSION, '8.0.0', '<')) { libxml_disable_entity_loader(false); - $loaded = $doc->load($tmp_file); + } + + $loaded = $doc->load($tmp_file); + + if (version_compare(PHP_VERSION, '8.0.0', '<')) { libxml_disable_entity_loader(true); - unlink($tmp_file); - } else if (empty($doc)) { - print_error(__('Error: unable to find moved OPML file.')); - return; } + // only remove temporary i.e. HTTP uploaded files + if (!$filename) + unlink($tmp_file); + if ($loaded) { - $this->pdo->beginTransaction(); - $this->opml_import_category($doc, false, $owner_uid, false); - $this->pdo->commit(); + // we're using ORM while importing so we can't transaction-lock things anymore + //$this->pdo->beginTransaction(); + $this->opml_import_category($doc, null, $owner_uid, 0, 0); + //$this->pdo->commit(); } else { - print_error(__('Error while parsing document.')); + $this->opml_notice(__('Error while parsing document.')); + return false; } - } - private function opml_notice($msg) { - print "$msg<br/>"; + return true; } - function get_feed_category($feed_cat, $parent_cat_id = false) { + private function opml_notice(string $msg, int $prefix_length = 0) { + if (php_sapi_name() == "cli") { + Debug::log(str_repeat(" ", $prefix_length) . $msg); + } else { + // TODO: use better separator i.e. CSS-defined span of certain width or something + print str_repeat(" ", $prefix_length) . $msg . "<br/>"; + } + } - $parent_cat_id = (int) $parent_cat_id; + function get_feed_category(string $feed_cat, int $owner_uid, int $parent_cat_id) : int { $sth = $this->pdo->prepare("SELECT id FROM ttrss_feed_categories WHERE title = :title AND (parent_cat = :parent OR (:parent = 0 AND parent_cat IS NULL)) AND owner_uid = :uid"); - $sth->execute([':title' => $feed_cat, ':parent' => $parent_cat_id, ':uid' => $_SESSION['uid']]); + $sth->execute([':title' => $feed_cat, ':parent' => $parent_cat_id, ':uid' => $owner_uid]); if ($row = $sth->fetch()) { return $row['id']; } else { - return false; + return 0; } } - } diff --git a/classes/pluginhost.php b/classes/pluginhost.php index ee4107ae7..f067543bb 100755 --- a/classes/pluginhost.php +++ b/classes/pluginhost.php @@ -23,56 +23,153 @@ class PluginHost { // Hooks marked with *1 are run in global context and available // to plugins loaded in config.php only - const HOOK_ARTICLE_BUTTON = "hook_article_button"; // hook_article_button($line) - const HOOK_ARTICLE_FILTER = "hook_article_filter"; // hook_article_filter($article) - const HOOK_PREFS_TAB = "hook_prefs_tab"; // hook_prefs_tab($tab) - const HOOK_PREFS_TAB_SECTION = "hook_prefs_tab_section"; // hook_prefs_tab_section($section) - const HOOK_PREFS_TABS = "hook_prefs_tabs"; // hook_prefs_tabs() - const HOOK_FEED_PARSED = "hook_feed_parsed"; // hook_feed_parsed($parser, $feed_id) - const HOOK_UPDATE_TASK = "hook_update_task"; //*1 // GLOBAL: hook_update_task($cli_options) - const HOOK_AUTH_USER = "hook_auth_user"; // hook_auth_user($login, $password, $service) (byref) - const HOOK_HOTKEY_MAP = "hook_hotkey_map"; // hook_hotkey_map($hotkeys) (byref) - const HOOK_RENDER_ARTICLE = "hook_render_article"; // hook_render_article($article) - const HOOK_RENDER_ARTICLE_CDM = "hook_render_article_cdm"; // hook_render_article_cdm($article) - const HOOK_FEED_FETCHED = "hook_feed_fetched"; // hook_feed_fetched($feed_data, $fetch_url, $owner_uid, $feed) (byref) - const HOOK_SANITIZE = "hook_sanitize"; // hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) (byref) - const HOOK_RENDER_ARTICLE_API = "hook_render_article_api"; // hook_render_article_api($params) - const HOOK_TOOLBAR_BUTTON = "hook_toolbar_button"; // hook_toolbar_button() - const HOOK_ACTION_ITEM = "hook_action_item"; // hook_action_item() - const HOOK_HEADLINE_TOOLBAR_BUTTON = "hook_headline_toolbar_button"; // hook_headline_toolbar_button($feed_id, $is_cat) - const HOOK_HOTKEY_INFO = "hook_hotkey_info"; // hook_hotkey_info($hotkeys) (byref) - const HOOK_ARTICLE_LEFT_BUTTON = "hook_article_left_button"; // hook_article_left_button($row) - const HOOK_PREFS_EDIT_FEED = "hook_prefs_edit_feed"; // hook_prefs_edit_feed($feed_id) - const HOOK_PREFS_SAVE_FEED = "hook_prefs_save_feed"; // hook_prefs_save_feed($feed_id) - const HOOK_FETCH_FEED = "hook_fetch_feed"; // hook_fetch_feed($feed_data, $fetch_url, $owner_uid, $feed, $last_article_timestamp, $auth_login, $auth_pass) (byref) - const HOOK_QUERY_HEADLINES = "hook_query_headlines"; // hook_query_headlines($row) (byref) - const HOOK_HOUSE_KEEPING = "hook_house_keeping"; //*1 // GLOBAL: hook_house_keeping() - const HOOK_SEARCH = "hook_search"; // hook_search($query) - const HOOK_FORMAT_ENCLOSURES = "hook_format_enclosures"; // hook__format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images) (byref) - const HOOK_SUBSCRIBE_FEED = "hook_subscribe_feed"; // hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) (byref) - const HOOK_HEADLINES_BEFORE = "hook_headlines_before"; // hook_headlines_before($feed, $is_cat, $qfh_ret) - const HOOK_RENDER_ENCLOSURE = "hook_render_enclosure"; // hook_render_enclosure($entry, $id, $rv) - const HOOK_ARTICLE_FILTER_ACTION = "hook_article_filter_action"; // hook_article_filter_action($article, $action) - const HOOK_ARTICLE_EXPORT_FEED = "hook_article_export_feed"; // hook_article_export_feed($line, $feed, $is_cat, $owner_uid) (byref) - const HOOK_MAIN_TOOLBAR_BUTTON = "hook_main_toolbar_button"; // hook_main_toolbar_button() - const HOOK_ENCLOSURE_ENTRY = "hook_enclosure_entry"; // hook_enclosure_entry($entry, $id, $rv) (byref) - const HOOK_FORMAT_ARTICLE = "hook_format_article"; // hook_format_article($html, $row) - const HOOK_FORMAT_ARTICLE_CDM = "hook_format_article_cdm"; /* RIP */ - const HOOK_FEED_BASIC_INFO = "hook_feed_basic_info"; // hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed_id, $auth_login, $auth_pass) (byref) - const HOOK_SEND_LOCAL_FILE = "hook_send_local_file"; // hook_send_local_file($filename) - const HOOK_UNSUBSCRIBE_FEED = "hook_unsubscribe_feed"; // hook_unsubscribe_feed($feed_id, $owner_uid) - const HOOK_SEND_MAIL = "hook_send_mail"; // hook_send_mail(Mailer $mailer, $params) - const HOOK_FILTER_TRIGGERED = "hook_filter_triggered"; // hook_filter_triggered($feed_id, $owner_uid, $article, $matched_filters, $matched_rules, $article_filters) - const HOOK_GET_FULL_TEXT = "hook_get_full_text"; // hook_get_full_text($url) - const HOOK_ARTICLE_IMAGE = "hook_article_image"; // hook_article_image($enclosures, $content, $site_url) - const HOOK_FEED_TREE = "hook_feed_tree"; // hook_feed_tree() - const HOOK_IFRAME_WHITELISTED = "hook_iframe_whitelisted"; // hook_iframe_whitelisted($url) - const HOOK_ENCLOSURE_IMPORTED = "hook_enclosure_imported"; // hook_enclosure_imported($enclosure, $feed) - const HOOK_HEADLINES_CUSTOM_SORT_MAP = "hook_headlines_custom_sort_map"; // hook_headlines_custom_sort_map() + /** hook_article_button($line) */ + const HOOK_ARTICLE_BUTTON = "hook_article_button"; + + /** hook_article_filter($article) */ + const HOOK_ARTICLE_FILTER = "hook_article_filter"; + + /** hook_prefs_tab($tab) */ + const HOOK_PREFS_TAB = "hook_prefs_tab"; + + /** hook_prefs_tab_section($section) */ + const HOOK_PREFS_TAB_SECTION = "hook_prefs_tab_section"; + + /** hook_prefs_tabs() */ + const HOOK_PREFS_TABS = "hook_prefs_tabs"; + + /** hook_feed_parsed($parser, $feed_id) */ + const HOOK_FEED_PARSED = "hook_feed_parsed"; + + /** GLOBAL: hook_update_task($cli_options) */ + const HOOK_UPDATE_TASK = "hook_update_task"; //*1 + + /** hook_auth_user($login, $password, $service) (byref) */ + const HOOK_AUTH_USER = "hook_auth_user"; + + /** hook_hotkey_map($hotkeys) (byref) */ + const HOOK_HOTKEY_MAP = "hook_hotkey_map"; + + /** hook_render_article($article) */ + const HOOK_RENDER_ARTICLE = "hook_render_article"; + + /** hook_render_article_cdm($article) */ + const HOOK_RENDER_ARTICLE_CDM = "hook_render_article_cdm"; + + /** hook_feed_fetched($feed_data, $fetch_url, $owner_uid, $feed) (byref) */ + const HOOK_FEED_FETCHED = "hook_feed_fetched"; + + /** hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) (byref) */ + const HOOK_SANITIZE = "hook_sanitize"; + + /** hook_render_article_api($params) */ + const HOOK_RENDER_ARTICLE_API = "hook_render_article_api"; + + /** hook_toolbar_button() */ + const HOOK_TOOLBAR_BUTTON = "hook_toolbar_button"; + + /** hook_action_item() */ + const HOOK_ACTION_ITEM = "hook_action_item"; + + /** hook_headline_toolbar_button($feed_id, $is_cat) */ + const HOOK_HEADLINE_TOOLBAR_BUTTON = "hook_headline_toolbar_button"; + + /** hook_hotkey_info($hotkeys) (byref) */ + const HOOK_HOTKEY_INFO = "hook_hotkey_info"; + + /** hook_article_left_button($row) */ + const HOOK_ARTICLE_LEFT_BUTTON = "hook_article_left_button"; + + /** hook_prefs_edit_feed($feed_id) */ + const HOOK_PREFS_EDIT_FEED = "hook_prefs_edit_feed"; + + /** hook_prefs_save_feed($feed_id) */ + const HOOK_PREFS_SAVE_FEED = "hook_prefs_save_feed"; + + /** hook_fetch_feed($feed_data, $fetch_url, $owner_uid, $feed, $last_article_timestamp, $auth_login, $auth_pass) (byref) */ + const HOOK_FETCH_FEED = "hook_fetch_feed"; + + /** hook_query_headlines($row) (byref) */ + const HOOK_QUERY_HEADLINES = "hook_query_headlines"; + + /** GLOBAL: hook_house_keeping() */ + const HOOK_HOUSE_KEEPING = "hook_house_keeping"; //*1 + + /** hook_search($query) */ + const HOOK_SEARCH = "hook_search"; + + /** hook_format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images) (byref) */ + const HOOK_FORMAT_ENCLOSURES = "hook_format_enclosures"; + + /** hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) (byref) */ + const HOOK_SUBSCRIBE_FEED = "hook_subscribe_feed"; + + /** hook_headlines_before($feed, $is_cat, $qfh_ret) */ + const HOOK_HEADLINES_BEFORE = "hook_headlines_before"; + + /** hook_render_enclosure($entry, $id, $rv) */ + const HOOK_RENDER_ENCLOSURE = "hook_render_enclosure"; + + /** hook_article_filter_action($article, $action) */ + const HOOK_ARTICLE_FILTER_ACTION = "hook_article_filter_action"; + + /** hook_article_export_feed($line, $feed, $is_cat, $owner_uid) (byref) */ + const HOOK_ARTICLE_EXPORT_FEED = "hook_article_export_feed"; + + /** hook_main_toolbar_button() */ + const HOOK_MAIN_TOOLBAR_BUTTON = "hook_main_toolbar_button"; + + /** hook_enclosure_entry($entry, $id, $rv) (byref) */ + const HOOK_ENCLOSURE_ENTRY = "hook_enclosure_entry"; + + /** hook_format_article($html, $row) */ + const HOOK_FORMAT_ARTICLE = "hook_format_article"; + + /** @deprecated removed, do not use */ + const HOOK_FORMAT_ARTICLE_CDM = "hook_format_article_cdm"; + + /** hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed_id, $auth_login, $auth_pass) (byref) */ + const HOOK_FEED_BASIC_INFO = "hook_feed_basic_info"; + + /** hook_send_local_file($filename) */ + const HOOK_SEND_LOCAL_FILE = "hook_send_local_file"; + + /** hook_unsubscribe_feed($feed_id, $owner_uid) */ + const HOOK_UNSUBSCRIBE_FEED = "hook_unsubscribe_feed"; + + /** hook_send_mail(Mailer $mailer, $params) */ + const HOOK_SEND_MAIL = "hook_send_mail"; + + /** hook_filter_triggered($feed_id, $owner_uid, $article, $matched_filters, $matched_rules, $article_filters) */ + const HOOK_FILTER_TRIGGERED = "hook_filter_triggered"; + + /** hook_get_full_text($url) */ + const HOOK_GET_FULL_TEXT = "hook_get_full_text"; + + /** hook_article_image($enclosures, $content, $site_url) */ + const HOOK_ARTICLE_IMAGE = "hook_article_image"; + + /** hook_feed_tree() */ + const HOOK_FEED_TREE = "hook_feed_tree"; + + /** hook_iframe_whitelisted($url) */ + const HOOK_IFRAME_WHITELISTED = "hook_iframe_whitelisted"; + + /** hook_enclosure_imported($enclosure, $feed) */ + const HOOK_ENCLOSURE_IMPORTED = "hook_enclosure_imported"; + + /** hook_headlines_custom_sort_map() */ + const HOOK_HEADLINES_CUSTOM_SORT_MAP = "hook_headlines_custom_sort_map"; + + /** hook_headlines_custom_sort_override($order) */ const HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE = "hook_headlines_custom_sort_override"; - // hook_headlines_custom_sort_override($order) + + /** hook_headline_toolbar_select_menu_item($feed_id, $is_cat) */ const HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM = "hook_headline_toolbar_select_menu_item"; - // hook_headline_toolbar_select_menu_item($feed_id, $is_cat) + + + /** hook_pre_subscribe($url, $auth_login, $auth_pass) (byref) */ + const HOOK_PRE_SUBSCRIBE = "hook_pre_subscribe"; const KIND_ALL = 1; const KIND_SYSTEM = 2; @@ -103,12 +200,12 @@ class PluginHost { $this->plugins[$name] = $plugin; } - // needed for compatibility with API 1 + /** needed for compatibility with API 1 */ function get_link() { return false; } - // needed for compatibility with API 2 (?) + /** needed for compatibility with API 2 (?) */ function get_dbh() { return false; } diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php index 5f7635736..95bbcd190 100755 --- a/classes/pref/feeds.php +++ b/classes/pref/feeds.php @@ -12,19 +12,12 @@ class Pref_Feeds extends Handler_Protected { } public static function get_ts_languages() { - $rv = []; - - if (Config::get(Config::DB_TYPE) == "pgsql") { - $dbh = Db::pdo(); - - $res = $dbh->query("SELECT cfgname FROM pg_ts_config"); - - while ($row = $res->fetch()) { - array_push($rv, ucfirst($row['cfgname'])); - } + if (Config::get(Config::DB_TYPE) == 'pgsql') { + return array_map('ucfirst', + array_column(ORM::for_table('pg_ts_config')->select('cfgname')->find_array(), 'cfgname')); } - return $rv; + return []; } function renameCat() { @@ -51,61 +44,60 @@ class Pref_Feeds extends Handler_Protected { $show_empty_cats = clean($_REQUEST['force_show_empty'] ?? false) || (clean($_REQUEST['mode'] ?? 0) != 2 && !$search); - $items = array(); - - $sth = $this->pdo->prepare("SELECT id, title FROM ttrss_feed_categories - WHERE owner_uid = ? AND parent_cat = ? ORDER BY order_id, title"); - $sth->execute([$_SESSION['uid'], $cat_id]); - - while ($line = $sth->fetch()) { - - $cat = array(); - $cat['id'] = 'CAT:' . $line['id']; - $cat['bare_id'] = (int)$line['id']; - $cat['name'] = $line['title']; - $cat['items'] = array(); - $cat['checkbox'] = false; - $cat['type'] = 'category'; - $cat['unread'] = -1; - $cat['child_unread'] = -1; - $cat['auxcounter'] = -1; - $cat['parent_id'] = $cat_id; - - $cat['items'] = $this->get_category_items($line['id']); + $items = []; + + $feed_categories = ORM::for_table('ttrss_feed_categories') + ->select_many('id', 'title') + ->where(['owner_uid' => $_SESSION['uid'], 'parent_cat' => $cat_id]) + ->order_by_asc('order_id') + ->order_by_asc('title') + ->find_many(); + + foreach ($feed_categories as $feed_category) { + $cat = [ + 'id' => 'CAT:' . $feed_category->id, + 'bare_id' => (int)$feed_category->id, + 'name' => $feed_category->title, + 'items' => $this->get_category_items($feed_category->id), + 'checkbox' => false, + 'type' => 'category', + 'unread' => -1, + 'child_unread' => -1, + 'auxcounter' => -1, + 'parent_id' => $cat_id, + ]; $num_children = $this->calculate_children_count($cat); $cat['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', (int) $num_children), $num_children); if ($num_children > 0 || $show_empty_cats) array_push($items, $cat); + } + + $feeds_obj = ORM::for_table('ttrss_feeds') + ->select_many('id', 'title', 'last_error', 'update_interval') + ->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated') + ->where(['cat_id' => $cat_id, 'owner_uid' => $_SESSION['uid']]) + ->order_by_asc('order_id') + ->order_by_asc('title'); + if ($search) { + $feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]); } - $fsth = $this->pdo->prepare("SELECT id, title, last_error, - ".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated, update_interval - FROM ttrss_feeds - WHERE cat_id = :cat AND - owner_uid = :uid AND - (:search = '' OR (LOWER(title) LIKE :search OR LOWER(feed_url) LIKE :search)) - ORDER BY order_id, title"); - - $fsth->execute([":cat" => $cat_id, ":uid" => $_SESSION['uid'], ":search" => $search ? "%$search%" : ""]); - - while ($feed_line = $fsth->fetch()) { - $feed = array(); - $feed['id'] = 'FEED:' . $feed_line['id']; - $feed['bare_id'] = (int)$feed_line['id']; - $feed['auxcounter'] = -1; - $feed['name'] = $feed_line['title']; - $feed['checkbox'] = false; - $feed['unread'] = -1; - $feed['error'] = $feed_line['last_error']; - $feed['icon'] = Feeds::_get_icon($feed_line['id']); - $feed['param'] = TimeHelper::make_local_datetime( - $feed_line['last_updated'], true); - $feed['updates_disabled'] = (int)($feed_line['update_interval'] < 0); - - array_push($items, $feed); + foreach ($feeds_obj->find_many() as $feed) { + array_push($items, [ + 'id' => 'FEED:' . $feed->id, + 'bare_id' => (int) $feed->id, + 'auxcounter' => -1, + 'name' => $feed->title, + 'checkbox' => false, + 'unread' => -1, + 'error' => $feed->last_error, + 'icon' => Feeds::_get_icon($feed->id), + 'param' => TimeHelper::make_local_datetime($feed->last_updated, true), + 'updates_disabled' => (int)($feed->update_interval < 0), + ]); } return $items; @@ -181,24 +173,23 @@ class Pref_Feeds extends Handler_Protected { if (get_pref(Prefs::ENABLE_FEED_CATS)) { $cat = $this->feedlist_init_cat(-2); } else { - $cat['items'] = array(); + $cat['items'] = []; } - $num_labels = 0; - while ($line = $sth->fetch()) { - ++$num_labels; - - $label_id = Labels::label_to_feed_id($line['id']); - - $feed = $this->feedlist_init_feed($label_id, false, 0); - - $feed['fg_color'] = $line['fg_color']; - $feed['bg_color'] = $line['bg_color']; - - array_push($cat['items'], $feed); - } + $labels = ORM::for_table('ttrss_labels2') + ->where('owner_uid', $_SESSION['uid']) + ->order_by_asc('caption') + ->find_many(); + + if (count($labels)) { + foreach ($labels as $label) { + $label_id = Labels::label_to_feed_id($label->id); + $feed = $this->feedlist_init_feed($label_id, false, 0); + $feed['fg_color'] = $label->fg_color; + $feed['bg_color'] = $label->bg_color; + array_push($cat['items'], $feed); + } - if ($num_labels) { if ($enable_cats) { array_push($root['items'], $cat); } else { @@ -211,23 +202,26 @@ class Pref_Feeds extends Handler_Protected { $show_empty_cats = clean($_REQUEST['force_show_empty'] ?? false) || (clean($_REQUEST['mode'] ?? 0) != 2 && !$search); - $sth = $this->pdo->prepare("SELECT id, title FROM ttrss_feed_categories - WHERE owner_uid = ? AND parent_cat IS NULL ORDER BY order_id, title"); - $sth->execute([$_SESSION['uid']]); - - while ($line = $sth->fetch()) { - $cat = array(); - $cat['id'] = 'CAT:' . $line['id']; - $cat['bare_id'] = (int)$line['id']; - $cat['auxcounter'] = -1; - $cat['name'] = $line['title']; - $cat['items'] = array(); - $cat['checkbox'] = false; - $cat['type'] = 'category'; - $cat['unread'] = -1; - $cat['child_unread'] = -1; - - $cat['items'] = $this->get_category_items($line['id']); + $feed_categories = ORM::for_table('ttrss_feed_categories') + ->select_many('id', 'title') + ->where('owner_uid', $_SESSION['uid']) + ->where_null('parent_cat') + ->order_by_asc('order_id') + ->order_by_asc('title') + ->find_many(); + + foreach ($feed_categories as $feed_category) { + $cat = [ + 'id' => 'CAT:' . $feed_category->id, + 'bare_id' => (int) $feed_category->id, + 'auxcounter' => -1, + 'name' => $feed_category->title, + 'items' => $this->get_category_items($feed_category->id), + 'checkbox' => false, + 'type' => 'category', + 'unread' => -1, + 'child_unread' => -1, + ]; $num_children = $this->calculate_children_count($cat); $cat['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', (int) $num_children), $num_children); @@ -235,47 +229,48 @@ class Pref_Feeds extends Handler_Protected { if ($num_children > 0 || $show_empty_cats) array_push($root['items'], $cat); - $root['param'] += count($cat['items']); + //$root['param'] += count($cat['items']); } /* Uncategorized is a special case */ + $cat = [ + 'id' => 'CAT:0', + 'bare_id' => 0, + 'auxcounter' => -1, + 'name' => __('Uncategorized'), + 'items' => [], + 'type' => 'category', + 'checkbox' => false, + 'unread' => -1, + 'child_unread' => -1, + ]; + + $feeds_obj = ORM::for_table('ttrss_feeds') + ->select_many('id', 'title', 'last_error', 'update_interval') + ->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated') + ->where('owner_uid', $_SESSION['uid']) + ->where_null('cat_id') + ->order_by_asc('order_id') + ->order_by_asc('title'); - $cat = array(); - $cat['id'] = 'CAT:0'; - $cat['bare_id'] = 0; - $cat['auxcounter'] = -1; - $cat['name'] = __("Uncategorized"); - $cat['items'] = array(); - $cat['type'] = 'category'; - $cat['checkbox'] = false; - $cat['unread'] = -1; - $cat['child_unread'] = -1; - - $fsth = $this->pdo->prepare("SELECT id, title,last_error, - ".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated, update_interval - FROM ttrss_feeds - WHERE cat_id IS NULL AND - owner_uid = :uid AND - (:search = '' OR (LOWER(title) LIKE :search OR LOWER(feed_url) LIKE :search)) - ORDER BY order_id, title"); - $fsth->execute([":uid" => $_SESSION['uid'], ":search" => $search ? "%$search%" : ""]); - - while ($feed_line = $fsth->fetch()) { - $feed = array(); - $feed['id'] = 'FEED:' . $feed_line['id']; - $feed['bare_id'] = (int)$feed_line['id']; - $feed['auxcounter'] = -1; - $feed['name'] = $feed_line['title']; - $feed['checkbox'] = false; - $feed['error'] = $feed_line['last_error']; - $feed['icon'] = Feeds::_get_icon($feed_line['id']); - $feed['param'] = TimeHelper::make_local_datetime( - $feed_line['last_updated'], true); - $feed['unread'] = -1; - $feed['type'] = 'feed'; - $feed['updates_disabled'] = (int)($feed_line['update_interval'] < 0); - - array_push($cat['items'], $feed); + if ($search) { + $feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]); + } + + foreach ($feeds_obj->find_many() as $feed) { + array_push($cat['items'], [ + 'id' => 'FEED:' . $feed->id, + 'bare_id' => (int) $feed->id, + 'auxcounter' => -1, + 'name' => $feed->title, + 'checkbox' => false, + 'error' => $feed->last_error, + 'icon' => Feeds::_get_icon($feed->id), + 'param' => TimeHelper::make_local_datetime($feed->last_updated, true), + 'unread' => -1, + 'type' => 'feed', + 'updates_disabled' => (int)($feed->update_interval < 0), + ]); } $cat['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', count($cat['items'])), count($cat['items'])); @@ -287,46 +282,41 @@ class Pref_Feeds extends Handler_Protected { $root['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', (int) $num_children), $num_children); } else { - $fsth = $this->pdo->prepare("SELECT id, title, last_error, - ".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated, update_interval - FROM ttrss_feeds - WHERE owner_uid = :uid AND - (:search = '' OR (LOWER(title) LIKE :search OR LOWER(feed_url) LIKE :search)) - ORDER BY order_id, title"); - $fsth->execute([":uid" => $_SESSION['uid'], ":search" => $search ? "%$search%" : ""]); - - while ($feed_line = $fsth->fetch()) { - $feed = array(); - $feed['id'] = 'FEED:' . $feed_line['id']; - $feed['bare_id'] = (int)$feed_line['id']; - $feed['auxcounter'] = -1; - $feed['name'] = $feed_line['title']; - $feed['checkbox'] = false; - $feed['error'] = $feed_line['last_error']; - $feed['icon'] = Feeds::_get_icon($feed_line['id']); - $feed['param'] = TimeHelper::make_local_datetime( - $feed_line['last_updated'], true); - $feed['unread'] = -1; - $feed['type'] = 'feed'; - $feed['updates_disabled'] = (int)($feed_line['update_interval'] < 0); - - array_push($root['items'], $feed); - } + $feeds_obj = ORM::for_table('ttrss_feeds') + ->select_many('id', 'title', 'last_error', 'update_interval') + ->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated') + ->where('owner_uid', $_SESSION['uid']) + ->order_by_asc('order_id') + ->order_by_asc('title'); - $root['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', count($root['items'])), count($root['items'])); - } + if ($search) { + $feeds_obj->where_raw('(LOWER(title) LIKE ? OR LOWER(feed_url) LIKE LOWER(?))', ["%$search%", "%$search%"]); + } - $fl = array(); - $fl['identifier'] = 'id'; - $fl['label'] = 'name'; + foreach ($feeds_obj->find_many() as $feed) { + array_push($root['items'], [ + 'id' => 'FEED:' . $feed->id, + 'bare_id' => (int) $feed->id, + 'auxcounter' => -1, + 'name' => $feed->title, + 'checkbox' => false, + 'error' => $feed->last_error, + 'icon' => Feeds::_get_icon($feed->id), + 'param' => TimeHelper::make_local_datetime($feed->last_updated, true), + 'unread' => -1, + 'type' => 'feed', + 'updates_disabled' => (int)($feed->update_interval < 0), + ]); + } - if (clean($_REQUEST['mode'] ?? 0) != 2) { - $fl['items'] = array($root); - } else { - $fl['items'] = $root['items']; + $root['param'] = sprintf(_ngettext('(%d feed)', '(%d feeds)', count($root['items'])), count($root['items'])); } - return $fl; + return [ + 'identifier' => 'id', + 'label' => 'name', + 'items' => clean($_REQUEST['mode'] ?? 0) != 2 ? [$root] : $root['items'], + ]; } function catsortreset() { @@ -359,10 +349,14 @@ class Pref_Feeds extends Handler_Protected { $parent_qpart = null; } - $sth = $this->pdo->prepare("UPDATE ttrss_feed_categories - SET parent_cat = ? WHERE id = ? AND - owner_uid = ?"); - $sth->execute([$parent_qpart, $bare_item_id, $_SESSION['uid']]); + $feed_category = ORM::for_table('ttrss_feed_categories') + ->where('owner_uid', $_SESSION['uid']) + ->find_one($bare_item_id); + + if ($feed_category) { + $feed_category->parent_cat = $parent_qpart; + $feed_category->save(); + } } $order_id = 1; @@ -380,22 +374,27 @@ class Pref_Feeds extends Handler_Protected { if (strpos($id, "FEED") === 0) { - $cat_id = ($item_id != "root") ? $bare_item_id : null; - - $sth = $this->pdo->prepare("UPDATE ttrss_feeds - SET order_id = ?, cat_id = ? - WHERE id = ? AND owner_uid = ?"); - - $sth->execute([$order_id, $cat_id ? $cat_id : null, $bare_id, $_SESSION['uid']]); + $feed = ORM::for_table('ttrss_feeds') + ->where('owner_uid', $_SESSION['uid']) + ->find_one($bare_id); + if ($feed) { + $feed->order_id = $order_id; + $feed->cat_id = ($item_id != "root" && $bare_item_id) ? $bare_item_id : null; + $feed->save(); + } } else if (strpos($id, "CAT:") === 0) { $this->process_category_order($data_map, $item['_reference'], $item_id, $nest_level+1); - $sth = $this->pdo->prepare("UPDATE ttrss_feed_categories - SET order_id = ? WHERE id = ? AND - owner_uid = ?"); - $sth->execute([$order_id, $bare_id, $_SESSION['uid']]); + $feed_category = ORM::for_table('ttrss_feed_categories') + ->where('owner_uid', $_SESSION['uid']) + ->find_one($bare_id); + + if ($feed_category) { + $feed_category->order_id = $order_id; + $feed_category->save(); + } } } @@ -1120,41 +1119,38 @@ class Pref_Feeds extends Handler_Protected { $interval_qpart = "DATE_SUB(NOW(), INTERVAL 3 MONTH)"; } - $sth = $this->pdo->prepare("SELECT ttrss_feeds.title, ttrss_feeds.site_url, - ttrss_feeds.feed_url, ttrss_feeds.id, MAX(updated) AS last_article - FROM ttrss_feeds, ttrss_entries, ttrss_user_entries WHERE - (SELECT MAX(updated) FROM ttrss_entries, ttrss_user_entries WHERE - ttrss_entries.id = ref_id AND - ttrss_user_entries.feed_id = ttrss_feeds.id) < $interval_qpart - AND ttrss_feeds.owner_uid = ? AND - ttrss_user_entries.feed_id = ttrss_feeds.id AND - ttrss_entries.id = ref_id - GROUP BY ttrss_feeds.title, ttrss_feeds.id, ttrss_feeds.site_url, ttrss_feeds.feed_url - ORDER BY last_article"); - $sth->execute([$_SESSION['uid']]); - - $rv = []; - - while ($row = $sth->fetch(PDO::FETCH_ASSOC)) { - $row['last_article'] = TimeHelper::make_local_datetime($row['last_article'], false); - array_push($rv, $row); + $inactive_feeds = ORM::for_table('ttrss_feeds') + ->table_alias('f') + ->select_many('f.id', 'f.title', 'f.site_url', 'f.feed_url') + ->select_expr('MAX(e.updated)', 'last_article') + ->join('ttrss_user_entries', [ 'ue.feed_id', '=', 'f.id'], 'ue') + ->join('ttrss_entries', ['e.id', '=', 'ue.ref_id'], 'e') + ->where('f.owner_uid', $_SESSION['uid']) + ->where_raw( + "(SELECT MAX(ttrss_entries.updated) + FROM ttrss_entries + JOIN ttrss_user_entries ON ttrss_entries.id = ttrss_user_entries.ref_id + WHERE ttrss_user_entries.feed_id = f.id) < $interval_qpart") + ->group_by('f.title') + ->group_by('f.id') + ->group_by('f.site_url') + ->group_by('f.feed_url') + ->order_by_asc('last_article') + ->find_array(); + + foreach ($inactive_feeds as $inactive_feed) { + $inactive_feed['last_article'] = TimeHelper::make_local_datetime($inactive_feed['last_article'], false); } - print json_encode($rv); + print json_encode($inactive_feeds); } function feedsWithErrors() { - $sth = $this->pdo->prepare("SELECT id,title,feed_url,last_error,site_url - FROM ttrss_feeds WHERE last_error != '' AND owner_uid = ?"); - $sth->execute([$_SESSION['uid']]); - - $rv = []; - - while ($row = $sth->fetch()) { - array_push($rv, $row); - } - - print json_encode($rv); + print json_encode(ORM::for_table('ttrss_feeds') + ->select_many('id', 'title', 'feed_url', 'last_error', 'site_url') + ->where_not_equal('last_error', '') + ->where('owner_uid', $_SESSION['uid']) + ->find_array()); } static function remove_feed($id, $owner_uid) { @@ -1272,7 +1268,7 @@ class Pref_Feeds extends Handler_Protected { $c = 0; foreach ($cat['items'] ?? [] as $child) { - if ($child['type'] ?? '' == 'category') { + if (($child['type'] ?? '') == 'category') { $c += $this->calculate_children_count($child); } else { $c += 1; diff --git a/classes/pref/filters.php b/classes/pref/filters.php index 29d309dbb..c4017e4ec 100755 --- a/classes/pref/filters.php +++ b/classes/pref/filters.php @@ -203,7 +203,7 @@ class Pref_Filters extends Handler_Protected { } else { $where = $line["cat_filter"] ? - Feeds::_get_cat_title($line["cat_id"]) : + Feeds::_get_cat_title($line["cat_id"] ?? 0) : ($line["feed_id"] ? Feeds::_get_title($line["feed_id"]) : __("All feeds")); } diff --git a/classes/pref/labels.php b/classes/pref/labels.php index 2cdb919ce..0eb88ea36 100644 --- a/classes/pref/labels.php +++ b/classes/pref/labels.php @@ -160,7 +160,7 @@ class Pref_Labels extends Handler_Protected { function add() { $caption = clean($_REQUEST["caption"]); - $output = clean($_REQUEST["output"]); + $output = clean($_REQUEST["output"] ?? false); if ($caption) { if (Labels::create($caption)) { diff --git a/classes/rpc.php b/classes/rpc.php index 23a45d951..b6c4a5fc9 100755 --- a/classes/rpc.php +++ b/classes/rpc.php @@ -522,6 +522,7 @@ class RPC extends Handler_Protected { WHERE errno NOT IN (".E_USER_NOTICE.", ".E_USER_DEPRECATED.") AND $log_interval AND + errstr NOT LIKE '%Returning bool from comparison function is deprecated%' AND errstr NOT LIKE '%imagecreatefromstring(): Data is not in a recognized format%'"); $sth->execute(); diff --git a/classes/rssutils.php b/classes/rssutils.php index 216792a0e..87e52ba42 100755 --- a/classes/rssutils.php +++ b/classes/rssutils.php @@ -646,7 +646,7 @@ class RSSUtils { $entry_title = strip_tags($item->get_title()); - $entry_link = rewrite_relative_url($site_url, clean($item->get_link())); + $entry_link = UrlHelper::rewrite_relative($site_url, clean($item->get_link()), "a", "href"); $entry_language = mb_substr(trim($item->get_language()), 0, 2); diff --git a/classes/sanitizer.php b/classes/sanitizer.php index 0a444a296..3f6e9504e 100644 --- a/classes/sanitizer.php +++ b/classes/sanitizer.php @@ -68,7 +68,7 @@ class Sanitizer { // $rewrite_base_url = $site_url ? $site_url : Config::get_self_url(); $rewrite_base_url = $site_url ? $site_url : "http://domain.invalid/"; - $entries = $xpath->query('(//a[@href]|//img[@src]|//source[@srcset|@src])'); + $entries = $xpath->query('(//a[@href]|//img[@src]|//source[@srcset|@src]|//video[@poster])'); foreach ($entries as $entry) { @@ -100,6 +100,11 @@ class Sanitizer { $entry->setAttribute("srcset", RSSUtils::encode_srcset($matches)); } + if ($entry->hasAttribute('poster')) { + $entry->setAttribute('poster', + UrlHelper::rewrite_relative($rewrite_base_url, $entry->getAttribute('poster'), $entry->tagName, "poster")); + } + if ($entry->hasAttribute('src') && ($owner && get_pref(Prefs::STRIP_IMAGES, $owner)) || $force_remove_images || ($_SESSION["bw_limit"] ?? false)) { diff --git a/classes/urlhelper.php b/classes/urlhelper.php index b2c1331b6..0e4834b72 100644 --- a/classes/urlhelper.php +++ b/classes/urlhelper.php @@ -52,7 +52,7 @@ class UrlHelper { $owner_attribute == "href") { return $rel_url; // allow limited subset of inline base64-encoded images for IMG elements - } else if ($rel_parts["scheme"] ?? "" == "data" && + } else if (($rel_parts["scheme"] ?? "") == "data" && preg_match('%^image/(webp|gif|jpg|png|svg);base64,%', $rel_parts["path"]) && $owner_element == "img" && $owner_attribute == "src") { @@ -281,8 +281,7 @@ class UrlHelper { curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY); - curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent : - SELF_USER_AGENT); + curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent : Config::get_user_agent()); curl_setopt($ch, CURLOPT_ENCODING, ""); if ($http_referrer) |