diff --git a/.gitignore b/.gitignore index ac3c24319..e7e5407d0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,13 +2,13 @@ *~ *.DS_Store #* -lib/floIcon.php .idea/* config.php feed-icons/* cache/*/* lock/* tags +plugins/fever cache/htmlpurifier/*/*ser lib/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serializer/*/*ser web.config diff --git a/README.md b/README.md index 3aeea3319..0c3176f13 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,25 @@ Tiny Tiny RSS Web-based news feed aggregator, designed to allow you to read news from any location, while feeling as close to a real desktop application as possible. -http://tt-rss.org +http://tt-rss.org (http://mirror.tt-rss.org) -Licensed under GNU GPL version 2 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . Copyright (c) 2005 Andrew Dolgov (unless explicitly stated otherwise). +Uses Silk icons by Mark James: http://www.famfamfam.com/lab/icons/silk/ + ## Requirements * Compatible web browser (http://tt-rss.org/wiki/CompatibleBrowsers) diff --git a/atom-to-html.xsl b/atom-to-html.xsl index e1042648c..1b827bb8f 100644 --- a/atom-to-html.xsl +++ b/atom-to-html.xsl @@ -21,9 +21,6 @@
- feed icon -

This feed has been exported from 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.") . "

"; print "
"; - $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 ""; + print ""; } print ""; 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 @@ 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 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 .= ""; + $rss_link = htmlspecialchars(get_self_url_prefix() . "/public.php?op=rss&id=$feed_id$cat_q$search_q"); // right part - $reply .= ""; - $reply .= ""; - $reply .= ""; + $error_class = $error ? "error" : ""; + + $reply .= " + + "; + + +# $reply .= ""; + $reply .= ""; if ($feed_site_url) { $last_updated = T_sprintf("Last updated: %s", @@ -58,10 +62,11 @@ class Feeds extends Handler_Protected { $target = "target=\"_blank\""; $reply .= "". - truncate_string($feed_title,30).""; + truncate_string($feed_title, 30).""; if ($error) { - $reply .= " (Error)"; + $error = htmlspecialchars($error); + $reply .= " error"; } } else { @@ -70,17 +75,16 @@ class Feeds extends Handler_Protected { $reply .= ""; - $reply .= " - - "; - $reply .= ""; +# $reply .= ""; + // left part - $reply .= __('Select:')." + $reply .= ""; + $reply .= ""; + + $reply .= " ".__('All').", ".__('Unread').", ".__('Invert').", @@ -129,14 +133,14 @@ class Feeds extends Handler_Protected { $reply .= ""; - //$reply .= "
"; - //$reply .= "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 .= ""; + 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 = ""; + $labels_str = ""; $labels_str .= format_article_labels($labels, $id); $labels_str .= ""; @@ -323,24 +344,24 @@ class Feeds extends Handler_Protected { if (sql_bool_to_bool($line["marked"])) { $marked_pic = "\"Unstar"; $class .= " marked"; } else { $marked_pic = "\"Star"; } if (sql_bool_to_bool($line["published"])) { - $published_pic = "\"Unpublish"; $class .= " published"; } else { - $published_pic = "\"Publish"; } @@ -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 = "\"\""; } else { - $feed_icon_img = "\"\""; + $feed_icon_img = "\"\""; } $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 = "(".__('Mark as read').")"; + $vf_catchup_link = "".__('mark feed as read').""; - $reply['content'] .= "
". - "
$feed_icon_img
". - "". - $line["feed_title"]." $vf_catchup_link
"; + $reply['content'] .= "
". + "
$feed_icon_img
". + "". $line["feed_title"]." + $vf_catchup_link
"; } } @@ -434,7 +450,7 @@ class Feeds extends Handler_Protected { $mouseover_attrs = "onmouseover='postMouseIn(event, $id)' onmouseout='postMouseOut($id)'"; - $reply['content'] .= "
"; + $reply['content'] .= "
"; $reply['content'] .= "
"; @@ -448,16 +464,14 @@ class Feeds extends Handler_Protected { $reply['content'] .= "
"; $reply['content'] .= ""; - $reply['content'] .= ""; - - if (!get_pref('VFEED_GROUP_BY_FEED')) { + if (!$vfeed_group_enabled) { if (@$line["feed_title"]) { $rgba = @$rgba_cache[$feed_id]; - $reply['content'] .= "". - truncate_string($line["feed_title"],30).""; + $reply['content'] .= "". + truncate_string($line["feed_title"],30).""; } } + + $reply['content'] .= ""; + $reply['content'] .= "
$updated_fmt
"; @@ -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'] .= " - $feed_icon_img"; + $feed_icon_img"; } $reply['content'] .= "
"; @@ -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 = "(".__('mark as read').")"; + $vf_catchup_link = "".__('mark feed as read').""; $has_feed_icon = feed_has_icon($feed_id); @@ -527,7 +542,7 @@ class Feeds extends Handler_Protected { //$feed_icon_img = "\"\""; } - $reply['content'] .= "
". + $reply['content'] .= "
". "
$feed_icon_img
". "". $line["feed_title"]." $vf_catchup_link
"; @@ -539,10 +554,10 @@ class Feeds extends Handler_Protected { $expanded_class = $expand_cdm ? "expanded" : "expandable"; - $reply['content'] .= "
"; + $reply['content'] .= "
"; - $reply['content'] .= "
"; + $reply['content'] .= "
"; $reply['content'] .= ""; - $reply['content'] .= "
"; + if (!$line['lang']) $line['lang'] = 'en'; + + $reply['content'] .= ""; } @@ -685,10 +709,13 @@ class Feeds extends Handler_Protected { } else { $comments_url = htmlspecialchars($line["link"]); } - $entry_comments = "$num_comments comments"; + $entry_comments = "$num_comments ". + _ngettext("comment", "comments", $num_comments).""; + } else { if ($line["comments"] && $line["link"] != $line["comments"]) { - $entry_comments = "comments"; + $entry_comments = "".__("comments").""; } } @@ -706,7 +733,7 @@ class Feeds extends Handler_Protected { $reply['content'] .= "
"; $reply['content'] .= "
"; - $reply['content'] .= "

"; + $reply['content'] .= "
"; $reply['content'] .= "
"; @@ -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 ""; print ""; + print ""; + print "
".__("Feed or site URL")."
"; print "
"; @@ -1054,20 +1083,18 @@ class Feeds extends Handler_Protected { print " "; print "
"; - $owner_uid = $_SESSION["uid"]; - require_once "feedbrowser.php"; print "
    "; - print make_feed_browser($search, 25); + print make_feed_browser("", 25); print "
"; print "
@@ -1126,9 +1153,9 @@ class Feeds extends Handler_Protected { print "
"; - if (!SPHINX_ENABLED) { + if (count(PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH)) == 0) { print ""; } 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 = "
Article note: " . $line['note'] . "
" . @@ -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 "Tiny Tiny RSS"; - - stylesheet_tag("css/utility.css"); - javascript_tag("lib/prototype.js"); - javascript_tag("lib/scriptaculous/scriptaculous.js?load=effects,dragdrop,controls"); + print "Tiny Tiny RSS + + "; + + 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 " "; @@ -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 { Tiny Tiny RSS + + + 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 %s.", $feed_url)); - break; - case 2: - print_error(T_sprintf("Could not subscribe to %s.", $feed_url)); - break; - case 3: - print_error(T_sprintf("No feeds found in %s.", $feed_url)); - break; - case 0: - print_warning(T_sprintf("Already subscribed to %s.", $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 %s.
Can't download the Feed URL.", $feed_url)); - break; - } - - if ($feed_urls) { - print "
"; - print ""; - print ""; - print ""; - - print ""; - print "
"; - } - - $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 "

"; - - if ($feed_id) { - print "

- - - - -
"; - } - - print "
- -

"; - - print ""; - } - 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 "Tiny Tiny RSS"; + print "Tiny Tiny RSS + + "; - stylesheet_tag("css/utility.css"); - javascript_tag("lib/prototype.js"); + echo stylesheet_tag("css/utility.css"); + echo javascript_tag("lib/prototype.js"); print " "; @@ -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 "

"."Completed."."

"; + + } 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 "
+ +
"; + + } 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 "
"; print ""; @@ -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 "

"; + $tpl->addBlock('message'); - print "

"."Completed."."

"; + $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 "

"; + + print "

"."Completed."."

"; + } else { + print_error("User ID not found."); + } print " @@ -853,6 +900,8 @@ class Handler_Public extends Handler { Database Updater + + "; - } - - } - - function rewrite_urls($html) { - libxml_use_internal_errors(true); - - $charset_hack = ' - - '; - - $doc = new DOMDocument(); - $doc->loadHTML($charset_hack . $html); - $xpath = new DOMXPath($doc); - - $entries = $xpath->query('//*/text()'); - - foreach ($entries as $entry) { - if (strstr($entry->wholeText, "://") !== false) { - $text = preg_replace("/((?\\1", $entry->wholeText); - - if ($text != $entry->wholeText) { - $cdoc = new DOMDocument(); - $cdoc->loadHTML($charset_hack . $text); - - - foreach ($cdoc->childNodes as $cnode) { - $cnode = $doc->importNode($cnode, true); - - if ($cnode) { - $entry->parentNode->insertBefore($cnode); - } - } - - $entry->parentNode->removeChild($entry); - - } - } - } - - $node = $doc->getElementsByTagName('body')->item(0); - - // http://tt-rss.org/forum/viewtopic.php?f=1&t=970 - if ($node) - return $doc->saveXML($node); - else - return $html; - } - - function filter_to_sql($filter, $owner_uid) { - $query = array(); - - if (DB_TYPE == "pgsql") - $reg_qpart = "~"; - else - $reg_qpart = "REGEXP"; - - foreach ($filter["rules"] AS $rule) { - $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/', - $rule['reg_exp']) !== FALSE; - - if ($regexp_valid) { - - $rule['reg_exp'] = db_escape_string($rule['reg_exp']); - - switch ($rule["type"]) { - case "title": - $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('". - $rule['reg_exp'] . "')"; - break; - case "content": - $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('". - $rule['reg_exp'] . "')"; - break; - case "both": - $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('". - $rule['reg_exp'] . "') OR LOWER(" . - "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')"; - break; - case "tag": - $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('". - $rule['reg_exp'] . "')"; - break; - case "link": - $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('". - $rule['reg_exp'] . "')"; - break; - case "author": - $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('". - $rule['reg_exp'] . "')"; - break; - } - - if (isset($rule['inverse'])) $qpart = "NOT ($qpart)"; - - if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) { - $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]); - } - - if (isset($rule["cat_id"])) { - - if ($rule["cat_id"] > 0) { - $children = getChildCategories($rule["cat_id"], $owner_uid); - array_push($children, $rule["cat_id"]); - - $children = join(",", $children); - - $cat_qpart = "cat_id IN ($children)"; - } else { - $cat_qpart = "cat_id IS NULL"; - } - - $qpart .= " AND $cat_qpart"; - } - - array_push($query, "($qpart)"); - - } - } - - if (count($query) > 0) { - $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")"; - } else { - $fullquery = "(false)"; - } - - if ($filter['inverse']) $fullquery = "(NOT $fullquery)"; - - return $fullquery; - } - - if (!function_exists('gzdecode')) { - function gzdecode($string) { // no support for 2nd argument - return file_get_contents('compress.zlib://data:who/cares;base64,'. - base64_encode($string)); - } - } - - function get_random_bytes($length) { - if (function_exists('openssl_random_pseudo_bytes')) { - return openssl_random_pseudo_bytes($length); - } else { - $output = ""; - - for ($i = 0; $i < $length; $i++) - $output .= chr(mt_rand(0, 255)); - - return $output; - } - } - - function read_stdin() { - $fp = fopen("php://stdin", "r"); - - if ($fp) { - $line = trim(fgets($fp)); - fclose($fp); - return $line; - } - - return null; - } - - function tmpdirname($path, $prefix) { - // Use PHP's tmpfile function to create a temporary - // directory name. Delete the file and keep the name. - $tempname = tempnam($path,$prefix); - if (!$tempname) - return false; - - if (!unlink($tempname)) - return false; - - return $tempname; - } - - function getFeedCategory($feed) { - $result = db_query("SELECT cat_id FROM ttrss_feeds - WHERE id = '$feed'"); - - if (db_num_rows($result) > 0) { - return db_fetch_result($result, 0, "cat_id"); - } else { - return false; - } - - } - - function implements_interface($class, $interface) { - return in_array($interface, class_implements($class)); - } - - function geturl($url){ - - if (!function_exists('curl_init')) - return user_error('CURL Must be installed for geturl function to work. Ask your host to enable it or uncomment extension=php_curl.dll in php.ini', E_USER_ERROR); - - $curl = curl_init(); - $header[0] = "Accept: text/xml,application/xml,application/xhtml+xml,"; - $header[0] .= "text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"; - $header[] = "Cache-Control: max-age=0"; - $header[] = "Connection: keep-alive"; - $header[] = "Keep-Alive: 300"; - $header[] = "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7"; - $header[] = "Accept-Language: en-us,en;q=0.5"; - $header[] = "Pragma: "; - - curl_setopt($curl, CURLOPT_URL, $url); - curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 5.1; rv:5.0) Gecko/20100101 Firefox/5.0 Firefox/5.0'); - curl_setopt($curl, CURLOPT_HTTPHEADER, $header); - curl_setopt($curl, CURLOPT_HEADER, true); - curl_setopt($curl, CURLOPT_REFERER, $url); - curl_setopt($curl, CURLOPT_ENCODING, 'gzip,deflate'); - curl_setopt($curl, CURLOPT_AUTOREFERER, true); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - //curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); //CURLOPT_FOLLOWLOCATION Disabled... - curl_setopt($curl, CURLOPT_TIMEOUT, 60); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); - - $html = curl_exec($curl); - - $status = curl_getinfo($curl); - - if($status['http_code']!=200){ - if($status['http_code'] == 301 || $status['http_code'] == 302) { - curl_close($curl); - list($header) = explode("\r\n\r\n", $html, 2); - $matches = array(); - preg_match("/(Location:|URI:)[^(\n)]*/", $header, $matches); - $url = trim(str_replace($matches[1],"",$matches[0])); - $url_parsed = parse_url($url); - return (isset($url_parsed))? geturl($url):''; - } - - global $fetch_last_error; - - $fetch_last_error = curl_errno($curl) . " " . curl_error($curl); - curl_close($curl); - - $oline=''; - foreach($status as $key=>$eline){$oline.='['.$key.']'.$eline.' ';} - $line =$oline." \r\n ".$url."\r\n-----------------\r\n"; -# $handle = @fopen('./curl.error.log', 'a'); -# fwrite($handle, $line); - return FALSE; - } - curl_close($curl); - return $url; - } - - function get_minified_js($files) { - require_once 'lib/jshrink/Minifier.php'; - - $rv = ''; - - foreach ($files as $js) { - if (!isset($_GET['debug'])) { - $cached_file = CACHE_DIR . "/js/".basename($js).".js"; - - if (file_exists($cached_file) && - is_readable($cached_file) && - filemtime($cached_file) >= filemtime("js/$js.js")) { - - $rv .= file_get_contents($cached_file); - - } else { - $minified = JShrink\Minifier::minify(file_get_contents("js/$js.js")); - file_put_contents($cached_file, $minified); - $rv .= $minified; - } - } else { - $rv .= file_get_contents("js/$js.js"); - } - } - - return $rv; - } - - function stylesheet_tag($filename) { - $timestamp = filemtime($filename); - - echo "\n"; - } - - function javascript_tag($filename) { - $query = ""; - - if (!(strpos($filename, "?") === FALSE)) { - $query = substr($filename, strpos($filename, "?")+1); - $filename = substr($filename, 0, strpos($filename, "?")); - } - - $timestamp = filemtime($filename); - - if ($query) $timestamp .= "&$query"; - - echo "\n"; - } - - function calculate_dep_timestamp() { - $files = array_merge(glob("js/*.js"), glob("css/*.css")); - - $max_ts = -1; - - foreach ($files as $file) { - if (filemtime($file) > $max_ts) $max_ts = filemtime($file); - } - - return $max_ts; - } - - function T_js_decl($s1, $s2) { - if ($s1 && $s2) { - $s1 = preg_replace("/\n/", "", $s1); - $s2 = preg_replace("/\n/", "", $s2); - - $s1 = preg_replace("/\"/", "\\\"", $s1); - $s2 = preg_replace("/\"/", "\\\"", $s2); - - return "T_messages[\"$s1\"] = \"$s2\";\n"; - } - } - - function init_js_translations() { - - print 'var T_messages = new Object(); - - function __(msg) { - if (T_messages[msg]) { - return T_messages[msg]; - } else { - return msg; - } - } - - function ngettext(msg1, msg2, n) { - return (parseInt(n) > 1) ? msg2 : msg1; - }'; - - $l10n = _get_reader(); - - for ($i = 0; $i < $l10n->total; $i++) { - $orig = $l10n->get_original_string($i); - $translation = __($orig); - - print T_js_decl($orig, $translation); - } - } - - function label_to_feed_id($label) { - return LABEL_BASE_INDEX - 1 - abs($label); - } - - function feed_to_label_id($feed) { - return LABEL_BASE_INDEX - 1 + abs($feed); - } - - function format_libxml_error($error) { - return T_sprintf("LibXML error %s at line %d (column %d): %s", - $error->code, $error->line, $error->column, - $error->message); - } + // TODO: less dumb splitting + require_once "functions2.php"; ?> diff --git a/include/functions2.php b/include/functions2.php new file mode 100644 index 000000000..672373e6d --- /dev/null +++ b/include/functions2.php @@ -0,0 +1,2403 @@ +get_plugin_names()); + + $params["php_platform"] = PHP_OS; + $params["php_version"] = PHP_VERSION; + + $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php")); + + $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM + ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]); + + $max_feed_id = db_fetch_result($result, 0, "mid"); + $num_feeds = db_fetch_result($result, 0, "nf"); + + $params["max_feed_id"] = (int) $max_feed_id; + $params["num_feeds"] = (int) $num_feeds; + + $params["hotkeys"] = get_hotkeys_map(); + + $params["csrf_token"] = $_SESSION["csrf_token"]; + $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"]; + + $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE; + + return $params; + } + + function get_hotkeys_info() { + $hotkeys = array( + __("Navigation") => array( + "next_feed" => __("Open next feed"), + "prev_feed" => __("Open previous feed"), + "next_article" => __("Open next article"), + "prev_article" => __("Open previous article"), + "next_article_noscroll" => __("Open next article (don't scroll long articles)"), + "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"), + "next_article_noexpand" => __("Move to next article (don't expand or mark read)"), + "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"), + "search_dialog" => __("Show search dialog")), + __("Article") => array( + "toggle_mark" => __("Toggle starred"), + "toggle_publ" => __("Toggle published"), + "toggle_unread" => __("Toggle unread"), + "edit_tags" => __("Edit tags"), + "dismiss_selected" => __("Dismiss selected"), + "dismiss_read" => __("Dismiss read"), + "open_in_new_window" => __("Open in new window"), + "catchup_below" => __("Mark below as read"), + "catchup_above" => __("Mark above as read"), + "article_scroll_down" => __("Scroll down"), + "article_scroll_up" => __("Scroll up"), + "select_article_cursor" => __("Select article under cursor"), + "email_article" => __("Email article"), + "close_article" => __("Close/collapse article"), + "toggle_expand" => __("Toggle article expansion (combined mode)"), + "toggle_widescreen" => __("Toggle widescreen mode"), + "toggle_embed_original" => __("Toggle embed original")), + __("Article selection") => array( + "select_all" => __("Select all articles"), + "select_unread" => __("Select unread"), + "select_marked" => __("Select starred"), + "select_published" => __("Select published"), + "select_invert" => __("Invert selection"), + "select_none" => __("Deselect everything")), + __("Feed") => array( + "feed_refresh" => __("Refresh current feed"), + "feed_unhide_read" => __("Un/hide read feeds"), + "feed_subscribe" => __("Subscribe to feed"), + "feed_edit" => __("Edit feed"), + "feed_catchup" => __("Mark as read"), + "feed_reverse" => __("Reverse headlines"), + "feed_debug_update" => __("Debug feed update"), + "catchup_all" => __("Mark all feeds as read"), + "cat_toggle_collapse" => __("Un/collapse current category"), + "toggle_combined_mode" => __("Toggle combined mode"), + "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")), + __("Go to") => array( + "goto_all" => __("All articles"), + "goto_fresh" => __("Fresh"), + "goto_marked" => __("Starred"), + "goto_published" => __("Published"), + "goto_tagcloud" => __("Tag cloud"), + "goto_prefs" => __("Preferences")), + __("Other") => array( + "create_label" => __("Create label"), + "create_filter" => __("Create filter"), + "collapse_sidebar" => __("Un/collapse sidebar"), + "help_dialog" => __("Show help dialog")) + ); + + foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) { + $hotkeys = $plugin->hook_hotkey_info($hotkeys); + } + + return $hotkeys; + } + + function get_hotkeys_map() { + $hotkeys = array( +// "navigation" => array( + "k" => "next_feed", + "j" => "prev_feed", + "n" => "next_article", + "p" => "prev_article", + "(38)|up" => "prev_article", + "(40)|down" => "next_article", +// "^(38)|Ctrl-up" => "prev_article_noscroll", +// "^(40)|Ctrl-down" => "next_article_noscroll", + "(191)|/" => "search_dialog", +// "article" => array( + "s" => "toggle_mark", + "*s" => "toggle_publ", + "u" => "toggle_unread", + "*t" => "edit_tags", + "*d" => "dismiss_selected", + "*x" => "dismiss_read", + "o" => "open_in_new_window", + "c p" => "catchup_below", + "c n" => "catchup_above", + "*n" => "article_scroll_down", + "*p" => "article_scroll_up", + "*(38)|Shift+up" => "article_scroll_up", + "*(40)|Shift+down" => "article_scroll_down", + "a *w" => "toggle_widescreen", + "a e" => "toggle_embed_original", + "e" => "email_article", + "a q" => "close_article", +// "article_selection" => array( + "a a" => "select_all", + "a u" => "select_unread", + "a *u" => "select_marked", + "a p" => "select_published", + "a i" => "select_invert", + "a n" => "select_none", +// "feed" => array( + "f r" => "feed_refresh", + "f a" => "feed_unhide_read", + "f s" => "feed_subscribe", + "f e" => "feed_edit", + "f q" => "feed_catchup", + "f x" => "feed_reverse", + "f *d" => "feed_debug_update", + "f *c" => "toggle_combined_mode", + "f c" => "toggle_cdm_expanded", + "*q" => "catchup_all", + "x" => "cat_toggle_collapse", +// "goto" => array( + "g a" => "goto_all", + "g f" => "goto_fresh", + "g s" => "goto_marked", + "g p" => "goto_published", + "g t" => "goto_tagcloud", + "g *p" => "goto_prefs", +// "other" => array( + "(9)|Tab" => "select_article_cursor", // tab + "c l" => "create_label", + "c f" => "create_filter", + "c s" => "collapse_sidebar", + "^(191)|Ctrl+/" => "help_dialog", + ); + + if (get_pref('COMBINED_DISPLAY_MODE')) { + $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll"; + $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll"; + } + + foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) { + $hotkeys = $plugin->hook_hotkey_map($hotkeys); + } + + $prefixes = array(); + + foreach (array_keys($hotkeys) as $hotkey) { + $pair = explode(" ", $hotkey, 2); + + if (count($pair) > 1 && !in_array($pair[0], $prefixes)) { + array_push($prefixes, $pair[0]); + } + } + + return array($prefixes, $hotkeys); + } + + function make_runtime_info() { + $data = array(); + + $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM + ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]); + + $max_feed_id = db_fetch_result($result, 0, "mid"); + $num_feeds = db_fetch_result($result, 0, "nf"); + + $data["max_feed_id"] = (int) $max_feed_id; + $data["num_feeds"] = (int) $num_feeds; + + $data['last_article_id'] = getLastArticleId(); + $data['cdm_expanded'] = get_pref('CDM_EXPANDED'); + + $data['dep_ts'] = calculate_dep_timestamp(); + $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE'); + + if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) { + + $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock"); + + if (time() - $_SESSION["daemon_stamp_check"] > 30) { + + $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp"); + + if ($stamp) { + $stamp_delta = time() - $stamp; + + if ($stamp_delta > 1800) { + $stamp_check = 0; + } else { + $stamp_check = 1; + $_SESSION["daemon_stamp_check"] = time(); + } + + $data['daemon_stamp_ok'] = $stamp_check; + + $stamp_fmt = date("Y.m.d, G:i", $stamp); + + $data['daemon_stamp'] = $stamp_fmt; + } + } + } + + if ($_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) { + $new_version_details = @check_for_update(); + + $data['new_version_available'] = (int) ($new_version_details != false); + + $_SESSION["last_version_check"] = time(); + $_SESSION["version_data"] = $new_version_details; + } + + return $data; + } + + function search_to_sql($search) { + + $search_query_part = ""; + + $keywords = str_getcsv($search, " "); + $query_keywords = array(); + $search_words = array(); + + 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 '%". + db_escape_string(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('%$k%'))"); + array_push($search_words, $k); + } + break; + case "author": + if ($commandpair[1]) { + array_push($query_keywords, "($not (LOWER(author) LIKE '%". + db_escape_string(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('%$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 '%". + db_escape_string(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('%$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('%$k%') + OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$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('%$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 { + array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%') + OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))"); + + if (!$not) array_push($search_words, $k); + } + } + } + + $search_query_part = implode("AND", $query_keywords); + + return array($search_query_part, $search_words); + } + + function getParentCategories($cat, $owner_uid) { + $rv = array(); + + $result = db_query("SELECT parent_cat FROM ttrss_feed_categories + WHERE id = '$cat' AND parent_cat IS NOT NULL AND owner_uid = $owner_uid"); + + while ($line = db_fetch_assoc($result)) { + array_push($rv, $line["parent_cat"]); + $rv = array_merge($rv, getParentCategories($line["parent_cat"], $owner_uid)); + } + + return $rv; + } + + function getChildCategories($cat, $owner_uid) { + $rv = array(); + + $result = db_query("SELECT id FROM ttrss_feed_categories + WHERE parent_cat = '$cat' AND owner_uid = $owner_uid"); + + while ($line = db_fetch_assoc($result)) { + array_push($rv, $line["id"]); + $rv = array_merge($rv, getChildCategories($line["id"], $owner_uid)); + } + + return $rv; + } + + 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) { + + if (!$owner_uid) $owner_uid = $_SESSION["uid"]; + + $ext_tables_part = ""; + $search_words = array(); + + if ($search) { + foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SEARCH) as $plugin) { + list($search_query_part, $search_words) = $plugin->hook_search($search); + break; + } + + // fall back in case of no plugins + if (!$search_query_part) { + list($search_query_part, $search_words) = search_to_sql($search); + } + $search_query_part .= " AND "; + } else { + $search_query_part = ""; + } + + if ($filter) { + + if (DB_TYPE == "pgsql") { + $query_strategy_part .= " AND updated > NOW() - INTERVAL '14 days' "; + } else { + $query_strategy_part .= " AND updated > DATE_SUB(NOW(), INTERVAL 14 DAY) "; + } + + $override_order = "updated DESC"; + + $filter_query_part = filter_to_sql($filter, $owner_uid); + + // Try to check if SQL regexp implementation chokes on a valid regexp + + + $result = db_query("SELECT true AS true_val + FROM ttrss_entries + JOIN ttrss_user_entries ON ttrss_entries.id = ttrss_user_entries.ref_id + JOIN ttrss_feeds ON ttrss_feeds.id = ttrss_user_entries.feed_id + WHERE $filter_query_part LIMIT 1", false); + + if ($result) { + $test = db_fetch_result($result, 0, "true_val"); + + if (!$test) { + $filter_query_part = "false AND"; + } else { + $filter_query_part .= " AND"; + } + } else { + $filter_query_part = "false AND"; + } + + } else { + $filter_query_part = ""; + } + + if ($since_id) { + $since_id_part = "ttrss_entries.id > $since_id AND "; + } else { + $since_id_part = ""; + } + + $view_query_part = ""; + + if ($view_mode == "adaptive") { + if ($search) { + $view_query_part = " "; + } else if ($feed != -1) { + + $unread = getFeedUnread($feed, $cat_view); + + if ($cat_view && $feed > 0 && $include_children) + $unread += getCategoryChildrenUnread($feed); + + if ($unread > 0) + $view_query_part = " unread = true AND "; + + } + } + + if ($view_mode == "marked") { + $view_query_part = " marked = true AND "; + } + + if ($view_mode == "has_note") { + $view_query_part = " (note IS NOT NULL AND note != '') AND "; + } + + if ($view_mode == "published") { + $view_query_part = " published = true AND "; + } + + if ($view_mode == "unread" && $feed != -6) { + $view_query_part = " unread = true AND "; + } + + if ($limit > 0) { + $limit_query_part = "LIMIT " . $limit; + } + + $allow_archived = false; + + $vfeed_query_part = ""; + + // override query strategy and enable feed display when searching globally + if ($search && $search_mode == "all_feeds") { + $query_strategy_part = "true"; + $vfeed_query_part = "ttrss_feeds.title AS feed_title,"; + /* tags */ + } else if (!is_numeric($feed)) { + $query_strategy_part = "true"; + $vfeed_query_part = "(SELECT title FROM ttrss_feeds WHERE + id = feed_id) as feed_title,"; + } else if ($search && $search_mode == "this_cat") { + $vfeed_query_part = "ttrss_feeds.title AS feed_title,"; + + if ($feed > 0) { + if ($include_children) { + $subcats = getChildCategories($feed, $owner_uid); + array_push($subcats, $feed); + $cats_qpart = join(",", $subcats); + } else { + $cats_qpart = $feed; + } + + $query_strategy_part = "ttrss_feeds.cat_id IN ($cats_qpart)"; + + } else { + $query_strategy_part = "ttrss_feeds.cat_id IS NULL"; + } + + } else if ($feed > 0) { + + if ($cat_view) { + + if ($feed > 0) { + if ($include_children) { + # sub-cats + $subcats = getChildCategories($feed, $owner_uid); + + array_push($subcats, $feed); + $query_strategy_part = "cat_id IN (". + implode(",", $subcats).")"; + + } else { + $query_strategy_part = "cat_id = '$feed'"; + } + + } else { + $query_strategy_part = "cat_id IS NULL"; + } + + $vfeed_query_part = "ttrss_feeds.title AS feed_title,"; + + } else { + $query_strategy_part = "feed_id = '$feed'"; + } + } else if ($feed == 0 && !$cat_view) { // archive virtual feed + $query_strategy_part = "feed_id IS NULL"; + $allow_archived = true; + } else if ($feed == 0 && $cat_view) { // uncategorized + $query_strategy_part = "cat_id IS NULL AND feed_id IS NOT NULL"; + $vfeed_query_part = "ttrss_feeds.title AS feed_title,"; + } else if ($feed == -1) { // starred virtual feed + $query_strategy_part = "marked = true"; + $vfeed_query_part = "ttrss_feeds.title AS feed_title,"; + $allow_archived = true; + + if (!$override_order) { + $override_order = "last_marked DESC, date_entered DESC, updated DESC"; + } + + } else if ($feed == -2) { // published virtual feed OR labels category + + if (!$cat_view) { + $query_strategy_part = "published = true"; + $vfeed_query_part = "ttrss_feeds.title AS feed_title,"; + $allow_archived = true; + + if (!$override_order) { + $override_order = "last_published DESC, date_entered DESC, updated DESC"; + } + + } else { + $vfeed_query_part = "ttrss_feeds.title AS feed_title,"; + + $ext_tables_part = ",ttrss_labels2,ttrss_user_labels2"; + + $query_strategy_part = "ttrss_labels2.id = ttrss_user_labels2.label_id AND + ttrss_user_labels2.article_id = ref_id"; + + } + } else if ($feed == -6) { // recently read + $query_strategy_part = "unread = false AND last_read IS NOT NULL"; + $vfeed_query_part = "ttrss_feeds.title AS feed_title,"; + $allow_archived = true; + $ignore_vfeed_group = true; + + if (!$override_order) $override_order = "last_read DESC"; + +/* } else if ($feed == -7) { // shared + $query_strategy_part = "uuid != ''"; + $vfeed_query_part = "ttrss_feeds.title AS feed_title,"; + $allow_archived = true; */ + } else if ($feed == -3) { // fresh virtual feed + $query_strategy_part = "unread = true AND score >= 0"; + + $intl = get_pref("FRESH_ARTICLE_MAX_AGE", $owner_uid); + + if (DB_TYPE == "pgsql") { + $query_strategy_part .= " AND date_entered > NOW() - INTERVAL '$intl hour' "; + } else { + $query_strategy_part .= " AND date_entered > DATE_SUB(NOW(), INTERVAL $intl HOUR) "; + } + + $vfeed_query_part = "ttrss_feeds.title AS feed_title,"; + } else if ($feed == -4) { // all articles virtual feed + $allow_archived = true; + $query_strategy_part = "true"; + $vfeed_query_part = "ttrss_feeds.title AS feed_title,"; + } else if ($feed <= LABEL_BASE_INDEX) { // labels + $label_id = feed_to_label_id($feed); + + $query_strategy_part = "label_id = '$label_id' AND + ttrss_labels2.id = ttrss_user_labels2.label_id AND + ttrss_user_labels2.article_id = ref_id"; + + $vfeed_query_part = "ttrss_feeds.title AS feed_title,"; + $ext_tables_part = ",ttrss_labels2,ttrss_user_labels2"; + $allow_archived = true; + + } else { + $query_strategy_part = "true"; + } + + $order_by = "score DESC, date_entered DESC, updated DESC"; + + if ($view_mode == "unread_first") { + $order_by = "unread DESC, $order_by"; + } + + if ($override_order) { + $order_by = $override_order; + } + + if ($override_strategy) { + $query_strategy_part = $override_strategy; + } + + if ($override_vfeed) { + $vfeed_query_part = $override_vfeed; + } + + $feed_title = ""; + + if ($search) { + $feed_title = T_sprintf("Search results: %s", $search); + } else { + if ($cat_view) { + $feed_title = getCategoryTitle($feed); + } else { + if (is_numeric($feed) && $feed > 0) { + $result = db_query("SELECT title,site_url,last_error,last_updated + FROM ttrss_feeds WHERE id = '$feed' AND owner_uid = $owner_uid"); + + $feed_title = db_fetch_result($result, 0, "title"); + $feed_site_url = db_fetch_result($result, 0, "site_url"); + $last_error = db_fetch_result($result, 0, "last_error"); + $last_updated = db_fetch_result($result, 0, "last_updated"); + } else { + $feed_title = getFeedTitle($feed); + } + } + } + + + $content_query_part = "content, "; + + + if (is_numeric($feed)) { + + if ($feed >= 0) { + $feed_kind = "Feeds"; + } else { + $feed_kind = "Labels"; + } + + if ($limit_query_part) { + $offset_query_part = "OFFSET $offset"; + } + + // proper override_order applied above + if ($vfeed_query_part && !$ignore_vfeed_group && get_pref('VFEED_GROUP_BY_FEED', $owner_uid)) { + if (!$override_order) { + $order_by = "ttrss_feeds.title, $order_by"; + } else { + $order_by = "ttrss_feeds.title, $override_order"; + } + } + + if (!$allow_archived) { + $from_qpart = "ttrss_entries,ttrss_user_entries,ttrss_feeds$ext_tables_part"; + $feed_check_qpart = "ttrss_user_entries.feed_id = ttrss_feeds.id AND"; + + } else { + $from_qpart = "ttrss_entries$ext_tables_part,ttrss_user_entries + LEFT JOIN ttrss_feeds ON (feed_id = ttrss_feeds.id)"; + } + + 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 = ""; + } + + $query = "SELECT DISTINCT + date_entered, + guid, + ttrss_entries.id,ttrss_entries.title, + updated, + label_cache, + tag_cache, + always_display_enclosures, + site_url, + note, + num_comments, + comments, + int_id, + uuid, + lang, + hide_images, + unread,feed_id,marked,published,link,last_read,orig_feed_id, + last_marked, last_published, + $vfeed_query_part + $content_query_part + author,score + FROM + $from_qpart + WHERE + $feed_check_qpart + ttrss_user_entries.ref_id = ttrss_entries.id AND + ttrss_user_entries.owner_uid = '$owner_uid' AND + $search_query_part + $start_ts_query_part + $filter_query_part + $view_query_part + $since_id_part + $query_strategy_part ORDER BY $order_by + $limit_query_part $offset_query_part"; + + if ($_REQUEST["debug"]) print $query; + + $result = db_query($query); + + } else { + // browsing by tag + + $select_qpart = "SELECT DISTINCT " . + "date_entered," . + "guid," . + "note," . + "ttrss_entries.id as id," . + "title," . + "updated," . + "unread," . + "feed_id," . + "orig_feed_id," . + "marked," . + "num_comments, " . + "comments, " . + "tag_cache," . + "label_cache," . + "link," . + "lang," . + "uuid," . + "last_read," . + "(SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) AS hide_images," . + "last_marked, last_published, " . + $since_id_part . + $vfeed_query_part . + $content_query_part . + "score "; + + $feed_kind = "Tags"; + $all_tags = explode(",", $feed); + if ($search_mode == 'any') { + $tag_sql = "tag_name in (" . implode(", ", array_map("db_quote", $all_tags)) . ")"; + $from_qpart = " FROM ttrss_entries,ttrss_user_entries,ttrss_tags "; + $where_qpart = " WHERE " . + "ref_id = ttrss_entries.id AND " . + "ttrss_user_entries.owner_uid = $owner_uid AND " . + "post_int_id = int_id AND $tag_sql AND " . + $view_query_part . + $search_query_part . + $query_strategy_part . " ORDER BY $order_by " . + $limit_query_part; + + } else { + $i = 1; + $sub_selects = array(); + $sub_ands = array(); + foreach ($all_tags as $term) { + array_push($sub_selects, "(SELECT post_int_id from ttrss_tags WHERE tag_name = " . db_quote($term) . " AND owner_uid = $owner_uid) as A$i"); + $i++; + } + if ($i > 2) { + $x = 1; + $y = 2; + do { + array_push($sub_ands, "A$x.post_int_id = A$y.post_int_id"); + $x++; + $y++; + } while ($y < $i); + } + array_push($sub_ands, "A1.post_int_id = ttrss_user_entries.int_id and ttrss_user_entries.owner_uid = $owner_uid"); + array_push($sub_ands, "ttrss_user_entries.ref_id = ttrss_entries.id"); + $from_qpart = " FROM " . implode(", ", $sub_selects) . ", ttrss_user_entries, ttrss_entries"; + $where_qpart = " WHERE " . implode(" AND ", $sub_ands); + } + // error_log("TAG SQL: " . $tag_sql); + // $tag_sql = "tag_name = '$feed'"; DEFAULT way + + // error_log("[". $select_qpart . "][" . $from_qpart . "][" .$where_qpart . "]"); + $result = db_query($select_qpart . $from_qpart . $where_qpart); + } + + return array($result, $feed_title, $feed_site_url, $last_error, $last_updated, $search_words); + + } + + function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) { + if (!$owner) $owner = $_SESSION["uid"]; + + $res = trim($str); if (!$res) return ''; + + $charset_hack = ' + + '; + + $res = trim($res); if (!$res) return ''; + + libxml_use_internal_errors(true); + + $doc = new DOMDocument(); + $doc->loadHTML($charset_hack . $res); + $xpath = new DOMXPath($doc); + + $entries = $xpath->query('(//a[@href]|//img[@src])'); + + foreach ($entries as $entry) { + + if ($site_url) { + + if ($entry->hasAttribute('href')) { + $entry->setAttribute('href', + rewrite_relative_url($site_url, $entry->getAttribute('href'))); + + $entry->setAttribute('rel', 'noreferrer'); + } + + if ($entry->hasAttribute('src')) { + $src = rewrite_relative_url($site_url, $entry->getAttribute('src')); + + $cached_filename = CACHE_DIR . '/images/' . sha1($src) . '.png'; + + if (file_exists($cached_filename)) { + $src = SELF_URL_PATH . '/image.php?hash=' . sha1($src); + } + + $entry->setAttribute('src', $src); + } + + if ($entry->nodeName == 'img') { + if (($owner && get_pref("STRIP_IMAGES", $owner)) || + $force_remove_images || $_SESSION["bw_limit"]) { + + $p = $doc->createElement('p'); + + $a = $doc->createElement('a'); + $a->setAttribute('href', $entry->getAttribute('src')); + + $a->appendChild(new DOMText($entry->getAttribute('src'))); + $a->setAttribute('target', '_blank'); + + $p->appendChild($a); + + $entry->parentNode->replaceChild($p, $entry); + } + } + } + + if (strtolower($entry->nodeName) == "a") { + $entry->setAttribute("target", "_blank"); + } + } + + $entries = $xpath->query('//iframe'); + foreach ($entries as $entry) { + $entry->setAttribute('sandbox', 'allow-scripts'); + + } + + $allowed_elements = array('a', 'address', 'audio', 'article', 'aside', + 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br', + 'caption', 'cite', 'center', 'code', 'col', 'colgroup', + 'data', 'dd', 'del', 'details', 'div', 'dl', 'font', + 'dt', 'em', 'footer', 'figure', 'figcaption', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'html', 'i', + 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript', + 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section', + 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary', + 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', + 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video' ); + + if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe'; + + $disallowed_attributes = array('id', 'style', 'class'); + + foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) { + $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id); + if (is_array($retval)) { + $doc = $retval[0]; + $allowed_elements = $retval[1]; + $disallowed_attributes = $retval[2]; + } else { + $doc = $retval; + } + } + + $doc->removeChild($doc->firstChild); //remove doctype + $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes); + + if ($highlight_words) { + foreach ($highlight_words as $word) { + + // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph + + $elements = $xpath->query("//*/text()"); + + foreach ($elements as $child) { + + $fragment = $doc->createDocumentFragment(); + $text = $child->textContent; + + while (($pos = mb_stripos($text, $word)) !== false) { + $fragment->appendChild(new DomText(mb_substr($text, 0, $pos))); + $word = mb_substr($text, $pos, mb_strlen($word)); + $highlight = $doc->createElement('span'); + $highlight->appendChild(new DomText($word)); + $highlight->setAttribute('class', 'highlight'); + $fragment->appendChild($highlight); + $text = mb_substr($text, $pos + mb_strlen($word)); + } + + if (!empty($text)) $fragment->appendChild(new DomText($text)); + + $child->parentNode->replaceChild($fragment, $child); + } + } + } + + $res = $doc->saveHTML(); + + return $res; + } + + function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) { + $xpath = new DOMXPath($doc); + $entries = $xpath->query('//*'); + + foreach ($entries as $entry) { + if (!in_array($entry->nodeName, $allowed_elements)) { + $entry->parentNode->removeChild($entry); + } + + if ($entry->hasAttributes()) { + $attrs_to_remove = array(); + + foreach ($entry->attributes as $attr) { + + if (strpos($attr->nodeName, 'on') === 0) { + array_push($attrs_to_remove, $attr); + } + + if (in_array($attr->nodeName, $disallowed_attributes)) { + array_push($attrs_to_remove, $attr); + } + } + + foreach ($attrs_to_remove as $attr) { + $entry->removeAttributeNode($attr); + } + } + } + + return $doc; + } + + function check_for_update() { + if (CHECK_FOR_NEW_VERSION && $_SESSION['access_level'] >= 10) { + $version_url = "http://tt-rss.org/version.php?ver=" . VERSION . + "&iid=" . sha1(SELF_URL_PATH); + + $version_data = @fetch_file_contents($version_url); + + if ($version_data) { + $version_data = json_decode($version_data, true); + if ($version_data && $version_data['version']) { + if (version_compare(VERSION_STATIC, $version_data['version']) == -1) { + return $version_data; + } + } + } + } + return false; + } + + function catchupArticlesById($ids, $cmode, $owner_uid = false) { + + if (!$owner_uid) $owner_uid = $_SESSION["uid"]; + if (count($ids) == 0) return; + + $tmp_ids = array(); + + foreach ($ids as $id) { + array_push($tmp_ids, "ref_id = '$id'"); + } + + $ids_qpart = join(" OR ", $tmp_ids); + + if ($cmode == 0) { + db_query("UPDATE ttrss_user_entries SET + unread = false,last_read = NOW() + WHERE ($ids_qpart) AND owner_uid = $owner_uid"); + } else if ($cmode == 1) { + db_query("UPDATE ttrss_user_entries SET + unread = true + WHERE ($ids_qpart) AND owner_uid = $owner_uid"); + } else { + db_query("UPDATE ttrss_user_entries SET + unread = NOT unread,last_read = NOW() + WHERE ($ids_qpart) AND owner_uid = $owner_uid"); + } + + /* update ccache */ + + $result = db_query("SELECT DISTINCT feed_id FROM ttrss_user_entries + WHERE ($ids_qpart) AND owner_uid = $owner_uid"); + + while ($line = db_fetch_assoc($result)) { + ccache_update($line["feed_id"], $owner_uid); + } + } + + function get_article_tags($id, $owner_uid = 0, $tag_cache = false) { + + $a_id = db_escape_string($id); + + if (!$owner_uid) $owner_uid = $_SESSION["uid"]; + + $query = "SELECT DISTINCT tag_name, + owner_uid as owner FROM + ttrss_tags WHERE post_int_id = (SELECT int_id FROM ttrss_user_entries WHERE + ref_id = '$a_id' AND owner_uid = '$owner_uid' LIMIT 1) ORDER BY tag_name"; + + $tags = array(); + + /* check cache first */ + + if ($tag_cache === false) { + $result = db_query("SELECT tag_cache FROM ttrss_user_entries + WHERE ref_id = '$id' AND owner_uid = $owner_uid"); + + $tag_cache = db_fetch_result($result, 0, "tag_cache"); + } + + if ($tag_cache) { + $tags = explode(",", $tag_cache); + } else { + + /* do it the hard way */ + + $tmp_result = db_query($query); + + while ($tmp_line = db_fetch_assoc($tmp_result)) { + array_push($tags, $tmp_line["tag_name"]); + } + + /* update the cache */ + + $tags_str = db_escape_string(join(",", $tags)); + + db_query("UPDATE ttrss_user_entries + SET tag_cache = '$tags_str' WHERE ref_id = '$id' + AND owner_uid = $owner_uid"); + } + + return $tags; + } + + function trim_array($array) { + $tmp = $array; + array_walk($tmp, 'trim'); + return $tmp; + } + + function tag_is_valid($tag) { + if ($tag == '') return false; + if (preg_match("/^[0-9]*$/", $tag)) return false; + if (mb_strlen($tag) > 250) return false; + + if (!$tag) return false; + + return true; + } + + function render_login_form() { + header('Cache-Control: public'); + + require_once "login_form.php"; + exit; + } + + function format_warning($msg, $id = "") { + return "
+ $msg
"; + } + + function format_notice($msg, $id = "") { + return "
+ $msg
"; + } + + function format_error($msg, $id = "") { + return "
+ $msg
"; + } + + function print_notice($msg) { + return print format_notice($msg); + } + + function print_warning($msg) { + return print format_warning($msg); + } + + function print_error($msg) { + return print format_error($msg); + } + + + function T_sprintf() { + $args = func_get_args(); + return vsprintf(__(array_shift($args)), $args); + } + + function format_inline_player($url, $ctype) { + + $entry = ""; + + $url = htmlspecialchars($url); + + if (strpos($ctype, "audio/") === 0) { + + if ($_SESSION["hasAudio"] && (strpos($ctype, "ogg") !== false || + $_SESSION["hasMp3"])) { + + $entry .= ""; + + } else { + + $entry .= " + + "; + } + + if ($entry) $entry .= "  " . basename($url) . ""; + + return $entry; + + } + + return ""; + +/* $filename = substr($url, strrpos($url, "/")+1); + + $entry .= " " . + $filename . " (" . $ctype . ")" . ""; */ + + } + + function format_article($id, $mark_as_read = true, $zoom_mode = false, $owner_uid = false) { + if (!$owner_uid) $owner_uid = $_SESSION["uid"]; + + $rv = array(); + + $rv['id'] = $id; + + /* we can figure out feed_id from article id anyway, why do we + * pass feed_id here? let's ignore the argument :(*/ + + $result = db_query("SELECT feed_id FROM ttrss_user_entries + WHERE ref_id = '$id'"); + + $feed_id = (int) db_fetch_result($result, 0, "feed_id"); + + $rv['feed_id'] = $feed_id; + + //if (!$zoom_mode) { print "
get_hooks(PluginHost::HOOK_RENDER_ARTICLE) as $p) { + $line = $p->hook_render_article($line); + } + + $num_comments = $line["num_comments"]; + $entry_comments = ""; + + if ($num_comments > 0) { + if ($line["comments"]) { + $comments_url = htmlspecialchars($line["comments"]); + } else { + $comments_url = htmlspecialchars($line["link"]); + } + $entry_comments = "$num_comments ". + _ngettext("comment", "comments", $num_comments).""; + + } else { + if ($line["comments"] && $line["link"] != $line["comments"]) { + $entry_comments = "".__("comments").""; + } + } + + if ($zoom_mode) { + header("Content-Type: text/html"); + $rv['content'] .= " + + Tiny Tiny RSS - ".$line["title"]."". + stylesheet_tag("css/tt-rss.css"). + stylesheet_tag("css/zoom.css"). + stylesheet_tag("css/dijit.css")." + + + + + + "; + } + + $rv['content'] .= "
"; + + $rv['content'] .= "
"; + + $entry_author = $line["author"]; + + if ($entry_author) { + $entry_author = __(" - ") . $entry_author; + } + + $parsed_updated = make_local_datetime($line["updated"], true, + $owner_uid, true); + + if (!$zoom_mode) + $rv['content'] .= "
$parsed_updated
"; + + if ($line["link"]) { + $rv['content'] .= "
" . + $line["title"] . "" . + "$entry_author
"; + } else { + $rv['content'] .= "
" . $line["title"] . "$entry_author
"; + } + + if ($zoom_mode) { + $feed_title = "". + htmlspecialchars($line["feed_title"]).""; + + $rv['content'] .= "
$feed_title
"; + + $rv['content'] .= "
$parsed_updated
"; + } + + $tags_str = format_tags_string($line["tags"], $id); + $tags_str_full = join(", ", $line["tags"]); + + if (!$tags_str_full) $tags_str_full = __("no tags"); + + if (!$entry_comments) $entry_comments = " "; # placeholder + + $rv['content'] .= ""; + $rv['content'] .= "
"; + + foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ARTICLE_LEFT_BUTTON) as $p) { + $rv['content'] .= $p->hook_article_left_button($line); + } + + $rv['content'] .= "$entry_comments
"; + + if ($line["orig_feed_id"]) { + + $tmp_result = db_query("SELECT * FROM ttrss_archived_feeds + WHERE id = ".$line["orig_feed_id"]); + + if (db_num_rows($tmp_result) != 0) { + + $rv['content'] .= "
"; + $rv['content'] .= __("Originally from:"); + + $rv['content'] .= " "; + + $tmp_line = db_fetch_assoc($tmp_result); + + $rv['content'] .= "" . + $tmp_line['title'] . ""; + + $rv['content'] .= " "; + + $rv['content'] .= ""; + $rv['content'] .= ""; + + $rv['content'] .= "
"; + } + } + + $rv['content'] .= "
"; + + $rv['content'] .= "
"; + if ($line['note']) { + $rv['content'] .= format_article_note($id, $line['note'], !$zoom_mode); + } + $rv['content'] .= "
"; + + if (!$line['lang']) $line['lang'] = 'en'; + + $rv['content'] .= "
"; + + $rv['content'] .= $line["content"]; + $rv['content'] .= format_article_enclosures($id, + sql_bool_to_bool($line["always_display_enclosures"]), + $line["content"], + sql_bool_to_bool($line["hide_images"])); + + $rv['content'] .= "
"; + + $rv['content'] .= "
"; + + } + + if ($zoom_mode) { + $rv['content'] .= " + "; + $rv['content'] .= ""; + } + + return $rv; + + } + + function print_checkpoint($n, $s) { + $ts = microtime(true); + echo sprintf("\n", $ts - $s); + return $ts; + } + + function sanitize_tag($tag) { + $tag = trim($tag); + + $tag = mb_strtolower($tag, 'utf-8'); + + $tag = preg_replace('/[\'\"\+\>\<]/', "", $tag); + +// $tag = str_replace('"', "", $tag); +// $tag = str_replace("+", " ", $tag); + $tag = str_replace("technorati tag: ", "", $tag); + + return $tag; + } + + function get_self_url_prefix() { + if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) { + return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1); + } else { + return SELF_URL_PATH; + } + } + + /** + * Compute the Mozilla Firefox feed adding URL from server HOST and REQUEST_URI. + * + * @return string The Mozilla Firefox feed adding URL. + */ + function add_feed_url() { + //$url_path = ($_SERVER['HTTPS'] != "on" ? 'http://' : 'https://') . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH); + + $url_path = get_self_url_prefix() . + "/public.php?op=subscribe&feed_url=%s"; + return $url_path; + } // function add_feed_url + + function encrypt_password($pass, $salt = '', $mode2 = false) { + if ($salt && $mode2) { + return "MODE2:" . hash('sha256', $salt . $pass); + } else if ($salt) { + return "SHA1X:" . sha1("$salt:$pass"); + } else { + return "SHA1:" . sha1($pass); + } + } // function encrypt_password + + function load_filters($feed_id, $owner_uid, $action_id = false) { + $filters = array(); + + $cat_id = (int)getFeedCategory($feed_id); + + if ($cat_id == 0) + $null_cat_qpart = "cat_id IS NULL OR"; + else + $null_cat_qpart = ""; + + $result = db_query("SELECT * FROM ttrss_filters2 WHERE + owner_uid = $owner_uid AND enabled = true ORDER BY order_id, title"); + + $check_cats = join(",", array_merge( + getParentCategories($cat_id, $owner_uid), + array($cat_id))); + + while ($line = db_fetch_assoc($result)) { + $filter_id = $line["id"]; + + $result2 = db_query("SELECT + r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, t.name AS type_name + FROM ttrss_filters2_rules AS r, + ttrss_filter_types AS t + WHERE + ($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats)) AND + (feed_id IS NULL OR feed_id = '$feed_id') AND + filter_type = t.id AND filter_id = '$filter_id'"); + + $rules = array(); + $actions = array(); + + while ($rule_line = db_fetch_assoc($result2)) { +# print_r($rule_line); + + $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); + } + + $result2 = db_query("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 = '$filter_id'"); + + while ($action_line = db_fetch_assoc($result2)) { +# print_r($action_line); + + $action = array(); + $action["type"] = $action_line["type_name"]; + $action["param"] = $action_line["action_param"]; + + array_push($actions, $action); + } + + + $filter = array(); + $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; + } + + function get_score_pic($score) { + if ($score > 100) { + return "score_high.png"; + } else if ($score > 0) { + return "score_half_high.png"; + } else if ($score < -100) { + return "score_low.png"; + } else if ($score < 0) { + return "score_half_low.png"; + } else { + return "score_neutral.png"; + } + } + + function feed_has_icon($id) { + return is_file(ICONS_DIR . "/$id.ico") && filesize(ICONS_DIR . "/$id.ico") > 0; + } + + function init_plugins() { + PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL); + + return true; + } + + function format_tags_string($tags, $id) { + if (!is_array($tags) || count($tags) == 0) { + return __("no tags"); + } else { + $maxtags = min(5, count($tags)); + + for ($i = 0; $i < $maxtags; $i++) { + $tags_str .= "" . $tags[$i] . ", "; + } + + $tags_str = mb_substr($tags_str, 0, mb_strlen($tags_str)-2); + + if (count($tags) > $maxtags) + $tags_str .= ", …"; + + return $tags_str; + } + } + + function format_article_labels($labels, $id) { + + if (!is_array($labels)) return ''; + + $labels_str = ""; + + foreach ($labels as $l) { + $labels_str .= sprintf("%s", + $l[2], $l[3], $l[1]); + } + + return $labels_str; + + } + + function format_article_note($id, $note, $allow_edit = true) { + + $str = "
+
". + ($allow_edit ? __('(edit note)') : "")."
$note
"; + + return $str; + } + + + function get_feed_category($feed_cat, $parent_cat_id = false) { + if ($parent_cat_id) { + $parent_qpart = "parent_cat = '$parent_cat_id'"; + $parent_insert = "'$parent_cat_id'"; + } else { + $parent_qpart = "parent_cat IS NULL"; + $parent_insert = "NULL"; + } + + $result = db_query( + "SELECT id FROM ttrss_feed_categories + WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]); + + if (db_num_rows($result) == 0) { + return false; + } else { + return db_fetch_result($result, 0, "id"); + } + } + + function add_feed_category($feed_cat, $parent_cat_id = false) { + + if (!$feed_cat) return false; + + db_query("BEGIN"); + + if ($parent_cat_id) { + $parent_qpart = "parent_cat = '$parent_cat_id'"; + $parent_insert = "'$parent_cat_id'"; + } else { + $parent_qpart = "parent_cat IS NULL"; + $parent_insert = "NULL"; + } + + $feed_cat = mb_substr($feed_cat, 0, 250); + + $result = db_query( + "SELECT id FROM ttrss_feed_categories + WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]); + + if (db_num_rows($result) == 0) { + + $result = db_query( + "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat) + VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)"); + + db_query("COMMIT"); + + return true; + } + + return false; + } + + function getArticleFeed($id) { + $result = db_query("SELECT feed_id FROM ttrss_user_entries + WHERE ref_id = '$id' AND owner_uid = " . $_SESSION["uid"]); + + if (db_num_rows($result) != 0) { + return db_fetch_result($result, 0, "feed_id"); + } else { + return 0; + } + } + + /** + * 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. + */ + function fix_url($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 .= '/'; + } + + if ($url != "http:///") + return $url; + else + return ''; + } + + function validate_feed_url($url) { + $parts = parse_url($url); + + return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https'); + + } + + function get_article_enclosures($id) { + + $query = "SELECT * FROM ttrss_enclosures + WHERE post_id = '$id' AND content_url != ''"; + + $rv = array(); + + $result = db_query($query); + + if (db_num_rows($result) > 0) { + while ($line = db_fetch_assoc($result)) { + array_push($rv, $line); + } + } + + return $rv; + } + + /* function save_email_address($email) { + // FIXME: implement persistent storage of emails + + if (!$_SESSION['stored_emails']) + $_SESSION['stored_emails'] = array(); + + if (!in_array($email, $_SESSION['stored_emails'])) + array_push($_SESSION['stored_emails'], $email); + } */ + + + function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) { + + if (!$owner_uid) $owner_uid = $_SESSION["uid"]; + + $sql_is_cat = bool_to_sql_bool($is_cat); + + $result = db_query("SELECT access_key FROM ttrss_access_keys + WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat + AND owner_uid = " . $owner_uid); + + if (db_num_rows($result) == 1) { + return db_fetch_result($result, 0, "access_key"); + } else { + $key = db_escape_string(uniqid(base_convert(rand(), 10, 36))); + + $result = db_query("INSERT INTO ttrss_access_keys + (access_key, feed_id, is_cat, owner_uid) + VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')"); + + return $key; + } + return false; + } + + function get_feeds_from_html($url, $content) + { + $url = fix_url($url); + $baseUrl = substr($url, 0, strrpos($url, '/') + 1); + + libxml_use_internal_errors(true); + + $doc = new DOMDocument(); + $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"]'); + $feedUrls = array(); + 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; + } + + function is_html($content) { + return preg_match("/"; + + while ($line = db_fetch_assoc($result)) { + + $issel = ($line["caption"] == $value) ? "selected=\"1\"" : ""; + + print ""; + + } + +# print ""; + + print ""; + + + } + + function format_article_enclosures($id, $always_display_enclosures, + $article_content, $hide_images = false) { + + $result = get_article_enclosures($id); + $rv = ''; + + foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FORMAT_ENCLOSURES) as $plugin) { + $retval = $plugin->hook_format_enclosures($rv, $result, $id, $always_display_enclosures, $article_content, $hide_images); + if (is_array($retval)) { + $rv = $retval[0]; + $result = $retval[1]; + } else { + $rv = $retval; + } + } + + if ($rv === '' && !empty($result)) { + $entries_html = array(); + $entries = array(); + $entries_inline = array(); + + foreach ($result as $line) { + + $url = $line["content_url"]; + $ctype = $line["content_type"]; + $title = $line["title"]; + $width = $line["width"]; + $height = $line["height"]; + + if (!$ctype) $ctype = __("unknown type"); + + $filename = substr($url, strrpos($url, "/")+1); + + $player = format_inline_player($url, $ctype); + + if ($player) array_push($entries_inline, $player); + +# $entry .= " " . +# $filename . " (" . $ctype . ")" . ""; + + $entry = "
$filename ($ctype)
"; + + array_push($entries_html, $entry); + + $entry = array(); + + $entry["type"] = $ctype; + $entry["filename"] = $filename; + $entry["url"] = $url; + $entry["title"] = $title; + $entry["width"] = $width; + $entry["height"] = $height; + + array_push($entries, $entry); + } + + if ($_SESSION['uid'] && !get_pref("STRIP_IMAGES") && !$_SESSION["bw_limit"]) { + if ($always_display_enclosures || + !preg_match("/ 0) + $encsize .= ' height="' . intval($entry['width']) . '"'; + if ($entry['width'] > 0) + $encsize .= ' width="' . intval($entry['height']) . '"'; + $rv .= "

\"".htmlspecialchars($entry["filename"])."\"

"; + } else { + $rv .= "

" .htmlspecialchars($entry["url"]) . "

"; + } + + if ($entry['title']) { + $rv.= "
${entry['title']}
"; + } + } + } + } + } + + if (count($entries_inline) > 0) { + $rv .= "
"; + foreach ($entries_inline as $entry) { $rv .= $entry; }; + $rv .= "
"; + } + + $rv .= ""; + } + + return $rv; + } + + function getLastArticleId() { + $result = db_query("SELECT MAX(ref_id) AS id FROM ttrss_user_entries + WHERE owner_uid = " . $_SESSION["uid"]); + + if (db_num_rows($result) == 1) { + return db_fetch_result($result, 0, "id"); + } else { + return -1; + } + } + + function build_url($parts) { + return $parts['scheme'] . "://" . $parts['host'] . $parts['path']; + } + + /** + * Converts a (possibly) relative URL to a absolute one. + * + * @param string $url Base URL (i.e. from where the document is) + * @param string $rel_url Possibly relative URL in the document + * + * @return string Absolute URL + */ + function rewrite_relative_url($url, $rel_url) { + if (strpos($rel_url, ":") !== false) { + return $rel_url; + } else if (strpos($rel_url, "://") !== false) { + return $rel_url; + } else if (strpos($rel_url, "//") === 0) { + # protocol-relative URL (rare but they exist) + return $rel_url; + } else if (strpos($rel_url, "/") === 0) + { + $parts = parse_url($url); + $parts['path'] = $rel_url; + + return build_url($parts); + + } else { + $parts = parse_url($url); + if (!isset($parts['path'])) { + $parts['path'] = '/'; + } + $dir = $parts['path']; + if (substr($dir, -1) !== '/') { + $dir = dirname($parts['path']); + $dir !== '/' && $dir .= '/'; + } + $parts['path'] = $dir . $rel_url; + + return build_url($parts); + } + } + + function cleanup_tags($days = 14, $limit = 1000) { + + if (DB_TYPE == "pgsql") { + $interval_query = "date_updated < NOW() - INTERVAL '$days days'"; + } else if (DB_TYPE == "mysql") { + $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)"; + } + + $tags_deleted = 0; + + while ($limit > 0) { + $limit_part = 500; + + $query = "SELECT ttrss_tags.id AS id + FROM ttrss_tags, ttrss_user_entries, ttrss_entries + WHERE post_int_id = int_id AND $interval_query AND + ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part"; + + $result = db_query($query); + + $ids = array(); + + while ($line = db_fetch_assoc($result)) { + array_push($ids, $line['id']); + } + + if (count($ids) > 0) { + $ids = join(",", $ids); + + $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)"); + $tags_deleted += db_affected_rows($tmp_result); + } else { + break; + } + + $limit -= $limit_part; + } + + return $tags_deleted; + } + + function print_user_stylesheet() { + $value = get_pref('USER_STYLESHEET'); + + if ($value) { + print ""; + } + + } + + function filter_to_sql($filter, $owner_uid) { + $query = array(); + + if (DB_TYPE == "pgsql") + $reg_qpart = "~"; + else + $reg_qpart = "REGEXP"; + + foreach ($filter["rules"] AS $rule) { + $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]); + $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/', + $rule['reg_exp']) !== FALSE; + + if ($regexp_valid) { + + $rule['reg_exp'] = db_escape_string($rule['reg_exp']); + + switch ($rule["type"]) { + case "title": + $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('". + $rule['reg_exp'] . "')"; + break; + case "content": + $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('". + $rule['reg_exp'] . "')"; + break; + case "both": + $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('". + $rule['reg_exp'] . "') OR LOWER(" . + "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')"; + break; + case "tag": + $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('". + $rule['reg_exp'] . "')"; + break; + case "link": + $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('". + $rule['reg_exp'] . "')"; + break; + case "author": + $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('". + $rule['reg_exp'] . "')"; + break; + } + + if (isset($rule['inverse'])) $qpart = "NOT ($qpart)"; + + if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) { + $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]); + } + + if (isset($rule["cat_id"])) { + + if ($rule["cat_id"] > 0) { + $children = getChildCategories($rule["cat_id"], $owner_uid); + array_push($children, $rule["cat_id"]); + + $children = join(",", $children); + + $cat_qpart = "cat_id IN ($children)"; + } else { + $cat_qpart = "cat_id IS NULL"; + } + + $qpart .= " AND $cat_qpart"; + } + + $qpart .= " AND feed_id IS NOT NULL"; + + array_push($query, "($qpart)"); + + } + } + + if (count($query) > 0) { + $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")"; + } else { + $fullquery = "(false)"; + } + + if ($filter['inverse']) $fullquery = "(NOT $fullquery)"; + + return $fullquery; + } + + if (!function_exists('gzdecode')) { + function gzdecode($string) { // no support for 2nd argument + return file_get_contents('compress.zlib://data:who/cares;base64,'. + base64_encode($string)); + } + } + + function get_random_bytes($length) { + if (function_exists('openssl_random_pseudo_bytes')) { + return openssl_random_pseudo_bytes($length); + } else { + $output = ""; + + for ($i = 0; $i < $length; $i++) + $output .= chr(mt_rand(0, 255)); + + return $output; + } + } + + function read_stdin() { + $fp = fopen("php://stdin", "r"); + + if ($fp) { + $line = trim(fgets($fp)); + fclose($fp); + return $line; + } + + return null; + } + + function tmpdirname($path, $prefix) { + // Use PHP's tmpfile function to create a temporary + // directory name. Delete the file and keep the name. + $tempname = tempnam($path,$prefix); + if (!$tempname) + return false; + + if (!unlink($tempname)) + return false; + + return $tempname; + } + + function getFeedCategory($feed) { + $result = db_query("SELECT cat_id FROM ttrss_feeds + WHERE id = '$feed'"); + + if (db_num_rows($result) > 0) { + return db_fetch_result($result, 0, "cat_id"); + } else { + return false; + } + + } + + function implements_interface($class, $interface) { + return in_array($interface, class_implements($class)); + } + + function geturl($url, $depth = 0, $nobody = true){ + + if ($depth == 20) return $url; + + if (!function_exists('curl_init')) + return user_error('CURL Must be installed for geturl function to work. Ask your host to enable it or uncomment extension=php_curl.dll in php.ini', E_USER_ERROR); + + $curl = curl_init(); + $header[0] = "Accept: text/xml,application/xml,application/xhtml+xml,"; + $header[0] .= "text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"; + $header[] = "Cache-Control: max-age=0"; + $header[] = "Connection: keep-alive"; + $header[] = "Keep-Alive: 300"; + $header[] = "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7"; + $header[] = "Accept-Language: en-us,en;q=0.5"; + $header[] = "Pragma: "; + + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 5.1; rv:5.0) Gecko/20100101 Firefox/5.0 Firefox/5.0'); + curl_setopt($curl, CURLOPT_HTTPHEADER, $header); + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_NOBODY, $nobody); + curl_setopt($curl, CURLOPT_REFERER, $url); + curl_setopt($curl, CURLOPT_ENCODING, 'gzip,deflate'); + curl_setopt($curl, CURLOPT_AUTOREFERER, true); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + //curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); //CURLOPT_FOLLOWLOCATION Disabled... + curl_setopt($curl, CURLOPT_TIMEOUT, 60); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + + if (defined('_CURL_HTTP_PROXY')) { + curl_setopt($curl, CURLOPT_PROXY, _CURL_HTTP_PROXY); + } + + $html = curl_exec($curl); + + $status = curl_getinfo($curl); + + if($status['http_code']!=200){ + + // idiot site not allowing http head + if($status['http_code'] == 405) { + curl_close($curl); + return geturl($url, $depth +1, false); + } + + if($status['http_code'] == 301 || $status['http_code'] == 302) { + curl_close($curl); + list($header) = explode("\r\n\r\n", $html, 2); + $matches = array(); + preg_match("/(Location:|URI:)[^(\n)]*/", $header, $matches); + $url = trim(str_replace($matches[1],"",$matches[0])); + $url_parsed = parse_url($url); + return (isset($url_parsed))? geturl($url, $depth + 1):''; + } + + global $fetch_last_error; + + $fetch_last_error = curl_errno($curl) . " " . curl_error($curl); + curl_close($curl); + +# $oline=''; +# foreach($status as $key=>$eline){$oline.='['.$key.']'.$eline.' ';} +# $line =$oline." \r\n ".$url."\r\n-----------------\r\n"; +# $handle = @fopen('./curl.error.log', 'a'); +# fwrite($handle, $line); + return FALSE; + } + curl_close($curl); + return $url; + } + + function get_minified_js($files) { + require_once 'lib/jshrink/Minifier.php'; + + $rv = ''; + + foreach ($files as $js) { + if (!isset($_GET['debug'])) { + $cached_file = CACHE_DIR . "/js/".basename($js).".js"; + + if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) { + + list($header, $contents) = explode("\n", file_get_contents($cached_file), 2); + + if ($header && $contents) { + list($htag, $hversion) = explode(":", $header); + + if ($htag == "tt-rss" && $hversion == VERSION) { + $rv .= $contents; + continue; + } + } + } + + $minified = JShrink\Minifier::minify(file_get_contents("js/$js.js")); + file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified); + $rv .= $minified; + + } else { + $rv .= file_get_contents("js/$js.js"); // no cache in debug mode + } + } + + return $rv; + } + + function stylesheet_tag($filename) { + $timestamp = filemtime($filename); + + return "\n"; + } + + function javascript_tag($filename) { + $query = ""; + + if (!(strpos($filename, "?") === FALSE)) { + $query = substr($filename, strpos($filename, "?")+1); + $filename = substr($filename, 0, strpos($filename, "?")); + } + + $timestamp = filemtime($filename); + + if ($query) $timestamp .= "&$query"; + + return "\n"; + } + + function calculate_dep_timestamp() { + $files = array_merge(glob("js/*.js"), glob("css/*.css")); + + $max_ts = -1; + + foreach ($files as $file) { + if (filemtime($file) > $max_ts) $max_ts = filemtime($file); + } + + return $max_ts; + } + + function T_js_decl($s1, $s2) { + if ($s1 && $s2) { + $s1 = preg_replace("/\n/", "", $s1); + $s2 = preg_replace("/\n/", "", $s2); + + $s1 = preg_replace("/\"/", "\\\"", $s1); + $s2 = preg_replace("/\"/", "\\\"", $s2); + + return "T_messages[\"$s1\"] = \"$s2\";\n"; + } + } + + function init_js_translations() { + + print 'var T_messages = new Object(); + + function __(msg) { + if (T_messages[msg]) { + return T_messages[msg]; + } else { + return msg; + } + } + + function ngettext(msg1, msg2, n) { + return __((parseInt(n) > 1) ? msg2 : msg1); + }'; + + $l10n = _get_reader(); + + for ($i = 0; $i < $l10n->total; $i++) { + $orig = $l10n->get_original_string($i); + if(strpos($orig, "\000") !== FALSE) { // Plural forms + $key = explode(chr(0), $orig); + print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular + print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural + } else { + $translation = __($orig); + print T_js_decl($orig, $translation); + } + } + } + + function label_to_feed_id($label) { + return LABEL_BASE_INDEX - 1 - abs($label); + } + + function feed_to_label_id($feed) { + return LABEL_BASE_INDEX - 1 + abs($feed); + } + +?> diff --git a/include/labels.php b/include/labels.php index c2a48a338..4149864b3 100644 --- a/include/labels.php +++ b/include/labels.php @@ -42,7 +42,8 @@ ORDER BY caption"); while ($line = db_fetch_assoc($result)) { - $rk = array($line["label_id"], $line["caption"], $line["fg_color"], + $rk = array(label_to_feed_id($line["label_id"]), + $line["caption"], $line["fg_color"], $line["bg_color"]); array_push($rv, $rk); } @@ -108,7 +109,7 @@ if (!$label_id) return; - $result = db_query( + db_query( "DELETE FROM ttrss_user_labels2 WHERE label_id = '$label_id' AND diff --git a/include/login_form.php b/include/login_form.php index 891e25e13..0000bc2f6 100644 --- a/include/login_form.php +++ b/include/login_form.php @@ -2,15 +2,22 @@ Tiny Tiny RSS : Login - - + + + - - - - - + + + + + @@ -86,19 +90,19 @@ "; + + print ""; + print ""; + print ""; + + $addresslist = $this->host->get($this, "addresslist"); + + print ""; + + print "

"; + + print "

"; + + print "
"; + } + function hook_article_button($line) { return "readTemplateFromFile("templates/email_article_template.txt"); @@ -56,7 +104,7 @@ class Mail extends Plugin { $tpl->setVariable('USER_EMAIL', $user_email, true); $tpl->setVariable('TTRSS_HOST', $_SERVER["HTTP_HOST"], true); - $result = db_query("SELECT link, content, title, note + $result = db_query("SELECT DISTINCT link, content, title, note FROM ttrss_user_entries, ttrss_entries WHERE id = ref_id AND id IN ($param) AND owner_uid = " . $_SESSION["uid"]); @@ -87,25 +135,20 @@ class Mail extends Plugin { print "
"; - print __('From:'); - - print ""; - - print "\">"; - - print "
"; + $addresslist = explode(",", $this->host->get($this, "addresslist")); print __('To:'); print ""; - print ""; + name=\"destination\" id=\"emailArticleDlg_destination\">"; */ + + print_select("destination", "", $addresslist, 'style="width: 30em" dojoType="dijit.form.ComboBox"'); - print "
"; +/* print "
"; */ print "
"; @@ -139,8 +182,8 @@ class Mail extends Plugin { $mail = new ttrssMailer(); - $mail->From = strip_tags($_REQUEST['from_email']); - $mail->FromName = strip_tags($_REQUEST['from_name']); + $mail->AddReplyTo(strip_tags($_REQUEST['from_email']), + strip_tags($_REQUEST['from_name'])); //$mail->AddAddress($_REQUEST['destination']); $addresses = explode(';', $_REQUEST['destination']); foreach($addresses as $nextaddr) @@ -155,14 +198,14 @@ class Mail extends Plugin { if (!$rc) { $reply['error'] = $mail->ErrorInfo; } else { - save_email_address(db_escape_string($destination)); + //save_email_address(db_escape_string($destination)); $reply['message'] = "UPDATE_COUNTERS"; } print json_encode($reply); } - function completeEmails() { + /* function completeEmails() { $search = db_escape_string($_REQUEST["search"]); print "
    "; @@ -174,7 +217,7 @@ class Mail extends Plugin { } print "
"; - } + } */ function api_version() { return 2; diff --git a/plugins/mail/mail.js b/plugins/mail/mail.js index 6166f01c1..db0503ff9 100644 --- a/plugins/mail/mail.js +++ b/plugins/mail/mail.js @@ -44,13 +44,13 @@ function emailArticle(id) { }, href: query}); - var tmph = dojo.connect(dialog, 'onLoad', function() { + /* var tmph = dojo.connect(dialog, 'onLoad', function() { dojo.disconnect(tmph); new Ajax.Autocompleter('emailArticleDlg_destination', 'emailArticleDlg_dst_choices', "backend.php?op=pluginhandler&plugin=mail&method=completeEmails", { tokens: '', paramName: "search" }); - }); + }); */ dialog.show(); diff --git a/plugins/mail/mail.png b/plugins/mail/mail.png index 4d3fe7751..7348aed77 100644 Binary files a/plugins/mail/mail.png and b/plugins/mail/mail.png differ diff --git a/plugins/mailto/init.php b/plugins/mailto/init.php index aa6d173f8..12f80ec89 100644 --- a/plugins/mailto/init.php +++ b/plugins/mailto/init.php @@ -41,7 +41,7 @@ class MailTo extends Plugin { $tpl->setVariable('TTRSS_HOST', $_SERVER["HTTP_HOST"], true); - $result = db_query("SELECT link, content, title + $result = db_query("SELECT DISTINCT link, content, title FROM ttrss_user_entries, ttrss_entries WHERE id = ref_id AND id IN ($param) AND owner_uid = " . $_SESSION["uid"]); @@ -65,7 +65,7 @@ class MailTo extends Plugin { $content = ""; $tpl->generateOutputToString($content); - $mailto_link = htmlspecialchars("mailto: ?subject=".rawurlencode($subject). + $mailto_link = htmlspecialchars("mailto:?subject=".rawurlencode($subject). "&body=".rawurlencode($content)); print __("Clicking the following link to invoke your mail client:"); diff --git a/plugins/mailto/mail.png b/plugins/mailto/mail.png index fcdcbd604..2c49f78a6 100644 Binary files a/plugins/mailto/mail.png and b/plugins/mailto/mail.png differ diff --git a/plugins/mark_button/init.php b/plugins/mark_button/init.php index 971b12932..4cdff53d0 100644 --- a/plugins/mark_button/init.php +++ b/plugins/mark_button/init.php @@ -21,12 +21,12 @@ class Mark_Button extends Plugin { if (get_pref("COMBINED_DISPLAY_MODE")) { if (sql_bool_to_bool($line["marked"])) { $marked_pic = "\"Unstar"; } else { $marked_pic = "\"Star"; } diff --git a/plugins/no_iframes/init.php b/plugins/no_iframes/init.php new file mode 100644 index 000000000..c66d7abaf --- /dev/null +++ b/plugins/no_iframes/init.php @@ -0,0 +1,29 @@ +host = $host; + + $host->add_hook($host::HOOK_SANITIZE, $this); + } + + function hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes) { + + $allowed_elements = array_diff($allowed_elements, array("iframe")); + + return array($doc, $allowed_elements, $disallowed_attributes); + } + + function api_version() { + return 2; + } + +} +?> diff --git a/plugins/no_title_counters/init.js b/plugins/no_title_counters/init.js new file mode 100644 index 000000000..9c16856a2 --- /dev/null +++ b/plugins/no_title_counters/init.js @@ -0,0 +1,5 @@ +dojo.addOnLoad(function() { + updateTitle = function() { + document.title = "Tiny Tiny RSS"; + }; +}); diff --git a/plugins/no_title_counters/init.php b/plugins/no_title_counters/init.php new file mode 100644 index 000000000..a9f22831d --- /dev/null +++ b/plugins/no_title_counters/init.php @@ -0,0 +1,25 @@ +host = $host; + + } + + function get_js() { + return file_get_contents(__DIR__ . "/init.js"); + } + + function api_version() { + return 2; + } + +} +?> diff --git a/plugins/no_url_hashes/init.js b/plugins/no_url_hashes/init.js new file mode 100644 index 000000000..a437a1f3e --- /dev/null +++ b/plugins/no_url_hashes/init.js @@ -0,0 +1,4 @@ +dojo.addOnLoad(function() { + hash_set = function() { }; + hash_get = function() { }; +}); diff --git a/plugins/no_url_hashes/init.php b/plugins/no_url_hashes/init.php new file mode 100644 index 000000000..da31267c2 --- /dev/null +++ b/plugins/no_url_hashes/init.php @@ -0,0 +1,25 @@ +host = $host; + + } + + function get_js() { + return file_get_contents(__DIR__ . "/init.js"); + } + + function api_version() { + return 2; + } + +} +?> diff --git a/plugins/note/note.png b/plugins/note/note.png index 7f9f3fc3f..244e6ca04 100644 Binary files a/plugins/note/note.png and b/plugins/note/note.png differ diff --git a/plugins/search_sphinx/init.php b/plugins/search_sphinx/init.php new file mode 100644 index 000000000..557b2682c --- /dev/null +++ b/plugins/search_sphinx/init.php @@ -0,0 +1,64 @@ +add_hook($host::HOOK_SEARCH, $this); + + if (class_exists("SphinxClient")) { + user_error("Your PHP has a separate systemwide Sphinx client installed which conflicts with the client library used by tt-rss. Either remove the system library or disable Sphinx support."); + } + + require_once __DIR__ . "/sphinxapi.php"; + } + + function hook_search($search) { + $offset = 0; + $limit = 500; + + $sphinxClient = new SphinxClient(); + + $sphinxpair = explode(":", SPHINX_SERVER, 2); + + $sphinxClient->SetServer($sphinxpair[0], (int)$sphinxpair[1]); + $sphinxClient->SetConnectTimeout(1); + + $sphinxClient->SetFieldWeights(array('title' => 70, 'content' => 30, + 'feed_title' => 20)); + + $sphinxClient->SetMatchMode(SPH_MATCH_EXTENDED2); + $sphinxClient->SetRankingMode(SPH_RANK_PROXIMITY_BM25); + $sphinxClient->SetLimits($offset, $limit, 1000); + $sphinxClient->SetArrayResult(false); + $sphinxClient->SetFilter('owner_uid', array($_SESSION['uid'])); + + $result = $sphinxClient->Query($search, SPHINX_INDEX); + + $ids = array(); + + if (is_array($result['matches'])) { + foreach (array_keys($result['matches']) as $int_id) { + $ref_id = $result['matches'][$int_id]['attrs']['ref_id']; + array_push($ids, $ref_id); + } + } + + $ids = join(",", $ids); + + if ($ids) + return array("ref_id IN ($ids)", array()); + else + return array("ref_id = -1", array()); + } + + function api_version() { + return 2; + } +} +?> diff --git a/lib/sphinxapi.php b/plugins/search_sphinx/sphinxapi.php similarity index 100% rename from lib/sphinxapi.php rename to plugins/search_sphinx/sphinxapi.php diff --git a/plugins/share/init.php b/plugins/share/init.php index 72a4d4bf9..899677c32 100644 --- a/plugins/share/init.php +++ b/plugins/share/init.php @@ -12,14 +12,66 @@ class Share extends Plugin { $this->host = $host; $host->add_hook($host::HOOK_ARTICLE_BUTTON, $this); + $host->add_hook($host::HOOK_PREFS_TAB_SECTION, $this); } function get_js() { return file_get_contents(dirname(__FILE__) . "/share.js"); } + function get_prefs_js() { + return file_get_contents(dirname(__FILE__) . "/share_prefs.js"); + } + + + function unshare() { + $id = db_escape_string($_REQUEST['id']); + + db_query("UPDATE ttrss_user_entries SET uuid = '' WHERE int_id = '$id' + AND owner_uid = " . $_SESSION['uid']); + + print "OK"; + } + + function hook_prefs_tab_section($id) { + if ($id == "prefFeedsPublishedGenerated") { + + print_warning(__("You can disable all articles shared by unique URLs here.")); + + print "

"; + + print " "; + + print "

"; + + } + } + + // Silent + function clearArticleKeys() { + db_query("UPDATE ttrss_user_entries SET uuid = '' WHERE + owner_uid = " . $_SESSION["uid"]); + + return; + } + + + function newkey() { + $id = db_escape_string($_REQUEST['id']); + + $uuid = db_escape_string(uniqid(base_convert(rand(), 10, 36))); + + db_query("UPDATE ttrss_user_entries SET uuid = '$uuid' WHERE int_id = '$id' + AND owner_uid = " . $_SESSION['uid']); + + print json_encode(array("link" => $uuid)); + } + function hook_article_button($line) { - return ""; @@ -39,7 +91,7 @@ class Share extends Plugin { $ref_id = db_fetch_result($result, 0, "ref_id"); if (!$uuid) { - $uuid = db_escape_string(sha1(uniqid(rand(), true))); + $uuid = db_escape_string(uniqid(base_convert(rand(), 10, 36))); db_query("UPDATE ttrss_user_entries SET uuid = '$uuid' WHERE int_id = '$param' AND owner_uid = " . $_SESSION['uid']); } @@ -50,7 +102,7 @@ class Share extends Plugin { $url_path .= "/public.php?op=share&key=$uuid"; print "
"; - print "$url_path"; + print "$url_path"; print "
"; /* if (!label_find_id(__('Shared'), $_SESSION["uid"])) @@ -61,6 +113,12 @@ class Share extends Plugin { print "
"; + print ""; + + print ""; + print ""; diff --git a/plugins/share/notshared.png b/plugins/share/notshared.png new file mode 100644 index 000000000..523575306 Binary files /dev/null and b/plugins/share/notshared.png differ diff --git a/plugins/share/share.js b/plugins/share/share.js index 6752189ea..09c973ed8 100644 --- a/plugins/share/share.js +++ b/plugins/share/share.js @@ -9,12 +9,79 @@ function shareArticle(id) { id: "shareArticleDlg", title: __("Share article by URL"), style: "width: 600px", + newurl: function() { + + var ok = confirm(__("Generate new share URL for this article?")); + + if (ok) { + + notify_progress("Trying to change URL...", true); + + var query = "op=pluginhandler&plugin=share&method=newkey&id=" + param_escape(id); + + new Ajax.Request("backend.php", { + parameters: query, + onComplete: function(transport) { + var reply = JSON.parse(transport.responseText); + var new_link = reply.link; + + var e = $('gen_article_url'); + + if (new_link) { + + e.innerHTML = e.innerHTML.replace(/\&key=.*$/, + "&key=" + new_link); + + e.href = e.href.replace(/\&key=.*$/, + "&key=" + new_link); + + new Effect.Highlight(e); + + var img = $("SHARE-IMG-" + id); + if (img) img.src = img.src.replace("notshared.png", "share.png"); + + notify(''); + + } else { + notify_error("Could not change URL."); + } + } }); + + } + + }, + unshare: function() { + + var ok = confirm(__("Remove sharing for this article?")); + + if (ok) { + + notify_progress("Trying to unshare...", true); + + var query = "op=pluginhandler&plugin=share&method=unshare&id=" + param_escape(id); + + new Ajax.Request("backend.php", { + parameters: query, + onComplete: function(transport) { + notify("Article unshared."); + + var img = $("SHARE-IMG-" + id); + if (img) img.src = img.src.replace("share.png", "notshared.png"); + + dialog.hide(); + } }); + } + + }, href: query}); dialog.show(); + var img = $("SHARE-IMG-" + id); + if (img) img.src = img.src.replace("notshared.png", "share.png"); + } catch (e) { - exception_error("emailArticle", e); + exception_error("shareArticle", e); } } diff --git a/plugins/share/share.png b/plugins/share/share.png index 3b6398f4a..25eacb7c2 100644 Binary files a/plugins/share/share.png and b/plugins/share/share.png differ diff --git a/plugins/share/share_prefs.js b/plugins/share/share_prefs.js new file mode 100644 index 000000000..9efe291f9 --- /dev/null +++ b/plugins/share/share_prefs.js @@ -0,0 +1,21 @@ +function clearArticleAccessKeys() { + + var ok = confirm(__("This will invalidate all previously shared article URLs. Continue?")); + + if (ok) { + notify_progress("Clearing URLs..."); + + var query = "?op=pluginhandler&plugin=share&method=clearArticleKeys"; + + new Ajax.Request("backend.php", { + parameters: query, + onComplete: function(transport) { + notify_info("Shared URLs cleared."); + } }); + } + + return false; +} + + + diff --git a/plugins/shorten_expanded/init.css b/plugins/shorten_expanded/init.css new file mode 100644 index 000000000..5e1bfa8f3 --- /dev/null +++ b/plugins/shorten_expanded/init.css @@ -0,0 +1,9 @@ +div.contentSizeWrapper { + overflow : hidden; + text-overflow: ellipsis; + height : 700px; +} + +button.expandPrompt { + margin-top : 20px; +} diff --git a/plugins/shorten_expanded/init.js b/plugins/shorten_expanded/init.js new file mode 100644 index 000000000..ba82b643b --- /dev/null +++ b/plugins/shorten_expanded/init.js @@ -0,0 +1,45 @@ +var _shorten_expanded_threshold = 900; //px, longer than css height so that we would only clip articles significantly longer than limit + +function expandSizeWrapper(id) { + try { + var row = $(id); + + console.log(row); + + if (row) { + var content = row.select(".contentSizeWrapper")[0]; + var link = row.select(".expandPrompt")[0]; + + if (content) content.removeClassName("contentSizeWrapper"); + if (link) Element.hide(link); + + } + } catch (e) { + exception_error("expandSizeWrapper", e); + } + + return false; + +} + +dojo.addOnLoad(function() { + PluginHost.register(PluginHost.HOOK_ARTICLE_RENDERED_CDM, function(row) { + if (getInitParam('cdm_expanded')) { + + window.setTimeout(function() { + if (row) { + if (row.offsetHeight >= _shorten_expanded_threshold) { + var content = row.select(".cdmContentInner")[0]; + + if (content) { + content.innerHTML = "
" + + content.innerHTML + "
"; + + } + } + } + }, 150); + } + }); +}); diff --git a/plugins/shorten_expanded/init.php b/plugins/shorten_expanded/init.php new file mode 100644 index 000000000..1d0c99eca --- /dev/null +++ b/plugins/shorten_expanded/init.php @@ -0,0 +1,29 @@ +host = $host; + + } + + function get_css() { + return file_get_contents(__DIR__ . "/init.css"); + } + + function get_js() { + return file_get_contents(__DIR__ . "/init.js"); + } + + function api_version() { + return 2; + } + +} +?> diff --git a/plugins/updater/init.php b/plugins/updater/init.php index fa283c8be..6ee018f74 100644 --- a/plugins/updater/init.php +++ b/plugins/updater/init.php @@ -24,6 +24,8 @@ class Updater extends Plugin { // __FILE__ is in plugins/updater so we need to go one level up $work_dir = dirname(dirname(dirname(__FILE__))); $parent_dir = dirname($work_dir); + // Set PATH to run "which" + putenv('PATH="$PATH:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin"'); $log = array(); if (!is_array($params)) $params = array(); @@ -61,6 +63,20 @@ class Updater extends Plugin { putenv("PATH=" . getenv("PATH") . PATH_SEPARATOR . "/bin" . PATH_SEPARATOR . "/usr/bin"); + array_push($log, "Checking for system() call..."); + + $disabled = explode(',', ini_get('disable_functions')); + foreach ($disabled as $function) { + if ( trim($function) == 'system' ) { + array_push($log, "Can not execute commands with PHP's system() function."); + $stop = true; + } + } + + if ( $stop == true ) { + break; + } + array_push($log, "Checking for tar..."); $system_rc = 0; @@ -234,6 +250,7 @@ class Updater extends Plugin { CACHE_DIR . "/images", CACHE_DIR . "/js", CACHE_DIR . "/simplepie", + CACHE_DIR . "/upload", ICONS_DIR, LOCK_DIRECTORY); @@ -339,6 +356,10 @@ class Updater extends Plugin { } else { print_notice(__("Your Tiny Tiny RSS installation is up to date.")); + + print "

"; + } print "
"; #pane diff --git a/plugins/vf_shared/init.php b/plugins/vf_shared/init.php new file mode 100644 index 000000000..c7eac9659 --- /dev/null +++ b/plugins/vf_shared/init.php @@ -0,0 +1,60 @@ +host = $host; + + $host->add_feed(-1, __("Shared articles"), 'plugins/vf_shared/share.png', $this); + } + + function api_version() { + return 2; + } + + function get_unread($feed_id) { + $result = db_query("select count(int_id) AS count from ttrss_user_entries where owner_uid = ".$_SESSION["uid"]." and unread = true and uuid != ''"); + + return db_fetch_result($result, 0, "count"); + } + + function get_total($feed_id) { + $result = db_query("select count(int_id) AS count from ttrss_user_entries where owner_uid = ".$_SESSION["uid"]." and uuid != ''"); + + return db_fetch_result($result, 0, "count"); + } + + //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) { + + function get_headlines($feed_id, $options) { + $qfh_ret = queryFeedHeadlines(-4, + $options['limit'], + $this->get_unread(-1) > 0 ? "adaptive" : "all_articles", + false, + $options['search'], + $options['search_mode'], + $options['override_order'], + $options['offset'], + $options['owner_uid'], + $options['filter'], + $options['since_id'], + $options['include_children'], + false, + "uuid != ''", + "ttrss_feeds.title AS feed_title,"); + + $qfh_ret[1] = __("Shared articles"); + + return $qfh_ret; + } + +} +?> diff --git a/plugins/vf_shared/share.png b/plugins/vf_shared/share.png new file mode 100644 index 000000000..25eacb7c2 Binary files /dev/null and b/plugins/vf_shared/share.png differ diff --git a/prefs.php b/prefs.php index fcf21cf2e..cc6267798 100644 --- a/prefs.php +++ b/prefs.php @@ -32,15 +32,19 @@ Tiny Tiny RSS : <?php echo __("Preferences") ?> - - + + + + @@ -52,19 +56,19 @@ - - + + + + +