diff options
Diffstat (limited to 'classes')
-rwxr-xr-x | classes/article.php | 25 | ||||
-rw-r--r-- | classes/dlg.php | 2 | ||||
-rwxr-xr-x | classes/feeditem/common.php | 9 | ||||
-rwxr-xr-x | classes/feeds.php | 422 | ||||
-rwxr-xr-x | classes/handler/public.php | 38 | ||||
-rwxr-xr-x | classes/logger/sql.php | 4 | ||||
-rw-r--r-- | classes/opml.php | 6 | ||||
-rwxr-xr-x | classes/pref/feeds.php | 6 | ||||
-rwxr-xr-x | classes/pref/filters.php | 11 | ||||
-rw-r--r-- | classes/pref/prefs.php | 16 | ||||
-rw-r--r-- | classes/pref/system.php | 2 | ||||
-rw-r--r-- | classes/pref/users.php | 12 | ||||
-rwxr-xr-x | classes/rpc.php | 2 | ||||
-rwxr-xr-x | classes/rssutils.php | 196 |
14 files changed, 660 insertions, 91 deletions
diff --git a/classes/article.php b/classes/article.php index c23a1b820..43b25f94f 100755 --- a/classes/article.php +++ b/classes/article.php @@ -306,9 +306,9 @@ class Article extends Handler_Protected { $sth->execute([$int_id, $_SESSION['uid']]); foreach ($tags as $tag) { - $tag = sanitize_tag($tag); + $tag = Article::sanitize_tag($tag); - if (!tag_is_valid($tag)) { + if (!Article::tag_is_valid($tag)) { continue; } @@ -800,4 +800,25 @@ class Article extends Handler_Protected { return $rv; } + static function sanitize_tag($tag) { + $tag = trim($tag); + + $tag = mb_strtolower($tag, 'utf-8'); + + $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag); + + if (DB_TYPE == "mysql") { + $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag); + } + + return $tag; + } + + static function tag_is_valid($tag) { + if (!$tag || is_numeric($tag) || mb_strlen($tag) > 250) + return false; + + return true; + } + } diff --git a/classes/dlg.php b/classes/dlg.php index 4489af51a..d3e82ee59 100644 --- a/classes/dlg.php +++ b/classes/dlg.php @@ -161,7 +161,7 @@ class Dlg extends Handler_Protected { $feed_id = $this->params[0]; $is_cat = (bool) $this->params[1]; - $key = get_feed_access_key($feed_id, $is_cat); + $key = Feeds::get_feed_access_key($feed_id, $is_cat); $url_path = htmlspecialchars($this->params[2]) . "&key=" . $key; diff --git a/classes/feeditem/common.php b/classes/feeditem/common.php index de8d3aefa..3193ed273 100755 --- a/classes/feeditem/common.php +++ b/classes/feeditem/common.php @@ -41,11 +41,14 @@ abstract class FeedItem_Common extends FeedItem { return clean($author->nodeValue); } - $author = $this->xpath->query("dc:creator", $this->elem)->item(0); + $author_elems = $this->xpath->query("dc:creator", $this->elem); + $authors = []; - if ($author) { - return clean($author->nodeValue); + foreach ($author_elems as $author) { + array_push($authors, clean($author->nodeValue)); } + + return implode(", ", $authors); } function get_comments_url() { diff --git a/classes/feeds.php b/classes/feeds.php index 86fa45ea7..c1f973830 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -860,7 +860,7 @@ class Feeds extends Handler_Protected { // fall back in case of no plugins if (!$search_qpart) { - list($search_qpart, $search_words) = search_to_sql($search[0], $search[1]); + list($search_qpart, $search_words) = Feeds::search_to_sql($search[0], $search[1]); } } else { $search_qpart = "true"; @@ -1135,9 +1135,9 @@ class Feeds extends Handler_Protected { $pdo = Db::pdo(); - $url = fix_url($url); + $url = Feeds::fix_url($url); - if (!$url || !validate_feed_url($url)) return array("code" => 2); + if (!$url || !Feeds::validate_feed_url($url)) return array("code" => 2); $contents = @fetch_file_contents($url, false, $auth_login, $auth_pass); @@ -1153,8 +1153,8 @@ class Feeds extends Handler_Protected { return array("code" => 5, "message" => $fetch_last_error); } - if (mb_strpos($fetch_last_content_type, "html") !== FALSE && is_html($contents)) { - $feedUrls = get_feeds_from_html($url, $contents); + if (mb_strpos($fetch_last_content_type, "html") !== FALSE && Feeds::is_html($contents)) { + $feedUrls = Feeds::get_feeds_from_html($url, $contents); if (count($feedUrls) == 0) { return array("code" => 3); @@ -1456,7 +1456,7 @@ class Feeds extends Handler_Protected { // fall back in case of no plugins if (!$search_query_part) { - list($search_query_part, $search_words) = search_to_sql($search, $search_language); + list($search_query_part, $search_words) = Feeds::search_to_sql($search, $search_language); } if (DB_TYPE == "pgsql") { @@ -1683,6 +1683,13 @@ class Feeds extends Handler_Protected { $offset_query_part = ""; } + if ($start_ts) { + $start_ts_formatted = date("Y/m/d H:i:s", strtotime($start_ts)); + $start_ts_query_part = "date_entered >= '$start_ts_formatted' AND"; + } else { + $start_ts_query_part = ""; + } + if (is_numeric($feed)) { // proper override_order applied above if ($vfeed_query_part && !$ignore_vfeed_group && get_pref('VFEED_GROUP_BY_FEED', $owner_uid)) { @@ -1706,13 +1713,6 @@ class Feeds extends Handler_Protected { if ($vfeed_query_part) $vfeed_query_part .= "favicon_avg_color,"; - if ($start_ts) { - $start_ts_formatted = date("Y/m/d H:i:s", strtotime($start_ts)); - $start_ts_query_part = "date_entered >= '$start_ts_formatted' AND"; - } else { - $start_ts_query_part = ""; - } - $first_id = 0; $first_id_query_strategy_part = $query_strategy_part; @@ -1845,6 +1845,7 @@ class Feeds extends Handler_Protected { tag_name = ".$pdo->quote($feed)." AND $view_query_part $search_query_part + $start_ts_query_part $query_strategy_part ORDER BY $order_by $limit_query_part $offset_query_part"; @@ -1922,5 +1923,400 @@ class Feeds extends Handler_Protected { return $colormap[$sum]; } + static function get_feeds_from_html($url, $content) { + $url = Feeds::fix_url($url); + $baseUrl = substr($url, 0, strrpos($url, '/') + 1); + + $feedUrls = []; + + $doc = new DOMDocument(); + if ($doc->loadHTML($content)) { + $xpath = new DOMXPath($doc); + $entries = $xpath->query('/html/head/link[@rel="alternate" and '. + '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]'); + + foreach ($entries as $entry) { + if ($entry->hasAttribute('href')) { + $title = $entry->getAttribute('title'); + if ($title == '') { + $title = $entry->getAttribute('type'); + } + $feedUrl = rewrite_relative_url( + $baseUrl, $entry->getAttribute('href') + ); + $feedUrls[$feedUrl] = $title; + } + } + } + return $feedUrls; + } + + static function is_html($content) { + return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 8192)) !== 0; + } + + static function validate_feed_url($url) { + $parts = parse_url($url); + + return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https'); + } + + /** + * Fixes incomplete URLs by prepending "http://". + * Also replaces feed:// with http://, and + * prepends a trailing slash if the url is a domain name only. + * + * @param string $url Possibly incomplete URL + * + * @return string Fixed URL. + */ + static function fix_url($url) { + + // support schema-less urls + if (strpos($url, '//') === 0) { + $url = 'https:' . $url; + } + + if (strpos($url, '://') === false) { + $url = 'http://' . $url; + } else if (substr($url, 0, 5) == 'feed:') { + $url = 'http:' . substr($url, 5); + } + + //prepend slash if the URL has no slash in it + // "http://www.example" -> "http://www.example/" + if (strpos($url, '/', strpos($url, ':') + 3) === false) { + $url .= '/'; + } + + //convert IDNA hostname to punycode if possible + if (function_exists("idn_to_ascii")) { + $parts = parse_url($url); + if (mb_detect_encoding($parts['host']) != 'ASCII') + { + $parts['host'] = idn_to_ascii($parts['host']); + $url = build_url($parts); + } + } + + if ($url != "http:///") + return $url; + else + return ''; + } + + static function add_feed_category($feed_cat, $parent_cat_id = false, $order_id = 0) { + + if (!$feed_cat) return false; + + $feed_cat = mb_substr($feed_cat, 0, 250); + if (!$parent_cat_id) $parent_cat_id = null; + + $pdo = Db::pdo(); + $tr_in_progress = false; + + try { + $pdo->beginTransaction(); + } catch (Exception $e) { + $tr_in_progress = true; + } + + $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories + WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL)) + AND title = :title AND owner_uid = :uid"); + $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]); + + if (!$sth->fetch()) { + + $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat,order_id) + VALUES (?, ?, ?, ?)"); + $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id, (int)$order_id]); + + if (!$tr_in_progress) $pdo->commit(); + + return true; + } + + $pdo->commit(); + + return false; + } + + static function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) { + + if (!$owner_uid) $owner_uid = $_SESSION["uid"]; + + $is_cat = bool_to_sql_bool($is_cat); + + $pdo = Db::pdo(); + + $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys + WHERE feed_id = ? AND is_cat = ? + AND owner_uid = ?"); + $sth->execute([$feed_id, $is_cat, $owner_uid]); + + if ($row = $sth->fetch()) { + return $row["access_key"]; + } else { + $key = uniqid_short(); + + $sth = $pdo->prepare("INSERT INTO ttrss_access_keys + (access_key, feed_id, is_cat, owner_uid) + VALUES (?, ?, ?, ?)"); + + $sth->execute([$key, $feed_id, $is_cat, $owner_uid]); + + return $key; + } + } + + /** + * Purge a feed old posts. + * + * @param mixed $link A database connection. + * @param mixed $feed_id The id of the purged feed. + * @param mixed $purge_interval Olderness of purged posts. + * @param boolean $debug Set to True to enable the debug. False by default. + * @access public + * @return void + */ + static function purge_feed($feed_id, $purge_interval) { + + if (!$purge_interval) $purge_interval = Feeds::feed_purge_interval($feed_id); + + $pdo = Db::pdo(); + + $sth = $pdo->prepare("SELECT owner_uid FROM ttrss_feeds WHERE id = ?"); + $sth->execute([$feed_id]); + + $owner_uid = false; + + if ($row = $sth->fetch()) { + $owner_uid = $row["owner_uid"]; + } + + if ($purge_interval == -1 || !$purge_interval) { + if ($owner_uid) { + CCache::update($feed_id, $owner_uid); + } + return; + } + + if (!$owner_uid) return; + + if (FORCE_ARTICLE_PURGE == 0) { + $purge_unread = get_pref("PURGE_UNREAD_ARTICLES", + $owner_uid, false); + } else { + $purge_unread = true; + $purge_interval = FORCE_ARTICLE_PURGE; + } + + if (!$purge_unread) + $query_limit = " unread = false AND "; + else + $query_limit = ""; + + $purge_interval = (int) $purge_interval; + + if (DB_TYPE == "pgsql") { + $sth = $pdo->prepare("DELETE FROM ttrss_user_entries + USING ttrss_entries + WHERE ttrss_entries.id = ref_id AND + marked = false AND + feed_id = ? AND + $query_limit + ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'"); + $sth->execute([$feed_id]); + + } else { + $sth = $pdo->prepare("DELETE FROM ttrss_user_entries + USING ttrss_user_entries, ttrss_entries + WHERE ttrss_entries.id = ref_id AND + marked = false AND + feed_id = ? AND + $query_limit + ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)"); + $sth->execute([$feed_id]); + + } + + $rows = $sth->rowCount(); + + CCache::update($feed_id, $owner_uid); + + Debug::log("Purged feed $feed_id ($purge_interval): deleted $rows articles"); + + return $rows; + } + + static function feed_purge_interval($feed_id) { + + $pdo = DB::pdo(); + + $sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds + WHERE id = ?"); + $sth->execute([$feed_id]); + + if ($row = $sth->fetch()) { + $purge_interval = $row["purge_interval"]; + $owner_uid = $row["owner_uid"]; + + if ($purge_interval == 0) $purge_interval = get_pref( + 'PURGE_OLD_DAYS', $owner_uid); + + return $purge_interval; + + } else { + return -1; + } + } + + static function search_to_sql($search, $search_language) { + + $keywords = str_getcsv(trim($search), " "); + $query_keywords = array(); + $search_words = array(); + $search_query_leftover = array(); + + $pdo = Db::pdo(); + + if ($search_language) + $search_language = $pdo->quote(mb_strtolower($search_language)); + else + $search_language = $pdo->quote("english"); + + foreach ($keywords as $k) { + if (strpos($k, "-") === 0) { + $k = substr($k, 1); + $not = "NOT"; + } else { + $not = ""; + } + + $commandpair = explode(":", mb_strtolower($k), 2); + + switch ($commandpair[0]) { + case "title": + if ($commandpair[1]) { + array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ". + $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))"); + } else { + array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%') + OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))"); + array_push($search_words, $k); + } + break; + case "author": + if ($commandpair[1]) { + array_push($query_keywords, "($not (LOWER(author) LIKE ". + $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))"); + } else { + array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%') + OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))"); + array_push($search_words, $k); + } + break; + case "note": + if ($commandpair[1]) { + if ($commandpair[1] == "true") + array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))"); + else if ($commandpair[1] == "false") + array_push($query_keywords, "($not (note IS NULL OR note = ''))"); + else + array_push($query_keywords, "($not (LOWER(note) LIKE ". + $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))"); + } else { + array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").") + OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))"); + if (!$not) array_push($search_words, $k); + } + break; + case "star": + + if ($commandpair[1]) { + if ($commandpair[1] == "true") + array_push($query_keywords, "($not (marked = true))"); + else + array_push($query_keywords, "($not (marked = false))"); + } else { + array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").") + OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))"); + if (!$not) array_push($search_words, $k); + } + break; + case "pub": + if ($commandpair[1]) { + if ($commandpair[1] == "true") + array_push($query_keywords, "($not (published = true))"); + else + array_push($query_keywords, "($not (published = false))"); + + } else { + array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%') + OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))"); + if (!$not) array_push($search_words, $k); + } + break; + case "unread": + if ($commandpair[1]) { + if ($commandpair[1] == "true") + array_push($query_keywords, "($not (unread = true))"); + else + array_push($query_keywords, "($not (unread = false))"); + + } else { + array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").") + OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))"); + if (!$not) array_push($search_words, $k); + } + break; + default: + if (strpos($k, "@") === 0) { + + $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']); + $orig_ts = strtotime(substr($k, 1)); + $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC')); + + //$k = date("Y-m-d", strtotime(substr($k, 1))); + + array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')"); + } else { + + if (DB_TYPE == "pgsql") { + $k = mb_strtolower($k); + array_push($search_query_leftover, $not ? "!$k" : $k); + } else { + array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").") + OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))"); + } + + if (!$not) array_push($search_words, $k); + } + } + } + + if (count($search_query_leftover) > 0) { + + if (DB_TYPE == "pgsql") { + + // if there's no joiners consider this a "simple" search and + // concatenate everything with &, otherwise don't try to mess with tsquery syntax + if (preg_match("/[&|]/", implode(" " , $search_query_leftover))) { + $tsquery = $pdo->quote(implode(" ", $search_query_leftover)); + } else { + $tsquery = $pdo->quote(implode(" & ", $search_query_leftover)); + } + + array_push($query_keywords, + "(tsvector_combined @@ to_tsquery($search_language, $tsquery))"); + } + + } + + $search_query_part = implode("AND", $query_keywords); + + return array($search_query_part, $search_words); + } } diff --git a/classes/handler/public.php b/classes/handler/public.php index 318cecd72..0e990bec7 100755 --- a/classes/handler/public.php +++ b/classes/handler/public.php @@ -75,7 +75,7 @@ class Handler_Public extends Handler { $feed_self_url = get_self_url_prefix() . "/public.php?op=rss&id=$feed&key=" . - get_feed_access_key($feed, false, $owner_uid); + Feeds::get_feed_access_key($feed, false, $owner_uid); if (!$feed_site_url) $feed_site_url = get_self_url_prefix(); @@ -298,23 +298,25 @@ class Handler_Public extends Handler { function share() { $uuid = clean($_REQUEST["key"]); - $sth = $this->pdo->prepare("SELECT ref_id, owner_uid FROM ttrss_user_entries WHERE - uuid = ?"); - $sth->execute([$uuid]); + if ($uuid) { + $sth = $this->pdo->prepare("SELECT ref_id, owner_uid + FROM ttrss_user_entries WHERE uuid = ?"); + $sth->execute([$uuid]); - if ($row = $sth->fetch()) { - header("Content-Type: text/html"); + if ($row = $sth->fetch()) { + header("Content-Type: text/html"); - $id = $row["ref_id"]; - $owner_uid = $row["owner_uid"]; + $id = $row["ref_id"]; + $owner_uid = $row["owner_uid"]; - print $this->format_article($id, $owner_uid); + print $this->format_article($id, $owner_uid); - } else { - header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); - print "Article not found."; + return; + } } + header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); + print "Article not found."; } private function get_article_image($enclosures, $content, $site_url) { @@ -728,18 +730,6 @@ class Handler_Public extends Handler { } } - /* function subtest() { - header("Content-type: text/plain; charset=utf-8"); - - $url = clean($_REQUEST["url"]); - - print "$url\n\n"; - - - print_r(get_feeds_from_html($url, fetch_file_contents($url))); - - } */ - function subscribe() { if (SINGLE_USER_MODE) { login_sequence(); diff --git a/classes/logger/sql.php b/classes/logger/sql.php index 352d71324..989539e5d 100755 --- a/classes/logger/sql.php +++ b/classes/logger/sql.php @@ -12,8 +12,8 @@ class Logger_SQL { $owner_uid = $_SESSION["uid"] ? $_SESSION["uid"] : null; - if (DB_TYPE == "mysql") - $context = substr($context, 0, 65534); + // limit context length, DOMDocument dumps entire XML in here sometimes, which may be huge + $context = mb_substr($context, 0, 8192); // passed error message may contain invalid unicode characters, failing to insert an error here // would break the execution entirely by generating an actual fatal error instead of a E_WARNING etc diff --git a/classes/opml.php b/classes/opml.php index 720798065..6982aea27 100644 --- a/classes/opml.php +++ b/classes/opml.php @@ -31,7 +31,7 @@ class Opml extends Handler_Protected { <body class='claro ttrss_utility'> <h1>".__('OPML Utility')."</h1><div class='content'>"; - add_feed_category("Imported feeds"); + Feeds::add_feed_category("Imported feeds"); $this->opml_notice(__("Importing OPML...")); @@ -515,7 +515,7 @@ class Opml extends Handler_Protected { $order_id = (int) $root_node->attributes->getNamedItem('ttrssSortOrder')->nodeValue; if (!$order_id) $order_id = 0; - add_feed_category($cat_title, $parent_id, $order_id); + Feeds::add_feed_category($cat_title, $parent_id, $order_id); $cat_id = $this->get_feed_category($cat_title, $parent_id); } @@ -627,7 +627,7 @@ class Opml extends Handler_Protected { $url_path = get_self_url_prefix(); $url_path .= "/opml.php?op=publish&key=" . - get_feed_access_key('OPML:Publish', false, $_SESSION["uid"]); + Feeds::get_feed_access_key('OPML:Publish', false, $_SESSION["uid"]); return $url_path; } diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php index 6cbf15a58..c55affd77 100755 --- a/classes/pref/feeds.php +++ b/classes/pref/feeds.php @@ -1166,7 +1166,7 @@ class Pref_Feeds extends Handler_Protected { function addCat() { $feed_cat = trim(clean($_REQUEST["cat"])); - add_feed_category($feed_cat); + Feeds::add_feed_category($feed_cat); } function index() { @@ -1708,7 +1708,7 @@ class Pref_Feeds extends Handler_Protected { foreach ($feeds as $feed) { $feed = trim($feed); - if (validate_feed_url($feed)) { + if (Feeds::validate_feed_url($feed)) { $this->pdo->beginTransaction(); @@ -1750,7 +1750,7 @@ class Pref_Feeds extends Handler_Protected { WHERE feed_id = ? AND is_cat = ? AND owner_uid = ?"); $sth->execute([$feed_id, bool_to_sql_bool($is_cat), $owner_uid]); - return get_feed_access_key($feed_id, $is_cat, $owner_uid); + return Feeds::get_feed_access_key($feed_id, $is_cat, $owner_uid); } // Silent diff --git a/classes/pref/filters.php b/classes/pref/filters.php index 041951b35..a3a0ce77f 100755 --- a/classes/pref/filters.php +++ b/classes/pref/filters.php @@ -976,13 +976,14 @@ class Pref_Filters extends Handler_Protected { print "<section>"; print "<input dojoType=\"dijit.form.ValidationTextBox\" - required=\"true\" id=\"filterDlg_regExp\" - style=\"font-size : 16px; width : 20em;\" + required=\"true\" id=\"filterDlg_regExp\" + onchange='Filters.filterDlgCheckRegExp(this)' + onblur='Filters.filterDlgCheckRegExp(this)' + onfocus='Filters.filterDlgCheckRegExp(this)' + style=\"font-size : 16px; width : 500px\" name=\"reg_exp\" value=\"$reg_exp\"/>"; - print "<div dojoType=\"dijit.Tooltip\" connectId=\"filterDlg_regExp\" position=\"below\"> - ".__("Regular expression, without outer delimiters (i.e. slashes)")." - </div>"; + print "<div dojoType='dijit.Tooltip' id='filterDlg_regExp_tip' connectId='filterDlg_regExp' position='below'></div>"; print "<fieldset>"; print "<label class='checkbox'><input id=\"filterDlg_inverse\" dojoType=\"dijit.form.CheckBox\" diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php index cb19998ce..e7e7a365e 100644 --- a/classes/pref/prefs.php +++ b/classes/pref/prefs.php @@ -224,7 +224,7 @@ class Pref_Prefs extends Handler_Protected { $_SESSION["prefs_op_result"] = ""; print "<div dojoType='dijit.layout.AccordionContainer' region='center'>"; - print "<div dojoType='dijit.layout.AccordionPane' + print "<div dojoType='dijit.layout.AccordionPane' title=\"<i class='material-icons'>person</i> ".__('Personal data / Authentication')."\">"; print "<div dojoType='dijit.layout.TabContainer'>"; @@ -373,7 +373,7 @@ class Pref_Prefs extends Handler_Protected { evt.preventDefault(); if (this.validate()) { Notify.progress('Disabling OTP', true); - + new Ajax.Request('backend.php', { parameters: dojo.objectToQuery(this.getValues()), onComplete: function(transport) { @@ -469,7 +469,7 @@ class Pref_Prefs extends Handler_Protected { print "</div>"; #pane - print "<div dojoType='dijit.layout.AccordionPane' selected='true' + print "<div dojoType='dijit.layout.AccordionPane' selected='true' title=\"<i class='material-icons'>settings</i> ".__('Preferences')."\">"; print "<form dojoType='dijit.form.Form' id='changeSettingsForm'>"; @@ -678,8 +678,8 @@ class Pref_Prefs extends Handler_Protected { onclick=\"dijit.byId('SSL_CERT_SERIAL').attr('value', '')\">" . __('Clear') . "</button>"; - print "<button dojoType='dijit.form.Button' class='alt-info' - onclick='window.open(\"https://tt-rss.org/wiki/SSL+Certificate+Authentication\")'> + print "<button dojoType='dijit.form.Button' class='alt-info' + onclick='window.open(\"https://tt-rss.org/wiki/SSL%20Certificate%20Authentication\")'> <i class='material-icons'>help</i> ".__("More info...")."</button>"; } else if ($pref_name == 'DIGEST_PREFERRED_TIME') { @@ -741,7 +741,7 @@ class Pref_Prefs extends Handler_Protected { print "</div>"; #pane - print "<div dojoType=\"dijit.layout.AccordionPane\" + print "<div dojoType=\"dijit.layout.AccordionPane\" title=\"<i class='material-icons'>extension</i> ".__('Plugins')."\">"; print "<form dojoType=\"dijit.form.Form\" id=\"changePluginsForm\">"; @@ -801,7 +801,7 @@ class Pref_Prefs extends Handler_Protected { ".htmlspecialchars($about[1]). "</label>"; if (@$about[4]) { - print "<button dojoType='dijit.form.Button' class='alt-info' + print "<button dojoType='dijit.form.Button' class='alt-info' onclick='window.open(\"".htmlspecialchars($about[4])."\")'> <i class='material-icons'>open_in_new</i> ".__("More info...")."</button>"; } @@ -840,7 +840,7 @@ class Pref_Prefs extends Handler_Protected { if (count($tmppluginhost->get_all($plugin)) > 0) { if (in_array($name, $system_enabled) || in_array($name, $user_enabled)) { - print " <button dojoType='dijit.form.Button' + print " <button dojoType='dijit.form.Button' onclick=\"Helpers.clearPluginData('$name')\"> <i class='material-icons'>clear</i> ".__("Clear data")."</button>"; } diff --git a/classes/pref/system.php b/classes/pref/system.php index f36fd07bb..d0f8a8273 100644 --- a/classes/pref/system.php +++ b/classes/pref/system.php @@ -54,7 +54,7 @@ class Pref_System extends Handler_Protected { </tr>"; while ($line = $res->fetch()) { - print "<tr class=\"errrow\">"; + print "<tr>"; foreach ($line as $k => $v) { $line[$k] = htmlspecialchars($v); diff --git a/classes/pref/users.php b/classes/pref/users.php index 680290b74..851d4fa9e 100644 --- a/classes/pref/users.php +++ b/classes/pref/users.php @@ -362,7 +362,7 @@ class Pref_Users extends Handler_Protected { print "</div>"; #pane print "<div style='padding : 0px' dojoType='dijit.layout.ContentPane' region='center'>"; - $sort = validate_field($sort, + $sort = $this->validate_field($sort, ["login", "access_level", "created", "num_feeds", "created", "last_login"], "login"); if ($sort != "login") $sort = "$sort DESC"; @@ -435,4 +435,12 @@ class Pref_Users extends Handler_Protected { print "</div>"; #container } - } + + function validate_field($string, $allowed, $default = "") { + if (in_array($string, $allowed)) + return $string; + else + return $default; + } + +} diff --git a/classes/rpc.php b/classes/rpc.php index 725ca9622..8736cbb65 100755 --- a/classes/rpc.php +++ b/classes/rpc.php @@ -591,7 +591,7 @@ class RPC extends Handler_Protected { $rv = []; if (CHECK_FOR_UPDATES && $_SESSION["access_level"] >= 10 && defined("GIT_VERSION_TIMESTAMP")) { - $content = @fetch_file_contents(["url" => "https://tt-rss.org/version.json"]); + $content = @fetch_file_contents(["url" => "https://srv.tt-rss.org/version.json"]); if ($content) { $content = json_decode($content, true); diff --git a/classes/rssutils.php b/classes/rssutils.php index 68e0255ed..4c8da4546 100755 --- a/classes/rssutils.php +++ b/classes/rssutils.php @@ -507,7 +507,7 @@ class RSSUtils { Debug::log("loading filters & labels...", Debug::$LOG_VERBOSE); - $filters = load_filters($feed, $owner_uid); + $filters = RSSUtils::load_filters($feed, $owner_uid); if (Debug::get_loglevel() >= Debug::$LOG_EXTENDED) { print_r($filters); @@ -1071,7 +1071,7 @@ class RSSUtils { $manual_tags = trim_array(explode(",", $f["param"])); foreach ($manual_tags as $tag) { - if (tag_is_valid($tag)) { + if (Article::tag_is_valid($tag)) { array_push($entry_tags, $tag); } } @@ -1115,9 +1115,9 @@ class RSSUtils { foreach ($filtered_tags as $tag) { - $tag = sanitize_tag($tag); + $tag = Article::sanitize_tag($tag); - if (!tag_is_valid($tag)) continue; + if (!Article::tag_is_valid($tag)) continue; $tsth->execute([$tag, $entry_int_id, $owner_uid]); @@ -1147,7 +1147,7 @@ class RSSUtils { Debug::log("purging feed...", Debug::$LOG_VERBOSE); - purge_feed($feed, 0); + Feeds::purge_feed($feed, 0); $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_updated = NOW(), last_unconditional = NOW(), last_error = '' WHERE id = ?"); @@ -1205,32 +1205,31 @@ class RSSUtils { } static function cache_media($html, $site_url) { - libxml_use_internal_errors(true); - $doc = new DOMDocument(); - $doc->loadHTML('<?xml encoding="UTF-8">' . $html); - $xpath = new DOMXPath($doc); + if ($doc->loadHTML($html)) { + $xpath = new DOMXPath($doc); - $entries = $xpath->query('(//img[@src])|(//video/source[@src])|(//audio/source[@src])'); + $entries = $xpath->query('(//img[@src])|(//video/source[@src])|(//audio/source[@src])'); - foreach ($entries as $entry) { - if ($entry->hasAttribute('src') && strpos($entry->getAttribute('src'), "data:") !== 0) { - $src = rewrite_relative_url($site_url, $entry->getAttribute('src')); + foreach ($entries as $entry) { + if ($entry->hasAttribute('src') && strpos($entry->getAttribute('src'), "data:") !== 0) { + $src = rewrite_relative_url($site_url, $entry->getAttribute('src')); - $local_filename = CACHE_DIR . "/images/" . sha1($src); + $local_filename = CACHE_DIR . "/images/" . sha1($src); - Debug::log("cache_media: checking $src", Debug::$LOG_VERBOSE); + Debug::log("cache_media: checking $src", Debug::$LOG_VERBOSE); - if (!file_exists($local_filename)) { - Debug::log("cache_media: downloading: $src to $local_filename", Debug::$LOG_VERBOSE); + if (!file_exists($local_filename)) { + Debug::log("cache_media: downloading: $src to $local_filename", Debug::$LOG_VERBOSE); - $file_content = fetch_file_contents($src); + $file_content = fetch_file_contents($src); - if ($file_content && strlen($file_content) > MIN_CACHE_FILE_SIZE) { - file_put_contents($local_filename, $file_content); + if ($file_content && strlen($file_content) > MIN_CACHE_FILE_SIZE) { + file_put_contents($local_filename, $file_content); + } + } else if (is_writable($local_filename)) { + touch($local_filename); } - } else if (is_writable($local_filename)) { - touch($local_filename); } } } @@ -1517,7 +1516,7 @@ class RSSUtils { $icon_file = ICONS_DIR . "/$feed.ico"; if (!file_exists($icon_file)) { - $favicon_url = get_favicon_url($site_url); + $favicon_url = RSSUtils::get_favicon_url($site_url); if ($favicon_url) { // Limiting to "image" type misses those served with text/plain @@ -1570,4 +1569,155 @@ class RSSUtils { return mb_strpos($feed_data, "\x1f" . "\x8b" . "\x08", 0, "US-ASCII") === 0; } + static function load_filters($feed_id, $owner_uid) { + $filters = array(); + + $feed_id = (int) $feed_id; + $cat_id = (int)Feeds::getFeedCategory($feed_id); + + if ($cat_id == 0) + $null_cat_qpart = "cat_id IS NULL OR"; + else + $null_cat_qpart = ""; + + $pdo = Db::pdo(); + + $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE + owner_uid = ? AND enabled = true ORDER BY order_id, title"); + $sth->execute([$owner_uid]); + + $check_cats = array_merge( + Feeds::getParentCategories($cat_id, $owner_uid), + [$cat_id]); + + $check_cats_str = join(",", $check_cats); + $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats); + + while ($line = $sth->fetch()) { + $filter_id = $line["id"]; + + $match_any_rule = sql_bool_to_bool($line["match_any_rule"]); + + $sth2 = $pdo->prepare("SELECT + r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name + FROM ttrss_filters2_rules AS r, + ttrss_filter_types AS t + WHERE + (match_on IS NOT NULL OR + (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND + (feed_id IS NULL OR feed_id = ?))) AND + filter_type = t.id AND filter_id = ?"); + $sth2->execute([$feed_id, $filter_id]); + + $rules = array(); + $actions = array(); + + while ($rule_line = $sth2->fetch()) { + # print_r($rule_line); + + if ($rule_line["match_on"]) { + $match_on = json_decode($rule_line["match_on"], true); + + if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) { + + $rule = array(); + $rule["reg_exp"] = $rule_line["reg_exp"]; + $rule["type"] = $rule_line["type_name"]; + $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]); + + array_push($rules, $rule); + } else if (!$match_any_rule) { + // this filter contains a rule that doesn't match to this feed/category combination + // thus filter has to be rejected + + $rules = []; + break; + } + + } else { + + $rule = array(); + $rule["reg_exp"] = $rule_line["reg_exp"]; + $rule["type"] = $rule_line["type_name"]; + $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]); + + array_push($rules, $rule); + } + } + + if (count($rules) > 0) { + $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name + FROM ttrss_filters2_actions AS a, + ttrss_filter_actions AS t + WHERE + action_id = t.id AND filter_id = ?"); + $sth2->execute([$filter_id]); + + while ($action_line = $sth2->fetch()) { + # print_r($action_line); + + $action = array(); + $action["type"] = $action_line["type_name"]; + $action["param"] = $action_line["action_param"]; + + array_push($actions, $action); + } + } + + $filter = []; + $filter["id"] = $filter_id; + $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]); + $filter["inverse"] = sql_bool_to_bool($line["inverse"]); + $filter["rules"] = $rules; + $filter["actions"] = $actions; + + if (count($rules) > 0 && count($actions) > 0) { + array_push($filters, $filter); + } + } + + return $filters; + } + + /** + * Try to determine the favicon URL for a feed. + * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/) + * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php + * + * @param string $url A feed or page URL + * @access public + * @return mixed The favicon URL, or false if none was found. + */ + static function get_favicon_url($url) { + + $favicon_url = false; + + if ($html = @fetch_file_contents($url)) { + + $doc = new DOMDocument(); + if ($doc->loadHTML($html)) { + $xpath = new DOMXPath($doc); + + $base = $xpath->query('/html/head/base[@href]'); + foreach ($base as $b) { + $url = rewrite_relative_url($url, $b->getAttribute("href")); + break; + } + + $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]'); + if (count($entries) > 0) { + foreach ($entries as $entry) { + $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href")); + break; + } + } + } + } + + if (!$favicon_url) + $favicon_url = rewrite_relative_url($url, "/favicon.ico"); + + return $favicon_url; + } + } |