diff options
Diffstat (limited to 'classes')
-rwxr-xr-x | classes/api.php | 24 | ||||
-rwxr-xr-x | classes/article.php | 8 | ||||
-rw-r--r-- | classes/counters.php | 4 | ||||
-rw-r--r-- | classes/digest.php | 10 | ||||
-rw-r--r-- | classes/diskcache.php | 243 | ||||
-rwxr-xr-x | classes/feeds.php | 77 | ||||
-rwxr-xr-x | classes/handler/public.php | 42 | ||||
-rw-r--r-- | classes/labels.php | 6 | ||||
-rw-r--r-- | classes/opml.php | 42 | ||||
-rwxr-xr-x | classes/pluginhost.php | 21 | ||||
-rwxr-xr-x | classes/pref/feeds.php | 23 | ||||
-rwxr-xr-x | classes/pref/filters.php | 55 | ||||
-rw-r--r-- | classes/pref/prefs.php | 12 | ||||
-rwxr-xr-x | classes/rssutils.php | 175 | ||||
-rw-r--r-- | classes/templator.php | 21 |
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 " <i class=\"material-icons\" + print " <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'> + <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'> <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); + } + } +} |