summaryrefslogtreecommitdiff
path: root/classes
diff options
context:
space:
mode:
Diffstat (limited to 'classes')
-rwxr-xr-xclasses/api.php24
-rwxr-xr-xclasses/article.php8
-rw-r--r--classes/counters.php4
-rw-r--r--classes/digest.php10
-rw-r--r--classes/diskcache.php243
-rwxr-xr-xclasses/feeds.php77
-rwxr-xr-xclasses/handler/public.php42
-rw-r--r--classes/labels.php6
-rw-r--r--classes/opml.php42
-rwxr-xr-xclasses/pluginhost.php21
-rwxr-xr-xclasses/pref/feeds.php23
-rwxr-xr-xclasses/pref/filters.php55
-rw-r--r--classes/pref/prefs.php12
-rwxr-xr-xclasses/rssutils.php175
-rw-r--r--classes/templator.php21
15 files changed, 533 insertions, 230 deletions
diff --git a/classes/api.php b/classes/api.php
index 339e9eef1..7b0c58a98 100755
--- a/classes/api.php
+++ b/classes/api.php
@@ -160,9 +160,9 @@ class API extends Handler {
$unread += Feeds::getCategoryChildrenUnread($line["id"]);
if ($unread || !$unread_only) {
- array_push($cats, array("id" => $line["id"],
+ array_push($cats, array("id" => (int) $line["id"],
"title" => $line["title"],
- "unread" => $unread,
+ "unread" => (int) $unread,
"order_id" => (int) $line["order_id"],
));
}
@@ -174,9 +174,9 @@ class API extends Handler {
$unread = getFeedUnread($cat_id, true);
if ($unread || !$unread_only) {
- array_push($cats, array("id" => $cat_id,
+ array_push($cats, array("id" => (int) $cat_id,
"title" => Feeds::getCategoryTitle($cat_id),
- "unread" => $unread));
+ "unread" => (int) $unread));
}
}
}
@@ -214,21 +214,7 @@ class API extends Handler {
$_SESSION['hasSandbox'] = $has_sandbox;
- $skip_first_id_check = false;
-
- $override_order = false;
- switch (clean($_REQUEST["order_by"])) {
- case "title":
- $override_order = "ttrss_entries.title, date_entered, updated";
- break;
- case "date_reverse":
- $override_order = "score DESC, date_entered, updated";
- $skip_first_id_check = true;
- break;
- case "feed_dates":
- $override_order = "updated DESC";
- break;
- }
+ list($override_order, $skip_first_id_check) = Feeds::order_to_override_query(clean($_REQUEST["order_by"]));
/* do not rely on params below */
diff --git a/classes/article.php b/classes/article.php
index 74dbdae53..998528fe8 100755
--- a/classes/article.php
+++ b/classes/article.php
@@ -94,7 +94,7 @@ class Article extends Handler_Protected {
":id" => $ref_id];
$sth->execute($params);
}
-
+
$sth = $pdo->prepare("UPDATE ttrss_user_entries SET published = true,
last_published = NOW() WHERE
int_id = ? AND owner_uid = ?");
@@ -393,7 +393,7 @@ class Article extends Handler_Protected {
# $entry .= " <a target=\"_blank\" href=\"" . htmlspecialchars($url) . "\" rel=\"noopener noreferrer\">" .
# $filename . " (" . $ctype . ")" . "</a>";
- $entry = "<div onclick=\"popupOpenUrl('".htmlspecialchars($url)."')\"
+ $entry = "<div onclick=\"Article.popupOpenUrl('".htmlspecialchars($url)."')\"
dojoType=\"dijit.MenuItem\">$filename ($ctype)</div>";
array_push($entries_html, $entry);
@@ -473,7 +473,7 @@ class Article extends Handler_Protected {
else
$filename = "";
- $rv .= "<div onclick='popupOpenUrl(\"".htmlspecialchars($entry["url"])."\")'
+ $rv .= "<div onclick='Article.popupOpenUrl(\"".htmlspecialchars($entry["url"])."\")'
dojoType=\"dijit.MenuItem\">".$filename . $title."</div>";
};
@@ -583,7 +583,7 @@ class Article extends Handler_Protected {
return "<div class='article-note $note_class'>
<i class='material-icons'>note</i>
- <div $onclick class='body'>$note</div>
+ <div $onclick class='body'>$note</div>
</div>";
return $str;
diff --git a/classes/counters.php b/classes/counters.php
index d8ed27621..4230fa4d4 100644
--- a/classes/counters.php
+++ b/classes/counters.php
@@ -234,8 +234,8 @@ class Counters {
COUNT(u1.unread) AS total
FROM ttrss_labels2 LEFT JOIN ttrss_user_labels2 ON
(ttrss_labels2.id = label_id)
- LEFT JOIN ttrss_user_entries AS u1 ON u1.ref_id = article_id
- WHERE ttrss_labels2.owner_uid = :uid AND u1.owner_uid = :uid
+ LEFT JOIN ttrss_user_entries AS u1 ON u1.ref_id = article_id AND u1.owner_uid = :uid
+ WHERE ttrss_labels2.owner_uid = :uid
GROUP BY ttrss_labels2.id, ttrss_labels2.caption");
$sth->execute([":uid" => $_SESSION['uid']]);
diff --git a/classes/digest.php b/classes/digest.php
index c9e9f24e7..9101b52f4 100644
--- a/classes/digest.php
+++ b/classes/digest.php
@@ -90,13 +90,11 @@ class Digest
static function prepare_headlines_digest($user_id, $days = 1, $limit = 1000) {
- require_once "lib/MiniTemplator.class.php";
+ $tpl = new Templator();
+ $tpl_t = new Templator();
- $tpl = new MiniTemplator;
- $tpl_t = new MiniTemplator;
-
- $tpl->readTemplateFromFile("templates/digest_template_html.txt");
- $tpl_t->readTemplateFromFile("templates/digest_template.txt");
+ $tpl->readTemplateFromFile("digest_template_html.txt");
+ $tpl_t->readTemplateFromFile("digest_template.txt");
$user_tz_string = get_pref('USER_TIMEZONE', $user_id);
$local_ts = convert_timestamp(time(), 'UTC', $user_tz_string);
diff --git a/classes/diskcache.php b/classes/diskcache.php
index 7e4a8335d..68829b8e3 100644
--- a/classes/diskcache.php
+++ b/classes/diskcache.php
@@ -2,6 +2,194 @@
class DiskCache {
private $dir;
+ // https://stackoverflow.com/a/53662733
+ private $mimeMap = [
+ 'video/3gpp2' => '3g2',
+ 'video/3gp' => '3gp',
+ 'video/3gpp' => '3gp',
+ 'application/x-compressed' => '7zip',
+ 'audio/x-acc' => 'aac',
+ 'audio/ac3' => 'ac3',
+ 'application/postscript' => 'ai',
+ 'audio/x-aiff' => 'aif',
+ 'audio/aiff' => 'aif',
+ 'audio/x-au' => 'au',
+ 'video/x-msvideo' => 'avi',
+ 'video/msvideo' => 'avi',
+ 'video/avi' => 'avi',
+ 'application/x-troff-msvideo' => 'avi',
+ 'application/macbinary' => 'bin',
+ 'application/mac-binary' => 'bin',
+ 'application/x-binary' => 'bin',
+ 'application/x-macbinary' => 'bin',
+ 'image/bmp' => 'bmp',
+ 'image/x-bmp' => 'bmp',
+ 'image/x-bitmap' => 'bmp',
+ 'image/x-xbitmap' => 'bmp',
+ 'image/x-win-bitmap' => 'bmp',
+ 'image/x-windows-bmp' => 'bmp',
+ 'image/ms-bmp' => 'bmp',
+ 'image/x-ms-bmp' => 'bmp',
+ 'application/bmp' => 'bmp',
+ 'application/x-bmp' => 'bmp',
+ 'application/x-win-bitmap' => 'bmp',
+ 'application/cdr' => 'cdr',
+ 'application/coreldraw' => 'cdr',
+ 'application/x-cdr' => 'cdr',
+ 'application/x-coreldraw' => 'cdr',
+ 'image/cdr' => 'cdr',
+ 'image/x-cdr' => 'cdr',
+ 'zz-application/zz-winassoc-cdr' => 'cdr',
+ 'application/mac-compactpro' => 'cpt',
+ 'application/pkix-crl' => 'crl',
+ 'application/pkcs-crl' => 'crl',
+ 'application/x-x509-ca-cert' => 'crt',
+ 'application/pkix-cert' => 'crt',
+ 'text/css' => 'css',
+ 'text/x-comma-separated-values' => 'csv',
+ 'text/comma-separated-values' => 'csv',
+ 'application/vnd.msexcel' => 'csv',
+ 'application/x-director' => 'dcr',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
+ 'application/x-dvi' => 'dvi',
+ 'message/rfc822' => 'eml',
+ 'application/x-msdownload' => 'exe',
+ 'video/x-f4v' => 'f4v',
+ 'audio/x-flac' => 'flac',
+ 'video/x-flv' => 'flv',
+ 'image/gif' => 'gif',
+ 'application/gpg-keys' => 'gpg',
+ 'application/x-gtar' => 'gtar',
+ 'application/x-gzip' => 'gzip',
+ 'application/mac-binhex40' => 'hqx',
+ 'application/mac-binhex' => 'hqx',
+ 'application/x-binhex40' => 'hqx',
+ 'application/x-mac-binhex40' => 'hqx',
+ 'text/html' => 'html',
+ 'image/x-icon' => 'ico',
+ 'image/x-ico' => 'ico',
+ 'image/vnd.microsoft.icon' => 'ico',
+ 'text/calendar' => 'ics',
+ 'application/java-archive' => 'jar',
+ 'application/x-java-application' => 'jar',
+ 'application/x-jar' => 'jar',
+ 'image/jp2' => 'jp2',
+ 'video/mj2' => 'jp2',
+ 'image/jpx' => 'jp2',
+ 'image/jpm' => 'jp2',
+ 'image/jpeg' => 'jpg',
+ 'image/pjpeg' => 'jpg',
+ 'application/x-javascript' => 'js',
+ 'application/json' => 'json',
+ 'text/json' => 'json',
+ 'application/vnd.google-earth.kml+xml' => 'kml',
+ 'application/vnd.google-earth.kmz' => 'kmz',
+ 'text/x-log' => 'log',
+ 'audio/x-m4a' => 'm4a',
+ 'audio/mp4' => 'm4a',
+ 'application/vnd.mpegurl' => 'm4u',
+ 'audio/midi' => 'mid',
+ 'application/vnd.mif' => 'mif',
+ 'video/quicktime' => 'mov',
+ 'video/x-sgi-movie' => 'movie',
+ 'audio/mpeg' => 'mp3',
+ 'audio/mpg' => 'mp3',
+ 'audio/mpeg3' => 'mp3',
+ 'audio/mp3' => 'mp3',
+ 'video/mp4' => 'mp4',
+ 'video/mpeg' => 'mpeg',
+ 'application/oda' => 'oda',
+ 'audio/ogg' => 'ogg',
+ 'video/ogg' => 'ogg',
+ 'application/ogg' => 'ogg',
+ 'font/otf' => 'otf',
+ 'application/x-pkcs10' => 'p10',
+ 'application/pkcs10' => 'p10',
+ 'application/x-pkcs12' => 'p12',
+ 'application/x-pkcs7-signature' => 'p7a',
+ 'application/pkcs7-mime' => 'p7c',
+ 'application/x-pkcs7-mime' => 'p7c',
+ 'application/x-pkcs7-certreqresp' => 'p7r',
+ 'application/pkcs7-signature' => 'p7s',
+ 'application/pdf' => 'pdf',
+ 'application/octet-stream' => 'pdf',
+ 'application/x-x509-user-cert' => 'pem',
+ 'application/x-pem-file' => 'pem',
+ 'application/pgp' => 'pgp',
+ 'application/x-httpd-php' => 'php',
+ 'application/php' => 'php',
+ 'application/x-php' => 'php',
+ 'text/php' => 'php',
+ 'text/x-php' => 'php',
+ 'application/x-httpd-php-source' => 'php',
+ 'image/png' => 'png',
+ 'image/x-png' => 'png',
+ 'application/powerpoint' => 'ppt',
+ 'application/vnd.ms-powerpoint' => 'ppt',
+ 'application/vnd.ms-office' => 'ppt',
+ 'application/msword' => 'ppt',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
+ 'application/x-photoshop' => 'psd',
+ 'image/vnd.adobe.photoshop' => 'psd',
+ 'audio/x-realaudio' => 'ra',
+ 'audio/x-pn-realaudio' => 'ram',
+ 'application/x-rar' => 'rar',
+ 'application/rar' => 'rar',
+ 'application/x-rar-compressed' => 'rar',
+ 'audio/x-pn-realaudio-plugin' => 'rpm',
+ 'application/x-pkcs7' => 'rsa',
+ 'text/rtf' => 'rtf',
+ 'text/richtext' => 'rtx',
+ 'video/vnd.rn-realvideo' => 'rv',
+ 'application/x-stuffit' => 'sit',
+ 'application/smil' => 'smil',
+ 'text/srt' => 'srt',
+ 'image/svg+xml' => 'svg',
+ 'application/x-shockwave-flash' => 'swf',
+ 'application/x-tar' => 'tar',
+ 'application/x-gzip-compressed' => 'tgz',
+ 'image/tiff' => 'tiff',
+ 'font/ttf' => 'ttf',
+ 'text/plain' => 'txt',
+ 'text/x-vcard' => 'vcf',
+ 'application/videolan' => 'vlc',
+ 'text/vtt' => 'vtt',
+ 'audio/x-wav' => 'wav',
+ 'audio/wave' => 'wav',
+ 'audio/wav' => 'wav',
+ 'application/wbxml' => 'wbxml',
+ 'video/webm' => 'webm',
+ 'image/webp' => 'webp',
+ 'audio/x-ms-wma' => 'wma',
+ 'application/wmlc' => 'wmlc',
+ 'video/x-ms-wmv' => 'wmv',
+ 'video/x-ms-asf' => 'wmv',
+ 'font/woff' => 'woff',
+ 'font/woff2' => 'woff2',
+ 'application/xhtml+xml' => 'xhtml',
+ 'application/excel' => 'xl',
+ 'application/msexcel' => 'xls',
+ 'application/x-msexcel' => 'xls',
+ 'application/x-ms-excel' => 'xls',
+ 'application/x-excel' => 'xls',
+ 'application/x-dos_ms_excel' => 'xls',
+ 'application/xls' => 'xls',
+ 'application/x-xls' => 'xls',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
+ 'application/vnd.ms-excel' => 'xlsx',
+ 'application/xml' => 'xml',
+ 'text/xml' => 'xml',
+ 'text/xsl' => 'xsl',
+ 'application/xspf+xml' => 'xspf',
+ 'application/x-compress' => 'z',
+ 'application/x-zip' => 'zip',
+ 'application/zip' => 'zip',
+ 'application/x-zip-compressed' => 'zip',
+ 'application/s-compressed' => 'zip',
+ 'multipart/x-zip' => 'zip',
+ 'text/x-scriptzsh' => 'zsh'
+ ];
+
public function __construct($dir) {
$this->dir = CACHE_DIR . "/" . clean_filename($dir);
}
@@ -66,8 +254,22 @@ class DiskCache {
return null;
}
+ public function getFakeExtension($filename) {
+ $mimetype = $this->getMimeType($filename);
+
+ if ($mimetype)
+ return isset($this->mimeMap[$mimetype]) ? $this->mimeMap[$mimetype] : "";
+ else
+ return "";
+ }
+
public function send($filename) {
- header("Content-Disposition: inline; filename=\"$filename\"");
+ $fake_extension = $this->getFakeExtension($filename);
+
+ if ($fake_extension)
+ $fake_extension = ".$fake_extension";
+
+ header("Content-Disposition: inline; filename=\"${filename}${fake_extension}\"");
return send_local_file($this->getFullPath($filename));
}
@@ -79,6 +281,7 @@ class DiskCache {
// check for locally cached (media) URLs and rewrite to local versions
// this is called separately after sanitize() and plugin render article hooks to allow
// plugins work on original source URLs used before caching
+ // NOTE: URLs should be already absolutized because this is called after sanitize()
static public function rewriteUrls($str)
{
$res = trim($str);
@@ -89,31 +292,41 @@ class DiskCache {
$xpath = new DOMXPath($doc);
$cache = new DiskCache("images");
- $entries = $xpath->query('(//img[@src]|//picture/source[@src]|//video[@poster]|//video/source[@src]|//audio/source[@src])');
+ $entries = $xpath->query('(//img[@src]|//source[@src|@srcset]|//video[@poster|@src])');
$need_saving = false;
foreach ($entries as $entry) {
+ foreach (array('src', 'poster') as $attr) {
+ if ($entry->hasAttribute($attr)) {
+ $url = $entry->getAttribute($attr);
+ $cached_filename = sha1($url);
+
+ if ($cache->exists($cached_filename)) {
+ $url = $cache->getUrl($cached_filename);
- if ($entry->hasAttribute('src') || $entry->hasAttribute('poster')) {
+ $entry->setAttribute($attr, $url);
+ $entry->removeAttribute("srcset");
+
+ $need_saving = true;
+ }
+ }
+ }
- // should be already absolutized because this is called after sanitize()
- $src = $entry->hasAttribute('poster') ? $entry->getAttribute('poster') : $entry->getAttribute('src');
- $cached_filename = sha1($src);
+ if ($entry->hasAttribute("srcset")) {
+ $matches = RSSUtils::decode_srcset($entry->getAttribute('srcset'));
- if ($cache->exists($cached_filename)) {
+ for ($i = 0; $i < count($matches); $i++) {
+ $cached_filename = sha1($matches[$i]["url"]);
- $src = $cache->getUrl(sha1($src));
+ if ($cache->exists($cached_filename)) {
+ $matches[$i]["url"] = $cache->getUrl($cached_filename);
- if ($entry->hasAttribute('poster'))
- $entry->setAttribute('poster', $src);
- else {
- $entry->setAttribute('src', $src);
- $entry->removeAttribute("srcset");
+ $need_saving = true;
}
-
- $need_saving = true;
}
+
+ $entry->setAttribute("srcset", RSSUtils::encode_srcset($matches));
}
}
diff --git a/classes/feeds.php b/classes/feeds.php
index 77add790e..55a514cc0 100755
--- a/classes/feeds.php
+++ b/classes/feeds.php
@@ -529,21 +529,7 @@ class Feeds extends Handler_Protected {
$reply['headlines'] = [];
- $override_order = false;
- $skip_first_id_check = false;
-
- switch ($order_by) {
- case "title":
- $override_order = "ttrss_entries.title, date_entered, updated";
- break;
- case "date_reverse":
- $override_order = "score DESC, date_entered, updated";
- $skip_first_id_check = true;
- break;
- case "feed_dates":
- $override_order = "updated DESC";
- break;
- }
+ list($override_order, $skip_first_id_check) = Feeds::order_to_override_query($order_by);
$ret = $this->format_headlines_list($feed, $method,
$view_mode, $limit, $cat_view, $offset,
@@ -701,12 +687,12 @@ class Feeds extends Handler_Protected {
print "<section>";
print "<label>
<label class='checkbox'><input type='checkbox' name='need_auth' dojoType='dijit.form.CheckBox' id='feedDlg_loginCheck'
- onclick='displayIfChecked(this, \"feedDlg_loginContainer\")'>
+ onclick='App.displayIfChecked(this, \"feedDlg_loginContainer\")'>
".__('This feed requires authentication.')."</label>";
print "</section>";
print "<footer>";
- print "<button dojoType='dijit.form.Button' class='alt-primary' type='submit'
+ print "<button dojoType='dijit.form.Button' class='alt-primary' type='submit'
onclick=\"return dijit.byId('feedAddDlg').execute()\">".__('Subscribe')."</button>";
print "<button dojoType='dijit.form.Button' onclick=\"return dijit.byId('feedAddDlg').hide()\">".__('Cancel')."</button>";
@@ -1337,7 +1323,7 @@ class Feeds extends Handler_Protected {
return 0;
} else if ($cat == -2) {
- $sth = $pdo->prepare("SELECT COUNT(DISTINCT article_id) AS unread
+ $sth = $pdo->prepare("SELECT COUNT(DISTINCT article_id) AS unread
FROM ttrss_user_entries ue, ttrss_user_labels2 l
WHERE article_id = ref_id AND unread IS true AND ue.owner_uid = :uid");
$sth->execute(["uid" => $owner_uid]);
@@ -1373,8 +1359,8 @@ class Feeds extends Handler_Protected {
$pdo = Db::pdo();
- $sth = $pdo->prepare("SELECT SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count
- FROM ttrss_user_entries ue
+ $sth = $pdo->prepare("SELECT SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count
+ FROM ttrss_user_entries ue
WHERE ue.owner_uid = ?");
$sth->execute([$user_id]);
@@ -1468,7 +1454,7 @@ class Feeds extends Handler_Protected {
}
if (DB_TYPE == "pgsql") {
- $test_sth = $pdo->prepare("select $search_query_part
+ $test_sth = $pdo->prepare("select $search_query_part
FROM ttrss_entries, ttrss_user_entries WHERE id = ref_id limit 1");
try {
@@ -2267,6 +2253,24 @@ class Feeds extends Handler_Protected {
if (!$not) array_push($search_words, $k);
}
break;
+ case "label":
+ if ($commandpair[1]) {
+ $label_id = Labels::find_id($commandpair[1], $_SESSION["uid"]);
+
+ if ($label_id) {
+ array_push($query_keywords, "($not
+ (ttrss_entries.id IN (
+ SELECT article_id FROM ttrss_user_labels2 WHERE
+ label_id = ".$pdo->quote($label_id).")))");
+ } else {
+ array_push($query_keywords, "(false)");
+ }
+ } else {
+ array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
+ OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
+ if (!$not) array_push($search_words, $k);
+ }
+ break;
case "unread":
if ($commandpair[1]) {
if ($commandpair[1] == "true")
@@ -2323,9 +2327,38 @@ class Feeds extends Handler_Protected {
}
- $search_query_part = implode("AND", $query_keywords);
+ if (count($query_keywords) > 0)
+ $search_query_part = implode("AND", $query_keywords);
+ else
+ $search_query_part = "false";
return array($search_query_part, $search_words);
}
+
+ static function order_to_override_query($order) {
+ $query = "";
+ $skip_first_id = false;
+
+ foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE) as $p) {
+ list ($query, $skip_first_id) = $p->hook_headlines_custom_sort_override($order);
+
+ if ($query) return [$query, $skip_first_id];
+ }
+
+ switch ($order) {
+ case "title":
+ $query = "ttrss_entries.title, date_entered, updated";
+ break;
+ case "date_reverse":
+ $query = "updated";
+ $skip_first_id = true;
+ break;
+ case "feed_dates":
+ $query = "updated DESC";
+ break;
+ }
+
+ return [$query, $skip_first_id];
+ }
}
diff --git a/classes/handler/public.php b/classes/handler/public.php
index 8c2700012..e6d94e223 100755
--- a/classes/handler/public.php
+++ b/classes/handler/public.php
@@ -5,8 +5,6 @@ class Handler_Public extends Handler {
$limit, $offset, $search,
$view_mode = false, $format = 'atom', $order = false, $orig_guid = false, $start_ts = false) {
- require_once "lib/MiniTemplator.class.php";
-
$note_style = "background-color : #fff7d5;
border-width : 1px; ".
"padding : 5px; border-style : dashed; border-color : #e7d796;".
@@ -14,24 +12,16 @@ class Handler_Public extends Handler {
if (!$limit) $limit = 60;
- $date_sort_field = "date_entered DESC, updated DESC";
+ list($override_order, $skip_first_id_check) = Feeds::order_to_override_query($order);
- if ($feed == -2 && !$is_cat) {
- $date_sort_field = "last_published DESC";
- } else if ($feed == -1 && !$is_cat) {
- $date_sort_field = "last_marked DESC";
- }
+ if (!$override_order) {
+ $override_order = "date_entered DESC, updated DESC";
- switch ($order) {
- case "title":
- $date_sort_field = "ttrss_entries.title, date_entered, updated";
- break;
- case "date_reverse":
- $date_sort_field = "date_entered, updated";
- break;
- case "feed_dates":
- $date_sort_field = "updated DESC";
- break;
+ if ($feed == -2 && !$is_cat) {
+ $override_order = "last_published DESC";
+ } else if ($feed == -1 && !$is_cat) {
+ $override_order = "last_marked DESC";
+ }
}
$params = array(
@@ -41,7 +31,7 @@ class Handler_Public extends Handler {
"view_mode" => $view_mode,
"cat_view" => $is_cat,
"search" => $search,
- "override_order" => $date_sort_field,
+ "override_order" => $override_order,
"include_children" => true,
"ignore_vfeed_group" => true,
"offset" => $offset,
@@ -80,9 +70,9 @@ class Handler_Public extends Handler {
if (!$feed_site_url) $feed_site_url = get_self_url_prefix();
if ($format == 'atom') {
- $tpl = new MiniTemplator;
+ $tpl = new Templator();
- $tpl->readTemplateFromFile("templates/generated_feed.txt");
+ $tpl->readTemplateFromFile("generated_feed.txt");
$tpl->setVariable('FEED_TITLE', $feed_title, true);
$tpl->setVariable('VERSION', get_version(), true);
@@ -680,9 +670,9 @@ class Handler_Public extends Handler {
$remember_me = clean($_POST["remember_me"]);
if ($remember_me) {
- session_set_cookie_params(SESSION_COOKIE_LIFETIME);
+ @session_set_cookie_params(SESSION_COOKIE_LIFETIME);
} else {
- session_set_cookie_params(0);
+ @session_set_cookie_params(0);
}
if (authenticate_user($login, $password)) {
@@ -1030,11 +1020,9 @@ class Handler_Public extends Handler {
$resetpass_link = get_self_url_prefix() . "/public.php?op=forgotpass&hash=" . $resetpass_token .
"&login=" . urlencode($login);
- require_once "lib/MiniTemplator.class.php";
-
- $tpl = new MiniTemplator;
+ $tpl = new Templator();
- $tpl->readTemplateFromFile("templates/resetpass_link_template.txt");
+ $tpl->readTemplateFromFile("resetpass_link_template.txt");
$tpl->setVariable('LOGIN', $login);
$tpl->setVariable('RESETPASS_LINK', $resetpass_link);
diff --git a/classes/labels.php b/classes/labels.php
index 19d060617..7a69a5191 100644
--- a/classes/labels.php
+++ b/classes/labels.php
@@ -12,7 +12,7 @@ class Labels
static function find_id($label, $owner_uid) {
$pdo = Db::pdo();
- $sth = $pdo->prepare("SELECT id FROM ttrss_labels2 WHERE caption = ?
+ $sth = $pdo->prepare("SELECT id FROM ttrss_labels2 WHERE LOWER(caption) = LOWER(?)
AND owner_uid = ? LIMIT 1");
$sth->execute([$label, $owner_uid]);
@@ -186,7 +186,7 @@ class Labels
}
$sth = $pdo->prepare("SELECT id FROM ttrss_labels2
- WHERE caption = ? AND owner_uid = ?");
+ WHERE LOWER(caption) = LOWER(?) AND owner_uid = ?");
$sth->execute([$caption, $owner_uid]);
if (!$sth->fetch()) {
@@ -202,4 +202,4 @@ class Labels
return $result;
}
-} \ No newline at end of file
+}
diff --git a/classes/opml.php b/classes/opml.php
index 48db9a8a3..37e653a39 100644
--- a/classes/opml.php
+++ b/classes/opml.php
@@ -8,7 +8,7 @@ class Opml extends Handler_Protected {
}
function export() {
- $output_name = "tt-rss_".date("Y-m-d").".opml";
+ $output_name = sprintf("tt-rss_%s_%s.opml", $_SESSION["name"], date("Y-m-d"));
$include_settings = $_REQUEST["include_settings"] == "1";
$owner_uid = $_SESSION["uid"];
@@ -62,7 +62,7 @@ class Opml extends Handler_Protected {
$ttrss_specific_qpart = "";
if ($cat_id) {
- $sth = $this->pdo->prepare("SELECT title,order_id
+ $sth = $this->pdo->prepare("SELECT title,order_id
FROM ttrss_feed_categories WHERE id = ?
AND owner_uid = ?");
$sth->execute([$cat_id, $owner_uid]);
@@ -90,7 +90,7 @@ class Opml extends Handler_Protected {
$out .= $this->opml_export_category($owner_uid, $line["id"], $hide_private_feeds, $include_settings);
}
- $fsth = $this->pdo->prepare("select title, feed_url, site_url, update_interval, order_id
+ $fsth = $this->pdo->prepare("select title, feed_url, site_url, update_interval, order_id, purge_interval
FROM ttrss_feeds WHERE
(cat_id = :cat OR (:cat = 0 AND cat_id IS NULL)) AND owner_uid = :uid AND $hide_qpart
ORDER BY order_id, title");
@@ -105,8 +105,9 @@ class Opml extends Handler_Protected {
if ($include_settings) {
$update_interval = (int)$fline["update_interval"];
$order_id = (int)$fline["order_id"];
+ $purge_interval = (int)$fline["purge_interval"];
- $ttrss_specific_qpart = "ttrssSortOrder=\"$order_id\" ttrssUpdateInterval=\"$update_interval\"";
+ $ttrss_specific_qpart = "ttrssSortOrder=\"$order_id\" ttrssPurgeInterval=\"$purge_interval\" ttrssUpdateInterval=\"$update_interval\"";
} else {
$ttrss_specific_qpart = "";
}
@@ -125,15 +126,16 @@ class Opml extends Handler_Protected {
return $out;
}
- function opml_export($name, $owner_uid, $hide_private_feeds = false, $include_settings = true) {
+ function opml_export($filename, $owner_uid, $hide_private_feeds = false, $include_settings = true, $file_output = false) {
if (!$owner_uid) return;
- if (!isset($_REQUEST["debug"])) {
- header("Content-type: application/xml+opml");
- header("Content-Disposition: attachment; filename=" . $name );
- } else {
- header("Content-type: text/xml");
- }
+ if (!$file_output)
+ if (!isset($_REQUEST["debug"])) {
+ header("Content-type: application/xml+opml");
+ header("Content-Disposition: attachment; filename=$filename");
+ } else {
+ header("Content-type: text/xml");
+ }
$out = "<?xml version=\"1.0\" encoding=\"utf-8\"?".">";
@@ -288,7 +290,10 @@ class Opml extends Handler_Protected {
'return str_repeat("\t", intval(strlen($matches[0])/2));'),
$res); */
- print $res;
+ if ($file_output)
+ return file_put_contents($filename, $res) > 0;
+ else
+ print $res;
}
// Import
@@ -323,11 +328,14 @@ class Opml extends Handler_Protected {
$order_id = (int) $attrs->getNamedItem('ttrssSortOrder')->nodeValue;
if (!$order_id) $order_id = 0;
+ $purge_interval = (int) $attrs->getNamedItem('ttrssPurgeInterval')->nodeValue;
+ if (!$purge_interval) $purge_interval = 0;
+
$sth = $this->pdo->prepare("INSERT INTO ttrss_feeds
- (title, feed_url, owner_uid, cat_id, site_url, order_id, update_interval) VALUES
- (?, ?, ?, ?, ?, ?, ?)");
+ (title, feed_url, owner_uid, cat_id, site_url, order_id, update_interval, purge_interval) VALUES
+ (?, ?, ?, ?, ?, ?, ?, ?)");
- $sth->execute([$feed_title, $feed_url, $owner_uid, $cat_id, $site_url, $order_id, $update_interval]);
+ $sth->execute([$feed_title, $feed_url, $owner_uid, $cat_id, $site_url, $order_id, $update_interval, $purge_interval]);
} else {
$this->opml_notice(T_sprintf("Duplicate feed: %s", $feed_title == '[Unknown]' ? $feed_url : $feed_title));
@@ -602,7 +610,7 @@ class Opml extends Handler_Protected {
if (is_file($tmp_file)) {
$doc = new DOMDocument();
libxml_disable_entity_loader(false);
- $doc->load($tmp_file);
+ $loaded = $doc->load($tmp_file);
libxml_disable_entity_loader(true);
unlink($tmp_file);
} else if (!$doc) {
@@ -610,7 +618,7 @@ class Opml extends Handler_Protected {
return;
}
- if ($doc) {
+ if ($loaded) {
$this->pdo->beginTransaction();
$this->opml_import_category($doc, false, $owner_uid, false);
$this->pdo->commit();
diff --git a/classes/pluginhost.php b/classes/pluginhost.php
index 6158880f2..4fec13000 100755
--- a/classes/pluginhost.php
+++ b/classes/pluginhost.php
@@ -1,6 +1,9 @@
<?php
class PluginHost {
private $pdo;
+ /* separate handle for plugin data so transaction while saving wouldn't clash with possible main
+ tt-rss code transactions; only initialized when first needed */
+ private $pdo_data;
private $hooks = array();
private $plugins = array();
private $handlers = array();
@@ -62,6 +65,9 @@ class PluginHost {
const HOOK_ARTICLE_IMAGE = 42;
const HOOK_FEED_TREE = 43;
const HOOK_IFRAME_WHITELISTED = 44;
+ const HOOK_ENCLOSURE_IMPORTED = 45;
+ const HOOK_HEADLINES_CUSTOM_SORT_MAP = 46;
+ const HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE = 47;
const KIND_ALL = 1;
const KIND_SYSTEM = 2;
@@ -73,7 +79,6 @@ class PluginHost {
function __construct() {
$this->pdo = Db::pdo();
-
$this->storage = array();
}
@@ -361,9 +366,13 @@ class PluginHost {
private function save_data($plugin) {
if ($this->owner_uid) {
- $this->pdo->beginTransaction();
- $sth = $this->pdo->prepare("SELECT id FROM ttrss_plugin_storage WHERE
+ if (!$this->pdo_data)
+ $this->pdo_data = Db::instance()->pdo_connect();
+
+ $this->pdo_data->beginTransaction();
+
+ $sth = $this->pdo_data->prepare("SELECT id FROM ttrss_plugin_storage WHERE
owner_uid= ? AND name = ?");
$sth->execute([$this->owner_uid, $plugin]);
@@ -373,18 +382,18 @@ class PluginHost {
$content = serialize($this->storage[$plugin]);
if ($sth->fetch()) {
- $sth = $this->pdo->prepare("UPDATE ttrss_plugin_storage SET content = ?
+ $sth = $this->pdo_data->prepare("UPDATE ttrss_plugin_storage SET content = ?
WHERE owner_uid= ? AND name = ?");
$sth->execute([(string)$content, $this->owner_uid, $plugin]);
} else {
- $sth = $this->pdo->prepare("INSERT INTO ttrss_plugin_storage
+ $sth = $this->pdo_data->prepare("INSERT INTO ttrss_plugin_storage
(name,owner_uid,content) VALUES
(?, ?, ?)");
$sth->execute([$plugin, $this->owner_uid, (string)$content]);
}
- $this->pdo->commit();
+ $this->pdo_data->commit();
}
}
diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php
index 6d7295beb..9d29ab478 100755
--- a/classes/pref/feeds.php
+++ b/classes/pref/feeds.php
@@ -449,7 +449,7 @@ class Pref_Feeds extends Handler_Protected {
if ($row = $sth->fetch()) {
@unlink(ICONS_DIR . "/$feed_id.ico");
- $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET favicon_avg_color = NULL
+ $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET favicon_avg_color = NULL, favicon_last_checked = '1970-01-01'
where id = ?");
$sth->execute([$feed_id]);
}
@@ -554,7 +554,7 @@ class Pref_Feeds extends Handler_Protected {
$last_error = $row["last_error"];
if ($last_error) {
- print "&nbsp;<i class=\"material-icons\"
+ print "&nbsp;<i class=\"material-icons\"
title=\"".htmlspecialchars($last_error)."\">error</i>";
}
@@ -676,7 +676,7 @@ class Pref_Feeds extends Handler_Protected {
$auth_checked = $auth_enabled ? 'checked' : '';
print "<label class='checkbox'>
<input type='checkbox' $auth_checked name='need_auth' dojoType='dijit.form.CheckBox' id='feedEditDlg_loginCheck'
- onclick='displayIfChecked(this, \"feedEditDlg_loginContainer\")'>
+ onclick='App.displayIfChecked(this, \"feedEditDlg_loginContainer\")'>
".__('This feed requires authentication.')."</label>";
print '</div><div dojoType="dijit.layout.ContentPane" title="'.__('Options').'">';
@@ -1172,7 +1172,7 @@ class Pref_Feeds extends Handler_Protected {
function index() {
print "<div dojoType='dijit.layout.AccordionContainer' region='center'>";
- print "<div style='padding : 0px' dojoType='dijit.layout.AccordionPane'
+ print "<div style='padding : 0px' dojoType='dijit.layout.AccordionPane'
title=\"<i class='material-icons'>rss_feed</i> ".__('Feeds')."\">";
$sth = $this->pdo->prepare("SELECT COUNT(id) AS num_errors
@@ -1307,7 +1307,7 @@ class Pref_Feeds extends Handler_Protected {
print "</div>"; # feeds pane
- print "<div dojoType='dijit.layout.AccordionPane'
+ print "<div dojoType='dijit.layout.AccordionPane'
title='<i class=\"material-icons\">import_export</i> ".__('OPML')."'>";
print "<h3>" . __("Using OPML you can export and import your feeds, filters, labels and Tiny Tiny RSS settings.") . "</h3>";
@@ -1360,7 +1360,7 @@ class Pref_Feeds extends Handler_Protected {
print "</div>"; # pane
- print "<div dojoType=\"dijit.layout.AccordionPane\"
+ print "<div dojoType=\"dijit.layout.AccordionPane\"
title=\"<i class='material-icons'>share</i> ".__('Published & shared articles / Generated feeds')."\">";
print "<h3>" . __('Published articles can be subscribed by anyone who knows the following URL:') . "</h3>";
@@ -1637,6 +1637,8 @@ class Pref_Feeds extends Handler_Protected {
}
function batchSubscribe() {
+ print "<form onsubmit='return false'>";
+
print_hidden("op", "pref-feeds");
print_hidden("method", "batchaddfeeds");
@@ -1645,7 +1647,7 @@ class Pref_Feeds extends Handler_Protected {
print "<textarea
style='font-size : 12px; width : 98%; height: 200px;'
- dojoType='dijit.form.SimpleTextarea' name='feeds'></textarea>";
+ dojoType='fox.form.ValidationTextArea' required='1' name='feeds'></textarea>";
if (get_pref('ENABLE_FEED_CATS')) {
print "<fieldset>";
@@ -1670,14 +1672,17 @@ class Pref_Feeds extends Handler_Protected {
print "<fieldset class='narrow'>
<label class='checkbox'><input type='checkbox' name='need_auth' dojoType='dijit.form.CheckBox'
- onclick='displayIfChecked(this, \"feedDlg_loginContainer\")'> ".
+ onclick='App.displayIfChecked(this, \"feedDlg_loginContainer\")'> ".
__('Feeds require authentication.')."</label></div>";
print "</fieldset>";
print "<footer>
- <button dojoType='dijit.form.Button' type='submit' class='alt-primary'>".__('Subscribe')."</button>
+ <button dojoType='dijit.form.Button' onclick=\"return dijit.byId('batchSubDlg').execute()\" type='submit' class='alt-primary'>".
+ __('Subscribe')."</button>
<button dojoType='dijit.form.Button' onclick=\"return dijit.byId('batchSubDlg').hide()\">".__('Cancel')."</button>
</footer>";
+
+ print "</form>";
}
function batchAddFeeds() {
diff --git a/classes/pref/filters.php b/classes/pref/filters.php
index a3a0ce77f..6121e4c14 100755
--- a/classes/pref/filters.php
+++ b/classes/pref/filters.php
@@ -3,7 +3,7 @@ class Pref_Filters extends Handler_Protected {
function csrf_ignore($method) {
$csrf_ignored = array("index", "getfiltertree", "edit", "newfilter", "newrule",
- "newaction", "savefilterorder");
+ "newaction", "savefilterorder", "testfilterdlg");
return array_search($method, $csrf_ignored) !== false;
}
@@ -159,22 +159,19 @@ class Pref_Filters extends Handler_Protected {
print json_encode($rv);
}
- function testFilter() {
+ function testFilterDlg() {
+ ?>
+ <div>
+ <img id='prefFilterLoadingIndicator' src='images/indicator_tiny.gif'>&nbsp;
+ <span id='prefFilterProgressMsg'>Looking for articles...</span>
+ </div>
- if (isset($_REQUEST["offset"])) return $this->testFilterDo();
-
- //print __("Articles matching this filter:");
-
- print "<div><img id='prefFilterLoadingIndicator' src='images/indicator_tiny.gif'>&nbsp;<span id='prefFilterProgressMsg'>Looking for articles...</span></div>";
-
- print "<ul class='panel panel-scrollable list list-unstyled' id='prefFilterTestResultList'>";
- print "</ul>";
-
- print "<footer class='text-center'>";
- print "<button dojoType='dijit.form.Button' onclick=\"dijit.byId('filterTestDlg').hide()\">".
- __('Close this window')."</button>";
- print "</footer>";
+ <ul class='panel panel-scrollable list list-unstyled' id='prefFilterTestResultList'></ul>
+ <footer class='text-center'>
+ <button dojoType='dijit.form.Button' onclick="dijit.byId('filterTestDlg').hide()"><?php echo __('Close this window') ?></button>
+ </footer>
+ <?php
}
private function getfilterrules_list($filter_id) {
@@ -241,6 +238,7 @@ class Pref_Filters extends Handler_Protected {
$root = array();
$root['id'] = 'root';
$root['name'] = __('Filters');
+ $root['enabled'] = true;
$root['items'] = array();
$filter_search = $_SESSION["prefs_filter_search"];
@@ -305,7 +303,7 @@ class Pref_Filters extends Handler_Protected {
$filter['param'] = $name[1];
$filter['checkbox'] = false;
$filter['last_triggered'] = $line["last_triggered"] ? make_local_datetime($line["last_triggered"], false) : null;
- $filter['enabled'] = $line["enabled"];
+ $filter['enabled'] = sql_bool_to_bool($line["enabled"]);
$filter['rules'] = $this->getfilterrules_list($line['id']);
if (!$filter_search || $match_ok) {
@@ -600,10 +598,6 @@ class Pref_Filters extends Handler_Protected {
}
function editSave() {
- if (clean($_REQUEST["savemode"] && $_REQUEST["savemode"]) == "test") {
- return $this->testFilter();
- }
-
$filter_id = clean($_REQUEST["id"]);
$enabled = checkbox_to_sql_bool(clean($_REQUEST["enabled"]));
$match_any_rule = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"]));
@@ -714,10 +708,6 @@ class Pref_Filters extends Handler_Protected {
}
function add() {
- if (clean($_REQUEST["savemode"] && $_REQUEST["savemode"]) == "test") {
- return $this->testFilter();
- }
-
$enabled = checkbox_to_sql_bool(clean($_REQUEST["enabled"]));
$match_any_rule = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"]));
$title = clean($_REQUEST["title"]);
@@ -975,19 +965,18 @@ class Pref_Filters extends Handler_Protected {
print "<section>";
- print "<input dojoType=\"dijit.form.ValidationTextBox\"
- required=\"true\" id=\"filterDlg_regExp\"
- onchange='Filters.filterDlgCheckRegExp(this)'
- onblur='Filters.filterDlgCheckRegExp(this)'
- onfocus='Filters.filterDlgCheckRegExp(this)'
- style=\"font-size : 16px; width : 500px\"
- name=\"reg_exp\" value=\"$reg_exp\"/>";
+ print "<textarea dojoType='fox.form.ValidationTextArea'
+ required='true' id='filterDlg_regExp'
+ ValidRegExp='true'
+ rows='4'
+ style='font-size : 14px; width : 490px; word-break: break-all'
+ name='reg_exp'>$reg_exp</textarea>";
print "<div dojoType='dijit.Tooltip' id='filterDlg_regExp_tip' connectId='filterDlg_regExp' position='below'></div>";
print "<fieldset>";
- print "<label class='checkbox'><input id=\"filterDlg_inverse\" dojoType=\"dijit.form.CheckBox\"
- name=\"inverse\" $inverse_checked/> ".
+ print "<label class='checkbox'><input id='filterDlg_inverse' dojoType='dijit.form.CheckBox'
+ name='inverse' $inverse_checked/> ".
__("Inverse regular expression matching")."</label>";
print "</fieldset>";
diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php
index ac16b5971..475cd797f 100644
--- a/classes/pref/prefs.php
+++ b/classes/pref/prefs.php
@@ -213,11 +213,9 @@ class Pref_Prefs extends Handler_Protected {
if ($old_email != $email) {
$mailer = new Mailer();
- require_once "lib/MiniTemplator.class.php";
+ $tpl = new Templator();
- $tpl = new MiniTemplator;
-
- $tpl->readTemplateFromFile("templates/mail_change_template.txt");
+ $tpl->readTemplateFromFile("mail_change_template.txt");
$tpl->setVariable('LOGIN', $row["login"]);
$tpl->setVariable('NEWMAIL', $email);
@@ -1087,11 +1085,9 @@ class Pref_Prefs extends Handler_Protected {
if ($row = $sth->fetch()) {
$mailer = new Mailer();
- require_once "lib/MiniTemplator.class.php";
-
- $tpl = new MiniTemplator;
+ $tpl = new Templator();
- $tpl->readTemplateFromFile("templates/otp_disabled_template.txt");
+ $tpl->readTemplateFromFile("otp_disabled_template.txt");
$tpl->setVariable('LOGIN', $row["login"]);
$tpl->setVariable('TTRSS_HOST', SELF_URL_PATH);
diff --git a/classes/rssutils.php b/classes/rssutils.php
index 831ac1baf..8a554946c 100755
--- a/classes/rssutils.php
+++ b/classes/rssutils.php
@@ -3,7 +3,12 @@ class RSSUtils {
static function calculate_article_hash($article, $pluginhost) {
$tmp = "";
+ $ignored_fields = [ "feed", "guid", "guid_hashed", "owner_uid", "force_catchup" ];
+
foreach ($article as $k => $v) {
+ if (in_array($k, $ignored_fields))
+ continue;
+
if ($k != "feed" && isset($v)) {
$x = strip_tags(is_array($v) ? implode(",", $v) : $v);
@@ -469,7 +474,7 @@ class RSSUtils {
foreach ($pluginhost->get_hooks(PluginHost::HOOK_FEED_PARSED) as $plugin) {
Debug::log("... " . get_class($plugin), Debug::$LOG_VERBOSE);
$start = microtime(true);
- $plugin->hook_feed_parsed($rss);
+ $plugin->hook_feed_parsed($rss, $feed);
Debug::log(sprintf("=== %.4f (sec)", microtime(true) - $start), Debug::$LOG_VERBOSE);
}
@@ -586,11 +591,11 @@ class RSSUtils {
continue;
}
+ $entry_guid_hashed_compat = 'SHA1:' . sha1("$owner_uid,$entry_guid");
+ $entry_guid_hashed = json_encode(["ver" => 2, "uid" => $owner_uid, "hash" => 'SHA1:' . sha1($entry_guid)]);
$entry_guid = "$owner_uid,$entry_guid";
- $entry_guid_hashed = 'SHA1:' . sha1($entry_guid);
-
- Debug::log("guid $entry_guid / $entry_guid_hashed", Debug::$LOG_VERBOSE);
+ Debug::log("guid $entry_guid (hash: $entry_guid_hashed compat: $entry_guid_hashed_compat)", Debug::$LOG_VERBOSE);
$entry_timestamp = (int)$item->get_date();
@@ -632,8 +637,8 @@ class RSSUtils {
Debug::log("done collecting data.", Debug::$LOG_VERBOSE);
$sth = $pdo->prepare("SELECT id, content_hash, lang FROM ttrss_entries
- WHERE guid = ? OR guid = ?");
- $sth->execute([$entry_guid, $entry_guid_hashed]);
+ WHERE guid IN (?, ?, ?)");
+ $sth->execute([$entry_guid, $entry_guid_hashed, $entry_guid_hashed_compat]);
if ($row = $sth->fetch()) {
$base_entry_id = $row["id"];
@@ -648,6 +653,38 @@ class RSSUtils {
$article_labels = array();
}
+ Debug::log("looking for enclosures...", Debug::$LOG_VERBOSE);
+
+ // enclosures
+
+ $enclosures = array();
+
+ $encs = $item->get_enclosures();
+
+ if (is_array($encs)) {
+ foreach ($encs as $e) {
+
+ foreach ($pluginhost->get_hooks(PluginHost::HOOK_ENCLOSURE_IMPORTED) as $plugin) {
+ $e = $plugin->hook_enclosure_imported($e, $feed);
+ }
+
+ $e_item = array(
+ rewrite_relative_url($site_url, $e->link),
+ $e->type, $e->length, $e->title, $e->width, $e->height);
+
+ // Yet another episode of "mysql utf8_general_ci is gimped"
+ if (DB_TYPE == "mysql" && MYSQL_CHARSET != "UTF8MB4") {
+ for ($i = 0; $i < count($e_item); $i++) {
+ if (is_string($e_item[$i])) {
+ $e_item[$i] = RSSUtils::strip_utf8mb4($e_item[$i]);
+ }
+ }
+ }
+
+ array_push($enclosures, $e_item);
+ }
+ }
+
$article = array("owner_uid" => $owner_uid, // read only
"guid" => $entry_guid, // read only
"guid_hashed" => $entry_guid_hashed, // read only
@@ -662,6 +699,7 @@ class RSSUtils {
"language" => $entry_language,
"timestamp" => $entry_timestamp,
"num_comments" => $num_comments,
+ "enclosures" => $enclosures,
"feed" => array("id" => $feed,
"fetch_url" => $fetch_url,
"site_url" => $site_url,
@@ -804,6 +842,7 @@ class RSSUtils {
$entry_language = $article["language"];
$entry_timestamp = $article["timestamp"];
$num_comments = $article["num_comments"];
+ $enclosures = $article["enclosures"];
if ($entry_timestamp == -1 || !$entry_timestamp || $entry_timestamp > time()) {
$entry_timestamp = time();
@@ -828,8 +867,8 @@ class RSSUtils {
RSSUtils::cache_media($entry_content, $site_url);
$csth = $pdo->prepare("SELECT id FROM ttrss_entries
- WHERE guid = ? OR guid = ?");
- $csth->execute([$entry_guid, $entry_guid_hashed]);
+ WHERE guid IN (?, ?, ?)");
+ $csth->execute([$entry_guid, $entry_guid_hashed, $entry_guid_hashed_compat]);
if (!$row = $csth->fetch()) {
@@ -874,7 +913,7 @@ class RSSUtils {
}
- $csth->execute([$entry_guid, $entry_guid_hashed]);
+ $csth->execute([$entry_guid, $entry_guid_hashed, $entry_guid_hashed_compat]);
$entry_ref_id = 0;
$entry_int_id = 0;
@@ -1022,33 +1061,6 @@ class RSSUtils {
RSSUtils::assign_article_to_label_filters($entry_ref_id, $article_filters,
$owner_uid, $article_labels);
- Debug::log("looking for enclosures...", Debug::$LOG_VERBOSE);
-
- // enclosures
-
- $enclosures = array();
-
- $encs = $item->get_enclosures();
-
- if (is_array($encs)) {
- foreach ($encs as $e) {
- $e_item = array(
- rewrite_relative_url($site_url, $e->link),
- $e->type, $e->length, $e->title, $e->width, $e->height);
-
- // Yet another episode of "mysql utf8_general_ci is gimped"
- if (DB_TYPE == "mysql" && MYSQL_CHARSET != "UTF8MB4") {
- for ($i = 0; $i < count($e_item); $i++) {
- if (is_string($e_item[$i])) {
- $e_item[$i] = RSSUtils::strip_utf8mb4($e_item[$i]);
- }
- }
- }
-
- array_push($enclosures, $e_item);
- }
- }
-
if ($cache_images)
RSSUtils::cache_enclosures($enclosures, $site_url);
@@ -1186,6 +1198,7 @@ class RSSUtils {
return true;
}
+ /* TODO: move to DiskCache? */
static function cache_enclosures($enclosures, $site_url) {
$cache = new DiskCache("images");
@@ -1221,6 +1234,34 @@ class RSSUtils {
}
}
+ /* TODO: move to DiskCache? */
+ static function cache_media_url($cache, $url, $site_url) {
+ $url = rewrite_relative_url($site_url, $url);
+ $local_filename = sha1($url);
+
+ Debug::log("cache_media: checking $url", Debug::$LOG_VERBOSE);
+
+ if (!$cache->exists($local_filename)) {
+ Debug::log("cache_media: downloading: $url to $local_filename", Debug::$LOG_VERBOSE);
+
+ global $fetch_last_error_code;
+ global $fetch_last_error;
+
+ $file_content = fetch_file_contents(array("url" => $url,
+ "http_referrer" => $url,
+ "max_size" => MAX_CACHE_FILE_SIZE));
+
+ if ($file_content) {
+ $cache->put($local_filename, $file_content);
+ } else {
+ Debug::log("cache_media: failed with $fetch_last_error_code: $fetch_last_error");
+ }
+ } else if ($cache->isWritable($local_filename)) {
+ $cache->touch($local_filename);
+ }
+ }
+
+ /* TODO: move to DiskCache? */
static function cache_media($html, $site_url) {
$cache = new DiskCache("images");
@@ -1229,33 +1270,20 @@ class RSSUtils {
if ($doc->loadHTML($html)) {
$xpath = new DOMXPath($doc);
- $entries = $xpath->query('(//img[@src])|(//video/source[@src])|(//audio/source[@src])');
+ $entries = $xpath->query('(//img[@src]|//source[@src|@srcset]|//video[@poster|@src])');
foreach ($entries as $entry) {
- if ($entry->hasAttribute('src') && strpos($entry->getAttribute('src'), "data:") !== 0) {
- $src = rewrite_relative_url($site_url, $entry->getAttribute('src'));
-
- $local_filename = sha1($src);
-
- Debug::log("cache_media: checking $src", Debug::$LOG_VERBOSE);
-
- if (!$cache->exists($local_filename)) {
- Debug::log("cache_media: downloading: $src to $local_filename", Debug::$LOG_VERBOSE);
-
- global $fetch_last_error_code;
- global $fetch_last_error;
+ foreach (array('src', 'poster') as $attr) {
+ if ($entry->hasAttribute($attr) && strpos($entry->getAttribute($attr), "data:") !== 0) {
+ RSSUtils::cache_media_url($cache, $entry->getAttribute($attr), $site_url);
+ }
+ }
- $file_content = fetch_file_contents(array("url" => $src,
- "http_referrer" => $src,
- "max_size" => MAX_CACHE_FILE_SIZE));
+ if ($entry->hasAttribute("srcset")) {
+ $matches = RSSUtils::decode_srcset($entry->getAttribute('srcset'));
- if ($file_content) {
- $cache->put($local_filename, $file_content);
- } else {
- Debug::log("cache_media: failed with $fetch_last_error_code: $fetch_last_error");
- }
- } else if ($cache->isWritable($local_filename)) {
- $cache->touch($local_filename);
+ for ($i = 0; $i < count($matches); $i++) {
+ RSSUtils::cache_media_url($cache, $matches[$i]["url"], $site_url);
}
}
}
@@ -1343,6 +1371,7 @@ class RSSUtils {
foreach ($filter["rules"] as $rule) {
$match = false;
$reg_exp = str_replace('/', '\/', $rule["reg_exp"]);
+ $reg_exp = str_replace("\n", "", $reg_exp); // reg_exp may be formatted with CRs now because of textarea, we need to strip those
$rule_inverse = $rule["inverse"];
if (!$reg_exp)
@@ -1710,4 +1739,32 @@ class RSSUtils {
return $favicon_url;
}
+ // https://community.tt-rss.org/t/problem-with-img-srcset/3519
+ static function decode_srcset($srcset) {
+ $matches = [];
+
+ preg_match_all(
+ '/(?:\A|,)\s*(?P<url>(?!,)\S+(?<!,))\s*(?P<size>\s\d+w|\s\d+(?:\.\d+)?(?:[eE][+-]?\d+)?x|)\s*(?=,|\Z)/',
+ $srcset, $matches, PREG_SET_ORDER
+ );
+
+ foreach ($matches as $m) {
+ array_push($matches, [
+ "url" => trim($m["url"]),
+ "size" => trim($m["size"])
+ ]);
+ }
+
+ return $matches;
+ }
+
+ static function encode_srcset($matches) {
+ $tokens = [];
+
+ foreach ($matches as $m) {
+ array_push($tokens, trim($m["url"]) . " " . trim($m["size"]));
+ }
+
+ return implode(",", $tokens);
+ }
}
diff --git a/classes/templator.php b/classes/templator.php
new file mode 100644
index 000000000..3d270f837
--- /dev/null
+++ b/classes/templator.php
@@ -0,0 +1,21 @@
+<?php
+require_once "lib/MiniTemplator.class.php";
+
+class Templator extends MiniTemplator {
+
+ /* this reads tt-rss template from templates.local/ or templates/ if only base filename is given */
+ function readTemplateFromFile ($fileName) {
+ if (strpos($fileName, "/") === FALSE) {
+
+ $fileName = basename($fileName);
+
+ if (file_exists("templates.local/$fileName"))
+ return parent::readTemplateFromFile("templates.local/$fileName");
+ else
+ return parent::readTemplateFromFile("templates/$fileName");
+
+ } else {
+ return parent::readTemplateFromFile($fileName);
+ }
+ }
+}