summaryrefslogtreecommitdiff
path: root/classes
diff options
context:
space:
mode:
Diffstat (limited to 'classes')
-rwxr-xr-xclasses/api.php34
-rwxr-xr-xclasses/article.php20
-rw-r--r--classes/backend.php86
-rw-r--r--classes/counters.php6
-rwxr-xr-xclasses/db.php9
-rw-r--r--classes/digest.php10
-rw-r--r--classes/diskcache.php305
-rwxr-xr-xclasses/feeds.php151
-rw-r--r--classes/handler.php4
-rwxr-xr-xclasses/handler/public.php98
-rw-r--r--classes/labels.php6
-rw-r--r--classes/opml.php42
-rwxr-xr-xclasses/pluginhost.php27
-rwxr-xr-xclasses/pref/feeds.php27
-rwxr-xr-xclasses/pref/filters.php55
-rw-r--r--classes/pref/prefs.php122
-rw-r--r--classes/pref/users.php23
-rwxr-xr-xclasses/rpc.php314
-rwxr-xr-xclasses/rssutils.php199
-rw-r--r--classes/sanitizer.php217
-rw-r--r--classes/templator.php21
-rw-r--r--classes/urlhelper.php474
-rw-r--r--classes/userhelper.php141
23 files changed, 1934 insertions, 457 deletions
diff --git a/classes/api.php b/classes/api.php
index 339e9eef1..ce046e66a 100755
--- a/classes/api.php
+++ b/classes/api.php
@@ -74,10 +74,10 @@ class API extends Handler {
}
if (get_pref("ENABLE_API_ACCESS", $uid)) {
- if (authenticate_user($login, $password, false, Auth_Base::AUTH_SERVICE_API)) { // try login with normal password
+ if (UserHelper::authenticate($login, $password, false, Auth_Base::AUTH_SERVICE_API)) { // try login with normal password
$this->wrap(self::STATUS_OK, array("session_id" => session_id(),
"api_level" => self::API_LEVEL));
- } else if (authenticate_user($login, $password_base64, false, Auth_Base::AUTH_SERVICE_API)) { // else try with base64_decoded password
+ } else if (UserHelper::authenticate($login, $password_base64, false, Auth_Base::AUTH_SERVICE_API)) { // else try with base64_decoded password
$this->wrap(self::STATUS_OK, array("session_id" => session_id(),
"api_level" => self::API_LEVEL));
} else { // else we are not logged in
@@ -91,7 +91,7 @@ class API extends Handler {
}
function logout() {
- logout_user();
+ Pref_Users::logout_user();
$this->wrap(self::STATUS_OK, array("status" => "OK"));
}
@@ -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 */
@@ -357,7 +343,7 @@ class API extends Handler {
);
if ($sanitize_content) {
- $article["content"] = sanitize(
+ $article["content"] = Sanitizer::sanitize(
$line["content"],
API::param_to_bool($line['hide_images']),
false, $line["site_url"], false, $line["id"]);
@@ -762,7 +748,7 @@ class API extends Handler {
if ($show_content) {
if ($sanitize_content) {
- $headline_row["content"] = sanitize(
+ $headline_row["content"] = Sanitizer::sanitize(
$line["content"],
API::param_to_bool($line['hide_images']),
false, $line["site_url"], false, $line["id"]);
diff --git a/classes/article.php b/classes/article.php
index 74dbdae53..c94b69615 100755
--- a/classes/article.php
+++ b/classes/article.php
@@ -1,12 +1,6 @@
<?php
class Article extends Handler_Protected {
- function csrf_ignore($method) {
- $csrf_ignored = array("redirect", "editarticletags");
-
- return array_search($method, $csrf_ignored) !== false;
- }
-
function redirect() {
$id = clean($_REQUEST['id']);
@@ -60,7 +54,7 @@ class Article extends Handler_Protected {
if (!$title) $title = $url;
if (!$title && !$url) return false;
- if (filter_var($url, FILTER_VALIDATE_URL) === FALSE) return false;
+ if (filter_var($url, FILTER_VALIDATE_URL) === false) return false;
$pdo = Db::pdo();
@@ -94,7 +88,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 +387,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 +467,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 +577,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;
@@ -658,7 +652,7 @@ class Article extends Handler_Protected {
}
static function getLastArticleId() {
- $pdo = DB::pdo();
+ $pdo = Db::pdo();
$sth = $pdo->prepare("SELECT ref_id AS id FROM ttrss_user_entries
WHERE owner_uid = ? ORDER BY ref_id DESC LIMIT 1");
@@ -763,7 +757,7 @@ class Article extends Handler_Protected {
if (!$article_image)
foreach ($enclosures as $enc) {
- if (strpos($enc["content_type"], "image/") !== FALSE) {
+ if (strpos($enc["content_type"], "image/") !== false) {
$article_image = $enc["content_url"];
break;
}
diff --git a/classes/backend.php b/classes/backend.php
index 122e28c65..16c20660a 100644
--- a/classes/backend.php
+++ b/classes/backend.php
@@ -1,12 +1,6 @@
<?php
-class Backend extends Handler {
- function loading() {
- header("Content-type: text/html");
- print __("Loading, please wait...") . " " .
- "<img src='images/indicator_tiny.gif'>";
- }
-
- function digestTest() {
+class Backend extends Handler_Protected {
+ /* function digestTest() {
if (isset($_SESSION['uid'])) {
header("Content-type: text/html");
@@ -19,80 +13,14 @@ class Backend extends Handler {
} else {
print error_json(6);
}
- }
-
- private function display_main_help() {
- $info = get_hotkeys_info();
- $imap = get_hotkeys_map();
- $omap = array();
-
- foreach ($imap[1] as $sequence => $action) {
- if (!isset($omap[$action])) $omap[$action] = array();
-
- array_push($omap[$action], $sequence);
- }
-
- print "<ul class='panel panel-scrollable hotkeys-help' style='height : 300px'>";
-
- print "<h2>" . __("Keyboard Shortcuts") . "</h2>";
-
- foreach ($info as $section => $hotkeys) {
-
- print "<li><hr></li>";
- print "<li><h3>" . $section . "</h3></li>";
-
- foreach ($hotkeys as $action => $description) {
-
- if (is_array($omap[$action])) {
- foreach ($omap[$action] as $sequence) {
- if (strpos($sequence, "|") !== FALSE) {
- $sequence = substr($sequence,
- strpos($sequence, "|")+1,
- strlen($sequence));
- } else {
- $keys = explode(" ", $sequence);
-
- for ($i = 0; $i < count($keys); $i++) {
- if (strlen($keys[$i]) > 1) {
- $tmp = '';
- foreach (str_split($keys[$i]) as $c) {
- switch ($c) {
- case '*':
- $tmp .= __('Shift') . '+';
- break;
- case '^':
- $tmp .= __('Ctrl') . '+';
- break;
- default:
- $tmp .= $c;
- }
- }
- $keys[$i] = $tmp;
- }
- }
- $sequence = join(" ", $keys);
- }
-
- print "<li>";
- print "<div class='hk'><code>$sequence</code></div>";
- print "<div class='desc'>$description</div>";
- print "</li>";
- }
- }
- }
- }
-
- print "</ul>";
-
-
- }
+ } */
function help() {
- $topic = clean_filename($_REQUEST["topic"]); // only one for now
+ $topic = basename(clean($_REQUEST["topic"])); // only one for now
if ($topic == "main") {
- $info = get_hotkeys_info();
- $imap = get_hotkeys_map();
+ $info = RPC::get_hotkeys_info();
+ $imap = RPC::get_hotkeys_map();
$omap = array();
foreach ($imap[1] as $sequence => $action) {
@@ -114,7 +42,7 @@ class Backend extends Handler {
if (is_array($omap[$action])) {
foreach ($omap[$action] as $sequence) {
- if (strpos($sequence, "|") !== FALSE) {
+ if (strpos($sequence, "|") !== false) {
$sequence = substr($sequence,
strpos($sequence, "|")+1,
strlen($sequence));
diff --git a/classes/counters.php b/classes/counters.php
index d8ed27621..4518fa9d2 100644
--- a/classes/counters.php
+++ b/classes/counters.php
@@ -42,7 +42,7 @@ class Counters {
array_push($ret, $cv);
- $pdo = DB::pdo();
+ $pdo = Db::pdo();
$sth = $pdo->prepare("SELECT fc.id,
SUM(CASE WHEN unread THEN 1 ELSE 0 END) AS count,
@@ -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/db.php b/classes/db.php
index ac493f6d6..70b97d0ee 100755
--- a/classes/db.php
+++ b/classes/db.php
@@ -113,4 +113,13 @@ class Db
return self::$instance->pdo;
}
+
+ public static function sql_random_function() {
+ if (DB_TYPE == "mysql") {
+ return "RAND()";
+ } else {
+ return "RANDOM()";
+ }
+ }
+
}
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..24e45ea8b 100644
--- a/classes/diskcache.php
+++ b/classes/diskcache.php
@@ -2,8 +2,196 @@
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);
+ $this->dir = CACHE_DIR . "/" . basename(clean($dir));
}
public function getDir() {
@@ -39,9 +227,7 @@ class DiskCache {
}
public function getFullPath($filename) {
- $filename = clean_filename($filename);
-
- return $this->dir . "/" . $filename;
+ return $this->dir . "/" . basename(clean($filename));
}
public function put($filename, $data) {
@@ -66,19 +252,34 @@ 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));
+ return $this->send_local_file($this->getFullPath($filename));
}
public function getUrl($filename) {
- return get_self_url_prefix() . "/public.php?op=cached_url&file=" . basename($this->dir) . "/" . $filename;
+ return get_self_url_prefix() . "/public.php?op=cached_url&file=" . basename($this->dir) . "/" . basename($filename);
}
// 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 +290,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 ($entry->hasAttribute('src') || $entry->hasAttribute('poster')) {
+ if ($cache->exists($cached_filename)) {
+ $url = $cache->getUrl($cached_filename);
- // should be already absolutized because this is called after sanitize()
- $src = $entry->hasAttribute('poster') ? $entry->getAttribute('poster') : $entry->getAttribute('src');
- $cached_filename = sha1($src);
+ $entry->setAttribute($attr, $url);
+ $entry->removeAttribute("srcset");
- if ($cache->exists($cached_filename)) {
+ $need_saving = true;
+ }
+ }
+ }
- $src = $cache->getUrl(sha1($src));
+ if ($entry->hasAttribute("srcset")) {
+ $matches = RSSUtils::decode_srcset($entry->getAttribute('srcset'));
- if ($entry->hasAttribute('poster'))
- $entry->setAttribute('poster', $src);
- else {
- $entry->setAttribute('src', $src);
- $entry->removeAttribute("srcset");
- }
+ for ($i = 0; $i < count($matches); $i++) {
+ $cached_filename = sha1($matches[$i]["url"]);
+
+ if ($cache->exists($cached_filename)) {
+ $matches[$i]["url"] = $cache->getUrl($cached_filename);
- $need_saving = true;
+ $need_saving = true;
+ }
}
+
+ $entry->setAttribute("srcset", RSSUtils::encode_srcset($matches));
}
}
@@ -148,4 +359,56 @@ class DiskCache {
}
}
}
+
+ /* this is essentially a wrapper for readfile() which allows plugins to hook
+ output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
+
+ hook function should return true if request was handled (or at least attempted to)
+
+ note that this can be called without user context so the plugin to handle this
+ should be loaded systemwide in config.php */
+ function send_local_file($filename) {
+ if (file_exists($filename)) {
+
+ if (is_writable($filename)) touch($filename);
+
+ $mimetype = mime_content_type($filename);
+
+ // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
+ // video files are detected as octet-stream by mime_content_type()
+
+ if ($mimetype == "application/octet-stream")
+ $mimetype = "video/mp4";
+
+ # block SVG because of possible embedded javascript (.....)
+ $mimetype_blacklist = [ "image/svg+xml" ];
+
+ /* only serve video and images */
+ if (!preg_match("/(image|video)\//", $mimetype) || in_array($mimetype, $mimetype_blacklist)) {
+ http_response_code(400);
+ header("Content-type: text/plain");
+
+ print "Stored file has disallowed content type ($mimetype)";
+ return false;
+ }
+
+ $tmppluginhost = new PluginHost();
+
+ $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
+ $tmppluginhost->load_data();
+
+ foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
+ if ($plugin->hook_send_local_file($filename)) return true;
+ }
+
+ header("Content-type: $mimetype");
+
+ $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
+ header("Last-Modified: $stamp", true);
+
+ return readfile($filename);
+ } else {
+ return false;
+ }
+ }
}
diff --git a/classes/feeds.php b/classes/feeds.php
index 77add790e..b428252d8 100755
--- a/classes/feeds.php
+++ b/classes/feeds.php
@@ -8,7 +8,7 @@ class Feeds extends Handler_Protected {
private $params;
function csrf_ignore($method) {
- $csrf_ignored = array("index", "quickaddfeed", "search");
+ $csrf_ignored = array("index");
return array_search($method, $csrf_ignored) !== false;
}
@@ -210,7 +210,7 @@ class Feeds extends Handler_Protected {
$feed_title = $qfh_ret[1];
$feed_site_url = $qfh_ret[2];
$last_error = $qfh_ret[3];
- $last_updated = strpos($qfh_ret[4], '1970-') === FALSE ?
+ $last_updated = strpos($qfh_ret[4], '1970-') === false ?
make_local_datetime($qfh_ret[4], false) : __("Never");
$highlight_words = $qfh_ret[5];
$reply['first_id'] = $qfh_ret[6];
@@ -305,7 +305,7 @@ class Feeds extends Handler_Protected {
$line["buttons"] .= $p->hook_article_button($line);
}
- $line["content"] = sanitize($line["content"],
+ $line["content"] = Sanitizer::sanitize($line["content"],
$line['hide_images'], false, $line["site_url"], $highlight_words, $line["id"]);
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_RENDER_ARTICLE_CDM) as $p) {
@@ -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,
@@ -564,7 +550,7 @@ class Feeds extends Handler_Protected {
"disable_cache" => (bool) $disable_cache];
// this is parsed by handleRpcJson() on first viewfeed() to set cdm expanded, etc
- $reply['runtime-info'] = make_runtime_info();
+ $reply['runtime-info'] = RPC::make_runtime_info();
$reply_json = json_encode($reply);
@@ -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>";
@@ -765,7 +751,7 @@ class Feeds extends Handler_Protected {
$feed_id = (int)$_REQUEST["feed_id"];
@$do_update = $_REQUEST["action"] == "do_update";
- $csrf_token = $_REQUEST["csrf_token"];
+ $csrf_token = $_POST["csrf_token"];
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ? AND owner_uid = ?");
$sth->execute([$feed_id, $_SESSION['uid']]);
@@ -813,7 +799,7 @@ class Feeds extends Handler_Protected {
<div class="container">
<h1>Feed Debugger: <?php echo "$feed_id: " . $this->getFeedTitle($feed_id) ?></h1>
<div class="content">
- <form method="GET" action="">
+ <form method="post" action="">
<input type="hidden" name="op" value="feeds">
<input type="hidden" name="method" value="update_debugger">
<input type="hidden" name="xdebug" value="1">
@@ -1138,11 +1124,11 @@ class Feeds extends Handler_Protected {
$pdo = Db::pdo();
- $url = Feeds::fix_url($url);
+ $url = UrlHelper::validate($url);
- if (!$url || !Feeds::validate_feed_url($url)) return array("code" => 2);
+ if (!$url) return array("code" => 2);
- $contents = @fetch_file_contents($url, false, $auth_login, $auth_pass);
+ $contents = @UrlHelper::fetch($url, false, $auth_login, $auth_pass);
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SUBSCRIBE_FEED) as $plugin) {
$contents = $plugin->hook_subscribe_feed($contents, $url, $auth_login, $auth_pass);
@@ -1156,7 +1142,7 @@ class Feeds extends Handler_Protected {
return array("code" => 5, "message" => $fetch_last_error);
}
- if (mb_strpos($fetch_last_content_type, "html") !== FALSE && Feeds::is_html($contents)) {
+ if (mb_strpos($fetch_last_content_type, "html") !== false && Feeds::is_html($contents)) {
$feedUrls = Feeds::get_feeds_from_html($url, $contents);
if (count($feedUrls) == 0) {
@@ -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 {
@@ -1938,7 +1924,7 @@ class Feeds extends Handler_Protected {
}
static function get_feeds_from_html($url, $content) {
- $url = Feeds::fix_url($url);
+ $url = UrlHelper::validate($url);
$baseUrl = substr($url, 0, strrpos($url, '/') + 1);
$feedUrls = [];
@@ -1969,56 +1955,6 @@ class Feeds extends Handler_Protected {
return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 8192)) !== 0;
}
- static function validate_feed_url($url) {
- $parts = parse_url($url);
-
- return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
- }
-
- /**
- * Fixes incomplete URLs by prepending "http://".
- * Also replaces feed:// with http://, and
- * prepends a trailing slash if the url is a domain name only.
- *
- * @param string $url Possibly incomplete URL
- *
- * @return string Fixed URL.
- */
- static function fix_url($url) {
-
- // support schema-less urls
- if (strpos($url, '//') === 0) {
- $url = 'https:' . $url;
- }
-
- if (strpos($url, '://') === false) {
- $url = 'http://' . $url;
- } else if (substr($url, 0, 5) == 'feed:') {
- $url = 'http:' . substr($url, 5);
- }
-
- //prepend slash if the URL has no slash in it
- // "http://www.example" -> "http://www.example/"
- if (strpos($url, '/', strpos($url, ':') + 3) === false) {
- $url .= '/';
- }
-
- //convert IDNA hostname to punycode if possible
- if (function_exists("idn_to_ascii")) {
- $parts = parse_url($url);
- if (mb_detect_encoding($parts['host']) != 'ASCII')
- {
- $parts['host'] = idn_to_ascii($parts['host']);
- $url = build_url($parts);
- }
- }
-
- if ($url != "http:///")
- return $url;
- else
- return '';
- }
-
static function add_feed_category($feed_cat, $parent_cat_id = false, $order_id = 0) {
if (!$feed_cat) return false;
@@ -2161,7 +2097,7 @@ class Feeds extends Handler_Protected {
static function feed_purge_interval($feed_id) {
- $pdo = DB::pdo();
+ $pdo = Db::pdo();
$sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds
WHERE id = ?");
@@ -2267,6 +2203,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 +2277,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.php b/classes/handler.php
index 5b1109492..09557c284 100644
--- a/classes/handler.php
+++ b/classes/handler.php
@@ -9,7 +9,7 @@ class Handler implements IHandler {
}
function csrf_ignore($method) {
- return true;
+ return false;
}
function before($method) {
@@ -20,4 +20,4 @@ class Handler implements IHandler {
return true;
}
-} \ No newline at end of file
+}
diff --git a/classes/handler/public.php b/classes/handler/public.php
index 8c2700012..9671b18b2 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);
@@ -91,7 +81,7 @@ class Handler_Public extends Handler {
$tpl->setVariable('SELF_URL', htmlspecialchars(get_self_url_prefix()), true);
while ($line = $result->fetch()) {
- $line["content_preview"] = sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
+ $line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) {
$line = $p->hook_query_headlines($line);
@@ -108,7 +98,7 @@ class Handler_Public extends Handler {
$tpl->setVariable('ARTICLE_TITLE', htmlspecialchars($line['title']), true);
$tpl->setVariable('ARTICLE_EXCERPT', $line["content_preview"], true);
- $content = sanitize($line["content"], false, $owner_uid,
+ $content = Sanitizer::sanitize($line["content"], false, $owner_uid,
$feed_site_url, false, $line["id"]);
$content = DiskCache::rewriteUrls($content);
@@ -190,7 +180,7 @@ class Handler_Public extends Handler {
while ($line = $result->fetch()) {
- $line["content_preview"] = sanitize(truncate_string(strip_tags($line["content_preview"]), 100, '...'));
+ $line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content_preview"]), 100, '...'));
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) {
$line = $p->hook_query_headlines($line, 100);
@@ -206,7 +196,7 @@ class Handler_Public extends Handler {
$article['link'] = $line['link'];
$article['title'] = $line['title'];
$article['excerpt'] = $line["content_preview"];
- $article['content'] = sanitize($line["content"], false, $owner_uid, $feed_site_url, false, $line["id"]);
+ $article['content'] = Sanitizer::sanitize($line["content"], false, $owner_uid, $feed_site_url, false, $line["id"]);
$article['updated'] = date('c', strtotime($line["updated"]));
if ($line['note']) $article['note'] = $line['note'];
@@ -293,15 +283,20 @@ class Handler_Public extends Handler {
}
function logout() {
- logout_user();
- header("Location: index.php");
+ if (validate_csrf($_POST["csrf_token"])) {
+ Pref_Users::logout_user();
+ header("Location: index.php");
+ } else {
+ header("Content-Type: text/json");
+ print error_json(6);
+ }
}
function share() {
$uuid = clean($_REQUEST["key"]);
if ($uuid) {
- $sth = $this->pdo->prepare("SELECT ref_id, owner_uid
+ $sth = $this->pdo->prepare("SELECT ref_id, owner_uid
FROM ttrss_user_entries WHERE uuid = ?");
$sth->execute([$uuid]);
@@ -348,7 +343,7 @@ class Handler_Public extends Handler {
$line["tags"] = Article::get_article_tags($id, $owner_uid, $line["tag_cache"]);
unset($line["tag_cache"]);
- $line["content"] = sanitize($line["content"],
+ $line["content"] = Sanitizer::sanitize($line["content"],
$line['hide_images'],
$owner_uid, $line["site_url"], false, $line["id"]);
@@ -376,7 +371,7 @@ class Handler_Public extends Handler {
}
body.css_loading * {
display : none;
- }
+ }
</style>
<link rel='shortcut icon' type='image/png' href='images/favicon.png'>
<link rel='icon' type='image/png' sizes='72x72' href='images/favicon-72px.png'>";
@@ -475,7 +470,7 @@ class Handler_Public extends Handler {
if (!$format) $format = 'atom';
if (SINGLE_USER_MODE) {
- authenticate_user("admin", null);
+ UserHelper::authenticate("admin", null);
}
$owner_id = false;
@@ -513,7 +508,7 @@ class Handler_Public extends Handler {
function sharepopup() {
if (SINGLE_USER_MODE) {
- login_sequence();
+ UserHelper::login_sequence();
}
header('Content-Type: text/html; charset=utf-8');
@@ -678,14 +673,15 @@ class Handler_Public extends Handler {
$login = clean($_POST["login"]);
$password = clean($_POST["password"]);
$remember_me = clean($_POST["remember_me"]);
+ $safe_mode = checkbox_to_sql_bool(clean($_POST["safe_mode"]));
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)) {
+ if (UserHelper::authenticate($login, $password)) {
$_POST["password"] = "";
if (get_schema_version() >= 120) {
@@ -694,6 +690,7 @@ class Handler_Public extends Handler {
$_SESSION["ref_schema_version"] = get_schema_version(true);
$_SESSION["bw_limit"] = !!clean($_POST["bw_limit"]);
+ $_SESSION["safe_mode"] = $safe_mode;
if (clean($_POST["profile"])) {
@@ -732,12 +729,13 @@ class Handler_Public extends Handler {
function subscribe() {
if (SINGLE_USER_MODE) {
- login_sequence();
+ UserHelper::login_sequence();
}
if ($_SESSION["uid"]) {
$feed_url = trim(clean($_REQUEST["feed_url"]));
+ $csrf_token = clean($_POST["csrf_token"]);
header('Content-Type: text/html; charset=utf-8');
?>
@@ -784,13 +782,14 @@ class Handler_Public extends Handler {
<div class='content'>
<?php
- if (!$feed_url) {
+ if (!$feed_url || !validate_csrf($csrf_token)) {
?>
<form method="post">
<input type="hidden" name="op" value="subscribe">
+ <?php print_hidden("csrf_token", $_SESSION["csrf_token"]) ?>
<fieldset>
<label>Feed or site URL:</label>
- <input style="width: 300px" dojoType="dijit.form.ValidationTextBox" required="1" name="feed_url">
+ <input style="width: 300px" dojoType="dijit.form.ValidationTextBox" required="1" name="feed_url" value="<?php echo htmlspecialchars($feed_url) ?>">
</fieldset>
<button class="alt-primary" dojoType="dijit.form.Button" type="submit">
@@ -830,6 +829,7 @@ class Handler_Public extends Handler {
print "<form action='public.php'>";
print "<input type='hidden' name='op' value='subscribe'>";
+ print_hidden("csrf_token", $_SESSION["csrf_token"]);
print "<fieldset>";
print "<label style='display : inline'>" . __("Multiple feed URLs found:") . "</label>";
@@ -878,7 +878,7 @@ class Handler_Public extends Handler {
print "</div></div></body></html>";
} else {
- render_login_form();
+ $this->render_login_form();
}
}
@@ -942,7 +942,7 @@ class Handler_Public extends Handler {
if ($timestamp && $resetpass_token &&
$timestamp >= time() - 15*60*60 &&
- $resetpass_token == $hash) {
+ $resetpass_token === $hash) {
$sth = $this->pdo->prepare("UPDATE ttrss_users SET resetpass_token = NULL
WHERE id = ?");
@@ -1030,11 +1030,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);
@@ -1094,7 +1092,7 @@ class Handler_Public extends Handler {
if (!SINGLE_USER_MODE && $_SESSION["access_level"] < 10) {
$_SESSION["login_error_msg"] = __("Your access level is insufficient to run this script.");
- render_login_form();
+ $this->render_login_form();
exit;
}
@@ -1246,7 +1244,7 @@ class Handler_Public extends Handler {
public function pluginhandler() {
$host = new PluginHost();
- $plugin_name = clean_filename($_REQUEST["plugin"]);
+ $plugin_name = basename(clean($_REQUEST["plugin"]));
$method = clean($_REQUEST["pmethod"]);
$host->load($plugin_name, PluginHost::KIND_USER, 0);
@@ -1274,5 +1272,13 @@ class Handler_Public extends Handler {
print error_json(14);
}
}
+
+ static function render_login_form() {
+ header('Cache-Control: public');
+
+ require_once "login_form.php";
+ exit;
+ }
+
}
?>
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..acccea5db 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();
}
@@ -150,7 +155,7 @@ class PluginHost {
foreach (array_keys($this->hooks[$type]) as $prio) {
$key = array_search($sender, $this->hooks[$type][$prio]);
- if ($key !== FALSE) {
+ if ($key !== false) {
unset($this->hooks[$type][$prio][$key]);
}
}
@@ -188,7 +193,7 @@ class PluginHost {
foreach ($plugins as $class) {
$class = trim($class);
- $class_file = strtolower(clean_filename($class));
+ $class_file = strtolower(basename(clean($class)));
if (!is_dir(__DIR__."/../plugins/$class_file") &&
!is_dir(__DIR__."/../plugins.local/$class_file")) continue;
@@ -213,7 +218,7 @@ class PluginHost {
if (file_exists($vendor_dir)) {
spl_autoload_register(function($class) use ($vendor_dir) {
- if (strpos($class, '\\') !== FALSE) {
+ if (strpos($class, '\\') !== false) {
list ($namespace, $class_name) = explode('\\', $class, 2);
if ($namespace && $class_name) {
@@ -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..4ff6ac9c2 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').'">';
@@ -772,6 +772,7 @@ class Pref_Feeds extends Handler_Protected {
<input style='display: none' id='icon_file' size='10' name='icon_file' type='file'>
</label>
<input type='hidden' name='op' value='pref-feeds'>
+ <input type='hidden' name='csrf_token' value='".$_SESSION['csrf_token']."'>
<input type='hidden' name='feed_id' value='$feed_id'>
<input type='hidden' name='method' value='uploadicon'>
<button dojoType='dijit.form.Button' onclick=\"return CommonDialogs.uploadFeedIcon();\"
@@ -1172,7 +1173,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 +1308,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>";
@@ -1325,6 +1326,7 @@ class Pref_Feeds extends Handler_Protected {
<input style='display : none' id='opml_file' name='opml_file' type='file'>&nbsp;
</label>
<input type='hidden' name='op' value='dlg'>
+ <input type='hidden' name='csrf_token' value='".$_SESSION['csrf_token']."'>
<input type='hidden' name='method' value='importOpml'>
<button dojoType='dijit.form.Button' class='alt-primary' onclick=\"return Helpers.OPML.import();\" type=\"submit\">" .
__('Import OPML') . "</button>";
@@ -1360,7 +1362,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 +1639,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 +1649,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 +1674,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() {
@@ -1696,7 +1703,7 @@ class Pref_Feeds extends Handler_Protected {
foreach ($feeds as $feed) {
$feed = trim($feed);
- if (Feeds::validate_feed_url($feed)) {
+ if (UrlHelper::validate($feed)) {
$this->pdo->beginTransaction();
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..f825454dd 100644
--- a/classes/pref/prefs.php
+++ b/classes/pref/prefs.php
@@ -8,7 +8,7 @@ class Pref_Prefs extends Handler_Protected {
private $profile_blacklist = [];
function csrf_ignore($method) {
- $csrf_ignored = array("index", "updateself", "customizecss", "editprefprofiles");
+ $csrf_ignored = array("index", "updateself", "customizecss", "editprefprofiles", "otpqrcode");
return array_search($method, $csrf_ignored) !== false;
}
@@ -125,8 +125,14 @@ class Pref_Prefs extends Handler_Protected {
$old_pw = clean($_POST["old_password"]);
$new_pw = clean($_POST["new_password"]);
+ $new_unclean_pw = $_POST["new_password"];
$con_pw = clean($_POST["confirm_password"]);
+ if ($new_unclean_pw != $new_pw) {
+ print "ERROR: ".format_error("New password contains disallowed characters.");
+ return;
+ }
+
if ($old_pw == $new_pw) {
print "ERROR: ".format_error("New password must be different from the old one.");
return;
@@ -213,11 +219,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);
@@ -253,7 +257,7 @@ class Pref_Prefs extends Handler_Protected {
AND owner_uid = :uid");
$sth->execute([":profile" => $_SESSION['profile'], ":uid" => $_SESSION['uid']]);
- initialize_user_prefs($_SESSION["uid"], $_SESSION["profile"]);
+ $this->initialize_user_prefs($_SESSION["uid"], $_SESSION["profile"]);
echo __("Your preferences are now set to default values.");
}
@@ -382,12 +386,12 @@ class Pref_Prefs extends Handler_Protected {
print "<fieldset>";
print "<label>" . __("New password:") . "</label>";
- print "<input dojoType='dijit.form.ValidationTextBox' type='password' required='1' name='new_password'>";
+ print "<input dojoType='dijit.form.ValidationTextBox' type='password' regexp='^[^<>]+' required='1' name='new_password'>";
print "</fieldset>";
print "<fieldset>";
print "<label>" . __("Confirm password:") . "</label>";
- print "<input dojoType='dijit.form.ValidationTextBox' type='password' required='1' name='confirm_password'>";
+ print "<input dojoType='dijit.form.ValidationTextBox' type='password' regexp='^[^<>]+' required='1' name='confirm_password'>";
print "</fieldset>";
print_hidden("op", "pref-prefs");
@@ -479,8 +483,8 @@ class Pref_Prefs extends Handler_Protected {
if (function_exists("imagecreatefromstring")) {
print "<h3>" . __("Scan the following code by the Authenticator application or copy the key manually") . "</h3>";
- $csrf_token = $_SESSION["csrf_token"];
- print "<img alt='otp qr-code' src='backend.php?op=pref-prefs&method=otpqrcode&csrf_token=$csrf_token'>";
+ $csrf_token_hash = sha1($_SESSION["csrf_token"]);
+ print "<img alt='otp qr-code' src='backend.php?op=pref-prefs&method=otpqrcode&csrf_token_hash=$csrf_token_hash'>";
} else {
print_error("PHP GD functions are required to generate QR codes.");
print "<h3>" . __("Use the following OTP key with a compatible Authenticator application") . "</h3>";
@@ -586,9 +590,9 @@ class Pref_Prefs extends Handler_Protected {
if ($profile) {
print_notice(__("Some preferences are only available in default profile."));
- initialize_user_prefs($_SESSION["uid"], $profile);
+ $this->initialize_user_prefs($_SESSION["uid"], $profile);
} else {
- initialize_user_prefs($_SESSION["uid"]);
+ $this->initialize_user_prefs($_SESSION["uid"]);
}
$prefs_available = [];
@@ -854,6 +858,10 @@ class Pref_Prefs extends Handler_Protected {
print_warning("Your PHP configuration has open_basedir restrictions enabled. Some plugins relying on CURL for functionality may not work correctly.");
}
+ if ($_SESSION["safe_mode"]) {
+ print_error("You have logged in using safe mode, no user plugins will be actually enabled until you login again.");
+ }
+
$feed_handler_whitelist = [ "Af_Comics" ];
$feed_handlers = array_merge(
@@ -862,7 +870,7 @@ class Pref_Prefs extends Handler_Protected {
PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FETCH_FEED));
$feed_handlers = array_filter($feed_handlers, function($plugin) use ($feed_handler_whitelist) {
- return in_array(get_class($plugin), $feed_handler_whitelist) === FALSE; });
+ return in_array(get_class($plugin), $feed_handler_whitelist) === false; });
if (count($feed_handlers) > 0) {
print_error(
@@ -1006,21 +1014,28 @@ class Pref_Prefs extends Handler_Protected {
}
function otpqrcode() {
- require_once "lib/phpqrcode/phpqrcode.php";
+ $csrf_token_hash = clean($_REQUEST["csrf_token_hash"]);
- $sth = $this->pdo->prepare("SELECT login
- FROM ttrss_users
- WHERE id = ?");
- $sth->execute([$_SESSION['uid']]);
+ if (sha1($_SESSION["csrf_token"]) === $csrf_token_hash) {
+ require_once "lib/phpqrcode/phpqrcode.php";
- if ($row = $sth->fetch()) {
- $secret = $this->otpsecret();
- $login = $row['login'];
+ $sth = $this->pdo->prepare("SELECT login
+ FROM ttrss_users
+ WHERE id = ?");
+ $sth->execute([$_SESSION['uid']]);
- if ($secret) {
- QRcode::png("otpauth://totp/".urlencode($login).
- "?secret=$secret&issuer=".urlencode("Tiny Tiny RSS"));
+ if ($row = $sth->fetch()) {
+ $secret = $this->otpsecret();
+ $login = $row['login'];
+
+ if ($secret) {
+ QRcode::png("otpauth://totp/".urlencode($login).
+ "?secret=$secret&issuer=".urlencode("Tiny Tiny RSS"));
+ }
}
+ } else {
+ header("Content-Type: text/json");
+ print error_json(6);
}
}
@@ -1087,11 +1102,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);
@@ -1353,4 +1366,57 @@ class Pref_Prefs extends Handler_Protected {
$this->appPasswordList();
}
+
+ static function initialize_user_prefs($uid, $profile = false) {
+
+ if (get_schema_version() < 63) $profile_qpart = "";
+
+ $pdo = Db::pdo();
+ $in_nested_tr = false;
+
+ try {
+ $pdo->beginTransaction();
+ } catch (Exception $e) {
+ $in_nested_tr = true;
+ }
+
+ $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
+
+ if (!is_numeric($profile) || !$profile || get_schema_version() < 63) $profile = null;
+
+ $u_sth = $pdo->prepare("SELECT pref_name
+ FROM ttrss_user_prefs WHERE owner_uid = :uid AND
+ (profile = :profile OR (:profile IS NULL AND profile IS NULL))");
+ $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
+
+ $active_prefs = array();
+
+ while ($line = $u_sth->fetch()) {
+ array_push($active_prefs, $line["pref_name"]);
+ }
+
+ while ($line = $sth->fetch()) {
+ if (array_search($line["pref_name"], $active_prefs) === false) {
+// print "adding " . $line["pref_name"] . "<br>";
+
+ if (get_schema_version() < 63) {
+ $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
+ (owner_uid,pref_name,value) VALUES
+ (?, ?, ?)");
+ $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
+
+ } else {
+ $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
+ (owner_uid,pref_name,value, profile) VALUES
+ (?, ?, ?, ?)");
+ $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
+ }
+
+ }
+ }
+
+ if (!$in_nested_tr) $pdo->commit();
+
+ }
+
}
diff --git a/classes/pref/users.php b/classes/pref/users.php
index 851d4fa9e..aeabc4502 100644
--- a/classes/pref/users.php
+++ b/classes/pref/users.php
@@ -259,7 +259,7 @@ class Pref_Users extends Handler_Protected {
print T_sprintf("Added user %s with password %s",
$login, $tmp_user_pwd);
- initialize_user($new_uid);
+ $this->initialize_user($new_uid);
} else {
@@ -443,4 +443,25 @@ class Pref_Users extends Handler_Protected {
return $default;
}
+ // this is called after user is created to initialize default feeds, labels
+ // or whatever else
+ // user preferences are checked on every login, not here
+ static function initialize_user($uid) {
+
+ $pdo = Db::pdo();
+
+ $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
+ values (?, 'Tiny Tiny RSS: Forum',
+ 'https://tt-rss.org/forum/rss.php')");
+ $sth->execute([$uid]);
+ }
+
+ static function logout_user() {
+ @session_destroy();
+ if (isset($_COOKIE[session_name()])) {
+ setcookie(session_name(), '', time()-42000, '/');
+ }
+ session_commit();
+ }
+
}
diff --git a/classes/rpc.php b/classes/rpc.php
index 208551075..a7123566c 100755
--- a/classes/rpc.php
+++ b/classes/rpc.php
@@ -2,7 +2,7 @@
class RPC extends Handler_Protected {
function csrf_ignore($method) {
- $csrf_ignored = array("sanitycheck", "completelabels", "saveprofile");
+ $csrf_ignored = array("completelabels", "saveprofile");
return array_search($method, $csrf_ignored) !== false;
}
@@ -52,7 +52,7 @@ class RPC extends Handler_Protected {
$profile_id = $row['id'];
if ($profile_id) {
- initialize_user_prefs($_SESSION["uid"], $profile_id);
+ Pref_Prefs::initialize_user_prefs($_SESSION["uid"], $profile_id);
}
}
}
@@ -279,7 +279,7 @@ class RPC extends Handler_Protected {
];
if ($seq % 2 == 0)
- $reply['runtime-info'] = make_runtime_info();
+ $reply['runtime-info'] = $this->make_runtime_info();
print json_encode($reply);
}
@@ -323,8 +323,8 @@ class RPC extends Handler_Protected {
$reply['error'] = sanity_check();
if ($reply['error']['code'] == 0) {
- $reply['init-params'] = make_init_params();
- $reply['runtime-info'] = make_runtime_info();
+ $reply['init-params'] = $this->make_init_params();
+ $reply['runtime-info'] = $this->make_runtime_info();
}
print json_encode($reply);
@@ -436,7 +436,10 @@ class RPC extends Handler_Protected {
ttrss_feeds.update_interval > 0
AND ttrss_feeds.last_updated < NOW() - CAST((ttrss_feeds.update_interval || ' minutes') AS INTERVAL)
) OR ttrss_feeds.last_updated IS NULL
- OR last_updated = '1970-01-01 00:00:00')";
+ OR (
+ ttrss_feeds.update_interval > 0
+ AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL)
+ ))";
} else {
$update_limit_qpart = "AND ((
ttrss_feeds.update_interval = 0
@@ -445,7 +448,10 @@ class RPC extends Handler_Protected {
ttrss_feeds.update_interval > 0
AND ttrss_feeds.last_updated < DATE_SUB(NOW(), INTERVAL ttrss_feeds.update_interval MINUTE)
) OR ttrss_feeds.last_updated IS NULL
- OR last_updated = '1970-01-01 00:00:00')";
+ OR (
+ ttrss_feeds.update_interval > 0
+ AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL)
+ ))";
}
// Test if feed is currently being updated by another process.
@@ -455,7 +461,7 @@ class RPC extends Handler_Protected {
$updstart_thresh_qpart = "AND (ttrss_feeds.last_update_started IS NULL OR ttrss_feeds.last_update_started < DATE_SUB(NOW(), INTERVAL 5 MINUTE))";
}
- $random_qpart = sql_random_function();
+ $random_qpart = Db::sql_random_function();
$pdo = Db::pdo();
@@ -572,7 +578,7 @@ class RPC extends Handler_Protected {
function log() {
$msg = clean($_REQUEST['msg']);
- $file = clean_filename($_REQUEST['file']);
+ $file = basename(clean($_REQUEST['file']));
$line = (int) clean($_REQUEST['line']);
$context = clean($_REQUEST['context']);
@@ -596,7 +602,7 @@ class RPC extends Handler_Protected {
get_version($git_commit, $git_timestamp);
if (defined('CHECK_FOR_UPDATES') && CHECK_FOR_UPDATES && $_SESSION["access_level"] >= 10 && $git_timestamp) {
- $content = @fetch_file_contents(["url" => "https://srv.tt-rss.org/version.json"]);
+ $content = @UrlHelper::fetch(["url" => "https://tt-rss.org/version.json"]);
if ($content) {
$content = json_decode($content, true);
@@ -614,4 +620,292 @@ class RPC extends Handler_Protected {
print json_encode($rv);
}
+ private function make_init_params() {
+ $params = array();
+
+ foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
+ "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
+ "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
+ "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
+
+ $params[strtolower($param)] = (int) get_pref($param);
+ }
+
+ $params["check_for_updates"] = CHECK_FOR_UPDATES;
+ $params["icons_url"] = ICONS_URL;
+ $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
+ $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
+ $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
+ $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
+ $params["bw_limit"] = (int) $_SESSION["bw_limit"];
+ $params["is_default_pw"] = Pref_Prefs::isdefaultpassword();
+ $params["label_base_index"] = (int) LABEL_BASE_INDEX;
+
+ $theme = get_pref( "USER_CSS_THEME", false, false);
+ $params["theme"] = theme_exists($theme) ? $theme : "";
+
+ $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
+
+ $params["php_platform"] = PHP_OS;
+ $params["php_version"] = PHP_VERSION;
+
+ $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
+
+ $pdo = Db::pdo();
+
+ $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
+ ttrss_feeds WHERE owner_uid = ?");
+ $sth->execute([$_SESSION['uid']]);
+ $row = $sth->fetch();
+
+ $max_feed_id = $row["mid"];
+ $num_feeds = $row["nf"];
+
+ $params["self_url_prefix"] = get_self_url_prefix();
+ $params["max_feed_id"] = (int) $max_feed_id;
+ $params["num_feeds"] = (int) $num_feeds;
+
+ $params["hotkeys"] = $this->get_hotkeys_map();
+
+ $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
+
+ $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
+
+ $params["icon_indicator_white"] = $this->image_to_base64("images/indicator_white.gif");
+
+ $params["labels"] = Labels::get_all_labels($_SESSION["uid"]);
+
+ return $params;
+ }
+
+ private function image_to_base64($filename) {
+ if (file_exists($filename)) {
+ $ext = pathinfo($filename, PATHINFO_EXTENSION);
+
+ return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
+ } else {
+ return "";
+ }
+ }
+
+ static function make_runtime_info() {
+ $data = array();
+
+ $pdo = Db::pdo();
+
+ $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
+ ttrss_feeds WHERE owner_uid = ?");
+ $sth->execute([$_SESSION['uid']]);
+ $row = $sth->fetch();
+
+ $max_feed_id = $row['mid'];
+ $num_feeds = $row['nf'];
+
+ $data["max_feed_id"] = (int) $max_feed_id;
+ $data["num_feeds"] = (int) $num_feeds;
+ $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
+ $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
+
+ if (LOG_DESTINATION == 'sql' && $_SESSION['access_level'] >= 10) {
+ if (DB_TYPE == 'pgsql') {
+ $log_interval = "created_at > NOW() - interval '1 hour'";
+ } else {
+ $log_interval = "created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)";
+ }
+
+ $sth = $pdo->prepare("SELECT COUNT(id) AS cid FROM ttrss_error_log WHERE $log_interval");
+ $sth->execute();
+
+ if ($row = $sth->fetch()) {
+ $data['recent_log_events'] = $row['cid'];
+ }
+ }
+
+ if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
+
+ $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
+
+ if (time() - $_SESSION["daemon_stamp_check"] > 30) {
+
+ $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
+
+ if ($stamp) {
+ $stamp_delta = time() - $stamp;
+
+ if ($stamp_delta > 1800) {
+ $stamp_check = 0;
+ } else {
+ $stamp_check = 1;
+ $_SESSION["daemon_stamp_check"] = time();
+ }
+
+ $data['daemon_stamp_ok'] = $stamp_check;
+
+ $stamp_fmt = date("Y.m.d, G:i", $stamp);
+
+ $data['daemon_stamp'] = $stamp_fmt;
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ static function get_hotkeys_info() {
+ $hotkeys = array(
+ __("Navigation") => array(
+ "next_feed" => __("Open next feed"),
+ "prev_feed" => __("Open previous feed"),
+ "next_article_or_scroll" => __("Open next article (in combined mode, scroll down)"),
+ "prev_article_or_scroll" => __("Open previous article (in combined mode, scroll up)"),
+ "next_headlines_page" => __("Scroll headlines by one page down"),
+ "prev_headlines_page" => __("Scroll headlines by one page up"),
+ "next_article_noscroll" => __("Open next article"),
+ "prev_article_noscroll" => __("Open previous article"),
+ "next_article_noexpand" => __("Move to next article (don't expand)"),
+ "prev_article_noexpand" => __("Move to previous article (don't expand)"),
+ "search_dialog" => __("Show search dialog"),
+ "cancel_search" => __("Cancel active search")),
+ __("Article") => array(
+ "toggle_mark" => __("Toggle starred"),
+ "toggle_publ" => __("Toggle published"),
+ "toggle_unread" => __("Toggle unread"),
+ "edit_tags" => __("Edit tags"),
+ "open_in_new_window" => __("Open in new window"),
+ "catchup_below" => __("Mark below as read"),
+ "catchup_above" => __("Mark above as read"),
+ "article_scroll_down" => __("Scroll down"),
+ "article_scroll_up" => __("Scroll up"),
+ "article_page_down" => __("Scroll down page"),
+ "article_page_up" => __("Scroll up page"),
+ "select_article_cursor" => __("Select article under cursor"),
+ "email_article" => __("Email article"),
+ "close_article" => __("Close/collapse article"),
+ "toggle_expand" => __("Toggle article expansion (combined mode)"),
+ "toggle_widescreen" => __("Toggle widescreen mode"),
+ "toggle_full_text" => __("Toggle full article text via Readability")),
+ __("Article selection") => array(
+ "select_all" => __("Select all articles"),
+ "select_unread" => __("Select unread"),
+ "select_marked" => __("Select starred"),
+ "select_published" => __("Select published"),
+ "select_invert" => __("Invert selection"),
+ "select_none" => __("Deselect everything")),
+ __("Feed") => array(
+ "feed_refresh" => __("Refresh current feed"),
+ "feed_unhide_read" => __("Un/hide read feeds"),
+ "feed_subscribe" => __("Subscribe to feed"),
+ "feed_edit" => __("Edit feed"),
+ "feed_catchup" => __("Mark as read"),
+ "feed_reverse" => __("Reverse headlines"),
+ "feed_toggle_vgroup" => __("Toggle headline grouping"),
+ "feed_debug_update" => __("Debug feed update"),
+ "feed_debug_viewfeed" => __("Debug viewfeed()"),
+ "catchup_all" => __("Mark all feeds as read"),
+ "cat_toggle_collapse" => __("Un/collapse current category"),
+ "toggle_cdm_expanded" => __("Toggle auto expand in combined mode"),
+ "toggle_combined_mode" => __("Toggle combined mode")),
+ __("Go to") => array(
+ "goto_all" => __("All articles"),
+ "goto_fresh" => __("Fresh"),
+ "goto_marked" => __("Starred"),
+ "goto_published" => __("Published"),
+ "goto_read" => __("Recently read"),
+ "goto_tagcloud" => __("Tag cloud"),
+ "goto_prefs" => __("Preferences")),
+ __("Other") => array(
+ "create_label" => __("Create label"),
+ "create_filter" => __("Create filter"),
+ "collapse_sidebar" => __("Un/collapse sidebar"),
+ "help_dialog" => __("Show help dialog"))
+ );
+
+ foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
+ $hotkeys = $plugin->hook_hotkey_info($hotkeys);
+ }
+
+ return $hotkeys;
+ }
+
+ // {3} - 3 panel mode only
+ // {C} - combined mode only
+ static function get_hotkeys_map() {
+ $hotkeys = array(
+ "k" => "next_feed",
+ "j" => "prev_feed",
+ "n" => "next_article_noscroll",
+ "p" => "prev_article_noscroll",
+ "N" => "article_page_down",
+ "P" => "article_page_up",
+ "*(33)|Shift+PgUp" => "article_page_up",
+ "*(34)|Shift+PgDn" => "article_page_down",
+ "{3}(38)|Up" => "prev_article_or_scroll",
+ "{3}(40)|Down" => "next_article_or_scroll",
+ "*(38)|Shift+Up" => "article_scroll_up",
+ "*(40)|Shift+Down" => "article_scroll_down",
+ "^(38)|Ctrl+Up" => "prev_article_noscroll",
+ "^(40)|Ctrl+Down" => "next_article_noscroll",
+ "/" => "search_dialog",
+ "\\" => "cancel_search",
+ "s" => "toggle_mark",
+ "S" => "toggle_publ",
+ "u" => "toggle_unread",
+ "T" => "edit_tags",
+ "o" => "open_in_new_window",
+ "c p" => "catchup_below",
+ "c n" => "catchup_above",
+ "a W" => "toggle_widescreen",
+ "a e" => "toggle_full_text",
+ "e" => "email_article",
+ "a q" => "close_article",
+ "a a" => "select_all",
+ "a u" => "select_unread",
+ "a U" => "select_marked",
+ "a p" => "select_published",
+ "a i" => "select_invert",
+ "a n" => "select_none",
+ "f r" => "feed_refresh",
+ "f a" => "feed_unhide_read",
+ "f s" => "feed_subscribe",
+ "f e" => "feed_edit",
+ "f q" => "feed_catchup",
+ "f x" => "feed_reverse",
+ "f g" => "feed_toggle_vgroup",
+ "f D" => "feed_debug_update",
+ "f G" => "feed_debug_viewfeed",
+ "f C" => "toggle_combined_mode",
+ "f c" => "toggle_cdm_expanded",
+ "Q" => "catchup_all",
+ "x" => "cat_toggle_collapse",
+ "g a" => "goto_all",
+ "g f" => "goto_fresh",
+ "g s" => "goto_marked",
+ "g p" => "goto_published",
+ "g r" => "goto_read",
+ "g t" => "goto_tagcloud",
+ "g P" => "goto_prefs",
+ "r" => "select_article_cursor",
+ "c l" => "create_label",
+ "c f" => "create_filter",
+ "c s" => "collapse_sidebar",
+ "?" => "help_dialog",
+ );
+
+ foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
+ $hotkeys = $plugin->hook_hotkey_map($hotkeys);
+ }
+
+ $prefixes = array();
+
+ foreach (array_keys($hotkeys) as $hotkey) {
+ $pair = explode(" ", $hotkey, 2);
+
+ if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
+ array_push($prefixes, $pair[0]);
+ }
+ }
+
+ return array($prefixes, $hotkeys);
+ }
+
}
diff --git a/classes/rssutils.php b/classes/rssutils.php
index 831ac1baf..14a0a18a7 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);
@@ -52,8 +57,10 @@ class RSSUtils {
ttrss_feeds.update_interval > 0
AND ttrss_feeds.last_updated < NOW() - CAST((ttrss_feeds.update_interval || ' minutes') AS INTERVAL)
) OR (ttrss_feeds.last_updated IS NULL
+ AND ttrss_feeds.update_interval > 0
AND ttrss_user_prefs.value != '-1')
OR (last_updated = '1970-01-01 00:00:00'
+ AND ttrss_feeds.update_interval > 0
AND ttrss_user_prefs.value != '-1'))";
} else {
$update_limit_qpart = "AND ((
@@ -64,8 +71,10 @@ class RSSUtils {
ttrss_feeds.update_interval > 0
AND ttrss_feeds.last_updated < DATE_SUB(NOW(), INTERVAL ttrss_feeds.update_interval MINUTE)
) OR (ttrss_feeds.last_updated IS NULL
+ AND ttrss_feeds.update_interval > 0
AND ttrss_user_prefs.value != '-1')
OR (last_updated = '1970-01-01 00:00:00'
+ AND ttrss_feeds.update_interval > 0
AND ttrss_user_prefs.value != '-1'))";
}
@@ -139,7 +148,7 @@ class RSSUtils {
if ($tline = $usth->fetch()) {
Debug::log(" => " . $tline["last_updated"] . ", " . $tline["id"] . " " . $tline["owner_uid"]);
- if (array_search($tline["owner_uid"], $batch_owners) === FALSE)
+ if (array_search($tline["owner_uid"], $batch_owners) === false)
array_push($batch_owners, $tline["owner_uid"]);
$fstarted = microtime(true);
@@ -209,7 +218,7 @@ class RSSUtils {
}
if (!$basic_info) {
- $feed_data = fetch_file_contents($fetch_url, false,
+ $feed_data = UrlHelper::fetch($fetch_url, false,
$auth_login, $auth_pass, false,
FEED_FETCH_TIMEOUT,
0);
@@ -259,8 +268,6 @@ class RSSUtils {
*/
static function update_rss_feed($feed, $no_cache = false) {
- reset_fetch_domain_quota();
-
Debug::log("start", Debug::$LOG_VERBOSE);
$pdo = Db::pdo();
@@ -391,7 +398,7 @@ class RSSUtils {
Debug::log("fetching [$fetch_url] (force_refetch: $force_refetch)...", Debug::$LOG_VERBOSE);
- $feed_data = fetch_file_contents([
+ $feed_data = UrlHelper::fetch([
"url" => $fetch_url,
"login" => $auth_login,
"pass" => $auth_pass,
@@ -401,7 +408,11 @@ class RSSUtils {
$feed_data = trim($feed_data);
+ global $fetch_effective_url;
+ global $fetch_effective_ip_addr;
+
Debug::log("fetch done.", Debug::$LOG_VERBOSE);
+ Debug::log("effective URL (after redirects): " . clean($fetch_effective_url) . " (IP: $fetch_effective_ip_addr)", Debug::$LOG_VERBOSE);
Debug::log("source last modified: " . $fetch_last_modified, Debug::$LOG_VERBOSE);
if ($feed_data && $fetch_last_modified != $stored_last_modified) {
@@ -469,7 +480,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 +597,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 +643,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 +659,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 +705,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 +848,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 +873,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 +919,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 +1067,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 +1204,7 @@ class RSSUtils {
return true;
}
+ /* TODO: move to DiskCache? */
static function cache_enclosures($enclosures, $site_url) {
$cache = new DiskCache("images");
@@ -1204,7 +1223,7 @@ class RSSUtils {
global $fetch_last_error_code;
global $fetch_last_error;
- $file_content = fetch_file_contents(array("url" => $src,
+ $file_content = UrlHelper::fetch(array("url" => $src,
"http_referrer" => $src,
"max_size" => MAX_CACHE_FILE_SIZE));
@@ -1221,6 +1240,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 = UrlHelper::fetch(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 +1276,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 +1377,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)
@@ -1480,7 +1515,7 @@ class RSSUtils {
static function housekeeping_user($owner_uid) {
$tmph = new PluginHost();
- load_user_plugins($owner_uid, $tmph);
+ UserHelper::load_user_plugins($owner_uid, $tmph);
$tmph->run_hooks(PluginHost::HOOK_HOUSE_KEEPING, "hook_house_keeping", "");
}
@@ -1509,7 +1544,7 @@ class RSSUtils {
if ($favicon_url) {
// Limiting to "image" type misses those served with text/plain
- $contents = fetch_file_contents($favicon_url); // , "image");
+ $contents = UrlHelper::fetch($favicon_url); // , "image");
if ($contents) {
// Crude image type matching.
@@ -1682,7 +1717,7 @@ class RSSUtils {
$favicon_url = false;
- if ($html = @fetch_file_contents($url)) {
+ if ($html = @UrlHelper::fetch($url)) {
$doc = new DOMDocument();
if ($doc->loadHTML($html)) {
@@ -1710,4 +1745,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/sanitizer.php b/classes/sanitizer.php
new file mode 100644
index 000000000..8e008ef85
--- /dev/null
+++ b/classes/sanitizer.php
@@ -0,0 +1,217 @@
+<?php
+class Sanitizer {
+ private static function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
+ $xpath = new DOMXPath($doc);
+ $entries = $xpath->query('//*');
+
+ foreach ($entries as $entry) {
+ if (!in_array($entry->nodeName, $allowed_elements)) {
+ $entry->parentNode->removeChild($entry);
+ }
+
+ if ($entry->hasAttributes()) {
+ $attrs_to_remove = array();
+
+ foreach ($entry->attributes as $attr) {
+
+ if (strpos($attr->nodeName, 'on') === 0) {
+ array_push($attrs_to_remove, $attr);
+ }
+
+ if (strpos($attr->nodeName, "data-") === 0) {
+ array_push($attrs_to_remove, $attr);
+ }
+
+ if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
+ array_push($attrs_to_remove, $attr);
+ }
+
+ if (in_array($attr->nodeName, $disallowed_attributes)) {
+ array_push($attrs_to_remove, $attr);
+ }
+ }
+
+ foreach ($attrs_to_remove as $attr) {
+ $entry->removeAttributeNode($attr);
+ }
+ }
+ }
+
+ return $doc;
+ }
+
+ public static function iframe_whitelisted($entry) {
+ @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
+
+ if ($src) {
+ foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_IFRAME_WHITELISTED) as $plugin) {
+ if ($plugin->hook_iframe_whitelisted($src))
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
+ if (!$owner) $owner = $_SESSION["uid"];
+
+ $res = trim($str); if (!$res) return '';
+
+ $doc = new DOMDocument();
+ $doc->loadHTML('<?xml encoding="UTF-8">' . $res);
+ $xpath = new DOMXPath($doc);
+
+ $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
+
+ $entries = $xpath->query('(//a[@href]|//img[@src]|//source[@srcset|@src])');
+
+ foreach ($entries as $entry) {
+
+ if ($entry->hasAttribute('href')) {
+ $entry->setAttribute('href',
+ rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
+
+ $entry->setAttribute('rel', 'noopener noreferrer');
+ $entry->setAttribute("target", "_blank");
+ }
+
+ if ($entry->hasAttribute('src')) {
+ $entry->setAttribute('src',
+ rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src')));
+ }
+
+ if ($entry->nodeName == 'img') {
+ $entry->setAttribute('referrerpolicy', 'no-referrer');
+ $entry->setAttribute('loading', 'lazy');
+ }
+
+ if ($entry->hasAttribute('srcset')) {
+ $matches = RSSUtils::decode_srcset($entry->getAttribute('srcset'));
+
+ for ($i = 0; $i < count($matches); $i++) {
+ $matches[$i]["url"] = rewrite_relative_url($rewrite_base_url, $matches[$i]["url"]);
+ }
+
+ $entry->setAttribute("srcset", RSSUtils::encode_srcset($matches));
+ }
+
+ if ($entry->hasAttribute('src') &&
+ ($owner && get_pref("STRIP_IMAGES", $owner)) || $force_remove_images || $_SESSION["bw_limit"]) {
+
+ $p = $doc->createElement('p');
+
+ $a = $doc->createElement('a');
+ $a->setAttribute('href', $entry->getAttribute('src'));
+
+ $a->appendChild(new DOMText($entry->getAttribute('src')));
+ $a->setAttribute('target', '_blank');
+ $a->setAttribute('rel', 'noopener noreferrer');
+
+ $p->appendChild($a);
+
+ if ($entry->nodeName == 'source') {
+
+ if ($entry->parentNode && $entry->parentNode->parentNode)
+ $entry->parentNode->parentNode->replaceChild($p, $entry->parentNode);
+
+ } else if ($entry->nodeName == 'img') {
+ if ($entry->parentNode)
+ $entry->parentNode->replaceChild($p, $entry);
+ }
+ }
+ }
+
+ $entries = $xpath->query('//iframe');
+ foreach ($entries as $entry) {
+ if (!Sanitizer::iframe_whitelisted($entry)) {
+ $entry->setAttribute('sandbox', 'allow-scripts');
+ } else {
+ if (is_prefix_https()) {
+ $entry->setAttribute("src",
+ str_replace("http://", "https://",
+ $entry->getAttribute("src")));
+ }
+ }
+ }
+
+ $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
+ 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
+ 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
+ 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
+ 'dt', 'em', 'footer', 'figure', 'figcaption',
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
+ 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
+ 'ol', 'p', 'picture', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
+ 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
+ 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
+ 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
+
+ if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
+
+ $disallowed_attributes = array('id', 'style', 'class', 'width', 'height', 'allow');
+
+ foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
+ $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
+ if (is_array($retval)) {
+ $doc = $retval[0];
+ $allowed_elements = $retval[1];
+ $disallowed_attributes = $retval[2];
+ } else {
+ $doc = $retval;
+ }
+ }
+
+ $doc->removeChild($doc->firstChild); //remove doctype
+ $doc = Sanitizer::strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
+
+ $entries = $xpath->query('//iframe');
+ foreach ($entries as $entry) {
+ $div = $doc->createElement('div');
+ $div->setAttribute('class', 'embed-responsive');
+ $entry->parentNode->replaceChild($div, $entry);
+ $div->appendChild($entry);
+ }
+
+ if ($highlight_words && is_array($highlight_words)) {
+ foreach ($highlight_words as $word) {
+
+ // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
+
+ $elements = $xpath->query("//*/text()");
+
+ foreach ($elements as $child) {
+
+ $fragment = $doc->createDocumentFragment();
+ $text = $child->textContent;
+
+ while (($pos = mb_stripos($text, $word)) !== false) {
+ $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
+ $word = mb_substr($text, $pos, mb_strlen($word));
+ $highlight = $doc->createElement('span');
+ $highlight->appendChild(new DomText($word));
+ $highlight->setAttribute('class', 'highlight');
+ $fragment->appendChild($highlight);
+ $text = mb_substr($text, $pos + mb_strlen($word));
+ }
+
+ if (!empty($text)) $fragment->appendChild(new DomText($text));
+
+ $child->parentNode->replaceChild($fragment, $child);
+ }
+ }
+ }
+
+ $res = $doc->saveHTML();
+
+ /* strip everything outside of <body>...</body> */
+
+ $res_frag = array();
+ if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
+ return $res_frag[1];
+ } else {
+ return $res;
+ }
+ }
+
+}
diff --git a/classes/templator.php b/classes/templator.php
new file mode 100644
index 000000000..b682f8b82
--- /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);
+ }
+ }
+}
diff --git a/classes/urlhelper.php b/classes/urlhelper.php
new file mode 100644
index 000000000..a83d10a5b
--- /dev/null
+++ b/classes/urlhelper.php
@@ -0,0 +1,474 @@
+<?php
+class UrlHelper {
+ static function build_url($parts) {
+ $tmp = $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
+
+ if (isset($parts['query'])) $tmp .= '?' . $parts['query'];
+ if (isset($parts['fragment'])) $tmp .= '#' . $parts['fragment'];
+
+ return $tmp;
+ }
+
+ /**
+ * Converts a (possibly) relative URL to a absolute one.
+ *
+ * @param string $url Base URL (i.e. from where the document is)
+ * @param string $rel_url Possibly relative URL in the document
+ *
+ * @return string Absolute URL
+ */
+ public static function rewrite_relative($url, $rel_url) {
+
+ $rel_parts = parse_url($rel_url);
+
+ if ($rel_parts['host'] && $rel_parts['scheme']) {
+ return UrlHelper::validate($rel_url);
+ } else if (strpos($rel_url, "//") === 0) {
+ # protocol-relative URL (rare but they exist)
+ return UrlHelper::validate("https:" . $rel_url);
+ } else if (strpos($rel_url, "magnet:") === 0) {
+ # allow magnet links
+ return $rel_url;
+ } else {
+ $parts = parse_url($url);
+
+ $rel_parts['host'] = $parts['host'];
+ $rel_parts['scheme'] = $parts['scheme'];
+
+ if (strpos($rel_parts['path'], '/') !== 0)
+ $rel_parts['path'] = '/' . $rel_parts['path'];
+
+ $rel_parts['path'] = str_replace("/./", "/", $rel_parts['path']);
+ $rel_parts['path'] = str_replace("//", "/", $rel_parts['path']);
+
+ return UrlHelper::validate(UrlHelper::build_url($rel_parts));
+ }
+ }
+
+ // extended filtering involves validation for safe ports and loopback
+ static function validate($url, $extended_filtering = false) {
+
+ $url = clean($url);
+
+ # fix protocol-relative URLs
+ if (strpos($url, "//") === 0)
+ $url = "https:" . $url;
+
+ if (filter_var($url, FILTER_VALIDATE_URL) === false)
+ return false;
+
+ $tokens = parse_url($url);
+
+ if (!$tokens['host'])
+ return false;
+
+ if (!in_array(strtolower($tokens['scheme']), ['http', 'https']))
+ return false;
+
+ if ($extended_filtering) {
+ if (!in_array($tokens['port'], [80, 443, '']))
+ return false;
+
+ if (strtolower($tokens['host']) == 'localhost' || $tokens['host'] == '::1' || strpos($tokens['host'], '127.') === 0)
+ return false;
+ }
+
+ //convert IDNA hostname to punycode if possible
+ if (function_exists("idn_to_ascii")) {
+ if (mb_detect_encoding($tokens['host']) != 'ASCII') {
+ $parts['host'] = idn_to_ascii($tokens['host']);
+ $url = UrlHelper::build_url($tokens);
+ }
+ }
+
+ return $url;
+ }
+
+ static function resolve_redirects($url, $timeout, $nest = 0) {
+
+ // too many redirects
+ if ($nest > 10)
+ return false;
+
+ if (version_compare(PHP_VERSION, '7.1.0', '>=')) {
+ $context_options = array(
+ 'http' => array(
+ 'header' => array(
+ 'Connection: close'
+ ),
+ 'method' => 'HEAD',
+ 'timeout' => $timeout,
+ 'protocol_version'=> 1.1)
+ );
+
+ if (defined('_HTTP_PROXY')) {
+ $context_options['http']['request_fulluri'] = true;
+ $context_options['http']['proxy'] = _HTTP_PROXY;
+ }
+
+ $context = stream_context_create($context_options);
+
+ $headers = get_headers($url, 0, $context);
+ } else {
+ $headers = get_headers($url, 0);
+ }
+
+ if (is_array($headers)) {
+ $headers = array_reverse($headers); // last one is the correct one
+
+ foreach($headers as $header) {
+ if (stripos($header, 'Location:') === 0) {
+ $url = UrlHelper::rewrite_relative($url, trim(substr($header, strlen('Location:'))));
+
+ return UrlHelper::resolve_redirects($url, $timeout, $nest + 1);
+ }
+ }
+
+ return $url;
+ }
+
+ // request failed?
+ return false;
+ }
+
+ // TODO: max_size currently only works for CURL transfers
+ // TODO: multiple-argument way is deprecated, first parameter is a hash now
+ public static function fetch($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
+ 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
+
+ global $fetch_last_error;
+ global $fetch_last_error_code;
+ global $fetch_last_error_content;
+ global $fetch_last_content_type;
+ global $fetch_last_modified;
+ global $fetch_effective_url;
+ global $fetch_effective_ip_addr;
+ global $fetch_curl_used;
+ global $fetch_domain_hits;
+
+ $fetch_last_error = false;
+ $fetch_last_error_code = -1;
+ $fetch_last_error_content = "";
+ $fetch_last_content_type = "";
+ $fetch_curl_used = false;
+ $fetch_last_modified = "";
+ $fetch_effective_url = "";
+ $fetch_effective_ip_addr = "";
+
+ if (!is_array($fetch_domain_hits))
+ $fetch_domain_hits = [];
+
+ if (!is_array($options)) {
+
+ // falling back on compatibility shim
+ $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
+ $tmp = [];
+
+ for ($i = 0; $i < func_num_args(); $i++) {
+ $tmp[$option_names[$i]] = func_get_arg($i);
+ }
+
+ $options = $tmp;
+
+ /*$options = array(
+ "url" => func_get_arg(0),
+ "type" => @func_get_arg(1),
+ "login" => @func_get_arg(2),
+ "pass" => @func_get_arg(3),
+ "post_query" => @func_get_arg(4),
+ "timeout" => @func_get_arg(5),
+ "timestamp" => @func_get_arg(6),
+ "useragent" => @func_get_arg(7)
+ ); */
+ }
+
+ $url = $options["url"];
+ $type = isset($options["type"]) ? $options["type"] : false;
+ $login = isset($options["login"]) ? $options["login"] : false;
+ $pass = isset($options["pass"]) ? $options["pass"] : false;
+ $post_query = isset($options["post_query"]) ? $options["post_query"] : false;
+ $timeout = isset($options["timeout"]) ? $options["timeout"] : false;
+ $last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
+ $useragent = isset($options["useragent"]) ? $options["useragent"] : false;
+ $followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
+ $max_size = isset($options["max_size"]) ? $options["max_size"] : MAX_DOWNLOAD_FILE_SIZE; // in bytes
+ $http_accept = isset($options["http_accept"]) ? $options["http_accept"] : false;
+ $http_referrer = isset($options["http_referrer"]) ? $options["http_referrer"] : false;
+
+ $url = ltrim($url, ' ');
+ $url = str_replace(' ', '%20', $url);
+
+ $url = UrlHelper::validate($url, true);
+
+ if (!$url) {
+ $fetch_last_error = "Requested URL failed extended validation.";
+ return false;
+ }
+
+ $url_host = parse_url($url, PHP_URL_HOST);
+ $ip_addr = gethostbyname($url_host);
+
+ if (!$ip_addr || strpos($ip_addr, "127.") === 0) {
+ $fetch_last_error = "URL hostname failed to resolve or resolved to a loopback address ($ip_addr)";
+ return false;
+ }
+
+ $fetch_domain_hits[$url_host] += 1;
+
+ /*if ($fetch_domain_hits[$url_host] > MAX_FETCH_REQUESTS_PER_HOST) {
+ user_error("Exceeded fetch request quota for $url_host: " . $fetch_domain_hits[$url_host], E_USER_WARNING);
+ #return false;
+ }*/
+
+ if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
+
+ $fetch_curl_used = true;
+
+ $ch = curl_init($url);
+
+ $curl_http_headers = [];
+
+ if ($last_modified && !$post_query)
+ array_push($curl_http_headers, "If-Modified-Since: $last_modified");
+
+ if ($http_accept)
+ array_push($curl_http_headers, "Accept: " . $http_accept);
+
+ if (count($curl_http_headers) > 0)
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $curl_http_headers);
+
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout ? $timeout : FILE_FETCH_CONNECT_TIMEOUT);
+ curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ? $timeout : FILE_FETCH_TIMEOUT);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("open_basedir") && $followlocation);
+ curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
+ curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_HEADER, true);
+ curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
+ curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent :
+ SELF_USER_AGENT);
+ curl_setopt($ch, CURLOPT_ENCODING, "");
+
+ if ($http_referrer)
+ curl_setopt($ch, CURLOPT_REFERER, $http_referrer);
+
+ if ($max_size) {
+ curl_setopt($ch, CURLOPT_NOPROGRESS, false);
+ curl_setopt($ch, CURLOPT_BUFFERSIZE, 16384); // needed to get 5 arguments in progress function?
+
+ // holy shit closures in php
+ // download & upload are *expected* sizes respectively, could be zero
+ curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($curl_handle, $download_size, $downloaded, $upload_size, $uploaded) use( &$max_size) {
+ Debug::log("[curl progressfunction] $downloaded $max_size", Debug::$LOG_EXTENDED);
+
+ return ($downloaded > $max_size) ? 1 : 0; // if max size is set, abort when exceeding it
+ });
+
+ }
+
+ if (!ini_get("open_basedir")) {
+ curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
+ }
+
+ if (defined('_HTTP_PROXY')) {
+ curl_setopt($ch, CURLOPT_PROXY, _HTTP_PROXY);
+ }
+
+ if ($post_query) {
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $post_query);
+ }
+
+ if ($login && $pass)
+ curl_setopt($ch, CURLOPT_USERPWD, "$login:$pass");
+
+ $ret = @curl_exec($ch);
+
+ $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
+ $headers = explode("\r\n", substr($ret, 0, $headers_length));
+ $contents = substr($ret, $headers_length);
+
+ foreach ($headers as $header) {
+ if (strstr($header, ": ") !== false) {
+ list ($key, $value) = explode(": ", $header);
+
+ if (strtolower($key) == "last-modified") {
+ $fetch_last_modified = $value;
+ }
+ }
+
+ if (substr(strtolower($header), 0, 7) == 'http/1.') {
+ $fetch_last_error_code = (int) substr($header, 9, 3);
+ $fetch_last_error = $header;
+ }
+ }
+
+ if (curl_errno($ch) === 23 || curl_errno($ch) === 61) {
+ curl_setopt($ch, CURLOPT_ENCODING, 'none');
+ $contents = @curl_exec($ch);
+ }
+
+ $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
+
+ $fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
+
+ if (!UrlHelper::validate($fetch_effective_url, true)) {
+ $fetch_last_error = "URL received after redirection failed extended validation.";
+
+ return false;
+ }
+
+ $fetch_effective_ip_addr = gethostbyname(parse_url($fetch_effective_url, PHP_URL_HOST));
+
+ if (!$fetch_effective_ip_addr || strpos($fetch_effective_ip_addr, "127.") === 0) {
+ $fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address ($fetch_effective_ip_addr)";
+
+ return false;
+ }
+
+ $fetch_last_error_code = $http_code;
+
+ if ($http_code != 200 || $type && strpos($fetch_last_content_type, "$type") === false) {
+
+ if (curl_errno($ch) != 0) {
+ $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
+ }
+
+ $fetch_last_error_content = $contents;
+ curl_close($ch);
+ return false;
+ }
+
+ if (!$contents) {
+ $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
+ curl_close($ch);
+ return false;
+ }
+
+ curl_close($ch);
+
+ $is_gzipped = RSSUtils::is_gzipped($contents);
+
+ if ($is_gzipped) {
+ $tmp = @gzdecode($contents);
+
+ if ($tmp) $contents = $tmp;
+ }
+
+ return $contents;
+ } else {
+
+ $fetch_curl_used = false;
+
+ if ($login && $pass){
+ $url_parts = array();
+
+ preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
+
+ $pass = urlencode($pass);
+
+ if ($url_parts[1] && $url_parts[2]) {
+ $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
+ }
+ }
+
+ // TODO: should this support POST requests or not? idk
+
+ $context_options = array(
+ 'http' => array(
+ 'header' => array(
+ 'Connection: close'
+ ),
+ 'method' => 'GET',
+ 'ignore_errors' => true,
+ 'timeout' => $timeout ? $timeout : FILE_FETCH_TIMEOUT,
+ 'protocol_version'=> 1.1)
+ );
+
+ if (!$post_query && $last_modified)
+ array_push($context_options['http']['header'], "If-Modified-Since: $last_modified");
+
+ if ($http_accept)
+ array_push($context_options['http']['header'], "Accept: $http_accept");
+
+ if ($http_referrer)
+ array_push($context_options['http']['header'], "Referer: $http_referrer");
+
+ if (defined('_HTTP_PROXY')) {
+ $context_options['http']['request_fulluri'] = true;
+ $context_options['http']['proxy'] = _HTTP_PROXY;
+ }
+
+ $context = stream_context_create($context_options);
+
+ $old_error = error_get_last();
+
+ $fetch_effective_url = UrlHelper::resolve_redirects($url, $timeout ? $timeout : FILE_FETCH_CONNECT_TIMEOUT);
+
+ if (!UrlHelper::validate($fetch_effective_url, true)) {
+ $fetch_last_error = "URL received after redirection failed extended validation.";
+
+ return false;
+ }
+
+ $fetch_effective_ip_addr = gethostbyname(parse_url($fetch_effective_url, PHP_URL_HOST));
+
+ if (!$fetch_effective_ip_addr || strpos($fetch_effective_ip_addr, "127.") === 0) {
+ $fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address ($fetch_effective_ip_addr)";
+
+ return false;
+ }
+
+ $data = @file_get_contents($url, false, $context);
+
+ if (isset($http_response_header) && is_array($http_response_header)) {
+ foreach ($http_response_header as $header) {
+ if (strstr($header, ": ") !== false) {
+ list ($key, $value) = explode(": ", $header);
+
+ $key = strtolower($key);
+
+ if ($key == 'content-type') {
+ $fetch_last_content_type = $value;
+ // don't abort here b/c there might be more than one
+ // e.g. if we were being redirected -- last one is the right one
+ } else if ($key == 'last-modified') {
+ $fetch_last_modified = $value;
+ } else if ($key == 'location') {
+ $fetch_effective_url = $value;
+ }
+ }
+
+ if (substr(strtolower($header), 0, 7) == 'http/1.') {
+ $fetch_last_error_code = (int) substr($header, 9, 3);
+ $fetch_last_error = $header;
+ }
+ }
+ }
+
+ if ($fetch_last_error_code != 200) {
+ $error = error_get_last();
+
+ if ($error['message'] != $old_error['message']) {
+ $fetch_last_error .= "; " . $error["message"];
+ }
+
+ $fetch_last_error_content = $data;
+
+ return false;
+ }
+
+ $is_gzipped = RSSUtils::is_gzipped($data);
+
+ if ($is_gzipped) {
+ $tmp = @gzdecode($data);
+
+ if ($tmp) $data = $tmp;
+ }
+
+ return $data;
+ }
+ }
+
+}
diff --git a/classes/userhelper.php b/classes/userhelper.php
new file mode 100644
index 000000000..2ae1f7b83
--- /dev/null
+++ b/classes/userhelper.php
@@ -0,0 +1,141 @@
+<?php
+class UserHelper {
+
+ static function authenticate($login, $password, $check_only = false, $service = false) {
+
+ if (!SINGLE_USER_MODE) {
+ $user_id = false;
+ $auth_module = false;
+
+ foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_AUTH_USER) as $plugin) {
+
+ $user_id = (int) $plugin->authenticate($login, $password, $service);
+
+ if ($user_id) {
+ $auth_module = strtolower(get_class($plugin));
+ break;
+ }
+ }
+
+ if ($user_id && !$check_only) {
+
+ session_start();
+ session_regenerate_id(true);
+
+ $_SESSION["uid"] = $user_id;
+ $_SESSION["auth_module"] = $auth_module;
+
+ $pdo = Db::pdo();
+ $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
+ WHERE id = ?");
+ $sth->execute([$user_id]);
+ $row = $sth->fetch();
+
+ $_SESSION["name"] = $row["login"];
+ $_SESSION["access_level"] = $row["access_level"];
+ $_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
+
+ $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
+ $usth->execute([$user_id]);
+
+ $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
+ $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
+ $_SESSION["pwd_hash"] = $row["pwd_hash"];
+
+ Pref_Prefs::initialize_user_prefs($_SESSION["uid"]);
+
+ return true;
+ }
+
+ return false;
+
+ } else {
+
+ $_SESSION["uid"] = 1;
+ $_SESSION["name"] = "admin";
+ $_SESSION["access_level"] = 10;
+
+ $_SESSION["hide_hello"] = true;
+ $_SESSION["hide_logout"] = true;
+
+ $_SESSION["auth_module"] = false;
+
+ if (!$_SESSION["csrf_token"])
+ $_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
+
+ $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
+
+ Pref_Prefs::initialize_user_prefs($_SESSION["uid"]);
+
+ return true;
+ }
+ }
+
+ static function load_user_plugins($owner_uid, $pluginhost = false) {
+
+ if (!$pluginhost) $pluginhost = PluginHost::getInstance();
+
+ if ($owner_uid && SCHEMA_VERSION >= 100 && !$_SESSION["safe_mode"]) {
+ $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
+
+ $pluginhost->load($plugins, PluginHost::KIND_USER, $owner_uid);
+
+ if (get_schema_version() > 100) {
+ $pluginhost->load_data();
+ }
+ }
+ }
+
+ static function login_sequence() {
+ $pdo = Db::pdo();
+
+ if (SINGLE_USER_MODE) {
+ @session_start();
+ UserHelper::authenticate("admin", null);
+ startup_gettext();
+ UserHelper::load_user_plugins($_SESSION["uid"]);
+ } else {
+ if (!validate_session()) $_SESSION["uid"] = false;
+
+ if (!$_SESSION["uid"]) {
+
+ if (AUTH_AUTO_LOGIN && UserHelper::authenticate(null, null)) {
+ $_SESSION["ref_schema_version"] = get_schema_version(true);
+ } else {
+ UserHelper::authenticate(null, null, true);
+ }
+
+ if (!$_SESSION["uid"]) {
+ Pref_Users::logout_user();
+
+ Handler_Public::render_login_form();
+ exit;
+ }
+
+ } else {
+ /* bump login timestamp */
+ $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
+ $sth->execute([$_SESSION['uid']]);
+
+ $_SESSION["last_login_update"] = time();
+ }
+
+ if ($_SESSION["uid"]) {
+ startup_gettext();
+ UserHelper::load_user_plugins($_SESSION["uid"]);
+ }
+ }
+ }
+
+ static function print_user_stylesheet() {
+ $value = get_pref('USER_STYLESHEET');
+
+ if ($value) {
+ print "<style type='text/css' id='user_css_style'>";
+ print str_replace("<br/>", "\n", $value);
+ print "</style>";
+ }
+
+ }
+
+}