diff options
author | YoungMin Park <[email protected]> | 2014-11-04 11:49:43 +0900 |
---|---|---|
committer | YoungMin Park <[email protected]> | 2014-11-04 11:49:43 +0900 |
commit | a5bbb2bec133bdee08b361628f32430ae3884107 (patch) | |
tree | b203105149ec234ebe155d5718d2cbb6390b4a45 /plugins | |
parent | 12727ad17d125eb2f3f243231ccca1cb0a5a7b4b (diff) | |
parent | 2f43089de1ead3f164b8b31967d1abbb784319fa (diff) |
Merge pull request #1 from gothfox/master
Update from original
Diffstat (limited to 'plugins')
54 files changed, 3100 insertions, 361 deletions
diff --git a/plugins/af_buni/init.php b/plugins/af_buni/init.php deleted file mode 100644 index 50166e325..000000000 --- a/plugins/af_buni/init.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php -class Af_Buni extends Plugin { - - private $host; - - function about() { - return array(1.0, - "Fix Buni rss feed", - "fox"); - } - - function init($host) { - $this->host = $host; - - $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); - } - - function hook_article_filter($article) { - $owner_uid = $article["owner_uid"]; - - if (strpos($article["guid"], "bunicomic.com") !== FALSE) { - if (strpos($article["plugin_data"], "buni,$owner_uid:") === FALSE) { - - $doc = new DOMDocument(); - @$doc->loadHTML(fetch_file_contents($article["link"])); - - $basenode = false; - - if ($doc) { - $xpath = new DOMXPath($doc); - $entries = $xpath->query('(//img[@src])'); - - $matches = array(); - - foreach ($entries as $entry) { - - if (preg_match("/(http:\/\/www.bunicomic.com\/comics\/\d{4}.*)/i", $entry->getAttribute("src"), $matches)) { - - $basenode = $entry; - break; - } - } - - if ($basenode) { - $article["content"] = $doc->saveXML($basenode); - $article["plugin_data"] = "buni,$owner_uid:" . $article["plugin_data"]; - } - } - } else if (isset($article["stored"]["content"])) { - $article["content"] = $article["stored"]["content"]; - } - } - - return $article; - } - - function api_version() { - return 2; - } - -} -?> diff --git a/plugins/af_buttersafe/init.php b/plugins/af_buttersafe/init.php deleted file mode 100644 index 05e684aa0..000000000 --- a/plugins/af_buttersafe/init.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php -class Af_Buttersafe extends Plugin { - - private $host; - - function about() { - return array(1.0, - "Strip unnecessary stuff from Buttersafe feeds", - "fox"); - } - - function init($host) { - $this->host = $host; - - $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); - } - - function hook_article_filter($article) { - $owner_uid = $article["owner_uid"]; - - if (strpos($article["guid"], "buttersafe.com") !== FALSE) { - if (strpos($article["plugin_data"], "buttersafe,$owner_uid:") === FALSE) { - - $doc = new DOMDocument(); - @$doc->loadHTML(fetch_file_contents($article["link"])); - - $basenode = false; - - if ($doc) { - $xpath = new DOMXPath($doc); - $entries = $xpath->query('(//img[@src])'); - - $matches = array(); - - foreach ($entries as $entry) { - - if (preg_match("/(http:\/\/buttersafe.com\/comics\/\d{4}.*)/i", $entry->getAttribute("src"), $matches)) { - - $basenode = $entry; - break; - } - } - - if ($basenode) { - $article["content"] = $doc->saveXML($basenode); - $article["plugin_data"] = "buttersafe,$owner_uid:" . $article["plugin_data"]; - } - } - } else if (isset($article["stored"]["content"])) { - $article["content"] = $article["stored"]["content"]; - } - } - - return $article; - } - - function api_version() { - return 2; - } - -} -?> diff --git a/plugins/af_comics/af_comics_template.php b/plugins/af_comics/af_comics_template.php new file mode 100644 index 000000000..117d69280 --- /dev/null +++ b/plugins/af_comics/af_comics_template.php @@ -0,0 +1,14 @@ +<?php +class Af_Comics_Template extends Af_ComicFilter { + + function supported() { + return array("Example"); + } + + function process(&$article) { + $owner_uid = $article["owner_uid"]; + + return false; + } +} +?> diff --git a/plugins/af_comics/filter_base.php b/plugins/af_comics/filter_base.php new file mode 100644 index 000000000..a53430645 --- /dev/null +++ b/plugins/af_comics/filter_base.php @@ -0,0 +1,6 @@ +<?php +abstract class Af_ComicFilter { + public abstract function supported(); + public abstract function process(&$article); +} +?> diff --git a/plugins/af_comics/filters/af_comics_cad.php b/plugins/af_comics/filters/af_comics_cad.php new file mode 100644 index 000000000..43836724f --- /dev/null +++ b/plugins/af_comics/filters/af_comics_cad.php @@ -0,0 +1,36 @@ +<?php +class Af_Comics_Cad extends Af_ComicFilter { + + function supported() { + return array("Ctrl+Alt+Del"); + } + + function process(&$article) { + $owner_uid = $article["owner_uid"]; + + if (strpos($article["link"], "cad-comic.com/cad/") !== FALSE) { + if (strpos($article["title"], "News:") === FALSE) { + + $doc = new DOMDocument(); + @$doc->loadHTML(fetch_file_contents($article["link"])); + + $basenode = false; + + if ($doc) { + $xpath = new DOMXPath($doc); + $basenode = $xpath->query('(//img[contains(@src, "/comics/cad-")])')->item(0); + + if ($basenode) { + $article["content"] = $doc->saveXML($basenode); + } + } + + } + + return true; + } + + return false; + } +} +?> diff --git a/plugins/af_comics/filters/af_comics_comicpress.php b/plugins/af_comics/filters/af_comics_comicpress.php new file mode 100644 index 000000000..ec4b7ac57 --- /dev/null +++ b/plugins/af_comics/filters/af_comics_comicpress.php @@ -0,0 +1,46 @@ +<?php +class Af_Comics_ComicPress extends Af_ComicFilter { + + function supported() { + return array("Buni", "Buttersafe", "Whomp!", "Happy Jar", "CSection", + "Extra Fabulous Comics"); + } + + function process(&$article) { + $owner_uid = $article["owner_uid"]; + + if (strpos($article["guid"], "bunicomic.com") !== FALSE || + strpos($article["guid"], "buttersafe.com") !== FALSE || + strpos($article["guid"], "whompcomic.com") !== FALSE || + strpos($article["guid"], "extrafabulouscomics.com") !== FALSE || + strpos($article["guid"], "happyjar.com") !== FALSE || + strpos($article["guid"], "csectioncomics.com") !== FALSE) { + + // lol at people who block clients by user agent + // oh noes my ad revenue Q_Q + + $res = fetch_file_contents($article["link"], false, false, false, + false, false, 0, + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)"); + + $doc = new DOMDocument(); + @$doc->loadHTML($res); + + $basenode = false; + + if ($doc) { + $xpath = new DOMXPath($doc); + $basenode = $xpath->query('//div[@id="comic"]')->item(0); + + if ($basenode) { + $article["content"] = $doc->saveXML($basenode); + } + } + + return true; + } + + return false; + } +} +?> diff --git a/plugins/af_comics/filters/af_comics_darklegacy.php b/plugins/af_comics/filters/af_comics_darklegacy.php new file mode 100644 index 000000000..c03919b1d --- /dev/null +++ b/plugins/af_comics/filters/af_comics_darklegacy.php @@ -0,0 +1,43 @@ +<?php +class Af_Comics_DarkLegacy extends Af_ComicFilter { + + function supported() { + return array("Dark Legacy Comics"); + } + + function process(&$article) { + $owner_uid = $article["owner_uid"]; + + if (strpos($article["guid"], "darklegacycomics.com") !== FALSE) { + + $res = fetch_file_contents($article["link"], false, false, false, + false, false, 0, + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)"); + + global $fetch_last_error_content; + + if (!$res && $fetch_last_error_content) + $res = $fetch_last_error_content; + + $doc = new DOMDocument(); + @$doc->loadHTML($res); + + $basenode = false; + + if ($doc) { + $xpath = new DOMXPath($doc); + $basenode = $xpath->query('//div[@class="comic"]')->item(0); + + if ($basenode) { + + $article["content"] = $doc->saveXML($basenode); + } + } + + return true; + } + + return false; + } +} +?> diff --git a/plugins/af_comics/filters/af_comics_dilbert.php b/plugins/af_comics/filters/af_comics_dilbert.php new file mode 100644 index 000000000..f0e31cf88 --- /dev/null +++ b/plugins/af_comics/filters/af_comics_dilbert.php @@ -0,0 +1,47 @@ +<?php +class Af_Comics_Dilbert extends Af_ComicFilter { + + function supported() { + return array("Dilbert"); + } + + function process(&$article) { + $owner_uid = $article["owner_uid"]; + + if (strpos($article["guid"], "dilbert.com") !== FALSE) { + $doc = new DOMDocument(); + @$doc->loadHTML(fetch_file_contents($article["link"])); + + $basenode = false; + + if ($doc) { + $xpath = new DOMXPath($doc); + $entries = $xpath->query('(//img[@src])'); // we might also check for img[@class='strip'] I guess... + + $matches = array(); + + foreach ($entries as $entry) { + + if (preg_match("/dyn\/str_strip\/.*zoom\.gif$/", $entry->getAttribute("src"), $matches)) { + + $entry->setAttribute("src", + rewrite_relative_url("http://dilbert.com/", + $matches[0])); + + $basenode = $entry; + break; + } + } + + if ($basenode) { + $article["content"] = $doc->saveXML($basenode); + } + } + + return true; + } + + return false; + } +} +?> diff --git a/plugins/af_explosm/init.php b/plugins/af_comics/filters/af_comics_explosm.php index dd106653a..cba7ad0ad 100644 --- a/plugins/af_explosm/init.php +++ b/plugins/af_comics/filters/af_comics_explosm.php @@ -1,25 +1,14 @@ <?php -class Af_Explosm extends Plugin { +class Af_Comics_Explosm extends Af_ComicFilter { - private $host; - - function about() { - return array(1.0, - "Strip unnecessary stuff from Cyanide and Happiness feeds", - "fox"); - } - - function init($host) { - $this->host = $host; - - $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); + function supported() { + return array("Cyanide and Happiness"); } - function hook_article_filter($article) { + function process(&$article) { $owner_uid = $article["owner_uid"]; if (strpos($article["link"], "explosm.net/comics") !== FALSE) { - if (strpos($article["plugin_data"], "explosm,$owner_uid:") === FALSE) { $doc = new DOMDocument(); @$doc->loadHTML(fetch_file_contents($article["link"])); @@ -43,19 +32,13 @@ class Af_Explosm extends Plugin { if ($basenode) { $article["content"] = $doc->saveXML($basenode); - $article["plugin_data"] = "explosm,$owner_uid:" . $article["plugin_data"]; } } - } else if (isset($article["stored"]["content"])) { - $article["content"] = $article["stored"]["content"]; - } - } - return $article; - } + return true; + } - function api_version() { - return 2; + return false; } } ?> diff --git a/plugins/af_comics/filters/af_comics_gocomics.php b/plugins/af_comics/filters/af_comics_gocomics.php new file mode 100644 index 000000000..9b3c787de --- /dev/null +++ b/plugins/af_comics/filters/af_comics_gocomics.php @@ -0,0 +1,53 @@ +<?php +class Af_Comics_GoComics extends Af_ComicFilter { + + function supported() { + return array("GoComics"); + } + + function process(&$article) { + $owner_uid = $article["owner_uid"]; + + if (strpos($article["guid"], "gocomics.com") !== FALSE) { + $doc = new DOMDocument(); + @$doc->loadHTML(fetch_file_contents($article["link"])); + + $basenode = false; + + if ($doc) { + $xpath = new DOMXPath($doc); + $entries = $xpath->query("(//img[@class='strip'])"); + + $matches = array(); + + if ($entries->length > 1) { // if we have more than one match, then get the zoomed one, which is the second for gocomics + $entry = $entries->item(1); // get the second element (items start at 0) + if (preg_match("/(http:\/\/assets.amuniversal.com\/.*)/i", $entry->getAttribute("src"), $matches)) { + $entry->setAttribute("src", $matches[0]); + $basenode = $entry; + } + } + + if (!$basenode) { + // fallback on the smaller version + foreach ($entries as $entry) { + if (preg_match("/(http:\/\/assets.amuniversal.com\/.*)/i", $entry->getAttribute("src"), $matches)) { + $entry->setAttribute("src", $matches[0]); + $basenode = $entry; + break; + } + } + } + + if ($basenode) { + $article["content"] = $doc->saveXML($basenode); + } + } + + return true; + } + + return false; + } +} +?> diff --git a/plugins/af_pennyarcade/init.php b/plugins/af_comics/filters/af_comics_pa.php index 8ad02e14c..a13857cc1 100644 --- a/plugins/af_pennyarcade/init.php +++ b/plugins/af_comics/filters/af_comics_pa.php @@ -1,25 +1,12 @@ <?php -class Af_PennyArcade extends Plugin { +class Af_Comics_Pa extends Af_ComicFilter { - private $host; - - function about() { - return array(1.1, - "Strip unnecessary stuff from PA feeds", - "fox"); + function supported() { + return array("Penny Arcade"); } - function init($host) { - $this->host = $host; - - $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); - } - - function hook_article_filter($article) { - $owner_uid = $article["owner_uid"]; - + function process(&$article) { if (strpos($article["link"], "penny-arcade.com") !== FALSE && strpos($article["title"], "Comic:") !== FALSE) { - if (strpos($article["plugin_data"], "pennyarcade,$owner_uid:") === FALSE) { if ($debug_enabled) { _debug("af_pennyarcade: Processing comic"); @@ -32,24 +19,17 @@ class Af_PennyArcade extends Plugin { if ($doc) { $xpath = new DOMXPath($doc); - $entries = $xpath->query('(//div[@class="post comic"])'); - - foreach ($entries as $entry) { - $basenode = $entry; - } + $basenode = $xpath->query('(//div[@id="comicFrame"])')->item(0); if ($basenode) { $article["content"] = $doc->saveXML($basenode); - $article["plugin_data"] = "pennyarcade,$owner_uid:" . $article["plugin_data"]; } } - } else if (isset($article["stored"]["content"])) { - $article["content"] = $article["stored"]["content"]; - } + + return true; } if (strpos($article["link"], "penny-arcade.com") !== FALSE && strpos($article["title"], "News Post:") !== FALSE) { - if (strpos($article["plugin_data"], "pennyarcade,$owner_uid:") === FALSE) { if ($debug_enabled) { _debug("af_pennyarcade: Processing news post"); } @@ -66,27 +46,32 @@ class Af_PennyArcade extends Plugin { $basenode = $entry; } - $uninteresting = $xpath->query('(//div[@class="heading"])'); + $meta = $xpath->query('(//div[@class="meta"])')->item(0); + if ($meta->parentNode) { $meta->parentNode->removeChild($meta); } + + $header = $xpath->query('(//div[@class="postBody"]/h2)')->item(0); + if ($header->parentNode) { $header->parentNode->removeChild($header); } + + $header = $xpath->query('(//div[@class="postBody"]/div[@class="comicPost"])')->item(0); + if ($header->parentNode) { $header->parentNode->removeChild($header); } + + $avatar = $xpath->query('(//div[@class="avatar"]//img)')->item(0); + $basenode->insertBefore($avatar, $basenode->firstChild); + + $uninteresting = $xpath->query('(//div[@class="avatar"])'); foreach ($uninteresting as $i) { $i->parentNode->removeChild($i); } if ($basenode){ $article["content"] = $doc->saveXML($basenode); - $article["plugin_data"] = "pennyarcade,$owner_uid:" . $article["plugin_data"]; } } - } else if (isset($article["stored"]["content"])) { - $article["content"] = $article["stored"]["content"]; - } - } - return $article; - } + return true; + } - function api_version() { - return 2; + return false; } - } ?> diff --git a/plugins/af_comics/filters/af_comics_tfd.php b/plugins/af_comics/filters/af_comics_tfd.php new file mode 100644 index 000000000..c4e594551 --- /dev/null +++ b/plugins/af_comics/filters/af_comics_tfd.php @@ -0,0 +1,32 @@ +<?php +class Af_Comics_Tfd extends Af_ComicFilter { + + function supported() { + return array("Toothpaste For Dinner"); + } + + function process(&$article) { + $owner_uid = $article["owner_uid"]; + + if (strpos($article["link"], "toothpastefordinner.com") !== FALSE) { + $doc = new DOMDocument(); + + @$doc->loadHTML(fetch_file_contents($article["link"])); + + $basenode = false; + + if ($doc) { + $xpath = new DOMXPath($doc); + $basenode = $xpath->query('//img[@class="comic"]')->item(0); + + if ($basenode) { + $article["content"] = $doc->saveXML($basenode); + return true; + } + } + } + + return false; + } +} +?> diff --git a/plugins/af_comics/filters/af_comics_twp.php b/plugins/af_comics/filters/af_comics_twp.php new file mode 100644 index 000000000..ec43c1379 --- /dev/null +++ b/plugins/af_comics/filters/af_comics_twp.php @@ -0,0 +1,33 @@ +<?php +class Af_Comics_Twp extends Af_ComicFilter { + + function supported() { + return array("Three Word Phrase"); + } + + function process(&$article) { + + if (strpos($article["link"], "threewordphrase.com") !== FALSE) { + + $doc = new DOMDocument(); + @$doc->loadHTML(fetch_file_contents($article["link"])); + + $basenode = false; + + if ($doc) { + $xpath = new DOMXpath($doc); + + $basenode = $xpath->query("//td/center/img")->item(0); + + if ($basenode) { + $article["content"] = $doc->saveXML($basenode); + } + } + + return true; + } + + return false; + } +} +?> diff --git a/plugins/af_comics/init.php b/plugins/af_comics/init.php new file mode 100644 index 000000000..efc51d187 --- /dev/null +++ b/plugins/af_comics/init.php @@ -0,0 +1,79 @@ +<?php +class Af_Comics extends Plugin { + + private $host; + private $filters = array(); + + function about() { + return array(1.0, + "Fixes RSS feeds of assorted comic strips", + "fox"); + } + + function init($host) { + $this->host = $host; + + $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); + $host->add_hook($host::HOOK_PREFS_TAB, $this); + + require_once __DIR__ . "/filter_base.php"; + + $filters = glob(__DIR__ . "/filters/*.php"); + + foreach ($filters as $file) { + require_once $file; + $filter_name = preg_replace("/\..*$/", "", basename($file)); + + $filter = new $filter_name(); + + if (is_subclass_of($filter, "Af_ComicFilter")) { + array_push($this->filters, $filter); + } + } + + } + + function hook_prefs_tab($args) { + if ($args != "prefPrefs") return; + + print "<div dojoType=\"dijit.layout.AccordionPane\" title=\"".__('Feeds supported by af_comics')."\">"; + + print "<p>" . __("The following comics are currently supported:") . "</p>"; + + $comics = array(); + + foreach ($this->filters as $f) { + foreach ($f->supported() as $comic) { + array_push($comics, $comic); + } + } + + asort($comics); + + print "<ul class=\"browseFeedList\" style=\"border-width : 1px\">"; + foreach ($comics as $comic) { + print "<li>$comic</li>"; + } + print "</ul>"; + + print "</div>"; + } + + function hook_article_filter($article) { + $owner_uid = $article["owner_uid"]; + + foreach ($this->filters as $f) { + if ($f->process($article)) + break; + } + + return $article; + + } + + function api_version() { + return 2; + } + +} +?> diff --git a/plugins/af_elreg/init.php b/plugins/af_elreg/init.php new file mode 100644 index 000000000..c8d5516ee --- /dev/null +++ b/plugins/af_elreg/init.php @@ -0,0 +1,50 @@ +<?php +class Af_ElReg extends Plugin { + + private $host; + + function about() { + return array(1.0, + "Fetch content of The Register feeds", + "fox"); + } + + function init($host) { + $this->host = $host; + + $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); + } + + function hook_article_filter($article) { + if (strpos($article["link"], "theregister.co.uk") !== FALSE) { + + $doc = new DOMDocument(); + @$doc->loadHTML(fetch_file_contents($article["link"])); + + $basenode = false; + + if ($doc) { + $xpath = new DOMXPath($doc); + + $trash = $xpath->query("//*[@class='wptl top' or @class='wptl btm']"); + + foreach ($trash as $t) { + $t->parentNode->removeChild($t); + } + + $basenode = $doc->getElementById("body"); + + if ($basenode) { + $article["content"] = $doc->saveXML($basenode); + } + } + } + + return $article; + } + + function api_version() { + return 2; + } +} +?> diff --git a/plugins/af_fsckportal/init.php b/plugins/af_fsckportal/init.php new file mode 100644 index 000000000..4718eca91 --- /dev/null +++ b/plugins/af_fsckportal/init.php @@ -0,0 +1,52 @@ +<?php +class Af_Fsckportal extends Plugin { + + private $host; + + function about() { + return array(1.0, + "Remove feedsportal spamlinks from article content", + "fox"); + } + + function init($host) { + $this->host = $host; + + $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); + } + + function hook_article_filter($article) { + + $doc = new DOMDocument(); + + $charset_hack = '<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + </head>'; + + @$doc->loadHTML($charset_hack . $article["content"]); + + if ($doc) { + $xpath = new DOMXPath($doc); + $entries = $xpath->query('(//img[@src]|//a[@href])'); + + foreach ($entries as $entry) { + if (preg_match("/feedsportal.com/", $entry->getAttribute("src"))) { + $entry->parentNode->removeChild($entry); + } else if (preg_match("/feedsportal.com/", $entry->getAttribute("href"))) { + $entry->parentNode->removeChild($entry); + } + } + + $article["content"] = $doc->saveXML(); + + } + + return $article; + } + + function api_version() { + return 2; + } + +} +?> diff --git a/plugins/af_gocomics/init.php b/plugins/af_gocomics/init.php deleted file mode 100644 index e95de9f14..000000000 --- a/plugins/af_gocomics/init.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -class Af_GoComics extends Plugin { - private $host; - - function about() { - return array(1.0, - "Strip unnecessary stuff from gocomics feeds", - "fox"); - } - - function init($host) { - $this->host = $host; - - $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); - } - - function hook_article_filter($article) { - $owner_uid = $article["owner_uid"]; - - if (strpos($article["guid"], "gocomics.com") !== FALSE) { - if (strpos($article["plugin_data"], "gocomics,$owner_uid:") === FALSE) { - $doc = new DOMDocument(); - @$doc->loadHTML(fetch_file_contents($article["link"])); - - $basenode = false; - - if ($doc) { - $xpath = new DOMXPath($doc); - $entries = $xpath->query('(//img[@src])'); // we might also check for img[@class='strip'] I guess... - - $matches = array(); - - foreach ($entries as $entry) { - - if (preg_match("/(http:\/\/assets.amuniversal.com\/.*)/i", $entry->getAttribute("src"), $matches)) { - - $entry->setAttribute("src", $matches[0]); - $basenode = $entry; - break; - } - } - - if ($basenode) { - $article["content"] = $doc->saveXML($basenode); - $article["plugin_data"] = "gocomics,$owner_uid:" . $article["plugin_data"]; - } - } - } else if (isset($article["stored"]["content"])) { - $article["content"] = $article["stored"]["content"]; - } - } - - return $article; - } - - function api_version() { - return 2; - } - -} -?> diff --git a/plugins/af_habr/init.php b/plugins/af_habr/init.php new file mode 100644 index 000000000..e6dc54c24 --- /dev/null +++ b/plugins/af_habr/init.php @@ -0,0 +1,44 @@ +<?php +class Af_Habr extends Plugin { + + function about() { + return array(1.0, + "Fetch content of Habrahabr feeds", + "fox"); + } + + function init($host) { + $this->host = $host; + + $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); + } + + function hook_article_filter($article) { + $owner_uid = $article["owner_uid"]; + + if (strpos($article["link"], "habrahabr.ru") !== FALSE) { + + $doc = new DOMDocument(); + @$doc->loadHTML(fetch_file_contents($article["link"])); + + $basenode = false; + + if ($doc) { + $xpath = new DOMXPath($doc); + + $basenode = $xpath->query("//div[contains(@class,'content') and contains(@class, 'html_format')]")->item(0); + + if ($basenode) { + $article["content"] = $doc->saveXML($basenode); + } + } + } + + return $article; + } + + function api_version() { + return 2; + } +} +?> diff --git a/plugins/af_natgeo/init.php b/plugins/af_natgeo/init.php new file mode 100644 index 000000000..52a89e92d --- /dev/null +++ b/plugins/af_natgeo/init.php @@ -0,0 +1,52 @@ +<?php +class Af_NatGeo extends Plugin { + + private $host; + + function about() { + return array(1.0, + "Fetch content of National Geographic feeds", + "fox"); + } + + function init($host) { + $this->host = $host; + + $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); + } + + function hook_article_filter($article) { + $owner_uid = $article["owner_uid"]; + + if (strpos($article["link"], "nationalgeographic.com") !== FALSE) { + + $doc = new DOMDocument(); + @$doc->loadHTML(fetch_file_contents($article["link"])); + + $basenode = false; + + if ($doc) { + $xpath = new DOMXPath($doc); + + $basenode = $doc->getElementById("content_mainA"); + + $trash = $xpath->query("//*[@class='aside' or @id='livefyre' or @id='powered_by_livefyre' or @class='social_buttons']"); + + foreach ($trash as $t) { + $t->parentNode->removeChild($t); + } + + if ($basenode) { + $article["content"] = $doc->saveXML($basenode); + } + } + } + + return $article; + } + + function api_version() { + return 2; + } +} +?> diff --git a/plugins/af_redditimgur/init.php b/plugins/af_redditimgur/init.php index 2e466f167..e513cc6f4 100644 --- a/plugins/af_redditimgur/init.php +++ b/plugins/af_redditimgur/init.php @@ -15,12 +15,8 @@ class Af_RedditImgur extends Plugin { } function hook_article_filter($article) { - $owner_uid = $article["owner_uid"]; - - $force = false; if (strpos($article["link"], "reddit.com/r/") !== FALSE) { - if (strpos($article["plugin_data"], "redditimgur,$owner_uid:") === FALSE || $force) { $doc = new DOMDocument(); @$doc->loadHTML($article["content"]); @@ -62,7 +58,7 @@ class Af_RedditImgur extends Plugin { $aentries = $axpath->query('(//img[@src])'); foreach ($aentries as $aentry) { - if (preg_match("/^http:\/\/i.imgur.com\/$token\./", $aentry->getAttribute("src"))) { + if (preg_match("/\/\/i.imgur.com\/$token\./", $aentry->getAttribute("src"))) { $img = $doc->createElement('img'); $img->setAttribute("src", $aentry->getAttribute("src")); @@ -123,12 +119,8 @@ class Af_RedditImgur extends Plugin { if ($node && $found) { $article["content"] = $doc->saveXML($node); - if (!$force) $article["plugin_data"] = "redditimgur,$owner_uid:" . $article["plugin_data"]; } } - } else if (isset($article["stored"]["content"])) { - $article["content"] = $article["stored"]["content"]; - } } return $article; diff --git a/plugins/af_sciam/init.php b/plugins/af_sciam/init.php new file mode 100644 index 000000000..9c14515e0 --- /dev/null +++ b/plugins/af_sciam/init.php @@ -0,0 +1,46 @@ +<?php +class Af_SciAm extends Plugin { + + private $host; + + function about() { + return array(1.0, + "Fetch content of Scientific American feeds", + "fox"); + } + + function init($host) { + $this->host = $host; + + $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); + } + + function hook_article_filter($article) { + $owner_uid = $article["owner_uid"]; + + if (strpos($article["link"], "scientificamerican.com") !== FALSE || strpos($article["link"], "rss.sciam.com") !== FALSE) { + + $doc = new DOMDocument(); + @$doc->loadHTML(fetch_file_contents($article["link"])); + + $basenode = false; + + if ($doc) { + $xpath = new DOMXpath($doc); + + $basenode = $xpath->query("//*[@id='singleBlogPost' or @id='articleContent']")->item(0); + + if ($basenode) { + $article["content"] = $doc->saveXML($basenode); + } + } + } + + return $article; + } + + function api_version() { + return 2; + } +} +?> diff --git a/plugins/af_unburn/init.php b/plugins/af_unburn/init.php index a97502b12..5c9bc1387 100644 --- a/plugins/af_unburn/init.php +++ b/plugins/af_unburn/init.php @@ -24,8 +24,6 @@ class Af_Unburn extends Plugin { strpos($article["link"], "/~r/") !== FALSE || strpos($article["link"], "feedsportal.com") !== FALSE)) { - if (strpos($article["plugin_data"], "unburn,$owner_uid:") === FALSE) { - if (ini_get("safe_mode") || ini_get("open_basedir")) { $ch = curl_init(geturl($article["link"])); } else { @@ -38,7 +36,11 @@ class Af_Unburn extends Plugin { curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("safe_mode") && !ini_get("open_basedir")); curl_setopt($ch, CURLOPT_USERAGENT, SELF_USER_AGENT); - $contents = @curl_exec($ch); + if (defined('_CURL_HTTP_PROXY')) { + curl_setopt($ch, CURLOPT_PROXY, _CURL_HTTP_PROXY); + } + + @curl_exec($ch); $real_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); @@ -69,9 +71,6 @@ class Af_Unburn extends Plugin { $article["plugin_data"] = "unburn,$owner_uid:" . $article["plugin_data"]; $article["link"] = $real_url; } - } else if (isset($article["stored"]["link"])) { - $article["link"] = $article["stored"]["link"]; - } } return $article; diff --git a/plugins/af_whomp/init.php b/plugins/af_whomp/init.php deleted file mode 100644 index 46d6e921d..000000000 --- a/plugins/af_whomp/init.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -class Af_Whomp extends Plugin { - private $host; - - function about() { - return array(1.0, - "Embed images in Whomp!", - "fox"); - } - - function init($host) { - $this->host = $host; - - $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); - } - - function hook_article_filter($article) { - $owner_uid = $article["owner_uid"]; - - if (strpos($article["guid"], "whompcomic.com") !== FALSE) { - if (strpos($article["plugin_data"], "whomp,$owner_uid:") === FALSE) { - $doc = new DOMDocument(); - @$doc->loadHTML(fetch_file_contents($article["link"])); - - $basenode = false; - - if ($doc) { - $xpath = new DOMXPath($doc); - $entries = $xpath->query('(//img[@src])'); - - $matches = array(); - - foreach ($entries as $entry) { - - if (preg_match("/(http:\/\/www\.whompcomic\.com\/comics\/.*)/i", $entry->getAttribute("src"), $matches)) { - - $entry->setAttribute("src", $matches[0]); - $basenode = $entry; - break; - } - } - - if ($basenode) { - $article["content"] = $doc->saveXML($basenode); - $article["plugin_data"] = "whomp,$owner_uid:" . $article["plugin_data"]; - } - } - } else if (isset($article["stored"]["content"])) { - $article["content"] = $article["stored"]["content"]; - } - } - - return $article; - } - - function api_version() { - return 2; - } - -} -?> diff --git a/plugins/auth_internal/init.php b/plugins/auth_internal/init.php index 3c38ab8bf..dc49229f6 100644 --- a/plugins/auth_internal/init.php +++ b/plugins/auth_internal/init.php @@ -51,13 +51,16 @@ class Auth_Internal extends Plugin implements IAuthModule { $return = urlencode($_REQUEST["return"]); ?><html> <head><title>Tiny Tiny RSS</title></head> - <?php stylesheet_tag("css/utility.css") ?> + <?php echo stylesheet_tag("css/utility.css") ?> <body class="otp"><div class="content"> <form action="public.php?return=<?php echo $return ?>" method="POST" class="otpform"> <input type="hidden" name="op" value="login"> <input type="hidden" name="login" value="<?php echo htmlspecialchars($login) ?>"> <input type="hidden" name="password" value="<?php echo htmlspecialchars($password) ?>"> + <input type="hidden" name="bw_limit" value="<?php echo htmlspecialchars($_POST["bw_limit"]) ?>"> + <input type="hidden" name="remember_me" value="<?php echo htmlspecialchars($_POST["remember_me"]) ?>"> + <input type="hidden" name="profile" value="<?php echo htmlspecialchars($_POST["profile"]) ?>"> <label><?php echo __("Please enter your one time password:") ?></label> <input autocomplete="off" size="6" name="otp" value=""/> diff --git a/plugins/cache_starred_images/init.php b/plugins/cache_starred_images/init.php new file mode 100644 index 000000000..4e5f2d4f7 --- /dev/null +++ b/plugins/cache_starred_images/init.php @@ -0,0 +1,193 @@ +<?php +class Cache_Starred_Images extends Plugin { + + private $host; + private $cache_dir; + + function about() { + return array(1.0, + "Automatically cache images in Starred articles", + "fox", + true); + } + + function init($host) { + $this->host = $host; + + $this->cache_dir = CACHE_DIR . "/starred-images/"; + + if (!is_dir($this->cache_dir)) { + mkdir($this->cache_dir); + } + + if (is_dir($this->cache_dir)) { + + if (!is_writable($this->cache_dir)) + chmod($this->cache_dir, 0777); + + if (is_writable($this->cache_dir)) { + $host->add_hook($host::HOOK_UPDATE_TASK, $this); + $host->add_hook($host::HOOK_HOUSE_KEEPING, $this); + $host->add_hook($host::HOOK_SANITIZE, $this); + } else { + user_error("Starred cache directory is not writable.", E_USER_WARNING); + } + + } else { + user_error("Unable to create starred cache directory.", E_USER_WARNING); + } + } + + function image() { + ob_end_clean(); + + $hash = basename($_REQUEST["hash"]); + + if ($hash) { + + $filename = $this->cache_dir . "/" . $hash . '.png'; + + if (file_exists($filename)) { + /* See if we can use X-Sendfile */ + $xsendfile = false; + if (function_exists('apache_get_modules') && + array_search('mod_xsendfile', apache_get_modules())) + $xsendfile = true; + + if ($xsendfile) { + header("X-Sendfile: $filename"); + header("Content-type: application/octet-stream"); + header('Content-Disposition: attachment; filename="' . basename($filename) . '"'); + } else { + header("Content-type: image/png"); + $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)). " GMT"; + header("Last-Modified: $stamp", true); + readfile($filename); + } + } else { + header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); + echo "File not found."; + } + } + } + + function hook_house_keeping() { + $files = glob($this->cache_dir . "/*.png"); + + $last_article_id = 0; + $article_exists = 1; + + foreach ($files as $file) { + list ($article_id, $hash) = explode("-", basename($file)); + + if ($article_id != $last_article_id) { + $last_article_id = $article_id; + $article_id = db_escape_string($article_id); + + $result = db_query("SELECT id FROM ttrss_entries WHERE id = " . $article_id); + + $article_exists = db_num_rows($result) > 0; + } + + if (!$article_exists) { + unlink($file); + } + } + } + + function hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) { + $xpath = new DOMXpath($doc); + + if ($article_id) { + $entries = $xpath->query('(//img[@src])'); + + foreach ($entries as $entry) { + if ($entry->hasAttribute('src')) { + $src = rewrite_relative_url($site_url, $entry->getAttribute('src')); + + $local_filename = $this->cache_dir . $article_id . "-" . sha1($src) . ".png"; + + if (file_exists($local_filename)) { + $entry->setAttribute("src", get_self_url_prefix() . + "/backend.php?op=pluginhandler&plugin=cache_starred_images&method=image&hash=" . + $article_id . "-" . sha1($src)); + } + + } + } + } + + return $doc; + } + + function hook_update_task() { + $result = db_query("SELECT content, ttrss_user_entries.owner_uid, link, site_url, ttrss_entries.id, plugin_data + FROM ttrss_entries, ttrss_user_entries LEFT JOIN ttrss_feeds ON + (ttrss_user_entries.feed_id = ttrss_feeds.id) + WHERE ref_id = ttrss_entries.id AND + marked = true AND + UPPER(content) LIKE '%<IMG%' AND + site_url != '' AND + plugin_data NOT LIKE '%starred_cache_images%' + ORDER BY ".sql_random_function()." LIMIT 100"); + + + while ($line = db_fetch_assoc($result)) { + if ($line["site_url"]) { + $success = $this->cache_article_images($line["content"], $line["site_url"], $line["owner_uid"], $line["id"]); + + if ($success) { + $plugin_data = db_escape_string("starred_cache_images,${line['owner_uid']}:" . $line["plugin_data"]); + + db_query("UPDATE ttrss_entries SET plugin_data = '$plugin_data' WHERE id = " . $line["id"]); + } + } + } + } + + function cache_article_images($content, $site_url, $owner_uid, $article_id) { + libxml_use_internal_errors(true); + + $charset_hack = '<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + </head>'; + + $doc = new DOMDocument(); + $doc->loadHTML($charset_hack . $content); + $xpath = new DOMXPath($doc); + + $entries = $xpath->query('(//img[@src])'); + + $success = false; + $has_images = false; + + foreach ($entries as $entry) { + if ($entry->hasAttribute('src')) { + $has_images = true; + $src = rewrite_relative_url($site_url, $entry->getAttribute('src')); + + $local_filename = $this->cache_dir . $article_id . "-" . sha1($src) . ".png"; + + //_debug("cache_images: downloading: $src to $local_filename"); + + if (!file_exists($local_filename)) { + $file_content = fetch_file_contents($src); + + if ($file_content && strlen($file_content) > 0) { + file_put_contents($local_filename, $file_content); + $success = true; + } + } else { + $success = true; + } + } + } + + return $success || !$has_images; + } + + function api_version() { + return 2; + } +} +?> diff --git a/plugins/close_button/button.png b/plugins/close_button/button.png Binary files differindex b04aed796..1514d51a3 100644 --- a/plugins/close_button/button.png +++ b/plugins/close_button/button.png diff --git a/plugins/googlereaderimport/init.php b/plugins/googlereaderimport/init.php index 2e22161b9..dcb335a0d 100644 --- a/plugins/googlereaderimport/init.php +++ b/plugins/googlereaderimport/init.php @@ -114,6 +114,7 @@ class GoogleReaderImport extends Plugin { $guid = db_escape_string(mb_substr($item['id'], 0, 250)); $title = db_escape_string($item['title']); $updated = date('Y-m-d h:i:s', $item['updated']); + $last_marked = date('Y-m-d h:i:s', mb_substr($item['crawlTimeMsec'], 0, 10)); $link = ''; $content = ''; $author = db_escape_string($item['author']); @@ -165,7 +166,7 @@ class GoogleReaderImport extends Plugin { $imported += (int) $this->create_article($owner_uid, $guid, $title, $link, $updated, $content, $author, $sql_set_marked, $tags, - $orig_feed_data); + $orig_feed_data, $last_marked); if ($file && $processed % 25 == 0) { _debug("processed $processed articles..."); @@ -196,7 +197,7 @@ class GoogleReaderImport extends Plugin { } // expects ESCAPED data - private function create_article($owner_uid, $guid, $title, $link, $updated, $content, $author, $marked, $tags, $orig_feed_data) { + private function create_article($owner_uid, $guid, $title, $link, $updated, $content, $author, $marked, $tags, $orig_feed_data, $last_marked) { if (!$guid) $guid = sha1($link); @@ -299,7 +300,7 @@ class GoogleReaderImport extends Plugin { (ref_id, uuid, feed_id, orig_feed_id, owner_uid, marked, tag_cache, label_cache, last_read, note, unread, last_marked) VALUES - ('$ref_id', '', NULL, $orig_feed_id, $owner_uid, $marked, '', '', NOW(), '', false, NOW())"); + ('$ref_id', '', NULL, $orig_feed_id, $owner_uid, $marked, '', '', '$last_marked', '', false, '$last_marked')"); $result = db_query("SELECT int_id FROM ttrss_user_entries, ttrss_entries WHERE owner_uid = $owner_uid AND ref_id = id AND ref_id = $ref_id"); diff --git a/plugins/import_export/init.php b/plugins/import_export/init.php index d4bdec826..d185210ee 100644 --- a/plugins/import_export/init.php +++ b/plugins/import_export/init.php @@ -381,9 +381,9 @@ class Import_Export extends Plugin implements IHandler { print "<p>" . __("Finished: "). - vsprintf(ngettext("%d article processed, ", "%d articles processed, ", $num_processed), $num_processed). - vsprintf(ngettext("%d imported, ", "%d imported, ", $num_imported), $num_imported). - vsprintf(ngettext("%d feed created.", "%d feeds created.", $num_feeds_created), $num_feeds_created). + vsprintf(_ngettext("%d article processed, ", "%d articles processed, ", $num_processed), $num_processed). + vsprintf(_ngettext("%d imported, ", "%d imported, ", $num_imported), $num_imported). + vsprintf(_ngettext("%d feed created.", "%d feeds created.", $num_feeds_created), $num_feeds_created). "</p>"; } else { diff --git a/plugins/instances/init.php b/plugins/instances/init.php index aac28196f..2de0e5fd2 100644 --- a/plugins/instances/init.php +++ b/plugins/instances/init.php @@ -407,7 +407,7 @@ class Instances extends Plugin implements IHandler { print "<hr/>"; - $access_key = sha1(uniqid(rand(), true)); + $access_key = uniqid(rand(), true); /* Access key */ @@ -439,7 +439,7 @@ class Instances extends Plugin implements IHandler { } function genHash() { - $hash = sha1(uniqid(rand(), true)); + $hash = uniqid(base_convert(rand(), 10, 36)); print json_encode(array("hash" => $hash)); } diff --git a/plugins/mail/init.php b/plugins/mail/init.php index 5b61c2355..e1da315c3 100644 --- a/plugins/mail/init.php +++ b/plugins/mail/init.php @@ -13,12 +13,61 @@ class Mail extends Plugin { $this->host = $host; $host->add_hook($host::HOOK_ARTICLE_BUTTON, $this); + $host->add_hook($host::HOOK_PREFS_TAB, $this); } function get_js() { return file_get_contents(dirname(__FILE__) . "/mail.js"); } + function save() { + $addresslist = db_escape_string($_POST["addresslist"]); + + $this->host->set($this, "addresslist", $addresslist); + + echo __("Mail addresses saved."); + } + + function hook_prefs_tab($args) { + if ($args != "prefPrefs") return; + + print "<div dojoType=\"dijit.layout.AccordionPane\" title=\"".__('Mail plugin')."\">"; + + print "<p>" . __("You can set predefined email addressed here (comma-separated list):") . "</p>"; + + print "<form dojoType=\"dijit.form.Form\">"; + + print "<script type=\"dojo/method\" event=\"onSubmit\" args=\"evt\"> + evt.preventDefault(); + if (this.validate()) { + console.log(dojo.objectToQuery(this.getValues())); + new Ajax.Request('backend.php', { + parameters: dojo.objectToQuery(this.getValues()), + onComplete: function(transport) { + notify_info(transport.responseText); + } + }); + //this.reset(); + } + </script>"; + + print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"op\" value=\"pluginhandler\">"; + print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"method\" value=\"save\">"; + print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"plugin\" value=\"mail\">"; + + $addresslist = $this->host->get($this, "addresslist"); + + print "<textarea dojoType=\"dijit.form.SimpleTextarea\" style='font-size : 12px; width : 50%' rows=\"3\" + name='addresslist'>$addresslist</textarea>"; + + print "<p><button dojoType=\"dijit.form.Button\" type=\"submit\">". + __("Save")."</button>"; + + print "</form>"; + + print "</div>"; + } + function hook_article_button($line) { return "<img src=\"plugins/mail/mail.png\" class='tagsPic' style=\"cursor : pointer\" @@ -48,7 +97,6 @@ class Mail extends Plugin { require_once "lib/MiniTemplator.class.php"; $tpl = new MiniTemplator; - $tpl_t = new MiniTemplator; $tpl->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 "<table width='100%'><tr><td>"; - print __('From:'); - - print "</td><td>"; - - print "<input dojoType=\"dijit.form.TextBox\" disabled=\"1\" style=\"width : 30em;\" - value=\"$user_name <$user_email>\">"; - - print "</td></tr><tr><td>"; + $addresslist = explode(",", $this->host->get($this, "addresslist")); print __('To:'); print "</td><td>"; - print "<input dojoType=\"dijit.form.ValidationTextBox\" required=\"true\" +/* print "<input dojoType=\"dijit.form.ValidationTextBox\" required=\"true\" style=\"width : 30em;\" - name=\"destination\" id=\"emailArticleDlg_destination\">"; + name=\"destination\" id=\"emailArticleDlg_destination\">"; */ + + print_select("destination", "", $addresslist, 'style="width: 30em" dojoType="dijit.form.ComboBox"'); - print "<div class=\"autocomplete\" id=\"emailArticleDlg_dst_choices\" - style=\"z-index: 30; display : none\"></div>"; +/* print "<div class=\"autocomplete\" id=\"emailArticleDlg_dst_choices\" + style=\"z-index: 30; display : none\"></div>"; */ print "</td></tr><tr><td>"; @@ -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 "<ul>"; @@ -174,7 +217,7 @@ class Mail extends Plugin { } print "</ul>"; - } + } */ 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 Binary files differindex 4d3fe7751..7348aed77 100644 --- a/plugins/mail/mail.png +++ b/plugins/mail/mail.png 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 Binary files differindex fcdcbd604..2c49f78a6 100644 --- a/plugins/mailto/mail.png +++ b/plugins/mailto/mail.png 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 = "<img - src=\"images/mark_set.svg\" + src=\"images/mark_set.png\" class=\"markedPic\" alt=\"Unstar article\" onclick='toggleMark($id)'>"; } else { $marked_pic = "<img - src=\"images/mark_unset.svg\" + src=\"images/mark_unset.png\" class=\"markedPic\" alt=\"Star article\" onclick='toggleMark($id)'>"; } 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 @@ +<?php +class No_Iframes extends Plugin { + private $host; + + function about() { + return array(1.0, + "Remove embedded iframes", + "fox"); + } + + function init($host) { + $this->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 @@ +<?php +class No_Title_Counters extends Plugin { + private $host; + + function about() { + return array(1.0, + "Remove counters from window title (prevents tab flashing on new articles)", + "fox"); + } + + function init($host) { + $this->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 @@ +<?php +class No_URL_Hashes extends Plugin { + private $host; + + function about() { + return array(1.0, + "Disable URL hash usage (e.g. #f=10, etc)", + "fox"); + } + + function init($host) { + $this->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 Binary files differindex 7f9f3fc3f..244e6ca04 100644 --- a/plugins/note/note.png +++ b/plugins/note/note.png 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 @@ +<?php + +class Search_Sphinx extends Plugin { + function about() { + return array(1.0, + "Delegate searching for articles to Sphinx (don't forget to set options in config.php)", + "hoelzro", + true); + } + + function init($host) { + $host->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/plugins/search_sphinx/sphinxapi.php b/plugins/search_sphinx/sphinxapi.php new file mode 100644 index 000000000..90643d3c7 --- /dev/null +++ b/plugins/search_sphinx/sphinxapi.php @@ -0,0 +1,1691 @@ +<?php + +// +// $Id: sphinxapi.php 2758 2011-04-04 11:10:44Z kevg $ +// + +// +// Copyright (c) 2001-2011, Andrew Aksyonoff +// Copyright (c) 2008-2011, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +///////////////////////////////////////////////////////////////////////////// +// PHP version of Sphinx searchd client (PHP API) +///////////////////////////////////////////////////////////////////////////// + +/// known searchd commands +define ( "SEARCHD_COMMAND_SEARCH", 0 ); +define ( "SEARCHD_COMMAND_EXCERPT", 1 ); +define ( "SEARCHD_COMMAND_UPDATE", 2 ); +define ( "SEARCHD_COMMAND_KEYWORDS", 3 ); +define ( "SEARCHD_COMMAND_PERSIST", 4 ); +define ( "SEARCHD_COMMAND_STATUS", 5 ); +define ( "SEARCHD_COMMAND_FLUSHATTRS", 7 ); + +/// current client-side command implementation versions +define ( "VER_COMMAND_SEARCH", 0x118 ); +define ( "VER_COMMAND_EXCERPT", 0x103 ); +define ( "VER_COMMAND_UPDATE", 0x102 ); +define ( "VER_COMMAND_KEYWORDS", 0x100 ); +define ( "VER_COMMAND_STATUS", 0x100 ); +define ( "VER_COMMAND_QUERY", 0x100 ); +define ( "VER_COMMAND_FLUSHATTRS", 0x100 ); + +/// known searchd status codes +define ( "SEARCHD_OK", 0 ); +define ( "SEARCHD_ERROR", 1 ); +define ( "SEARCHD_RETRY", 2 ); +define ( "SEARCHD_WARNING", 3 ); + +/// known match modes +define ( "SPH_MATCH_ALL", 0 ); +define ( "SPH_MATCH_ANY", 1 ); +define ( "SPH_MATCH_PHRASE", 2 ); +define ( "SPH_MATCH_BOOLEAN", 3 ); +define ( "SPH_MATCH_EXTENDED", 4 ); +define ( "SPH_MATCH_FULLSCAN", 5 ); +define ( "SPH_MATCH_EXTENDED2", 6 ); // extended engine V2 (TEMPORARY, WILL BE REMOVED) + +/// known ranking modes (ext2 only) +define ( "SPH_RANK_PROXIMITY_BM25", 0 ); ///< default mode, phrase proximity major factor and BM25 minor one +define ( "SPH_RANK_BM25", 1 ); ///< statistical mode, BM25 ranking only (faster but worse quality) +define ( "SPH_RANK_NONE", 2 ); ///< no ranking, all matches get a weight of 1 +define ( "SPH_RANK_WORDCOUNT", 3 ); ///< simple word-count weighting, rank is a weighted sum of per-field keyword occurence counts +define ( "SPH_RANK_PROXIMITY", 4 ); +define ( "SPH_RANK_MATCHANY", 5 ); +define ( "SPH_RANK_FIELDMASK", 6 ); +define ( "SPH_RANK_SPH04", 7 ); +define ( "SPH_RANK_TOTAL", 8 ); + +/// known sort modes +define ( "SPH_SORT_RELEVANCE", 0 ); +define ( "SPH_SORT_ATTR_DESC", 1 ); +define ( "SPH_SORT_ATTR_ASC", 2 ); +define ( "SPH_SORT_TIME_SEGMENTS", 3 ); +define ( "SPH_SORT_EXTENDED", 4 ); +define ( "SPH_SORT_EXPR", 5 ); + +/// known filter types +define ( "SPH_FILTER_VALUES", 0 ); +define ( "SPH_FILTER_RANGE", 1 ); +define ( "SPH_FILTER_FLOATRANGE", 2 ); + +/// known attribute types +define ( "SPH_ATTR_INTEGER", 1 ); +define ( "SPH_ATTR_TIMESTAMP", 2 ); +define ( "SPH_ATTR_ORDINAL", 3 ); +define ( "SPH_ATTR_BOOL", 4 ); +define ( "SPH_ATTR_FLOAT", 5 ); +define ( "SPH_ATTR_BIGINT", 6 ); +define ( "SPH_ATTR_STRING", 7 ); +define ( "SPH_ATTR_MULTI", 0x40000000 ); + +/// known grouping functions +define ( "SPH_GROUPBY_DAY", 0 ); +define ( "SPH_GROUPBY_WEEK", 1 ); +define ( "SPH_GROUPBY_MONTH", 2 ); +define ( "SPH_GROUPBY_YEAR", 3 ); +define ( "SPH_GROUPBY_ATTR", 4 ); +define ( "SPH_GROUPBY_ATTRPAIR", 5 ); + +// important properties of PHP's integers: +// - always signed (one bit short of PHP_INT_SIZE) +// - conversion from string to int is saturated +// - float is double +// - div converts arguments to floats +// - mod converts arguments to ints + +// the packing code below works as follows: +// - when we got an int, just pack it +// if performance is a problem, this is the branch users should aim for +// +// - otherwise, we got a number in string form +// this might be due to different reasons, but we assume that this is +// because it didn't fit into PHP int +// +// - factor the string into high and low ints for packing +// - if we have bcmath, then it is used +// - if we don't, we have to do it manually (this is the fun part) +// +// - x64 branch does factoring using ints +// - x32 (ab)uses floats, since we can't fit unsigned 32-bit number into an int +// +// unpacking routines are pretty much the same. +// - return ints if we can +// - otherwise format number into a string + +/// pack 64-bit signed +function sphPackI64 ( $v ) +{ + assert ( is_numeric($v) ); + + // x64 + if ( PHP_INT_SIZE>=8 ) + { + $v = (int)$v; + return pack ( "NN", $v>>32, $v&0xFFFFFFFF ); + } + + // x32, int + if ( is_int($v) ) + return pack ( "NN", $v < 0 ? -1 : 0, $v ); + + // x32, bcmath + if ( function_exists("bcmul") ) + { + if ( bccomp ( $v, 0 ) == -1 ) + $v = bcadd ( "18446744073709551616", $v ); + $h = bcdiv ( $v, "4294967296", 0 ); + $l = bcmod ( $v, "4294967296" ); + return pack ( "NN", (float)$h, (float)$l ); // conversion to float is intentional; int would lose 31st bit + } + + // x32, no-bcmath + $p = max(0, strlen($v) - 13); + $lo = abs((float)substr($v, $p)); + $hi = abs((float)substr($v, 0, $p)); + + $m = $lo + $hi*1316134912.0; // (10 ^ 13) % (1 << 32) = 1316134912 + $q = floor($m/4294967296.0); + $l = $m - ($q*4294967296.0); + $h = $hi*2328.0 + $q; // (10 ^ 13) / (1 << 32) = 2328 + + if ( $v<0 ) + { + if ( $l==0 ) + $h = 4294967296.0 - $h; + else + { + $h = 4294967295.0 - $h; + $l = 4294967296.0 - $l; + } + } + return pack ( "NN", $h, $l ); +} + +/// pack 64-bit unsigned +function sphPackU64 ( $v ) +{ + assert ( is_numeric($v) ); + + // x64 + if ( PHP_INT_SIZE>=8 ) + { + assert ( $v>=0 ); + + // x64, int + if ( is_int($v) ) + return pack ( "NN", $v>>32, $v&0xFFFFFFFF ); + + // x64, bcmath + if ( function_exists("bcmul") ) + { + $h = bcdiv ( $v, 4294967296, 0 ); + $l = bcmod ( $v, 4294967296 ); + return pack ( "NN", $h, $l ); + } + + // x64, no-bcmath + $p = max ( 0, strlen($v) - 13 ); + $lo = (int)substr ( $v, $p ); + $hi = (int)substr ( $v, 0, $p ); + + $m = $lo + $hi*1316134912; + $l = $m % 4294967296; + $h = $hi*2328 + (int)($m/4294967296); + + return pack ( "NN", $h, $l ); + } + + // x32, int + if ( is_int($v) ) + return pack ( "NN", 0, $v ); + + // x32, bcmath + if ( function_exists("bcmul") ) + { + $h = bcdiv ( $v, "4294967296", 0 ); + $l = bcmod ( $v, "4294967296" ); + return pack ( "NN", (float)$h, (float)$l ); // conversion to float is intentional; int would lose 31st bit + } + + // x32, no-bcmath + $p = max(0, strlen($v) - 13); + $lo = (float)substr($v, $p); + $hi = (float)substr($v, 0, $p); + + $m = $lo + $hi*1316134912.0; + $q = floor($m / 4294967296.0); + $l = $m - ($q * 4294967296.0); + $h = $hi*2328.0 + $q; + + return pack ( "NN", $h, $l ); +} + +// unpack 64-bit unsigned +function sphUnpackU64 ( $v ) +{ + list ( $hi, $lo ) = array_values ( unpack ( "N*N*", $v ) ); + + if ( PHP_INT_SIZE>=8 ) + { + if ( $hi<0 ) $hi += (1<<32); // because php 5.2.2 to 5.2.5 is totally fucked up again + if ( $lo<0 ) $lo += (1<<32); + + // x64, int + if ( $hi<=2147483647 ) + return ($hi<<32) + $lo; + + // x64, bcmath + if ( function_exists("bcmul") ) + return bcadd ( $lo, bcmul ( $hi, "4294967296" ) ); + + // x64, no-bcmath + $C = 100000; + $h = ((int)($hi / $C) << 32) + (int)($lo / $C); + $l = (($hi % $C) << 32) + ($lo % $C); + if ( $l>$C ) + { + $h += (int)($l / $C); + $l = $l % $C; + } + + if ( $h==0 ) + return $l; + return sprintf ( "%d%05d", $h, $l ); + } + + // x32, int + if ( $hi==0 ) + { + if ( $lo>0 ) + return $lo; + return sprintf ( "%u", $lo ); + } + + $hi = sprintf ( "%u", $hi ); + $lo = sprintf ( "%u", $lo ); + + // x32, bcmath + if ( function_exists("bcmul") ) + return bcadd ( $lo, bcmul ( $hi, "4294967296" ) ); + + // x32, no-bcmath + $hi = (float)$hi; + $lo = (float)$lo; + + $q = floor($hi/10000000.0); + $r = $hi - $q*10000000.0; + $m = $lo + $r*4967296.0; + $mq = floor($m/10000000.0); + $l = $m - $mq*10000000.0; + $h = $q*4294967296.0 + $r*429.0 + $mq; + + $h = sprintf ( "%.0f", $h ); + $l = sprintf ( "%07.0f", $l ); + if ( $h=="0" ) + return sprintf( "%.0f", (float)$l ); + return $h . $l; +} + +// unpack 64-bit signed +function sphUnpackI64 ( $v ) +{ + list ( $hi, $lo ) = array_values ( unpack ( "N*N*", $v ) ); + + // x64 + if ( PHP_INT_SIZE>=8 ) + { + if ( $hi<0 ) $hi += (1<<32); // because php 5.2.2 to 5.2.5 is totally fucked up again + if ( $lo<0 ) $lo += (1<<32); + + return ($hi<<32) + $lo; + } + + // x32, int + if ( $hi==0 ) + { + if ( $lo>0 ) + return $lo; + return sprintf ( "%u", $lo ); + } + // x32, int + elseif ( $hi==-1 ) + { + if ( $lo<0 ) + return $lo; + return sprintf ( "%.0f", $lo - 4294967296.0 ); + } + + $neg = ""; + $c = 0; + if ( $hi<0 ) + { + $hi = ~$hi; + $lo = ~$lo; + $c = 1; + $neg = "-"; + } + + $hi = sprintf ( "%u", $hi ); + $lo = sprintf ( "%u", $lo ); + + // x32, bcmath + if ( function_exists("bcmul") ) + return $neg . bcadd ( bcadd ( $lo, bcmul ( $hi, "4294967296" ) ), $c ); + + // x32, no-bcmath + $hi = (float)$hi; + $lo = (float)$lo; + + $q = floor($hi/10000000.0); + $r = $hi - $q*10000000.0; + $m = $lo + $r*4967296.0; + $mq = floor($m/10000000.0); + $l = $m - $mq*10000000.0 + $c; + $h = $q*4294967296.0 + $r*429.0 + $mq; + if ( $l==10000000 ) + { + $l = 0; + $h += 1; + } + + $h = sprintf ( "%.0f", $h ); + $l = sprintf ( "%07.0f", $l ); + if ( $h=="0" ) + return $neg . sprintf( "%.0f", (float)$l ); + return $neg . $h . $l; +} + + +function sphFixUint ( $value ) +{ + if ( PHP_INT_SIZE>=8 ) + { + // x64 route, workaround broken unpack() in 5.2.2+ + if ( $value<0 ) $value += (1<<32); + return $value; + } + else + { + // x32 route, workaround php signed/unsigned braindamage + return sprintf ( "%u", $value ); + } +} + + +/// sphinx searchd client class +class SphinxClient +{ + var $_host; ///< searchd host (default is "localhost") + var $_port; ///< searchd port (default is 9312) + var $_offset; ///< how many records to seek from result-set start (default is 0) + var $_limit; ///< how many records to return from result-set starting at offset (default is 20) + var $_mode; ///< query matching mode (default is SPH_MATCH_ALL) + var $_weights; ///< per-field weights (default is 1 for all fields) + var $_sort; ///< match sorting mode (default is SPH_SORT_RELEVANCE) + var $_sortby; ///< attribute to sort by (defualt is "") + var $_min_id; ///< min ID to match (default is 0, which means no limit) + var $_max_id; ///< max ID to match (default is 0, which means no limit) + var $_filters; ///< search filters + var $_groupby; ///< group-by attribute name + var $_groupfunc; ///< group-by function (to pre-process group-by attribute value with) + var $_groupsort; ///< group-by sorting clause (to sort groups in result set with) + var $_groupdistinct;///< group-by count-distinct attribute + var $_maxmatches; ///< max matches to retrieve + var $_cutoff; ///< cutoff to stop searching at (default is 0) + var $_retrycount; ///< distributed retries count + var $_retrydelay; ///< distributed retries delay + var $_anchor; ///< geographical anchor point + var $_indexweights; ///< per-index weights + var $_ranker; ///< ranking mode (default is SPH_RANK_PROXIMITY_BM25) + var $_maxquerytime; ///< max query time, milliseconds (default is 0, do not limit) + var $_fieldweights; ///< per-field-name weights + var $_overrides; ///< per-query attribute values overrides + var $_select; ///< select-list (attributes or expressions, with optional aliases) + + var $_error; ///< last error message + var $_warning; ///< last warning message + var $_connerror; ///< connection error vs remote error flag + + var $_reqs; ///< requests array for multi-query + var $_mbenc; ///< stored mbstring encoding + var $_arrayresult; ///< whether $result["matches"] should be a hash or an array + var $_timeout; ///< connect timeout + + ///////////////////////////////////////////////////////////////////////////// + // common stuff + ///////////////////////////////////////////////////////////////////////////// + + /// create a new client object and fill defaults + function SphinxClient () + { + // per-client-object settings + $this->_host = "localhost"; + $this->_port = 9312; + $this->_path = false; + $this->_socket = false; + + // per-query settings + $this->_offset = 0; + $this->_limit = 20; + $this->_mode = SPH_MATCH_ALL; + $this->_weights = array (); + $this->_sort = SPH_SORT_RELEVANCE; + $this->_sortby = ""; + $this->_min_id = 0; + $this->_max_id = 0; + $this->_filters = array (); + $this->_groupby = ""; + $this->_groupfunc = SPH_GROUPBY_DAY; + $this->_groupsort = "@group desc"; + $this->_groupdistinct= ""; + $this->_maxmatches = 1000; + $this->_cutoff = 0; + $this->_retrycount = 0; + $this->_retrydelay = 0; + $this->_anchor = array (); + $this->_indexweights= array (); + $this->_ranker = SPH_RANK_PROXIMITY_BM25; + $this->_maxquerytime= 0; + $this->_fieldweights= array(); + $this->_overrides = array(); + $this->_select = "*"; + + $this->_error = ""; // per-reply fields (for single-query case) + $this->_warning = ""; + $this->_connerror = false; + + $this->_reqs = array (); // requests storage (for multi-query case) + $this->_mbenc = ""; + $this->_arrayresult = false; + $this->_timeout = 0; + } + + function __destruct() + { + if ( $this->_socket !== false ) + fclose ( $this->_socket ); + } + + /// get last error message (string) + function GetLastError () + { + return $this->_error; + } + + /// get last warning message (string) + function GetLastWarning () + { + return $this->_warning; + } + + /// get last error flag (to tell network connection errors from searchd errors or broken responses) + function IsConnectError() + { + return $this->_connerror; + } + + /// set searchd host name (string) and port (integer) + function SetServer ( $host, $port = 0 ) + { + assert ( is_string($host) ); + if ( $host[0] == '/') + { + $this->_path = 'unix://' . $host; + return; + } + if ( substr ( $host, 0, 7 )=="unix://" ) + { + $this->_path = $host; + return; + } + + assert ( is_int($port) ); + $this->_host = $host; + $this->_port = $port; + $this->_path = ''; + + } + + /// set server connection timeout (0 to remove) + function SetConnectTimeout ( $timeout ) + { + assert ( is_numeric($timeout) ); + $this->_timeout = $timeout; + } + + + function _Send ( $handle, $data, $length ) + { + if ( feof($handle) || fwrite ( $handle, $data, $length ) !== $length ) + { + $this->_error = 'connection unexpectedly closed (timed out?)'; + $this->_connerror = true; + return false; + } + return true; + } + + ///////////////////////////////////////////////////////////////////////////// + + /// enter mbstring workaround mode + function _MBPush () + { + $this->_mbenc = ""; + if ( ini_get ( "mbstring.func_overload" ) & 2 ) + { + $this->_mbenc = mb_internal_encoding(); + mb_internal_encoding ( "latin1" ); + } + } + + /// leave mbstring workaround mode + function _MBPop () + { + if ( $this->_mbenc ) + mb_internal_encoding ( $this->_mbenc ); + } + + /// connect to searchd server + function _Connect () + { + if ( $this->_socket!==false ) + { + // we are in persistent connection mode, so we have a socket + // however, need to check whether it's still alive + if ( !@feof ( $this->_socket ) ) + return $this->_socket; + + // force reopen + $this->_socket = false; + } + + $errno = 0; + $errstr = ""; + $this->_connerror = false; + + if ( $this->_path ) + { + $host = $this->_path; + $port = 0; + } + else + { + $host = $this->_host; + $port = $this->_port; + } + + if ( $this->_timeout<=0 ) + $fp = @fsockopen ( $host, $port, $errno, $errstr ); + else + $fp = @fsockopen ( $host, $port, $errno, $errstr, $this->_timeout ); + + if ( !$fp ) + { + if ( $this->_path ) + $location = $this->_path; + else + $location = "{$this->_host}:{$this->_port}"; + + $errstr = trim ( $errstr ); + $this->_error = "connection to $location failed (errno=$errno, msg=$errstr)"; + $this->_connerror = true; + return false; + } + + // send my version + // this is a subtle part. we must do it before (!) reading back from searchd. + // because otherwise under some conditions (reported on FreeBSD for instance) + // TCP stack could throttle write-write-read pattern because of Nagle. + if ( !$this->_Send ( $fp, pack ( "N", 1 ), 4 ) ) + { + fclose ( $fp ); + $this->_error = "failed to send client protocol version"; + return false; + } + + // check version + list(,$v) = unpack ( "N*", fread ( $fp, 4 ) ); + $v = (int)$v; + if ( $v<1 ) + { + fclose ( $fp ); + $this->_error = "expected searchd protocol version 1+, got version '$v'"; + return false; + } + + return $fp; + } + + /// get and check response packet from searchd server + function _GetResponse ( $fp, $client_ver ) + { + $response = ""; + $len = 0; + + $header = fread ( $fp, 8 ); + if ( strlen($header)==8 ) + { + list ( $status, $ver, $len ) = array_values ( unpack ( "n2a/Nb", $header ) ); + $left = $len; + while ( $left>0 && !feof($fp) ) + { + $chunk = fread ( $fp, min ( 8192, $left ) ); + if ( $chunk ) + { + $response .= $chunk; + $left -= strlen($chunk); + } + } + } + if ( $this->_socket === false ) + fclose ( $fp ); + + // check response + $read = strlen ( $response ); + if ( !$response || $read!=$len ) + { + $this->_error = $len + ? "failed to read searchd response (status=$status, ver=$ver, len=$len, read=$read)" + : "received zero-sized searchd response"; + return false; + } + + // check status + if ( $status==SEARCHD_WARNING ) + { + list(,$wlen) = unpack ( "N*", substr ( $response, 0, 4 ) ); + $this->_warning = substr ( $response, 4, $wlen ); + return substr ( $response, 4+$wlen ); + } + if ( $status==SEARCHD_ERROR ) + { + $this->_error = "searchd error: " . substr ( $response, 4 ); + return false; + } + if ( $status==SEARCHD_RETRY ) + { + $this->_error = "temporary searchd error: " . substr ( $response, 4 ); + return false; + } + if ( $status!=SEARCHD_OK ) + { + $this->_error = "unknown status code '$status'"; + return false; + } + + // check version + if ( $ver<$client_ver ) + { + $this->_warning = sprintf ( "searchd command v.%d.%d older than client's v.%d.%d, some options might not work", + $ver>>8, $ver&0xff, $client_ver>>8, $client_ver&0xff ); + } + + return $response; + } + + ///////////////////////////////////////////////////////////////////////////// + // searching + ///////////////////////////////////////////////////////////////////////////// + + /// set offset and count into result set, + /// and optionally set max-matches and cutoff limits + function SetLimits ( $offset, $limit, $max=0, $cutoff=0 ) + { + assert ( is_int($offset) ); + assert ( is_int($limit) ); + assert ( $offset>=0 ); + assert ( $limit>0 ); + assert ( $max>=0 ); + $this->_offset = $offset; + $this->_limit = $limit; + if ( $max>0 ) + $this->_maxmatches = $max; + if ( $cutoff>0 ) + $this->_cutoff = $cutoff; + } + + /// set maximum query time, in milliseconds, per-index + /// integer, 0 means "do not limit" + function SetMaxQueryTime ( $max ) + { + assert ( is_int($max) ); + assert ( $max>=0 ); + $this->_maxquerytime = $max; + } + + /// set matching mode + function SetMatchMode ( $mode ) + { + assert ( $mode==SPH_MATCH_ALL + || $mode==SPH_MATCH_ANY + || $mode==SPH_MATCH_PHRASE + || $mode==SPH_MATCH_BOOLEAN + || $mode==SPH_MATCH_EXTENDED + || $mode==SPH_MATCH_FULLSCAN + || $mode==SPH_MATCH_EXTENDED2 ); + $this->_mode = $mode; + } + + /// set ranking mode + function SetRankingMode ( $ranker ) + { + assert ( $ranker>=0 && $ranker<SPH_RANK_TOTAL ); + $this->_ranker = $ranker; + } + + /// set matches sorting mode + function SetSortMode ( $mode, $sortby="" ) + { + assert ( + $mode==SPH_SORT_RELEVANCE || + $mode==SPH_SORT_ATTR_DESC || + $mode==SPH_SORT_ATTR_ASC || + $mode==SPH_SORT_TIME_SEGMENTS || + $mode==SPH_SORT_EXTENDED || + $mode==SPH_SORT_EXPR ); + assert ( is_string($sortby) ); + assert ( $mode==SPH_SORT_RELEVANCE || strlen($sortby)>0 ); + + $this->_sort = $mode; + $this->_sortby = $sortby; + } + + /// bind per-field weights by order + /// DEPRECATED; use SetFieldWeights() instead + function SetWeights ( $weights ) + { + assert ( is_array($weights) ); + foreach ( $weights as $weight ) + assert ( is_int($weight) ); + + $this->_weights = $weights; + } + + /// bind per-field weights by name + function SetFieldWeights ( $weights ) + { + assert ( is_array($weights) ); + foreach ( $weights as $name=>$weight ) + { + assert ( is_string($name) ); + assert ( is_int($weight) ); + } + $this->_fieldweights = $weights; + } + + /// bind per-index weights by name + function SetIndexWeights ( $weights ) + { + assert ( is_array($weights) ); + foreach ( $weights as $index=>$weight ) + { + assert ( is_string($index) ); + assert ( is_int($weight) ); + } + $this->_indexweights = $weights; + } + + /// set IDs range to match + /// only match records if document ID is beetwen $min and $max (inclusive) + function SetIDRange ( $min, $max ) + { + assert ( is_numeric($min) ); + assert ( is_numeric($max) ); + assert ( $min<=$max ); + $this->_min_id = $min; + $this->_max_id = $max; + } + + /// set values set filter + /// only match records where $attribute value is in given set + function SetFilter ( $attribute, $values, $exclude=false ) + { + assert ( is_string($attribute) ); + assert ( is_array($values) ); + assert ( count($values) ); + + if ( is_array($values) && count($values) ) + { + foreach ( $values as $value ) + assert ( is_numeric($value) ); + + $this->_filters[] = array ( "type"=>SPH_FILTER_VALUES, "attr"=>$attribute, "exclude"=>$exclude, "values"=>$values ); + } + } + + /// set range filter + /// only match records if $attribute value is beetwen $min and $max (inclusive) + function SetFilterRange ( $attribute, $min, $max, $exclude=false ) + { + assert ( is_string($attribute) ); + assert ( is_numeric($min) ); + assert ( is_numeric($max) ); + assert ( $min<=$max ); + + $this->_filters[] = array ( "type"=>SPH_FILTER_RANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max ); + } + + /// set float range filter + /// only match records if $attribute value is beetwen $min and $max (inclusive) + function SetFilterFloatRange ( $attribute, $min, $max, $exclude=false ) + { + assert ( is_string($attribute) ); + assert ( is_float($min) ); + assert ( is_float($max) ); + assert ( $min<=$max ); + + $this->_filters[] = array ( "type"=>SPH_FILTER_FLOATRANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max ); + } + + /// setup anchor point for geosphere distance calculations + /// required to use @geodist in filters and sorting + /// latitude and longitude must be in radians + function SetGeoAnchor ( $attrlat, $attrlong, $lat, $long ) + { + assert ( is_string($attrlat) ); + assert ( is_string($attrlong) ); + assert ( is_float($lat) ); + assert ( is_float($long) ); + + $this->_anchor = array ( "attrlat"=>$attrlat, "attrlong"=>$attrlong, "lat"=>$lat, "long"=>$long ); + } + + /// set grouping attribute and function + function SetGroupBy ( $attribute, $func, $groupsort="@group desc" ) + { + assert ( is_string($attribute) ); + assert ( is_string($groupsort) ); + assert ( $func==SPH_GROUPBY_DAY + || $func==SPH_GROUPBY_WEEK + || $func==SPH_GROUPBY_MONTH + || $func==SPH_GROUPBY_YEAR + || $func==SPH_GROUPBY_ATTR + || $func==SPH_GROUPBY_ATTRPAIR ); + + $this->_groupby = $attribute; + $this->_groupfunc = $func; + $this->_groupsort = $groupsort; + } + + /// set count-distinct attribute for group-by queries + function SetGroupDistinct ( $attribute ) + { + assert ( is_string($attribute) ); + $this->_groupdistinct = $attribute; + } + + /// set distributed retries count and delay + function SetRetries ( $count, $delay=0 ) + { + assert ( is_int($count) && $count>=0 ); + assert ( is_int($delay) && $delay>=0 ); + $this->_retrycount = $count; + $this->_retrydelay = $delay; + } + + /// set result set format (hash or array; hash by default) + /// PHP specific; needed for group-by-MVA result sets that may contain duplicate IDs + function SetArrayResult ( $arrayresult ) + { + assert ( is_bool($arrayresult) ); + $this->_arrayresult = $arrayresult; + } + + /// set attribute values override + /// there can be only one override per attribute + /// $values must be a hash that maps document IDs to attribute values + function SetOverride ( $attrname, $attrtype, $values ) + { + assert ( is_string ( $attrname ) ); + assert ( in_array ( $attrtype, array ( SPH_ATTR_INTEGER, SPH_ATTR_TIMESTAMP, SPH_ATTR_BOOL, SPH_ATTR_FLOAT, SPH_ATTR_BIGINT ) ) ); + assert ( is_array ( $values ) ); + + $this->_overrides[$attrname] = array ( "attr"=>$attrname, "type"=>$attrtype, "values"=>$values ); + } + + /// set select-list (attributes or expressions), SQL-like syntax + function SetSelect ( $select ) + { + assert ( is_string ( $select ) ); + $this->_select = $select; + } + + ////////////////////////////////////////////////////////////////////////////// + + /// clear all filters (for multi-queries) + function ResetFilters () + { + $this->_filters = array(); + $this->_anchor = array(); + } + + /// clear groupby settings (for multi-queries) + function ResetGroupBy () + { + $this->_groupby = ""; + $this->_groupfunc = SPH_GROUPBY_DAY; + $this->_groupsort = "@group desc"; + $this->_groupdistinct= ""; + } + + /// clear all attribute value overrides (for multi-queries) + function ResetOverrides () + { + $this->_overrides = array (); + } + + ////////////////////////////////////////////////////////////////////////////// + + /// connect to searchd server, run given search query through given indexes, + /// and return the search results + function Query ( $query, $index="*", $comment="" ) + { + assert ( empty($this->_reqs) ); + + $this->AddQuery ( $query, $index, $comment ); + $results = $this->RunQueries (); + $this->_reqs = array (); // just in case it failed too early + + if ( !is_array($results) ) + return false; // probably network error; error message should be already filled + + $this->_error = $results[0]["error"]; + $this->_warning = $results[0]["warning"]; + if ( $results[0]["status"]==SEARCHD_ERROR ) + return false; + else + return $results[0]; + } + + /// helper to pack floats in network byte order + function _PackFloat ( $f ) + { + $t1 = pack ( "f", $f ); // machine order + list(,$t2) = unpack ( "L*", $t1 ); // int in machine order + return pack ( "N", $t2 ); + } + + /// add query to multi-query batch + /// returns index into results array from RunQueries() call + function AddQuery ( $query, $index="*", $comment="" ) + { + // mbstring workaround + $this->_MBPush (); + + // build request + $req = pack ( "NNNNN", $this->_offset, $this->_limit, $this->_mode, $this->_ranker, $this->_sort ); // mode and limits + $req .= pack ( "N", strlen($this->_sortby) ) . $this->_sortby; + $req .= pack ( "N", strlen($query) ) . $query; // query itself + $req .= pack ( "N", count($this->_weights) ); // weights + foreach ( $this->_weights as $weight ) + $req .= pack ( "N", (int)$weight ); + $req .= pack ( "N", strlen($index) ) . $index; // indexes + $req .= pack ( "N", 1 ); // id64 range marker + $req .= sphPackU64 ( $this->_min_id ) . sphPackU64 ( $this->_max_id ); // id64 range + + // filters + $req .= pack ( "N", count($this->_filters) ); + foreach ( $this->_filters as $filter ) + { + $req .= pack ( "N", strlen($filter["attr"]) ) . $filter["attr"]; + $req .= pack ( "N", $filter["type"] ); + switch ( $filter["type"] ) + { + case SPH_FILTER_VALUES: + $req .= pack ( "N", count($filter["values"]) ); + foreach ( $filter["values"] as $value ) + $req .= sphPackI64 ( $value ); + break; + + case SPH_FILTER_RANGE: + $req .= sphPackI64 ( $filter["min"] ) . sphPackI64 ( $filter["max"] ); + break; + + case SPH_FILTER_FLOATRANGE: + $req .= $this->_PackFloat ( $filter["min"] ) . $this->_PackFloat ( $filter["max"] ); + break; + + default: + assert ( 0 && "internal error: unhandled filter type" ); + } + $req .= pack ( "N", $filter["exclude"] ); + } + + // group-by clause, max-matches count, group-sort clause, cutoff count + $req .= pack ( "NN", $this->_groupfunc, strlen($this->_groupby) ) . $this->_groupby; + $req .= pack ( "N", $this->_maxmatches ); + $req .= pack ( "N", strlen($this->_groupsort) ) . $this->_groupsort; + $req .= pack ( "NNN", $this->_cutoff, $this->_retrycount, $this->_retrydelay ); + $req .= pack ( "N", strlen($this->_groupdistinct) ) . $this->_groupdistinct; + + // anchor point + if ( empty($this->_anchor) ) + { + $req .= pack ( "N", 0 ); + } else + { + $a =& $this->_anchor; + $req .= pack ( "N", 1 ); + $req .= pack ( "N", strlen($a["attrlat"]) ) . $a["attrlat"]; + $req .= pack ( "N", strlen($a["attrlong"]) ) . $a["attrlong"]; + $req .= $this->_PackFloat ( $a["lat"] ) . $this->_PackFloat ( $a["long"] ); + } + + // per-index weights + $req .= pack ( "N", count($this->_indexweights) ); + foreach ( $this->_indexweights as $idx=>$weight ) + $req .= pack ( "N", strlen($idx) ) . $idx . pack ( "N", $weight ); + + // max query time + $req .= pack ( "N", $this->_maxquerytime ); + + // per-field weights + $req .= pack ( "N", count($this->_fieldweights) ); + foreach ( $this->_fieldweights as $field=>$weight ) + $req .= pack ( "N", strlen($field) ) . $field . pack ( "N", $weight ); + + // comment + $req .= pack ( "N", strlen($comment) ) . $comment; + + // attribute overrides + $req .= pack ( "N", count($this->_overrides) ); + foreach ( $this->_overrides as $key => $entry ) + { + $req .= pack ( "N", strlen($entry["attr"]) ) . $entry["attr"]; + $req .= pack ( "NN", $entry["type"], count($entry["values"]) ); + foreach ( $entry["values"] as $id=>$val ) + { + assert ( is_numeric($id) ); + assert ( is_numeric($val) ); + + $req .= sphPackU64 ( $id ); + switch ( $entry["type"] ) + { + case SPH_ATTR_FLOAT: $req .= $this->_PackFloat ( $val ); break; + case SPH_ATTR_BIGINT: $req .= sphPackI64 ( $val ); break; + default: $req .= pack ( "N", $val ); break; + } + } + } + + // select-list + $req .= pack ( "N", strlen($this->_select) ) . $this->_select; + + // mbstring workaround + $this->_MBPop (); + + // store request to requests array + $this->_reqs[] = $req; + return count($this->_reqs)-1; + } + + /// connect to searchd, run queries batch, and return an array of result sets + function RunQueries () + { + if ( empty($this->_reqs) ) + { + $this->_error = "no queries defined, issue AddQuery() first"; + return false; + } + + // mbstring workaround + $this->_MBPush (); + + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop (); + return false; + } + + // send query, get response + $nreqs = count($this->_reqs); + $req = join ( "", $this->_reqs ); + $len = 8+strlen($req); + $req = pack ( "nnNNN", SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $len, 0, $nreqs ) . $req; // add header + + if ( !( $this->_Send ( $fp, $req, $len+8 ) ) || + !( $response = $this->_GetResponse ( $fp, VER_COMMAND_SEARCH ) ) ) + { + $this->_MBPop (); + return false; + } + + // query sent ok; we can reset reqs now + $this->_reqs = array (); + + // parse and return response + return $this->_ParseSearchResponse ( $response, $nreqs ); + } + + /// parse and return search query (or queries) response + function _ParseSearchResponse ( $response, $nreqs ) + { + $p = 0; // current position + $max = strlen($response); // max position for checks, to protect against broken responses + + $results = array (); + for ( $ires=0; $ires<$nreqs && $p<$max; $ires++ ) + { + $results[] = array(); + $result =& $results[$ires]; + + $result["error"] = ""; + $result["warning"] = ""; + + // extract status + list(,$status) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $result["status"] = $status; + if ( $status!=SEARCHD_OK ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $message = substr ( $response, $p, $len ); $p += $len; + + if ( $status==SEARCHD_WARNING ) + { + $result["warning"] = $message; + } else + { + $result["error"] = $message; + continue; + } + } + + // read schema + $fields = array (); + $attrs = array (); + + list(,$nfields) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + while ( $nfields-->0 && $p<$max ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $fields[] = substr ( $response, $p, $len ); $p += $len; + } + $result["fields"] = $fields; + + list(,$nattrs) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + while ( $nattrs-->0 && $p<$max ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $attr = substr ( $response, $p, $len ); $p += $len; + list(,$type) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $attrs[$attr] = $type; + } + $result["attrs"] = $attrs; + + // read match count + list(,$count) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + list(,$id64) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + + // read matches + $idx = -1; + while ( $count-->0 && $p<$max ) + { + // index into result array + $idx++; + + // parse document id and weight + if ( $id64 ) + { + $doc = sphUnpackU64 ( substr ( $response, $p, 8 ) ); $p += 8; + list(,$weight) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + } + else + { + list ( $doc, $weight ) = array_values ( unpack ( "N*N*", + substr ( $response, $p, 8 ) ) ); + $p += 8; + $doc = sphFixUint($doc); + } + $weight = sprintf ( "%u", $weight ); + + // create match entry + if ( $this->_arrayresult ) + $result["matches"][$idx] = array ( "id"=>$doc, "weight"=>$weight ); + else + $result["matches"][$doc]["weight"] = $weight; + + // parse and create attributes + $attrvals = array (); + foreach ( $attrs as $attr=>$type ) + { + // handle 64bit ints + if ( $type==SPH_ATTR_BIGINT ) + { + $attrvals[$attr] = sphUnpackI64 ( substr ( $response, $p, 8 ) ); $p += 8; + continue; + } + + // handle floats + if ( $type==SPH_ATTR_FLOAT ) + { + list(,$uval) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + list(,$fval) = unpack ( "f*", pack ( "L", $uval ) ); + $attrvals[$attr] = $fval; + continue; + } + + // handle everything else as unsigned ints + list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + if ( $type & SPH_ATTR_MULTI ) + { + $attrvals[$attr] = array (); + $nvalues = $val; + while ( $nvalues-->0 && $p<$max ) + { + list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $attrvals[$attr][] = sphFixUint($val); + } + } else if ( $type==SPH_ATTR_STRING ) + { + $attrvals[$attr] = substr ( $response, $p, $val ); + $p += $val; + } else + { + $attrvals[$attr] = sphFixUint($val); + } + } + + if ( $this->_arrayresult ) + $result["matches"][$idx]["attrs"] = $attrvals; + else + $result["matches"][$doc]["attrs"] = $attrvals; + } + + list ( $total, $total_found, $msecs, $words ) = + array_values ( unpack ( "N*N*N*N*", substr ( $response, $p, 16 ) ) ); + $result["total"] = sprintf ( "%u", $total ); + $result["total_found"] = sprintf ( "%u", $total_found ); + $result["time"] = sprintf ( "%.3f", $msecs/1000 ); + $p += 16; + + while ( $words-->0 && $p<$max ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $word = substr ( $response, $p, $len ); $p += $len; + list ( $docs, $hits ) = array_values ( unpack ( "N*N*", substr ( $response, $p, 8 ) ) ); $p += 8; + $result["words"][$word] = array ( + "docs"=>sprintf ( "%u", $docs ), + "hits"=>sprintf ( "%u", $hits ) ); + } + } + + $this->_MBPop (); + return $results; + } + + ///////////////////////////////////////////////////////////////////////////// + // excerpts generation + ///////////////////////////////////////////////////////////////////////////// + + /// connect to searchd server, and generate exceprts (snippets) + /// of given documents for given query. returns false on failure, + /// an array of snippets on success + function BuildExcerpts ( $docs, $index, $words, $opts=array() ) + { + assert ( is_array($docs) ); + assert ( is_string($index) ); + assert ( is_string($words) ); + assert ( is_array($opts) ); + + $this->_MBPush (); + + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop(); + return false; + } + + ///////////////// + // fixup options + ///////////////// + + if ( !isset($opts["before_match"]) ) $opts["before_match"] = "<b>"; + if ( !isset($opts["after_match"]) ) $opts["after_match"] = "</b>"; + if ( !isset($opts["chunk_separator"]) ) $opts["chunk_separator"] = " ... "; + if ( !isset($opts["limit"]) ) $opts["limit"] = 256; + if ( !isset($opts["limit_passages"]) ) $opts["limit_passages"] = 0; + if ( !isset($opts["limit_words"]) ) $opts["limit_words"] = 0; + if ( !isset($opts["around"]) ) $opts["around"] = 5; + if ( !isset($opts["exact_phrase"]) ) $opts["exact_phrase"] = false; + if ( !isset($opts["single_passage"]) ) $opts["single_passage"] = false; + if ( !isset($opts["use_boundaries"]) ) $opts["use_boundaries"] = false; + if ( !isset($opts["weight_order"]) ) $opts["weight_order"] = false; + if ( !isset($opts["query_mode"]) ) $opts["query_mode"] = false; + if ( !isset($opts["force_all_words"]) ) $opts["force_all_words"] = false; + if ( !isset($opts["start_passage_id"]) ) $opts["start_passage_id"] = 1; + if ( !isset($opts["load_files"]) ) $opts["load_files"] = false; + if ( !isset($opts["html_strip_mode"]) ) $opts["html_strip_mode"] = "index"; + if ( !isset($opts["allow_empty"]) ) $opts["allow_empty"] = false; + if ( !isset($opts["passage_boundary"]) ) $opts["passage_boundary"] = "none"; + if ( !isset($opts["emit_zones"]) ) $opts["emit_zones"] = false; + + ///////////////// + // build request + ///////////////// + + // v.1.2 req + $flags = 1; // remove spaces + if ( $opts["exact_phrase"] ) $flags |= 2; + if ( $opts["single_passage"] ) $flags |= 4; + if ( $opts["use_boundaries"] ) $flags |= 8; + if ( $opts["weight_order"] ) $flags |= 16; + if ( $opts["query_mode"] ) $flags |= 32; + if ( $opts["force_all_words"] ) $flags |= 64; + if ( $opts["load_files"] ) $flags |= 128; + if ( $opts["allow_empty"] ) $flags |= 256; + if ( $opts["emit_zones"] ) $flags |= 512; + $req = pack ( "NN", 0, $flags ); // mode=0, flags=$flags + $req .= pack ( "N", strlen($index) ) . $index; // req index + $req .= pack ( "N", strlen($words) ) . $words; // req words + + // options + $req .= pack ( "N", strlen($opts["before_match"]) ) . $opts["before_match"]; + $req .= pack ( "N", strlen($opts["after_match"]) ) . $opts["after_match"]; + $req .= pack ( "N", strlen($opts["chunk_separator"]) ) . $opts["chunk_separator"]; + $req .= pack ( "NN", (int)$opts["limit"], (int)$opts["around"] ); + $req .= pack ( "NNN", (int)$opts["limit_passages"], (int)$opts["limit_words"], (int)$opts["start_passage_id"] ); // v.1.2 + $req .= pack ( "N", strlen($opts["html_strip_mode"]) ) . $opts["html_strip_mode"]; + $req .= pack ( "N", strlen($opts["passage_boundary"]) ) . $opts["passage_boundary"]; + + // documents + $req .= pack ( "N", count($docs) ); + foreach ( $docs as $doc ) + { + assert ( is_string($doc) ); + $req .= pack ( "N", strlen($doc) ) . $doc; + } + + //////////////////////////// + // send query, get response + //////////////////////////// + + $len = strlen($req); + $req = pack ( "nnN", SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $len ) . $req; // add header + if ( !( $this->_Send ( $fp, $req, $len+8 ) ) || + !( $response = $this->_GetResponse ( $fp, VER_COMMAND_EXCERPT ) ) ) + { + $this->_MBPop (); + return false; + } + + ////////////////// + // parse response + ////////////////// + + $pos = 0; + $res = array (); + $rlen = strlen($response); + for ( $i=0; $i<count($docs); $i++ ) + { + list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); + $pos += 4; + + if ( $pos+$len > $rlen ) + { + $this->_error = "incomplete reply"; + $this->_MBPop (); + return false; + } + $res[] = $len ? substr ( $response, $pos, $len ) : ""; + $pos += $len; + } + + $this->_MBPop (); + return $res; + } + + + ///////////////////////////////////////////////////////////////////////////// + // keyword generation + ///////////////////////////////////////////////////////////////////////////// + + /// connect to searchd server, and generate keyword list for a given query + /// returns false on failure, + /// an array of words on success + function BuildKeywords ( $query, $index, $hits ) + { + assert ( is_string($query) ); + assert ( is_string($index) ); + assert ( is_bool($hits) ); + + $this->_MBPush (); + + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop(); + return false; + } + + ///////////////// + // build request + ///////////////// + + // v.1.0 req + $req = pack ( "N", strlen($query) ) . $query; // req query + $req .= pack ( "N", strlen($index) ) . $index; // req index + $req .= pack ( "N", (int)$hits ); + + //////////////////////////// + // send query, get response + //////////////////////////// + + $len = strlen($req); + $req = pack ( "nnN", SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $len ) . $req; // add header + if ( !( $this->_Send ( $fp, $req, $len+8 ) ) || + !( $response = $this->_GetResponse ( $fp, VER_COMMAND_KEYWORDS ) ) ) + { + $this->_MBPop (); + return false; + } + + ////////////////// + // parse response + ////////////////// + + $pos = 0; + $res = array (); + $rlen = strlen($response); + list(,$nwords) = unpack ( "N*", substr ( $response, $pos, 4 ) ); + $pos += 4; + for ( $i=0; $i<$nwords; $i++ ) + { + list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4; + $tokenized = $len ? substr ( $response, $pos, $len ) : ""; + $pos += $len; + + list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4; + $normalized = $len ? substr ( $response, $pos, $len ) : ""; + $pos += $len; + + $res[] = array ( "tokenized"=>$tokenized, "normalized"=>$normalized ); + + if ( $hits ) + { + list($ndocs,$nhits) = array_values ( unpack ( "N*N*", substr ( $response, $pos, 8 ) ) ); + $pos += 8; + $res [$i]["docs"] = $ndocs; + $res [$i]["hits"] = $nhits; + } + + if ( $pos > $rlen ) + { + $this->_error = "incomplete reply"; + $this->_MBPop (); + return false; + } + } + + $this->_MBPop (); + return $res; + } + + function EscapeString ( $string ) + { + $from = array ( '\\', '(',')','|','-','!','@','~','"','&', '/', '^', '$', '=' ); + $to = array ( '\\\\', '\(','\)','\|','\-','\!','\@','\~','\"', '\&', '\/', '\^', '\$', '\=' ); + + return str_replace ( $from, $to, $string ); + } + + ///////////////////////////////////////////////////////////////////////////// + // attribute updates + ///////////////////////////////////////////////////////////////////////////// + + /// batch update given attributes in given rows in given indexes + /// returns amount of updated documents (0 or more) on success, or -1 on failure + function UpdateAttributes ( $index, $attrs, $values, $mva=false ) + { + // verify everything + assert ( is_string($index) ); + assert ( is_bool($mva) ); + + assert ( is_array($attrs) ); + foreach ( $attrs as $attr ) + assert ( is_string($attr) ); + + assert ( is_array($values) ); + foreach ( $values as $id=>$entry ) + { + assert ( is_numeric($id) ); + assert ( is_array($entry) ); + assert ( count($entry)==count($attrs) ); + foreach ( $entry as $v ) + { + if ( $mva ) + { + assert ( is_array($v) ); + foreach ( $v as $vv ) + assert ( is_int($vv) ); + } else + assert ( is_int($v) ); + } + } + + // build request + $this->_MBPush (); + $req = pack ( "N", strlen($index) ) . $index; + + $req .= pack ( "N", count($attrs) ); + foreach ( $attrs as $attr ) + { + $req .= pack ( "N", strlen($attr) ) . $attr; + $req .= pack ( "N", $mva ? 1 : 0 ); + } + + $req .= pack ( "N", count($values) ); + foreach ( $values as $id=>$entry ) + { + $req .= sphPackU64 ( $id ); + foreach ( $entry as $v ) + { + $req .= pack ( "N", $mva ? count($v) : $v ); + if ( $mva ) + foreach ( $v as $vv ) + $req .= pack ( "N", $vv ); + } + } + + // connect, send query, get response + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop (); + return -1; + } + + $len = strlen($req); + $req = pack ( "nnN", SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $len ) . $req; // add header + if ( !$this->_Send ( $fp, $req, $len+8 ) ) + { + $this->_MBPop (); + return -1; + } + + if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_UPDATE ) )) + { + $this->_MBPop (); + return -1; + } + + // parse response + list(,$updated) = unpack ( "N*", substr ( $response, 0, 4 ) ); + $this->_MBPop (); + return $updated; + } + + ///////////////////////////////////////////////////////////////////////////// + // persistent connections + ///////////////////////////////////////////////////////////////////////////// + + function Open() + { + if ( $this->_socket !== false ) + { + $this->_error = 'already connected'; + return false; + } + if ( !$fp = $this->_Connect() ) + return false; + + // command, command version = 0, body length = 4, body = 1 + $req = pack ( "nnNN", SEARCHD_COMMAND_PERSIST, 0, 4, 1 ); + if ( !$this->_Send ( $fp, $req, 12 ) ) + return false; + + $this->_socket = $fp; + return true; + } + + function Close() + { + if ( $this->_socket === false ) + { + $this->_error = 'not connected'; + return false; + } + + fclose ( $this->_socket ); + $this->_socket = false; + + return true; + } + + ////////////////////////////////////////////////////////////////////////// + // status + ////////////////////////////////////////////////////////////////////////// + + function Status () + { + $this->_MBPush (); + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop(); + return false; + } + + $req = pack ( "nnNN", SEARCHD_COMMAND_STATUS, VER_COMMAND_STATUS, 4, 1 ); // len=4, body=1 + if ( !( $this->_Send ( $fp, $req, 12 ) ) || + !( $response = $this->_GetResponse ( $fp, VER_COMMAND_STATUS ) ) ) + { + $this->_MBPop (); + return false; + } + + $res = substr ( $response, 4 ); // just ignore length, error handling, etc + $p = 0; + list ( $rows, $cols ) = array_values ( unpack ( "N*N*", substr ( $response, $p, 8 ) ) ); $p += 8; + + $res = array(); + for ( $i=0; $i<$rows; $i++ ) + for ( $j=0; $j<$cols; $j++ ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $res[$i][] = substr ( $response, $p, $len ); $p += $len; + } + + $this->_MBPop (); + return $res; + } + + ////////////////////////////////////////////////////////////////////////// + // flush + ////////////////////////////////////////////////////////////////////////// + + function FlushAttributes () + { + $this->_MBPush (); + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop(); + return -1; + } + + $req = pack ( "nnN", SEARCHD_COMMAND_FLUSHATTRS, VER_COMMAND_FLUSHATTRS, 0 ); // len=0 + if ( !( $this->_Send ( $fp, $req, 8 ) ) || + !( $response = $this->_GetResponse ( $fp, VER_COMMAND_FLUSHATTRS ) ) ) + { + $this->_MBPop (); + return -1; + } + + $tag = -1; + if ( strlen($response)==4 ) + list(,$tag) = unpack ( "N*", $response ); + else + $this->_error = "unexpected response length"; + + $this->_MBPop (); + return $tag; + } +} + +// +// $Id: sphinxapi.php 2758 2011-04-04 11:10:44Z kevg $ +// 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 "<p>"; + + print "<button dojoType=\"dijit.form.Button\" onclick=\"return clearArticleAccessKeys()\">". + __('Unshare all articles')."</button> "; + + print "</p>"; + + } + } + + // 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 "<img src=\"plugins/share/share.png\" + $img = $line['uuid'] ? "share.png" : "notshared.png"; + + return "<img id='SHARE-IMG-".$line['int_id']."' src=\"plugins/share/$img\" class='tagsPic' style=\"cursor : pointer\" onclick=\"shareArticle(".$line['int_id'].")\" title='".__('Share by URL')."'>"; @@ -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 "<div class=\"tagCloudContainer\">"; - print "<a id='pub_opml_url' href='$url_path' target='_blank'>$url_path</a>"; + print "<a id='gen_article_url' href='$url_path' target='_blank'>$url_path</a>"; print "</div>"; /* if (!label_find_id(__('Shared'), $_SESSION["uid"])) @@ -61,6 +113,12 @@ class Share extends Plugin { print "<div align='center'>"; + print "<button dojoType=\"dijit.form.Button\" onclick=\"return dijit.byId('shareArticleDlg').unshare()\">". + __('Unshare article')."</button>"; + + print "<button dojoType=\"dijit.form.Button\" onclick=\"return dijit.byId('shareArticleDlg').newurl()\">". + __('Generate new URL')."</button>"; + print "<button dojoType=\"dijit.form.Button\" onclick=\"return dijit.byId('shareArticleDlg').hide()\">". __('Close this window')."</button>"; diff --git a/plugins/share/notshared.png b/plugins/share/notshared.png Binary files differnew file mode 100644 index 000000000..523575306 --- /dev/null +++ b/plugins/share/notshared.png 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 Binary files differindex 3b6398f4a..25eacb7c2 100644 --- a/plugins/share/share.png +++ b/plugins/share/share.png 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 = "<div class='contentSizeWrapper'>" + + content.innerHTML + "</div><button class='expandPrompt' onclick='return expandSizeWrapper(\""+row.id+"\")' "+ + "href='#'>" + __("Click to expand article") + "</button>"; + + } + } + } + }, 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 @@ +<?php +class Shorten_Expanded extends Plugin { + private $host; + + function about() { + return array(1.0, + "Shorten overly long articles in CDM/expanded", + "fox"); + } + + function init($host) { + $this->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 "<br/> <button dojoType=\"dijit.form.Button\" onclick=\"return updateSelf()\">". + __('Force update')."</button></p>"; + } print "</div>"; #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 @@ +<?php +class VF_Shared extends Plugin { + + private $host; + + function about() { + return array(1.0, + "Feed for all articles actively shared by URL", + "fox", + false); + } + + function init($host) { + $this->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 Binary files differnew file mode 100644 index 000000000..25eacb7c2 --- /dev/null +++ b/plugins/vf_shared/share.png |