diff options
Diffstat (limited to 'classes')
-rw-r--r-- | classes/api.php | 101 | ||||
-rw-r--r-- | classes/article.php | 3 | ||||
-rw-r--r-- | classes/db/mysql.php | 7 | ||||
-rw-r--r-- | classes/db/mysqli.php | 7 | ||||
-rw-r--r-- | classes/db/pgsql.php | 7 | ||||
-rw-r--r-- | classes/dlg.php | 3 | ||||
-rw-r--r-- | classes/feedenclosure.php | 3 | ||||
-rw-r--r-- | classes/feeditem/atom.php | 76 | ||||
-rw-r--r-- | classes/feeditem/common.php | 28 | ||||
-rw-r--r-- | classes/feeditem/rss.php | 76 | ||||
-rw-r--r-- | classes/feedparser.php | 116 | ||||
-rw-r--r-- | classes/feeds.php | 197 | ||||
-rw-r--r-- | classes/handler/public.php | 305 | ||||
-rw-r--r-- | classes/opml.php | 12 | ||||
-rw-r--r-- | classes/pluginhost.php | 18 | ||||
-rw-r--r-- | classes/pref/feeds.php | 63 | ||||
-rw-r--r-- | classes/pref/filters.php | 77 | ||||
-rw-r--r-- | classes/pref/prefs.php | 16 | ||||
-rw-r--r-- | classes/pref/users.php | 4 | ||||
-rw-r--r-- | classes/rpc.php | 44 |
20 files changed, 764 insertions, 399 deletions
diff --git a/classes/api.php b/classes/api.php index 23866072f..730e20ab9 100644 --- a/classes/api.php +++ b/classes/api.php @@ -2,7 +2,7 @@ class API extends Handler { - const API_LEVEL = 7; + const API_LEVEL = 9; const STATUS_OK = 0; const STATUS_ERR = 1; @@ -77,6 +77,7 @@ class API extends Handler { $this->wrap(self::STATUS_OK, array("session_id" => session_id(), "api_level" => self::API_LEVEL)); } else { // else we are not logged in + user_error("Failed login attempt for $login from {$_SERVER['REMOTE_ADDR']}", E_USER_WARNING); $this->wrap(self::STATUS_ERR, array("error" => "LOGIN_ERROR")); } } else { @@ -199,11 +200,15 @@ class API extends Handler { $include_nested = sql_bool_to_bool($_REQUEST["include_nested"]); $sanitize_content = !isset($_REQUEST["sanitize"]) || sql_bool_to_bool($_REQUEST["sanitize"]); + $force_update = sql_bool_to_bool($_REQUEST["force_update"]); $override_order = false; switch ($_REQUEST["order_by"]) { + case "title": + $override_order = "ttrss_entries.title"; + break; case "date_reverse": - $override_order = "date_entered, updated"; + $override_order = "score DESC, date_entered, updated"; break; case "feed_dates": $override_order = "updated DESC"; @@ -218,7 +223,7 @@ class API extends Handler { $headlines = $this->api_get_headlines($feed_id, $limit, $offset, $filter, $is_cat, $show_excerpt, $show_content, $view_mode, $override_order, $include_attachments, $since_id, $search, $search_mode, - $include_nested, $sanitize_content); + $include_nested, $sanitize_content, $force_update); $this->wrap(self::STATUS_OK, $headlines); } else { @@ -309,8 +314,8 @@ class API extends Handler { if ($article_id) { - $query = "SELECT id,title,link,content,cached_content,feed_id,comments,int_id, - marked,unread,published,score, + $query = "SELECT id,title,link,content,feed_id,comments,int_id, + marked,unread,published,score,note,lang, ".SUBSTRING_FOR_DATE."(updated,1,16) as updated, author,(SELECT title FROM ttrss_feeds WHERE id = feed_id) AS feed_title FROM ttrss_entries,ttrss_user_entries @@ -338,11 +343,13 @@ class API extends Handler { "comments" => $line["comments"], "author" => $line["author"], "updated" => (int) strtotime($line["updated"]), - "content" => $line["cached_content"] != "" ? $line["cached_content"] : $line["content"], + "content" => $line["content"], "feed_id" => $line["feed_id"], "attachments" => $attachments, "score" => (int)$line["score"], - "feed_title" => $line["feed_title"] + "feed_title" => $line["feed_title"], + "note" => $line["note"], + "lang" => $line["lang"] ); foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_API) as $p) { @@ -423,14 +430,14 @@ class API extends Handler { $checked = false; foreach ($article_labels as $al) { - if ($al[0] == $line['id']) { + if (feed_to_label_id($al[0]) == $line['id']) { $checked = true; break; } } array_push($rv, array( - "id" => (int)$line['id'], + "id" => (int)label_to_feed_id($line['id']), "caption" => $line['caption'], "fg_color" => $line['fg_color'], "bg_color" => $line['bg_color'], @@ -447,7 +454,7 @@ class API extends Handler { $assign = (bool) $this->dbh->escape_string($_REQUEST['assign']) == "true"; $label = $this->dbh->escape_string(label_find_caption( - $label_id, $_SESSION["uid"])); + feed_to_label_id($label_id), $_SESSION["uid"])); $num_updated = 0; @@ -511,7 +518,7 @@ class API extends Handler { if ($unread || !$unread_only) { $row = array( - "id" => $cv["id"], + "id" => (int) $cv["id"], "title" => $cv["description"], "unread" => $cv["counter"], "cat_id" => -2, @@ -557,7 +564,7 @@ class API extends Handler { if ($unread || !$unread_only) { $row = array( - "id" => $line["id"], + "id" => (int) $line["id"], "title" => $line["title"], "unread" => $unread, "is_cat" => true, @@ -626,7 +633,28 @@ class API extends Handler { $filter, $is_cat, $show_excerpt, $show_content, $view_mode, $order, $include_attachments, $since_id, $search = "", $search_mode = "", - $include_nested = false, $sanitize_content = true) { + $include_nested = false, $sanitize_content = true, $force_update = false) { + + if ($force_update && $feed_id > 0 && is_numeric($feed_id)) { + // Update the feed if required with some basic flood control + + $result = db_query( + "SELECT cache_images,".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated + FROM ttrss_feeds WHERE id = '$feed_id'"); + + if (db_num_rows($result) != 0) { + $last_updated = strtotime(db_fetch_result($result, 0, "last_updated")); + $cache_images = sql_bool_to_bool(db_fetch_result($result, 0, "cache_images")); + + if (!$cache_images && time() - $last_updated > 120) { + include "rssfuncs.php"; + update_rss_feed($feed_id, true, true); + } else { + db_query("UPDATE ttrss_feeds SET last_updated = '1970-01-01', last_update_started = '1970-01-01' + WHERE id = '$feed_id'"); + } + } + } $qfh_ret = queryFeedHeadlines($feed_id, $limit, $view_mode, $is_cat, $search, $search_mode, @@ -638,11 +666,31 @@ class API extends Handler { $headlines = array(); while ($line = db_fetch_assoc($result)) { + $line["content_preview"] = truncate_string(strip_tags($line["content"]), 100); + foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) { + $line = $p->hook_query_headlines($line, 100, true); + } + $is_updated = ($line["last_read"] == "" && ($line["unread"] != "t" && $line["unread"] != "1")); $tags = explode(",", $line["tag_cache"]); - $labels = json_decode($line["label_cache"], true); + + $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"] == 1) + $labels = array(); + else + $labels = $label_cache; + } + } + + if (!is_array($labels)) $labels = get_article_labels($line["id"]); //if (!$tags) $tags = get_article_tags($line["id"]); //if (!$labels) $labels = get_article_labels($line["id"]); @@ -660,28 +708,22 @@ class API extends Handler { "tags" => $tags, ); - if ($include_attachments) - $headline_row['attachments'] = get_article_enclosures( - $line['id']); + if ($include_attachments) + $headline_row['attachments'] = get_article_enclosures( + $line['id']); - if ($show_excerpt) { - $excerpt = truncate_string(strip_tags($line["content_preview"]), 100); - $headline_row["excerpt"] = $excerpt; - } + if ($show_excerpt) + $headline_row["excerpt"] = $line["content_preview"]; if ($show_content) { - if ($line["cached_content"] != "") { - $line["content_preview"] =& $line["cached_content"]; - } - if ($sanitize_content) { $headline_row["content"] = sanitize( - $line["content_preview"], + $line["content"], sql_bool_to_bool($line['hide_images']), - false, $line["site_url"]); + false, $line["site_url"], false, $line["id"]); } else { - $headline_row["content"] = $line["content_preview"]; + $headline_row["content"] = $line["content"]; } } @@ -699,7 +741,10 @@ class API extends Handler { $headline_row["always_display_attachments"] = sql_bool_to_bool($line["always_display_enclosures"]); $headline_row["author"] = $line["author"]; + $headline_row["score"] = (int)$line["score"]; + $headline_row["note"] = $line["note"]; + $headline_row["lang"] = $line["lang"]; foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_API) as $p) { $headline_row = $p->hook_render_article_api(array("headline" => $headline_row)); diff --git a/classes/article.php b/classes/article.php index 398132d12..9aef107ec 100644 --- a/classes/article.php +++ b/classes/article.php @@ -30,7 +30,6 @@ class Article extends Handler_Protected { $id = $this->dbh->escape_string($_REQUEST["id"]); $cids = explode(",", $this->dbh->escape_string($_REQUEST["cids"])); $mode = $this->dbh->escape_string($_REQUEST["mode"]); - $omode = $this->dbh->escape_string($_REQUEST["omode"]); // in prefetch mode we only output requested cids, main article // just gets marked as read (it already exists in client cache) @@ -108,7 +107,7 @@ class Article extends Handler_Protected { // only check for our user data here, others might have shared this with different content etc $result = db_query("SELECT id FROM ttrss_entries, ttrss_user_entries WHERE - link = '$url' AND ref_id = id AND owner_uid = '$owner_uid' LIMIT 1"); + guid = '$guid' AND ref_id = id AND owner_uid = '$owner_uid' LIMIT 1"); if (db_num_rows($result) != 0) { $ref_id = db_fetch_result($result, 0, "id"); diff --git a/classes/db/mysql.php b/classes/db/mysql.php index aab05aca2..d4b45b98c 100644 --- a/classes/db/mysql.php +++ b/classes/db/mysql.php @@ -26,9 +26,12 @@ class Db_Mysql implements IDb { } function query($query, $die_on_error = true) { - $result = mysql_query($query, $this->link); + $result = @mysql_query($query, $this->link); if (!$result) { - user_error("Query $query failed: " . ($this->link ? mysql_error($this->link) : "No connection"), + $error = @mysql_error($this->link); + + @mysql_query("ROLLBACK", $this->link); + user_error("Query $query failed: " . ($this->link ? $error : "No connection"), $die_on_error ? E_USER_ERROR : E_USER_WARNING); } return $result; diff --git a/classes/db/mysqli.php b/classes/db/mysqli.php index a41ebf8ec..c685b75a0 100644 --- a/classes/db/mysqli.php +++ b/classes/db/mysqli.php @@ -24,9 +24,12 @@ class Db_Mysqli implements IDb { } function query($query, $die_on_error = true) { - $result = mysqli_query($this->link, $query); + $result = @mysqli_query($this->link, $query); if (!$result) { - user_error("Query $query failed: " . ($this->link ? mysqli_error($this->link) : "No connection"), + $error = @mysqli_error($this->link); + + @mysqli_query($this->link, "ROLLBACK"); + user_error("Query $query failed: " . ($this->link ? $error : "No connection"), $die_on_error ? E_USER_ERROR : E_USER_WARNING); } diff --git a/classes/db/pgsql.php b/classes/db/pgsql.php index 4d860790b..ba37f83a6 100644 --- a/classes/db/pgsql.php +++ b/classes/db/pgsql.php @@ -35,11 +35,14 @@ class Db_Pgsql implements IDb { } function query($query, $die_on_error = true) { - $result = pg_query($query); + $result = @pg_query($this->link, $query); if (!$result) { + $error = @pg_last_error($this->link); + + @pg_query($this->link, "ROLLBACK"); $query = htmlspecialchars($query); // just in case - user_error("Query $query failed: " . ($this->link ? pg_last_error($this->link) : "No connection"), + user_error("Query $query failed: " . ($this->link ? $error : "No connection"), $die_on_error ? E_USER_ERROR : E_USER_WARNING); } return $result; diff --git a/classes/dlg.php b/classes/dlg.php index cfa960d9a..25a194bed 100644 --- a/classes/dlg.php +++ b/classes/dlg.php @@ -16,7 +16,6 @@ class Dlg extends Handler_Protected { print __("If you have imported labels and/or filters, you might need to reload preferences to see your new data.") . "</p>"; print "<div class=\"prefFeedOPMLHolder\">"; - $owner_uid = $_SESSION["uid"]; $this->dbh->query("BEGIN"); @@ -176,7 +175,7 @@ class Dlg extends Handler_Protected { while ($row = $this->dbh->fetch_assoc($result)) { $tmp = htmlspecialchars($row["tag_name"]); - print "<option value=\"" . str_replace(" ", "%20", $tmp) . "\">$tmp</option>"; + print "<option value=\"$tmp\">$tmp</option>"; } print "</select>"; diff --git a/classes/feedenclosure.php b/classes/feedenclosure.php index d610dd7c8..64f1a0616 100644 --- a/classes/feedenclosure.php +++ b/classes/feedenclosure.php @@ -3,5 +3,8 @@ class FeedEnclosure { public $link; public $type; public $length; + public $title; + public $height; + public $width; } ?> diff --git a/classes/feeditem/atom.php b/classes/feeditem/atom.php index 9680748f9..dfac7149f 100644 --- a/classes/feeditem/atom.php +++ b/classes/feeditem/atom.php @@ -1,5 +1,6 @@ <?php class FeedItem_Atom extends FeedItem_Common { + function get_id() { $id = $this->elem->getElementsByTagName("id")->item(0); @@ -30,6 +31,7 @@ class FeedItem_Atom extends FeedItem_Common { } } + function get_link() { $links = $this->elem->getElementsByTagName("link"); @@ -38,8 +40,13 @@ class FeedItem_Atom extends FeedItem_Common { (!$link->hasAttribute("rel") || $link->getAttribute("rel") == "alternate" || $link->getAttribute("rel") == "standout")) { + $base = $this->xpath->evaluate("string(ancestor-or-self::*[@xml:base][1]/@xml:base)", $link); + + if ($base) + return rewrite_relative_url($base, trim($link->getAttribute("href"))); + else + return trim($link->getAttribute("href")); - return $link->getAttribute("href"); } } } @@ -48,7 +55,7 @@ class FeedItem_Atom extends FeedItem_Common { $title = $this->elem->getElementsByTagName("title")->item(0); if ($title) { - return $title->nodeValue; + return trim($title->nodeValue); } } @@ -58,7 +65,13 @@ class FeedItem_Atom extends FeedItem_Common { if ($content) { if ($content->hasAttribute('type')) { if ($content->getAttribute('type') == 'xhtml') { - return $this->doc->saveXML($content->firstChild->nextSibling); + for ($i = 0; $i < $content->childNodes->length; $i++) { + $child = $content->childNodes->item($i); + + if ($child->hasChildNodes()) { + return $this->doc->saveXML($child); + } + } } } @@ -72,7 +85,13 @@ class FeedItem_Atom extends FeedItem_Common { if ($content) { if ($content->hasAttribute('type')) { if ($content->getAttribute('type') == 'xhtml') { - return $this->doc->saveXML($content->firstChild->nextSibling); + for ($i = 0; $i < $content->childNodes->length; $i++) { + $child = $content->childNodes->item($i); + + if ($child->hasChildNodes()) { + return $this->doc->saveXML($child); + } + } } } @@ -87,13 +106,13 @@ class FeedItem_Atom extends FeedItem_Common { foreach ($categories as $cat) { if ($cat->hasAttribute("term")) - array_push($cats, $cat->getAttribute("term")); + array_push($cats, trim($cat->getAttribute("term"))); } $categories = $this->xpath->query("dc:subject", $this->elem); foreach ($categories as $cat) { - array_push($cats, $cat->nodeValue); + array_push($cats, trim($cat->nodeValue)); } return $cats; @@ -126,6 +145,51 @@ class FeedItem_Atom extends FeedItem_Common { $enc->type = $enclosure->getAttribute("type"); $enc->link = $enclosure->getAttribute("url"); $enc->length = $enclosure->getAttribute("length"); + $enc->height = $enclosure->getAttribute("height"); + $enc->width = $enclosure->getAttribute("width"); + + $desc = $this->xpath->query("media:description", $enclosure)->item(0); + if ($desc) $enc->title = strip_tags($desc->nodeValue); + + array_push($encs, $enc); + } + + + $enclosures = $this->xpath->query("media:group", $this->elem); + + foreach ($enclosures as $enclosure) { + $enc = new FeedEnclosure(); + + $content = $this->xpath->query("media:content", $enclosure)->item(0); + + if ($content) { + $enc->type = $content->getAttribute("type"); + $enc->link = $content->getAttribute("url"); + $enc->length = $content->getAttribute("length"); + $enc->height = $content->getAttribute("height"); + $enc->width = $content->getAttribute("width"); + + $desc = $this->xpath->query("media:description", $content)->item(0); + if ($desc) { + $enc->title = strip_tags($desc->nodeValue); + } else { + $desc = $this->xpath->query("media:description", $enclosure)->item(0); + if ($desc) $enc->title = strip_tags($desc->nodeValue); + } + + array_push($encs, $enc); + } + } + + $enclosures = $this->xpath->query("media:thumbnail", $this->elem); + + foreach ($enclosures as $enclosure) { + $enc = new FeedEnclosure(); + + $enc->type = "image/generic"; + $enc->link = $enclosure->getAttribute("url"); + $enc->height = $enclosure->getAttribute("height"); + $enc->width = $enclosure->getAttribute("width"); array_push($encs, $enc); } diff --git a/classes/feeditem/common.php b/classes/feeditem/common.php index 0787a42cb..80bebf8fb 100644 --- a/classes/feeditem/common.php +++ b/classes/feeditem/common.php @@ -8,6 +8,17 @@ abstract class FeedItem_Common extends FeedItem { $this->elem = $elem; $this->xpath = $xpath; $this->doc = $doc; + + try { + + $source = $elem->getElementsByTagName("source")->item(0); + + // we don't need <source> element + if ($source) + $elem->removeChild($source); + } catch (DOMException $e) { + // + } } function get_author() { @@ -33,13 +44,26 @@ abstract class FeedItem_Common extends FeedItem { } } - // todo function get_comments_url() { + //RSS only. Use a query here to avoid namespace clashes (e.g. with slash). + //might give a wrong result if a default namespace was declared (possible with XPath 2.0) + $com_url = $this->xpath->query("comments", $this->elem)->item(0); + + if($com_url) + return $com_url->nodeValue; + + //Atom Threading Extension (RFC 4685) stuff. Could be used in RSS feeds, so it's in common. + //'text/html' for type is too restrictive? + $com_url = $this->xpath->query("atom:link[@rel='replies' and contains(@type,'text/html')]/@href", $this->elem)->item(0); + if($com_url) + return $com_url->nodeValue; } function get_comments_count() { - $comments = $this->xpath->query("slash:comments", $this->elem)->item(0); + //also query for ATE stuff here + $query = "slash:comments|thread:total|atom:link[@rel='replies']/@thread:count"; + $comments = $this->xpath->query($query, $this->elem)->item(0); if ($comments) { return $comments->nodeValue; diff --git a/classes/feeditem/rss.php b/classes/feeditem/rss.php index e5960243c..c9a7467cd 100644 --- a/classes/feeditem/rss.php +++ b/classes/feeditem/rss.php @@ -33,20 +33,20 @@ class FeedItem_RSS extends FeedItem_Common { || $link->getAttribute("rel") == "alternate" || $link->getAttribute("rel") == "standout")) { - return $link->getAttribute("href"); + return trim($link->getAttribute("href")); } } $link = $this->elem->getElementsByTagName("guid")->item(0); if ($link && $link->hasAttributes() && $link->getAttribute("isPermaLink") == "true") { - return $link->nodeValue; + return trim($link->nodeValue); } $link = $this->elem->getElementsByTagName("link")->item(0); if ($link) { - return $link->nodeValue; + return trim($link->nodeValue); } } @@ -54,21 +54,26 @@ class FeedItem_RSS extends FeedItem_Common { $title = $this->elem->getElementsByTagName("title")->item(0); if ($title) { - return $title->nodeValue; + return trim($title->nodeValue); } } function get_content() { - $content = $this->xpath->query("content:encoded", $this->elem)->item(0); + $contentA = $this->xpath->query("content:encoded", $this->elem)->item(0); + $contentB = $this->elem->getElementsByTagName("description")->item(0); - if ($content) { - return $content->nodeValue; + if ($contentA && !$contentB) { + return $contentA->nodeValue; } - $content = $this->elem->getElementsByTagName("description")->item(0); - if ($content) { - return $content->nodeValue; + if ($contentB && !$contentA) { + return $contentB->nodeValue; + } + + if ($contentA && $contentB) { + return mb_strlen($contentA->nodeValue) > mb_strlen($contentB->nodeValue) ? + $contentA->nodeValue : $contentB->nodeValue; } } @@ -85,13 +90,13 @@ class FeedItem_RSS extends FeedItem_Common { $cats = array(); foreach ($categories as $cat) { - array_push($cats, $cat->nodeValue); + array_push($cats, trim($cat->nodeValue)); } $categories = $this->xpath->query("dc:subject", $this->elem); foreach ($categories as $cat) { - array_push($cats, $cat->nodeValue); + array_push($cats, trim($cat->nodeValue)); } return $cats; @@ -108,6 +113,8 @@ class FeedItem_RSS extends FeedItem_Common { $enc->type = $enclosure->getAttribute("type"); $enc->link = $enclosure->getAttribute("url"); $enc->length = $enclosure->getAttribute("length"); + $enc->height = $enclosure->getAttribute("height"); + $enc->width = $enclosure->getAttribute("width"); array_push($encs, $enc); } @@ -120,6 +127,51 @@ class FeedItem_RSS extends FeedItem_Common { $enc->type = $enclosure->getAttribute("type"); $enc->link = $enclosure->getAttribute("url"); $enc->length = $enclosure->getAttribute("length"); + $enc->height = $enclosure->getAttribute("height"); + $enc->width = $enclosure->getAttribute("width"); + + $desc = $this->xpath->query("media:description", $enclosure)->item(0); + if ($desc) $enc->title = strip_tags($desc->nodeValue); + + array_push($encs, $enc); + } + + + $enclosures = $this->xpath->query("media:group", $this->elem); + + foreach ($enclosures as $enclosure) { + $enc = new FeedEnclosure(); + + $content = $this->xpath->query("media:content", $enclosure)->item(0); + + if ($content) { + $enc->type = $content->getAttribute("type"); + $enc->link = $content->getAttribute("url"); + $enc->length = $content->getAttribute("length"); + $enc->height = $content->getAttribute("height"); + $enc->width = $content->getAttribute("width"); + + $desc = $this->xpath->query("media:description", $content)->item(0); + if ($desc) { + $enc->title = strip_tags($desc->nodeValue); + } else { + $desc = $this->xpath->query("media:description", $enclosure)->item(0); + if ($desc) $enc->title = strip_tags($desc->nodeValue); + } + + array_push($encs, $enc); + } + } + + $enclosures = $this->xpath->query("media:thumbnail", $this->elem); + + foreach ($enclosures as $enclosure) { + $enc = new FeedEnclosure(); + + $enc->type = "image/generic"; + $enc->link = $enclosure->getAttribute("url"); + $enc->height = $enclosure->getAttribute("height"); + $enc->width = $enclosure->getAttribute("width"); array_push($encs, $enc); } diff --git a/classes/feedparser.php b/classes/feedparser.php index d93c575b2..239fdb7a6 100644 --- a/classes/feedparser.php +++ b/classes/feedparser.php @@ -2,6 +2,7 @@ class FeedParser { private $doc; private $error; + private $libxml_errors = array(); private $items; private $link; private $title; @@ -12,27 +13,75 @@ class FeedParser { const FEED_RSS = 1; const FEED_ATOM = 2; + function normalize_encoding($data) { + if (preg_match('/^(<\?xml[\t\n\r ].*?encoding[\t\n\r ]*=[\t\n\r ]*["\'])(.+?)(["\'].*?\?>)/s', $data, $matches) === 1) { + $data = mb_convert_encoding($data, 'UTF-8', $matches[2]); + + $data = preg_replace('/^<\?xml[\t\n\r ].*?\?>/s', $matches[1] . "UTF-8" . $matches[3] , $data); + } + + return $data; + } + function __construct($data) { libxml_use_internal_errors(true); libxml_clear_errors(); $this->doc = new DOMDocument(); $this->doc->loadXML($data); + mb_substitute_character("none"); + $error = libxml_get_last_error(); - if ($error && $error->code == 9) { - libxml_clear_errors(); + // libxml compiled without iconv? + if ($error && $error->code == 32) { + $data = $this->normalize_encoding($data); - // we might want to try guessing input encoding here too - $data = iconv("UTF-8", "UTF-8//IGNORE", $data); + if ($data) { + libxml_clear_errors(); - $this->doc = new DOMDocument(); - $this->doc->loadXML($data); + $this->doc = new DOMDocument(); + $this->doc->loadXML($data); - $error = libxml_get_last_error(); + $error = libxml_get_last_error(); + } } - $this->error = $this->format_error($error); + // some terrible invalid unicode entity? + if ($error) { + foreach (libxml_get_errors() as $err) { + if ($err->code == 9) { + // if the source feed is not in utf8, next conversion will fail + $data = $this->normalize_encoding($data); + + // remove dangling bytes + $data = mb_convert_encoding($data, 'UTF-8', 'UTF-8'); + + // apparently not all UTF-8 characters are valid for XML + $data = preg_replace('/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u', ' ', $data); + + if ($data) { + libxml_clear_errors(); + + $this->doc = new DOMDocument(); + $this->doc->loadXML($data); + + $error = libxml_get_last_error(); + } + break; + } + } + } + + if ($error) { + foreach (libxml_get_errors() as $error) { + if ($error->level == LIBXML_ERR_FATAL) { + if(!isset($this->error)) //currently only the first error is reported + $this->error = $this->format_error($error); + $this->libxml_errors [] = $this->format_error($error); + } + } + } libxml_clear_errors(); $this->items = array(); @@ -48,27 +97,32 @@ class FeedParser { $xpath->registerNamespace('slash', 'http://purl.org/rss/1.0/modules/slash/'); $xpath->registerNamespace('dc', 'http://purl.org/dc/elements/1.1/'); $xpath->registerNamespace('content', 'http://purl.org/rss/1.0/modules/content/'); + $xpath->registerNamespace('thread', 'http://purl.org/syndication/thread/1.0'); $this->xpath = $xpath; - $root = $xpath->query("(//atom03:feed|//atom:feed|//channel|//rdf:rdf|//rdf:RDF)")->item(0); - - if ($root) { - switch (mb_strtolower($root->tagName)) { - case "rdf:rdf": - $this->type = $this::FEED_RDF; - break; - case "channel": - $this->type = $this::FEED_RSS; - break; - case "feed": - $this->type = $this::FEED_ATOM; - break; - default: - if( !isset($this->error) ){ - $this->error = "Unknown/unsupported feed type"; + $root = $xpath->query("(//atom03:feed|//atom:feed|//channel|//rdf:rdf|//rdf:RDF)"); + + if ($root && $root->length > 0) { + $root = $root->item(0); + + if ($root) { + switch (mb_strtolower($root->tagName)) { + case "rdf:rdf": + $this->type = $this::FEED_RDF; + break; + case "channel": + $this->type = $this::FEED_RSS; + break; + case "feed": + $this->type = $this::FEED_ATOM; + break; + default: + if( !isset($this->error) ){ + $this->error = "Unknown/unsupported feed type"; + } + return; } - return; } switch ($this->type) { @@ -151,6 +205,10 @@ class FeedParser { break; } + + if ($this->title) $this->title = trim($this->title); + if ($this->link) $this->link = trim($this->link); + } else { if( !isset($this->error) ){ $this->error = "Unknown/unsupported feed type"; @@ -173,6 +231,10 @@ class FeedParser { return $this->error; } + function errors() { + return $this->libxml_errors; + } + function get_link() { return $this->link; } @@ -194,7 +256,7 @@ class FeedParser { foreach ($links as $link) { if (!$rel || $link->hasAttribute('rel') && $link->getAttribute('rel') == $rel) { - array_push($rv, $link->getAttribute('href')); + array_push($rv, trim($link->getAttribute('href'))); } } break; @@ -203,7 +265,7 @@ class FeedParser { foreach ($links as $link) { if (!$rel || $link->hasAttribute('rel') && $link->getAttribute('rel') == $rel) { - array_push($rv, $link->getAttribute('href')); + array_push($rv, trim($link->getAttribute('href'))); } } break; diff --git a/classes/feeds.php b/classes/feeds.php index 83736925c..5ec109614 100644 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -13,12 +13,6 @@ class Feeds extends Handler_Protected { $feed_id, $is_cat, $search, $search_mode, $view_mode, $error, $feed_last_updated) { - $page_prev_link = "viewFeedGoPage(-1)"; - $page_next_link = "viewFeedGoPage(1)"; - $page_first_link = "viewFeedGoPage(0)"; - - $catchup_page_link = "catchupPage()"; - $catchup_feed_link = "catchupCurrentFeed()"; $catchup_sel_link = "catchupSelection()"; $archive_sel_link = "archiveSelection()"; @@ -43,14 +37,24 @@ class Feeds extends Handler_Protected { $search_q = ""; } + $reply .= "<span class=\"holder\">"; + $rss_link = htmlspecialchars(get_self_url_prefix() . "/public.php?op=rss&id=$feed_id$cat_q$search_q"); // right part - $reply .= "<span class='r'>"; - $reply .= "<span id='selected_prompt'></span>"; - $reply .= "<span id='feed_title'>"; + $error_class = $error ? "error" : ""; + + $reply .= "<span class='r'> + <a href=\"#\" + title=\"".__("View as RSS feed")."\" + onclick=\"displayDlg('".__("View as RSS")."','generatedFeed', '$feed_id:$is_cat:$rss_link')\"> + <img class=\"noborder\" src=\"images/pub_set.png\"></a>"; + + +# $reply .= "<span>"; + $reply .= "<span id='feed_title' class='$error_class'>"; if ($feed_site_url) { $last_updated = T_sprintf("Last updated: %s", @@ -58,10 +62,11 @@ class Feeds extends Handler_Protected { $target = "target=\"_blank\""; $reply .= "<a title=\"$last_updated\" $target href=\"$feed_site_url\">". - truncate_string($feed_title,30)."</a>"; + truncate_string($feed_title, 30)."</a>"; if ($error) { - $reply .= " (<span class=\"error\" title=\"$error\">Error</span>)"; + $error = htmlspecialchars($error); + $reply .= " <img title=\"$error\" src='images/error.png' alt='error' class=\"noborder\">"; } } else { @@ -70,17 +75,16 @@ class Feeds extends Handler_Protected { $reply .= "</span>"; - $reply .= " - <a href=\"#\" - title=\"".__("View as RSS feed")."\" - onclick=\"displayDlg('".__("View as RSS")."','generatedFeed', '$feed_id:$is_cat:$rss_link')\"> - <img class=\"noborder\" style=\"vertical-align : middle\" src=\"images/pub_set.svg\"></a>"; - $reply .= "</span>"; +# $reply .= "</span>"; + // left part - $reply .= __('Select:')." + $reply .= "<span class=\"main\">"; + $reply .= "<span id='selected_prompt'></span>"; + + $reply .= " <a href=\"#\" onclick=\"$sel_all_link\">".__('All')."</a>, <a href=\"#\" onclick=\"$sel_unread_link\">".__('Unread')."</a>, <a href=\"#\" onclick=\"$sel_inv_link\">".__('Invert')."</a>, @@ -129,14 +133,14 @@ class Feeds extends Handler_Protected { $reply .= "</select>"; - //$reply .= "</div>"; - //$reply .= "</h2"; foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HEADLINE_TOOLBAR_BUTTON) as $p) { - echo $p->hook_headline_toolbar_button($feed_id, $is_cat); + $reply .= $p->hook_headline_toolbar_button($feed_id, $is_cat); } + $reply .= "</span></span>"; + return $reply; } @@ -145,7 +149,7 @@ class Feeds extends Handler_Protected { $override_order = false, $include_children = false) { if (isset($_REQUEST["DevForceUpdate"])) - header("Content-Type: text/plain"); + header("Content-Type: text/plain; charset=utf-8"); $disable_cache = false; @@ -244,6 +248,8 @@ class Feeds extends Handler_Protected { false, 0, $include_children); } + $vfeed_group_enabled = get_pref("VFEED_GROUP_BY_FEED") && $feed != -6; + if ($_REQUEST["debug"]) $timing_info = print_checkpoint("H1", $timing_info); $result = $qfh_ret[0]; @@ -252,6 +258,7 @@ class Feeds extends Handler_Protected { $last_error = $qfh_ret[3]; $last_updated = strpos($qfh_ret[4], '1970-') === FALSE ? make_local_datetime($qfh_ret[4], false) : __("Never"); + $highlight_words = $qfh_ret[5]; $vgroup_last_feed = $vgr_last_feed; @@ -274,6 +281,12 @@ class Feeds extends Handler_Protected { } } */ + if ($offset == 0) { + foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HEADLINES_BEFORE) as $p) { + $reply['content'] .= $p->hook_headlines_before($feed, $cat_view, $qfh_ret); + } + } + if ($this->dbh->num_rows($result) > 0) { $lnum = $offset; @@ -281,13 +294,21 @@ class Feeds extends Handler_Protected { $num_unread = 0; $cur_feed_title = ''; - $fresh_intl = get_pref("FRESH_ARTICLE_MAX_AGE") * 60 * 60; - if ($_REQUEST["debug"]) $timing_info = print_checkpoint("PS", $timing_info); $expand_cdm = get_pref('CDM_EXPANDED'); while ($line = $this->dbh->fetch_assoc($result)) { + $line["content_preview"] = "— " . truncate_string(strip_tags($line["content"]), 250); + + foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) { + $line = $p->hook_query_headlines($line, 250, false); + } + + if (get_pref('SHOW_CONTENT_PREVIEW')) { + $content_preview = $line["content_preview"]; + } + $id = $line["id"]; $feed_id = $line["feed_id"]; $label_cache = $line["label_cache"]; @@ -306,7 +327,7 @@ class Feeds extends Handler_Protected { if (!is_array($labels)) $labels = get_article_labels($id); - $labels_str = "<span id=\"HLLCTR-$id\">"; + $labels_str = "<span class=\"HLLCTR-$id\">"; $labels_str .= format_article_labels($labels, $id); $labels_str .= "</span>"; @@ -323,24 +344,24 @@ class Feeds extends Handler_Protected { if (sql_bool_to_bool($line["marked"])) { $marked_pic = "<img - src=\"images/mark_set.svg\" + src=\"images/mark_set.png\" class=\"markedPic\" alt=\"Unstar article\" onclick='toggleMark($id)'>"; $class .= " marked"; } else { $marked_pic = "<img - src=\"images/mark_unset.svg\" + src=\"images/mark_unset.png\" class=\"markedPic\" alt=\"Star article\" onclick='toggleMark($id)'>"; } if (sql_bool_to_bool($line["published"])) { - $published_pic = "<img src=\"images/pub_set.svg\" + $published_pic = "<img src=\"images/pub_set.png\" class=\"pubPic\" alt=\"Unpublish article\" onclick='togglePub($id)'>"; $class .= " published"; } else { - $published_pic = "<img src=\"images/pub_unset.svg\" + $published_pic = "<img src=\"images/pub_unset.png\" class=\"pubPic\" alt=\"Publish article\" onclick='togglePub($id)'>"; } @@ -360,11 +381,6 @@ class Feeds extends Handler_Protected { $date_entered_fmt = T_sprintf("Imported at %s", make_local_datetime($line["date_entered"], false)); - if (get_pref('SHOW_CONTENT_PREVIEW')) { - $content_preview = truncate_string(strip_tags($line["content_preview"]), - 250); - } - $score = $line["score"]; $score_pic = "images/" . get_score_pic($score); @@ -377,9 +393,9 @@ class Feeds extends Handler_Protected { title=\"$score\">"; if ($score > 500) { - $hlc_suffix = "H"; + $hlc_suffix = "high"; } else if ($score < -100) { - $hlc_suffix = "L"; + $hlc_suffix = "low"; } else { $hlc_suffix = ""; } @@ -395,7 +411,7 @@ class Feeds extends Handler_Protected { if ($has_feed_icon) { $feed_icon_img = "<img class=\"tinyFeedIcon\" src=\"".ICONS_URL."/$feed_id.ico\" alt=\"\">"; } else { - $feed_icon_img = "<img class=\"tinyFeedIcon\" src=\"images/pub_set.svg\" alt=\"\">"; + $feed_icon_img = "<img class=\"tinyFeedIcon\" src=\"images/pub_set.png\" alt=\"\">"; } $entry_site_url = $line["site_url"]; @@ -413,7 +429,7 @@ class Feeds extends Handler_Protected { if (!get_pref('COMBINED_DISPLAY_MODE')) { - if (get_pref('VFEED_GROUP_BY_FEED')) { + if ($vfeed_group_enabled) { if ($feed_id != $vgroup_last_feed && $line["feed_title"]) { $cur_feed_title = $line["feed_title"]; @@ -421,12 +437,12 @@ class Feeds extends Handler_Protected { $cur_feed_title = htmlspecialchars($cur_feed_title); - $vf_catchup_link = "(<a class='catchup' onclick='catchupFeedInGroup($feed_id);' href='#'>".__('Mark as read')."</a>)"; + $vf_catchup_link = "<a class='catchup' onclick='catchupFeedInGroup($feed_id);' href='#'>".__('mark feed as read')."</a>"; - $reply['content'] .= "<div class='cdmFeedTitle'>". - "<div style=\"float : right\">$feed_icon_img</div>". - "<a class='title' href=\"#\" onclick=\"viewfeed($feed_id)\">". - $line["feed_title"]."</a> $vf_catchup_link</div>"; + $reply['content'] .= "<div id='FTITLE-$feed_id' class='cdmFeedTitle'>". + "<div style='float : right'>$feed_icon_img</div>". + "<a class='title' href=\"#\" onclick=\"viewfeed($feed_id)\">". $line["feed_title"]."</a> + $vf_catchup_link</div>"; } } @@ -434,7 +450,7 @@ class Feeds extends Handler_Protected { $mouseover_attrs = "onmouseover='postMouseIn(event, $id)' onmouseout='postMouseOut($id)'"; - $reply['content'] .= "<div class='hl $class' id='RROW-$id' $mouseover_attrs>"; + $reply['content'] .= "<div class='hl $class' orig-feed-id='$feed_id' id='RROW-$id' $mouseover_attrs>"; $reply['content'] .= "<div class='hlLeft'>"; @@ -448,16 +464,14 @@ class Feeds extends Handler_Protected { $reply['content'] .= "</div>"; $reply['content'] .= "<div onclick='return hlClicked(event, $id)' - class=\"hlTitle\"><span class='hlContent$hlc_suffix'>"; - $reply['content'] .= "<a id=\"RTITLE-$id\" class=\"title\" + class=\"hlTitle\"><span class='hlContent $hlc_suffix'>"; + $reply['content'] .= "<a id=\"RTITLE-$id\" class=\"title $hlc_suffix\" href=\"" . htmlspecialchars($line["link"]) . "\" onclick=\"\">" . truncate_string($line["title"], 200); if (get_pref('SHOW_CONTENT_PREVIEW')) { - if ($content_preview) { - $reply['content'] .= "<span class=\"contentPreview\"> - $content_preview</span>"; - } + $reply['content'] .= "<span class=\"contentPreview\">" . $line["content_preview"] . "</span>"; } $reply['content'] .= "</a></span>"; @@ -466,17 +480,18 @@ class Feeds extends Handler_Protected { $reply['content'] .= "</div>"; - $reply['content'] .= "<span class=\"hlUpdated\">"; - - if (!get_pref('VFEED_GROUP_BY_FEED')) { + if (!$vfeed_group_enabled) { if (@$line["feed_title"]) { $rgba = @$rgba_cache[$feed_id]; - $reply['content'] .= "<a class=\"hlFeed\" style=\"background : rgba($rgba, 0.3)\" href=\"#\" onclick=\"viewfeed($feed_id)\">". - truncate_string($line["feed_title"],30)."</a>"; + $reply['content'] .= "<span class=\"hlFeed\"><a style=\"background : rgba($rgba, 0.3)\" href=\"#\" onclick=\"viewfeed($feed_id)\">". + truncate_string($line["feed_title"],30)."</a></span>"; } } + + $reply['content'] .= "<span class=\"hlUpdated\">"; + $reply['content'] .= "<div title='$date_entered_fmt'>$updated_fmt</div> </span>"; @@ -484,12 +499,12 @@ class Feeds extends Handler_Protected { $reply['content'] .= $score_pic; - if ($line["feed_title"] && !get_pref('VFEED_GROUP_BY_FEED')) { + if ($line["feed_title"] && !$vfeed_group_enabled) { $reply['content'] .= "<span onclick=\"viewfeed($feed_id)\" style=\"cursor : pointer\" title=\"".htmlspecialchars($line['feed_title'])."\"> - $feed_icon_img<span>"; + $feed_icon_img</span>"; } $reply['content'] .= "</div>"; @@ -502,14 +517,14 @@ class Feeds extends Handler_Protected { else $tags = false; - $line["content"] = sanitize($line["content_preview"], - sql_bool_to_bool($line['hide_images']), false, $entry_site_url); + $line["content"] = sanitize($line["content"], + sql_bool_to_bool($line['hide_images']), false, $entry_site_url, $highlight_words, $line["id"]); foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_CDM) as $p) { $line = $p->hook_render_article_cdm($line); } - if (get_pref('VFEED_GROUP_BY_FEED') && $line["feed_title"]) { + if ($vfeed_group_enabled && $line["feed_title"]) { if ($feed_id != $vgroup_last_feed) { $cur_feed_title = $line["feed_title"]; @@ -517,7 +532,7 @@ class Feeds extends Handler_Protected { $cur_feed_title = htmlspecialchars($cur_feed_title); - $vf_catchup_link = "(<a class='catchup' onclick='javascript:catchupFeedInGroup($feed_id);' href='#'>".__('mark as read')."</a>)"; + $vf_catchup_link = "<a class='catchup' onclick='catchupFeedInGroup($feed_id);' href='#'>".__('mark feed as read')."</a>"; $has_feed_icon = feed_has_icon($feed_id); @@ -527,7 +542,7 @@ class Feeds extends Handler_Protected { //$feed_icon_img = "<img class=\"tinyFeedIcon\" src=\"images/blank_icon.gif\" alt=\"\">"; } - $reply['content'] .= "<div class='cdmFeedTitle'>". + $reply['content'] .= "<div id='FTITLE-$feed_id' class='cdmFeedTitle'>". "<div style=\"float : right\">$feed_icon_img</div>". "<a href=\"#\" class='title' onclick=\"viewfeed($feed_id)\">". $line["feed_title"]."</a> $vf_catchup_link</div>"; @@ -539,10 +554,10 @@ class Feeds extends Handler_Protected { $expanded_class = $expand_cdm ? "expanded" : "expandable"; - $reply['content'] .= "<div class=\"cdm $expanded_class $class\" - id=\"RROW-$id\" $mouseover_attrs>"; + $reply['content'] .= "<div class=\"cdm $hlc_suffix $expanded_class $class\" + id=\"RROW-$id\" orig-feed-id='$feed_id' $mouseover_attrs>"; - $reply['content'] .= "<div class=\"cdmHeader\" style=\"$row_background\">"; + $reply['content'] .= "<div class=\"cdmHeader\">"; $reply['content'] .= "<div style=\"vertical-align : middle\">"; $reply['content'] .= "<input dojoType=\"dijit.form.CheckBox\" @@ -554,10 +569,17 @@ class Feeds extends Handler_Protected { $reply['content'] .= "</div>"; + if ($highlight_words && count($highlight_words > 0)) { + foreach ($highlight_words as $word) { + $line["title"] = preg_replace("/(\Q$word\E)/i", + "<span class=\"highlight\">$1</span>", $line["title"]); + } + } + $reply['content'] .= "<span id=\"RTITLE-$id\" onclick=\"return cdmClicked(event, $id);\" - class=\"titleWrap$hlc_suffix\"> - <a class=\"title\" + class=\"titleWrap $hlc_suffix\"> + <a class=\"title $hlc_suffix\" target=\"_blank\" href=\"". htmlspecialchars($line["link"])."\">". $line["title"] . @@ -574,11 +596,11 @@ class Feeds extends Handler_Protected { else $excerpt_hidden = "style=\"display : none\""; - $reply['content'] .= "<span $excerpt_hidden - id=\"CEXC-$id\" class=\"cdmExcerpt\"> - $content_preview</span>"; + $reply['content'] .= "<span $excerpt_hidden id=\"CEXC-$id\" class=\"cdmExcerpt\">" . $content_preview . "</span>"; + $reply['content'] .= "</span>"; - if (!get_pref('VFEED_GROUP_BY_FEED')) { + if (!$vfeed_group_enabled) { if (@$line["feed_title"]) { $rgba = @$rgba_cache[$feed_id]; @@ -615,7 +637,9 @@ class Feeds extends Handler_Protected { } $reply['content'] .= "</div>"; - $reply['content'] .= "<div class=\"cdmContentInner\">"; + if (!$line['lang']) $line['lang'] = 'en'; + + $reply['content'] .= "<div class=\"cdmContentInner\" lang=\"".$line['lang']."\">"; if ($line["orig_feed_id"]) { @@ -638,7 +662,7 @@ class Feeds extends Handler_Protected { $reply['content'] .= " "; $reply['content'] .= "<a target='_blank' href='" . htmlspecialchars($tmp_line['feed_url']) . "'>"; - $reply['content'] .= "<img title='".__('Feed URL')."'class='tinyFeedIcon' src='images/pub_unset.svg'></a>"; + $reply['content'] .= "<img title='".__('Feed URL')."'class='tinyFeedIcon' src='images/pub_unset.png'></a>"; $reply['content'] .= "</div>"; } @@ -685,10 +709,13 @@ class Feeds extends Handler_Protected { } else { $comments_url = htmlspecialchars($line["link"]); } - $entry_comments = "<a target='_blank' href=\"$comments_url\">$num_comments comments</a>"; + $entry_comments = "<a class=\"postComments\" + target='_blank' href=\"$comments_url\">$num_comments ". + _ngettext("comment", "comments", $num_comments)."</a>"; + } else { if ($line["comments"] && $line["link"] != $line["comments"]) { - $entry_comments = "<a target='_blank' href=\"".htmlspecialchars($line["comments"])."\">comments</a>"; + $entry_comments = "<a class=\"postComments\" target='_blank' href=\"".htmlspecialchars($line["comments"])."\">".__("comments")."</a>"; } } @@ -706,7 +733,7 @@ class Feeds extends Handler_Protected { $reply['content'] .= "</div>"; $reply['content'] .= "</div>"; - $reply['content'] .= "</div><hr/>"; + $reply['content'] .= "</div>"; $reply['content'] .= "</div>"; @@ -784,8 +811,6 @@ class Feeds extends Handler_Protected { if ($_REQUEST["debug"]) $timing_info = print_checkpoint("0", $timing_info); - $omode = $this->dbh->escape_string($_REQUEST["omode"]); - $feed = $this->dbh->escape_string($_REQUEST["feed"]); $method = $this->dbh->escape_string($_REQUEST["m"]); $view_mode = $this->dbh->escape_string($_REQUEST["view_mode"]); @@ -863,7 +888,7 @@ class Feeds extends Handler_Protected { $override_order = "ttrss_entries.title"; break; case "date_reverse": - $override_order = "date_entered, updated"; + $override_order = "score DESC, date_entered, updated"; break; case "feed_dates": $override_order = "updated DESC"; @@ -878,7 +903,7 @@ class Feeds extends Handler_Protected { //$topmost_article_ids = $ret[0]; $headlines_count = $ret[1]; - $returned_feed = $ret[2]; + /* $returned_feed = $ret[2]; */ $disable_cache = $ret[3]; $vgroup_last_feed = $ret[4]; @@ -959,6 +984,10 @@ class Feeds extends Handler_Protected { print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"op\" value=\"rpc\">"; print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"method\" value=\"addfeed\">"; + print "<div id='fadd_multiple_notify' style='display : none'>"; + print_notice("Provided URL is a HTML page referencing multiple feeds, please select required feed from the dropdown menu below."); + print "<p></div>"; + print "<div class=\"dlgSec\">".__("Feed or site URL")."</div>"; print "<div class=\"dlgSecCont\">"; @@ -1054,20 +1083,18 @@ class Feeds extends Handler_Protected { print " <select dojoType=\"dijit.form.Select\" name=\"limit\" onchange=\"dijit.byId('feedBrowserDlg').update()\">"; foreach (array(25, 50, 100, 200) as $l) { - $issel = ($l == $limit) ? "selected=\"1\"" : ""; - print "<option $issel value=\"$l\">$l</option>"; + //$issel = ($l == $limit) ? "selected=\"1\"" : ""; + print "<option value=\"$l\">$l</option>"; } print "</select> "; print "</div>"; - $owner_uid = $_SESSION["uid"]; - require_once "feedbrowser.php"; print "<ul class='browseFeedList' id='browseFeedList'>"; - print make_feed_browser($search, 25); + print make_feed_browser("", 25); print "</ul>"; print "<div align='center'> @@ -1126,9 +1153,9 @@ class Feeds extends Handler_Protected { print "<div class=\"dlgButtons\">"; - if (!SPHINX_ENABLED) { + if (count(PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH)) == 0) { print "<div style=\"float : left\"> - <a class=\"visibleLink\" target=\"_blank\" href=\"http://tt-rss.org/wiki/SearchSyntax\">Search syntax</a> + <a class=\"visibleLink\" target=\"_blank\" href=\"http://tt-rss.org/wiki/SearchSyntax\">".__("Search syntax")."</a> </div>"; } diff --git a/classes/handler/public.php b/classes/handler/public.php index f05beafd2..34d577441 100644 --- a/classes/handler/public.php +++ b/classes/handler/public.php @@ -3,7 +3,7 @@ class Handler_Public extends Handler { private function generate_syndicated_feed($owner_uid, $feed, $is_cat, $limit, $offset, $search, $search_mode, - $view_mode = false, $format = 'atom', $order = false) { + $view_mode = false, $format = 'atom', $order = false, $orig_guid = false, $start_ts = false) { require_once "lib/MiniTemplator.class.php"; @@ -15,11 +15,15 @@ class Handler_Public extends Handler { if (!$limit) $limit = 60; $date_sort_field = "date_entered DESC, updated DESC"; + $date_check_field = "date_entered"; - if ($feed == -2) + if ($feed == -2 && !$is_cat) { $date_sort_field = "last_published DESC"; - else if ($feed == -1) + $date_check_field = "last_published"; + } else if ($feed == -1 && !$is_cat) { $date_sort_field = "last_marked DESC"; + $date_check_field = "last_marked"; + } switch ($order) { case "title": @@ -33,15 +37,18 @@ class Handler_Public extends Handler { break; } + //function queryFeedHeadlines($feed, $limit, $view_mode, $cat_view, $search, $search_mode, $override_order = false, $offset = 0, $owner_uid = 0, $filter = false, $since_id = 0, $include_children = false, $ignore_vfeed_group = false, $override_strategy = false, $override_vfeed = false, $start_ts = false) { + $qfh_ret = queryFeedHeadlines($feed, 1, $view_mode, $is_cat, $search, $search_mode, $date_sort_field, $offset, $owner_uid, - false, 0, false, true); + false, 0, true, true, false, false, $start_ts); $result = $qfh_ret[0]; if ($this->dbh->num_rows($result) != 0) { - $ts = strtotime($this->dbh->fetch_result($result, 0, "date_entered")); + + $ts = strtotime($this->dbh->fetch_result($result, 0, $date_check_field)); if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $ts) { @@ -56,17 +63,17 @@ class Handler_Public extends Handler { $qfh_ret = queryFeedHeadlines($feed, $limit, $view_mode, $is_cat, $search, $search_mode, $date_sort_field, $offset, $owner_uid, - false, 0, false, true); + false, 0, true, true, false, false, $start_ts); $result = $qfh_ret[0]; $feed_title = htmlspecialchars($qfh_ret[1]); $feed_site_url = $qfh_ret[2]; - $last_error = $qfh_ret[3]; + /* $last_error = $qfh_ret[3]; */ $feed_self_url = get_self_url_prefix() . - "/public.php?op=rss&id=-2&key=" . - get_feed_access_key(-2, false, $owner_uid); + "/public.php?op=rss&id=$feed&key=" . + get_feed_access_key($feed, false, $owner_uid); if (!$feed_site_url) $feed_site_url = get_self_url_prefix(); @@ -85,16 +92,23 @@ class Handler_Public extends Handler { } $tpl->setVariable('SELF_URL', htmlspecialchars(get_self_url_prefix()), true); - while ($line = $this->dbh->fetch_assoc($result)) { + $line["content_preview"] = truncate_string(strip_tags($line["content"]), 100, '...'); + + foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) { + $line = $p->hook_query_headlines($line); + } - $tpl->setVariable('ARTICLE_ID', htmlspecialchars($line['link']), true); + $tpl->setVariable('ARTICLE_ID', + htmlspecialchars($orig_guid ? $line['link'] : + get_self_url_prefix() . + "/public.php?url=" . urlencode($line['link'])), true); $tpl->setVariable('ARTICLE_LINK', htmlspecialchars($line['link']), true); $tpl->setVariable('ARTICLE_TITLE', htmlspecialchars($line['title']), true); - $tpl->setVariable('ARTICLE_EXCERPT', - truncate_string(strip_tags($line["content_preview"]), 100, '...'), true); + $tpl->setVariable('ARTICLE_EXCERPT', $line["content_preview"], true); - $content = sanitize($line["content_preview"], false, $owner_uid); + $content = sanitize($line["content"], false, $owner_uid, + $feed_site_url); if ($line['note']) { $content = "<div style=\"$note_style\">Article note: " . $line['note'] . "</div>" . @@ -111,6 +125,9 @@ class Handler_Public extends Handler { $tpl->setVariable('ARTICLE_AUTHOR', htmlspecialchars($line['author']), true); + $tpl->setVariable('ARTICLE_SOURCE_LINK', htmlspecialchars($line['site_url']), true); + $tpl->setVariable('ARTICLE_SOURCE_TITLE', htmlspecialchars($line['feed_title'] ? $line['feed_title'] : $feed_title), true); + $tags = get_article_tags($line["id"], $owner_uid); foreach ($tags as $tag) { @@ -164,13 +181,17 @@ class Handler_Public extends Handler { $feed['articles'] = array(); while ($line = $this->dbh->fetch_assoc($result)) { + $line["content_preview"] = truncate_string(strip_tags($line["content_preview"]), 100, '...'); + foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) { + $line = $p->hook_query_headlines($line, 100); + } $article = array(); $article['id'] = $line['link']; $article['link'] = $line['link']; $article['title'] = $line['title']; - $article['excerpt'] = truncate_string(strip_tags($line["content_preview"]), 100, '...'); - $article['content'] = sanitize($line["content_preview"], false, $owner_uid); + $article['excerpt'] = $line["content_preview"]; + $article['content'] = sanitize($line["content"], false, $owner_uid); $article['updated'] = date('c', strtotime($line["updated"])); if ($line['note']) $article['note'] = $line['note']; @@ -256,16 +277,22 @@ class Handler_Public extends Handler { function pubsub() { $mode = $this->dbh->escape_string($_REQUEST['hub_mode']); + if (!$mode) $mode = $this->dbh->escape_string($_REQUEST['hub.mode']); + $feed_id = (int) $this->dbh->escape_string($_REQUEST['id']); $feed_url = $this->dbh->escape_string($_REQUEST['hub_topic']); + if (!$feed_url) $feed_url = $this->dbh->escape_string($_REQUEST['hub.topic']); + if (!PUBSUBHUBBUB_ENABLED) { header('HTTP/1.0 404 Not Found'); - echo "404 Not found"; + echo "404 Not found (Disabled by server)"; return; } // TODO: implement hub_verifytoken checking + // TODO: store requested rel=self or whatever for verification + // (may be different from stored feed url) e.g. http://url/ or http://url $result = $this->dbh->query("SELECT feed_url FROM ttrss_feeds WHERE id = '$feed_id'"); @@ -274,7 +301,8 @@ class Handler_Public extends Handler { $check_feed_url = $this->dbh->fetch_result($result, 0, "feed_url"); - if ($check_feed_url && ($check_feed_url == $feed_url || !$feed_url)) { + // ignore url checking for the time being + if ($check_feed_url && (true || $check_feed_url == $feed_url || !$feed_url)) { if ($mode == "subscribe") { $this->dbh->query("UPDATE ttrss_feeds SET pubsub_state = 2 @@ -303,11 +331,11 @@ class Handler_Public extends Handler { } } else { header('HTTP/1.0 404 Not Found'); - echo "404 Not found"; + echo "404 Not found (URL check failed)"; } } else { header('HTTP/1.0 404 Not Found'); - echo "404 Not found"; + echo "404 Not found (Feed not found)"; } } @@ -342,7 +370,7 @@ class Handler_Public extends Handler { function rss() { $feed = $this->dbh->escape_string($_REQUEST["id"]); $key = $this->dbh->escape_string($_REQUEST["key"]); - $is_cat = $_REQUEST["is_cat"] != false; + $is_cat = sql_bool_to_bool($_REQUEST["is_cat"]); $limit = (int)$this->dbh->escape_string($_REQUEST["limit"]); $offset = (int)$this->dbh->escape_string($_REQUEST["offset"]); @@ -350,8 +378,10 @@ class Handler_Public extends Handler { $search_mode = $this->dbh->escape_string($_REQUEST["smode"]); $view_mode = $this->dbh->escape_string($_REQUEST["view-mode"]); $order = $this->dbh->escape_string($_REQUEST["order"]); + $start_ts = $this->dbh->escape_string($_REQUEST["ts"]); $format = $this->dbh->escape_string($_REQUEST['format']); + $orig_guid = sql_bool_to_bool($_REQUEST["orig_guid"]); if (!$format) $format = 'atom'; @@ -371,20 +401,24 @@ class Handler_Public extends Handler { if ($owner_id) { $this->generate_syndicated_feed($owner_id, $feed, $is_cat, $limit, - $offset, $search, $search_mode, $view_mode, $format, $order); + $offset, $search, $search_mode, $view_mode, $format, $order, $orig_guid, $start_ts); } else { header('HTTP/1.1 403 Forbidden'); } } - function globalUpdateFeeds() { - include "rssfuncs.php"; - // Update all feeds needing a update. - update_daemon_common(0, true, false); - housekeeping_common(false); + function updateTask() { + PluginHost::getInstance()->run_hooks(PluginHost::HOOK_UPDATE_TASK, "hook_update_task", false); + } - PluginHost::getInstance()->run_hooks(PluginHost::HOOK_UPDATE_TASK, "hook_update_task", $op); + function housekeepingTask() { + PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING, "hook_house_keeping", false); + } + + function globalUpdateFeeds() { + RPC::updaterandomfeed_real($this->dbh); + PluginHost::getInstance()->run_hooks(PluginHost::HOOK_UPDATE_TASK, "hook_update_task", false); } function sharepopup() { @@ -393,11 +427,14 @@ class Handler_Public extends Handler { } header('Content-Type: text/html; charset=utf-8'); - print "<html><head><title>Tiny Tiny RSS</title>"; - - stylesheet_tag("css/utility.css"); - javascript_tag("lib/prototype.js"); - javascript_tag("lib/scriptaculous/scriptaculous.js?load=effects,dragdrop,controls"); + print "<html><head><title>Tiny Tiny RSS</title> + <link rel=\"shortcut icon\" type=\"image/png\" href=\"images/favicon.png\"> + <link rel=\"icon\" type=\"image/png\" sizes=\"72x72\" href=\"images/favicon-72px.png\">"; + + echo stylesheet_tag("css/utility.css"); + echo stylesheet_tag("css/dijit.css"); + echo javascript_tag("lib/prototype.js"); + echo javascript_tag("lib/scriptaculous/scriptaculous.js?load=effects,controls"); print "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/> </head><body id='sharepopup'>"; @@ -543,6 +580,7 @@ class Handler_Public extends Handler { } } else { $_SESSION["login_error_msg"] = __("Incorrect username or password"); + user_error("Failed login attempt for $login from {$_SERVER['REMOTE_ADDR']}", E_USER_WARNING); } if ($_REQUEST['return']) { @@ -553,6 +591,18 @@ class Handler_Public extends Handler { } } + /* function subtest() { + header("Content-type: text/plain; charset=utf-8"); + + $url = $_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(); @@ -568,6 +618,9 @@ class Handler_Public extends Handler { <title>Tiny Tiny RSS</title> <link rel=\"stylesheet\" type=\"text/css\" href=\"css/utility.css\"> <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/> + <link rel=\"shortcut icon\" type=\"image/png\" href=\"images/favicon.png\"> + <link rel=\"icon\" type=\"image/png\" sizes=\"72x72\" href=\"images/favicon-72px.png\"> + </head> <body> <img class=\"floatingLogo\" src=\"images/logo_small.png\" @@ -652,93 +705,6 @@ class Handler_Public extends Handler { } } - function subscribe2() { - $feed_url = $this->dbh->escape_string(trim($_REQUEST["feed_url"])); - $cat_id = $this->dbh->escape_string($_REQUEST["cat_id"]); - $from = $this->dbh->escape_string($_REQUEST["from"]); - $feed_urls = array(); - - /* only read authentication information from POST */ - - $auth_login = $this->dbh->escape_string(trim($_POST["auth_login"])); - $auth_pass = $this->dbh->escape_string(trim($_POST["auth_pass"])); - - $rc = subscribe_to_feed($feed_url, $cat_id, $auth_login, $auth_pass); - - switch ($rc) { - case 1: - print_notice(T_sprintf("Subscribed to <b>%s</b>.", $feed_url)); - break; - case 2: - print_error(T_sprintf("Could not subscribe to <b>%s</b>.", $feed_url)); - break; - case 3: - print_error(T_sprintf("No feeds found in <b>%s</b>.", $feed_url)); - break; - case 0: - print_warning(T_sprintf("Already subscribed to <b>%s</b>.", $feed_url)); - break; - case 4: - print_notice(__("Multiple feed URLs found.")); - $contents = @fetch_file_contents($url, false, $auth_login, $auth_pass); - if (is_html($contents)) { - $feed_urls = get_feeds_from_html($url, $contents); - } - break; - case 5: - print_error(T_sprintf("Could not subscribe to <b>%s</b>.<br>Can't download the Feed URL.", $feed_url)); - break; - } - - if ($feed_urls) { - print "<form action=\"backend.php\">"; - print "<input type=\"hidden\" name=\"op\" value=\"pref-feeds\">"; - print "<input type=\"hidden\" name=\"quiet\" value=\"1\">"; - print "<input type=\"hidden\" name=\"method\" value=\"add\">"; - - print "<select name=\"feed_url\">"; - - foreach ($feed_urls as $url => $name) { - $url = htmlspecialchars($url); - $name = htmlspecialchars($name); - print "<option value=\"$url\">$name</option>"; - } - - print "<input type=\"submit\" value=\"".__("Subscribe to selected feed")."\">"; - print "</form>"; - } - - $tp_uri = get_self_url_prefix() . "/prefs.php"; - $tt_uri = get_self_url_prefix(); - - if ($rc <= 2){ - $result = $this->dbh->query("SELECT id FROM ttrss_feeds WHERE - feed_url = '$feed_url' AND owner_uid = " . $_SESSION["uid"]); - - $feed_id = $this->dbh->fetch_result($result, 0, "id"); - } else { - $feed_id = 0; - } - - print "<p>"; - - if ($feed_id) { - print "<form method=\"GET\" style='display: inline' - action=\"$tp_uri\"> - <input type=\"hidden\" name=\"tab\" value=\"feedConfig\"> - <input type=\"hidden\" name=\"method\" value=\"editFeed\"> - <input type=\"hidden\" name=\"methodparam\" value=\"$feed_id\"> - <input type=\"submit\" value=\"".__("Edit subscription options")."\"> - </form>"; - } - - print "<form style='display: inline' method=\"GET\" action=\"$tt_uri\"> - <input type=\"submit\" value=\"".__("Return to Tiny Tiny RSS")."\"> - </form></p>"; - - print "</body></html>"; - } - function index() { header("Content-Type: text/plain"); print json_encode(array("error" => array("code" => 7))); @@ -747,11 +713,15 @@ class Handler_Public extends Handler { function forgotpass() { startup_gettext(); + @$hash = $_REQUEST["hash"]; + header('Content-Type: text/html; charset=utf-8'); - print "<html><head><title>Tiny Tiny RSS</title>"; + print "<html><head><title>Tiny Tiny RSS</title> + <link rel=\"shortcut icon\" type=\"image/png\" href=\"images/favicon.png\"> + <link rel=\"icon\" type=\"image/png\" sizes=\"72x72\" href=\"images/favicon-72px.png\">"; - stylesheet_tag("css/utility.css"); - javascript_tag("lib/prototype.js"); + echo stylesheet_tag("css/utility.css"); + echo javascript_tag("lib/prototype.js"); print "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/> </head><body id='forgotpass'>"; @@ -762,8 +732,45 @@ class Handler_Public extends Handler { @$method = $_POST['method']; - if (!$method) { - print_notice(__("You will need to provide valid account name and email. New password will be sent on your email address.")); + if ($hash) { + $login = $_REQUEST["login"]; + + if ($login) { + $result = $this->dbh->query("SELECT id, resetpass_token FROM ttrss_users + WHERE login = '$login'"); + + if ($this->dbh->num_rows($result) != 0) { + $id = $this->dbh->fetch_result($result, 0, "id"); + $resetpass_token_full = $this->dbh->fetch_result($result, 0, "resetpass_token"); + list($timestamp, $resetpass_token) = explode(":", $resetpass_token_full); + + if ($timestamp && $resetpass_token && + $timestamp >= time() - 15*60*60 && + $resetpass_token == $hash) { + + $result = $this->dbh->query("UPDATE ttrss_users SET resetpass_token = NULL + WHERE id = $id"); + + Pref_Users::resetUserPassword($id, true); + + print "<p>"."Completed."."</p>"; + + } else { + print_error("Some of the information provided is missing or incorrect."); + } + } else { + print_error("Some of the information provided is missing or incorrect."); + } + } else { + print_error("Some of the information provided is missing or incorrect."); + } + + print "<form method=\"GET\" action=\"index.php\"> + <input type=\"submit\" value=\"".__("Return to Tiny Tiny RSS")."\"> + </form>"; + + } else if (!$method) { + print_notice(__("You will need to provide valid account name and email. A password reset link will be sent to your email address.")); print "<form method='POST' action='public.php'>"; print "<input type='hidden' name='method' value='do'>"; @@ -804,17 +811,57 @@ class Handler_Public extends Handler { } else { + print_notice("Password reset instructions are being sent to your email address."); + $result = $this->dbh->query("SELECT id FROM ttrss_users WHERE login = '$login' AND email = '$email'"); if ($this->dbh->num_rows($result) != 0) { $id = $this->dbh->fetch_result($result, 0, "id"); - Pref_Users::resetUserPassword($id, false); + if ($id) { + $resetpass_token = sha1(get_random_bytes(128)); + $resetpass_link = get_self_url_prefix() . "/public.php?op=forgotpass&hash=" . $resetpass_token . + "&login=" . urlencode($login); + + require_once 'classes/ttrssmailer.php'; + require_once "lib/MiniTemplator.class.php"; + + $tpl = new MiniTemplator; + + $tpl->readTemplateFromFile("templates/resetpass_link_template.txt"); + + $tpl->setVariable('LOGIN', $login); + $tpl->setVariable('RESETPASS_LINK', $resetpass_link); - print "<p>"; + $tpl->addBlock('message'); - print "<p>"."Completed."."</p>"; + $message = ""; + + $tpl->generateOutputToString($message); + + $mail = new ttrssMailer(); + + $rc = $mail->quickMail($email, $login, + __("[tt-rss] Password reset request"), + $message, false); + + if (!$rc) print_error($mail->ErrorInfo); + + $resetpass_token_full = $this->dbh->escape_string(time() . ":" . $resetpass_token); + + $result = $this->dbh->query("UPDATE ttrss_users + SET resetpass_token = '$resetpass_token_full' + WHERE login = '$login' AND email = '$email'"); + + //Pref_Users::resetUserPassword($id, false); + + print "<p>"; + + print "<p>"."Completed."."</p>"; + } else { + print_error("User ID not found."); + } print "<form method=\"GET\" action=\"index.php\"> <input type=\"submit\" value=\"".__("Return to Tiny Tiny RSS")."\"> @@ -853,6 +900,8 @@ class Handler_Public extends Handler { <title>Database Updater</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <link rel="stylesheet" type="text/css" href="css/utility.css"/> + <link rel=\"shortcut icon\" type=\"image/png\" href=\"images/favicon.png\"> + <link rel=\"icon\" type=\"image/png\" sizes=\"72x72\" href=\"images/favicon-72px.png\"> </head> <style type="text/css"> span.ok { color : #009000; font-weight : bold; } diff --git a/classes/opml.php b/classes/opml.php index c5d14cdde..c8c59e8a2 100644 --- a/classes/opml.php +++ b/classes/opml.php @@ -97,7 +97,7 @@ class Opml extends Handler_Protected { $html_url_qpart = ""; } - $out .= "<outline text=\"$title\" xmlUrl=\"$url\" $html_url_qpart/>\n"; + $out .= "<outline type=\"rss\" text=\"$title\" xmlUrl=\"$url\" $html_url_qpart/>\n"; } if ($cat_title) $out .= "</outline>\n"; @@ -190,6 +190,7 @@ class Opml extends Handler_Protected { } $tmp_line["cat_filter"] = sql_bool_to_bool($tmp_line["cat_filter"]); + $tmp_line["inverse"] = sql_bool_to_bool($tmp_line["inverse"]); unset($tmp_line["feed_id"]); unset($tmp_line["cat_id"]); @@ -256,8 +257,8 @@ class Opml extends Handler_Protected { $feed_title = $this->dbh->escape_string(mb_substr($attrs->getNamedItem('text')->nodeValue, 0, 250)); if (!$feed_title) $feed_title = $this->dbh->escape_string(mb_substr($attrs->getNamedItem('title')->nodeValue, 0, 250)); - $feed_url = $this->dbh->escape_string(mb_substr($attrs->getNamedItem('xmlUrl')->nodeValue, 0, 250)); - if (!$feed_url) $feed_url = $this->dbh->escape_string(mb_substr($attrs->getNamedItem('xmlURL')->nodeValue, 0, 250)); + $feed_url = $this->dbh->escape_string($attrs->getNamedItem('xmlUrl')->nodeValue); + if (!$feed_url) $feed_url = $this->dbh->escape_string($attrs->getNamedItem('xmlURL')->nodeValue); $site_url = $this->dbh->escape_string(mb_substr($attrs->getNamedItem('htmlUrl')->nodeValue, 0, 250)); @@ -363,9 +364,10 @@ class Opml extends Handler_Protected { $cat_filter = bool_to_sql_bool($rule["cat_filter"]); $reg_exp = $this->dbh->escape_string($rule["reg_exp"]); $filter_type = (int)$rule["filter_type"]; + $inverse = bool_to_sql_bool($rule["inverse"]); - $this->dbh->query("INSERT INTO ttrss_filters2_rules (feed_id,cat_id,filter_id,filter_type,reg_exp,cat_filter) - VALUES ($feed_id, $cat_id, $filter_id, $filter_type, '$reg_exp', $cat_filter)"); + $this->dbh->query("INSERT INTO ttrss_filters2_rules (feed_id,cat_id,filter_id,filter_type,reg_exp,cat_filter,inverse) + VALUES ($feed_id, $cat_id, $filter_id, $filter_type, '$reg_exp', $cat_filter,$inverse)"); } foreach ($filter["actions"] as $action) { diff --git a/classes/pluginhost.php b/classes/pluginhost.php index 53adf01f9..1ad7afd60 100644 --- a/classes/pluginhost.php +++ b/classes/pluginhost.php @@ -37,6 +37,12 @@ class PluginHost { const HOOK_PREFS_EDIT_FEED = 20; const HOOK_PREFS_SAVE_FEED = 21; const HOOK_FETCH_FEED = 22; + const HOOK_QUERY_HEADLINES = 23; + const HOOK_HOUSE_KEEPING = 24; + const HOOK_SEARCH = 25; + const HOOK_FORMAT_ENCLOSURES = 26; + const HOOK_SUBSCRIBE_FEED = 27; + const HOOK_HEADLINES_BEFORE = 28; const KIND_ALL = 1; const KIND_SYSTEM = 2; @@ -73,6 +79,16 @@ class PluginHost { return $this->dbh; } + function get_plugin_names() { + $names = array(); + + foreach ($this->plugins as $p) { + array_push($names, get_class($p)); + } + + return $names; + } + function get_plugins() { return $this->plugins; } @@ -97,7 +113,7 @@ class PluginHost { function del_hook($type, $sender) { if (is_array($this->hooks[$type])) { - $key = array_Search($this->hooks[$type], $sender); + $key = array_Search($sender, $this->hooks[$type]); if ($key !== FALSE) { unset($this->hooks[$type][$key]); } diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php index d2dc6f7c3..d70c1a26a 100644 --- a/classes/pref/feeds.php +++ b/classes/pref/feeds.php @@ -55,6 +55,7 @@ class Pref_Feeds extends Handler_Protected { $cat['unread'] = 0; $cat['child_unread'] = 0; $cat['auxcounter'] = 0; + $cat['parent_id'] = $cat_id; $cat['items'] = $this->get_category_items($line['id']); @@ -395,7 +396,7 @@ class Pref_Feeds extends Handler_Protected { # print_r($data['items']); if (is_array($data) && is_array($data['items'])) { - $cat_order_id = 0; +# $cat_order_id = 0; $data_map = array(); $root_item = false; @@ -494,7 +495,7 @@ class Pref_Feeds extends Handler_Protected { $feed_id = $this->dbh->escape_string($_REQUEST["feed_id"]); if (is_file($icon_file) && $feed_id) { - if (filesize($icon_file) < 20000) { + if (filesize($icon_file) < 65535) { $result = $this->dbh->query("SELECT id FROM ttrss_feeds WHERE id = '$feed_id' AND owner_uid = ". $_SESSION["uid"]); @@ -572,8 +573,9 @@ class Pref_Feeds extends Handler_Protected { $last_error = $this->dbh->fetch_result($result, 0, "last_error"); if ($last_error) { - print " <span title=\"".htmlspecialchars($last_error)."\" - class=\"feed_error\">(error)</span>"; + print " <img src=\"images/error.png\" alt=\"(error)\" + style=\"vertical-align : middle\" + title=\"".htmlspecialchars($last_error)."\">"; } @@ -736,9 +738,9 @@ class Pref_Feeds extends Handler_Protected { <input type=\"hidden\" name=\"op\" value=\"pref-feeds\"> <input type=\"hidden\" name=\"feed_id\" value=\"$feed_id\"> <input type=\"hidden\" name=\"method\" value=\"uploadicon\"> - <button dojoType=\"dijit.form.Button\" onclick=\"return uploadFeedIcon();\" + <button class=\"small\" dojoType=\"dijit.form.Button\" onclick=\"return uploadFeedIcon();\" type=\"submit\">".__('Replace')."</button> - <button dojoType=\"dijit.form.Button\" onclick=\"return removeFeedIcon($feed_id);\" + <button class=\"small\" dojoType=\"dijit.form.Button\" onclick=\"return removeFeedIcon($feed_id);\" type=\"submit\">".__('Remove')."</button> </form>"; @@ -792,31 +794,10 @@ class Pref_Feeds extends Handler_Protected { print "<div class=\"dlgSec\">".__("Feed")."</div>"; print "<div class=\"dlgSecCont\">"; - /* Title */ - - print "<input dojoType=\"dijit.form.ValidationTextBox\" - disabled=\"1\" style=\"font-size : 16px; width : 20em;\" required=\"1\" - name=\"title\" value=\"\">"; - - $this->batch_edit_cbox("title"); - - /* Feed URL */ - - print "<br/>"; - - print __('URL:') . " "; - print "<input dojoType=\"dijit.form.ValidationTextBox\" disabled=\"1\" - required=\"1\" regExp='^(http|https)://.*' style=\"width : 20em\" - name=\"feed_url\" value=\"\">"; - - $this->batch_edit_cbox("feed_url"); - /* Category */ if (get_pref('ENABLE_FEED_CATS')) { - print "<br/>"; - print __('Place in category:') . " "; print_feed_cat_select("cat_id", false, @@ -862,7 +843,7 @@ class Pref_Feeds extends Handler_Protected { $this->batch_edit_cbox("auth_login"); - print "<br/><input dojoType=\"dijit.form.TextBox\" type=\"password\" name=\"auth_pass\" + print "<hr/> <input dojoType=\"dijit.form.TextBox\" type=\"password\" name=\"auth_pass\" placeHolder=\"".__("Password")."\" disabled=\"1\" value=\"\">"; @@ -982,7 +963,7 @@ class Pref_Feeds extends Handler_Protected { if (!$batch) { - $result = $this->dbh->query("UPDATE ttrss_feeds SET + $this->dbh->query("UPDATE ttrss_feeds SET $category_qpart title = '$feed_title', feed_url = '$feed_link', update_interval = '$upd_intl', @@ -1279,13 +1260,18 @@ class Pref_Feeds extends Handler_Protected { $interval_qpart = "DATE_SUB(NOW(), INTERVAL 3 MONTH)"; } - $result = $this->dbh->query("SELECT COUNT(*) AS num_inactive FROM ttrss_feeds WHERE + // could be performance-intensive and prevent feeds pref-panel from showing + if (!defined('_DISABLE_INACTIVE_FEEDS') || !_DISABLE_INACTIVE_FEEDS) { + $result = $this->dbh->query("SELECT COUNT(*) AS num_inactive FROM ttrss_feeds 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 = ".$_SESSION["uid"]); - $num_inactive = $this->dbh->fetch_result($result, 0, "num_inactive"); + $num_inactive = $this->dbh->fetch_result($result, 0, "num_inactive"); + } else { + $num_inactive = 0; + } if ($num_inactive > 0) { $inactive_button = "<button dojoType=\"dijit.form.Button\" @@ -1492,15 +1478,6 @@ class Pref_Feeds extends Handler_Protected { print "</p>"; - print_warning(__("You can disable all articles shared by unique URLs here.")); - - print "<p>"; - - print "<button dojoType=\"dijit.form.Button\" onclick=\"return clearArticleAccessKeys()\">". - __('Unshare all articles')."</button> "; - - print "</p>"; - PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "hook_prefs_tab_section", "prefFeedsPublishedGenerated"); @@ -1602,8 +1579,6 @@ class Pref_Feeds extends Handler_Protected { # class needed for selectTableRows() print "<tr class=\"placeholder\" $this_row_id>"; - $edit_title = htmlspecialchars($line["title"]); - # id needed for selectTableRows() print "<td width='5%' align='center'><input onclick='toggleSelectRow2(this);' dojoType=\"dijit.form.CheckBox\" @@ -1668,8 +1643,6 @@ class Pref_Feeds extends Handler_Protected { # class needed for selectTableRows() print "<tr class=\"placeholder\" $this_row_id>"; - $edit_title = htmlspecialchars($line["title"]); - # id needed for selectTableRows() print "<td width='5%' align='center'><input onclick='toggleSelectRow2(this);' dojoType=\"dijit.form.CheckBox\" @@ -1920,7 +1893,7 @@ class Pref_Feeds extends Handler_Protected { AND owner_uid = " . $owner_uid); if ($this->dbh->num_rows($result) == 1) { - $key = $this->dbh->escape_string(sha1(uniqid(rand(), true))); + $key = $this->dbh->escape_string(uniqid(base_convert(rand(), 10, 36))); $this->dbh->query("UPDATE ttrss_access_keys SET access_key = '$key' WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat diff --git a/classes/pref/filters.php b/classes/pref/filters.php index bcc7b5aec..170c1a527 100644 --- a/classes/pref/filters.php +++ b/classes/pref/filters.php @@ -88,7 +88,6 @@ class Pref_Filters extends Handler_Protected { $result = $qfh_ret[0]; - $articles = array(); $found = 0; print __("Articles matching this filter:"); @@ -97,12 +96,13 @@ class Pref_Filters extends Handler_Protected { print "<table width=\"100%\" cellspacing=\"0\" id=\"prefErrorFeedList\">"; while ($line = $this->dbh->fetch_assoc($result)) { + $line["content_preview"] = truncate_string(strip_tags($line["content"]), 100, '...'); - $entry_timestamp = strtotime($line["updated"]); - $entry_tags = get_article_tags($line["id"], $_SESSION["uid"]); + foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) { + $line = $p->hook_query_headlines($line, 100); + } - $content_preview = truncate_string( - strip_tags($line["content_preview"]), 100, '...'); + $content_preview = $line["content_preview"]; if ($line["feed_title"]) $feed_title = $line["feed_title"]; @@ -147,6 +147,40 @@ class Pref_Filters extends Handler_Protected { } + private function getfilterrules_concise($filter_id) { + $result = $this->dbh->query("SELECT reg_exp, + inverse, + feed_id, + cat_id, + cat_filter, + ttrss_filter_types.description AS field + FROM + ttrss_filters2_rules, ttrss_filter_types + WHERE + filter_id = '$filter_id' AND filter_type = ttrss_filter_types.id"); + + $rv = ""; + + while ($line = $this->dbh->fetch_assoc($result)) { + + $where = sql_bool_to_bool($line["cat_filter"]) ? + getCategoryTitle($line["cat_id"]) : + ($line["feed_id"] ? + getFeedTitle($line["feed_id"]) : __("All feeds")); + +# $where = $line["cat_id"] . "/" . $line["feed_id"]; + + $inverse = sql_bool_to_bool($line["inverse"]) ? "inverse" : ""; + + $rv .= "<span class='$inverse'>" . T_sprintf("%s on %s in %s %s", + strip_tags($line["reg_exp"]), + $line["field"], + $where, + sql_bool_to_bool($line["inverse"]) ? __("(inverse)") : "") . "</span>"; + } + + return $rv; + } function getfiltertree() { $root = array(); @@ -170,24 +204,11 @@ class Pref_Filters extends Handler_Protected { owner_uid = ".$_SESSION["uid"]." ORDER BY order_id, title"); - $action_id = -1; $folder = array(); $folder['items'] = array(); while ($line = $this->dbh->fetch_assoc($result)) { - /* if ($action_id != $line["action_id"]) { - if (count($folder['items']) > 0) { - array_push($root['items'], $folder); - } - - $folder = array(); - $folder['id'] = $line["action_id"]; - $folder['name'] = __($line["action_name"]); - $folder['items'] = array(); - $action_id = $line["action_id"]; - } */ - $name = $this->getFilterName($line["id"]); $match_ok = false; @@ -223,6 +244,7 @@ class Pref_Filters extends Handler_Protected { $filter['param'] = $name[1]; $filter['checkbox'] = false; $filter['enabled'] = sql_bool_to_bool($line["enabled"]); + $filter['rules'] = $this->getfilterrules_concise($line['id']); if (!$filter_search || $match_ok) { array_push($folder['items'], $filter); @@ -429,8 +451,11 @@ class Pref_Filters extends Handler_Protected { WHERE id = ".(int)$rule["filter_type"]); $filter_type = $this->dbh->fetch_result($result, 0, "description"); - return T_sprintf("%s on %s in %s %s", strip_tags($rule["reg_exp"]), - $filter_type, $feed, isset($rule["inverse"]) ? __("(inverse)") : ""); + $inverse = isset($rule["inverse"]) ? "inverse" : ""; + + return "<span class='filterRule $inverse'>" . + T_sprintf("%s on %s in %s %s", strip_tags($rule["reg_exp"]), + $filter_type, $feed, isset($rule["inverse"]) ? __("(inverse)") : "") . "</span>"; } function printRuleName() { @@ -467,7 +492,7 @@ class Pref_Filters extends Handler_Protected { $inverse = checkbox_to_sql_bool($this->dbh->escape_string($_REQUEST["inverse"])); $title = $this->dbh->escape_string($_REQUEST["title"]); - $result = $this->dbh->query("UPDATE ttrss_filters2 SET enabled = $enabled, + $this->dbh->query("UPDATE ttrss_filters2 SET enabled = $enabled, match_any_rule = $match_any_rule, inverse = $inverse, title = '$title' @@ -585,14 +610,15 @@ class Pref_Filters extends Handler_Protected { $enabled = checkbox_to_sql_bool($_REQUEST["enabled"]); $match_any_rule = checkbox_to_sql_bool($_REQUEST["match_any_rule"]); $title = $this->dbh->escape_string($_REQUEST["title"]); + $inverse = checkbox_to_sql_bool($_REQUEST["inverse"]); $this->dbh->query("BEGIN"); /* create base filter */ $result = $this->dbh->query("INSERT INTO ttrss_filters2 - (owner_uid, match_any_rule, enabled, title) VALUES - (".$_SESSION["uid"].",$match_any_rule,$enabled, '$title')"); + (owner_uid, match_any_rule, enabled, title, inverse) VALUES + (".$_SESSION["uid"].",$match_any_rule,$enabled, '$title', $inverse)"); $result = $this->dbh->query("SELECT MAX(id) AS id FROM ttrss_filters2 WHERE owner_uid = ".$_SESSION["uid"]); @@ -870,6 +896,11 @@ class Pref_Filters extends Handler_Protected { print "<div class=\"dlgButtons\">"; + print "<div style=\"float : left\"> + <a class=\"visibleLink\" target=\"_blank\" href=\"http://tt-rss.org/wiki/ContentFilters\">".__("Wiki: Filters")."</a> + </div>"; + + print "<button dojoType=\"dijit.form.Button\" onclick=\"return dijit.byId('filterNewRuleDlg').execute()\">". ($rule ? __("Save rule") : __('Add rule'))."</button> "; diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php index 32071e3b3..571237239 100644 --- a/classes/pref/prefs.php +++ b/classes/pref/prefs.php @@ -181,7 +181,8 @@ class Pref_Prefs extends Handler_Protected { global $access_level_names; $prefs_blacklist = array("STRIP_UNSAFE_TAGS", "REVERSE_HEADLINES", - "SORT_HEADLINES_BY_FEED_DATE", "DEFAULT_ARTICLE_LIMIT"); + "SORT_HEADLINES_BY_FEED_DATE", "DEFAULT_ARTICLE_LIMIT", + "FEEDS_SORT_BY_UNREAD"); /* "FEEDS_SORT_BY_UNREAD", "HIDE_READ_FEEDS", "REVERSE_HEADLINES" */ @@ -765,7 +766,9 @@ class Pref_Prefs extends Handler_Protected { dojoType=\"dijit.form.CheckBox\" $checked type=\"checkbox\"></td>"; - print "<td>$name</td>"; + $plugin_icon = $checked ? "plugin.png" : "plugin_disabled.png"; + + print "<td><label><img src='images/$plugin_icon' alt=''> $name</label></td>"; print "<td>" . htmlspecialchars($about[1]); if (@$about[4]) { print " — <a target=\"_blank\" class=\"visibleLink\" @@ -818,11 +821,13 @@ class Pref_Prefs extends Handler_Protected { print "<tr class='$rowclass'>"; + $plugin_icon = $checked ? "plugin.png" : "plugin_disabled.png"; + print "<td align='center'><input id='FPCHK-$name' name='plugins[]' value='$name' onclick='toggleSelectRow2(this);' dojoType=\"dijit.form.CheckBox\" $checked $disabled type=\"checkbox\"></td>"; - print "<td><label for='FPCHK-$name'>$name</label></td>"; + print "<td><label for='FPCHK-$name'><img src='images/$plugin_icon' alt=''> $name</label></td>"; print "<td><label for='FPCHK-$name'>" . htmlspecialchars($about[1]) . "</label>"; if (@$about[4]) { print " — <a target=\"_blank\" class=\"visibleLink\" @@ -883,8 +888,9 @@ class Pref_Prefs extends Handler_Protected { if (!$otp_enabled) { $secret = $base32->encode(sha1($this->dbh->fetch_result($result, 0, "salt"))); - $topt = new \OTPHP\TOTP($secret); - print QRcode::png($topt->provisioning_uri($login)); + print QRcode::png("otpauth://totp/".urlencode($login). + "?secret=$secret&issuer=".urlencode("Tiny Tiny RSS")); + } } diff --git a/classes/pref/users.php b/classes/pref/users.php index 8a0202483..a5d48ac96 100644 --- a/classes/pref/users.php +++ b/classes/pref/users.php @@ -258,7 +258,7 @@ class Pref_Users extends Handler_Protected { $pwd_hash = encrypt_password($tmp_user_pwd, $new_salt, true); - db_query("UPDATE ttrss_users SET pwd_hash = '$pwd_hash', salt = '$new_salt' + db_query("UPDATE ttrss_users SET pwd_hash = '$pwd_hash', salt = '$new_salt', otp_enabled = false WHERE id = '$uid'"); if ($show_password) { @@ -418,7 +418,7 @@ class Pref_Users extends Handler_Protected { $onclick = "onclick='editUser($uid, event)' title='".__('Click to edit')."'"; - print "<td $onclick>" . $line["login"] . "</td>"; + print "<td $onclick><img src='images/user.png' class='markedPic' alt=''> " . $line["login"] . "</td>"; if (!$line["email"]) $line["email"] = " "; diff --git a/classes/rpc.php b/classes/rpc.php index 46583feb5..b4de44a74 100644 --- a/classes/rpc.php +++ b/classes/rpc.php @@ -95,7 +95,7 @@ class RPC extends Handler_Protected { WHERE orig_feed_id = '$id') = 0 AND id = '$id' AND owner_uid = ".$_SESSION["uid"]); - $rc = $this->dbh->affected_rows($result); + $this->dbh->affected_rows($result); } } @@ -138,7 +138,7 @@ class RPC extends Handler_Protected { $mark = "false"; } - $result = $this->dbh->query("UPDATE ttrss_user_entries SET marked = $mark, + $this->dbh->query("UPDATE ttrss_user_entries SET marked = $mark, last_marked = NOW() WHERE ref_id = '$id' AND owner_uid = " . $_SESSION["uid"]); @@ -148,8 +148,8 @@ class RPC extends Handler_Protected { function delete() { $ids = $this->dbh->escape_string($_REQUEST["ids"]); - $result = $this->dbh->query("DELETE FROM ttrss_user_entries - WHERE ref_id IN ($ids) AND owner_uid = " . $_SESSION["uid"]); + $this->dbh->query("DELETE FROM ttrss_user_entries + WHERE ref_id IN ($ids) AND owner_uid = " . $_SESSION["uid"]); purge_orphans(); @@ -258,7 +258,6 @@ class RPC extends Handler_Protected { function publ() { $pub = $_REQUEST["pub"]; $id = $this->dbh->escape_string($_REQUEST["id"]); - $note = trim(strip_tags($this->dbh->escape_string($_REQUEST["note"]))); if ($pub == "1") { $pub = "true"; @@ -266,7 +265,7 @@ class RPC extends Handler_Protected { $pub = "false"; } - $result = $this->dbh->query("UPDATE ttrss_user_entries SET + $this->dbh->query("UPDATE ttrss_user_entries SET published = $pub, last_published = NOW() WHERE ref_id = '$id' AND owner_uid = " . $_SESSION["uid"]); @@ -467,14 +466,6 @@ class RPC extends Handler_Protected { print_feed_cat_select("cat_id", $id, ''); } - // Silent - function clearArticleKeys() { - $this->dbh->query("UPDATE ttrss_user_entries SET uuid = '' WHERE - owner_uid = " . $_SESSION["uid"]); - - return; - } - function setpanelmode() { $wide = (int) $_REQUEST["wide"]; @@ -484,7 +475,8 @@ class RPC extends Handler_Protected { print json_encode(array("wide" => $wide)); } - function updaterandomfeed() { + static function updaterandomfeed_real($dbh) { + // Test if the feed need a update (update interval exceded). if (DB_TYPE == "pgsql") { $update_limit_qpart = "AND (( @@ -515,16 +507,24 @@ class RPC extends Handler_Protected { $random_qpart = sql_random_function(); + // we could be invoked from public.php with no active session + if ($_SESSION["uid"]) { + $owner_check_qpart = "AND ttrss_feeds.owner_uid = '".$_SESSION["uid"]."'"; + } else { + $owner_check_qpart = ""; + } + // We search for feed needing update. - $result = $this->dbh->query("SELECT ttrss_feeds.feed_url,ttrss_feeds.id + $result = $dbh->query("SELECT ttrss_feeds.feed_url,ttrss_feeds.id FROM ttrss_feeds, ttrss_users, ttrss_user_prefs WHERE ttrss_feeds.owner_uid = ttrss_users.id AND ttrss_users.id = ttrss_user_prefs.owner_uid AND ttrss_user_prefs.pref_name = 'DEFAULT_UPDATE_INTERVAL' - AND ttrss_feeds.owner_uid = ".$_SESSION["uid"]." - $update_limit_qpart $updstart_thresh_qpart + $owner_check_qpart + $update_limit_qpart + $updstart_thresh_qpart ORDER BY $random_qpart LIMIT 30"); $feed_id = -1; @@ -535,7 +535,7 @@ class RPC extends Handler_Protected { $tstart = time(); - while ($line = $this->dbh->fetch_assoc($result)) { + while ($line = $dbh->fetch_assoc($result)) { $feed_id = $line["id"]; if (time() - $tstart < ini_get("max_execution_time") * 0.7) { @@ -559,6 +559,10 @@ class RPC extends Handler_Protected { } + function updaterandomfeed() { + RPC::updaterandomfeed_real($this->dbh); + } + private function markArticlesById($ids, $cmode) { $tmp_ids = array(); @@ -615,7 +619,7 @@ class RPC extends Handler_Protected { $p = new Publisher(PUBSUBHUBBUB_HUB); - $pubsub_result = $p->publish_update($rss_link); + /* $pubsub_result = */ $p->publish_update($rss_link); } } |