Automated build for v0.01

This commit is contained in:
Fmstrat
2019-03-22 10:17:29 -04:00
commit 791b998489
2771 changed files with 222096 additions and 0 deletions

1
plugins/af_comics/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
filters.local/*.php

View 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;
}
}

View File

@ -0,0 +1,5 @@
<?php
abstract class Af_ComicFilter {
public abstract function supported();
public abstract function process(&$article);
}

View File

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}
?>

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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
View 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;
}
}

View 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;
}
}

View File

@ -0,0 +1,21 @@
Plugins.Psql_Trgm = {
showRelated: function (id) {
const query = "backend.php?op=pluginhandler&plugin=af_psql_trgm&method=showrelated&param=" + 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();
}
};

View 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 " &mdash; $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
View 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>&nbsp;".__('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
View 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
View 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
View 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;
}
}

View 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
View 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 "&nbsp;<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 "&nbsp;<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;
}
}

View 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;
});
});
});

View 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;
}
}

View 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;
});
});
});

View 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;
}
}

View 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
View 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;
}
}
?>

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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
View 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\">&nbsp;
</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
View File

240
plugins/mail/init.php Normal file
View 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
View 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&param=" + 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

34
plugins/mailto/init.js Normal file
View 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&param=" + 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

View 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;
}
}

View 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";
};
});
});
});

View 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;
}
}

View File

@ -0,0 +1,9 @@
require(['dojo/_base/kernel', 'dojo/ready'], function (dojo, ready) {
ready(function () {
hash_set = function () {
};
hash_get = function () {
};
});
});

View 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
View 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
View File

@ -0,0 +1,40 @@
Plugins.Note = {
edit: function(id) {
const query = "backend.php?op=pluginhandler&plugin=note&method=edit&param=" + 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
View 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
View 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;
}
}

View 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;
}
}

File diff suppressed because it is too large Load Diff

142
plugins/share/init.php Normal file
View 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
View File

@ -0,0 +1,3 @@
i.icon-share.shared {
color : #0a0;
}

80
plugins/share/share.js Normal file
View 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&param=" + 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(/\&amp;key=.*$/,
"&amp;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");
}
};

View 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;
}
};

View File

@ -0,0 +1,9 @@
.content-shrink-wrap {
overflow : hidden;
text-overflow: ellipsis;
height : 800px;
}
.expand-prompt {
margin-top : 16px;
}

View 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;
});
});
});

View 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
View 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;
}
}

View 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';
}
};

View 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;
}
}
?>

View 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;
}
}