summaryrefslogtreecommitdiff
path: root/classes
diff options
context:
space:
mode:
Diffstat (limited to 'classes')
-rwxr-xr-xclasses/api.php411
-rw-r--r--classes/config.php135
-rwxr-xr-xclasses/db.php10
-rwxr-xr-xclasses/feeds.php8
-rwxr-xr-xclasses/handler/public.php176
-rwxr-xr-xclasses/logger.php2
-rw-r--r--classes/mailer.php2
-rw-r--r--classes/opml.php6
-rwxr-xr-xclasses/pluginhost.php45
-rwxr-xr-xclasses/pref/feeds.php22
-rw-r--r--classes/pref/prefs.php56
-rw-r--r--classes/pref/system.php4
-rw-r--r--classes/pref/users.php2
-rw-r--r--classes/prefs.php2
-rwxr-xr-xclasses/rpc.php12
-rwxr-xr-xclasses/rssutils.php78
-rw-r--r--classes/urlhelper.php33
-rw-r--r--classes/userhelper.php4
18 files changed, 561 insertions, 447 deletions
diff --git a/classes/api.php b/classes/api.php
index a1ed7968c..5f825e551 100755
--- a/classes/api.php
+++ b/classes/api.php
@@ -1,7 +1,7 @@
<?php
class API extends Handler {
- const API_LEVEL = 16;
+ const API_LEVEL = 17;
const STATUS_OK = 0;
const STATUS_ERR = 1;
@@ -74,7 +74,12 @@ class API extends Handler {
if ($uid = UserHelper::find_user_by_login($login)) {
if (get_pref(Prefs::ENABLE_API_ACCESS, $uid)) {
if (UserHelper::authenticate($login, $password, false, Auth_Base::AUTH_SERVICE_API)) {
+
+ // needed for _get_config()
+ UserHelper::load_user_plugins($_SESSION['uid']);
+
$this->_wrap(self::STATUS_OK, array("session_id" => session_id(),
+ "config" => $this->_get_config(),
"api_level" => self::API_LEVEL));
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_LOGIN_ERROR));
@@ -132,49 +137,48 @@ class API extends Handler {
// TODO do not return empty categories, return Uncategorized and standard virtual cats
- if ($enable_nested)
- $nested_qpart = "parent_cat IS NULL";
- else
- $nested_qpart = "true";
+ $categories = ORM::for_table('ttrss_feed_categories')
+ ->select_many('id', 'title', 'order_id')
+ ->select_many_expr([
+ 'num_feeds' => '(SELECT COUNT(id) FROM ttrss_feeds WHERE ttrss_feed_categories.id IS NOT NULL AND cat_id = ttrss_feed_categories.id)',
+ 'num_cats' => '(SELECT COUNT(id) FROM ttrss_feed_categories AS c2 WHERE c2.parent_cat = ttrss_feed_categories.id)',
+ ])
+ ->where('owner_uid', $_SESSION['uid']);
- $sth = $this->pdo->prepare("SELECT
- id, title, order_id, (SELECT COUNT(id) FROM
- ttrss_feeds WHERE
- ttrss_feed_categories.id IS NOT NULL AND cat_id = ttrss_feed_categories.id) AS num_feeds,
- (SELECT COUNT(id) FROM
- ttrss_feed_categories AS c2 WHERE
- c2.parent_cat = ttrss_feed_categories.id) AS num_cats
- FROM ttrss_feed_categories
- WHERE $nested_qpart AND owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
+ if ($enable_nested) {
+ $categories->where_null('parent_cat');
+ }
- $cats = array();
+ $cats = [];
- while ($line = $sth->fetch()) {
- if ($include_empty || $line["num_feeds"] > 0 || $line["num_cats"] > 0) {
- $unread = getFeedUnread($line["id"], true);
+ foreach ($categories->find_many() as $category) {
+ if ($include_empty || $category->num_feeds > 0 || $category->num_cats > 0) {
+ $unread = getFeedUnread($category->id, true);
if ($enable_nested)
- $unread += Feeds::_get_cat_children_unread($line["id"]);
+ $unread += Feeds::_get_cat_children_unread($category->id);
if ($unread || !$unread_only) {
- array_push($cats, array("id" => (int) $line["id"],
- "title" => $line["title"],
- "unread" => (int) $unread,
- "order_id" => (int) $line["order_id"],
- ));
+ array_push($cats, [
+ 'id' => (int) $category->id,
+ 'title' => $category->title,
+ 'unread' => (int) $unread,
+ 'order_id' => (int) $category->order_id,
+ ]);
}
}
}
- foreach (array(-2,-1,0) as $cat_id) {
+ foreach ([-2,-1,0] as $cat_id) {
if ($include_empty || !$this->_is_cat_empty($cat_id)) {
$unread = getFeedUnread($cat_id, true);
if ($unread || !$unread_only) {
- array_push($cats, array("id" => $cat_id,
- "title" => Feeds::_get_cat_title($cat_id),
- "unread" => (int) $unread));
+ array_push($cats, [
+ 'id' => $cat_id,
+ 'title' => Feeds::_get_cat_title($cat_id),
+ 'unread' => (int) $unread,
+ ]);
}
}
}
@@ -300,60 +304,58 @@ class API extends Handler {
}
function getArticle() {
-
- $article_ids = explode(",", clean($_REQUEST["article_id"]));
- $sanitize_content = !isset($_REQUEST["sanitize"]) ||
- self::_param_to_bool($_REQUEST["sanitize"]);
-
- if (count($article_ids) > 0) {
-
- $article_qmarks = arr_qmarks($article_ids);
-
- $sth = $this->pdo->prepare("SELECT id,guid,title,link,content,feed_id,comments,int_id,
- marked,unread,published,score,note,lang,
- ".SUBSTRING_FOR_DATE."(updated,1,16) as updated,
- author,(SELECT title FROM ttrss_feeds WHERE id = feed_id) AS feed_title,
- (SELECT site_url FROM ttrss_feeds WHERE id = feed_id) AS site_url,
- (SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) AS hide_images
- FROM ttrss_entries,ttrss_user_entries
- WHERE id IN ($article_qmarks) AND ref_id = id AND owner_uid = ?");
-
- $sth->execute(array_merge($article_ids, [$_SESSION['uid']]));
-
- $articles = array();
-
- while ($line = $sth->fetch()) {
-
- $article = array(
- "id" => $line["id"],
- "guid" => $line["guid"],
- "title" => $line["title"],
- "link" => $line["link"],
- "labels" => Article::_get_labels($line['id']),
- "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"]),
- "feed_id" => $line["feed_id"],
- "attachments" => Article::_get_enclosures($line['id']),
- "score" => (int)$line["score"],
- "feed_title" => $line["feed_title"],
- "note" => $line["note"],
- "lang" => $line["lang"]
- );
+ $article_ids = explode(',', clean($_REQUEST['article_id'] ?? ''));
+ $sanitize_content = self::_param_to_bool($_REQUEST['sanitize'] ?? true);
+
+ if (count($article_ids)) {
+ $entries = ORM::for_table('ttrss_entries')
+ ->table_alias('e')
+ ->select_many('e.id', 'e.guid', 'e.title', 'e.link', 'e.author', 'e.content', 'e.lang', 'e.comments',
+ 'ue.feed_id', 'ue.int_id', 'ue.marked', 'ue.unread', 'ue.published', 'ue.score', 'ue.note')
+ ->select_many_expr([
+ 'updated' => SUBSTRING_FOR_DATE.'(updated,1,16)',
+ 'feed_title' => '(SELECT title FROM ttrss_feeds WHERE id = ue.feed_id)',
+ 'site_url' => '(SELECT site_url FROM ttrss_feeds WHERE id = ue.feed_id)',
+ 'hide_images' => '(SELECT hide_images FROM ttrss_feeds WHERE id = feed_id)',
+ ])
+ ->join('ttrss_user_entries', [ 'ue.ref_id', '=', 'e.id'], 'ue')
+ ->where_in('e.id', array_map('intval', $article_ids))
+ ->where('ue.owner_uid', $_SESSION['uid'])
+ ->find_many();
+
+ $articles = [];
+
+ foreach ($entries as $entry) {
+ $article = [
+ 'id' => $entry->id,
+ 'guid' => $entry->guid,
+ 'title' => $entry->title,
+ 'link' => $entry->link,
+ 'labels' => Article::_get_labels($entry->id),
+ 'unread' => self::_param_to_bool($entry->unread),
+ 'marked' => self::_param_to_bool($entry->marked),
+ 'published' => self::_param_to_bool($entry->published),
+ 'comments' => $entry->comments,
+ 'author' => $entry->author,
+ 'updated' => (int) strtotime($entry->updated),
+ 'feed_id' => $entry->feed_id,
+ 'attachments' => Article::_get_enclosures($entry->id),
+ 'score' => (int) $entry->score,
+ 'feed_title' => $entry->feed_title,
+ 'note' => $entry->note,
+ 'lang' => $entry->lang,
+ ];
if ($sanitize_content) {
- $article["content"] = Sanitizer::sanitize(
- $line["content"],
- self::_param_to_bool($line['hide_images']),
- false, $line["site_url"], false, $line["id"]);
+ $article['content'] = Sanitizer::sanitize(
+ $entry->content,
+ self::_param_to_bool($entry->hide_images),
+ false, $entry->site_url, false, $entry->id);
} else {
- $article["content"] = $line["content"];
+ $article['content'] = $entry->content;
}
- $hook_object = ["article" => &$article];
+ $hook_object = ['article' => &$article];
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_API,
function ($result) use (&$article) {
@@ -364,30 +366,33 @@ class API extends Handler {
$article['content'] = DiskCache::rewrite_urls($article['content']);
array_push($articles, $article);
-
}
$this->_wrap(self::STATUS_OK, $articles);
// @phpstan-ignore-next-line
} else {
- $this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
+ $this->_wrap(self::STATUS_ERR, ['error' => self::E_INCORRECT_USAGE]);
}
}
- function getConfig() {
+ private function _get_config() {
$config = [
"icons_dir" => Config::get(Config::ICONS_DIR),
"icons_url" => Config::get(Config::ICONS_URL)
];
$config["daemon_is_running"] = file_is_locked("update_daemon.lock");
+ $config["custom_sort_types"] = $this->_get_custom_sort_types();
- $sth = $this->pdo->prepare("SELECT COUNT(*) AS cf FROM
- ttrss_feeds WHERE owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
- $row = $sth->fetch();
+ $config["num_feeds"] = ORM::for_table('ttrss_feeds')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->count();
- $config["num_feeds"] = $row["cf"];
+ return $config;
+ }
+
+ function getConfig() {
+ $config = $this->_get_config();
$this->_wrap(self::STATUS_OK, $config);
}
@@ -422,36 +427,36 @@ class API extends Handler {
}
function getLabels() {
- $article_id = (int)clean($_REQUEST['article_id']);
+ $article_id = (int)clean($_REQUEST['article_id'] ?? -1);
- $rv = array();
+ $rv = [];
- $sth = $this->pdo->prepare("SELECT id, caption, fg_color, bg_color
- FROM ttrss_labels2
- WHERE owner_uid = ? ORDER BY caption");
- $sth->execute([$_SESSION['uid']]);
+ $labels = ORM::for_table('ttrss_labels2')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->order_by_asc('caption')
+ ->find_many();
if ($article_id)
$article_labels = Article::_get_labels($article_id);
else
- $article_labels = array();
-
- while ($line = $sth->fetch()) {
+ $article_labels = [];
+ foreach ($labels as $label) {
$checked = false;
foreach ($article_labels as $al) {
- if (Labels::feed_to_label_id($al[0]) == $line['id']) {
+ if (Labels::feed_to_label_id($al[0]) == $label->id) {
$checked = true;
break;
}
}
- array_push($rv, array(
- "id" => (int)Labels::label_to_feed_id($line['id']),
- "caption" => $line['caption'],
- "fg_color" => $line['fg_color'],
- "bg_color" => $line['bg_color'],
- "checked" => $checked));
+ array_push($rv, [
+ 'id' => (int) Labels::label_to_feed_id($label->id),
+ 'caption' => $label->caption,
+ 'fg_color' => $label->fg_color,
+ 'bg_color' => $label->bg_color,
+ 'checked' => $checked,
+ ]);
}
$this->_wrap(self::STATUS_OK, $rv);
@@ -512,10 +517,7 @@ class API extends Handler {
}
private static function _api_get_feeds($cat_id, $unread_only, $limit, $offset, $include_nested = false) {
-
- $feeds = array();
-
- $pdo = Db::pdo();
+ $feeds = [];
$limit = (int) $limit;
$offset = (int) $offset;
@@ -528,17 +530,15 @@ class API extends Handler {
$counters = Counters::get_labels();
foreach (array_values($counters) as $cv) {
-
- $unread = $cv["counter"];
+ $unread = $cv['counter'];
if ($unread || !$unread_only) {
-
- $row = array(
- "id" => (int) $cv["id"],
- "title" => $cv["description"],
- "unread" => $cv["counter"],
- "cat_id" => -2,
- );
+ $row = [
+ 'id' => (int) $cv['id'],
+ 'title' => $cv['description'],
+ 'unread' => $cv['counter'],
+ 'cat_id' => -2,
+ ];
array_push($feeds, $row);
}
@@ -548,45 +548,45 @@ class API extends Handler {
/* Virtual feeds */
if ($cat_id == -4 || $cat_id == -1) {
- foreach (array(-1, -2, -3, -4, -6, 0) as $i) {
+ foreach ([-1, -2, -3, -4, -6, 0] as $i) {
$unread = getFeedUnread($i);
if ($unread || !$unread_only) {
$title = Feeds::_get_title($i);
- $row = array(
- "id" => $i,
- "title" => $title,
- "unread" => $unread,
- "cat_id" => -1,
- );
+ $row = [
+ 'id' => $i,
+ 'title' => $title,
+ 'unread' => $unread,
+ 'cat_id' => -1,
+ ];
+
array_push($feeds, $row);
}
-
}
}
/* Child cats */
if ($include_nested && $cat_id) {
- $sth = $pdo->prepare("SELECT
- id, title, order_id FROM ttrss_feed_categories
- WHERE parent_cat = ? AND owner_uid = ? ORDER BY order_id, title");
-
- $sth->execute([$cat_id, $_SESSION['uid']]);
+ $categories = ORM::for_table('ttrss_feed_categories')
+ ->where(['parent_cat' => $cat_id, 'owner_uid' => $_SESSION['uid']])
+ ->order_by_asc('order_id')
+ ->order_by_asc('title')
+ ->find_many();
- while ($line = $sth->fetch()) {
- $unread = getFeedUnread($line["id"], true) +
- Feeds::_get_cat_children_unread($line["id"]);
+ foreach ($categories as $category) {
+ $unread = getFeedUnread($category->id, true) +
+ Feeds::_get_cat_children_unread($category->id);
if ($unread || !$unread_only) {
- $row = array(
- "id" => (int) $line["id"],
- "title" => $line["title"],
- "unread" => $unread,
- "is_cat" => true,
- "order_id" => (int) $line["order_id"]
- );
+ $row = [
+ 'id' => (int) $category->id,
+ 'title' => $category->title,
+ 'unread' => $unread,
+ 'is_cat' => true,
+ 'order_id' => (int) $category->order_id,
+ ];
array_push($feeds, $row);
}
}
@@ -594,51 +594,36 @@ class API extends Handler {
/* Real feeds */
- if ($limit) {
- $limit_qpart = "LIMIT $limit OFFSET $offset";
- } else {
- $limit_qpart = "";
- }
-
/* API only: -3 All feeds, excluding virtual feeds (e.g. Labels and such) */
- if ($cat_id == -4 || $cat_id == -3) {
- $sth = $pdo->prepare("SELECT
- id, feed_url, cat_id, title, order_id, ".
- SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated
- FROM ttrss_feeds WHERE owner_uid = ?
- ORDER BY order_id, title " . $limit_qpart);
- $sth->execute([$_SESSION['uid']]);
-
- } else {
-
- $sth = $pdo->prepare("SELECT
- id, feed_url, cat_id, title, order_id, ".
- SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated
- FROM ttrss_feeds WHERE
- (cat_id = :cat OR (:cat = 0 AND cat_id IS NULL))
- AND owner_uid = :uid
- ORDER BY order_id, title " . $limit_qpart);
- $sth->execute([":uid" => $_SESSION['uid'], ":cat" => $cat_id]);
+ $feeds_obj = ORM::for_table('ttrss_feeds')
+ ->select_many('id', 'feed_url', 'cat_id', 'title', 'order_id')
+ ->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->order_by_asc('order_id')
+ ->order_by_asc('title');
+
+ if ($limit) $feeds_obj->limit($limit);
+ if ($offset) $feeds_obj->offset($offset);
+
+ if ($cat_id != -3 && $cat_id != -4) {
+ $feeds_obj->where_raw('(cat_id = ? OR (? = 0 AND cat_id IS NULL))', [$cat_id, $cat_id]);
}
- while ($line = $sth->fetch()) {
-
- $unread = getFeedUnread($line["id"]);
-
- $has_icon = Feeds::_has_icon($line['id']);
+ foreach ($feeds_obj->find_many() as $feed) {
+ $unread = getFeedUnread($feed->id);
+ $has_icon = Feeds::_has_icon($feed->id);
if ($unread || !$unread_only) {
-
- $row = array(
- "feed_url" => $line["feed_url"],
- "title" => $line["title"],
- "id" => (int)$line["id"],
- "unread" => (int)$unread,
- "has_icon" => $has_icon,
- "cat_id" => (int)$line["cat_id"],
- "last_updated" => (int) strtotime($line["last_updated"]),
- "order_id" => (int) $line["order_id"],
- );
+ $row = [
+ 'feed_url' => $feed->feed_url,
+ 'title' => $feed->title,
+ 'id' => (int) $feed->id,
+ 'unread' => (int) $unread,
+ 'has_icon' => $has_icon,
+ 'cat_id' => (int) $feed->cat_id,
+ 'last_updated' => (int) strtotime($feed->last_updated),
+ 'order_id' => (int) $feed->order_id,
+ ];
array_push($feeds, $row);
}
@@ -653,26 +638,24 @@ class API extends Handler {
$search = "", $include_nested = false, $sanitize_content = true,
$force_update = false, $excerpt_length = 100, $check_first_id = false, $skip_first_id_check = false) {
- $pdo = Db::pdo();
-
if ($force_update && $feed_id > 0 && is_numeric($feed_id)) {
// Update the feed if required with some basic flood control
- $sth = $pdo->prepare(
- "SELECT cache_images,".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated
- FROM ttrss_feeds WHERE id = ?");
- $sth->execute([$feed_id]);
+ $feed = ORM::for_table('ttrss_feeds')
+ ->select_many('id', 'cache_images')
+ ->select_expr(SUBSTRING_FOR_DATE.'(last_updated,1,19)', 'last_updated')
+ ->find_one($feed_id);
- if ($row = $sth->fetch()) {
- $last_updated = strtotime($row["last_updated"]);
- $cache_images = self::_param_to_bool($row["cache_images"]);
+ if ($feed) {
+ $last_updated = strtotime($feed->last_updated);
+ $cache_images = self::_param_to_bool($feed->cache_images);
if (!$cache_images && time() - $last_updated > 120) {
RSSUtils::update_rss_feed($feed_id, true);
} else {
- $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_updated = '1970-01-01', last_update_started = '1970-01-01'
- WHERE id = ?");
- $sth->execute([$feed_id]);
+ $feed->last_updated = '1970-01-01';
+ $feed->last_update_started = '1970-01-01';
+ $feed->save();
}
}
}
@@ -792,7 +775,7 @@ class API extends Handler {
list ($flavor_image, $flavor_stream, $flavor_kind) = Article::_get_image($enclosures,
$line["content"], // unsanitized
- $line["site_url"],
+ $line["site_url"] ?? "", // could be null if archived article
$headline_row);
$headline_row["flavor_image"] = $flavor_image;
@@ -823,15 +806,15 @@ class API extends Handler {
function unsubscribeFeed() {
$feed_id = (int) clean($_REQUEST["feed_id"]);
- $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE
- id = ? AND owner_uid = ?");
- $sth->execute([$feed_id, $_SESSION['uid']]);
+ $feed_exists = ORM::for_table('ttrss_feeds')
+ ->where(['id' => $feed_id, 'owner_uid' => $_SESSION['uid']])
+ ->count();
- if ($row = $sth->fetch()) {
- Pref_Feeds::remove_feed($feed_id, $_SESSION["uid"]);
- $this->_wrap(self::STATUS_OK, array("status" => "OK"));
+ if ($feed_exists) {
+ Pref_Feeds::remove_feed($feed_id, $_SESSION['uid']);
+ $this->_wrap(self::STATUS_OK, ['status' => 'OK']);
} else {
- $this->_wrap(self::STATUS_ERR, array("error" => self::E_OPERATION_FAILED));
+ $this->_wrap(self::STATUS_ERR, ['error' => self::E_OPERATION_FAILED]);
}
}
@@ -864,27 +847,33 @@ class API extends Handler {
// only works for labels or uncategorized for the time being
private function _is_cat_empty($id) {
-
if ($id == -2) {
- $sth = $this->pdo->prepare("SELECT COUNT(id) AS count FROM ttrss_labels2
- WHERE owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
- $row = $sth->fetch();
-
- return $row["count"] == 0;
+ $label_count = ORM::for_table('ttrss_labels2')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->count();
+ return $label_count == 0;
} else if ($id == 0) {
- $sth = $this->pdo->prepare("SELECT COUNT(id) AS count FROM ttrss_feeds
- WHERE cat_id IS NULL AND owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
- $row = $sth->fetch();
-
- return $row["count"] == 0;
+ $uncategorized_count = ORM::for_table('ttrss_feeds')
+ ->where_null('cat_id')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->count();
+ return $uncategorized_count == 0;
}
return false;
}
+ private function _get_custom_sort_types() {
+ $ret = [];
+ PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_HEADLINES_CUSTOM_SORT_MAP, function ($result) use (&$ret) {
+ foreach ($result as $sort_value => $sort_title) {
+ $ret[$sort_value] = $sort_title;
+ }
+ });
+
+ return $ret;
+ }
}
diff --git a/classes/config.php b/classes/config.php
index 567a019c6..4ae4a2407 100644
--- a/classes/config.php
+++ b/classes/config.php
@@ -6,9 +6,22 @@ class Config {
const T_STRING = 2;
const T_INT = 3;
- const SCHEMA_VERSION = 144;
+ const SCHEMA_VERSION = 145;
- // override defaults, defined below in _DEFAULTS[], via environment: DB_TYPE becomes TTRSS_DB_TYPE, etc
+ /* override defaults, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX:
+
+ DB_TYPE becomes:
+
+ .env:
+
+ TTRSS_DB_TYPE=pgsql
+
+ or config.php:
+
+ putenv('TTRSS_DB_TYPE=pgsql');
+
+ etc, etc.
+ */
const DB_TYPE = "DB_TYPE";
const DB_HOST = "DB_HOST";
@@ -16,48 +29,148 @@ class Config {
const DB_NAME = "DB_NAME";
const DB_PASS = "DB_PASS";
const DB_PORT = "DB_PORT";
+ // database credentials
+
const MYSQL_CHARSET = "MYSQL_CHARSET";
+ // connection charset for MySQL. if you have a legacy database and/or experience
+ // garbage unicode characters with this option, try setting it to a blank string.
+
const SELF_URL_PATH = "SELF_URL_PATH";
+ // this should be set to a fully qualified URL used to access
+ // your tt-rss instance over the net, such as: https://example.com/tt-rss/
+ // if your tt-rss instance is behind a reverse proxy, use external URL.
+ // tt-rss will likely help you pick correct value for this on startup
+
const SINGLE_USER_MODE = "SINGLE_USER_MODE";
+ // operate in single user mode, disables all functionality related to
+ // multiple users and authentication. enabling this assumes you have
+ // your tt-rss directory protected by other means (e.g. http auth).
+
const SIMPLE_UPDATE_MODE = "SIMPLE_UPDATE_MODE";
+ // enables fallback update mode where tt-rss tries to update feeds in
+ // background while tt-rss is open in your browser.
+ // if you don't have a lot of feeds and don't want to or can't run
+ // background processes while not running tt-rss, this method is generally
+ // viable to keep your feeds up to date.
+
const PHP_EXECUTABLE = "PHP_EXECUTABLE";
+ // use this PHP CLI executable to start various tasks
+
const LOCK_DIRECTORY = "LOCK_DIRECTORY";
+ // base directory for lockfiles (must be writable)
+
const CACHE_DIR = "CACHE_DIR";
+ // base directory for local cache (must be writable)
+
const ICONS_DIR = "ICONS_DIR";
const ICONS_URL = "ICONS_URL";
+ // directory and URL for feed favicons (directory must be writable)
+
const AUTH_AUTO_CREATE = "AUTH_AUTO_CREATE";
+ // auto create users authenticated via external modules
+
const AUTH_AUTO_LOGIN = "AUTH_AUTO_LOGIN";
+ // auto log in users authenticated via external modules i.e. auth_remote
+
const FORCE_ARTICLE_PURGE = "FORCE_ARTICLE_PURGE";
+ // unconditinally purge all articles older than this amount, in days
+ // overrides user-controlled purge interval
+
const SESSION_COOKIE_LIFETIME = "SESSION_COOKIE_LIFETIME";
+ // default lifetime of a session (e.g. login) cookie. In seconds,
+ // 0 means cookie will be deleted when browser closes.
+
const SMTP_FROM_NAME = "SMTP_FROM_NAME";
const SMTP_FROM_ADDRESS = "SMTP_FROM_ADDRESS";
+ // send email using this name and address
+
const DIGEST_SUBJECT = "DIGEST_SUBJECT";
+ // default subject for email digest
+
const CHECK_FOR_UPDATES = "CHECK_FOR_UPDATES";
+ // enable built-in update checker, both for core code and plugins (using git)
+
const PLUGINS = "PLUGINS";
+ // system plugins enabled for all users, comma separated list, no quotes
+ // keep at least one auth module in there (i.e. auth_internal)
+
const LOG_DESTINATION = "LOG_DESTINATION";
+ // available options: sql (default, event log), syslog, stdout (for debugging)
+
const LOCAL_OVERRIDE_STYLESHEET = "LOCAL_OVERRIDE_STYLESHEET";
+ // link this stylesheet on all pages (if it exists), should be placed in themes.local
+
+ const LOCAL_OVERRIDE_JS = "LOCAL_OVERRIDE_JS";
+ // same but this javascript file (you can use that for polyfills), should be placed in themes.local
+
const DAEMON_MAX_CHILD_RUNTIME = "DAEMON_MAX_CHILD_RUNTIME";
+ // in seconds, terminate update tasks that ran longer than this interval
+
const DAEMON_MAX_JOBS = "DAEMON_MAX_JOBS";
+ // max concurrent update jobs forking update daemon starts
+
const FEED_FETCH_TIMEOUT = "FEED_FETCH_TIMEOUT";
+ // How long to wait for response when requesting feed from a site (seconds)
+
const FEED_FETCH_NO_CACHE_TIMEOUT = "FEED_FETCH_NO_CACHE_TIMEOUT";
+ // Same but not cached
+
const FILE_FETCH_TIMEOUT = "FILE_FETCH_TIMEOUT";
+ // Default timeout when fetching files from remote sites
+
const FILE_FETCH_CONNECT_TIMEOUT = "FILE_FETCH_CONNECT_TIMEOUT";
+ // How long to wait for initial response from website when fetching files from remote sites
+
const DAEMON_UPDATE_LOGIN_LIMIT = "DAEMON_UPDATE_LOGIN_LIMIT";
+ // stop updating feeds if user haven't logged in for X days
+
const DAEMON_FEED_LIMIT = "DAEMON_FEED_LIMIT";
+ // how many feeds to update in one batch
+
const DAEMON_SLEEP_INTERVAL = "DAEMON_SLEEP_INTERVAL";
+ // default sleep interval between feed updates (sec)
+
const MAX_CACHE_FILE_SIZE = "MAX_CACHE_FILE_SIZE";
+ // do not cache files larger than that (bytes)
+
const MAX_DOWNLOAD_FILE_SIZE = "MAX_DOWNLOAD_FILE_SIZE";
+ // do not download files larger than that (bytes)
+
const MAX_FAVICON_FILE_SIZE = "MAX_FAVICON_FILE_SIZE";
+ // max file size for downloaded favicons (bytes)
+
const CACHE_MAX_DAYS = "CACHE_MAX_DAYS";
+ // max age in days for various automatically cached (temporary) files
+
const MAX_CONDITIONAL_INTERVAL = "MAX_CONDITIONAL_INTERVAL";
+ // max interval between forced unconditional updates for servers not complying with http if-modified-since (seconds)
+
const DAEMON_UNSUCCESSFUL_DAYS_LIMIT = "DAEMON_UNSUCCESSFUL_DAYS_LIMIT";
+ // automatically disable updates for feeds which failed to
+ // update for this amount of days; 0 disables
+
const LOG_SENT_MAIL = "LOG_SENT_MAIL";
+ // log all sent emails in the event log
+
const HTTP_PROXY = "HTTP_PROXY";
+ // use HTTP proxy for requests
+
const FORBID_PASSWORD_CHANGES = "FORBID_PASSWORD_CHANGES";
+ // prevent users from changing passwords
+
const SESSION_NAME = "SESSION_NAME";
+ // default session cookie name
+
const CHECK_FOR_PLUGIN_UPDATES = "CHECK_FOR_PLUGIN_UPDATES";
+ // enable plugin update checker (using git)
+
const ENABLE_PLUGIN_INSTALLER = "ENABLE_PLUGIN_INSTALLER";
+ // allow installing first party plugins using plugin installer in prefs
+
+ const AUTH_MIN_INTERVAL = "AUTH_MIN_INTERVAL";
+ // minimum amount of seconds required between authentication attempts
+ // default values for all of the above:
private const _DEFAULTS = [
Config::DB_TYPE => [ "pgsql", Config::T_STRING ],
Config::DB_HOST => [ "db", Config::T_STRING ],
@@ -87,6 +200,8 @@ class Config {
Config::LOG_DESTINATION => [ Logger::LOG_DEST_SQL, Config::T_STRING ],
Config::LOCAL_OVERRIDE_STYLESHEET => [ "local-overrides.css",
Config::T_STRING ],
+ Config::LOCAL_OVERRIDE_JS => [ "local-overrides.js",
+ Config::T_STRING ],
Config::DAEMON_MAX_CHILD_RUNTIME => [ 1800, Config::T_INT ],
Config::DAEMON_MAX_JOBS => [ 2, Config::T_INT ],
Config::FEED_FETCH_TIMEOUT => [ 45, Config::T_INT ],
@@ -108,6 +223,7 @@ class Config {
Config::SESSION_NAME => [ "ttrss_sid", Config::T_STRING ],
Config::CHECK_FOR_PLUGIN_UPDATES => [ "true", Config::T_BOOL ],
Config::ENABLE_PLUGIN_INSTALLER => [ "true", Config::T_BOOL ],
+ Config::AUTH_MIN_INTERVAL => [ 5, Config::T_INT ],
];
private static $instance;
@@ -368,7 +484,8 @@ class Config {
array_push($errors, "Data export cache is not writable (chmod -R 777 ".self::get(Config::CACHE_DIR)."/export)");
}
- if (self::get(Config::SINGLE_USER_MODE) && class_exists("PDO")) {
+ // ttrss_users won't be there on initial startup (before migrations are done)
+ if (!Config::is_migration_needed() && self::get(Config::SINGLE_USER_MODE)) {
if (UserHelper::get_login_by_id(1) != "admin") {
array_push($errors, "SINGLE_USER_MODE is enabled but default admin account (ID: 1) is not found.");
}
@@ -503,4 +620,16 @@ class Config {
private static function format_error($msg) {
return "<div class=\"alert alert-danger\">$msg</div>";
}
+
+ static function get_override_links() {
+ $rv = "";
+
+ $local_css = get_theme_path(self::get(self::LOCAL_OVERRIDE_STYLESHEET));
+ if ($local_css) $rv .= stylesheet_tag($local_css);
+
+ $local_js = get_theme_path(self::get(self::LOCAL_OVERRIDE_JS));
+ if ($local_js) $rv .= javascript_tag($local_js);
+
+ return $rv;
+ }
}
diff --git a/classes/db.php b/classes/db.php
index 008275bca..a09c44628 100755
--- a/classes/db.php
+++ b/classes/db.php
@@ -14,6 +14,9 @@ class Db
ORM::configure('username', Config::get(Config::DB_USER));
ORM::configure('password', Config::get(Config::DB_PASS));
ORM::configure('return_result_sets', true);
+ if (Config::get(Config::DB_TYPE) == "mysql" && Config::get(Config::MYSQL_CHARSET)) {
+ ORM::configure('driver_options', array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES ' . Config::get(Config::MYSQL_CHARSET)));
+ }
}
static function NOW() {
@@ -27,8 +30,13 @@ class Db
public static function get_dsn() {
$db_port = Config::get(Config::DB_PORT) ? ';port=' . Config::get(Config::DB_PORT) : '';
$db_host = Config::get(Config::DB_HOST) ? ';host=' . Config::get(Config::DB_HOST) : '';
+ if (Config::get(Config::DB_TYPE) == "mysql" && Config::get(Config::MYSQL_CHARSET)) {
+ $db_charset = ';charset=' . Config::get(Config::MYSQL_CHARSET);
+ } else {
+ $db_charset = '';
+ }
- return Config::get(Config::DB_TYPE) . ':dbname=' . Config::get(Config::DB_NAME) . $db_host . $db_port;
+ return Config::get(Config::DB_TYPE) . ':dbname=' . Config::get(Config::DB_NAME) . $db_host . $db_port . $db_charset;
}
// this really shouldn't be used unless a separate PDO connection is needed
diff --git a/classes/feeds.php b/classes/feeds.php
index 68d535481..42673ca95 100755
--- a/classes/feeds.php
+++ b/classes/feeds.php
@@ -1751,9 +1751,10 @@ class Feeds extends Handler_Protected {
author, score,
(SELECT count(label_id) FROM ttrss_user_labels2 WHERE article_id = ttrss_entries.id) AS num_labels,
(SELECT count(id) FROM ttrss_enclosures WHERE post_id = ttrss_entries.id) AS num_enclosures
- FROM ttrss_entries, ttrss_user_entries, ttrss_tags, ttrss_feeds
+ FROM ttrss_entries,
+ ttrss_user_entries LEFT JOIN ttrss_feeds ON (ttrss_feeds.id = ttrss_user_entries.feed_id),
+ ttrss_tags
WHERE
- ttrss_feeds.id = ttrss_user_entries.feed_id AND
ref_id = ttrss_entries.id AND
ttrss_user_entries.owner_uid = ".$pdo->quote($owner_uid)." AND
post_int_id = int_id AND
@@ -2054,8 +2055,7 @@ class Feeds extends Handler_Protected {
}
private static function _search_to_sql($search, $search_language, $owner_uid) {
-
- $keywords = str_getcsv(trim($search), " ");
+ $keywords = str_getcsv(preg_replace('/(-?\w+)\:"(\w+)/', '"${1}:${2}', trim($search)), ' ');
$query_keywords = array();
$search_words = array();
$search_query_leftover = array();
diff --git a/classes/handler/public.php b/classes/handler/public.php
index 2de073cc2..4da32e90d 100755
--- a/classes/handler/public.php
+++ b/classes/handler/public.php
@@ -125,7 +125,7 @@ class Handler_Public extends Handler {
$tpl->setVariable('ARTICLE_AUTHOR', htmlspecialchars($line['author']), true);
$tpl->setVariable('ARTICLE_SOURCE_LINK', htmlspecialchars($line['site_url'] ? $line["site_url"] : get_self_url_prefix()), true);
- $tpl->setVariable('ARTICLE_SOURCE_TITLE', htmlspecialchars($line['feed_title'] ? $line['feed_title'] : $feed_title), true);
+ $tpl->setVariable('ARTICLE_SOURCE_TITLE', htmlspecialchars($line['feed_title'] ?? $feed_title), true);
foreach ($line["tags"] as $tag) {
$tpl->setVariable('ARTICLE_CATEGORY', htmlspecialchars($tag), true);
@@ -266,19 +266,20 @@ class Handler_Public extends Handler {
$rv = [];
if ($login) {
- $sth = $this->pdo->prepare("SELECT ttrss_settings_profiles.* FROM ttrss_settings_profiles,ttrss_users
- WHERE ttrss_users.id = ttrss_settings_profiles.owner_uid AND LOWER(login) = LOWER(?) ORDER BY title");
- $sth->execute([$login]);
+ $profiles = ORM::for_table('ttrss_settings_profiles')
+ ->table_alias('p')
+ ->select_many('title' , 'p.id')
+ ->join('ttrss_users', ['owner_uid', '=', 'u.id'], 'u')
+ ->where_raw('LOWER(login) = LOWER(?)', [$login])
+ ->order_by_asc('title')
+ ->find_many();
$rv = [ [ "value" => 0, "label" => __("Default profile") ] ];
- while ($line = $sth->fetch()) {
- $id = $line["id"];
- $title = $line["title"];
-
- array_push($rv, [ "label" => $title, "value" => $id ]);
+ foreach ($profiles as $profile) {
+ array_push($rv, [ "label" => $profile->title, "value" => $profile->id ]);
}
- }
+ }
print json_encode($rv);
}
@@ -312,23 +313,20 @@ class Handler_Public extends Handler {
UserHelper::authenticate("admin", null);
}
- $owner_id = false;
-
if ($key) {
- $sth = $this->pdo->prepare("SELECT owner_uid FROM
- ttrss_access_keys WHERE access_key = ? AND feed_id = ?");
- $sth->execute([$key, $feed]);
-
- if ($row = $sth->fetch())
- $owner_id = $row["owner_uid"];
+ $access_key = ORM::for_table('ttrss_access_keys')
+ ->select('owner_uid')
+ ->where(['access_key' => $key, 'feed_id' => $feed])
+ ->find_one();
+
+ if ($access_key) {
+ $this->generate_syndicated_feed($access_key->owner_uid, $feed, $is_cat, $limit,
+ $offset, $search, $view_mode, $format, $order, $orig_guid, $start_ts);
+ return;
+ }
}
- if ($owner_id) {
- $this->generate_syndicated_feed($owner_id, $feed, $is_cat, $limit,
- $offset, $search, $view_mode, $format, $order, $orig_guid, $start_ts);
- } else {
- header('HTTP/1.1 403 Forbidden');
- }
+ header('HTTP/1.1 403 Forbidden');
}
function updateTask() {
@@ -373,18 +371,13 @@ class Handler_Public extends Handler {
$_SESSION["safe_mode"] = $safe_mode;
if (!empty($_POST["profile"])) {
-
$profile = (int) clean($_POST["profile"]);
- $sth = $this->pdo->prepare("SELECT id FROM ttrss_settings_profiles
- WHERE id = ? AND owner_uid = ?");
- $sth->execute([$profile, $_SESSION['uid']]);
+ $profile_obj = ORM::for_table('ttrss_settings_profiles')
+ ->where(['id' => $profile, 'owner_uid' => $_SESSION['uid']])
+ ->find_one();
- if ($sth->fetch()) {
- $_SESSION["profile"] = $profile;
- } else {
- $_SESSION["profile"] = null;
- }
+ $_SESSION["profile"] = $profile_obj ? $profile : null;
}
} else {
@@ -415,7 +408,7 @@ class Handler_Public extends Handler {
startup_gettext();
session_start();
- @$hash = clean($_REQUEST["hash"]);
+ $hash = clean($_REQUEST["hash"] ?? '');
header('Content-Type: text/html; charset=utf-8');
?>
@@ -448,30 +441,27 @@ class Handler_Public extends Handler {
print "<h1>".__("Password recovery")."</h1>";
print "<div class='content'>";
- @$method = clean($_POST['method']);
+ $method = clean($_POST['method'] ?? '');
if ($hash) {
$login = clean($_REQUEST["login"]);
if ($login) {
- $sth = $this->pdo->prepare("SELECT id, resetpass_token FROM ttrss_users
- WHERE LOWER(login) = LOWER(?)");
- $sth->execute([$login]);
+ $user = ORM::for_table('ttrss_users')
+ ->select('id', 'resetpass_token')
+ ->where_raw('LOWER(login) = LOWER(?)', [$login])
+ ->find_one();
- if ($row = $sth->fetch()) {
- $id = $row["id"];
- $resetpass_token_full = $row["resetpass_token"];
- list($timestamp, $resetpass_token) = explode(":", $resetpass_token_full);
+ if ($user) {
+ list($timestamp, $resetpass_token) = explode(":", $user->resetpass_token);
if ($timestamp && $resetpass_token &&
$timestamp >= time() - 15*60*60 &&
$resetpass_token === $hash) {
+ $user->resetpass_token = null;
+ $user->save();
- $sth = $this->pdo->prepare("UPDATE ttrss_users SET resetpass_token = NULL
- WHERE id = ?");
- $sth->execute([$id]);
-
- UserHelper::reset_password($id, true);
+ UserHelper::reset_password($user->id, true);
print "<p>"."Completed."."</p>";
@@ -520,7 +510,6 @@ class Handler_Public extends Handler {
</form>";
} else if ($method == 'do') {
-
$login = clean($_POST["login"]);
$email = clean($_POST["email"]);
$test = clean($_POST["test"]);
@@ -532,64 +521,51 @@ class Handler_Public extends Handler {
<input type='hidden' name='op' value='forgotpass'>
<button dojoType='dijit.form.Button' type='submit' class='alt-primary'>".__("Go back")."</button>
</form>";
-
} else {
-
// prevent submitting this form multiple times
$_SESSION["pwdreset:testvalue1"] = rand(1, 1000);
$_SESSION["pwdreset:testvalue2"] = rand(1, 1000);
- $sth = $this->pdo->prepare("SELECT id FROM ttrss_users
- WHERE LOWER(login) = LOWER(?) AND email = ?");
- $sth->execute([$login, $email]);
+ $user = ORM::for_table('ttrss_users')
+ ->select('id')
+ ->where_raw('LOWER(login) = LOWER(?)', [$login])
+ ->where('email', $email)
+ ->find_one();
- if ($row = $sth->fetch()) {
+ if ($user) {
print_notice("Password reset instructions are being sent to your email address.");
- $id = $row["id"];
-
- if ($id) {
- $resetpass_token = sha1(get_random_bytes(128));
- $resetpass_link = get_self_url_prefix() . "/public.php?op=forgotpass&hash=" . $resetpass_token .
- "&login=" . urlencode($login);
+ $resetpass_token = sha1(get_random_bytes(128));
+ $resetpass_link = get_self_url_prefix() . "/public.php?op=forgotpass&hash=" . $resetpass_token .
+ "&login=" . urlencode($login);
- $tpl = new Templator();
+ $tpl = new Templator();
- $tpl->readTemplateFromFile("resetpass_link_template.txt");
+ $tpl->readTemplateFromFile("resetpass_link_template.txt");
- $tpl->setVariable('LOGIN', $login);
- $tpl->setVariable('RESETPASS_LINK', $resetpass_link);
- $tpl->setVariable('TTRSS_HOST', Config::get(Config::SELF_URL_PATH));
+ $tpl->setVariable('LOGIN', $login);
+ $tpl->setVariable('RESETPASS_LINK', $resetpass_link);
+ $tpl->setVariable('TTRSS_HOST', Config::get(Config::SELF_URL_PATH));
- $tpl->addBlock('message');
+ $tpl->addBlock('message');
- $message = "";
+ $message = "";
- $tpl->generateOutputToString($message);
+ $tpl->generateOutputToString($message);
- $mailer = new Mailer();
+ $mailer = new Mailer();
- $rc = $mailer->mail(["to_name" => $login,
- "to_address" => $email,
- "subject" => __("[tt-rss] Password reset request"),
- "message" => $message]);
+ $rc = $mailer->mail(["to_name" => $login,
+ "to_address" => $email,
+ "subject" => __("[tt-rss] Password reset request"),
+ "message" => $message]);
- if (!$rc) print_error($mailer->error());
+ if (!$rc) print_error($mailer->error());
- $resetpass_token_full = time() . ":" . $resetpass_token;
-
- $sth = $this->pdo->prepare("UPDATE ttrss_users
- SET resetpass_token = ?
- WHERE LOWER(login) = LOWER(?) AND email = ?");
-
- $sth->execute([$resetpass_token_full, $login, $email]);
-
- } else {
- print_error("User ID not found.");
- }
+ $user->resetpass_token = time() . ":" . $resetpass_token;
+ $user->save();
print "<a href='index.php'>".__("Return to Tiny Tiny RSS")."</a>";
-
} else {
print_error(__("Sorry, login and email combination not found."));
@@ -597,17 +573,14 @@ class Handler_Public extends Handler {
<input type='hidden' name='op' value='forgotpass'>
<button dojoType='dijit.form.Button' type='submit'>".__("Go back")."</button>
</form>";
-
}
}
-
}
print "</div>";
print "</div>";
print "</body>";
print "</html>";
-
}
function dbupdate() {
@@ -638,9 +611,7 @@ class Handler_Public extends Handler {
} ?>
- <?php if (theme_exists(Config::get(Config::LOCAL_OVERRIDE_STYLESHEET))) {
- echo stylesheet_tag(get_theme_path(Config::get(Config::LOCAL_OVERRIDE_STYLESHEET)));
- } ?>
+ <?= Config::get_override_links() ?>
<style type="text/css">
@media (prefers-color-scheme: dark) {
@@ -755,27 +726,6 @@ class Handler_Public extends Handler {
<?php
}
- function publishOpml() {
- $key = clean($_REQUEST["key"]);
- $pdo = Db::pdo();
-
- $sth = $pdo->prepare( "SELECT owner_uid
- FROM ttrss_access_keys WHERE
- access_key = ? AND feed_id = 'OPML:Publish'");
- $sth->execute([$key]);
-
- if ($row = $sth->fetch()) {
- $owner_uid = $row['owner_uid'];
-
- $opml = new OPML($_REQUEST);
- $opml->opml_export("published.opml", $owner_uid, true, false);
-
- } else {
- header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
- echo "File not found.";
- }
- }
-
function cached() {
list ($cache_dir, $filename) = explode("/", $_GET["file"], 2);
diff --git a/classes/logger.php b/classes/logger.php
index f8abb5f84..42ab4452c 100755
--- a/classes/logger.php
+++ b/classes/logger.php
@@ -46,7 +46,7 @@ class Logger {
if ($this->adapter)
return $this->adapter->log_error($errno, $errstr, '', 0, $context);
else
- return false;
+ return user_error($errstr, $errno);
}
private function __clone() {
diff --git a/classes/mailer.php b/classes/mailer.php
index 564338f69..8238904ee 100644
--- a/classes/mailer.php
+++ b/classes/mailer.php
@@ -4,7 +4,7 @@ class Mailer {
function mail($params) {
- $to_name = $params["to_name"];
+ $to_name = $params["to_name"] ?? "";
$to_address = $params["to_address"];
$subject = $params["subject"];
$message = $params["message"];
diff --git a/classes/opml.php b/classes/opml.php
index f8e9f6728..2cfc890fa 100644
--- a/classes/opml.php
+++ b/classes/opml.php
@@ -633,12 +633,6 @@ class OPML extends Handler_Protected {
print "$msg<br/>";
}
- static function get_publish_url(){
- return Config::get_self_url() .
- "/public.php?op=publishOpml&key=" .
- Feeds::_get_access_key('OPML:Publish', false, $_SESSION["uid"]);
- }
-
function get_feed_category($feed_cat, $parent_cat_id = false) {
$parent_cat_id = (int) $parent_cat_id;
diff --git a/classes/pluginhost.php b/classes/pluginhost.php
index 17d1e0c5f..ee4107ae7 100755
--- a/classes/pluginhost.php
+++ b/classes/pluginhost.php
@@ -352,7 +352,7 @@ class PluginHost {
$method = strtolower($method);
if ($this->is_system($sender)) {
- if (!is_array($this->handlers[$handler])) {
+ if (!isset($this->handlers[$handler])) {
$this->handlers[$handler] = array();
}
@@ -469,6 +469,29 @@ class PluginHost {
}
}
+ // same as set(), but sets data to current preference profile
+ function profile_set(Plugin $sender, string $name, $value) {
+ $profile_id = $_SESSION["profile"] ?? null;
+
+ if ($profile_id) {
+ $idx = get_class($sender);
+
+ if (!isset($this->storage[$idx])) {
+ $this->storage[$idx] = [];
+ }
+
+ if (!isset($this->storage[$idx][$profile_id])) {
+ $this->storage[$idx][$profile_id] = [];
+ }
+
+ $this->storage[$idx][$profile_id][$name] = $value;
+
+ $this->save_data(get_class($sender));
+ } else {
+ return $this->set($sender, $name, $value);
+ }
+ }
+
function set(Plugin $sender, string $name, $value) {
$idx = get_class($sender);
@@ -492,6 +515,26 @@ class PluginHost {
$this->save_data(get_class($sender));
}
+ // same as get(), but sets data to current preference profile
+ function profile_get(Plugin $sender, string $name, $default_value = false) {
+ $profile_id = $_SESSION["profile"] ?? null;
+
+ if ($profile_id) {
+ $idx = get_class($sender);
+
+ $this->load_data();
+
+ if (isset($this->storage[$idx][$profile_id][$name])) {
+ return $this->storage[$idx][$profile_id][$name];
+ } else {
+ return $default_value;
+ }
+
+ } else {
+ return $this->get($sender, $name, $default_value);
+ }
+ }
+
function get(Plugin $sender, string $name, $default_value = false) {
$idx = get_class($sender);
diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php
index 788104d38..5f7635736 100755
--- a/classes/pref/feeds.php
+++ b/classes/pref/feeds.php
@@ -1012,17 +1012,6 @@ class Pref_Feeds extends Handler_Protected {
</label>
</form>
- <hr/>
-
- <h2><?= __("Published OPML") ?></h2>
-
- <?= format_notice("Your OPML can be published and then subscribed by anyone who knows the URL below. This won't include your settings nor authenticated feeds.") ?>
-
- <button dojoType='dijit.form.Button' class='alt-primary' onclick="return Helpers.OPML.publish()">
- <?= \Controls\icon("share") ?>
- <?= __('Display published OPML URL') ?>
- </button>
-
<?php
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB_SECTION, "prefFeedsOPML");
}
@@ -1251,17 +1240,6 @@ class Pref_Feeds extends Handler_Protected {
return Feeds::_clear_access_keys($_SESSION['uid']);
}
- function getOPMLKey() {
- print json_encode(["link" => OPML::get_publish_url()]);
- }
-
- function regenOPMLKey() {
- Feeds::_update_access_key('OPML:Publish',
- false, $_SESSION["uid"]);
-
- print json_encode(["link" => OPML::get_publish_url()]);
- }
-
function regenFeedKey() {
$feed_id = clean($_REQUEST['id']);
$is_cat = clean($_REQUEST['is_cat']);
diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php
index 16c41df9d..1eaa99345 100644
--- a/classes/pref/prefs.php
+++ b/classes/pref/prefs.php
@@ -54,6 +54,7 @@ class Pref_Prefs extends Handler_Protected {
'BLOCK_SEPARATOR',
Prefs::COMBINED_DISPLAY_MODE,
Prefs::CDM_EXPANDED,
+ Prefs::CDM_ENABLE_GRID,
'BLOCK_SEPARATOR',
Prefs::CDM_AUTO_CATCHUP,
Prefs::VFEED_GROUP_BY_FEED,
@@ -117,6 +118,7 @@ class Pref_Prefs extends Handler_Protected {
Prefs::HEADLINES_NO_DISTINCT => array(__("Don't enforce DISTINCT headlines"), __("May produce duplicate entries")),
Prefs::DEBUG_HEADLINE_IDS => array(__("Show article and feed IDs"), __("In the headlines buffer")),
Prefs::DISABLE_CONDITIONAL_COUNTERS => array(__("Disable conditional counter updates"), __("May increase server load")),
+ Prefs::CDM_ENABLE_GRID => array(__("Grid view"), __("On wider screens, if always expanded")),
];
// hidden in the main prefs UI (use to hide things that have description set above)
@@ -229,29 +231,29 @@ class Pref_Prefs extends Handler_Protected {
if ($user) {
$user->full_name = clean($_POST['full_name']);
- if ($user->email != $new_email)
+ if ($user->email != $new_email) {
Logger::log(E_USER_NOTICE, "Email address of user ".$user->login." has been changed to ${new_email}.");
- if ($user->email && $user->email != $new_email) {
+ if ($user->email) {
+ $mailer = new Mailer();
- $mailer = new Mailer();
+ $tpl = new Templator();
- $tpl = new Templator();
+ $tpl->readTemplateFromFile("mail_change_template.txt");
- $tpl->readTemplateFromFile("mail_change_template.txt");
+ $tpl->setVariable('LOGIN', $user->login);
+ $tpl->setVariable('NEWMAIL', $new_email);
+ $tpl->setVariable('TTRSS_HOST', Config::get(Config::SELF_URL_PATH));
- $tpl->setVariable('LOGIN', $user->login);
- $tpl->setVariable('NEWMAIL', $new_email);
- $tpl->setVariable('TTRSS_HOST', Config::get(Config::SELF_URL_PATH));
-
- $tpl->addBlock('message');
+ $tpl->addBlock('message');
- $tpl->generateOutputToString($message);
+ $tpl->generateOutputToString($message);
- $mailer->mail(["to_name" => $user->login,
- "to_address" => $user->email,
- "subject" => "[tt-rss] Email address change notification",
- "message" => $message]);
+ $mailer->mail(["to_name" => $user->login,
+ "to_address" => $user->email,
+ "subject" => "[tt-rss] Email address change notification",
+ "message" => $message]);
+ }
$user->email = $new_email;
}
@@ -467,8 +469,8 @@ class Pref_Prefs extends Handler_Protected {
<?= \Controls\hidden_tag("method", "otpenable") ?>
<fieldset>
- <label><?= __("OTP Key:") ?></label>
- <input dojoType='dijit.form.ValidationTextBox' disabled='disabled' value="<?= $otp_secret ?>" style='width : 215px'>
+ <label><?= __("OTP secret:") ?></label>
+ <code><?= $this->format_otp_secret($otp_secret) ?></code>
</fieldset>
<!-- TODO: return JSON from the backend call -->
@@ -494,7 +496,7 @@ class Pref_Prefs extends Handler_Protected {
</fieldset>
<fieldset>
- <label><?= __("One time password:") ?></label>
+ <label><?= __("Verification code:") ?></label>
<input dojoType='dijit.form.ValidationTextBox' autocomplete='off' required='1' name='otp'>
</fieldset>
@@ -1434,10 +1436,10 @@ class Pref_Prefs extends Handler_Protected {
<div class='panel panel-scrollable'>
<table width='100%' id='app-password-list'>
<tr>
- <th width='2%'> </th>
- <th align='left'><?= __("Description") ?></th>
- <th align='right'><?= __("Created") ?></th>
- <th align='right'><?= __("Last used") ?></th>
+ <th class="checkbox"> </th>
+ <th width='50%'><?= __("Description") ?></th>
+ <th><?= __("Created") ?></th>
+ <th><?= __("Last used") ?></th>
</tr>
<?php
@@ -1448,16 +1450,16 @@ class Pref_Prefs extends Handler_Protected {
foreach ($passwords as $pass) { ?>
<tr data-row-id='<?= $pass['id'] ?>'>
- <td align='center'>
+ <td class="checkbox">
<input onclick='Tables.onRowChecked(this)' dojoType='dijit.form.CheckBox' type='checkbox'>
</td>
<td>
<?= htmlspecialchars($pass["title"]) ?>
</td>
- <td align='right' class='text-muted'>
+ <td class='text-muted'>
<?= TimeHelper::make_local_datetime($pass['created'], false) ?>
</td>
- <td align='right' class='text-muted'>
+ <td class='text-muted'>
<?= TimeHelper::make_local_datetime($pass['last_used'], false) ?>
</td>
</tr>
@@ -1516,4 +1518,8 @@ class Pref_Prefs extends Handler_Protected {
}
return "";
}
+
+ private function format_otp_secret($secret) {
+ return implode(" ", str_split($secret, 4));
+ }
}
diff --git a/classes/pref/system.php b/classes/pref/system.php
index c79b5095d..8bebcc7ce 100644
--- a/classes/pref/system.php
+++ b/classes/pref/system.php
@@ -42,10 +42,10 @@ class Pref_System extends Handler_Administrative {
switch ($severity) {
case E_USER_ERROR:
- $errno_values = [ E_ERROR, E_USER_ERROR, E_PARSE ];
+ $errno_values = [ E_ERROR, E_USER_ERROR, E_PARSE, E_COMPILE_ERROR ];
break;
case E_USER_WARNING:
- $errno_values = [ E_ERROR, E_USER_ERROR, E_PARSE, E_WARNING, E_USER_WARNING, E_DEPRECATED, E_USER_DEPRECATED ];
+ $errno_values = [ E_ERROR, E_USER_ERROR, E_PARSE, E_COMPILE_ERROR, E_WARNING, E_USER_WARNING, E_DEPRECATED, E_USER_DEPRECATED ];
break;
}
diff --git a/classes/pref/users.php b/classes/pref/users.php
index 2e3dc4b67..76a879efd 100644
--- a/classes/pref/users.php
+++ b/classes/pref/users.php
@@ -117,7 +117,7 @@ class Pref_Users extends Handler_Administrative {
$user->login = mb_strtolower($login);
$user->access_level = (int) clean($_REQUEST["access_level"]);
$user->email = clean($_REQUEST["email"]);
- $user->otp_enabled = checkbox_to_sql_bool($_REQUEST["otp_enabled"]);
+ $user->otp_enabled = checkbox_to_sql_bool($_REQUEST["otp_enabled"] ?? "");
// force new OTP secret when next enabled
if (Config::get_schema_version() >= 143 && !$user->otp_enabled) {
diff --git a/classes/prefs.php b/classes/prefs.php
index 24f0f7a80..85e7c34db 100644
--- a/classes/prefs.php
+++ b/classes/prefs.php
@@ -60,6 +60,7 @@ class Prefs {
const DEBUG_HEADLINE_IDS = "DEBUG_HEADLINE_IDS";
const DISABLE_CONDITIONAL_COUNTERS = "DISABLE_CONDITIONAL_COUNTERS";
const WIDESCREEN_MODE = "WIDESCREEN_MODE";
+ const CDM_ENABLE_GRID = "CDM_ENABLE_GRID";
private const _DEFAULTS = [
Prefs::PURGE_OLD_DAYS => [ 60, Config::T_INT ],
@@ -120,6 +121,7 @@ class Prefs {
Prefs::DEBUG_HEADLINE_IDS => [ false, Config::T_BOOL ],
Prefs::DISABLE_CONDITIONAL_COUNTERS => [ false, Config::T_BOOL ],
Prefs::WIDESCREEN_MODE => [ false, Config::T_BOOL ],
+ Prefs::CDM_ENABLE_GRID => [ false, Config::T_BOOL ],
];
const _PROFILE_BLACKLIST = [
diff --git a/classes/rpc.php b/classes/rpc.php
index 35125ae04..94b29ec44 100755
--- a/classes/rpc.php
+++ b/classes/rpc.php
@@ -431,7 +431,7 @@ class RPC extends Handler_Protected {
Prefs::ENABLE_FEED_CATS, Prefs::FEEDS_SORT_BY_UNREAD,
Prefs::CONFIRM_FEED_CATCHUP, Prefs::CDM_AUTO_CATCHUP,
Prefs::FRESH_ARTICLE_MAX_AGE, Prefs::HIDE_READ_SHOWS_SPECIAL,
- Prefs::COMBINED_DISPLAY_MODE, Prefs::DEBUG_HEADLINE_IDS] as $param) {
+ Prefs::COMBINED_DISPLAY_MODE, Prefs::DEBUG_HEADLINE_IDS, Prefs::CDM_ENABLE_GRID] as $param) {
$params[strtolower($param)] = (int) get_pref($param);
}
@@ -472,6 +472,9 @@ class RPC extends Handler_Protected {
$params["widescreen"] = (int) get_pref(Prefs::WIDESCREEN_MODE);
$params['simple_update'] = Config::get(Config::SIMPLE_UPDATE_MODE);
$params["icon_indicator_white"] = $this->image_to_base64("images/indicator_white.gif");
+ $params["icon_oval"] = $this->image_to_base64("images/oval.svg");
+ $params["icon_three_dots"] = $this->image_to_base64("images/three-dots.svg");
+ $params["icon_blank"] = $this->image_to_base64("images/blank_icon.gif");
$params["labels"] = Labels::get_all($_SESSION["uid"]);
return $params;
@@ -481,6 +484,8 @@ class RPC extends Handler_Protected {
if (file_exists($filename)) {
$ext = pathinfo($filename, PATHINFO_EXTENSION);
+ if ($ext == "svg") $ext = "svg+xml";
+
return "data:image/$ext;base64," . base64_encode((string)file_get_contents($filename));
} else {
return "";
@@ -603,6 +608,7 @@ class RPC extends Handler_Protected {
"feed_catchup" => __("Mark as read"),
"feed_reverse" => __("Reverse headlines"),
"feed_toggle_vgroup" => __("Toggle headline grouping"),
+ "feed_toggle_grid" => __("Toggle grid view"),
"feed_debug_update" => __("Debug feed update"),
"feed_debug_viewfeed" => __("Debug viewfeed()"),
"catchup_all" => __("Mark all feeds as read"),
@@ -663,6 +669,7 @@ class RPC extends Handler_Protected {
"a e" => "toggle_full_text",
"e" => "email_article",
"a q" => "close_article",
+ "a s" => "article_span_grid",
"a a" => "select_all",
"a u" => "select_unread",
"a U" => "select_marked",
@@ -676,8 +683,9 @@ class RPC extends Handler_Protected {
"f q" => "feed_catchup",
"f x" => "feed_reverse",
"f g" => "feed_toggle_vgroup",
+ "f G" => "feed_toggle_grid",
"f D" => "feed_debug_update",
- "f G" => "feed_debug_viewfeed",
+ "f %" => "feed_debug_viewfeed",
"f C" => "toggle_combined_mode",
"f c" => "toggle_cdm_expanded",
"Q" => "catchup_all",
diff --git a/classes/rssutils.php b/classes/rssutils.php
index e6bf08ab1..216792a0e 100755
--- a/classes/rssutils.php
+++ b/classes/rssutils.php
@@ -269,40 +269,33 @@ class RSSUtils {
return $nf;
}
- /** this is used when subscribing; TODO: update to ORM */
- static function update_basic_info(int $feed) {
-
- $pdo = Db::pdo();
-
- $sth = $pdo->prepare("SELECT owner_uid,feed_url,auth_pass,auth_login
- FROM ttrss_feeds WHERE id = ?");
- $sth->execute([$feed]);
-
- if ($row = $sth->fetch()) {
-
- $owner_uid = $row["owner_uid"];
- $auth_login = $row["auth_login"];
- $auth_pass = $row["auth_pass"];
- $fetch_url = $row["feed_url"];
+ /** this is used when subscribing */
+ static function update_basic_info(int $feed_id) {
+ $feed = ORM::for_table('ttrss_feeds')
+ ->select_many('id', 'owner_uid', 'feed_url', 'auth_pass', 'auth_login', 'title', 'site_url')
+ ->find_one($feed_id);
+ if ($feed) {
$pluginhost = new PluginHost();
- $user_plugins = get_pref(Prefs::_ENABLED_PLUGINS, $owner_uid);
+ $user_plugins = get_pref(Prefs::_ENABLED_PLUGINS, $feed->owner_uid);
$pluginhost->load(Config::get(Config::PLUGINS), PluginHost::KIND_ALL);
- $pluginhost->load((string)$user_plugins, PluginHost::KIND_USER, $owner_uid);
+ $pluginhost->load((string)$user_plugins, PluginHost::KIND_USER, $feed->owner_uid);
//$pluginhost->load_data();
$basic_info = [];
$pluginhost->run_hooks_callback(PluginHost::HOOK_FEED_BASIC_INFO, function ($result) use (&$basic_info) {
$basic_info = $result;
- }, $basic_info, $fetch_url, $owner_uid, $feed, $auth_login, $auth_pass);
+ }, $basic_info, $feed->feed_url, $feed->owner_uid, $feed_id, $feed->auth_login, $feed->auth_pass);
if (!$basic_info) {
- $feed_data = UrlHelper::fetch($fetch_url, false,
- $auth_login, $auth_pass, false,
- Config::get(Config::FEED_FETCH_TIMEOUT),
- 0);
+ $feed_data = UrlHelper::fetch([
+ 'url' => $feed->feed_url,
+ 'login' => $feed->auth_login,
+ 'pass' => $feed->auth_pass,
+ 'timeout' => Config::get(Config::FEED_FETCH_TIMEOUT),
+ ]);
$feed_data = trim($feed_data);
@@ -310,36 +303,23 @@ class RSSUtils {
$rss->init();
if (!$rss->error()) {
- $basic_info = array(
+ $basic_info = [
'title' => mb_substr(clean($rss->get_title()), 0, 199),
- 'site_url' => mb_substr(rewrite_relative_url($fetch_url, clean($rss->get_link())), 0, 245)
- );
+ 'site_url' => mb_substr(UrlHelper::rewrite_relative($feed->feed_url, clean($rss->get_link())), 0, 245),
+ ];
}
}
if ($basic_info && is_array($basic_info)) {
- $sth = $pdo->prepare("SELECT title, site_url FROM ttrss_feeds WHERE id = ?");
- $sth->execute([$feed]);
-
- if ($row = $sth->fetch()) {
-
- $registered_title = $row["title"];
- $orig_site_url = $row["site_url"];
-
- if ($basic_info['title'] && (!$registered_title || $registered_title == "[Unknown]")) {
-
- $sth = $pdo->prepare("UPDATE ttrss_feeds SET
- title = ? WHERE id = ?");
- $sth->execute([$basic_info['title'], $feed]);
- }
-
- if ($basic_info['site_url'] && $orig_site_url != $basic_info['site_url']) {
- $sth = $pdo->prepare("UPDATE ttrss_feeds SET
- site_url = ? WHERE id = ?");
- $sth->execute([$basic_info['site_url'], $feed]);
- }
+ if (!empty($basic_info['title']) && (!$feed->title || $feed->title == '[Unknown]')) {
+ $feed->title = $basic_info['title'];
+ }
+ if (!empty($basic_info['site_url']) && $feed->site_url != $basic_info['site_url']) {
+ $feed->site_url = $basic_info['site_url'];
}
+
+ $feed->save();
}
}
}
@@ -1422,8 +1402,8 @@ class RSSUtils {
$matches = array();
foreach ($filters as $filter) {
- $match_any_rule = $filter["match_any_rule"];
- $inverse = $filter["inverse"];
+ $match_any_rule = $filter["match_any_rule"] ?? false;
+ $inverse = $filter["inverse"] ?? false;
$filter_match = false;
$last_processed_rule = false;
@@ -1431,7 +1411,7 @@ class RSSUtils {
$match = false;
$reg_exp = str_replace('/', '\/', (string)$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"];
+ $rule_inverse = $rule["inverse"] ?? false;
$last_processed_rule = $rule;
if (empty($reg_exp))
@@ -1879,6 +1859,6 @@ class RSSUtils {
static function function_enabled($func) {
return !in_array($func,
- explode(',', (string)ini_get('disable_functions')));
+ explode(',', str_replace(" ", "", ini_get('disable_functions'))));
}
}
diff --git a/classes/urlhelper.php b/classes/urlhelper.php
index 55d5d1e6a..edfb2ad73 100644
--- a/classes/urlhelper.php
+++ b/classes/urlhelper.php
@@ -271,10 +271,15 @@ class UrlHelper {
// 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);
+ curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($curl_handle, $download_size, $downloaded, $upload_size, $uploaded) use(&$max_size, $url) {
+ //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 ($downloaded > $max_size) {
+ Debug::log("curl: reached max size of $max_size bytes requesting $url, aborting.", Debug::LOG_VERBOSE);
+ return 1;
+ }
+
+ return 0;
});
}
@@ -482,4 +487,26 @@ class UrlHelper {
}
}
+ public static function url_to_youtube_vid($url) {
+ $url = str_replace("youtube.com", "youtube-nocookie.com", $url);
+
+ $regexps = [
+ "/\/\/www\.youtube-nocookie\.com\/v\/([\w-]+)/",
+ "/\/\/www\.youtube-nocookie\.com\/embed\/([\w-]+)/",
+ "/\/\/www\.youtube-nocookie\.com\/watch?v=([\w-]+)/",
+ "/\/\/youtu.be\/([\w-]+)/",
+ ];
+
+ foreach ($regexps as $re) {
+ $matches = [];
+
+ if (preg_match($re, $url, $matches)) {
+ return $matches[1];
+ }
+ }
+
+ return false;
+ }
+
+
}
diff --git a/classes/userhelper.php b/classes/userhelper.php
index ce26e6c71..1cdd320a1 100644
--- a/classes/userhelper.php
+++ b/classes/userhelper.php
@@ -75,7 +75,7 @@ class UserHelper {
$_SESSION["auth_module"] = false;
- if (!$_SESSION["csrf_token"])
+ if (empty($_SESSION["csrf_token"]))
$_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
$_SESSION["ip_address"] = UserHelper::get_user_ip();
@@ -299,7 +299,7 @@ class UserHelper {
if ($user->otp_enabled) {
$user->otp_secret = $salt_based_secret;
} else {
- $user->otp_secret = bin2hex(get_random_bytes(6));
+ $user->otp_secret = bin2hex(get_random_bytes(10));
}
$user->save();