summaryrefslogtreecommitdiff
path: root/classes
diff options
context:
space:
mode:
Diffstat (limited to 'classes')
-rwxr-xr-xclasses/api.php88
-rwxr-xr-xclasses/article.php26
-rw-r--r--classes/backend.php86
-rw-r--r--classes/counters.php22
-rwxr-xr-xclasses/db.php9
-rw-r--r--classes/debug.php26
-rw-r--r--classes/digest.php14
-rw-r--r--classes/diskcache.php305
-rwxr-xr-xclasses/feeds.php207
-rw-r--r--classes/handler.php4
-rwxr-xr-xclasses/handler/public.php100
-rw-r--r--classes/labels.php16
-rwxr-xr-xclasses/logger.php4
-rw-r--r--classes/mailer.php2
-rw-r--r--classes/opml.php42
-rwxr-xr-xclasses/pluginhost.php31
-rwxr-xr-xclasses/pref/feeds.php37
-rwxr-xr-xclasses/pref/filters.php57
-rw-r--r--classes/pref/prefs.php126
-rw-r--r--classes/pref/system.php7
-rw-r--r--classes/pref/users.php33
-rwxr-xr-xclasses/rpc.php316
-rwxr-xr-xclasses/rssutils.php433
-rw-r--r--classes/sanitizer.php217
-rw-r--r--classes/templator.php21
-rw-r--r--classes/timehelper.php88
-rw-r--r--classes/urlhelper.php489
-rw-r--r--classes/userhelper.php141
28 files changed, 2318 insertions, 629 deletions
diff --git a/classes/api.php b/classes/api.php
index 339e9eef1..928148b5e 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"));
}
@@ -117,10 +117,10 @@ class API extends Handler {
function getFeeds() {
$cat_id = clean($_REQUEST["cat_id"]);
- $unread_only = API::param_to_bool(clean($_REQUEST["unread_only"]));
+ $unread_only = self::param_to_bool(clean($_REQUEST["unread_only"]));
$limit = (int) clean($_REQUEST["limit"]);
$offset = (int) clean($_REQUEST["offset"]);
- $include_nested = API::param_to_bool(clean($_REQUEST["include_nested"]));
+ $include_nested = self::param_to_bool(clean($_REQUEST["include_nested"]));
$feeds = $this->api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested);
@@ -128,9 +128,9 @@ class API extends Handler {
}
function getCategories() {
- $unread_only = API::param_to_bool(clean($_REQUEST["unread_only"]));
- $enable_nested = API::param_to_bool(clean($_REQUEST["enable_nested"]));
- $include_empty = API::param_to_bool(clean($_REQUEST['include_empty']));
+ $unread_only = self::param_to_bool(clean($_REQUEST["unread_only"]));
+ $enable_nested = self::param_to_bool(clean($_REQUEST["enable_nested"]));
+ $include_empty = self::param_to_bool(clean($_REQUEST['include_empty']));
// TODO do not return empty categories, return Uncategorized and standard virtual cats
@@ -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));
}
}
}
@@ -196,39 +196,25 @@ class API extends Handler {
$offset = (int)clean($_REQUEST["skip"]);
$filter = clean($_REQUEST["filter"]);
- $is_cat = API::param_to_bool(clean($_REQUEST["is_cat"]));
- $show_excerpt = API::param_to_bool(clean($_REQUEST["show_excerpt"]));
- $show_content = API::param_to_bool(clean($_REQUEST["show_content"]));
+ $is_cat = self::param_to_bool(clean($_REQUEST["is_cat"]));
+ $show_excerpt = self::param_to_bool(clean($_REQUEST["show_excerpt"]));
+ $show_content = self::param_to_bool(clean($_REQUEST["show_content"]));
/* all_articles, unread, adaptive, marked, updated */
$view_mode = clean($_REQUEST["view_mode"]);
- $include_attachments = API::param_to_bool(clean($_REQUEST["include_attachments"]));
+ $include_attachments = self::param_to_bool(clean($_REQUEST["include_attachments"]));
$since_id = (int)clean($_REQUEST["since_id"]);
- $include_nested = API::param_to_bool(clean($_REQUEST["include_nested"]));
+ $include_nested = self::param_to_bool(clean($_REQUEST["include_nested"]));
$sanitize_content = !isset($_REQUEST["sanitize"]) ||
- API::param_to_bool($_REQUEST["sanitize"]);
- $force_update = API::param_to_bool(clean($_REQUEST["force_update"]));
- $has_sandbox = API::param_to_bool(clean($_REQUEST["has_sandbox"]));
+ self::param_to_bool($_REQUEST["sanitize"]);
+ $force_update = self::param_to_bool(clean($_REQUEST["force_update"]));
+ $has_sandbox = self::param_to_bool(clean($_REQUEST["has_sandbox"]));
$excerpt_length = (int)clean($_REQUEST["excerpt_length"]);
$check_first_id = (int)clean($_REQUEST["check_first_id"]);
- $include_header = API::param_to_bool(clean($_REQUEST["include_header"]));
+ $include_header = self::param_to_bool(clean($_REQUEST["include_header"]));
$_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 */
@@ -313,7 +299,7 @@ class API extends Handler {
$article_ids = explode(",", clean($_REQUEST["article_id"]));
$sanitize_content = !isset($_REQUEST["sanitize"]) ||
- API::param_to_bool($_REQUEST["sanitize"]);
+ self::param_to_bool($_REQUEST["sanitize"]);
if ($article_ids) {
@@ -342,9 +328,9 @@ class API extends Handler {
"title" => $line["title"],
"link" => $line["link"],
"labels" => Article::get_article_labels($line['id']),
- "unread" => API::param_to_bool($line["unread"]),
- "marked" => API::param_to_bool($line["marked"]),
- "published" => API::param_to_bool($line["published"]),
+ "unread" => self::param_to_bool($line["unread"]),
+ "marked" => self::param_to_bool($line["marked"]),
+ "published" => self::param_to_bool($line["published"]),
"comments" => $line["comments"],
"author" => $line["author"],
"updated" => (int) strtotime($line["updated"]),
@@ -357,9 +343,9 @@ class API extends Handler {
);
if ($sanitize_content) {
- $article["content"] = sanitize(
+ $article["content"] = Sanitizer::sanitize(
$line["content"],
- API::param_to_bool($line['hide_images']),
+ self::param_to_bool($line['hide_images']),
false, $line["site_url"], false, $line["id"]);
} else {
$article["content"] = $line["content"];
@@ -463,7 +449,7 @@ class API extends Handler {
$article_ids = explode(",", clean($_REQUEST["article_ids"]));
$label_id = (int) clean($_REQUEST['label_id']);
- $assign = API::param_to_bool(clean($_REQUEST['assign']));
+ $assign = self::param_to_bool(clean($_REQUEST['assign']));
$label = Labels::find_caption(Labels::feed_to_label_id($label_id), $_SESSION["uid"]);
@@ -670,7 +656,7 @@ class API extends Handler {
if ($row = $sth->fetch()) {
$last_updated = strtotime($row["last_updated"]);
- $cache_images = API::param_to_bool($row["cache_images"]);
+ $cache_images = self::param_to_bool($row["cache_images"]);
if (!$cache_images && time() - $last_updated > 120) {
RSSUtils::update_rss_feed($feed_id, true);
@@ -740,9 +726,9 @@ class API extends Handler {
$headline_row = array(
"id" => (int)$line["id"],
"guid" => $line["guid"],
- "unread" => API::param_to_bool($line["unread"]),
- "marked" => API::param_to_bool($line["marked"]),
- "published" => API::param_to_bool($line["published"]),
+ "unread" => self::param_to_bool($line["unread"]),
+ "marked" => self::param_to_bool($line["marked"]),
+ "published" => self::param_to_bool($line["published"]),
"updated" => (int)strtotime($line["updated"]),
"is_updated" => $is_updated,
"title" => $line["title"],
@@ -762,9 +748,9 @@ 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']),
+ self::param_to_bool($line['hide_images']),
false, $line["site_url"], false, $line["id"]);
} else {
$headline_row["content"] = $line["content"];
@@ -782,7 +768,7 @@ class API extends Handler {
$headline_row["comments_count"] = (int)$line["num_comments"];
$headline_row["comments_link"] = $line["comments"];
- $headline_row["always_display_attachments"] = API::param_to_bool($line["always_display_enclosures"]);
+ $headline_row["always_display_attachments"] = self::param_to_bool($line["always_display_enclosures"]);
$headline_row["author"] = $line["author"];
@@ -841,7 +827,7 @@ class API extends Handler {
}
function getFeedTree() {
- $include_empty = API::param_to_bool(clean($_REQUEST['include_empty']));
+ $include_empty = self::param_to_bool(clean($_REQUEST['include_empty']));
$pf = new Pref_Feeds($_REQUEST);
diff --git a/classes/article.php b/classes/article.php
index 74dbdae53..430109283 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 = ?");
@@ -165,7 +159,7 @@ class Article extends Handler_Protected {
$param = clean($_REQUEST['param']);
- $tags = Article::get_article_tags($param);
+ $tags = self::get_article_tags($param);
$tags_str = join(", ", $tags);
@@ -267,7 +261,7 @@ class Article extends Handler_Protected {
$this->pdo->commit();
- $tags = Article::get_article_tags($id);
+ $tags = self::get_article_tags($id);
$tags_str = $this->format_tags_string($tags, $id);
$tags_str_full = join(", ", $tags);
@@ -350,7 +344,7 @@ class Article extends Handler_Protected {
static function format_article_enclosures($id, $always_display_enclosures,
$article_content, $hide_images = false) {
- $result = Article::get_article_enclosures($id);
+ $result = self::get_article_enclosures($id);
$rv = '';
foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FORMAT_ENCLOSURES) as $plugin) {
@@ -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..be634c52a 100644
--- a/classes/counters.php
+++ b/classes/counters.php
@@ -2,12 +2,12 @@
class Counters {
static function getAllCounters() {
- $data = Counters::getGlobalCounters();
+ $data = self::getGlobalCounters();
- $data = array_merge($data, Counters::getVirtCounters());
- $data = array_merge($data, Counters::getLabelCounters());
- $data = array_merge($data, Counters::getFeedCounters());
- $data = array_merge($data, Counters::getCategoryCounters());
+ $data = array_merge($data, self::getVirtCounters());
+ $data = array_merge($data, self::getLabelCounters());
+ $data = array_merge($data, self::getFeedCounters());
+ $data = array_merge($data, self::getCategoryCounters());
return $data;
}
@@ -23,7 +23,7 @@ class Counters {
$marked = 0;
while ($line = $sth->fetch()) {
- list ($tmp_unread, $tmp_marked) = Counters::getCategoryChildrenCounters($line["id"], $owner_uid);
+ list ($tmp_unread, $tmp_marked) = self::getCategoryChildrenCounters($line["id"], $owner_uid);
$unread += $tmp_unread + Feeds::getCategoryUnread($line["id"], $owner_uid);
$marked += $tmp_marked + Feeds::getCategoryMarked($line["id"], $owner_uid);
@@ -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,
@@ -68,7 +68,7 @@ class Counters {
while ($line = $sth->fetch()) {
if ($line["num_children"] > 0) {
- list ($child_counter, $child_marked_counter) = Counters::getCategoryChildrenCounters($line["id"], $_SESSION["uid"]);
+ list ($child_counter, $child_marked_counter) = self::getCategoryChildrenCounters($line["id"], $_SESSION["uid"]);
} else {
$child_counter = 0;
$child_marked_counter = 0;
@@ -112,7 +112,7 @@ class Counters {
$id = $line["id"];
$last_error = htmlspecialchars($line["last_error"]);
- $last_updated = make_local_datetime($line['last_updated'], false);
+ $last_updated = TimeHelper::make_local_datetime($line['last_updated'], false);
if (Feeds::feedHasIcon($id)) {
$has_img = filemtime(Feeds::getIconFile($id));
@@ -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/debug.php b/classes/debug.php
index c62f0c9f5..3061c6893 100644
--- a/classes/debug.php
+++ b/classes/debug.php
@@ -11,40 +11,40 @@ class Debug {
private static $loglevel = 0;
public static function set_logfile($logfile) {
- Debug::$logfile = $logfile;
+ self::$logfile = $logfile;
}
public static function enabled() {
- return Debug::$enabled;
+ return self::$enabled;
}
public static function set_enabled($enable) {
- Debug::$enabled = $enable;
+ self::$enabled = $enable;
}
public static function set_quiet($quiet) {
- Debug::$quiet = $quiet;
+ self::$quiet = $quiet;
}
public static function set_loglevel($level) {
- Debug::$loglevel = $level;
+ self::$loglevel = $level;
}
public static function get_loglevel() {
- return Debug::$loglevel;
+ return self::$loglevel;
}
public static function log($message, $level = 0) {
- if (!Debug::$enabled || Debug::$loglevel < $level) return false;
+ if (!self::$enabled || self::$loglevel < $level) return false;
$ts = strftime("%H:%M:%S", time());
if (function_exists('posix_getpid')) {
$ts = "$ts/" . posix_getpid();
}
- if (Debug::$logfile) {
- $fp = fopen(Debug::$logfile, 'a+');
+ if (self::$logfile) {
+ $fp = fopen(self::$logfile, 'a+');
if ($fp) {
$locked = false;
@@ -60,7 +60,7 @@ class Debug {
if (!$locked) {
fclose($fp);
- user_error("Unable to lock debugging log file: " . Debug::$logfile, E_USER_WARNING);
+ user_error("Unable to lock debugging log file: " . self::$logfile, E_USER_WARNING);
return;
}
}
@@ -73,14 +73,14 @@ class Debug {
fclose($fp);
- if (Debug::$quiet)
+ if (self::$quiet)
return;
} else {
- user_error("Unable to open debugging log file: " . Debug::$logfile, E_USER_WARNING);
+ user_error("Unable to open debugging log file: " . self::$logfile, E_USER_WARNING);
}
}
print "[$ts] $message\n";
}
-} \ No newline at end of file
+}
diff --git a/classes/digest.php b/classes/digest.php
index c9e9f24e7..5128b4186 100644
--- a/classes/digest.php
+++ b/classes/digest.php
@@ -90,16 +90,14 @@ 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);
+ $local_ts = TimeHelper::convert_timestamp(time(), 'UTC', $user_tz_string);
$tpl->setVariable('CUR_DATE', date('Y/m/d', $local_ts));
$tpl->setVariable('CUR_TIME', date('G:i', $local_ts));
@@ -161,7 +159,7 @@ class Digest
array_push($affected_ids, $line["ref_id"]);
- $updated = make_local_datetime($line['last_updated'], false,
+ $updated = TimeHelper::make_local_datetime($line['last_updated'], false,
$user_id);
if (get_pref('ENABLE_FEED_CATS', $user_id)) {
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..2d41be2e1 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;
}
@@ -204,14 +204,14 @@ class Feeds extends Handler_Protected {
}
$vfeed_group_enabled = get_pref("VFEED_GROUP_BY_FEED") &&
- !(in_array($feed, Feeds::NEVER_GROUP_FEEDS) && !$cat_view);
+ !(in_array($feed, self::NEVER_GROUP_FEEDS) && !$cat_view);
$result = $qfh_ret[0]; // this could be either a PDO query result or a -1 if first id changed
$feed_title = $qfh_ret[1];
$feed_site_url = $qfh_ret[2];
$last_error = $qfh_ret[3];
- $last_updated = strpos($qfh_ret[4], '1970-') === FALSE ?
- make_local_datetime($qfh_ret[4], false) : __("Never");
+ $last_updated = strpos($qfh_ret[4], '1970-') === false ?
+ TimeHelper::make_local_datetime($qfh_ret[4], false) : __("Never");
$highlight_words = $qfh_ret[5];
$reply['first_id'] = $qfh_ret[6];
$reply['is_vfeed'] = $qfh_ret[7];
@@ -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) {
@@ -343,12 +343,12 @@ class Feeds extends Handler_Protected {
}
}
- $line["updated_long"] = make_local_datetime($line["updated"],true);
- $line["updated"] = make_local_datetime($line["updated"], false, false, false, true);
+ $line["updated_long"] = TimeHelper::make_local_datetime($line["updated"],true);
+ $line["updated"] = TimeHelper::make_local_datetime($line["updated"], false, false, false, true);
$line['imported'] = T_sprintf("Imported at %s",
- make_local_datetime($line["date_entered"], false));
+ TimeHelper::make_local_datetime($line["date_entered"], false));
if ($line["tag_cache"])
$tags = explode(",", $line["tag_cache"]);
@@ -357,7 +357,7 @@ class Feeds extends Handler_Protected {
$line["tags_str"] = Article::format_tags_string($tags, $id);
- if (feeds::feedHasIcon($feed_id)) {
+ if (self::feedHasIcon($feed_id)) {
$line['feed_icon'] = "<img class=\"icon\" src=\"".ICONS_URL."/$feed_id.ico\" alt=\"\">";
} else {
$line['feed_icon'] = "<i class='icon-no-feed material-icons'>rss_feed</i>";
@@ -426,7 +426,7 @@ class Feeds extends Handler_Protected {
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
- $last_updated = make_local_datetime($row["last_updated"], false);
+ $last_updated = TimeHelper::make_local_datetime($row["last_updated"], false);
$reply['content'] .= sprintf(__("Feeds last updated at %s"), $last_updated);
@@ -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) = self::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);
@@ -594,7 +580,7 @@ class Feeds extends Handler_Protected {
$sth->execute([$_SESSION['uid']]);
$row = $sth->fetch();
- $last_updated = make_local_datetime($row["last_updated"], false);
+ $last_updated = TimeHelper::make_local_datetime($row["last_updated"], false);
$reply['headlines']['content'] .= sprintf(__("Feeds last updated at %s"), $last_updated);
@@ -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">
@@ -865,7 +851,7 @@ class Feeds extends Handler_Protected {
// fall back in case of no plugins
if (!$search_qpart) {
- list($search_qpart, $search_words) = Feeds::search_to_sql($search[0], $search[1]);
+ list($search_qpart, $search_words) = self::search_to_sql($search[0], $search[1], $owner_uid);
}
} else {
$search_qpart = "true";
@@ -905,7 +891,7 @@ class Feeds extends Handler_Protected {
if ($feed >= 0) {
if ($feed > 0) {
- $children = Feeds::getChildCategories($feed, $owner_uid);
+ $children = self::getChildCategories($feed, $owner_uid);
array_push($children, $feed);
$children = array_map("intval", $children);
@@ -1035,7 +1021,7 @@ class Feeds extends Handler_Protected {
$match_part = "";
if ($is_cat) {
- return Feeds::getCategoryUnread($n_feed, $owner_uid);
+ return self::getCategoryUnread($n_feed, $owner_uid);
} else if ($n_feed == -6) {
return 0;
} else if ($feed != "0" && $n_feed == 0) {
@@ -1081,7 +1067,7 @@ class Feeds extends Handler_Protected {
$label_id = Labels::feed_to_label_id($feed);
- return Feeds::getLabelUnread($label_id, $owner_uid);
+ return self::getLabelUnread($label_id, $owner_uid);
}
if ($match_part) {
@@ -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,8 +1142,8 @@ 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)) {
- $feedUrls = Feeds::get_feeds_from_html($url, $contents);
+ if (mb_strpos($fetch_last_content_type, "html") !== false && self::is_html($contents)) {
+ $feedUrls = self::get_feeds_from_html($url, $contents);
if (count($feedUrls) == 0) {
return array("code" => 3);
@@ -1248,7 +1234,7 @@ class Feeds extends Handler_Protected {
$pdo = Db::pdo();
if ($cat) {
- return Feeds::getCategoryTitle($id);
+ return self::getCategoryTitle($id);
} else if ($id == -1) {
return __("Starred articles");
} else if ($id == -2) {
@@ -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]);
@@ -1360,8 +1346,8 @@ class Feeds extends Handler_Protected {
$unread = 0;
while ($line = $sth->fetch()) {
- $unread += Feeds::getCategoryUnread($line["id"], $owner_uid);
- $unread += Feeds::getCategoryChildrenUnread($line["id"], $owner_uid);
+ $unread += self::getCategoryUnread($line["id"], $owner_uid);
+ $unread += self::getCategoryChildrenUnread($line["id"], $owner_uid);
}
return $unread;
@@ -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]);
@@ -1464,11 +1450,11 @@ class Feeds extends Handler_Protected {
// fall back in case of no plugins
if (!$search_query_part) {
- list($search_query_part, $search_words) = Feeds::search_to_sql($search, $search_language);
+ list($search_query_part, $search_words) = self::search_to_sql($search, $search_language, $owner_uid);
}
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 {
@@ -1502,7 +1488,7 @@ class Feeds extends Handler_Protected {
$unread = getFeedUnread($feed, $cat_view);
if ($cat_view && $feed > 0 && $include_children)
- $unread += Feeds::getCategoryChildrenUnread($feed);
+ $unread += self::getCategoryChildrenUnread($feed);
if ($unread > 0) {
$view_query_part = " unread = true AND ";
@@ -1546,7 +1532,7 @@ class Feeds extends Handler_Protected {
if ($feed > 0) {
if ($include_children) {
# sub-cats
- $subcats = Feeds::getChildCategories($feed, $owner_uid);
+ $subcats = self::getChildCategories($feed, $owner_uid);
array_push($subcats, $feed);
$subcats = array_map("intval", $subcats);
@@ -1665,7 +1651,7 @@ class Feeds extends Handler_Protected {
$feed_title = T_sprintf("Search results: %s", $search);
} else {
if ($cat_view) {
- $feed_title = Feeds::getCategoryTitle($feed);
+ $feed_title = self::getCategoryTitle($feed);
} else {
if (is_numeric($feed) && $feed > 0) {
$ssth = $pdo->prepare("SELECT title,site_url,last_error,last_updated
@@ -1678,7 +1664,7 @@ class Feeds extends Handler_Protected {
$last_error = $row["last_error"];
$last_updated = $row["last_updated"];
} else {
- $feed_title = Feeds::getFeedTitle($feed);
+ $feed_title = self::getFeedTitle($feed);
}
}
}
@@ -1702,7 +1688,7 @@ class Feeds extends Handler_Protected {
// proper override_order applied above
if ($vfeed_query_part && !$ignore_vfeed_group && get_pref('VFEED_GROUP_BY_FEED', $owner_uid)) {
- if (!(in_array($feed, Feeds::NEVER_GROUP_BY_DATE) && !$cat_view)) {
+ if (!(in_array($feed, self::NEVER_GROUP_BY_DATE) && !$cat_view)) {
$yyiw_desc = $order_by == "date_reverse" ? "" : "desc";
$yyiw_order_qpart = "yyiw $yyiw_desc, ";
} else {
@@ -1883,7 +1869,7 @@ class Feeds extends Handler_Protected {
while ($line = $sth->fetch()) {
array_push($rv, $line["parent_cat"]);
- $rv = array_merge($rv, Feeds::getParentCategories($line["parent_cat"], $owner_uid));
+ $rv = array_merge($rv, self::getParentCategories($line["parent_cat"], $owner_uid));
}
return $rv;
@@ -1900,7 +1886,7 @@ class Feeds extends Handler_Protected {
while ($line = $sth->fetch()) {
array_push($rv, $line["id"]);
- $rv = array_merge($rv, Feeds::getChildCategories($line["id"], $owner_uid));
+ $rv = array_merge($rv, self::getChildCategories($line["id"], $owner_uid));
}
return $rv;
@@ -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;
@@ -2096,7 +2032,7 @@ class Feeds extends Handler_Protected {
*/
static function purge_feed($feed_id, $purge_interval) {
- if (!$purge_interval) $purge_interval = Feeds::feed_purge_interval($feed_id);
+ if (!$purge_interval) $purge_interval = self::feed_purge_interval($feed_id);
$pdo = Db::pdo();
@@ -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 = ?");
@@ -2181,7 +2117,7 @@ class Feeds extends Handler_Protected {
}
}
- static function search_to_sql($search, $search_language) {
+ static function search_to_sql($search, $search_language, $owner_uid) {
$keywords = str_getcsv(trim($search), " ");
$query_keywords = array();
@@ -2193,7 +2129,7 @@ class Feeds extends Handler_Protected {
if ($search_language)
$search_language = $pdo->quote(mb_strtolower($search_language));
else
- $search_language = $pdo->quote("english");
+ $search_language = $pdo->quote(mb_strtolower(get_pref('DEFAULT_SEARCH_LANGUAGE', $owner_uid)));
foreach ($keywords as $k) {
if (strpos($k, "-") === 0) {
@@ -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")
@@ -2285,7 +2239,7 @@ class Feeds extends Handler_Protected {
$user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
$orig_ts = strtotime(substr($k, 1));
- $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
+ $k = date("Y-m-d", TimeHelper::convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
//$k = date("Y-m-d", strtotime(substr($k, 1)));
@@ -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..4bd9c06f9 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'>";
@@ -419,7 +414,7 @@ class Handler_Public extends Handler {
$rv .= "<div class='row'>"; # row
//$entry_author = $line["author"] ? " - " . $line["author"] : "";
- $parsed_updated = make_local_datetime($line["updated"], true,
+ $parsed_updated = TimeHelper::make_local_datetime($line["updated"], true,
$owner_uid, true);
$rv .= "<div>".$line['author']."</div>";
@@ -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..1f27ee25c 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]);
@@ -57,7 +57,7 @@ class Labels
$pdo = Db::pdo();
if ($force)
- Labels::clear_cache($id);
+ self::clear_cache($id);
if (!$labels)
$labels = Article::get_article_labels($id);
@@ -82,7 +82,7 @@ class Labels
static function remove_article($id, $label, $owner_uid) {
- $label_id = Labels::find_id($label, $owner_uid);
+ $label_id = self::find_id($label, $owner_uid);
if (!$label_id) return;
@@ -95,12 +95,12 @@ class Labels
$sth->execute([$label_id, $id]);
- Labels::clear_cache($id);
+ self::clear_cache($id);
}
static function add_article($id, $label, $owner_uid) {
- $label_id = Labels::find_id($label, $owner_uid);
+ $label_id = self::find_id($label, $owner_uid);
if (!$label_id) return;
@@ -123,7 +123,7 @@ class Labels
$sth->execute([$label_id, $id]);
}
- Labels::clear_cache($id);
+ self::clear_cache($id);
}
@@ -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/logger.php b/classes/logger.php
index 732f1fd5d..cdc6b240a 100755
--- a/classes/logger.php
+++ b/classes/logger.php
@@ -30,9 +30,9 @@ class Logger {
return false;
}
- function log($string, $context = "") {
+ function log($errno, $errstr, $context = "") {
if ($this->adapter)
- return $this->adapter->log_error(E_USER_NOTICE, $string, '', 0, $context);
+ return $this->adapter->log_error($errno, $errstr, '', 0, $context);
else
return false;
}
diff --git a/classes/mailer.php b/classes/mailer.php
index 2919eec79..16be16523 100644
--- a/classes/mailer.php
+++ b/classes/mailer.php
@@ -20,7 +20,7 @@ class Mailer {
$to_combined = $to_name ? "$to_name <$to_address>" : $to_address;
if (defined('_LOG_SENT_MAIL') && _LOG_SENT_MAIL)
- Logger::get()->log("Sending mail from $from_combined to $to_combined [$subject]: $message");
+ Logger::get()->log(E_USER_NOTICE, "Sending mail from $from_combined to $to_combined [$subject]: $message");
// HOOK_SEND_MAIL plugin instructions:
// 1. return 1 or true if mail is handled
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..3ff658918 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) {
@@ -230,8 +235,8 @@ class PluginHost {
$plugin_api = $plugin->api_version();
- if ($plugin_api < PluginHost::API_VERSION) {
- user_error("Plugin $class is not compatible with current API version (need: " . PluginHost::API_VERSION . ", got: $plugin_api)", E_USER_WARNING);
+ if ($plugin_api < self::API_VERSION) {
+ user_error("Plugin $class is not compatible with current API version (need: " . self::API_VERSION . ", got: $plugin_api)", E_USER_WARNING);
continue;
}
@@ -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..8b9099007 100755
--- a/classes/pref/feeds.php
+++ b/classes/pref/feeds.php
@@ -101,7 +101,7 @@ class Pref_Feeds extends Handler_Protected {
$feed['unread'] = -1;
$feed['error'] = $feed_line['last_error'];
$feed['icon'] = Feeds::getFeedIcon($feed_line['id']);
- $feed['param'] = make_local_datetime(
+ $feed['param'] = TimeHelper::make_local_datetime(
$feed_line['last_updated'], true);
$feed['updates_disabled'] = (int)($feed_line['update_interval'] < 0);
@@ -268,7 +268,7 @@ class Pref_Feeds extends Handler_Protected {
$feed['checkbox'] = false;
$feed['error'] = $feed_line['last_error'];
$feed['icon'] = Feeds::getFeedIcon($feed_line['id']);
- $feed['param'] = make_local_datetime(
+ $feed['param'] = TimeHelper::make_local_datetime(
$feed_line['last_updated'], true);
$feed['unread'] = -1;
$feed['type'] = 'feed';
@@ -303,7 +303,7 @@ class Pref_Feeds extends Handler_Protected {
$feed['checkbox'] = false;
$feed['error'] = $feed_line['last_error'];
$feed['icon'] = Feeds::getFeedIcon($feed_line['id']);
- $feed['param'] = make_local_datetime(
+ $feed['param'] = TimeHelper::make_local_datetime(
$feed_line['last_updated'], true);
$feed['unread'] = -1;
$feed['type'] = 'feed';
@@ -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();\"
@@ -1150,7 +1151,7 @@ class Pref_Feeds extends Handler_Protected {
$ids = explode(",", clean($_REQUEST["ids"]));
foreach ($ids as $id) {
- Pref_Feeds::remove_feed($id, $_SESSION["uid"]);
+ self::remove_feed($id, $_SESSION["uid"]);
}
return;
@@ -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>";
@@ -1476,7 +1478,7 @@ class Pref_Feeds extends Handler_Protected {
htmlspecialchars($line["title"])."</a>";
print "</td><td class='text-muted' align='right'>";
- print make_local_datetime($line['last_article'], false);
+ print TimeHelper::make_local_datetime($line['last_article'], false);
print "</td>";
print "</tr>";
@@ -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..1113f251e 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"];
@@ -304,8 +302,8 @@ class Pref_Filters extends Handler_Protected {
$filter['name'] = $name[0];
$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['last_triggered'] = $line["last_triggered"] ? TimeHelper::make_local_datetime($line["last_triggered"], false) : null;
+ $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..d7b486cbb 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);
@@ -1307,11 +1320,11 @@ class Pref_Prefs extends Handler_Protected {
print "<td>" . htmlspecialchars($row["title"]) . "</td>";
print "<td align='right' class='text-muted'>";
- print make_local_datetime($row['created'], false);
+ print TimeHelper::make_local_datetime($row['created'], false);
print "</td>";
print "<td align='right' class='text-muted'>";
- print make_local_datetime($row['last_used'], false);
+ print TimeHelper::make_local_datetime($row['last_used'], false);
print "</td>";
print "</tr>";
@@ -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/system.php b/classes/pref/system.php
index d0f8a8273..7e9aa44a1 100644
--- a/classes/pref/system.php
+++ b/classes/pref/system.php
@@ -26,7 +26,7 @@ class Pref_System extends Handler_Protected {
function index() {
print "<div dojoType=\"dijit.layout.AccordionContainer\" region=\"center\">";
- print "<div dojoType=\"dijit.layout.AccordionPane\"
+ print "<div dojoType=\"dijit.layout.AccordionPane\"
title=\"<i class='material-icons'>report</i> ".__('Event Log')."\">";
if (LOG_DESTINATION == "sql") {
@@ -66,8 +66,7 @@ class Pref_System extends Handler_Protected {
print "<td class='login'>" . $line["login"] . "</td>";
print "<td class='timestamp'>" .
- make_local_datetime(
- $line["created_at"], false) . "</td>";
+ TimeHelper::make_local_datetime($line["created_at"], false) . "</td>";
print "</tr>";
}
@@ -81,7 +80,7 @@ class Pref_System extends Handler_Protected {
print "</div>";
- print "<div dojoType=\"dijit.layout.AccordionPane\"
+ print "<div dojoType=\"dijit.layout.AccordionPane\"
title=\"<i class='material-icons'>info</i> ".__('PHP Information')."\">";
ob_start();
diff --git a/classes/pref/users.php b/classes/pref/users.php
index 851d4fa9e..5ec7aa2e6 100644
--- a/classes/pref/users.php
+++ b/classes/pref/users.php
@@ -137,10 +137,10 @@ class Pref_Users extends Handler_Protected {
if ($row = $sth->fetch()) {
print "<table width='100%'>";
- $last_login = make_local_datetime(
+ $last_login = TimeHelper::make_local_datetime(
$row["last_login"], true);
- $created = make_local_datetime(
+ $created = TimeHelper::make_local_datetime(
$row["created"], true);
$stored_articles = $row["stored_articles"];
@@ -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 {
@@ -304,7 +304,7 @@ class Pref_Users extends Handler_Protected {
function resetPass() {
$uid = clean($_REQUEST["id"]);
- Pref_Users::resetUserPassword($uid);
+ self::resetUserPassword($uid);
}
function index() {
@@ -399,8 +399,8 @@ class Pref_Users extends Handler_Protected {
print "<tr data-row-id='$uid' onclick='Users.edit($uid)'>";
$line["login"] = htmlspecialchars($line["login"]);
- $line["created"] = make_local_datetime($line["created"], false);
- $line["last_login"] = make_local_datetime($line["last_login"], false);
+ $line["created"] = TimeHelper::make_local_datetime($line["created"], false);
+ $line["last_login"] = TimeHelper::make_local_datetime($line["last_login"], false);
print "<td align='center'><input onclick='Tables.onRowChecked(this); event.stopPropagation();'
dojoType='dijit.form.CheckBox' type='checkbox'></td>";
@@ -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..6b41a51b8 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);
@@ -435,8 +435,10 @@ class RPC extends Handler_Protected {
) OR (
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
@@ -444,8 +446,10 @@ class RPC extends Handler_Protected {
) OR (
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 +459,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();
@@ -508,7 +512,7 @@ class RPC extends Handler_Protected {
}
function updaterandomfeed() {
- RPC::updaterandomfeed_real();
+ self::updaterandomfeed_real();
}
private function markArticlesById($ids, $cmode) {
@@ -572,7 +576,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 +600,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 +618,290 @@ 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;
+
+ $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 errno != 1024 AND $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..2ec24d9be 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);
@@ -24,7 +29,29 @@ class RSSUtils {
$pdo->query("DELETE FROM ttrss_feedbrowser_cache");
}
- static function update_daemon_common($limit = DAEMON_FEED_LIMIT) {
+ static function cleanup_feed_icons() {
+ $pdo = Db::pdo();
+ $sth = $pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ?");
+
+ // check icon files once every CACHE_MAX_DAYS days
+ $icon_files = array_filter(glob(ICONS_DIR . "/*.ico"),
+ function($f) { return filemtime($f) < time() - 86400*CACHE_MAX_DAYS; });
+
+ foreach ($icon_files as $icon) {
+ $feed_id = basename($icon, ".ico");
+
+ $sth->execute([$feed_id]);
+
+ if ($sth->fetch()) {
+ @touch($icon);
+ } else {
+ Debug::log("Removing orphaned feed icon: $icon");
+ unlink($icon);
+ }
+ }
+ }
+
+ static function update_daemon_common($limit = DAEMON_FEED_LIMIT, $options = []) {
$schema_version = get_schema_version();
if ($schema_version != SCHEMA_VERSION) {
@@ -47,33 +74,31 @@ class RSSUtils {
$update_limit_qpart = "AND ((
ttrss_feeds.update_interval = 0
AND ttrss_user_prefs.value != '-1'
- AND ttrss_feeds.last_updated < NOW() - CAST((ttrss_user_prefs.value || ' minutes') AS INTERVAL)
+ AND last_updated < NOW() - CAST((ttrss_user_prefs.value || ' minutes') AS INTERVAL)
) OR (
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_user_prefs.value != '-1')
- OR (last_updated = '1970-01-01 00:00:00'
+ AND last_updated < NOW() - CAST((ttrss_feeds.update_interval || ' minutes') AS INTERVAL)
+ ) OR ((last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL)
+ AND ttrss_feeds.update_interval >= 0
AND ttrss_user_prefs.value != '-1'))";
} else {
$update_limit_qpart = "AND ((
ttrss_feeds.update_interval = 0
AND ttrss_user_prefs.value != '-1'
- AND ttrss_feeds.last_updated < DATE_SUB(NOW(), INTERVAL CONVERT(ttrss_user_prefs.value, SIGNED INTEGER) MINUTE)
+ AND last_updated < DATE_SUB(NOW(), INTERVAL CONVERT(ttrss_user_prefs.value, SIGNED INTEGER) MINUTE)
) OR (
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_user_prefs.value != '-1')
- OR (last_updated = '1970-01-01 00:00:00'
+ AND last_updated < DATE_SUB(NOW(), INTERVAL ttrss_feeds.update_interval MINUTE)
+ ) OR ((last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL)
+ AND ttrss_feeds.update_interval >= 0
AND ttrss_user_prefs.value != '-1'))";
}
// Test if feed is currently being updated by another process.
if (DB_TYPE == "pgsql") {
- $updstart_thresh_qpart = "AND (ttrss_feeds.last_update_started IS NULL OR ttrss_feeds.last_update_started < NOW() - INTERVAL '10 minutes')";
+ $updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < NOW() - INTERVAL '10 minutes')";
} else {
- $updstart_thresh_qpart = "AND (ttrss_feeds.last_update_started IS NULL OR ttrss_feeds.last_update_started < DATE_SUB(NOW(), INTERVAL 10 MINUTE))";
+ $updstart_thresh_qpart = "AND (last_update_started IS NULL OR last_update_started < DATE_SUB(NOW(), INTERVAL 10 MINUTE))";
}
$query_limit = $limit ? sprintf("LIMIT %d", $limit) : "";
@@ -119,7 +144,11 @@ class RSSUtils {
$batch_owners = array();
// since we have the data cached, we can deal with other feeds with the same url
- $usth = $pdo->prepare("SELECT DISTINCT ttrss_feeds.id,last_updated,ttrss_feeds.owner_uid
+ $usth = $pdo->prepare("SELECT
+ DISTINCT ttrss_feeds.id,
+ last_updated,
+ ttrss_feeds.owner_uid,
+ ttrss_feeds.title
FROM ttrss_feeds, ttrss_users, ttrss_user_prefs WHERE
ttrss_user_prefs.owner_uid = ttrss_feeds.owner_uid AND
ttrss_users.id = ttrss_user_prefs.owner_uid AND
@@ -134,31 +163,70 @@ class RSSUtils {
Debug::log("Base feed: $feed");
$usth->execute([$feed]);
- //update_rss_feed($line["id"], true);
if ($tline = $usth->fetch()) {
- Debug::log(" => " . $tline["last_updated"] . ", " . $tline["id"] . " " . $tline["owner_uid"]);
+ Debug::log(sprintf("=> %s (ID: %d, UID: %d), last updated: %s", $tline["title"], $tline["id"], $tline["owner_uid"],
+ $tline["last_updated"] ? $tline["last_updated"] : "never"));
- 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);
- try {
- RSSUtils::update_rss_feed($tline["id"], true, false);
- } catch (PDOException $e) {
- Logger::get()->log_error(E_USER_NOTICE, $e->getMessage(), $e->getFile(), $e->getLine(), $e->getTraceAsString());
+ $quiet = (isset($options["quiet"])) ? "--quiet" : "";
+ $log = function_exists("flock") && isset($options['log']) ? '--log '.$options['log'] : '';
+ $log_level = isset($options['log-level']) ? '--log-level '.$options['log-level'] : '';
+
+ /* shared hosting may have this disabled and it's not strictly required */
+ if (self::function_enabled('passthru')) {
+ $exit_code = 0;
+
+ passthru(PHP_EXECUTABLE . " update.php --update-feed " . $tline["id"] . " --pidlock feed-" . $tline["id"] . " $quiet $log $log_level", $exit_code);
+
+ Debug::log(sprintf("<= %.4f (sec) exit code: %d", microtime(true) - $fstarted, $exit_code));
+
+ // -1 can be caused by a SIGCHLD handler which daemon master process installs (not every setup, apparently)
+ if ($exit_code != 0 && $exit_code != -1) {
+ $esth = $pdo->prepare("SELECT last_error FROM ttrss_feeds WHERE id = ?");
+ $esth->execute([$tline["id"]]);
+
+ if ($erow = $esth->fetch()) {
+ $error_message = $erow["last_error"];
+ } else {
+ $error_message = "N/A";
+ }
+
+ Debug::log("!! Last error: $error_message");
+
+ Logger::get()->log(E_USER_NOTICE,
+ sprintf("Update process for feed %d (%s, owner UID: %d) failed with exit code: %d (%s).",
+ $tline["id"], clean($tline["title"]), $tline["owner_uid"], $exit_code, clean($error_message)));
+ }
+ } else {
try {
- $pdo->rollback();
+ if (!self::update_rss_feed($tline["id"], true)) {
+ global $fetch_last_error;
+
+ Logger::get()->log(E_USER_NOTICE,
+ sprintf("Update request for feed %d (%s, owner UID: %d) failed: %s.",
+ $tline["id"], clean($tline["title"]), $tline["owner_uid"], clean($fetch_last_error)));
+ }
+
+ Debug::log(sprintf("<= %.4f (sec) (not using a separate process)", microtime(true) - $fstarted));
+
} catch (PDOException $e) {
- // it doesn't matter if there wasn't actually anything to rollback, PDO Exception can be
- // thrown outside of an active transaction during feed update
+ Logger::get()->log_error(E_USER_WARNING, $e->getMessage(), $e->getFile(), $e->getLine(), $e->getTraceAsString());
+
+ try {
+ $pdo->rollback();
+ } catch (PDOException $e) {
+ // it doesn't matter if there wasn't actually anything to rollback, PDO Exception can be
+ // thrown outside of an active transaction during feed update
+ }
}
}
- Debug::log(sprintf(" %.4f (sec)", microtime(true) - $fstarted));
-
++$nf;
}
}
@@ -171,7 +239,7 @@ class RSSUtils {
foreach ($batch_owners as $owner_uid) {
Debug::log("Running housekeeping tasks for user $owner_uid...");
- RSSUtils::housekeeping_user($owner_uid);
+ self::housekeeping_user($owner_uid);
}
// Send feed digests by email if needed.
@@ -209,7 +277,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 +327,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();
@@ -281,7 +347,7 @@ class RSSUtils {
// this is not optimal currently as it fetches stuff separately TODO: optimize
if ($title == "[Unknown]" || !$title || !$site_url) {
Debug::log("setting basic feed info for $feed [$title, $site_url]...");
- RSSUtils::set_basic_feed_info($feed);
+ self::set_basic_feed_info($feed);
}
$sth = $pdo->prepare("SELECT id,update_interval,auth_login,
@@ -391,7 +457,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 +467,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) {
@@ -427,18 +497,24 @@ class RSSUtils {
Debug::log("unable to fetch: $fetch_last_error [$fetch_last_error_code]", Debug::$LOG_VERBOSE);
// If-Modified-Since
- if ($fetch_last_error_code != 304) {
- $error_message = $fetch_last_error;
- } else {
+ if ($fetch_last_error_code == 304) {
Debug::log("source claims data not modified, nothing to do.", Debug::$LOG_VERBOSE);
$error_message = "";
- }
- $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_error = ?,
+ $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_error = ?,
+ last_successful_update = NOW(),
last_updated = NOW() WHERE id = ?");
+
+ } else {
+ $error_message = $fetch_last_error;
+
+ $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_error = ?,
+ last_updated = NOW() WHERE id = ?");
+ }
+
$sth->execute([$error_message, $feed]);
- return;
+ return $error_message == "";
}
Debug::log("running HOOK_FEED_FETCHED handlers...", Debug::$LOG_VERBOSE);
@@ -469,7 +545,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);
}
@@ -511,7 +587,7 @@ class RSSUtils {
Debug::log("checking favicon...", Debug::$LOG_VERBOSE);
- RSSUtils::check_feed_favicon($site_url, $feed);
+ self::check_feed_favicon($site_url, $feed);
$favicon_modified_new = @filemtime($favicon_file);
if ($favicon_modified_new > $favicon_modified)
@@ -540,7 +616,7 @@ class RSSUtils {
Debug::log("loading filters & labels...", Debug::$LOG_VERBOSE);
- $filters = RSSUtils::load_filters($feed, $owner_uid);
+ $filters = self::load_filters($feed, $owner_uid);
if (Debug::get_loglevel() >= Debug::$LOG_EXTENDED) {
print_r($filters);
@@ -579,18 +655,18 @@ class RSSUtils {
$entry_guid = strip_tags($item->get_id());
if (!$entry_guid) $entry_guid = strip_tags($item->get_link());
- if (!$entry_guid) $entry_guid = RSSUtils::make_guid_from_title($item->get_title());
+ if (!$entry_guid) $entry_guid = self::make_guid_from_title($item->get_title());
if (!$entry_guid) {
$pdo->commit();
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 +708,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 +724,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] = self::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 +770,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,
@@ -669,7 +778,7 @@ class RSSUtils {
);
$entry_plugin_data = "";
- $entry_current_hash = RSSUtils::calculate_article_hash($article, $pluginhost);
+ $entry_current_hash = self::calculate_article_hash($article, $pluginhost);
Debug::log("article hash: $entry_current_hash [stored=$entry_stored_hash]", Debug::$LOG_VERBOSE);
@@ -715,7 +824,7 @@ class RSSUtils {
foreach ($article as $k => $v) {
// i guess we'll have to take the risk of 4byte unicode labels & tags here
if (is_string($article[$k])) {
- $article[$k] = RSSUtils::strip_utf8mb4($v);
+ $article[$k] = self::strip_utf8mb4($v);
}
}
}
@@ -725,7 +834,7 @@ class RSSUtils {
$matched_rules = [];
$matched_filters = [];
- $article_filters = RSSUtils::get_article_filters($filters, $article["title"],
+ $article_filters = self::get_article_filters($filters, $article["title"],
$article["content"], $article["link"], $article["author"],
$article["tags"], $matched_rules, $matched_filters);
@@ -765,7 +874,7 @@ class RSSUtils {
}
}
- $plugin_filter_names = RSSUtils::find_article_filters($article_filters, "plugin");
+ $plugin_filter_names = self::find_article_filters($article_filters, "plugin");
$plugin_filter_actions = $pluginhost->get_filter_actions();
if (count($plugin_filter_names) > 0) {
@@ -804,6 +913,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();
@@ -825,11 +935,11 @@ class RSSUtils {
Debug::log("force catchup: $entry_force_catchup", Debug::$LOG_VERBOSE);
if ($cache_images)
- RSSUtils::cache_media($entry_content, $site_url);
+ self::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 +984,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;
@@ -886,13 +996,13 @@ class RSSUtils {
$ref_id = $row['id'];
$entry_ref_id = $ref_id;
- if (RSSUtils::find_article_filter($article_filters, "filter")) {
+ if (self::find_article_filter($article_filters, "filter")) {
Debug::log("article is filtered out, nothing to do.", Debug::$LOG_VERBOSE);
$pdo->commit();
continue;
}
- $score = RSSUtils::calculate_article_score($article_filters) + $entry_score_modifier;
+ $score = self::calculate_article_score($article_filters) + $entry_score_modifier;
Debug::log("initial score: $score [including plugin modifier: $entry_score_modifier]", Debug::$LOG_VERBOSE);
@@ -912,7 +1022,7 @@ class RSSUtils {
Debug::log("user record not found, creating...", Debug::$LOG_VERBOSE);
- if ($score >= -500 && !RSSUtils::find_article_filter($article_filters, 'catchup') && !$entry_force_catchup) {
+ if ($score >= -500 && !self::find_article_filter($article_filters, 'catchup') && !$entry_force_catchup) {
$unread = 1;
$last_read_qpart = null;
} else {
@@ -920,13 +1030,13 @@ class RSSUtils {
$last_read_qpart = date("Y-m-d H:i"); // we can't use NOW() here because it gets quoted
}
- if (RSSUtils::find_article_filter($article_filters, 'mark') || $score > 1000) {
+ if (self::find_article_filter($article_filters, 'mark') || $score > 1000) {
$marked = 1;
} else {
$marked = 0;
}
- if (RSSUtils::find_article_filter($article_filters, 'publish')) {
+ if (self::find_article_filter($article_filters, 'publish')) {
$published = 1;
} else {
$published = 0;
@@ -999,7 +1109,7 @@ class RSSUtils {
if ($mark_unread_on_update &&
!$entry_force_catchup &&
- !RSSUtils::find_article_filter($article_filters, 'catchup')) {
+ !self::find_article_filter($article_filters, 'catchup')) {
Debug::log("article updated, marking unread as requested.", Debug::$LOG_VERBOSE);
@@ -1019,38 +1129,11 @@ class RSSUtils {
Debug::log("assigning labels [filters]...", Debug::$LOG_VERBOSE);
- RSSUtils::assign_article_to_label_filters($entry_ref_id, $article_filters,
+ self::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);
+ self::cache_enclosures($enclosures, $site_url);
if (Debug::get_loglevel() >= Debug::$LOG_EXTENDED) {
Debug::log("article enclosures:", Debug::$LOG_VERBOSE);
@@ -1155,8 +1238,11 @@ class RSSUtils {
Feeds::purge_feed($feed, 0);
- $sth = $pdo->prepare("UPDATE ttrss_feeds
- SET last_updated = NOW(), last_unconditional = NOW(), last_error = '' WHERE id = ?");
+ $sth = $pdo->prepare("UPDATE ttrss_feeds SET
+ last_updated = NOW(),
+ last_unconditional = NOW(),
+ last_successful_update = NOW(),
+ last_error = '' WHERE id = ?");
$sth->execute([$feed]);
} else {
@@ -1171,8 +1257,10 @@ class RSSUtils {
}
}
- $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_error = ?,
- last_updated = NOW(), last_unconditional = NOW() WHERE id = ?");
+ $sth = $pdo->prepare("UPDATE ttrss_feeds SET
+ last_error = ?,
+ last_updated = NOW(),
+ last_unconditional = NOW() WHERE id = ?");
$sth->execute([$error_msg, $feed]);
unset($rss);
@@ -1186,6 +1274,7 @@ class RSSUtils {
return true;
}
+ /* TODO: move to DiskCache? */
static function cache_enclosures($enclosures, $site_url) {
$cache = new DiskCache("images");
@@ -1204,7 +1293,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 +1310,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 +1346,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) {
+ self::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 = self::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++) {
+ self::cache_media_url($cache, $matches[$i]["url"], $site_url);
}
}
}
@@ -1343,6 +1447,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)
@@ -1457,7 +1562,7 @@ class RSSUtils {
static function assign_article_to_label_filters($id, $filters, $owner_uid, $article_labels) {
foreach ($filters as $f) {
if ($f["type"] == "label") {
- if (!RSSUtils::labels_contains_caption($article_labels, $f["param"])) {
+ if (!self::labels_contains_caption($article_labels, $f["param"])) {
Labels::add_article($id, $f["param"], $owner_uid);
}
}
@@ -1477,10 +1582,47 @@ class RSSUtils {
$pdo->query("DELETE FROM ttrss_cat_counters_cache");
}
+ static function disable_failed_feeds() {
+ if (defined('DAEMON_UNSUCCESSFUL_DAYS_LIMIT') && DAEMON_UNSUCCESSFUL_DAYS_LIMIT > 0) {
+
+ $pdo = Db::pdo();
+
+ $pdo->beginTransaction();
+
+ $days = (int) DAEMON_UNSUCCESSFUL_DAYS_LIMIT;
+
+ if (DB_TYPE == "pgsql") {
+ $interval_query = "last_successful_update < NOW() - INTERVAL '$days days'";
+ } else if (DB_TYPE == "mysql") {
+ $interval_query = "last_successful_update < DATE_SUB(NOW(), INTERVAL $days DAY)";
+ }
+
+ $sth = $pdo->prepare("SELECT id, title, owner_uid
+ FROM ttrss_feeds
+ WHERE update_interval != -1 AND last_successful_update IS NOT NULL AND $interval_query");
+
+ $sth->execute();
+
+ while ($row = $sth->fetch()) {
+ Logger::get()->log(E_USER_NOTICE,
+ sprintf("Auto disabling feed %d (%s, UID: %d) because it failed to update for %d days.",
+ $row["id"], clean($row["title"]), $row["owner_uid"], DAEMON_UNSUCCESSFUL_DAYS_LIMIT));
+
+ Debug::log(sprintf("Auto-disabling feed %d (failed to update for %d days).", $row["id"], DAEMON_UNSUCCESSFUL_DAYS_LIMIT));
+ }
+
+ $sth = $pdo->prepare("UPDATE ttrss_feeds SET update_interval = -1 WHERE
+ update_interval != -1 AND last_successful_update IS NOT NULL AND $interval_query");
+ $sth->execute();
+
+ $pdo->commit();
+ }
+ }
+
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", "");
}
@@ -1488,13 +1630,15 @@ class RSSUtils {
static function housekeeping_common() {
DiskCache::expire();
- RSSUtils::expire_lock_files();
- RSSUtils::expire_error_log();
- RSSUtils::expire_feed_archive();
- RSSUtils::cleanup_feed_browser();
+ self::expire_lock_files();
+ self::expire_error_log();
+ self::expire_feed_archive();
+ self::cleanup_feed_browser();
+ self::cleanup_feed_icons();
+ self::disable_failed_feeds();
Article::purge_orphans();
- RSSUtils::cleanup_counters_cache();
+ self::cleanup_counters_cache();
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING, "hook_house_keeping", "");
}
@@ -1505,11 +1649,11 @@ class RSSUtils {
$icon_file = ICONS_DIR . "/$feed.ico";
if (!file_exists($icon_file)) {
- $favicon_url = RSSUtils::get_favicon_url($site_url);
+ $favicon_url = self::get_favicon_url($site_url);
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 +1826,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 +1854,37 @@ 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);
+ }
+
+ static function function_enabled($func) {
+ return !in_array($func,
+ explode(',', (string)ini_get('disable_functions')));
+ }
}
diff --git a/classes/sanitizer.php b/classes/sanitizer.php
new file mode 100644
index 000000000..9f3bfada0
--- /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 (!self::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 = self::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/timehelper.php b/classes/timehelper.php
new file mode 100644
index 000000000..ce9e35f3e
--- /dev/null
+++ b/classes/timehelper.php
@@ -0,0 +1,88 @@
+<?php
+class TimeHelper {
+
+ static function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
+ if (!$owner_uid) $owner_uid = $_SESSION['uid'];
+
+ if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
+ return T_sprintf("%d min", date("i", time() + $tz_offset - $timestamp));
+ } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() + $tz_offset)) {
+ $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
+ if (strpos((strtolower($format)), "a") === false)
+ return date("G:i", $timestamp);
+ else
+ return date("g:i a", $timestamp);
+ } else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
+ $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
+ return date($format, $timestamp);
+ } else {
+ $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
+ return date($format, $timestamp);
+ }
+ }
+
+ static function make_local_datetime($timestamp, $long, $owner_uid = false,
+ $no_smart_dt = false, $eta_min = false) {
+
+ if (!$owner_uid) $owner_uid = $_SESSION['uid'];
+ if (!$timestamp) $timestamp = '1970-01-01 0:00';
+
+ global $utc_tz;
+ global $user_tz;
+
+ if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
+
+ $timestamp = substr($timestamp, 0, 19);
+
+ # We store date in UTC internally
+ $dt = new DateTime($timestamp, $utc_tz);
+
+ $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
+
+ if ($user_tz_string != 'Automatic') {
+
+ try {
+ if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
+ } catch (Exception $e) {
+ $user_tz = $utc_tz;
+ }
+
+ $tz_offset = $user_tz->getOffset($dt);
+ } else {
+ $tz_offset = (int) -$_SESSION["clientTzOffset"];
+ }
+
+ $user_timestamp = $dt->format('U') + $tz_offset;
+
+ if (!$no_smart_dt) {
+ return self::smart_date_time($user_timestamp,
+ $tz_offset, $owner_uid, $eta_min);
+ } else {
+ if ($long)
+ $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
+ else
+ $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
+
+ return date($format, $user_timestamp);
+ }
+ }
+
+ static function convert_timestamp($timestamp, $source_tz, $dest_tz) {
+
+ try {
+ $source_tz = new DateTimeZone($source_tz);
+ } catch (Exception $e) {
+ $source_tz = new DateTimeZone('UTC');
+ }
+
+ try {
+ $dest_tz = new DateTimeZone($dest_tz);
+ } catch (Exception $e) {
+ $dest_tz = new DateTimeZone('UTC');
+ }
+
+ $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
+ return $dt->format('U') + $dest_tz->getOffset($dt);
+ }
+
+}
diff --git a/classes/urlhelper.php b/classes/urlhelper.php
new file mode 100644
index 000000000..d7b7d004a
--- /dev/null
+++ b/classes/urlhelper.php
@@ -0,0 +1,489 @@
+<?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 self::validate($rel_url);
+ } else if (strpos($rel_url, "//") === 0) {
+ # protocol-relative URL (rare but they exist)
+ return self::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 self::validate(self::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;
+
+ $tokens = parse_url($url);
+
+ // this isn't really necessary because filter_var(... FILTER_VALIDATE_URL) requires host and scheme
+ // as per https://php.watch/versions/7.3/filter-var-flag-deprecation but it might save time
+ if (!$tokens['host'])
+ return false;
+
+ if (!in_array(strtolower($tokens['scheme']), ['http', 'https']))
+ return false;
+
+ //convert IDNA hostname to punycode if possible
+ if (function_exists("idn_to_ascii")) {
+ if (mb_detect_encoding($tokens['host']) != 'ASCII') {
+ $tokens['host'] = idn_to_ascii($tokens['host']);
+ }
+ }
+
+ // separate set of tokens with urlencoded 'path' because filter_var() rightfully fails on non-latin characters
+ // (used for validation only, we actually request the original URL, in case of urlencode breaking it)
+ $tokens_filter_var = $tokens;
+
+ if ($tokens['path']) {
+ $tokens_filter_var['path'] = implode("/",
+ array_map("rawurlencode",
+ array_map("rawurldecode",
+ explode("/", $tokens['path']))));
+ }
+
+ $url = self::build_url($tokens);
+ $url_filter_var = self::build_url($tokens_filter_var);
+
+ if (filter_var($url_filter_var, FILTER_VALIDATE_URL) === false)
+ 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;
+ }
+
+ 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 = self::rewrite_relative($url, trim(substr($header, strlen('Location:'))));
+
+ return self::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 = self::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 (!self::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 = self::resolve_redirects($url, $timeout ? $timeout : FILE_FETCH_CONNECT_TIMEOUT);
+
+ if (!self::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..fd0b0ac57
--- /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();
+ self::authenticate("admin", null);
+ startup_gettext();
+ self::load_user_plugins($_SESSION["uid"]);
+ } else {
+ if (!validate_session()) $_SESSION["uid"] = false;
+
+ if (!$_SESSION["uid"]) {
+
+ if (AUTH_AUTO_LOGIN && self::authenticate(null, null)) {
+ $_SESSION["ref_schema_version"] = get_schema_version(true);
+ } else {
+ self::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();
+ self::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>";
+ }
+
+ }
+
+}