Automated build for v0.01
This commit is contained in:
1
plugins/af_comics/.gitignore
vendored
Normal file
1
plugins/af_comics/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
filters.local/*.php
|
13
plugins/af_comics/af_comics_template.php
Normal file
13
plugins/af_comics/af_comics_template.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
class Af_Comics_Template extends Af_ComicFilter {
|
||||
|
||||
function supported() {
|
||||
return array("Example");
|
||||
}
|
||||
|
||||
function process(&$article) {
|
||||
//$owner_uid = $article["owner_uid"];
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
5
plugins/af_comics/filter_base.php
Normal file
5
plugins/af_comics/filter_base.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
abstract class Af_ComicFilter {
|
||||
public abstract function supported();
|
||||
public abstract function process(&$article);
|
||||
}
|
0
plugins/af_comics/filters.local/.empty
Normal file
0
plugins/af_comics/filters.local/.empty
Normal file
39
plugins/af_comics/filters/af_comics_cad.php
Normal file
39
plugins/af_comics/filters/af_comics_cad.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
class Af_Comics_Cad extends Af_ComicFilter {
|
||||
|
||||
function supported() {
|
||||
return array("Ctrl+Alt+Del");
|
||||
}
|
||||
|
||||
function process(&$article) {
|
||||
if (strpos($article["link"], "cad-comic.com") !== FALSE) {
|
||||
if (strpos($article["title"], "News:") === FALSE) {
|
||||
|
||||
global $fetch_last_error_content;
|
||||
|
||||
$doc = new DOMDocument();
|
||||
|
||||
$res = fetch_file_contents($article["link"], false, false, false,
|
||||
false, false, 0,
|
||||
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0");
|
||||
|
||||
if (!$res && $fetch_last_error_content)
|
||||
$res = $fetch_last_error_content;
|
||||
|
||||
if (@$doc->loadHTML($res)) {
|
||||
$xpath = new DOMXPath($doc);
|
||||
$basenode = $xpath->query('//div[@class="comicpage"]/a/img')->item(0);
|
||||
|
||||
if ($basenode) {
|
||||
$article["content"] = $doc->saveHTML($basenode);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
34
plugins/af_comics/filters/af_comics_comicclass.php
Normal file
34
plugins/af_comics/filters/af_comics_comicclass.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
class Af_Comics_ComicClass extends Af_ComicFilter {
|
||||
|
||||
function supported() {
|
||||
return array("Loading Artist");
|
||||
}
|
||||
|
||||
function process(&$article) {
|
||||
if (strpos($article["guid"], "loadingartist.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();
|
||||
|
||||
if (@$doc->loadHTML($res)) {
|
||||
$xpath = new DOMXPath($doc);
|
||||
$basenode = $xpath->query('//div[@class="comic"]')->item(0);
|
||||
|
||||
if ($basenode) {
|
||||
$article["content"] = $doc->saveHTML($basenode);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
41
plugins/af_comics/filters/af_comics_comicpress.php
Executable file
41
plugins/af_comics/filters/af_comics_comicpress.php
Executable file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
class Af_Comics_ComicPress extends Af_ComicFilter {
|
||||
|
||||
function supported() {
|
||||
return array("Buni", "Buttersafe", "Happy Jar", "CSection",
|
||||
"Extra Fabulous Comics", "Nedroid", "Stonetoss");
|
||||
}
|
||||
|
||||
function process(&$article) {
|
||||
if (strpos($article["guid"], "bunicomic.com") !== FALSE ||
|
||||
strpos($article["guid"], "buttersafe.com") !== FALSE ||
|
||||
strpos($article["guid"], "extrafabulouscomics.com") !== FALSE ||
|
||||
strpos($article["guid"], "happyjar.com") !== FALSE ||
|
||||
strpos($article["guid"], "nedroid.com") !== FALSE ||
|
||||
strpos($article["guid"], "stonetoss.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();
|
||||
|
||||
if (@$doc->loadHTML($res)) {
|
||||
$xpath = new DOMXPath($doc);
|
||||
$basenode = $xpath->query('//div[@id="comic"]')->item(0);
|
||||
|
||||
if ($basenode) {
|
||||
$article["content"] = $doc->saveHTML($basenode);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
38
plugins/af_comics/filters/af_comics_darklegacy.php
Normal file
38
plugins/af_comics/filters/af_comics_darklegacy.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
class Af_Comics_DarkLegacy extends Af_ComicFilter {
|
||||
|
||||
function supported() {
|
||||
return array("Dark Legacy Comics");
|
||||
}
|
||||
|
||||
function process(&$article) {
|
||||
|
||||
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();
|
||||
|
||||
if (@$doc->loadHTML($res)) {
|
||||
$xpath = new DOMXPath($doc);
|
||||
$basenode = $xpath->query('//div[@class="comic"]')->item(0);
|
||||
|
||||
if ($basenode) {
|
||||
|
||||
$article["content"] = $doc->saveHTML($basenode);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
73
plugins/af_comics/filters/af_comics_dilbert.php
Normal file
73
plugins/af_comics/filters/af_comics_dilbert.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
class Af_Comics_Dilbert extends Af_ComicFilter {
|
||||
|
||||
function supported() {
|
||||
return array("Dilbert");
|
||||
}
|
||||
|
||||
function process(&$article) {
|
||||
if (strpos($article["link"], "dilbert.com") !== FALSE ||
|
||||
strpos($article["link"], "/DilbertDailyStrip") !== FALSE) {
|
||||
|
||||
$res = fetch_file_contents($article["link"], false, false, false,
|
||||
false, false, 0,
|
||||
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0");
|
||||
|
||||
global $fetch_last_error_content;
|
||||
|
||||
if (!$res && $fetch_last_error_content)
|
||||
$res = $fetch_last_error_content;
|
||||
|
||||
$doc = new DOMDocument();
|
||||
|
||||
if (@$doc->loadHTML($res)) {
|
||||
$xpath = new DOMXPath($doc);
|
||||
|
||||
// Get the image container
|
||||
$basenode = $xpath->query('(//div[@class="img-comic-container"]/a[@class="img-comic-link"])')->item(0);
|
||||
|
||||
// Get the comic title
|
||||
$comic_title = $xpath->query('(//span[@class="comic-title-name"])')->item(0)->textContent;
|
||||
|
||||
// Get tags from the article
|
||||
$matches = $xpath->query('(//p[contains(@class, "comic-tags")][1]//a)');
|
||||
$tags = array();
|
||||
|
||||
foreach ($matches as $tag) {
|
||||
// Only strings starting with a number sign are considered tags
|
||||
if ( substr($tag->textContent, 0, 1) == '#' ) {
|
||||
$tags[] = mb_strtolower(substr($tag->textContent, 1), 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
// Get the current comics transcript and set it
|
||||
// as the title so it will be visible on mousover
|
||||
$transcript = $xpath->query('(//div[starts-with(@id, "js-toggle-transcript-")]//p)')->item(0);
|
||||
if ($transcript) {
|
||||
$basenode->setAttribute("title", $transcript->textContent);
|
||||
}
|
||||
|
||||
if ($basenode) {
|
||||
$article["content"] = $doc->saveHTML($basenode);
|
||||
}
|
||||
|
||||
// Add comic title to article type if not empty (mostly Sunday strips)
|
||||
if ($comic_title) {
|
||||
$article["title"] = $article["title"] . " - " . $comic_title;
|
||||
}
|
||||
|
||||
if (!empty($tags)) {
|
||||
// Ignore existing tags and just replace them all
|
||||
$article["tags"] = array_unique($tags);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
?>
|
28
plugins/af_comics/filters/af_comics_explosm.php
Normal file
28
plugins/af_comics/filters/af_comics_explosm.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
class Af_Comics_Explosm extends Af_ComicFilter {
|
||||
|
||||
function supported() {
|
||||
return array("Cyanide and Happiness");
|
||||
}
|
||||
|
||||
function process(&$article) {
|
||||
|
||||
if (strpos($article["link"], "explosm.net/comics") !== FALSE) {
|
||||
|
||||
$doc = new DOMDocument();
|
||||
|
||||
if (@$doc->loadHTML(fetch_file_contents($article["link"]))) {
|
||||
$xpath = new DOMXPath($doc);
|
||||
$basenode = $xpath->query('(//img[@id="main-comic"])')->item(0);
|
||||
|
||||
if ($basenode) {
|
||||
$article["content"] = $doc->saveHTML($basenode);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
67
plugins/af_comics/filters/af_comics_pa.php
Normal file
67
plugins/af_comics/filters/af_comics_pa.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
class Af_Comics_Pa extends Af_ComicFilter {
|
||||
|
||||
function supported() {
|
||||
return array("Penny Arcade");
|
||||
}
|
||||
|
||||
function process(&$article) {
|
||||
if (strpos($article["link"], "penny-arcade.com") !== FALSE && strpos($article["title"], "Comic:") !== FALSE) {
|
||||
|
||||
$doc = new DOMDocument();
|
||||
|
||||
if ($doc->loadHTML(fetch_file_contents($article["link"]))) {
|
||||
$xpath = new DOMXPath($doc);
|
||||
$basenode = $xpath->query('(//div[@id="comicFrame"])')->item(0);
|
||||
|
||||
if ($basenode) {
|
||||
$article["content"] = $doc->saveHTML($basenode);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strpos($article["link"], "penny-arcade.com") !== FALSE && strpos($article["title"], "News Post:") !== FALSE) {
|
||||
$doc = new DOMDocument();
|
||||
|
||||
if ($doc->loadHTML(fetch_file_contents($article["link"]))) {
|
||||
$xpath = new DOMXPath($doc);
|
||||
$entries = $xpath->query('(//div[@class="post"])');
|
||||
|
||||
$basenode = false;
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$basenode = $entry;
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
if ($basenode)
|
||||
$basenode->insertBefore($avatar, $basenode->firstChild);
|
||||
|
||||
$uninteresting = $xpath->query('(//div[@class="avatar"])');
|
||||
foreach ($uninteresting as $i) {
|
||||
$i->parentNode->removeChild($i);
|
||||
}
|
||||
|
||||
if ($basenode){
|
||||
$article["content"] = $doc->saveHTML($basenode);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
31
plugins/af_comics/filters/af_comics_pvp.php
Normal file
31
plugins/af_comics/filters/af_comics_pvp.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
class Af_Comics_Pvp extends Af_ComicFilter {
|
||||
|
||||
function supported() {
|
||||
return array("PvP Online");
|
||||
}
|
||||
|
||||
function process(&$article) {
|
||||
if (strpos($article["guid"], "pvponline.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)");
|
||||
|
||||
$doc = new DOMDocument();
|
||||
|
||||
if (@$doc->loadHTML($res)) {
|
||||
$xpath = new DOMXPath($doc);
|
||||
$basenode = $xpath->query('//section[@class="comic-art"]')->item(0);
|
||||
|
||||
if ($basenode) {
|
||||
$article["content"] = $doc->saveHTML($basenode);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
32
plugins/af_comics/filters/af_comics_tfd.php
Normal file
32
plugins/af_comics/filters/af_comics_tfd.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
class Af_Comics_Tfd extends Af_ComicFilter {
|
||||
|
||||
function supported() {
|
||||
return array("Toothpaste For Dinner", "Married to the Sea");
|
||||
}
|
||||
|
||||
function process(&$article) {
|
||||
if (strpos($article["link"], "toothpastefordinner.com") !== FALSE ||
|
||||
strpos($article["link"], "marriedtothesea.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)");
|
||||
|
||||
if (!$res) return $article;
|
||||
|
||||
$doc = new DOMDocument();
|
||||
|
||||
if (@$doc->loadHTML(fetch_file_contents($article["link"]))) {
|
||||
$xpath = new DOMXPath($doc);
|
||||
$basenode = $xpath->query('//img[contains(@src, ".gif")]')->item(0);
|
||||
|
||||
if ($basenode) {
|
||||
$article["content"] = $doc->saveHTML($basenode);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
29
plugins/af_comics/filters/af_comics_twp.php
Normal file
29
plugins/af_comics/filters/af_comics_twp.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?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();
|
||||
|
||||
if (@$doc->loadHTML(fetch_file_contents($article["link"]))) {
|
||||
$xpath = new DOMXpath($doc);
|
||||
|
||||
$basenode = $xpath->query("//td/center/img")->item(0);
|
||||
|
||||
if ($basenode) {
|
||||
$article["content"] = $doc->saveHTML($basenode);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
36
plugins/af_comics/filters/af_comics_whomp.php
Normal file
36
plugins/af_comics/filters/af_comics_whomp.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
class Af_Comics_Whomp extends Af_ComicFilter {
|
||||
|
||||
function supported() {
|
||||
return array("Whomp!");
|
||||
}
|
||||
|
||||
function process(&$article) {
|
||||
if (strpos($article["guid"], "whompcomic.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();
|
||||
|
||||
if (@$doc->loadHTML($res)) {
|
||||
$xpath = new DOMXPath($doc);
|
||||
$basenode = $xpath->query('//img[@id="cc-comic"]')->item(0);
|
||||
|
||||
if ($basenode) {
|
||||
$article["content"] = $doc->saveHTML($basenode);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
184
plugins/af_comics/init.php
Executable file
184
plugins/af_comics/init.php
Executable file
@ -0,0 +1,184 @@
|
||||
<?php
|
||||
class Af_Comics extends Plugin {
|
||||
|
||||
private $host;
|
||||
private $filters = array();
|
||||
|
||||
function about() {
|
||||
return array(2.0,
|
||||
"Fixes RSS feeds of assorted comic strips",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_FETCH_FEED, $this);
|
||||
$host->add_hook($host::HOOK_FEED_BASIC_INFO, $this);
|
||||
$host->add_hook($host::HOOK_SUBSCRIBE_FEED, $this);
|
||||
$host->add_hook($host::HOOK_ARTICLE_FILTER, $this);
|
||||
$host->add_hook($host::HOOK_PREFS_TAB, $this);
|
||||
|
||||
require_once __DIR__ . "/filter_base.php";
|
||||
|
||||
$filters = array_merge(glob(__DIR__ . "/filters.local/*.php"), glob(__DIR__ . "/filters/*.php"));
|
||||
$names = [];
|
||||
|
||||
foreach ($filters as $file) {
|
||||
$filter_name = preg_replace("/\..*$/", "", basename($file));
|
||||
|
||||
if (array_search($filter_name, $names) === FALSE) {
|
||||
if (!class_exists($filter_name)) {
|
||||
require_once $file;
|
||||
}
|
||||
|
||||
array_push($names, $filter_name);
|
||||
|
||||
$filter = new $filter_name();
|
||||
|
||||
if (is_subclass_of($filter, "Af_ComicFilter")) {
|
||||
array_push($this->filters, $filter);
|
||||
array_push($names, $filter_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hook_prefs_tab($args) {
|
||||
if ($args != "prefFeeds") return;
|
||||
|
||||
print "<div dojoType=\"dijit.layout.AccordionPane\"
|
||||
title=\"<i class='material-icons'>photo</i> ".__('Feeds supported by af_comics')."\">";
|
||||
|
||||
print "<p>" . __("The following comics are currently supported:") . "</p>";
|
||||
|
||||
$comics = array("GoComics");
|
||||
|
||||
foreach ($this->filters as $f) {
|
||||
foreach ($f->supported() as $comic) {
|
||||
array_push($comics, $comic);
|
||||
}
|
||||
}
|
||||
|
||||
asort($comics);
|
||||
|
||||
print "<ul class='panel panel-scrollable list list-unstyled'>";
|
||||
foreach ($comics as $comic) {
|
||||
print "<li>$comic</li>";
|
||||
}
|
||||
print "</ul>";
|
||||
|
||||
print "<p>".__("To subscribe to GoComics use the comic's regular web page as the feed URL (e.g. for the <em>Garfield</em> comic use <code>http://www.gocomics.com/garfield</code>).")."</p>";
|
||||
|
||||
print "<p>".__('Drop any updated filters into <code>filters.local</code> in plugin directory.')."</p>";
|
||||
|
||||
print "</div>";
|
||||
}
|
||||
|
||||
function hook_article_filter($article) {
|
||||
foreach ($this->filters as $f) {
|
||||
if ($f->process($article))
|
||||
break;
|
||||
}
|
||||
|
||||
return $article;
|
||||
}
|
||||
|
||||
// GoComics dropped feed support so it needs to be handled when fetching the feed.
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function hook_fetch_feed($feed_data, $fetch_url, $owner_uid, $feed, $last_article_timestamp, $auth_login, $auth_pass) {
|
||||
if ($auth_login || $auth_pass)
|
||||
return $feed_data;
|
||||
|
||||
if (preg_match('#^https?://(?:feeds\.feedburner\.com/uclick|www\.gocomics\.com)/([-a-z0-9]+)$#i', $fetch_url, $comic)) {
|
||||
$site_url = 'https://www.gocomics.com/' . $comic[1];
|
||||
|
||||
$article_link = $site_url . date('/Y/m/d');
|
||||
|
||||
$body = fetch_file_contents(array('url' => $article_link, 'type' => 'text/html', 'followlocation' => false));
|
||||
|
||||
require_once 'lib/MiniTemplator.class.php';
|
||||
|
||||
$feed_title = htmlspecialchars($comic[1]);
|
||||
$site_url = htmlspecialchars($site_url);
|
||||
$article_link = htmlspecialchars($article_link);
|
||||
|
||||
$tpl = new MiniTemplator();
|
||||
|
||||
$tpl->readTemplateFromFile('templates/generated_feed.txt');
|
||||
|
||||
$tpl->setVariable('FEED_TITLE', $feed_title, true);
|
||||
$tpl->setVariable('VERSION', VERSION, true);
|
||||
$tpl->setVariable('FEED_URL', htmlspecialchars($fetch_url), true);
|
||||
$tpl->setVariable('SELF_URL', $site_url, true);
|
||||
|
||||
$tpl->setVariable('ARTICLE_UPDATED_ATOM', date('c'), true);
|
||||
$tpl->setVariable('ARTICLE_UPDATED_RFC822', date(DATE_RFC822), true);
|
||||
|
||||
if ($body) {
|
||||
$doc = new DOMDocument();
|
||||
|
||||
if (@$doc->loadHTML($body)) {
|
||||
$xpath = new DOMXPath($doc);
|
||||
|
||||
$node = $xpath->query('//picture[contains(@class, "item-comic-image")]/img')->item(0);
|
||||
|
||||
if ($node) {
|
||||
$node->removeAttribute("width");
|
||||
$node->removeAttribute("data-srcset");
|
||||
$node->removeAttribute("srcset");
|
||||
|
||||
$tpl->setVariable('ARTICLE_ID', $article_link, true);
|
||||
$tpl->setVariable('ARTICLE_LINK', $article_link, true);
|
||||
$tpl->setVariable('ARTICLE_TITLE', date('l, F d, Y'), true);
|
||||
$tpl->setVariable('ARTICLE_EXCERPT', '', true);
|
||||
$tpl->setVariable('ARTICLE_CONTENT', $doc->saveHTML($node), true);
|
||||
|
||||
$tpl->setVariable('ARTICLE_AUTHOR', '', true);
|
||||
$tpl->setVariable('ARTICLE_SOURCE_LINK', $site_url, true);
|
||||
$tpl->setVariable('ARTICLE_SOURCE_TITLE', $feed_title, true);
|
||||
|
||||
$tpl->addBlock('entry');
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$tpl->addBlock('feed');
|
||||
|
||||
$tmp_data = '';
|
||||
|
||||
if ($tpl->generateOutputToString($tmp_data))
|
||||
$feed_data = $tmp_data;
|
||||
}
|
||||
|
||||
return $feed_data;
|
||||
}
|
||||
|
||||
function hook_subscribe_feed($contents, $url, $auth_login, $auth_pass) {
|
||||
if ($auth_login || $auth_pass)
|
||||
return $contents;
|
||||
|
||||
if (preg_match('#^https?://www\.gocomics\.com/([-a-z0-9]+)$#i', $url))
|
||||
return '<?xml version="1.0" encoding="utf-8"?>'; // Get is_html() to return false.
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
function hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed, $auth_login, $auth_pass) {
|
||||
if ($auth_login || $auth_pass)
|
||||
return $basic_info;
|
||||
|
||||
if (preg_match('#^https?://www\.gocomics\.com/([-a-z0-9]+)$#i', $fetch_url, $matches))
|
||||
$basic_info = array('title' => ucfirst($matches[1]), 'site_url' => $matches[0]);
|
||||
|
||||
return $basic_info;
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
47
plugins/af_fsckportal/init.php
Normal file
47
plugins/af_fsckportal/init.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?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();
|
||||
|
||||
@$doc->loadHTML('<?xml encoding="UTF-8">' . $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->saveHTML();
|
||||
|
||||
}
|
||||
|
||||
return $article;
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
21
plugins/af_psql_trgm/init.js
Normal file
21
plugins/af_psql_trgm/init.js
Normal file
@ -0,0 +1,21 @@
|
||||
Plugins.Psql_Trgm = {
|
||||
showRelated: function (id) {
|
||||
const query = "backend.php?op=pluginhandler&plugin=af_psql_trgm&method=showrelated¶m=" + encodeURIComponent(id);
|
||||
|
||||
if (dijit.byId("trgmRelatedDlg"))
|
||||
dijit.byId("trgmRelatedDlg").destroyRecursive();
|
||||
|
||||
dialog = new dijit.Dialog({
|
||||
id: "trgmRelatedDlg",
|
||||
title: __("Related articles"),
|
||||
style: "width: 600px",
|
||||
execute: function () {
|
||||
|
||||
},
|
||||
href: query,
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
};
|
||||
|
348
plugins/af_psql_trgm/init.php
Normal file
348
plugins/af_psql_trgm/init.php
Normal file
@ -0,0 +1,348 @@
|
||||
<?php
|
||||
class Af_Psql_Trgm extends Plugin {
|
||||
|
||||
/* @var PluginHost $host */
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Marks similar articles as read (requires pg_trgm)",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function save() {
|
||||
$similarity = (float) $_POST["similarity"];
|
||||
$min_title_length = (int) $_POST["min_title_length"];
|
||||
$enable_globally = checkbox_to_sql_bool($_POST["enable_globally"]);
|
||||
|
||||
if ($similarity < 0) $similarity = 0;
|
||||
if ($similarity > 1) $similarity = 1;
|
||||
|
||||
if ($min_title_length < 0) $min_title_length = 0;
|
||||
|
||||
$similarity = sprintf("%.2f", $similarity);
|
||||
|
||||
$this->host->set($this, "similarity", $similarity);
|
||||
$this->host->set($this, "min_title_length", $min_title_length);
|
||||
$this->host->set($this, "enable_globally", $enable_globally);
|
||||
|
||||
echo T_sprintf("Data saved (%s, %d)", $similarity, $enable_globally);
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_ARTICLE_FILTER, $this);
|
||||
$host->add_hook($host::HOOK_PREFS_TAB, $this);
|
||||
$host->add_hook($host::HOOK_PREFS_EDIT_FEED, $this);
|
||||
$host->add_hook($host::HOOK_PREFS_SAVE_FEED, $this);
|
||||
$host->add_hook($host::HOOK_ARTICLE_BUTTON, $this);
|
||||
|
||||
}
|
||||
|
||||
function get_js() {
|
||||
return file_get_contents(__DIR__ . "/init.js");
|
||||
}
|
||||
|
||||
function showrelated() {
|
||||
$id = (int) $_REQUEST['param'];
|
||||
$owner_uid = $_SESSION["uid"];
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT title FROM ttrss_entries, ttrss_user_entries
|
||||
WHERE ref_id = id AND id = ? AND owner_uid = ?");
|
||||
$sth->execute([$id, $owner_uid]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
|
||||
$title = $row['title'];
|
||||
|
||||
print "<p>$title</p>";
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT ttrss_entries.id AS id,
|
||||
feed_id,
|
||||
ttrss_entries.title AS title,
|
||||
updated, link,
|
||||
ttrss_feeds.title AS feed_title,
|
||||
SIMILARITY(ttrss_entries.title, ?) AS sm
|
||||
FROM
|
||||
ttrss_entries, ttrss_user_entries LEFT JOIN ttrss_feeds ON (ttrss_feeds.id = feed_id)
|
||||
WHERE
|
||||
ttrss_entries.id = ref_id AND
|
||||
ttrss_user_entries.owner_uid = ? AND
|
||||
ttrss_entries.id != ? AND
|
||||
date_entered >= NOW() - INTERVAL '2 weeks'
|
||||
ORDER BY
|
||||
sm DESC, date_entered DESC
|
||||
LIMIT 10");
|
||||
|
||||
$sth->execute([$title, $owner_uid, $id]);
|
||||
|
||||
print "<ul class='panel panel-scrollable'>";
|
||||
|
||||
while ($line = $sth->fetch()) {
|
||||
print "<li style='display : flex'>";
|
||||
print "<i class='material-icons'>bookmark_outline</i>";
|
||||
|
||||
$sm = sprintf("%.2f", $line['sm']);
|
||||
$article_link = htmlspecialchars($line["link"]);
|
||||
|
||||
print "<div style='flex-grow : 2'>";
|
||||
|
||||
print " <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"$article_link\">".
|
||||
$line["title"]."</a>";
|
||||
|
||||
print " (<a href=\"#\" onclick=\"Feeds.open({feed:".$line["feed_id"]."})\">".
|
||||
htmlspecialchars($line["feed_title"])."</a>)";
|
||||
|
||||
print " — $sm";
|
||||
|
||||
print "</div>";
|
||||
|
||||
print "<div style='text-align : right' class='text-muted'>" . smart_date_time(strtotime($line["updated"])) . "</div>";
|
||||
|
||||
print "</li>";
|
||||
}
|
||||
|
||||
print "</ul>";
|
||||
|
||||
}
|
||||
|
||||
print "<footer class='text-center'>";
|
||||
print "<button dojoType='dijit.form.Button' onclick=\"dijit.byId('trgmRelatedDlg').hide()\">".__('Close this window')."</button>";
|
||||
print "</footer>";
|
||||
|
||||
|
||||
}
|
||||
|
||||
function hook_article_button($line) {
|
||||
return "<i style=\"cursor : pointer\" class='material-icons'
|
||||
onclick=\"Plugins.Psql_Trgm.showRelated(".$line["id"].")\"
|
||||
title='".__('Show related articles')."'>bookmark_outline</i>";
|
||||
}
|
||||
|
||||
function hook_prefs_tab($args) {
|
||||
if ($args != "prefFeeds") return;
|
||||
|
||||
print "<div dojoType=\"dijit.layout.AccordionPane\"
|
||||
title=\"<i class='material-icons'>extension</i> ".__('Mark similar articles as read')."\">";
|
||||
|
||||
if (DB_TYPE != "pgsql") {
|
||||
print_error("Database type not supported.");
|
||||
} else {
|
||||
|
||||
$res = $this->pdo->query("select 'similarity'::regproc");
|
||||
|
||||
if (!$res->fetch()) {
|
||||
print_error("pg_trgm extension not found.");
|
||||
}
|
||||
|
||||
$similarity = $this->host->get($this, "similarity");
|
||||
$min_title_length = $this->host->get($this, "min_title_length");
|
||||
$enable_globally = $this->host->get($this, "enable_globally");
|
||||
|
||||
if (!$similarity) $similarity = '0.75';
|
||||
if (!$min_title_length) $min_title_length = '32';
|
||||
|
||||
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_hidden("op", "pluginhandler");
|
||||
print_hidden("method", "save");
|
||||
print_hidden("plugin", "af_psql_trgm");
|
||||
|
||||
print "<h2>" . __("Global settings") . "</h2>";
|
||||
|
||||
print_notice("Enable for specific feeds in the feed editor.");
|
||||
|
||||
print "<fieldset>";
|
||||
|
||||
print "<label>" . __("Minimum similarity:") . "</label> ";
|
||||
print "<input dojoType=\"dijit.form.NumberSpinner\"
|
||||
placeholder=\"0.75\" id='psql_trgm_similarity'
|
||||
required=\"1\" name=\"similarity\" value=\"$similarity\">";
|
||||
|
||||
print "<div dojoType='dijit.Tooltip' connectId='psql_trgm_similarity' position='below'>" .
|
||||
__("PostgreSQL trigram extension returns string similarity as a floating point number (0-1). Setting it too low might produce false positives, zero disables checking.") .
|
||||
"</div>";
|
||||
|
||||
print "</fieldset><fieldset>";
|
||||
|
||||
print "<label>" . __("Minimum title length:") . "</label> ";
|
||||
print "<input dojoType=\"dijit.form.NumberSpinner\"
|
||||
placeholder=\"32\"
|
||||
required=\"1\" name=\"min_title_length\" value=\"$min_title_length\">";
|
||||
|
||||
print "</fieldset><fieldset>";
|
||||
|
||||
print "<label class='checkbox'>";
|
||||
print_checkbox("enable_globally", $enable_globally);
|
||||
print " " . __("Enable for all feeds:");
|
||||
print "</label>";
|
||||
|
||||
print "</fieldset>";
|
||||
|
||||
print_button("submit", __("Save"), "class='alt-primary'");
|
||||
print "</form>";
|
||||
|
||||
$enabled_feeds = $this->host->get($this, "enabled_feeds");
|
||||
if (!array($enabled_feeds)) $enabled_feeds = array();
|
||||
|
||||
$enabled_feeds = $this->filter_unknown_feeds($enabled_feeds);
|
||||
$this->host->set($this, "enabled_feeds", $enabled_feeds);
|
||||
|
||||
if (count($enabled_feeds) > 0) {
|
||||
print "<h3>" . __("Currently enabled for (click to edit):") . "</h3>";
|
||||
|
||||
print "<ul class=\"panel panel-scrollable list list-unstyled\">";
|
||||
foreach ($enabled_feeds as $f) {
|
||||
print "<li>" .
|
||||
"<i class='material-icons'>rss_feed</i> <a href='#'
|
||||
onclick='CommonDialogs.editFeed($f)'>" .
|
||||
Feeds::getFeedTitle($f) . "</a></li>";
|
||||
}
|
||||
print "</ul>";
|
||||
}
|
||||
}
|
||||
|
||||
print "</div>";
|
||||
}
|
||||
|
||||
function hook_prefs_edit_feed($feed_id) {
|
||||
print "<header>".__("Similarity (pg_trgm)")."</header>";
|
||||
print "<section>";
|
||||
|
||||
$enabled_feeds = $this->host->get($this, "enabled_feeds");
|
||||
if (!array($enabled_feeds)) $enabled_feeds = array();
|
||||
|
||||
$key = array_search($feed_id, $enabled_feeds);
|
||||
$checked = $key !== FALSE ? "checked" : "";
|
||||
|
||||
print "<fieldset>";
|
||||
|
||||
print "<label class='checkbox'><input dojoType='dijit.form.CheckBox' type='checkbox' id='trgm_similarity_enabled'
|
||||
name='trgm_similarity_enabled' $checked> ".__('Mark similar articles as read')."</label>";
|
||||
|
||||
print "</fieldset>";
|
||||
|
||||
print "</section>";
|
||||
}
|
||||
|
||||
function hook_prefs_save_feed($feed_id) {
|
||||
$enabled_feeds = $this->host->get($this, "enabled_feeds");
|
||||
if (!is_array($enabled_feeds)) $enabled_feeds = array();
|
||||
|
||||
$enable = checkbox_to_sql_bool($_POST["trgm_similarity_enabled"]);
|
||||
$key = array_search($feed_id, $enabled_feeds);
|
||||
|
||||
if ($enable) {
|
||||
if ($key === FALSE) {
|
||||
array_push($enabled_feeds, $feed_id);
|
||||
}
|
||||
} else {
|
||||
if ($key !== FALSE) {
|
||||
unset($enabled_feeds[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->host->set($this, "enabled_feeds", $enabled_feeds);
|
||||
}
|
||||
|
||||
function hook_article_filter($article) {
|
||||
|
||||
if (DB_TYPE != "pgsql") return $article;
|
||||
|
||||
$res = $this->pdo->query("select 'similarity'::regproc");
|
||||
if (!$res->fetch()) return $article;
|
||||
|
||||
$enable_globally = $this->host->get($this, "enable_globally");
|
||||
|
||||
if (!$enable_globally) {
|
||||
$enabled_feeds = $this->host->get($this, "enabled_feeds");
|
||||
$key = array_search($article["feed"]["id"], $enabled_feeds);
|
||||
if ($key === FALSE) return $article;
|
||||
}
|
||||
|
||||
$similarity = (float) $this->host->get($this, "similarity");
|
||||
if ($similarity < 0.01) return $article;
|
||||
|
||||
$min_title_length = (int) $this->host->get($this, "min_title_length");
|
||||
if (mb_strlen($article["title"]) < $min_title_length) return $article;
|
||||
|
||||
$owner_uid = $article["owner_uid"];
|
||||
$entry_guid = $article["guid_hashed"];
|
||||
$title_escaped = $article["title"];
|
||||
|
||||
// trgm does not return similarity=1 for completely equal strings
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT COUNT(id) AS nequal
|
||||
FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id AND
|
||||
date_entered >= NOW() - interval '3 days' AND
|
||||
title = ? AND
|
||||
guid != ? AND
|
||||
owner_uid = ?");
|
||||
$sth->execute([$title_escaped, $entry_guid, $owner_uid]);
|
||||
|
||||
$row = $sth->fetch();
|
||||
$nequal = $row['nequal'];
|
||||
|
||||
Debug::log("af_psql_trgm: num equals: $nequal", Debug::$LOG_EXTENDED);
|
||||
|
||||
if ($nequal != 0) {
|
||||
$article["force_catchup"] = true;
|
||||
return $article;
|
||||
}
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT MAX(SIMILARITY(title, ?)) AS ms
|
||||
FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id AND
|
||||
date_entered >= NOW() - interval '1 day' AND
|
||||
guid != ? AND
|
||||
owner_uid = ?");
|
||||
$sth->execute([$title_escaped, $entry_guid, $owner_uid]);
|
||||
|
||||
$row = $sth->fetch();
|
||||
$similarity_result = $row['ms'];
|
||||
|
||||
Debug::log("af_psql_trgm: similarity result: $similarity_result", Debug::$LOG_EXTENDED);
|
||||
|
||||
if ($similarity_result >= $similarity) {
|
||||
$article["force_catchup"] = true;
|
||||
}
|
||||
|
||||
return $article;
|
||||
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
private function filter_unknown_feeds($enabled_feeds) {
|
||||
$tmp = array();
|
||||
|
||||
foreach ($enabled_feeds as $feed) {
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ? AND owner_uid = ?");
|
||||
$sth->execute([$feed, $_SESSION['uid']]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
array_push($tmp, $feed);
|
||||
}
|
||||
}
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
}
|
260
plugins/af_readability/init.php
Executable file
260
plugins/af_readability/init.php
Executable file
@ -0,0 +1,260 @@
|
||||
<?php
|
||||
use andreskrey\Readability\Readability;
|
||||
use andreskrey\Readability\Configuration;
|
||||
|
||||
class Af_Readability extends Plugin {
|
||||
|
||||
/* @var PluginHost $host */
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Try to inline article content using Readability",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function flags() {
|
||||
return array("needs_curl" => true);
|
||||
}
|
||||
|
||||
function save() {
|
||||
$enable_share_anything = checkbox_to_sql_bool($_POST["enable_share_anything"]);
|
||||
|
||||
$this->host->set($this, "enable_share_anything", $enable_share_anything);
|
||||
|
||||
echo __("Data saved.");
|
||||
}
|
||||
|
||||
function init($host)
|
||||
{
|
||||
$this->host = $host;
|
||||
|
||||
if (version_compare(PHP_VERSION, '5.6.0', '<')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$host->add_hook($host::HOOK_ARTICLE_FILTER, $this);
|
||||
$host->add_hook($host::HOOK_PREFS_TAB, $this);
|
||||
$host->add_hook($host::HOOK_PREFS_EDIT_FEED, $this);
|
||||
$host->add_hook($host::HOOK_PREFS_SAVE_FEED, $this);
|
||||
|
||||
$host->add_filter_action($this, "action_inline", __("Inline content"));
|
||||
}
|
||||
|
||||
function hook_prefs_tab($args) {
|
||||
if ($args != "prefFeeds") return;
|
||||
|
||||
print "<div dojoType='dijit.layout.AccordionPane'
|
||||
title=\"<i class='material-icons'>extension</i> ".__('Readability settings (af_readability)')."\">";
|
||||
|
||||
if (version_compare(PHP_VERSION, '5.6.0', '<')) {
|
||||
print_error("This plugin requires PHP version 5.6.");
|
||||
} else {
|
||||
|
||||
print "<h2>" . __("Global settings") . "</h2>";
|
||||
|
||||
print_notice("Enable for specific feeds in the feed editor.");
|
||||
|
||||
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_hidden("op", "pluginhandler");
|
||||
print_hidden("method", "save");
|
||||
print_hidden("plugin", "af_readability");
|
||||
|
||||
$enable_share_anything = $this->host->get($this, "enable_share_anything");
|
||||
|
||||
print "<fieldset>";
|
||||
print "<label class='checkbox'> ";
|
||||
print_checkbox("enable_share_anything", $enable_share_anything);
|
||||
print " " . __("Use Readability for pages shared via bookmarklet.");
|
||||
print "</label>";
|
||||
print "</fieldset>";
|
||||
|
||||
print print_button("submit", __("Save"), "class='alt-primary'");
|
||||
print "</form>";
|
||||
|
||||
$enabled_feeds = $this->host->get($this, "enabled_feeds");
|
||||
if (!is_array($enabled_feeds)) $enabled_feeds = array();
|
||||
|
||||
$enabled_feeds = $this->filter_unknown_feeds($enabled_feeds);
|
||||
$this->host->set($this, "enabled_feeds", $enabled_feeds);
|
||||
|
||||
if (count($enabled_feeds) > 0) {
|
||||
print "<h3>" . __("Currently enabled for (click to edit):") . "</h3>";
|
||||
|
||||
print "<ul class='panel panel-scrollable list list-unstyled'>";
|
||||
foreach ($enabled_feeds as $f) {
|
||||
print "<li><i class='material-icons'>rss_feed</i> <a href='#'
|
||||
onclick='CommonDialogs.editFeed($f)'>".
|
||||
Feeds::getFeedTitle($f) . "</a></li>";
|
||||
}
|
||||
print "</ul>";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
print "</div>";
|
||||
}
|
||||
|
||||
function hook_prefs_edit_feed($feed_id) {
|
||||
print "<header>".__("Readability")."</header>";
|
||||
print "<section>";
|
||||
|
||||
$enabled_feeds = $this->host->get($this, "enabled_feeds");
|
||||
if (!is_array($enabled_feeds)) $enabled_feeds = array();
|
||||
|
||||
$key = array_search($feed_id, $enabled_feeds);
|
||||
$checked = $key !== FALSE ? "checked" : "";
|
||||
|
||||
print "<fieldset>";
|
||||
|
||||
print "<label class='checkbox'><input dojoType='dijit.form.CheckBox' type='checkbox' id='af_readability_enabled'
|
||||
name='af_readability_enabled' $checked> ".__('Inline article content')."</label>";
|
||||
|
||||
print "</fieldset>";
|
||||
|
||||
print "</section>";
|
||||
}
|
||||
|
||||
function hook_prefs_save_feed($feed_id) {
|
||||
$enabled_feeds = $this->host->get($this, "enabled_feeds");
|
||||
if (!is_array($enabled_feeds)) $enabled_feeds = array();
|
||||
|
||||
$enable = checkbox_to_sql_bool($_POST["af_readability_enabled"]);
|
||||
$key = array_search($feed_id, $enabled_feeds);
|
||||
|
||||
if ($enable) {
|
||||
if ($key === FALSE) {
|
||||
array_push($enabled_feeds, $feed_id);
|
||||
}
|
||||
} else {
|
||||
if ($key !== FALSE) {
|
||||
unset($enabled_feeds[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->host->set($this, "enabled_feeds", $enabled_feeds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function hook_article_filter_action($article, $action) {
|
||||
return $this->process_article($article);
|
||||
}
|
||||
|
||||
public function extract_content($url) {
|
||||
|
||||
global $fetch_effective_url;
|
||||
|
||||
$tmp = fetch_file_contents([
|
||||
"url" => $url,
|
||||
"http_accept" => "text/*",
|
||||
"type" => "text/html"]);
|
||||
|
||||
if ($tmp && mb_strlen($tmp) < 1024 * 500) {
|
||||
$tmpdoc = new DOMDocument("1.0", "UTF-8");
|
||||
|
||||
if (!$tmpdoc->loadHTML($tmp))
|
||||
return false;
|
||||
|
||||
// this is the worst hack yet :(
|
||||
if (strtolower($tmpdoc->encoding) != 'utf-8') {
|
||||
$tmp = preg_replace("/<meta.*?charset.*?\/>/i", "", $tmp);
|
||||
$tmp = mb_convert_encoding($tmp, 'utf-8', $tmpdoc->encoding);
|
||||
}
|
||||
|
||||
try {
|
||||
$r = new Readability(new Configuration());
|
||||
|
||||
if ($r->parse($tmp)) {
|
||||
|
||||
$tmpxpath = new DOMXPath($r->getDOMDOcument());
|
||||
$entries = $tmpxpath->query('(//a[@href]|//img[@src])');
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry->hasAttribute("href")) {
|
||||
$entry->setAttribute("href",
|
||||
rewrite_relative_url($fetch_effective_url, $entry->getAttribute("href")));
|
||||
|
||||
}
|
||||
|
||||
if ($entry->hasAttribute("src")) {
|
||||
$entry->setAttribute("src",
|
||||
rewrite_relative_url($fetch_effective_url, $entry->getAttribute("src")));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return $r->getContent();
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function process_article($article) {
|
||||
|
||||
$extracted_content = $this->extract_content($article["link"]);
|
||||
|
||||
# let's see if there's anything of value in there
|
||||
$content_test = trim(strip_tags(sanitize($extracted_content)));
|
||||
|
||||
if ($content_test) {
|
||||
$article["content"] = $extracted_content;
|
||||
}
|
||||
|
||||
return $article;
|
||||
}
|
||||
|
||||
function hook_article_filter($article) {
|
||||
|
||||
$enabled_feeds = $this->host->get($this, "enabled_feeds");
|
||||
if (!is_array($enabled_feeds)) return $article;
|
||||
|
||||
$key = array_search($article["feed"]["id"], $enabled_feeds);
|
||||
if ($key === FALSE) return $article;
|
||||
|
||||
return $this->process_article($article);
|
||||
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
private function filter_unknown_feeds($enabled_feeds) {
|
||||
$tmp = array();
|
||||
|
||||
foreach ($enabled_feeds as $feed) {
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ? AND owner_uid = ?");
|
||||
$sth->execute([$feed, $_SESSION['uid']]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
array_push($tmp, $feed);
|
||||
}
|
||||
}
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
}
|
603
plugins/af_redditimgur/init.php
Executable file
603
plugins/af_redditimgur/init.php
Executable file
@ -0,0 +1,603 @@
|
||||
<?php
|
||||
use andreskrey\Readability\Readability;
|
||||
use andreskrey\Readability\Configuration;
|
||||
|
||||
class Af_RedditImgur extends Plugin {
|
||||
|
||||
/* @var PluginHost $host */
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Inline images (and other content) in Reddit RSS feeds",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function flags() {
|
||||
return array("needs_curl" => true);
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_ARTICLE_FILTER, $this);
|
||||
$host->add_hook($host::HOOK_PREFS_TAB, $this);
|
||||
}
|
||||
|
||||
function hook_prefs_tab($args) {
|
||||
if ($args != "prefFeeds") return;
|
||||
|
||||
print "<div dojoType=\"dijit.layout.AccordionPane\"
|
||||
title=\"<i class='material-icons'>extension</i> ".__('Reddit content settings (af_redditimgur)')."\">";
|
||||
|
||||
$enable_readability = $this->host->get($this, "enable_readability");
|
||||
$enable_content_dupcheck = $this->host->get($this, "enable_content_dupcheck");
|
||||
|
||||
if (version_compare(PHP_VERSION, '5.6.0', '<')) {
|
||||
print_error("Readability requires PHP version 5.6.");
|
||||
}
|
||||
|
||||
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_hidden("op", "pluginhandler");
|
||||
print_hidden("method", "save");
|
||||
print_hidden("plugin", "af_redditimgur");
|
||||
|
||||
print "<fieldset class='narrow'>";
|
||||
print "<label class='checkbox'>";
|
||||
print_checkbox("enable_readability", $enable_readability);
|
||||
print " " . __("Extract missing content using Readability") . "</label>";
|
||||
print "</fieldset>";
|
||||
|
||||
print "<fieldset class='narrow'>";
|
||||
print "<label class='checkbox'>";
|
||||
print_checkbox("enable_content_dupcheck", $enable_content_dupcheck);
|
||||
print " " . __("Enable additional duplicate checking") . "</label>";
|
||||
print "</fieldset>";
|
||||
|
||||
print_button("submit", __("Save"), 'class="alt-primary"');
|
||||
print "</form>";
|
||||
|
||||
print "</div>";
|
||||
}
|
||||
|
||||
function save() {
|
||||
$enable_readability = checkbox_to_sql_bool($_POST["enable_readability"]);
|
||||
$enable_content_dupcheck = checkbox_to_sql_bool($_POST["enable_content_dupcheck"]);
|
||||
|
||||
$this->host->set($this, "enable_readability", $enable_readability, false);
|
||||
$this->host->set($this, "enable_content_dupcheck", $enable_content_dupcheck);
|
||||
|
||||
echo __("Configuration saved");
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
private function inline_stuff($article, &$doc, $xpath) {
|
||||
|
||||
$entries = $xpath->query('(//a[@href]|//img[@src])');
|
||||
$img_entries = $xpath->query("(//img[@src])");
|
||||
|
||||
$found = false;
|
||||
//$debug = 1;
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry->hasAttribute("href") && strpos($entry->getAttribute("href"), "reddit.com") === FALSE) {
|
||||
|
||||
Debug::log("processing href: " . $entry->getAttribute("href"), Debug::$LOG_VERBOSE);
|
||||
|
||||
$matches = array();
|
||||
|
||||
if (!$found && preg_match("/^https?:\/\/twitter.com\/(.*?)\/status\/(.*)/", $entry->getAttribute("href"), $matches)) {
|
||||
Debug::log("handling as twitter: " . $matches[1] . " " . $matches[2], Debug::$LOG_VERBOSE);
|
||||
|
||||
$oembed_result = fetch_file_contents("https://publish.twitter.com/oembed?url=" . urlencode($entry->getAttribute("href")));
|
||||
|
||||
if ($oembed_result) {
|
||||
$oembed_result = json_decode($oembed_result, true);
|
||||
|
||||
if ($oembed_result && isset($oembed_result["html"])) {
|
||||
|
||||
$tmp = new DOMDocument();
|
||||
if ($tmp->loadHTML('<?xml encoding="utf-8" ?>' . $oembed_result["html"])) {
|
||||
$p = $doc->createElement("p");
|
||||
|
||||
$p->appendChild($doc->importNode(
|
||||
$tmp->getElementsByTagName("blockquote")->item(0), TRUE));
|
||||
|
||||
$br = $doc->createElement('br');
|
||||
$entry->parentNode->insertBefore($p, $entry);
|
||||
$entry->parentNode->insertBefore($br, $entry);
|
||||
|
||||
$found = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found && preg_match("/\.gfycat.com\/([a-z]+)?(\.[a-z]+)$/i", $entry->getAttribute("href"), $matches)) {
|
||||
$entry->setAttribute("href", "http://www.gfycat.com/".$matches[1]);
|
||||
}
|
||||
|
||||
if (!$found && preg_match("/https?:\/\/(www\.)?gfycat.com\/([a-z]+)$/i", $entry->getAttribute("href"), $matches)) {
|
||||
|
||||
Debug::log("Handling as Gfycat", Debug::$LOG_VERBOSE);
|
||||
|
||||
$source_stream = 'https://giant.gfycat.com/' . $matches[2] . '.mp4';
|
||||
$poster_url = 'https://thumbs.gfycat.com/' . $matches[2] . '-mobile.jpg';
|
||||
|
||||
$content_type = $this->get_content_type($source_stream);
|
||||
|
||||
if (strpos($content_type, "video/") !== FALSE) {
|
||||
$this->handle_as_video($doc, $entry, $source_stream, $poster_url);
|
||||
$found = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found && preg_match("/https?:\/\/v\.redd\.it\/(.*)$/i", $entry->getAttribute("href"), $matches)) {
|
||||
|
||||
Debug::log("Handling as reddit inline video", Debug::$LOG_VERBOSE);
|
||||
|
||||
$img = $img_entries->item(0);
|
||||
|
||||
if ($img) {
|
||||
$poster_url = $img->getAttribute("src");
|
||||
} else {
|
||||
$poster_url = false;
|
||||
}
|
||||
|
||||
// Get original article URL from v.redd.it redirects
|
||||
$source_article_url = $this->get_location($matches[0]);
|
||||
Debug::log("Resolved ".$matches[0]." to ".$source_article_url, Debug::$LOG_VERBOSE);
|
||||
|
||||
$source_stream = false;
|
||||
|
||||
if ($source_article_url) {
|
||||
$j = json_decode(fetch_file_contents($source_article_url.".json"), true);
|
||||
|
||||
if ($j) {
|
||||
foreach ($j as $listing) {
|
||||
foreach ($listing["data"]["children"] as $child) {
|
||||
if ($child["data"]["url"] == $matches[0]) {
|
||||
try {
|
||||
$source_stream = $child["data"]["media"]["reddit_video"]["fallback_url"];
|
||||
}
|
||||
catch (Exception $e) {
|
||||
}
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$source_stream) {
|
||||
$source_stream = "https://v.redd.it/" . $matches[1] . "/DASH_600_K";
|
||||
}
|
||||
|
||||
$this->handle_as_video($doc, $entry, $source_stream, $poster_url);
|
||||
$found = 1;
|
||||
}
|
||||
|
||||
if (!$found && preg_match("/https?:\/\/(www\.)?streamable.com\//i", $entry->getAttribute("href"))) {
|
||||
|
||||
Debug::log("Handling as Streamable", Debug::$LOG_VERBOSE);
|
||||
|
||||
$tmp = fetch_file_contents($entry->getAttribute("href"));
|
||||
|
||||
if ($tmp) {
|
||||
$tmpdoc = new DOMDocument();
|
||||
|
||||
if (@$tmpdoc->loadHTML($tmp)) {
|
||||
$tmpxpath = new DOMXPath($tmpdoc);
|
||||
|
||||
$source_node = $tmpxpath->query("//video[contains(@class,'video-player-tag')]//source[contains(@src, '.mp4')]")->item(0);
|
||||
$poster_node = $tmpxpath->query("//video[contains(@class,'video-player-tag') and @poster]")->item(0);
|
||||
|
||||
if ($source_node && $poster_node) {
|
||||
$source_stream = $source_node->getAttribute("src");
|
||||
$poster_url = $poster_node->getAttribute("poster");
|
||||
|
||||
$this->handle_as_video($doc, $entry, $source_stream, $poster_url);
|
||||
$found = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// imgur .gif -> .gifv
|
||||
if (!$found && preg_match("/i\.imgur\.com\/(.*?)\.gif$/i", $entry->getAttribute("href"))) {
|
||||
Debug::log("Handling as imgur gif (->gifv)", Debug::$LOG_VERBOSE);
|
||||
|
||||
$entry->setAttribute("href",
|
||||
str_replace(".gif", ".gifv", $entry->getAttribute("href")));
|
||||
}
|
||||
|
||||
if (!$found && preg_match("/\.(gifv|mp4)$/i", $entry->getAttribute("href"))) {
|
||||
Debug::log("Handling as imgur gifv", Debug::$LOG_VERBOSE);
|
||||
|
||||
$source_stream = str_replace(".gifv", ".mp4", $entry->getAttribute("href"));
|
||||
|
||||
if (strpos($source_stream, "imgur.com") !== FALSE)
|
||||
$poster_url = str_replace(".mp4", "h.jpg", $source_stream);
|
||||
|
||||
$this->handle_as_video($doc, $entry, $source_stream, $poster_url);
|
||||
|
||||
$found = true;
|
||||
}
|
||||
|
||||
$matches = array();
|
||||
if (!$found && preg_match("/youtube\.com\/v\/([\w-]+)/", $entry->getAttribute("href"), $matches) ||
|
||||
preg_match("/youtube\.com\/.*?[\&\?]v=([\w-]+)/", $entry->getAttribute("href"), $matches) ||
|
||||
preg_match("/youtube\.com\/watch\?v=([\w-]+)/", $entry->getAttribute("href"), $matches) ||
|
||||
preg_match("/\/\/youtu.be\/([\w-]+)/", $entry->getAttribute("href"), $matches)) {
|
||||
|
||||
$vid_id = $matches[1];
|
||||
|
||||
Debug::log("Handling as youtube: $vid_id", Debug::$LOG_VERBOSE);
|
||||
|
||||
$iframe = $doc->createElement("iframe");
|
||||
$iframe->setAttribute("class", "youtube-player");
|
||||
$iframe->setAttribute("type", "text/html");
|
||||
$iframe->setAttribute("width", "640");
|
||||
$iframe->setAttribute("height", "385");
|
||||
$iframe->setAttribute("src", "https://www.youtube.com/embed/$vid_id");
|
||||
$iframe->setAttribute("allowfullscreen", "1");
|
||||
$iframe->setAttribute("frameborder", "0");
|
||||
|
||||
$br = $doc->createElement('br');
|
||||
$entry->parentNode->insertBefore($iframe, $entry);
|
||||
$entry->parentNode->insertBefore($br, $entry);
|
||||
|
||||
$found = true;
|
||||
}
|
||||
|
||||
if (!$found && preg_match("/\.(jpg|jpeg|gif|png)(\?[0-9][0-9]*)?$/i", $entry->getAttribute("href")) ||
|
||||
mb_strpos($entry->getAttribute("href"), "i.reddituploads.com") !== FALSE ||
|
||||
mb_strpos($this->get_content_type($entry->getAttribute("href")), "image/") !== FALSE) {
|
||||
|
||||
Debug::log("Handling as a picture", Debug::$LOG_VERBOSE);
|
||||
|
||||
$img = $doc->createElement('img');
|
||||
$img->setAttribute("src", $entry->getAttribute("href"));
|
||||
|
||||
$br = $doc->createElement('br');
|
||||
$entry->parentNode->insertBefore($img, $entry);
|
||||
$entry->parentNode->insertBefore($br, $entry);
|
||||
|
||||
$found = true;
|
||||
}
|
||||
|
||||
// imgur via link rel="image_src" href="..."
|
||||
if (!$found && preg_match("/imgur/", $entry->getAttribute("href"))) {
|
||||
|
||||
Debug::log("handling as imgur page/whatever", Debug::$LOG_VERBOSE);
|
||||
|
||||
$content = fetch_file_contents(["url" => $entry->getAttribute("href"),
|
||||
"http_accept" => "text/*"]);
|
||||
|
||||
if ($content) {
|
||||
$cdoc = new DOMDocument();
|
||||
|
||||
if (@$cdoc->loadHTML($content)) {
|
||||
$cxpath = new DOMXPath($cdoc);
|
||||
|
||||
$rel_image = $cxpath->query("//link[@rel='image_src']")->item(0);
|
||||
|
||||
if ($rel_image) {
|
||||
|
||||
$img = $doc->createElement('img');
|
||||
$img->setAttribute("src", $rel_image->getAttribute("href"));
|
||||
|
||||
$br = $doc->createElement('br');
|
||||
$entry->parentNode->insertBefore($img, $entry);
|
||||
$entry->parentNode->insertBefore($br, $entry);
|
||||
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wtf is this even
|
||||
if (!$found && preg_match("/^https?:\/\/gyazo\.com\/([^\.\/]+$)/", $entry->getAttribute("href"), $matches)) {
|
||||
$img_id = $matches[1];
|
||||
|
||||
Debug::log("handling as gyazo: $img_id", Debug::$LOG_VERBOSE);
|
||||
|
||||
$img = $doc->createElement('img');
|
||||
$img->setAttribute("src", "https://i.gyazo.com/$img_id.jpg");
|
||||
|
||||
$br = $doc->createElement('br');
|
||||
$entry->parentNode->insertBefore($img, $entry);
|
||||
$entry->parentNode->insertBefore($br, $entry);
|
||||
|
||||
$found = true;
|
||||
}
|
||||
|
||||
// let's try meta properties
|
||||
if (!$found) {
|
||||
Debug::log("looking for meta og:image", Debug::$LOG_VERBOSE);
|
||||
|
||||
$content = fetch_file_contents(["url" => $entry->getAttribute("href"),
|
||||
"http_accept" => "text/*"]);
|
||||
|
||||
if ($content) {
|
||||
$cdoc = new DOMDocument();
|
||||
|
||||
if (@$cdoc->loadHTML($content)) {
|
||||
$cxpath = new DOMXPath($cdoc);
|
||||
|
||||
$og_image = $cxpath->query("//meta[@property='og:image']")->item(0);
|
||||
$og_video = $cxpath->query("//meta[@property='og:video']")->item(0);
|
||||
|
||||
if ($og_video) {
|
||||
|
||||
$source_stream = $og_video->getAttribute("content");
|
||||
|
||||
if ($source_stream) {
|
||||
|
||||
if ($og_image) {
|
||||
$poster_url = $og_image->getAttribute("content");
|
||||
} else {
|
||||
$poster_url = false;
|
||||
}
|
||||
|
||||
$this->handle_as_video($doc, $entry, $source_stream, $poster_url);
|
||||
$found = true;
|
||||
}
|
||||
|
||||
} else if ($og_image) {
|
||||
|
||||
$og_src = $og_image->getAttribute("content");
|
||||
|
||||
if ($og_src) {
|
||||
$img = $doc->createElement('img');
|
||||
$img->setAttribute("src", $og_src);
|
||||
|
||||
$br = $doc->createElement('br');
|
||||
$entry->parentNode->insertBefore($img, $entry);
|
||||
$entry->parentNode->insertBefore($br, $entry);
|
||||
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// remove tiny thumbnails
|
||||
if ($entry->hasAttribute("src")) {
|
||||
if ($entry->parentNode && $entry->parentNode->parentNode) {
|
||||
$entry->parentNode->parentNode->removeChild($entry->parentNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
function hook_article_filter($article) {
|
||||
|
||||
if (strpos($article["link"], "reddit.com/r/") !== FALSE) {
|
||||
$doc = new DOMDocument();
|
||||
@$doc->loadHTML($article["content"]);
|
||||
$xpath = new DOMXPath($doc);
|
||||
|
||||
$content_link = $xpath->query("(//a[contains(., '[link]')])")->item(0);
|
||||
|
||||
if ($this->host->get($this, "enable_content_dupcheck")) {
|
||||
|
||||
if ($content_link) {
|
||||
$content_href = $content_link->getAttribute("href");
|
||||
$entry_guid = $article["guid_hashed"];
|
||||
$owner_uid = $article["owner_uid"];
|
||||
|
||||
if (DB_TYPE == "pgsql") {
|
||||
$interval_qpart = "date_entered < NOW() - INTERVAL '1 day'";
|
||||
} else {
|
||||
$interval_qpart = "date_entered < DATE_SUB(NOW(), INTERVAL 1 DAY)";
|
||||
}
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT COUNT(id) AS cid
|
||||
FROM ttrss_entries, ttrss_user_entries WHERE
|
||||
ref_id = id AND
|
||||
$interval_qpart AND
|
||||
guid != ? AND
|
||||
owner_uid = ? AND
|
||||
content LIKE ?");
|
||||
|
||||
$sth->execute([$entry_guid, $owner_uid, "%href=\"$content_href\">[link]%"]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
$num_found = $row['cid'];
|
||||
|
||||
if ($num_found > 0) $article["force_catchup"] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$found = $this->inline_stuff($article, $doc, $xpath);
|
||||
|
||||
$node = $doc->getElementsByTagName('body')->item(0);
|
||||
|
||||
if ($node && $found) {
|
||||
$article["content"] = $doc->saveHTML($node);
|
||||
} else if ($content_link) {
|
||||
$article = $this->readability($article, $content_link->getAttribute("href"), $doc, $xpath);
|
||||
}
|
||||
}
|
||||
|
||||
return $article;
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
private function handle_as_video($doc, $entry, $source_stream, $poster_url = false) {
|
||||
|
||||
Debug::log("handle_as_video: $source_stream", Debug::$LOG_VERBOSE);
|
||||
|
||||
$video = $doc->createElement('video');
|
||||
$video->setAttribute("autoplay", "1");
|
||||
$video->setAttribute("controls", "1");
|
||||
$video->setAttribute("loop", "1");
|
||||
|
||||
if ($poster_url) $video->setAttribute("poster", $poster_url);
|
||||
|
||||
$source = $doc->createElement('source');
|
||||
$source->setAttribute("src", $source_stream);
|
||||
$source->setAttribute("type", "video/mp4");
|
||||
|
||||
$video->appendChild($source);
|
||||
|
||||
$br = $doc->createElement('br');
|
||||
$entry->parentNode->insertBefore($video, $entry);
|
||||
$entry->parentNode->insertBefore($br, $entry);
|
||||
|
||||
$img = $doc->createElement('img');
|
||||
$img->setAttribute("src",
|
||||
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D");
|
||||
|
||||
$entry->parentNode->insertBefore($img, $entry);
|
||||
}
|
||||
|
||||
function testurl() {
|
||||
$url = htmlspecialchars($_REQUEST["url"]);
|
||||
|
||||
header("Content-type: text/plain");
|
||||
|
||||
print "URL: $url\n";
|
||||
|
||||
$doc = new DOMDocument();
|
||||
@$doc->loadHTML("<html><body><a href=\"$url\">[link]</a></body>");
|
||||
$xpath = new DOMXPath($doc);
|
||||
|
||||
$found = $this->inline_stuff([], $doc, $xpath);
|
||||
|
||||
print "Inline result: $found\n";
|
||||
|
||||
if (!$found) {
|
||||
print "\nReadability result:\n";
|
||||
|
||||
$article = $this->readability([], $url, $doc, $xpath);
|
||||
|
||||
print_r($article);
|
||||
} else {
|
||||
print "\nResulting HTML:\n";
|
||||
|
||||
print $doc->saveHTML();
|
||||
}
|
||||
}
|
||||
|
||||
private function get_header($url, $useragent = SELF_USER_AGENT, $header) {
|
||||
$ret = false;
|
||||
|
||||
if (function_exists("curl_init") && !defined("NO_CURL")) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
curl_setopt($ch, CURLOPT_NOBODY, true);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("open_basedir"));
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
|
||||
|
||||
@curl_exec($ch);
|
||||
$ret = curl_getinfo($ch, $header);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
private function get_content_type($url, $useragent = SELF_USER_AGENT) {
|
||||
return $this->get_header($url, $useragent, CURLINFO_CONTENT_TYPE);
|
||||
}
|
||||
|
||||
private function get_location($url, $useragent = SELF_USER_AGENT) {
|
||||
return $this->get_header($url, $useragent, CURLINFO_EFFECTIVE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
private function readability($article, $url, $doc, $xpath, $debug = false) {
|
||||
|
||||
if (!defined('NO_CURL') && function_exists("curl_init") && $this->host->get($this, "enable_readability") &&
|
||||
mb_strlen(strip_tags($article["content"])) <= 150) {
|
||||
|
||||
// do not try to embed posts linking back to other reddit posts
|
||||
// readability.php requires PHP 5.6
|
||||
if ($url && strpos($url, "reddit.com") === FALSE && version_compare(PHP_VERSION, '5.6.0', '>=')) {
|
||||
|
||||
/* link may lead to a huge video file or whatever, we need to check content type before trying to
|
||||
parse it which p much requires curl */
|
||||
|
||||
$useragent_compat = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)";
|
||||
|
||||
$content_type = $this->get_content_type($url, $useragent_compat);
|
||||
|
||||
if ($content_type && strpos($content_type, "text/html") !== FALSE) {
|
||||
|
||||
$tmp = fetch_file_contents(["url" => $url,
|
||||
"useragent" => $useragent_compat,
|
||||
"http_accept" => "text/html"]);
|
||||
|
||||
Debug::log("tmplen: " . mb_strlen($tmp), Debug::$LOG_VERBOSE);
|
||||
|
||||
if ($tmp && mb_strlen($tmp) < 1024 * 500) {
|
||||
|
||||
$r = new Readability(new Configuration());
|
||||
|
||||
try {
|
||||
if ($r->parse($tmp)) {
|
||||
|
||||
$tmpxpath = new DOMXPath($r->getDOMDocument());
|
||||
|
||||
$entries = $tmpxpath->query('(//a[@href]|//img[@src])');
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry->hasAttribute("href")) {
|
||||
$entry->setAttribute("href",
|
||||
rewrite_relative_url($url, $entry->getAttribute("href")));
|
||||
|
||||
}
|
||||
|
||||
if ($entry->hasAttribute("src")) {
|
||||
$entry->setAttribute("src",
|
||||
rewrite_relative_url($url, $entry->getAttribute("src")));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$article["content"] = $r->getContent() . "<hr/>" . $article["content"];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $article;
|
||||
}
|
||||
}
|
91
plugins/af_tumblr_1280/init.php
Executable file
91
plugins/af_tumblr_1280/init.php
Executable file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
class Af_Tumblr_1280 extends Plugin {
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Replace Tumblr pictures and videos with largest size if available (requires CURL)",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function flags() {
|
||||
return array("needs_curl" => true);
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
if (function_exists("curl_init")) {
|
||||
$host->add_hook($host::HOOK_ARTICLE_FILTER, $this);
|
||||
}
|
||||
}
|
||||
|
||||
function hook_article_filter($article) {
|
||||
|
||||
if (!function_exists("curl_init") || ini_get("open_basedir"))
|
||||
return $article;
|
||||
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML('<?xml encoding="UTF-8">' . $article["content"]);
|
||||
|
||||
$found = false;
|
||||
|
||||
if ($doc) {
|
||||
$xpath = new DOMXpath($doc);
|
||||
|
||||
$images = $xpath->query('(//img[contains(@src, \'media.tumblr.com\')])');
|
||||
|
||||
foreach ($images as $img) {
|
||||
$src = $img->getAttribute("src");
|
||||
|
||||
$test_src = preg_replace("/_\d{3}.(jpg|gif|png)/", "_1280.$1", $src);
|
||||
|
||||
if ($src != $test_src) {
|
||||
|
||||
$ch = curl_init($test_src);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
curl_setopt($ch, CURLOPT_NOBODY, true);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, SELF_USER_AGENT);
|
||||
|
||||
@$result = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if ($result && $http_code == 200) {
|
||||
$img->setAttribute("src", $test_src);
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$video_sources = $xpath->query('//video/source[contains(@src, \'.tumblr.com/video_file\')]');
|
||||
|
||||
foreach ($video_sources as $source) {
|
||||
$src = $source->getAttribute("src");
|
||||
|
||||
$new_src = preg_replace("/\/\d{3}$/", "", $src);
|
||||
|
||||
if ($src != $new_src) {
|
||||
$source->setAttribute("src", $new_src);
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($found) {
|
||||
$doc->removeChild($doc->firstChild); //remove doctype
|
||||
$article["content"] = $doc->saveHTML();
|
||||
}
|
||||
}
|
||||
|
||||
return $article;
|
||||
|
||||
}
|
||||
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
84
plugins/af_unburn/init.php
Executable file
84
plugins/af_unburn/init.php
Executable file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
class Af_Unburn extends Plugin {
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Resolves feedburner and similar feed redirector URLs (requires CURL)",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function flags() {
|
||||
return array("needs_curl" => true);
|
||||
}
|
||||
|
||||
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 (defined('NO_CURL') || !function_exists("curl_init") || ini_get("open_basedir"))
|
||||
return $article;
|
||||
|
||||
if ((strpos($article["link"], "feedproxy.google.com") !== FALSE ||
|
||||
strpos($article["link"], "/~r/") !== FALSE ||
|
||||
strpos($article["link"], "feedsportal.com") !== FALSE)) {
|
||||
|
||||
$ch = curl_init($article["link"]);
|
||||
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_NOBODY, true);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, SELF_USER_AGENT);
|
||||
|
||||
if (defined('_CURL_HTTP_PROXY')) {
|
||||
curl_setopt($ch, CURLOPT_PROXY, _CURL_HTTP_PROXY);
|
||||
}
|
||||
|
||||
@curl_exec($ch);
|
||||
|
||||
$real_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($real_url) {
|
||||
/* remove the rest of it */
|
||||
|
||||
$query = parse_url($real_url, PHP_URL_QUERY);
|
||||
|
||||
if ($query && strpos($query, "utm_source") !== FALSE) {
|
||||
$args = array();
|
||||
parse_str($query, $args);
|
||||
|
||||
foreach (array("utm_source", "utm_medium", "utm_campaign") as $param) {
|
||||
if (isset($args[$param])) unset($args[$param]);
|
||||
}
|
||||
|
||||
$new_query = http_build_query($args);
|
||||
|
||||
if ($new_query != $query) {
|
||||
$real_url = str_replace("?$query", "?$new_query", $real_url);
|
||||
}
|
||||
}
|
||||
|
||||
$real_url = preg_replace("/\?$/", "", $real_url);
|
||||
|
||||
$article["plugin_data"] = "unburn,$owner_uid:" . $article["plugin_data"];
|
||||
$article["link"] = $real_url;
|
||||
}
|
||||
}
|
||||
|
||||
return $article;
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
42
plugins/af_youtube_embed/init.php
Normal file
42
plugins/af_youtube_embed/init.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
class Af_Youtube_Embed extends Plugin {
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Embed videos in Youtube RSS feeds",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_RENDER_ENCLOSURE, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function hook_render_enclosure($entry, $hide_images) {
|
||||
|
||||
$matches = array();
|
||||
|
||||
if (preg_match("/\/\/www\.youtube\.com\/v\/([\w-]+)/", $entry["url"], $matches) ||
|
||||
preg_match("/\/\/www\.youtube\.com\/watch?v=([\w-]+)/", $entry["url"], $matches) ||
|
||||
preg_match("/\/\/youtu.be\/([\w-]+)/", $entry["url"], $matches)) {
|
||||
|
||||
$vid_id = $matches[1];
|
||||
|
||||
return "<iframe class=\"youtube-player\"
|
||||
type=\"text/html\" width=\"640\" height=\"385\"
|
||||
src=\"https://www.youtube.com/embed/$vid_id\"
|
||||
allowfullscreen frameborder=\"0\"></iframe>";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
259
plugins/af_zz_imgproxy/init.php
Executable file
259
plugins/af_zz_imgproxy/init.php
Executable file
@ -0,0 +1,259 @@
|
||||
<?php
|
||||
class Af_Zz_ImgProxy extends Plugin {
|
||||
|
||||
/* @var PluginHost $host */
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Load insecure images via built-in proxy",
|
||||
"fox");
|
||||
}
|
||||
|
||||
private $ssl_known_whitelist = "imgur.com gfycat.com i.reddituploads.com pbs.twimg.com i.redd.it i.sli.mg media.tumblr.com";
|
||||
|
||||
function is_public_method($method) {
|
||||
return $method === "imgproxy";
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_RENDER_ARTICLE, $this);
|
||||
$host->add_hook($host::HOOK_RENDER_ARTICLE_CDM, $this);
|
||||
$host->add_hook($host::HOOK_ENCLOSURE_ENTRY, $this);
|
||||
|
||||
$host->add_hook($host::HOOK_PREFS_TAB, $this);
|
||||
}
|
||||
|
||||
function hook_enclosure_entry($enc) {
|
||||
if (preg_match("/image/", $enc["content_type"])) {
|
||||
$proxy_all = $this->host->get($this, "proxy_all");
|
||||
|
||||
$enc["content_url"] = $this->rewrite_url_if_needed($enc["content_url"], $proxy_all);
|
||||
}
|
||||
|
||||
return $enc;
|
||||
}
|
||||
|
||||
function hook_render_article($article) {
|
||||
return $this->hook_render_article_cdm($article);
|
||||
}
|
||||
|
||||
public function imgproxy() {
|
||||
|
||||
$url = rewrite_relative_url(get_self_url_prefix(), $_REQUEST["url"]);
|
||||
|
||||
// called without user context, let's just redirect to original URL
|
||||
if (!$_SESSION["uid"]) {
|
||||
header("Location: $url");
|
||||
return;
|
||||
}
|
||||
|
||||
$local_filename = CACHE_DIR . "/images/" . sha1($url);
|
||||
|
||||
if ($_REQUEST["debug"] == "1") { print $url . "\n" . $local_filename; die; }
|
||||
|
||||
header("Content-Disposition: inline; filename=\"".basename($local_filename)."\"");
|
||||
|
||||
if (file_exists($local_filename)) {
|
||||
|
||||
send_local_file($local_filename);
|
||||
|
||||
} else {
|
||||
$data = fetch_file_contents(["url" => $url, "max_size" => MAX_CACHE_FILE_SIZE]);
|
||||
|
||||
if ($data) {
|
||||
|
||||
$disable_cache = $this->host->get($this, "disable_cache");
|
||||
|
||||
if (!$disable_cache && strlen($data) > MIN_CACHE_FILE_SIZE) {
|
||||
if (file_put_contents($local_filename, $data)) {
|
||||
$mimetype = mime_content_type($local_filename);
|
||||
header("Content-type: $mimetype");
|
||||
}
|
||||
}
|
||||
|
||||
print $data;
|
||||
} else {
|
||||
global $fetch_last_error;
|
||||
global $fetch_last_error_code;
|
||||
global $fetch_last_error_content;
|
||||
|
||||
if (function_exists("imagecreate") && !isset($_REQUEST["text"])) {
|
||||
$img = imagecreate(450, 75);
|
||||
|
||||
/*$bg =*/ imagecolorallocate($img, 255, 255, 255);
|
||||
$textcolor = imagecolorallocate($img, 255, 0, 0);
|
||||
|
||||
imagerectangle($img, 0, 0, 450-1, 75-1, $textcolor);
|
||||
|
||||
imagestring($img, 5, 5, 5, "Proxy request failed", $textcolor);
|
||||
imagestring($img, 5, 5, 30, truncate_middle($url, 46, "..."), $textcolor);
|
||||
imagestring($img, 5, 5, 55, "HTTP Code: $fetch_last_error_code", $textcolor);
|
||||
|
||||
header("Content-type: image/png");
|
||||
print imagepng($img);
|
||||
imagedestroy($img);
|
||||
|
||||
} else {
|
||||
header("Content-type: text/html");
|
||||
|
||||
http_response_code(400);
|
||||
|
||||
print "<h1>Proxy request failed.</h1>";
|
||||
print "<p>Fetch error $fetch_last_error ($fetch_last_error_code)</p>";
|
||||
print "<p>URL: $url</p>";
|
||||
print "<textarea cols='80' rows='25'>" . htmlspecialchars($fetch_last_error_content) . "</textarea>";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rewrite_url_if_needed($url, $all_remote = false) {
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||
|
||||
if ($all_remote) {
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
$self_host = parse_url(get_self_url_prefix(), PHP_URL_HOST);
|
||||
|
||||
$is_remote = $host != $self_host;
|
||||
} else {
|
||||
$is_remote = false;
|
||||
}
|
||||
|
||||
if (($scheme != 'https' && $scheme != "") || $is_remote) {
|
||||
if (strpos($url, "data:") !== 0) {
|
||||
$parts = parse_url($url);
|
||||
|
||||
foreach (explode(" " , $this->ssl_known_whitelist) as $host) {
|
||||
if (substr(strtolower($parts['host']), -strlen($host)) === strtolower($host)) {
|
||||
$parts['scheme'] = 'https';
|
||||
$url = build_url($parts);
|
||||
if ($all_remote && $is_remote) {
|
||||
break;
|
||||
} else {
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return get_self_url_prefix() . "/public.php?op=pluginhandler&plugin=af_zz_imgproxy&pmethod=imgproxy&url=" .
|
||||
urlencode($url);
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function hook_render_article_cdm($article, $api_mode = false) {
|
||||
|
||||
$need_saving = false;
|
||||
$proxy_all = $this->host->get($this, "proxy_all");
|
||||
|
||||
$doc = new DOMDocument();
|
||||
if (@$doc->loadHTML('<?xml encoding="UTF-8">' . $article["content"])) {
|
||||
$xpath = new DOMXPath($doc);
|
||||
$imgs = $xpath->query("//img[@src]");
|
||||
|
||||
foreach ($imgs as $img) {
|
||||
$new_src = $this->rewrite_url_if_needed($img->getAttribute("src"), $proxy_all);
|
||||
|
||||
if ($new_src != $img->getAttribute("src")) {
|
||||
$img->setAttribute("src", $new_src);
|
||||
$img->removeAttribute("srcset");
|
||||
|
||||
$need_saving = true;
|
||||
}
|
||||
}
|
||||
|
||||
$vids = $xpath->query("(//video|//picture)");
|
||||
|
||||
foreach ($vids as $vid) {
|
||||
if ($vid->hasAttribute("poster")) {
|
||||
$new_src = $this->rewrite_url_if_needed($vid->getAttribute("poster"), $proxy_all);
|
||||
|
||||
if ($new_src != $vid->getAttribute("poster")) {
|
||||
$vid->setAttribute("poster", $new_src);
|
||||
|
||||
$need_saving = true;
|
||||
}
|
||||
}
|
||||
|
||||
$vsrcs = $xpath->query("source", $vid);
|
||||
|
||||
foreach ($vsrcs as $vsrc) {
|
||||
$new_src = $this->rewrite_url_if_needed($vsrc->getAttribute("src"), $proxy_all);
|
||||
|
||||
if ($new_src != $vsrc->getAttribute("src")) {
|
||||
$vid->setAttribute("src", $new_src);
|
||||
|
||||
$need_saving = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($need_saving) $article["content"] = $doc->saveHTML();
|
||||
|
||||
return $article;
|
||||
}
|
||||
|
||||
function hook_prefs_tab($args) {
|
||||
if ($args != "prefFeeds") return;
|
||||
|
||||
print "<div dojoType=\"dijit.layout.AccordionPane\"
|
||||
title=\"<i class='material-icons'>extension</i> ".__('Image proxy settings (af_zz_imgproxy)')."\">";
|
||||
|
||||
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_hidden("op", "pluginhandler");
|
||||
print_hidden("method", "save");
|
||||
print_hidden("plugin", "af_zz_imgproxy");
|
||||
|
||||
$proxy_all = $this->host->get($this, "proxy_all");
|
||||
print_checkbox("proxy_all", $proxy_all);
|
||||
print " <label for=\"proxy_all\">" . __("Enable proxy for all remote images.") . "</label><br/>";
|
||||
|
||||
$disable_cache = $this->host->get($this, "disable_cache");
|
||||
print_checkbox("disable_cache", $disable_cache);
|
||||
print " <label for=\"disable_cache\">" . __("Don't cache files locally.") . "</label>";
|
||||
|
||||
print "<p>"; print_button("submit", __("Save"));
|
||||
|
||||
print "</form>";
|
||||
|
||||
print "</div>";
|
||||
}
|
||||
|
||||
function save() {
|
||||
$proxy_all = checkbox_to_sql_bool($_POST["proxy_all"]);
|
||||
$disable_cache = checkbox_to_sql_bool($_POST["disable_cache"]);
|
||||
|
||||
$this->host->set($this, "proxy_all", $proxy_all, false);
|
||||
$this->host->set($this, "disable_cache", $disable_cache);
|
||||
|
||||
echo __("Configuration saved");
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
}
|
41
plugins/af_zz_noautoplay/init.js
Normal file
41
plugins/af_zz_noautoplay/init.js
Normal file
@ -0,0 +1,41 @@
|
||||
require(['dojo/_base/kernel', 'dojo/ready'], function (dojo, ready) {
|
||||
ready(function () {
|
||||
PluginHost.register(PluginHost.HOOK_ARTICLE_RENDERED_CDM, function (row) {
|
||||
if (row) {
|
||||
console.log("af_zz_noautoplay!");
|
||||
console.log(row);
|
||||
|
||||
const videos = row.getElementsByTagName("video");
|
||||
console.log(row.innerHTML);
|
||||
|
||||
for (i = 0; i < videos.length; i++) {
|
||||
|
||||
videos[i].removeAttribute("autoplay");
|
||||
videos[i].pause();
|
||||
videos[i].onclick = function () {
|
||||
this.paused ? this.play() : this.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
PluginHost.register(PluginHost.HOOK_ARTICLE_RENDERED, function (row) {
|
||||
if (row) {
|
||||
const videos = row.getElementsByTagName("video");
|
||||
|
||||
for (i = 0; i < videos.length; i++) {
|
||||
videos[i].removeAttribute("autoplay");
|
||||
videos[i].pause();
|
||||
videos[i].onclick = function () {
|
||||
this.paused ? this.play() : this.pause();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
23
plugins/af_zz_noautoplay/init.php
Normal file
23
plugins/af_zz_noautoplay/init.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
class Af_Zz_NoAutoPlay extends Plugin {
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Don't autoplay HTML5 videos",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
}
|
||||
|
||||
function get_js() {
|
||||
return file_get_contents(__DIR__ . "/init.js");
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
25
plugins/af_zz_vidmute/init.js
Normal file
25
plugins/af_zz_vidmute/init.js
Normal file
@ -0,0 +1,25 @@
|
||||
require(['dojo/_base/kernel', 'dojo/ready'], function (dojo, ready) {
|
||||
ready(function () {
|
||||
PluginHost.register(PluginHost.HOOK_ARTICLE_RENDERED_CDM, function (row) {
|
||||
if (row) {
|
||||
|
||||
row.select("video").each(function (v) {
|
||||
v.muted = true;
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
PluginHost.register(PluginHost.HOOK_ARTICLE_RENDERED, function (row) {
|
||||
if (row) {
|
||||
|
||||
row.select("video").each(function (v) {
|
||||
v.muted = true;
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
23
plugins/af_zz_vidmute/init.php
Normal file
23
plugins/af_zz_vidmute/init.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
class Af_Zz_VidMute extends Plugin {
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Mute audio in HTML5 videos",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
}
|
||||
|
||||
function get_js() {
|
||||
return file_get_contents(__DIR__ . "/init.js");
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
225
plugins/auth_internal/init.php
Normal file
225
plugins/auth_internal/init.php
Normal file
@ -0,0 +1,225 @@
|
||||
<?php
|
||||
class Auth_Internal extends Plugin implements IAuthModule {
|
||||
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Authenticates against internal tt-rss database",
|
||||
"fox",
|
||||
true);
|
||||
}
|
||||
|
||||
/* @var PluginHost $host */
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
$this->pdo = Db::pdo();
|
||||
|
||||
$host->add_hook($host::HOOK_AUTH_USER, $this);
|
||||
}
|
||||
|
||||
function authenticate($login, $password) {
|
||||
|
||||
$pwd_hash1 = encrypt_password($password);
|
||||
$pwd_hash2 = encrypt_password($password, $login);
|
||||
$otp = $_REQUEST["otp"];
|
||||
|
||||
if (get_schema_version() > 96) {
|
||||
if (!defined('AUTH_DISABLE_OTP') || !AUTH_DISABLE_OTP) {
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT otp_enabled,salt FROM ttrss_users WHERE
|
||||
login = ?");
|
||||
$sth->execute([$login]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
|
||||
$base32 = new \OTPHP\Base32();
|
||||
|
||||
$otp_enabled = $row['otp_enabled'];
|
||||
$secret = $base32->encode(sha1($row['salt']));
|
||||
|
||||
$topt = new \OTPHP\TOTP($secret);
|
||||
$otp_check = $topt->now();
|
||||
|
||||
if ($otp_enabled) {
|
||||
if ($otp) {
|
||||
if ($otp != $otp_check) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$return = urlencode($_REQUEST["return"]);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Agriget</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
</head>
|
||||
<?php echo stylesheet_tag("css/default.css") ?>
|
||||
<body class="ttrss_utility otp">
|
||||
<h1><?php echo __("Authentication") ?></h1>
|
||||
<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"]) ?>">
|
||||
|
||||
<fieldset>
|
||||
<label><?php echo __("Please enter your one time password:") ?></label>
|
||||
<input autocomplete="off" size="6" name="otp" value=""/>
|
||||
<input type="submit" value="Continue"/>
|
||||
</fieldset>
|
||||
</form></div>
|
||||
<script type="text/javascript">
|
||||
document.forms[0].otp.focus();
|
||||
</script>
|
||||
<?php
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (get_schema_version() > 87) {
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT salt FROM ttrss_users WHERE login = ?");
|
||||
$sth->execute([$login]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
$salt = $row['salt'];
|
||||
|
||||
if ($salt == "") {
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_users WHERE
|
||||
login = ? AND (pwd_hash = ? OR pwd_hash = ?)");
|
||||
|
||||
$sth->execute([$login, $pwd_hash1, $pwd_hash2]);
|
||||
|
||||
// verify and upgrade password to new salt base
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
// upgrade password to MODE2
|
||||
|
||||
$user_id = $row['id'];
|
||||
|
||||
$salt = substr(bin2hex(get_random_bytes(125)), 0, 250);
|
||||
$pwd_hash = encrypt_password($password, $salt, true);
|
||||
|
||||
$sth = $this->pdo->prepare("UPDATE ttrss_users SET
|
||||
pwd_hash = ?, salt = ? WHERE login = ?");
|
||||
|
||||
$sth->execute([$pwd_hash, $salt, $login]);
|
||||
|
||||
return $user_id;
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
$pwd_hash = encrypt_password($password, $salt, true);
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT id
|
||||
FROM ttrss_users WHERE
|
||||
login = ? AND pwd_hash = ?");
|
||||
$sth->execute([$login, $pwd_hash]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
return $row['id'];
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
$sth = $this->pdo->prepare("SELECT id
|
||||
FROM ttrss_users WHERE
|
||||
login = ? AND (pwd_hash = ? OR pwd_hash = ?)");
|
||||
|
||||
$sth->execute([$login, $pwd_hash1, $pwd_hash2]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
return $row['id'];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$sth = $this->pdo->prepare("SELECT id
|
||||
FROM ttrss_users WHERE
|
||||
login = ? AND (pwd_hash = ? OR pwd_hash = ?)");
|
||||
|
||||
$sth->execute([$login, $pwd_hash1, $pwd_hash2]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
return $row['id'];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function check_password($owner_uid, $password) {
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT salt,login FROM ttrss_users WHERE
|
||||
id = ?");
|
||||
$sth->execute([$owner_uid]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
|
||||
$salt = $row['salt'];
|
||||
$login = $row['login'];
|
||||
|
||||
if (!$salt) {
|
||||
$password_hash1 = encrypt_password($password);
|
||||
$password_hash2 = encrypt_password($password, $login);
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_users WHERE
|
||||
id = ? AND (pwd_hash = ? OR pwd_hash = ?)");
|
||||
|
||||
$sth->execute([$owner_uid, $password_hash1, $password_hash2]);
|
||||
|
||||
return $sth->fetch();
|
||||
|
||||
} else {
|
||||
$password_hash = encrypt_password($password, $salt, true);
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_users WHERE
|
||||
id = ? AND pwd_hash = ?");
|
||||
|
||||
$sth->execute([$owner_uid, $password_hash]);
|
||||
|
||||
return $sth->fetch();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function change_password($owner_uid, $old_password, $new_password) {
|
||||
|
||||
if ($this->check_password($owner_uid, $old_password)) {
|
||||
|
||||
$new_salt = substr(bin2hex(get_random_bytes(125)), 0, 250);
|
||||
$new_password_hash = encrypt_password($new_password, $new_salt, true);
|
||||
|
||||
$sth = $this->pdo->prepare("UPDATE ttrss_users SET
|
||||
pwd_hash = ?, salt = ?, otp_enabled = false
|
||||
WHERE id = ?");
|
||||
$sth->execute([$new_password_hash, $new_salt, $owner_uid]);
|
||||
|
||||
$_SESSION["pwd_hash"] = $new_password_hash;
|
||||
|
||||
return __("Password has been changed.");
|
||||
} else {
|
||||
return "ERROR: ".__('Old password is incorrect.');
|
||||
}
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
403
plugins/auth_ldap/init.php
Normal file
403
plugins/auth_ldap/init.php
Normal file
@ -0,0 +1,403 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Tiny Tiny RSS plugin for LDAP authentication
|
||||
* @author tsmgeek (tsmgeek@gmail.com)
|
||||
* @author hydrian (ben.tyger@tygerclan.net)
|
||||
* @copyright GPL2
|
||||
* Requires php-ldap
|
||||
* @version 2.00
|
||||
*/
|
||||
/**
|
||||
* Configuration
|
||||
* Put the following options in config.php and customize them for your environment
|
||||
*
|
||||
* define('LDAP_AUTH_SERVER_URI', 'ldaps://LDAPServerHostname:port/');
|
||||
* define('LDAP_AUTH_USETLS', FALSE); // Enable TLS Support for ldaps://
|
||||
* define('LDAP_AUTH_ALLOW_UNTRUSTED_CERT', TRUE); // Allows untrusted certificate
|
||||
* define('LDAP_AUTH_BASEDN', 'dc=example,dc=com');
|
||||
* define('LDAP_AUTH_ANONYMOUSBEFOREBIND', FALSE);
|
||||
* // ??? will be replaced with the entered username(escaped) at login
|
||||
* define('LDAP_AUTH_SEARCHFILTER', '(&(objectClass=person)(uid=???))');
|
||||
* // Optional configuration
|
||||
* define('LDAP_AUTH_BINDDN', 'cn=serviceaccount,dc=example,dc=com');
|
||||
* define('LDAP_AUTH_BINDPW', 'ServiceAccountsPassword');
|
||||
* define('LDAP_AUTH_LOGIN_ATTRIB', 'uid');
|
||||
* define('LDAP_AUTH_LOG_ATTEMPTS', FALSE);
|
||||
* Enable Debug Logging
|
||||
* define('LDAP_AUTH_DEBUG', FALSE);
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Notes -
|
||||
* LDAP search does not support follow ldap referals. Referals are disabled to
|
||||
* allow proper login. This is particular to Active Directory.
|
||||
*
|
||||
* Also group membership can be supported if the user object contains the
|
||||
* the group membership via attributes. The following LDAP servers can
|
||||
* support this.
|
||||
* * Active Directory
|
||||
* * OpenLDAP support with MemberOf Overlay
|
||||
*
|
||||
*/
|
||||
class Auth_Ldap extends Plugin implements IAuthModule {
|
||||
|
||||
private $link;
|
||||
private $host;
|
||||
private $base;
|
||||
private $logClass;
|
||||
private $ldapObj = NULL;
|
||||
private $_debugMode;
|
||||
private $_serviceBindDN;
|
||||
private $_serviceBindPass;
|
||||
private $_baseDN;
|
||||
private $_useTLS;
|
||||
private $_host;
|
||||
private $_port;
|
||||
private $_scheme;
|
||||
private $_schemaCacheEnabled;
|
||||
private $_anonBeforeBind;
|
||||
private $_allowUntrustedCerts;
|
||||
private $_ldapLoginAttrib;
|
||||
|
||||
function about() {
|
||||
return array(0.05,
|
||||
"Authenticates against an LDAP server (configured in config.php)",
|
||||
"hydrian",
|
||||
true);
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->link = $host->get_link();
|
||||
$this->host = $host;
|
||||
$this->base = new Auth_Base($this->link);
|
||||
|
||||
$host->add_hook($host::HOOK_AUTH_USER, $this);
|
||||
}
|
||||
|
||||
private function _log($msg, $level = E_USER_NOTICE, $file = '', $line = 0, $context = '') {
|
||||
$loggerFunction = Logger::get();
|
||||
if (is_object($loggerFunction)) {
|
||||
$loggerFunction->log_error($level, $msg, $file, $line, $context);
|
||||
} else {
|
||||
trigger_error($msg, $level);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs login attempts
|
||||
* @param string $username Given username that attempts to log in to TTRSS
|
||||
* @param string $result "Logging message for type of result. (Success / Fail)"
|
||||
* @return boolean
|
||||
* @deprecated
|
||||
*
|
||||
* Now that _log support syslog and log levels and graceful fallback user.
|
||||
*/
|
||||
private function _logAttempt($username, $result) {
|
||||
|
||||
|
||||
return trigger_error('TT-RSS Login Attempt: user ' . (string) $username .
|
||||
' attempted to login (' . (string) $result . ') from ' . (string) $ip, E_USER_NOTICE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $subject The subject string
|
||||
* @param string $ignore Set of characters to leave untouched
|
||||
* @param int $flags Any combination of LDAP_ESCAPE_* flags to indicate the
|
||||
* set(s) of characters to escape.
|
||||
* @return string
|
||||
**/
|
||||
function ldap_escape($subject, $ignore = '', $flags = 0)
|
||||
{
|
||||
if (!function_exists('ldap_escape')) {
|
||||
define('LDAP_ESCAPE_FILTER', 0x01);
|
||||
define('LDAP_ESCAPE_DN', 0x02);
|
||||
|
||||
static $charMaps = array(
|
||||
LDAP_ESCAPE_FILTER => array('\\', '*', '(', ')', "\x00"),
|
||||
LDAP_ESCAPE_DN => array('\\', ',', '=', '+', '<', '>', ';', '"', '#'),
|
||||
);
|
||||
|
||||
// Pre-process the char maps on first call
|
||||
if (!isset($charMaps[0])) {
|
||||
$charMaps[0] = array();
|
||||
for ($i = 0; $i < 256; $i++) {
|
||||
$charMaps[0][chr($i)] = sprintf('\\%02x', $i);;
|
||||
}
|
||||
|
||||
for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_FILTER]); $i < $l; $i++) {
|
||||
$chr = $charMaps[LDAP_ESCAPE_FILTER][$i];
|
||||
unset($charMaps[LDAP_ESCAPE_FILTER][$i]);
|
||||
$charMaps[LDAP_ESCAPE_FILTER][$chr] = $charMaps[0][$chr];
|
||||
}
|
||||
|
||||
for ($i = 0, $l = count($charMaps[LDAP_ESCAPE_DN]); $i < $l; $i++) {
|
||||
$chr = $charMaps[LDAP_ESCAPE_DN][$i];
|
||||
unset($charMaps[LDAP_ESCAPE_DN][$i]);
|
||||
$charMaps[LDAP_ESCAPE_DN][$chr] = $charMaps[0][$chr];
|
||||
}
|
||||
}
|
||||
|
||||
// Create the base char map to escape
|
||||
$flags = (int)$flags;
|
||||
$charMap = array();
|
||||
if ($flags & LDAP_ESCAPE_FILTER) {
|
||||
$charMap += $charMaps[LDAP_ESCAPE_FILTER];
|
||||
}
|
||||
if ($flags & LDAP_ESCAPE_DN) {
|
||||
$charMap += $charMaps[LDAP_ESCAPE_DN];
|
||||
}
|
||||
if (!$charMap) {
|
||||
$charMap = $charMaps[0];
|
||||
}
|
||||
|
||||
// Remove any chars to ignore from the list
|
||||
$ignore = (string)$ignore;
|
||||
for ($i = 0, $l = strlen($ignore); $i < $l; $i++) {
|
||||
unset($charMap[$ignore[$i]]);
|
||||
}
|
||||
|
||||
// Do the main replacement
|
||||
$result = strtr($subject, $charMap);
|
||||
|
||||
// Encode leading/trailing spaces if LDAP_ESCAPE_DN is passed
|
||||
if ($flags & LDAP_ESCAPE_DN) {
|
||||
if ($result[0] === ' ') {
|
||||
$result = '\\20' . substr($result, 1);
|
||||
}
|
||||
if ($result[strlen($result) - 1] === ' ') {
|
||||
$result = substr($result, 0, -1) . '\\20';
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}else{
|
||||
return ldap_escape($subject, $ignore, $flags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds client's IP address
|
||||
* @return string
|
||||
*/
|
||||
private function _getClientIP() {
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
//check ip from share internet
|
||||
|
||||
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
//to check ip is pass from proxy
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
} else {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
|
||||
return $ip;
|
||||
}
|
||||
|
||||
private function _getBindDNWord() {
|
||||
return (strlen($this->_serviceBindDN) > 0 ) ? $this->_serviceBindDN : 'anonymous DN';
|
||||
}
|
||||
|
||||
private function _getTempDir() {
|
||||
if (!sys_get_temp_dir()) {
|
||||
$tmpFile = tempnam();
|
||||
$tmpDir = dirname($tmpFile);
|
||||
unlink($tmpFile);
|
||||
unset($tmpFile);
|
||||
return $tmpDir;
|
||||
} else {
|
||||
return sys_get_temp_dir();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main Authentication method
|
||||
* Required for plugin interface
|
||||
* @param unknown $login User's username
|
||||
* @param unknown $password User's password
|
||||
* @return boolean
|
||||
*/
|
||||
function authenticate($login, $password) {
|
||||
if ($login && $password) {
|
||||
|
||||
if (!function_exists('ldap_connect')) {
|
||||
trigger_error('auth_ldap requires PHP\'s PECL LDAP package installed.');
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
//Loading configuration
|
||||
$this->_debugMode = defined('LDAP_AUTH_DEBUG') ?
|
||||
LDAP_AUTH_DEBUG : FALSE;
|
||||
|
||||
$this->_anonBeforeBind = defined('LDAP_AUTH_ANONYMOUSBEFOREBIND') ?
|
||||
LDAP_AUTH_ANONYMOUSBEFOREBIND : FALSE;
|
||||
|
||||
$this->_serviceBindDN = defined('LDAP_AUTH_BINDDN') ? LDAP_AUTH_BINDDN : null;
|
||||
$this->_serviceBindPass = defined('LDAP_AUTH_BINDPW') ? LDAP_AUTH_BINDPW : null;
|
||||
$this->_baseDN = defined('LDAP_AUTH_BASEDN') ? LDAP_AUTH_BASEDN : null;
|
||||
if (!defined('LDAP_AUTH_BASEDN')) {
|
||||
$this->_log('LDAP_AUTH_BASEDN is required and not defined.', E_USER_ERROR);
|
||||
return FALSE;
|
||||
} else {
|
||||
$this->_baseDN = LDAP_AUTH_BASEDN;
|
||||
}
|
||||
|
||||
$parsedURI = parse_url(LDAP_AUTH_SERVER_URI);
|
||||
if ($parsedURI === FALSE) {
|
||||
$this->_log('Could not parse LDAP_AUTH_SERVER_URI in config.php', E_USER_ERROR);
|
||||
return FALSE;
|
||||
}
|
||||
$this->_host = $parsedURI['host'];
|
||||
$this->_scheme = $parsedURI['scheme'];
|
||||
|
||||
if (is_int($parsedURI['port'])) {
|
||||
$this->_port = $parsedURI['port'];
|
||||
} else {
|
||||
$this->_port = ($this->_scheme === 'ldaps') ? 636 : 389;
|
||||
}
|
||||
|
||||
$this->_useTLS = defined('LDAP_AUTH_USETLS') ? LDAP_AUTH_USETLS : FALSE;
|
||||
|
||||
$this->_logAttempts = defined('LDAP_AUTH_LOG_ATTEMPTS') ?
|
||||
LDAP_AUTH_LOG_ATTEMPTS : FALSE;
|
||||
|
||||
$this->_ldapLoginAttrib = defined('LDAP_AUTH_LOGIN_ATTRIB') ?
|
||||
LDAP_AUTH_LOGIN_ATTRIB : null;
|
||||
|
||||
|
||||
/**
|
||||
Building LDAP connection
|
||||
* */
|
||||
$ldapConnParams = array(
|
||||
'host' => $this->_host,
|
||||
'basedn' => $this->_baseDN,
|
||||
'port' => $this->_port,
|
||||
'starttls' => $this->_useTLS
|
||||
);
|
||||
|
||||
if ($this->_debugMode)
|
||||
$this->_log(print_r($ldapConnParams, TRUE), E_USER_NOTICE);
|
||||
$ldapConn = @ldap_connect($this->_host, $this->_port);
|
||||
if ($ldapConn === FALSE) {
|
||||
$this->_log('Could not connect to LDAP Server: \'' . $this->_host . '\'', E_USER_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Enable LDAP protocol version 3. */
|
||||
if (!@ldap_set_option($ldapConn, LDAP_OPT_PROTOCOL_VERSION, 3)) {
|
||||
$this->_log('Failed to set LDAP Protocol version (LDAP_OPT_PROTOCOL_VERSION) to 3', E_USER_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Set referral option */
|
||||
if (!@ldap_set_option($ldapConn, LDAP_OPT_REFERRALS, FALSE)) {
|
||||
$this->_log('Failed to set LDAP Referrals (LDAP_OPT_REFERRALS) to TRUE', E_USER_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (stripos($this->_host, "ldaps:") === FALSE and $this->_useTLS) {
|
||||
if (!@ldap_start_tls($ldapConn)) {
|
||||
$this->_log('Unable to force TLS', E_USER_ERROR);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$error = @ldap_bind($ldapConn, $this->_serviceBindDN, $this->_serviceBindPass);
|
||||
if ($error === FALSE) {
|
||||
$this->_log(
|
||||
'LDAP bind(): Bind failed (' . $error . ')with DN ' . $this->_serviceBindDN, E_USER_ERROR
|
||||
);
|
||||
return FALSE;
|
||||
} else {
|
||||
$this->_log(
|
||||
'Connected to LDAP Server: ' . LDAP_AUTH_SERVER_URI . ' with ' . $this->_getBindDNWord());
|
||||
}
|
||||
|
||||
// Bind with service account if orignal connexion was anonymous
|
||||
/* if (($this->_anonBeforeBind) && (strlen($this->_bindDN > 0))) {
|
||||
$binding=$this->ldapObj->bind($this->_serviceBindDN, $this->_serviceBindPass);
|
||||
if (get_class($binding) !== 'Net_LDAP2') {
|
||||
$this->_log(
|
||||
'Cound not bind service account: '.$binding->getMessage(),E_USER_ERROR);
|
||||
return FALSE;
|
||||
} else {
|
||||
$this->_log('Bind with '.$this->_serviceBindDN.' successful.',E_USER_NOTICE);
|
||||
}
|
||||
} */
|
||||
|
||||
//Searching for user
|
||||
$filterObj = str_replace('???', $this->ldap_escape($login), LDAP_AUTH_SEARCHFILTER);
|
||||
$searchResults = @ldap_search($ldapConn, $this->_baseDN, $filterObj, array('displayName', 'title', 'sAMAccountName', $this->_ldapLoginAttrib), 0, 0, 0);
|
||||
if ($searchResults === FALSE) {
|
||||
$this->_log('LDAP Search Failed on base \'' . $this->_baseDN . '\' for \'' . $filterObj . '\'', E_USER_ERROR);
|
||||
return FALSE;
|
||||
}
|
||||
$count = @ldap_count_entries($ldapConn, $searchResults);
|
||||
if ($count === FALSE) {
|
||||
|
||||
} elseif ($count > 1) {
|
||||
$this->_log('Multiple DNs found for username ' . (string) $login, E_USER_WARNING);
|
||||
return FALSE;
|
||||
} elseif ($count === 0) {
|
||||
$this->_log((string) $login, 'Unknown User', E_USER_NOTICE);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
//Getting user's DN from search
|
||||
$userEntry = @ldap_first_entry($ldapConn, $searchResults);
|
||||
if ($userEntry === FALSE) {
|
||||
$this->_log('LDAP search(): Unable to retrieve result after searching base \'' . $this->_baseDN . '\' for \'' . $filterObj . '\'', E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
$userAttributes = @ldap_get_attributes($ldapConn, $userEntry);
|
||||
$userDN = @ldap_get_dn($ldapConn, $userEntry);
|
||||
if ($userDN == FALSE) {
|
||||
$this->_log('LDAP search(): Unable to get DN after searching base \'' . $this->_baseDN . '\' for \'' . $filterObj . '\'', E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
//Binding with user's DN.
|
||||
if ($this->_debugMode)
|
||||
$this->_log('Try to bind with user\'s DN: ' . $userDN);
|
||||
$loginAttempt = @ldap_bind($ldapConn, $userDN, $password);
|
||||
if ($loginAttempt === TRUE) {
|
||||
$this->_log('User: ' . (string) $login . ' authentication successful');
|
||||
if (strlen($this->_ldapLoginAttrib) > 0) {
|
||||
if ($this->_debugMode)
|
||||
$this->_log('Looking up TT-RSS username attribute in ' . $this->_ldapLoginAttrib);
|
||||
$ttrssUsername = $userAttributes[$this->_ldapLoginAttrib][0];
|
||||
;
|
||||
@ldap_close($ldapConn);
|
||||
if (!is_string($ttrssUsername)) {
|
||||
$this->_log('Could not find user name attribute ' . $this->_ldapLoginAttrib . ' in LDAP entry', E_USER_WARNING);
|
||||
return FALSE;
|
||||
}
|
||||
return $this->base->auto_create_user($ttrssUsername);
|
||||
} else {
|
||||
@ldap_close($ldapConn);
|
||||
return $this->base->auto_create_user($login);
|
||||
}
|
||||
} else {
|
||||
@ldap_close($ldapConn);
|
||||
$this->_log('User: ' . (string) $login . ' authentication failed');
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns plugin API version
|
||||
* Required for plugin interface
|
||||
* @return number
|
||||
*/
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
88
plugins/auth_remote/init.php
Normal file
88
plugins/auth_remote/init.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
class Auth_Remote extends Plugin implements IAuthModule {
|
||||
|
||||
private $host;
|
||||
/* @var Auth_Base $base */
|
||||
private $base;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Authenticates against remote password (e.g. supplied by Apache)",
|
||||
"fox",
|
||||
true);
|
||||
}
|
||||
|
||||
/* @var PluginHost $host */
|
||||
function init($host ) {
|
||||
$this->host = $host;
|
||||
$this->base = new Auth_Base();
|
||||
|
||||
$host->add_hook($host::HOOK_AUTH_USER, $this);
|
||||
}
|
||||
|
||||
function get_login_by_ssl_certificate() {
|
||||
$cert_serial = get_ssl_certificate_id();
|
||||
|
||||
if ($cert_serial) {
|
||||
$sth = $this->pdo->prepare("SELECT login FROM ttrss_user_prefs, ttrss_users
|
||||
WHERE pref_name = 'SSL_CERT_SERIAL' AND value = ? AND
|
||||
owner_uid = ttrss_users.id");
|
||||
$sth->execute([$cert_serial]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
return $row['login'];
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function authenticate($login, $password) {
|
||||
$try_login = $_SERVER["REMOTE_USER"];
|
||||
|
||||
// php-cgi
|
||||
if (!$try_login) $try_login = $_SERVER["REDIRECT_REMOTE_USER"];
|
||||
if (!$try_login) $try_login = $_SERVER["PHP_AUTH_USER"];
|
||||
|
||||
if (!$try_login) $try_login = $this->get_login_by_ssl_certificate();
|
||||
|
||||
if ($try_login) {
|
||||
$user_id = $this->base->auto_create_user($try_login, $password);
|
||||
|
||||
if ($user_id) {
|
||||
$_SESSION["fake_login"] = $try_login;
|
||||
$_SESSION["fake_password"] = "******";
|
||||
$_SESSION["hide_hello"] = true;
|
||||
$_SESSION["hide_logout"] = true;
|
||||
|
||||
// LemonLDAP can send user informations via HTTP HEADER
|
||||
if (defined('AUTH_AUTO_CREATE') && AUTH_AUTO_CREATE){
|
||||
// update user name
|
||||
$fullname = $_SERVER['HTTP_USER_NAME'] ? $_SERVER['HTTP_USER_NAME'] : $_SERVER['AUTHENTICATE_CN'];
|
||||
if ($fullname){
|
||||
$sth = $this->pdo->prepare("UPDATE ttrss_users SET full_name = ? WHERE id = ?");
|
||||
$sth->execute([$fullname, $user_id]);
|
||||
}
|
||||
// update user mail
|
||||
$email = $_SERVER['HTTP_USER_MAIL'] ? $_SERVER['HTTP_USER_MAIL'] : $_SERVER['AUTHENTICATE_MAIL'];
|
||||
if ($email){
|
||||
$sth = $this->pdo->prepare("UPDATE ttrss_users SET email = ? WHERE id = ?");
|
||||
$sth->execute([$email, $user_id]);
|
||||
}
|
||||
}
|
||||
|
||||
return $user_id;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
57
plugins/auto_assign_labels/init.php
Executable file
57
plugins/auto_assign_labels/init.php
Executable file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
class Auto_Assign_Labels extends Plugin {
|
||||
|
||||
/* @var PluginHost $host */
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Assign labels automatically based on article title, content, and tags",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_ARTICLE_FILTER, $this);
|
||||
}
|
||||
|
||||
function get_all_labels_filter_format($owner_uid) {
|
||||
$rv = array();
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT id, fg_color, bg_color, caption FROM ttrss_labels2 WHERE owner_uid = ?");
|
||||
$sth->execute([$owner_uid]);
|
||||
|
||||
while ($line = $sth->fetch()) {
|
||||
array_push($rv, array(Labels::label_to_feed_id($line["id"]),
|
||||
$line["caption"], $line["fg_color"], $line["bg_color"]));
|
||||
}
|
||||
|
||||
return $rv;
|
||||
}
|
||||
|
||||
|
||||
function hook_article_filter($article) {
|
||||
|
||||
$owner_uid = $article["owner_uid"];
|
||||
$labels = $this->get_all_labels_filter_format($owner_uid);
|
||||
$tags_str = join(",", $article["tags"]);
|
||||
|
||||
foreach ($labels as $label) {
|
||||
$caption = preg_quote($label[1], "/");
|
||||
|
||||
if ($caption && preg_match("/\b$caption\b/i", "$tags_str " . strip_tags($article["content"]) . " " . $article["title"])) {
|
||||
|
||||
if (!RSSUtils::labels_contains_caption($article["labels"], $caption)) {
|
||||
array_push($article["labels"], $label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $article;
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
}
|
56
plugins/bookmarklets/init.php
Normal file
56
plugins/bookmarklets/init.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
class Bookmarklets extends Plugin {
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Easy feed subscription and web page sharing using bookmarklets",
|
||||
"fox",
|
||||
false,
|
||||
"https://git.tt-rss.org/fox/tt-rss/wiki/ShareAnything");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_PREFS_TAB, $this);
|
||||
}
|
||||
|
||||
function hook_prefs_tab($args) {
|
||||
if ($args == "prefFeeds") {
|
||||
|
||||
print "<div dojoType=\"dijit.layout.AccordionPane\"
|
||||
title=\"<i class='material-icons'>bookmark</i> ".__('Bookmarklets')."\">";
|
||||
|
||||
print "<h3>" . __("Drag the link below to your browser toolbar, open the feed you're interested in in your browser and click on the link to subscribe to it.") . "</h3>";
|
||||
|
||||
$bm_subscribe_url = str_replace('%s', '', Pref_Feeds::subscribe_to_feed_url());
|
||||
|
||||
$confirm_str = str_replace("'", "\'", __('Subscribe to %s in Agriget?'));
|
||||
|
||||
$bm_url = htmlspecialchars("javascript:{if(confirm('$confirm_str'.replace('%s',window.location.href)))window.location.href='$bm_subscribe_url'+window.location.href}");
|
||||
|
||||
print "<p><label class='dijitButton'>";
|
||||
print "<a href=\"$bm_url\">" . __('Subscribe in Agriget'). "</a>";
|
||||
print "</label></p>";
|
||||
|
||||
print "<h3>" . __("Use this bookmarklet to publish arbitrary pages using Agriget") . "</h3>";
|
||||
|
||||
print "<label class='dijitButton'>";
|
||||
$bm_url = htmlspecialchars("javascript:(function(){var d=document,w=window,e=w.getSelection,k=d.getSelection,x=d.selection,s=(e?e():(k)?k():(x?x.createRange().text:0)),f='".get_self_url_prefix()."/public.php?op=sharepopup',l=d.location,e=encodeURIComponent,g=f+'&title='+((e(s))?e(s):e(document.title))+'&url='+e(l.href);function a(){if(!w.open(g,'t','toolbar=0,resizable=0,scrollbars=1,status=1,width=500,height=250')){l.href=g;}}a();})()");
|
||||
print "<a href=\"$bm_url\">" . __('Share with Agriget'). "</a>";
|
||||
print "</label>";
|
||||
|
||||
print "<button dojoType='dijit.form.Button' class='alt-info' onclick='window.open(\"https://tt-rss.org/wiki/ShareAnything\")'>
|
||||
<i class='material-icons'>help</i> ".__("More info...")."</button>";
|
||||
|
||||
print "</div>"; #pane
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
237
plugins/cache_starred_images/init.php
Executable file
237
plugins/cache_starred_images/init.php
Executable file
@ -0,0 +1,237 @@
|
||||
<?php
|
||||
class Cache_Starred_Images extends Plugin implements IHandler {
|
||||
|
||||
/* @var PluginHost $host */
|
||||
private $host;
|
||||
private $cache_dir;
|
||||
private $max_cache_attempts = 5; // per-article
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Automatically cache Starred articles' images and HTML5 video files",
|
||||
"fox",
|
||||
true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function csrf_ignore($method) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function before($method) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function after() {
|
||||
return 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);
|
||||
$host->add_handler("public", "cache_starred_images_getimage", $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 cache_starred_images_getimage() {
|
||||
ob_end_clean();
|
||||
|
||||
$hash = basename($_REQUEST["hash"]);
|
||||
|
||||
if ($hash) {
|
||||
|
||||
$filename = $this->cache_dir . "/" . basename($hash);
|
||||
|
||||
if (file_exists($filename)) {
|
||||
header("Content-Disposition: attachment; filename=\"$hash\"");
|
||||
|
||||
send_local_file($filename);
|
||||
} else {
|
||||
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
|
||||
echo "File not found.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
|
||||
*/
|
||||
function hook_house_keeping() {
|
||||
$files = glob($this->cache_dir . "/*.{png,mp4,status}", GLOB_BRACE);
|
||||
|
||||
$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;
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_entries WHERE id = ?");
|
||||
$sth->execute([$article_id]);
|
||||
|
||||
$article_exists = $sth->fetch();
|
||||
}
|
||||
|
||||
if (!$article_exists) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id) {
|
||||
$xpath = new DOMXpath($doc);
|
||||
|
||||
if ($article_id) {
|
||||
$entries = $xpath->query('(//img[@src])|(//video/source[@src])');
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry->hasAttribute('src')) {
|
||||
$src = rewrite_relative_url($site_url, $entry->getAttribute('src'));
|
||||
|
||||
$extension = $entry->tagName == 'source' ? '.mp4' : '.png';
|
||||
$local_filename = $this->cache_dir . $article_id . "-" . sha1($src) . $extension;
|
||||
|
||||
if (file_exists($local_filename)) {
|
||||
$entry->setAttribute("src", get_self_url_prefix() .
|
||||
"/public.php?op=cache_starred_images_getimage&method=image&hash=" .
|
||||
$article_id . "-" . sha1($src) . $extension);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $doc;
|
||||
}
|
||||
|
||||
function hook_update_task() {
|
||||
$res = $this->pdo->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%' OR UPPER(content) LIKE '%<VIDEO%') AND
|
||||
site_url != '' AND
|
||||
plugin_data NOT LIKE '%starred_cache_images%'
|
||||
ORDER BY ".sql_random_function()." LIMIT 100");
|
||||
|
||||
$usth = $this->pdo->prepare("UPDATE ttrss_entries SET plugin_data = ? WHERE id = ?");
|
||||
|
||||
while ($line = $res->fetch()) {
|
||||
if ($line["site_url"]) {
|
||||
$success = $this->cache_article_images($line["content"], $line["site_url"], $line["owner_uid"], $line["id"]);
|
||||
|
||||
if ($success) {
|
||||
$plugin_data = "starred_cache_images,${line['owner_uid']}:" . $line["plugin_data"];
|
||||
|
||||
$usth->execute([$plugin_data, $line['id']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function cache_article_images($content, $site_url, $owner_uid, $article_id) {
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
$status_filename = $this->cache_dir . $article_id . "-" . sha1($site_url) . ".status";
|
||||
|
||||
Debug::log("status: $status_filename", Debug::$LOG_EXTENDED);
|
||||
|
||||
if (file_exists($status_filename))
|
||||
$status = json_decode(file_get_contents($status_filename), true);
|
||||
else
|
||||
$status = [];
|
||||
|
||||
$status["attempt"] += 1;
|
||||
|
||||
// only allow several download attempts for article
|
||||
if ($status["attempt"] > $this->max_cache_attempts) {
|
||||
Debug::log("too many attempts for $site_url", Debug::$LOG_VERBOSE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file_put_contents($status_filename, json_encode($status))) {
|
||||
user_error("unable to write status file: $status_filename", E_USER_WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML('<?xml encoding="UTF-8">' . $content);
|
||||
$xpath = new DOMXPath($doc);
|
||||
|
||||
$entries = $xpath->query('(//img[@src])|(//video/source[@src])');
|
||||
|
||||
$success = false;
|
||||
$has_images = false;
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
|
||||
if ($entry->hasAttribute('src') && strpos($entry->getAttribute('src'), "data:") !== 0) {
|
||||
|
||||
$has_images = true;
|
||||
$src = rewrite_relative_url($site_url, $entry->getAttribute('src'));
|
||||
|
||||
$extension = $entry->tagName == 'source' ? '.mp4' : '.png';
|
||||
|
||||
$local_filename = $this->cache_dir . $article_id . "-" . sha1($src) . $extension;
|
||||
|
||||
Debug::log("cache_images: downloading: $src to $local_filename", Debug::$LOG_VERBOSE);
|
||||
|
||||
if (!file_exists($local_filename)) {
|
||||
$file_content = fetch_file_contents(["url" => $src, "max_size" => MAX_CACHE_FILE_SIZE]);
|
||||
|
||||
if ($file_content) {
|
||||
if (strlen($file_content) > MIN_CACHE_FILE_SIZE) {
|
||||
file_put_contents($local_filename, $file_content);
|
||||
}
|
||||
|
||||
$success = true;
|
||||
}
|
||||
} else {
|
||||
$success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $success || !$has_images;
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
}
|
38
plugins/close_button/init.php
Normal file
38
plugins/close_button/init.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
class Close_Button extends Plugin {
|
||||
private $host;
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_ARTICLE_BUTTON, $this);
|
||||
}
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Adds a button to close article panel",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function get_css() {
|
||||
return "i.icon-close-article { color : red; }";
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function hook_article_button($line) {
|
||||
if (!get_pref("COMBINED_DISPLAY_MODE")) {
|
||||
$rv = "<i class='material-icons icon-close-article'
|
||||
style='cursor : pointer' onclick='Article.close()'
|
||||
title='".__('Close article')."'>close</i>";
|
||||
}
|
||||
|
||||
return $rv;
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
40
plugins/googlereaderkeys/init.php
Normal file
40
plugins/googlereaderkeys/init.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
class GoogleReaderKeys extends Plugin {
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Keyboard hotkeys emulate Google Reader",
|
||||
"markwaters");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_HOTKEY_MAP, $this);
|
||||
}
|
||||
|
||||
function hook_hotkey_map($hotkeys) {
|
||||
|
||||
$hotkeys["j"] = "next_article_noscroll";
|
||||
$hotkeys["k"] = "prev_article_noscroll";
|
||||
$hotkeys["N"] = "next_feed";
|
||||
$hotkeys["P"] = "prev_feed";
|
||||
$hotkeys["v"] = "open_in_new_window";
|
||||
$hotkeys["r"] = "feed_refresh";
|
||||
$hotkeys["m"] = "toggle_unread";
|
||||
$hotkeys["o"] = "toggle_expand";
|
||||
$hotkeys["\r|Enter"] = "toggle_expand";
|
||||
$hotkeys["?"] = "help_dialog";
|
||||
$hotkeys[" |Space"] = "next_article";
|
||||
$hotkeys["(38)|Up"] = "article_scroll_up";
|
||||
$hotkeys["(40)|Down"] = "article_scroll_down";
|
||||
|
||||
return $hotkeys;
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
31
plugins/hotkeys_noscroll/init.php
Normal file
31
plugins/hotkeys_noscroll/init.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
class Hotkeys_Noscroll extends Plugin {
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"n/p hotkeys move between articles without scrolling",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_HOTKEY_MAP, $this);
|
||||
}
|
||||
|
||||
function hook_hotkey_map($hotkeys) {
|
||||
|
||||
$hotkeys["(40)|Down"] = "next_article_noscroll";
|
||||
$hotkeys["(38)|Up"] = "prev_article_noscroll";
|
||||
$hotkeys["n"] = "next_article_noscroll";
|
||||
$hotkeys["p"] = "prev_article_noscroll";
|
||||
|
||||
return $hotkeys;
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
123
plugins/import_export/import_export.js
Normal file
123
plugins/import_export/import_export.js
Normal file
@ -0,0 +1,123 @@
|
||||
function exportData() {
|
||||
try {
|
||||
|
||||
var query = "backend.php?op=pluginhandler&plugin=import_export&method=exportData";
|
||||
|
||||
if (dijit.byId("dataExportDlg"))
|
||||
dijit.byId("dataExportDlg").destroyRecursive();
|
||||
|
||||
var exported = 0;
|
||||
|
||||
dialog = new dijit.Dialog({
|
||||
id: "dataExportDlg",
|
||||
title: __("Export Data"),
|
||||
style: "width: 600px",
|
||||
prepare: function() {
|
||||
|
||||
Notify.progress("Loading, please wait...");
|
||||
|
||||
new Ajax.Request("backend.php", {
|
||||
parameters: "op=pluginhandler&plugin=import_export&method=exportrun&offset=" + exported,
|
||||
onComplete: function(transport) {
|
||||
try {
|
||||
var rv = JSON.parse(transport.responseText);
|
||||
|
||||
if (rv && rv.exported != undefined) {
|
||||
if (rv.exported > 0) {
|
||||
|
||||
exported += rv.exported;
|
||||
|
||||
$("export_status_message").innerHTML =
|
||||
"<img src='images/indicator_tiny.gif'> " +
|
||||
"Exported %d articles, please wait...".replace("%d",
|
||||
exported);
|
||||
|
||||
setTimeout('dijit.byId("dataExportDlg").prepare()', 2000);
|
||||
|
||||
} else {
|
||||
|
||||
$("export_status_message").innerHTML =
|
||||
ngettext("Finished, exported %d article. You can download the data <a class='visibleLink' href='%u'>here</a>.", "Finished, exported %d articles. You can download the data <a class='visibleLink' href='%u'>here</a>.", exported)
|
||||
.replace("%d", exported)
|
||||
.replace("%u", "backend.php?op=pluginhandler&plugin=import_export&subop=exportget");
|
||||
|
||||
exported = 0;
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
$("export_status_message").innerHTML =
|
||||
"Error occured, could not export data.";
|
||||
}
|
||||
} catch (e) {
|
||||
App.Error.report(e);
|
||||
}
|
||||
|
||||
Notify.close();
|
||||
|
||||
} });
|
||||
|
||||
},
|
||||
execute: function() {
|
||||
if (this.validate()) {
|
||||
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
href: query});
|
||||
|
||||
dialog.show();
|
||||
|
||||
|
||||
} catch (e) {
|
||||
App.Error.report(e);
|
||||
}
|
||||
}
|
||||
|
||||
function dataImportComplete(iframe) {
|
||||
try {
|
||||
if (!iframe.contentDocument.body.innerHTML) return false;
|
||||
|
||||
Element.hide(iframe);
|
||||
|
||||
Notify.close();
|
||||
|
||||
if (dijit.byId('dataImportDlg'))
|
||||
dijit.byId('dataImportDlg').destroyRecursive();
|
||||
|
||||
var content = iframe.contentDocument.body.innerHTML;
|
||||
|
||||
dialog = new dijit.Dialog({
|
||||
id: "dataImportDlg",
|
||||
title: __("Data Import"),
|
||||
style: "width: 600px",
|
||||
onCancel: function() {
|
||||
|
||||
},
|
||||
content: content});
|
||||
|
||||
dialog.show();
|
||||
|
||||
} catch (e) {
|
||||
App.Error.report(e);
|
||||
}
|
||||
}
|
||||
|
||||
function importData() {
|
||||
|
||||
var file = $("export_file");
|
||||
|
||||
if (file.value.length == 0) {
|
||||
alert(__("Please choose the file first."));
|
||||
return false;
|
||||
} else {
|
||||
Notify.progress("Importing, please wait...", true);
|
||||
|
||||
Element.show("data_upload_iframe");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
500
plugins/import_export/init.php
Executable file
500
plugins/import_export/init.php
Executable file
@ -0,0 +1,500 @@
|
||||
<?php
|
||||
class Import_Export extends Plugin implements IHandler {
|
||||
private $host;
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_PREFS_TAB, $this);
|
||||
$host->add_command("xml-import", "import articles from XML", $this, ":", "FILE");
|
||||
}
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Imports and exports user data using neutral XML format",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function xml_import($args) {
|
||||
|
||||
$filename = $args['xml_import'];
|
||||
|
||||
if (!is_file($filename)) {
|
||||
print "error: input filename ($filename) doesn't exist.\n";
|
||||
return;
|
||||
}
|
||||
|
||||
Debug::log("please enter your username:");
|
||||
|
||||
$username = trim(read_stdin());
|
||||
|
||||
Debug::log("importing $filename for user $username...\n");
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_users WHERE login = ?");
|
||||
$sth->execute($username);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
$owner_uid = $row['id'];
|
||||
|
||||
$this->perform_data_import($filename, $owner_uid);
|
||||
} else {
|
||||
print "error: could not find user $username.\n";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function get_prefs_js() {
|
||||
return file_get_contents(dirname(__FILE__) . "/import_export.js");
|
||||
}
|
||||
|
||||
function hook_prefs_tab($args) {
|
||||
if ($args != "prefFeeds") return;
|
||||
|
||||
print "<div dojoType=\"dijit.layout.AccordionPane\"
|
||||
title=\"<i class='material-icons'>import_export</i> ".__('Import and export')."\">";
|
||||
|
||||
print_notice(__("You can export and import your Starred and Archived articles for safekeeping or when migrating between tt-rss instances of same version."));
|
||||
|
||||
print "<p>";
|
||||
|
||||
print "<button dojoType=\"dijit.form.Button\" onclick=\"return exportData()\">".
|
||||
__('Export my data')."</button> ";
|
||||
|
||||
print "<hr>";
|
||||
|
||||
print "<iframe id=\"data_upload_iframe\"
|
||||
name=\"data_upload_iframe\" onload=\"dataImportComplete(this)\"
|
||||
style=\"width: 400px; height: 100px; display: none;\"></iframe>";
|
||||
|
||||
print "<form name=\"import_form\" style='display : block' target=\"data_upload_iframe\"
|
||||
enctype=\"multipart/form-data\" method=\"POST\"
|
||||
action=\"backend.php\">
|
||||
<label class=\"dijitButton\">".__("Choose file...")."
|
||||
<input style=\"display : none\" id=\"export_file\" name=\"export_file\" type=\"file\">
|
||||
</label>
|
||||
<input type=\"hidden\" name=\"op\" value=\"pluginhandler\">
|
||||
<input type=\"hidden\" name=\"plugin\" value=\"import_export\">
|
||||
<input type=\"hidden\" name=\"method\" value=\"dataimport\">
|
||||
<button dojoType=\"dijit.form.Button\" onclick=\"return importData();\" type=\"submit\">" .
|
||||
__('Import') . "</button>";
|
||||
|
||||
print "</form>";
|
||||
|
||||
print "</p>";
|
||||
|
||||
print "</div>"; # pane
|
||||
}
|
||||
|
||||
function csrf_ignore($method) {
|
||||
return in_array($method, array("exportget"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function before($method) {
|
||||
return $_SESSION["uid"] != false;
|
||||
}
|
||||
|
||||
function after() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(unused)
|
||||
*/
|
||||
function exportget() {
|
||||
$exportname = CACHE_DIR . "/export/" .
|
||||
sha1($_SESSION['uid'] . $_SESSION['login']) . ".xml";
|
||||
|
||||
if (file_exists($exportname)) {
|
||||
header("Content-type: text/xml");
|
||||
|
||||
$timestamp_suffix = date("Y-m-d", filemtime($exportname));
|
||||
|
||||
if (function_exists('gzencode')) {
|
||||
header("Content-Disposition: attachment; filename=TinyTinyRSS_exported_${timestamp_suffix}.xml.gz");
|
||||
echo gzencode(file_get_contents($exportname));
|
||||
} else {
|
||||
header("Content-Disposition: attachment; filename=TinyTinyRSS_exported_${timestamp_suffix}.xml");
|
||||
echo file_get_contents($exportname);
|
||||
}
|
||||
} else {
|
||||
echo "File not found.";
|
||||
}
|
||||
}
|
||||
|
||||
function exportrun() {
|
||||
$offset = (int) $_REQUEST['offset'];
|
||||
$exported = 0;
|
||||
$limit = 250;
|
||||
|
||||
if ($offset < 10000 && is_writable(CACHE_DIR . "/export")) {
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT
|
||||
ttrss_entries.guid,
|
||||
ttrss_entries.title,
|
||||
content,
|
||||
marked,
|
||||
published,
|
||||
score,
|
||||
note,
|
||||
link,
|
||||
tag_cache,
|
||||
label_cache,
|
||||
ttrss_feeds.title AS feed_title,
|
||||
ttrss_feeds.feed_url AS feed_url,
|
||||
ttrss_entries.updated
|
||||
FROM
|
||||
ttrss_user_entries LEFT JOIN ttrss_feeds ON (ttrss_feeds.id = feed_id),
|
||||
ttrss_entries
|
||||
WHERE
|
||||
(marked = true OR feed_id IS NULL) AND
|
||||
ref_id = ttrss_entries.id AND
|
||||
ttrss_user_entries.owner_uid = ?
|
||||
ORDER BY ttrss_entries.id LIMIT $limit OFFSET $offset");
|
||||
|
||||
$sth->execute([$_SESSION['uid']]);
|
||||
|
||||
$exportname = sha1($_SESSION['uid'] . $_SESSION['login']);
|
||||
|
||||
if ($offset == 0) {
|
||||
$fp = fopen(CACHE_DIR . "/export/$exportname.xml", "w");
|
||||
fputs($fp, "<articles schema-version=\"".SCHEMA_VERSION."\">");
|
||||
} else {
|
||||
$fp = fopen(CACHE_DIR . "/export/$exportname.xml", "a");
|
||||
}
|
||||
|
||||
if ($fp) {
|
||||
|
||||
$exported = 0;
|
||||
while ($line = $sth->fetch(PDO::FETCH_ASSOC)) {
|
||||
++$exported;
|
||||
|
||||
fputs($fp, "<article>\n");
|
||||
|
||||
foreach ($line as $k => $v) {
|
||||
|
||||
fputs($fp, " ");
|
||||
|
||||
if (is_bool($v))
|
||||
$v = (int) $v;
|
||||
|
||||
if (!$v || is_numeric($v)) {
|
||||
fputs($fp, "<$k>$v</$k>\n");
|
||||
} else {
|
||||
$v = str_replace("]]>", "]]]]><![CDATA[>", $v);
|
||||
fputs($fp, "<$k><![CDATA[$v]]></$k>\n");
|
||||
}
|
||||
}
|
||||
|
||||
fputs($fp, "</article>\n");
|
||||
}
|
||||
|
||||
if ($exported < $limit && $exported > 0) {
|
||||
fputs($fp, "</articles>");
|
||||
}
|
||||
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
print json_encode(array("exported" => $exported));
|
||||
}
|
||||
|
||||
function perform_data_import($filename, $owner_uid) {
|
||||
|
||||
$num_imported = 0;
|
||||
$num_processed = 0;
|
||||
$num_feeds_created = 0;
|
||||
|
||||
libxml_disable_entity_loader(false);
|
||||
|
||||
$doc = new DOMDocument();
|
||||
|
||||
if (!$doc_loaded = @$doc->load($filename)) {
|
||||
$contents = file_get_contents($filename);
|
||||
|
||||
if ($contents) {
|
||||
$data = @gzuncompress($contents);
|
||||
}
|
||||
|
||||
if (!$data) {
|
||||
$data = @gzdecode($contents);
|
||||
}
|
||||
|
||||
if ($data)
|
||||
$doc_loaded = $doc->loadXML($data);
|
||||
}
|
||||
|
||||
libxml_disable_entity_loader(true);
|
||||
|
||||
if ($doc_loaded) {
|
||||
|
||||
$xpath = new DOMXpath($doc);
|
||||
|
||||
$container = $doc->firstChild;
|
||||
|
||||
if ($container && $container->hasAttribute('schema-version')) {
|
||||
$schema_version = $container->getAttribute('schema-version');
|
||||
|
||||
if ($schema_version != SCHEMA_VERSION) {
|
||||
print "<p>" .__("Could not import: incorrect schema version.") . "</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
print "<p>" . __("Could not import: unrecognized document format.") . "</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
$articles = $xpath->query("//article");
|
||||
|
||||
foreach ($articles as $article_node) {
|
||||
if ($article_node->childNodes) {
|
||||
|
||||
$ref_id = 0;
|
||||
|
||||
$article = array();
|
||||
|
||||
foreach ($article_node->childNodes as $child) {
|
||||
if ($child->nodeName == 'content' || $child->nodeName == 'label_cache') {
|
||||
$article[$child->nodeName] = $child->nodeValue;
|
||||
} else {
|
||||
$article[$child->nodeName] = clean($child->nodeValue);
|
||||
}
|
||||
}
|
||||
|
||||
//print_r($article);
|
||||
|
||||
if ($article['guid']) {
|
||||
|
||||
++$num_processed;
|
||||
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
//print 'GUID:' . $article['guid'] . "\n";
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_entries
|
||||
WHERE guid = ?");
|
||||
$sth->execute([$article['guid']]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
$ref_id = $row['id'];
|
||||
} else {
|
||||
$sth = $this->pdo->prepare(
|
||||
"INSERT INTO ttrss_entries
|
||||
(title,
|
||||
guid,
|
||||
link,
|
||||
updated,
|
||||
content,
|
||||
content_hash,
|
||||
no_orig_date,
|
||||
date_updated,
|
||||
date_entered,
|
||||
comments,
|
||||
num_comments,
|
||||
author)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?,
|
||||
false,
|
||||
NOW(),
|
||||
NOW(),
|
||||
'',
|
||||
'0',
|
||||
'')");
|
||||
|
||||
$sth->execute([
|
||||
$article['title'],
|
||||
$article['guid'],
|
||||
$article['link'],
|
||||
$article['updated'],
|
||||
$article['content'],
|
||||
sha1($article['content'])
|
||||
]);
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_entries
|
||||
WHERE guid = ?");
|
||||
$sth->execute([$article['guid']]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
$ref_id = $row['id'];
|
||||
}
|
||||
}
|
||||
|
||||
//print "Got ref ID: $ref_id\n";
|
||||
|
||||
if ($ref_id) {
|
||||
|
||||
$feed = NULL;
|
||||
|
||||
if ($article['feed_url'] && $article['feed_title']) {
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
|
||||
WHERE feed_url = ? AND owner_uid = ?");
|
||||
$sth->execute([$article['feed_url'], $owner_uid]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
$feed = $row['id'];
|
||||
} else {
|
||||
// try autocreating feed in Uncategorized...
|
||||
|
||||
$sth = $this->pdo->prepare("INSERT INTO ttrss_feeds (owner_uid,
|
||||
feed_url, title) VALUES (?, ?, ?)");
|
||||
$res = $sth->execute([$owner_uid, $article['feed_url'], $article['feed_title']]);
|
||||
|
||||
if ($res) {
|
||||
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
|
||||
WHERE feed_url = ? AND owner_uid = ?");
|
||||
$sth->execute([$article['feed_url'], $owner_uid]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
++$num_feeds_created;
|
||||
|
||||
$feed = $row['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($feed)
|
||||
$feed_qpart = "feed_id = " . (int) $feed;
|
||||
else
|
||||
$feed_qpart = "feed_id IS NULL";
|
||||
|
||||
//print "$ref_id / $feed / " . $article['title'] . "\n";
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT int_id FROM ttrss_user_entries
|
||||
WHERE ref_id = ? AND owner_uid = ? AND $feed_qpart");
|
||||
$sth->execute([$ref_id, $owner_uid]);
|
||||
|
||||
if (!$sth->fetch()) {
|
||||
|
||||
$score = (int) $article['score'];
|
||||
|
||||
$tag_cache = $article['tag_cache'];
|
||||
$note = $article['note'];
|
||||
|
||||
//print "Importing " . $article['title'] . "<br/>";
|
||||
|
||||
++$num_imported;
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"INSERT INTO ttrss_user_entries
|
||||
(ref_id, owner_uid, feed_id, unread, last_read, marked,
|
||||
published, score, tag_cache, label_cache, uuid, note)
|
||||
VALUES (?, ?, ?, false,
|
||||
NULL, ?, ?, ?, ?, '', '', ?)");
|
||||
|
||||
$res = $sth->execute([
|
||||
$ref_id,
|
||||
$owner_uid,
|
||||
$feed,
|
||||
(int)sql_bool_to_bool($article['marked']),
|
||||
(int)sql_bool_to_bool($article['published']),
|
||||
$score,
|
||||
$tag_cache,
|
||||
$note]);
|
||||
|
||||
if ($res) {
|
||||
|
||||
$label_cache = json_decode($article['label_cache'], true);
|
||||
|
||||
if (is_array($label_cache) && $label_cache["no-labels"] != 1) {
|
||||
foreach ($label_cache as $label) {
|
||||
Labels::create($label[1],
|
||||
$label[2], $label[3], $owner_uid);
|
||||
|
||||
Labels::add_article($ref_id, $label[1], $owner_uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->pdo->commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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).
|
||||
"</p>";
|
||||
|
||||
} else {
|
||||
|
||||
print "<p>" . __("Could not load XML document.") . "</p>";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function exportData() {
|
||||
|
||||
print "<p style='text-align : center' id='export_status_message'>You need to prepare exported data first by clicking the button below.</p>";
|
||||
|
||||
print "<footer class='text-center'>";
|
||||
print "<button dojoType='dijit.form.Button'
|
||||
type='submit' class='alt-primary'
|
||||
onclick=\"dijit.byId('dataExportDlg').prepare()\">".
|
||||
__('Prepare data')."</button>";
|
||||
|
||||
print "<button dojoType='dijit.form.Button'
|
||||
onclick=\"dijit.byId('dataExportDlg').hide()\">".
|
||||
__('Close this window')."</button>";
|
||||
|
||||
print "</footer>";
|
||||
}
|
||||
|
||||
function dataImport() {
|
||||
header("Content-Type: text/html"); # required for iframe
|
||||
|
||||
print "<footer class='text-center'>";
|
||||
|
||||
if ($_FILES['export_file']['error'] != 0) {
|
||||
print_error(T_sprintf("Upload failed with error code %d (%s)",
|
||||
$_FILES['export_file']['error'],
|
||||
get_upload_error_message($_FILES['export_file']['error'])));
|
||||
} else {
|
||||
|
||||
if (is_uploaded_file($_FILES['export_file']['tmp_name'])) {
|
||||
$tmp_file = tempnam(CACHE_DIR . '/upload', 'export');
|
||||
|
||||
$result = move_uploaded_file($_FILES['export_file']['tmp_name'],
|
||||
$tmp_file);
|
||||
|
||||
if (!$result) {
|
||||
print_error(__("Unable to move uploaded file."));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
print_error(__('Error: please upload OPML file.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_file($tmp_file)) {
|
||||
$this->perform_data_import($tmp_file, $_SESSION['uid']);
|
||||
unlink($tmp_file);
|
||||
} else {
|
||||
print_error(__('No file uploaded.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
print "<button dojoType='dijit.form.Button'
|
||||
onclick=\"dijit.byId('dataImportDlg').hide()\">".
|
||||
__('Close this window')."</button>";
|
||||
|
||||
print "</div>";
|
||||
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
0
plugins/index.html
Normal file
0
plugins/index.html
Normal file
240
plugins/mail/init.php
Normal file
240
plugins/mail/init.php
Normal file
@ -0,0 +1,240 @@
|
||||
<?php
|
||||
class Mail extends Plugin {
|
||||
|
||||
/* @var PluginHost $host */
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Share article via email",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$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 = $_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=\"<i class='material-icons'>mail</i> ".__('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_hidden("op", "pluginhandler");
|
||||
print_hidden("method", "save");
|
||||
print_hidden("plugin", "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 "<i class='material-icons' style=\"cursor : pointer\"
|
||||
onclick=\"Plugins.Mail.send(".$line["id"].")\"
|
||||
title='".__('Forward by email')."'>mail</i>";
|
||||
}
|
||||
|
||||
function emailArticle() {
|
||||
|
||||
$ids = explode(",", $_REQUEST['param']);
|
||||
$ids_qmarks = arr_qmarks($ids);
|
||||
|
||||
print_hidden("op", "pluginhandler");
|
||||
print_hidden("plugin", "mail");
|
||||
print_hidden("method", "sendEmail");
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT email, full_name FROM ttrss_users WHERE
|
||||
id = ?");
|
||||
$sth->execute([$_SESSION['uid']]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
$user_email = htmlspecialchars($row['email']);
|
||||
$user_name = htmlspecialchars($row['full_name']);
|
||||
}
|
||||
|
||||
if (!$user_name) $user_name = $_SESSION['name'];
|
||||
|
||||
print_hidden("from_email", "$user_email");
|
||||
print_hidden("from_name", "$user_name");
|
||||
|
||||
require_once "lib/MiniTemplator.class.php";
|
||||
|
||||
$tpl = new MiniTemplator;
|
||||
|
||||
$tpl->readTemplateFromFile("templates/email_article_template.txt");
|
||||
|
||||
$tpl->setVariable('USER_NAME', $_SESSION["name"], true);
|
||||
$tpl->setVariable('USER_EMAIL', $user_email, true);
|
||||
$tpl->setVariable('TTRSS_HOST', $_SERVER["HTTP_HOST"], true);
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT DISTINCT link, content, title, note
|
||||
FROM ttrss_user_entries, ttrss_entries WHERE id = ref_id AND
|
||||
id IN ($ids_qmarks) AND owner_uid = ?");
|
||||
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
|
||||
|
||||
if (count($ids) > 1) {
|
||||
$subject = __("[Forwarded]") . " " . __("Multiple articles");
|
||||
}
|
||||
|
||||
while ($line = $sth->fetch()) {
|
||||
|
||||
if (!$subject)
|
||||
$subject = __("[Forwarded]") . " " . htmlspecialchars($line["title"]);
|
||||
|
||||
$tpl->setVariable('ARTICLE_TITLE', strip_tags($line["title"]));
|
||||
$tnote = strip_tags($line["note"]);
|
||||
if( $tnote != ''){
|
||||
$tpl->setVariable('ARTICLE_NOTE', $tnote, true);
|
||||
$tpl->addBlock('note');
|
||||
}
|
||||
$tpl->setVariable('ARTICLE_URL', strip_tags($line["link"]));
|
||||
|
||||
$tpl->addBlock('article');
|
||||
}
|
||||
|
||||
$tpl->addBlock('email');
|
||||
|
||||
$content = "";
|
||||
$tpl->generateOutputToString($content);
|
||||
|
||||
print "<table width='100%'><tr><td>";
|
||||
|
||||
$addresslist = explode(",", $this->host->get($this, "addresslist"));
|
||||
|
||||
print __('To:');
|
||||
|
||||
print "</td><td>";
|
||||
|
||||
/* print "<input dojoType=\"dijit.form.ValidationTextBox\" required=\"true\"
|
||||
style=\"width : 30em;\"
|
||||
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 "</td></tr><tr><td>";
|
||||
|
||||
print __('Subject:');
|
||||
|
||||
print "</td><td>";
|
||||
|
||||
print "<input dojoType='dijit.form.ValidationTextBox' required='true'
|
||||
style='width : 30em;' name='subject' value=\"$subject\" id='subject'>";
|
||||
|
||||
print "</td></tr>";
|
||||
|
||||
print "<tr><td colspan='2'><textarea dojoType='dijit.form.SimpleTextarea'
|
||||
style='height : 200px; font-size : 12px; width : 98%' rows=\"20\"
|
||||
name='content'>$content</textarea>";
|
||||
|
||||
print "</td></tr></table>";
|
||||
|
||||
print "<footer>";
|
||||
print "<button dojoType='dijit.form.Button' onclick=\"dijit.byId('emailArticleDlg').execute()\">".__('Send e-mail')."</button> ";
|
||||
print "<button dojoType='dijit.form.Button' onclick=\"dijit.byId('emailArticleDlg').hide()\">".__('Cancel')."</button>";
|
||||
print "</footer>";
|
||||
|
||||
//return;
|
||||
}
|
||||
|
||||
function sendEmail() {
|
||||
$reply = array();
|
||||
|
||||
/*$mail->AddReplyTo(strip_tags($_REQUEST['from_email']),
|
||||
strip_tags($_REQUEST['from_name']));
|
||||
//$mail->AddAddress($_REQUEST['destination']);
|
||||
$addresses = explode(';', $_REQUEST['destination']);
|
||||
foreach($addresses as $nextaddr)
|
||||
$mail->AddAddress($nextaddr);
|
||||
|
||||
$mail->IsHTML(false);
|
||||
$mail->Subject = $_REQUEST['subject'];
|
||||
$mail->Body = $_REQUEST['content'];
|
||||
|
||||
$rc = $mail->Send(); */
|
||||
|
||||
$to = $_REQUEST["destination"];
|
||||
$subject = strip_tags($_REQUEST["subject"]);
|
||||
$message = strip_tags($_REQUEST["content"]);
|
||||
$from = strip_tags($_REQUEST["from_email"]);
|
||||
|
||||
$mailer = new Mailer();
|
||||
|
||||
$rc = $mailer->mail(["to_address" => $to,
|
||||
"headers" => ["Reply-To: $from"],
|
||||
"subject" => $subject,
|
||||
"message" => $message]);
|
||||
|
||||
if (!$rc) {
|
||||
$reply['error'] = $mailer->error();
|
||||
} else {
|
||||
//save_email_address($destination);
|
||||
$reply['message'] = "UPDATE_COUNTERS";
|
||||
}
|
||||
|
||||
print json_encode($reply);
|
||||
}
|
||||
|
||||
/* function completeEmails() {
|
||||
$search = $_REQUEST["search"];
|
||||
|
||||
print "<ul>";
|
||||
|
||||
foreach ($_SESSION['stored_emails'] as $email) {
|
||||
if (strpos($email, $search) !== false) {
|
||||
print "<li>$email</li>";
|
||||
}
|
||||
}
|
||||
|
||||
print "</ul>";
|
||||
} */
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
56
plugins/mail/mail.js
Normal file
56
plugins/mail/mail.js
Normal file
@ -0,0 +1,56 @@
|
||||
Plugins.Mail = {
|
||||
send: function(id) {
|
||||
if (!id) {
|
||||
let ids = Headlines.getSelected();
|
||||
|
||||
if (ids.length == 0) {
|
||||
alert(__("No articles selected."));
|
||||
return;
|
||||
}
|
||||
|
||||
id = ids.toString();
|
||||
}
|
||||
|
||||
if (dijit.byId("emailArticleDlg"))
|
||||
dijit.byId("emailArticleDlg").destroyRecursive();
|
||||
|
||||
const query = "backend.php?op=pluginhandler&plugin=mail&method=emailArticle¶m=" + encodeURIComponent(id);
|
||||
|
||||
const dialog = new dijit.Dialog({
|
||||
id: "emailArticleDlg",
|
||||
title: __("Forward article by email"),
|
||||
style: "width: 600px",
|
||||
execute: function () {
|
||||
if (this.validate()) {
|
||||
xhrJson("backend.php", this.attr('value'), (reply) => {
|
||||
if (reply) {
|
||||
const error = reply['error'];
|
||||
|
||||
if (error) {
|
||||
alert(__('Error sending email:') + ' ' + error);
|
||||
} else {
|
||||
Notify.info('Your message has been sent.');
|
||||
dialog.hide();
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
href: query
|
||||
});
|
||||
|
||||
/* 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();
|
||||
},
|
||||
onHotkey: function(id) {
|
||||
Plugins.Mail.send(id);
|
||||
}
|
||||
};
|
BIN
plugins/mail/mail.png
Normal file
BIN
plugins/mail/mail.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 641 B |
34
plugins/mailto/init.js
Normal file
34
plugins/mailto/init.js
Normal file
@ -0,0 +1,34 @@
|
||||
Plugins.Mailto = {
|
||||
send: function (id) {
|
||||
if (!id) {
|
||||
const ids = Headlines.getSelected();
|
||||
|
||||
if (ids.length == 0) {
|
||||
alert(__("No articles selected."));
|
||||
return;
|
||||
}
|
||||
|
||||
id = ids.toString();
|
||||
}
|
||||
|
||||
if (dijit.byId("emailArticleDlg"))
|
||||
dijit.byId("emailArticleDlg").destroyRecursive();
|
||||
|
||||
const query = "backend.php?op=pluginhandler&plugin=mailto&method=emailArticle¶m=" + encodeURIComponent(id);
|
||||
|
||||
const dialog = new dijit.Dialog({
|
||||
id: "emailArticleDlg",
|
||||
title: __("Forward article by email"),
|
||||
style: "width: 600px",
|
||||
href: query});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
};
|
||||
|
||||
// override default hotkey action if enabled
|
||||
Plugins.Mail = Plugins.Mail || {};
|
||||
|
||||
Plugins.Mail.onHotkey = function(id) {
|
||||
Plugins.Mailto.send(id);
|
||||
};
|
95
plugins/mailto/init.php
Normal file
95
plugins/mailto/init.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
class MailTo extends Plugin {
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Share article via email (using mailto: links, invoking your mail client)",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_ARTICLE_BUTTON, $this);
|
||||
}
|
||||
|
||||
function get_js() {
|
||||
return file_get_contents(dirname(__FILE__) . "/init.js");
|
||||
}
|
||||
|
||||
function hook_article_button($line) {
|
||||
return "<i class='material-icons' style=\"cursor : pointer\"
|
||||
onclick=\"Plugins.Mailto.send(".$line["id"].")\"
|
||||
title='".__('Forward by email')."'>mail_outline</i>";
|
||||
}
|
||||
|
||||
function emailArticle() {
|
||||
|
||||
$ids = explode(",", $_REQUEST['param']);
|
||||
$ids_qmarks = arr_qmarks($ids);
|
||||
|
||||
require_once "lib/MiniTemplator.class.php";
|
||||
|
||||
$tpl = new MiniTemplator;
|
||||
|
||||
$tpl->readTemplateFromFile("templates/email_article_template.txt");
|
||||
|
||||
$tpl->setVariable('USER_NAME', $_SESSION["name"], true);
|
||||
//$tpl->setVariable('USER_EMAIL', $user_email, true);
|
||||
$tpl->setVariable('TTRSS_HOST', $_SERVER["HTTP_HOST"], true);
|
||||
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT DISTINCT link, content, title
|
||||
FROM ttrss_user_entries, ttrss_entries WHERE id = ref_id AND
|
||||
id IN ($ids_qmarks) AND owner_uid = ?");
|
||||
$sth->execute(array_merge($ids, [$_SESSION['uid']]));
|
||||
|
||||
if (count($ids) > 1) {
|
||||
$subject = __("[Forwarded]") . " " . __("Multiple articles");
|
||||
} else {
|
||||
$subject = "";
|
||||
}
|
||||
|
||||
while ($line = $sth->fetch()) {
|
||||
|
||||
if (!$subject)
|
||||
$subject = __("[Forwarded]") . " " . htmlspecialchars($line["title"]);
|
||||
|
||||
$tpl->setVariable('ARTICLE_TITLE', strip_tags($line["title"]));
|
||||
$tpl->setVariable('ARTICLE_URL', strip_tags($line["link"]));
|
||||
|
||||
$tpl->addBlock('article');
|
||||
}
|
||||
|
||||
$tpl->addBlock('email');
|
||||
|
||||
$content = "";
|
||||
$tpl->generateOutputToString($content);
|
||||
|
||||
$mailto_link = htmlspecialchars("mailto:?subject=".rawurlencode($subject).
|
||||
"&body=".rawurlencode($content));
|
||||
|
||||
print __("Clicking the following link to invoke your mail client:");
|
||||
|
||||
print "<div class='panel text-center'>";
|
||||
print "<a target=\"_blank\" href=\"$mailto_link\">".
|
||||
__("Forward selected article(s) by email.")."</a>";
|
||||
print "</div>";
|
||||
|
||||
print __("You should be able to edit the message before sending in your mail client.");
|
||||
|
||||
print "<p>";
|
||||
|
||||
print "<footer class='text-center'>";
|
||||
print "<button dojoType='dijit.form.Button' onclick=\"dijit.byId('emailArticleDlg').hide()\">".__('Close this dialog')."</button>";
|
||||
print "</footer>";
|
||||
|
||||
//return;
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
BIN
plugins/mailto/mail.png
Normal file
BIN
plugins/mailto/mail.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 821 B |
37
plugins/no_iframes/init.php
Normal file
37
plugins/no_iframes/init.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
class No_Iframes extends Plugin {
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Remove embedded iframes (unless whitelisted)",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_SANITIZE, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes) {
|
||||
|
||||
$xpath = new DOMXpath($doc);
|
||||
$entries = $xpath->query('//iframe');
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if (!iframe_whitelisted($entry))
|
||||
$entry->parentNode->removeChild($entry);
|
||||
}
|
||||
|
||||
return array($doc, $allowed_elements, $disallowed_attributes);
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
9
plugins/no_title_counters/init.js
Normal file
9
plugins/no_title_counters/init.js
Normal file
@ -0,0 +1,9 @@
|
||||
require(['dojo/_base/kernel', 'dojo/ready'], function (dojo, ready) {
|
||||
ready(function () {
|
||||
PluginHost.register(PluginHost.HOOK_INIT_COMPLETE, () => {
|
||||
App.updateTitle = function () {
|
||||
document.title = "Agriget";
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
24
plugins/no_title_counters/init.php
Normal file
24
plugins/no_title_counters/init.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
}
|
9
plugins/no_url_hashes/init.js
Normal file
9
plugins/no_url_hashes/init.js
Normal file
@ -0,0 +1,9 @@
|
||||
require(['dojo/_base/kernel', 'dojo/ready'], function (dojo, ready) {
|
||||
ready(function () {
|
||||
hash_set = function () {
|
||||
};
|
||||
hash_get = function () {
|
||||
};
|
||||
});
|
||||
});
|
||||
|
24
plugins/no_url_hashes/init.php
Normal file
24
plugins/no_url_hashes/init.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
}
|
78
plugins/note/init.php
Normal file
78
plugins/note/init.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
class Note extends Plugin {
|
||||
|
||||
/* @var PluginHost $host */
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Adds support for setting article notes",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_ARTICLE_BUTTON, $this);
|
||||
}
|
||||
|
||||
function get_js() {
|
||||
return file_get_contents(dirname(__FILE__) . "/note.js");
|
||||
}
|
||||
|
||||
|
||||
function hook_article_button($line) {
|
||||
return "<i class='material-icons' onclick=\"Plugins.Note.edit(".$line["id"].")\"
|
||||
style='cursor : pointer' title='".__('Edit article note')."'>note</i>";
|
||||
}
|
||||
|
||||
function edit() {
|
||||
$param = $_REQUEST['param'];
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT note FROM ttrss_user_entries WHERE
|
||||
ref_id = ? AND owner_uid = ?");
|
||||
$sth->execute([$param, $_SESSION['uid']]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
|
||||
$note = $row['note'];
|
||||
|
||||
print_hidden("id", "$param");
|
||||
print_hidden("op", "pluginhandler");
|
||||
print_hidden("method", "setNote");
|
||||
print_hidden("plugin", "note");
|
||||
|
||||
print "<textarea dojoType='dijit.form.SimpleTextarea'
|
||||
style='font-size : 12px; width : 98%; height: 100px;'
|
||||
name='note'>$note</textarea>";
|
||||
|
||||
}
|
||||
|
||||
print "<footer class='text-center'>";
|
||||
print "<button dojoType=\"dijit.form.Button\"
|
||||
onclick=\"dijit.byId('editNoteDlg').execute()\">".__('Save')."</button> ";
|
||||
print "<button dojoType=\"dijit.form.Button\"
|
||||
onclick=\"dijit.byId('editNoteDlg').hide()\">".__('Cancel')."</button>";
|
||||
print "</footer>";
|
||||
|
||||
}
|
||||
|
||||
function setNote() {
|
||||
$id = $_REQUEST["id"];
|
||||
$note = trim(strip_tags($_REQUEST["note"]));
|
||||
|
||||
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET note = ?
|
||||
WHERE ref_id = ? AND owner_uid = ?");
|
||||
$sth->execute([$note, $id, $_SESSION['uid']]);
|
||||
|
||||
$formatted_note = Article::format_article_note($id, $note);
|
||||
|
||||
print json_encode(array("note" => $formatted_note,
|
||||
"raw_length" => mb_strlen($note)));
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
40
plugins/note/note.js
Normal file
40
plugins/note/note.js
Normal file
@ -0,0 +1,40 @@
|
||||
Plugins.Note = {
|
||||
edit: function(id) {
|
||||
const query = "backend.php?op=pluginhandler&plugin=note&method=edit¶m=" + encodeURIComponent(id);
|
||||
|
||||
if (dijit.byId("editNoteDlg"))
|
||||
dijit.byId("editNoteDlg").destroyRecursive();
|
||||
|
||||
const dialog = new dijit.Dialog({
|
||||
id: "editNoteDlg",
|
||||
title: __("Edit article note"),
|
||||
style: "width: 600px",
|
||||
execute: function () {
|
||||
if (this.validate()) {
|
||||
Notify.progress("Saving article note...", true);
|
||||
|
||||
xhrJson("backend.php", this.attr('value'), (reply) => {
|
||||
Notify.close();
|
||||
dialog.hide();
|
||||
|
||||
if (reply) {
|
||||
const elem = $("POSTNOTE-" + id);
|
||||
|
||||
if (elem) {
|
||||
elem.innerHTML = reply.note;
|
||||
|
||||
if (reply.raw_length != 0)
|
||||
Element.show(elem);
|
||||
else
|
||||
Element.hide(elem);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
href: query,
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
};
|
7
plugins/nsfw/init.js
Normal file
7
plugins/nsfw/init.js
Normal file
@ -0,0 +1,7 @@
|
||||
function nsfwShow(elem) {
|
||||
let content = elem.parentNode.getElementsBySelector("div.nswf.content")[0];
|
||||
|
||||
if (content) {
|
||||
Element.toggle(content);
|
||||
}
|
||||
}
|
108
plugins/nsfw/init.php
Normal file
108
plugins/nsfw/init.php
Normal file
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
class NSFW extends Plugin {
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Hide article content based on tags",
|
||||
"fox",
|
||||
false);
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_RENDER_ARTICLE, $this);
|
||||
$host->add_hook($host::HOOK_RENDER_ARTICLE_CDM, $this);
|
||||
$host->add_hook($host::HOOK_PREFS_TAB, $this);
|
||||
|
||||
}
|
||||
|
||||
function get_js() {
|
||||
return file_get_contents(dirname(__FILE__) . "/init.js");
|
||||
}
|
||||
|
||||
function hook_render_article($article) {
|
||||
$tags = array_map("trim", explode(",", $this->host->get($this, "tags")));
|
||||
$a_tags = array_map("trim", explode(",", $article["tag_cache"]));
|
||||
|
||||
if (count(array_intersect($tags, $a_tags)) > 0) {
|
||||
$article["content"] = "<div class='nswf wrapper'><button onclick=\"nsfwShow(this)\">".__("Not work safe (click to toggle)")."</button>
|
||||
<div class='nswf content' style='display : none'>".$article["content"]."</div></div>";
|
||||
}
|
||||
|
||||
return $article;
|
||||
}
|
||||
|
||||
function hook_render_article_cdm($article) {
|
||||
$tags = array_map("trim", explode(",", $this->host->get($this, "tags")));
|
||||
$a_tags = array_map("trim", explode(",", $article["tag_cache"]));
|
||||
|
||||
if (count(array_intersect($tags, $a_tags)) > 0) {
|
||||
$article["content"] = "<div class='nswf wrapper'><button onclick=\"nsfwShow(this)\">".__("Not work safe (click to toggle)")."</button>
|
||||
<div class='nswf content' style='display : none'>".$article["content"]."</div></div>";
|
||||
}
|
||||
|
||||
return $article;
|
||||
}
|
||||
|
||||
function hook_prefs_tab($args) {
|
||||
if ($args != "prefPrefs") return;
|
||||
|
||||
print "<div dojoType=\"dijit.layout.AccordionPane\"
|
||||
title=\"<i class='material-icons'>extension</i> ".__("NSFW Plugin")."\">";
|
||||
|
||||
print "<br/>";
|
||||
|
||||
$tags = $this->host->get($this, "tags");
|
||||
|
||||
print "<form dojoType=\"dijit.form.Form\">";
|
||||
|
||||
print "<script type=\"dojo/method\" event=\"onSubmit\" args=\"evt\">
|
||||
evt.preventDefault();
|
||||
if (this.validate()) {
|
||||
new Ajax.Request('backend.php', {
|
||||
parameters: dojo.objectToQuery(this.getValues()),
|
||||
onComplete: function(transport) {
|
||||
Notify.info(transport.responseText);
|
||||
}
|
||||
});
|
||||
//this.reset();
|
||||
}
|
||||
</script>";
|
||||
|
||||
print_hidden("op", "pluginhandler");
|
||||
print_hidden("method", "save");
|
||||
print_hidden("plugin", "nsfw");
|
||||
|
||||
print "<table width=\"100%\" class=\"prefPrefsList\">";
|
||||
|
||||
print "<tr><td width=\"40%\">".__("Tags to consider NSFW (comma-separated)")."</td>";
|
||||
print "<td class=\"prefValue\"><input dojoType=\"dijit.form.ValidationTextBox\" required=\"1\" name=\"tags\" value=\"$tags\"></td></tr>";
|
||||
|
||||
print "</table>";
|
||||
|
||||
print "<p><button dojoType=\"dijit.form.Button\" type=\"submit\">".
|
||||
__("Save")."</button>";
|
||||
|
||||
print "</form>";
|
||||
|
||||
print "</div>"; #pane
|
||||
}
|
||||
|
||||
function save() {
|
||||
$tags = explode(",", $_POST["tags"]);
|
||||
$tags = array_map("trim", $tags);
|
||||
$tags = array_map("mb_strtolower", $tags);
|
||||
$tags = join(", ", $tags);
|
||||
|
||||
$this->host->set($this, "tags", $tags);
|
||||
|
||||
echo __("Configuration saved.");
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
65
plugins/search_sphinx/init.php
Normal file
65
plugins/search_sphinx/init.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?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,
|
||||
"https://git.tt-rss.org/fox/tt-rss/wiki/SphinxSearch");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$host->add_hook($host::HOOK_SEARCH, $this);
|
||||
|
||||
// idk if that would work but checking for the class being loaded is somehow not enough
|
||||
if (class_exists("SphinxClient") && !defined('SEARCHD_COMMAND_SEARCH')) {
|
||||
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;
|
||||
}
|
||||
}
|
1695
plugins/search_sphinx/sphinxapi.php
Normal file
1695
plugins/search_sphinx/sphinxapi.php
Normal file
File diff suppressed because it is too large
Load Diff
142
plugins/share/init.php
Normal file
142
plugins/share/init.php
Normal file
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
class Share extends Plugin {
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Share article by unique URL",
|
||||
"fox");
|
||||
}
|
||||
|
||||
/* @var PluginHost $host */
|
||||
function init($host) {
|
||||
$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_css() {
|
||||
return file_get_contents(dirname(__FILE__) . "/share.css");
|
||||
}
|
||||
|
||||
function get_prefs_js() {
|
||||
return file_get_contents(dirname(__FILE__) . "/share_prefs.js");
|
||||
}
|
||||
|
||||
|
||||
function unshare() {
|
||||
$id = $_REQUEST['id'];
|
||||
|
||||
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET uuid = '' WHERE int_id = ?
|
||||
AND owner_uid = ?");
|
||||
$sth->execute([$id, $_SESSION['uid']]);
|
||||
|
||||
print "OK";
|
||||
}
|
||||
|
||||
function hook_prefs_tab_section($id) {
|
||||
if ($id == "prefFeedsPublishedGenerated") {
|
||||
|
||||
print "<h3>" . __("You can disable all articles shared by unique URLs here.") . "</h3>";
|
||||
|
||||
print "<button class='alt-danger' dojoType='dijit.form.Button' onclick=\"return Plugins.Share.clearKeys()\">".
|
||||
__('Unshare all articles')."</button> ";
|
||||
|
||||
print "</p>";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Silent
|
||||
function clearArticleKeys() {
|
||||
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET uuid = '' WHERE
|
||||
owner_uid = ?");
|
||||
$sth->execute([$_SESSION['uid']]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
function newkey() {
|
||||
$id = $_REQUEST['id'];
|
||||
$uuid = uniqid_short();
|
||||
|
||||
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET uuid = ? WHERE int_id = ?
|
||||
AND owner_uid = ?");
|
||||
$sth->execute([$uuid, $id, $_SESSION['uid']]);
|
||||
|
||||
print json_encode(array("link" => $uuid));
|
||||
}
|
||||
|
||||
function hook_article_button($line) {
|
||||
$img_class = $line['uuid'] ? "shared" : "";
|
||||
|
||||
return "<i id='SHARE-IMG-".$line['int_id']."' class='material-icons icon-share $img_class'
|
||||
style='cursor : pointer' onclick=\"Plugins.Share.shareArticle(".$line['int_id'].")\"
|
||||
title='".__('Share by URL')."'>link</i>";
|
||||
}
|
||||
|
||||
function shareArticle() {
|
||||
$param = $_REQUEST['param'];
|
||||
|
||||
$sth = $this->pdo->prepare("SELECT uuid FROM ttrss_user_entries WHERE int_id = ?
|
||||
AND owner_uid = ?");
|
||||
$sth->execute([$param, $_SESSION['uid']]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
|
||||
$uuid = $row['uuid'];
|
||||
|
||||
if (!$uuid) {
|
||||
$uuid = uniqid_short();
|
||||
|
||||
$sth = $this->pdo->prepare("UPDATE ttrss_user_entries SET uuid = ? WHERE int_id = ?
|
||||
AND owner_uid = ?");
|
||||
$sth->execute([$uuid, $param, $_SESSION['uid']]);
|
||||
}
|
||||
|
||||
print "<header>" . __("You can share this article by the following unique URL:") . "</header>";
|
||||
|
||||
$url_path = get_self_url_prefix();
|
||||
$url_path .= "/public.php?op=share&key=$uuid";
|
||||
|
||||
print "<section>
|
||||
<div class='panel text-center'>
|
||||
<a id='gen_article_url' href='$url_path' target='_blank' rel='noopener noreferrer'>$url_path</a>
|
||||
</div>
|
||||
</section>";
|
||||
|
||||
/* if (!label_find_id(__('Shared'), $_SESSION["uid"]))
|
||||
label_create(__('Shared'), $_SESSION["uid"]);
|
||||
|
||||
label_add_article($ref_id, __('Shared'), $_SESSION['uid']); */
|
||||
|
||||
|
||||
} else {
|
||||
print "Article not found.";
|
||||
}
|
||||
|
||||
print "<footer class='text-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>";
|
||||
|
||||
print "</footer>";
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
3
plugins/share/share.css
Normal file
3
plugins/share/share.css
Normal file
@ -0,0 +1,3 @@
|
||||
i.icon-share.shared {
|
||||
color : #0a0;
|
||||
}
|
80
plugins/share/share.js
Normal file
80
plugins/share/share.js
Normal file
@ -0,0 +1,80 @@
|
||||
Plugins.Share = {
|
||||
shareArticle: function(id) {
|
||||
if (dijit.byId("shareArticleDlg"))
|
||||
dijit.byId("shareArticleDlg").destroyRecursive();
|
||||
|
||||
const query = "backend.php?op=pluginhandler&plugin=share&method=shareArticle¶m=" + encodeURIComponent(id);
|
||||
|
||||
const dialog = new dijit.Dialog({
|
||||
id: "shareArticleDlg",
|
||||
title: __("Share article by URL"),
|
||||
style: "width: 600px",
|
||||
newurl: function () {
|
||||
if (confirm(__("Generate new share URL for this article?"))) {
|
||||
|
||||
Notify.progress("Trying to change URL...", true);
|
||||
|
||||
const query = {op: "pluginhandler", plugin: "share", method: "newkey", id: id};
|
||||
|
||||
xhrJson("backend.php", query, (reply) => {
|
||||
if (reply) {
|
||||
const new_link = reply.link;
|
||||
const 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);
|
||||
|
||||
const img = $("SHARE-IMG-" + id);
|
||||
img.addClassName("shared");
|
||||
|
||||
Notify.close();
|
||||
|
||||
} else {
|
||||
Notify.error("Could not change URL.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
unshare: function () {
|
||||
if (confirm(__("Remove sharing for this article?"))) {
|
||||
|
||||
const query = {op: "pluginhandler", plugin: "share", method: "unshare", id: id};
|
||||
|
||||
xhrPost("backend.php", query, () => {
|
||||
try {
|
||||
const img = $("SHARE-IMG-" + id);
|
||||
|
||||
if (img) {
|
||||
img.removeClassName("shared");
|
||||
img.up("div[id*=RROW]").removeClassName("shared");
|
||||
}
|
||||
|
||||
dialog.hide();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
href: query
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
|
||||
const img = $("SHARE-IMG-" + id);
|
||||
img.addClassName("shared");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
15
plugins/share/share_prefs.js
Normal file
15
plugins/share/share_prefs.js
Normal file
@ -0,0 +1,15 @@
|
||||
Plugins.Share = {
|
||||
clearKeys: function() {
|
||||
if (confirm(__("This will invalidate all previously shared article URLs. Continue?"))) {
|
||||
Notify.progress("Clearing URLs...");
|
||||
|
||||
const query = {op: "pluginhandler", plugin: "share", method: "clearArticleKeys"};
|
||||
|
||||
xhrPost("backend.php", query, () => {
|
||||
Notify.info("Shared URLs cleared.");
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
9
plugins/shorten_expanded/init.css
Normal file
9
plugins/shorten_expanded/init.css
Normal file
@ -0,0 +1,9 @@
|
||||
.content-shrink-wrap {
|
||||
overflow : hidden;
|
||||
text-overflow: ellipsis;
|
||||
height : 800px;
|
||||
}
|
||||
|
||||
.expand-prompt {
|
||||
margin-top : 16px;
|
||||
}
|
54
plugins/shorten_expanded/init.js
Normal file
54
plugins/shorten_expanded/init.js
Normal file
@ -0,0 +1,54 @@
|
||||
const _shorten_expanded_threshold = 1.5; //window heights
|
||||
|
||||
Plugins.Shorten_Expanded = {
|
||||
expand: function(id) {
|
||||
const row = $(id);
|
||||
|
||||
if (row) {
|
||||
const content = row.select(".content-shrink-wrap")[0];
|
||||
const link = row.select(".expand-prompt")[0];
|
||||
|
||||
if (content) content.removeClassName("content-shrink-wrap");
|
||||
if (link) Element.hide(link);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
require(['dojo/_base/kernel', 'dojo/ready'], function (dojo, ready) {
|
||||
ready(function() {
|
||||
PluginHost.register(PluginHost.HOOK_ARTICLE_RENDERED_CDM, function(row) {
|
||||
window.setTimeout(function() {
|
||||
if (row) {
|
||||
|
||||
const c_inner = row.select(".content-inner")[0];
|
||||
const c_inter = row.select(".intermediate")[0];
|
||||
|
||||
if (c_inner && c_inter &&
|
||||
row.offsetHeight >= _shorten_expanded_threshold * window.innerHeight) {
|
||||
|
||||
let tmp = document.createElement("div");
|
||||
|
||||
c_inter.select("> *:not([class*='attachments'])").each(function(p) {
|
||||
p.parentNode.removeChild(p);
|
||||
tmp.appendChild(p);
|
||||
});
|
||||
|
||||
c_inner.innerHTML = `<div class="content-shrink-wrap">
|
||||
${c_inner.innerHTML}
|
||||
${tmp.innerHTML}</div>
|
||||
<button dojoType="dijit.form.Button" class="alt-info expand-prompt" onclick="return Plugins.Shorten_Expanded.expand('${row.id}')" href="#">
|
||||
${__("Click to expand article")}</button>`;
|
||||
|
||||
dojo.parser.parse(c_inner);
|
||||
|
||||
Headlines.unpackVisible();
|
||||
}
|
||||
}
|
||||
}, 150);
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
28
plugins/shorten_expanded/init.php
Normal file
28
plugins/shorten_expanded/init.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
}
|
30
plugins/swap_jk/init.php
Normal file
30
plugins/swap_jk/init.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
class Swap_JK extends Plugin {
|
||||
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Swap j and k hotkeys (for vi brethren)",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_HOTKEY_MAP, $this);
|
||||
}
|
||||
|
||||
function hook_hotkey_map($hotkeys) {
|
||||
|
||||
$hotkeys["j"] = "next_feed";
|
||||
$hotkeys["k"] = "prev_feed";
|
||||
|
||||
return $hotkeys;
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
9
plugins/toggle_sidebar/init.js
Normal file
9
plugins/toggle_sidebar/init.js
Normal file
@ -0,0 +1,9 @@
|
||||
Plugins.Toggle_Sidebar = {
|
||||
toggle: function() {
|
||||
Feeds.toggle();
|
||||
|
||||
const label = document.querySelector("i.toggle-sidebar-label");
|
||||
|
||||
label.innerHTML = Element.visible("feeds-holder") ? 'chevron_left' : 'chevron_right';
|
||||
}
|
||||
};
|
38
plugins/toggle_sidebar/init.php
Normal file
38
plugins/toggle_sidebar/init.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
class Toggle_Sidebar extends Plugin {
|
||||
|
||||
private $host;
|
||||
|
||||
function about() {
|
||||
return array(1.0,
|
||||
"Adds a main toolbar button to toggle sidebar",
|
||||
"fox");
|
||||
}
|
||||
|
||||
function init($host) {
|
||||
$this->host = $host;
|
||||
|
||||
$host->add_hook($host::HOOK_MAIN_TOOLBAR_BUTTON, $this);
|
||||
}
|
||||
|
||||
function get_js() {
|
||||
return file_get_contents(__DIR__ . "/init.js");
|
||||
}
|
||||
|
||||
function hook_main_toolbar_button() {
|
||||
?>
|
||||
|
||||
<button dojoType="dijit.form.Button" onclick="Plugins.Toggle_Sidebar.toggle(this)">
|
||||
<i class="material-icons toggle-sidebar-label"
|
||||
title="<?php echo __('Toggle sidebar') ?>">chevron_left</i>
|
||||
</button>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
78
plugins/vf_shared/init.php
Normal file
78
plugins/vf_shared/init.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
class VF_Shared extends Plugin {
|
||||
|
||||
/* @var PluginHost $host */
|
||||
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"), 'link', $this);
|
||||
}
|
||||
|
||||
function api_version() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function get_unread($feed_id) {
|
||||
$sth = $this->pdo->prepare("select count(int_id) AS count
|
||||
from ttrss_user_entries where owner_uid = ? and unread = true and uuid != ''");
|
||||
$sth->execute([$_SESSION['uid']]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
return $row['count'];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function get_total($feed_id) {
|
||||
$sth = $this->pdo->prepare("select count(int_id) AS count
|
||||
from ttrss_user_entries where owner_uid = ? and uuid != ''");
|
||||
$sth->execute([$_SESSION['uid']]);
|
||||
|
||||
if ($row = $sth->fetch()) {
|
||||
return $row['count'];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
function get_headlines($feed_id, $options) {
|
||||
$params = array(
|
||||
"feed" => -4,
|
||||
"limit" => $options["limit"],
|
||||
"view_mode" => $this->get_unread(-1) > 0 ? "adaptive" : "all_articles",
|
||||
"search" => $options['search'],
|
||||
"override_order" => $options['override_order'],
|
||||
"offset" => $options["offset"],
|
||||
"filter" => $options["filter"],
|
||||
"since_id" => $options["since_id"],
|
||||
"include_children" => $options["include_children"],
|
||||
"override_strategy" => "uuid != ''",
|
||||
"override_vfeed" => "ttrss_feeds.title AS feed_title,"
|
||||
);
|
||||
|
||||
$qfh_ret = Feeds::queryFeedHeadlines($params);
|
||||
$qfh_ret[1] = __("Shared articles");
|
||||
|
||||
return $qfh_ret;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user