diff options
Diffstat (limited to 'classes')
-rwxr-xr-x | classes/api.php | 411 | ||||
-rw-r--r-- | classes/config.php | 135 | ||||
-rwxr-xr-x | classes/db.php | 10 | ||||
-rwxr-xr-x | classes/feeds.php | 8 | ||||
-rwxr-xr-x | classes/handler/public.php | 176 | ||||
-rwxr-xr-x | classes/logger.php | 2 | ||||
-rw-r--r-- | classes/mailer.php | 2 | ||||
-rw-r--r-- | classes/opml.php | 6 | ||||
-rwxr-xr-x | classes/pluginhost.php | 45 | ||||
-rwxr-xr-x | classes/pref/feeds.php | 22 | ||||
-rw-r--r-- | classes/pref/prefs.php | 56 | ||||
-rw-r--r-- | classes/pref/system.php | 4 | ||||
-rw-r--r-- | classes/pref/users.php | 2 | ||||
-rw-r--r-- | classes/prefs.php | 2 | ||||
-rwxr-xr-x | classes/rpc.php | 12 | ||||
-rwxr-xr-x | classes/rssutils.php | 78 | ||||
-rw-r--r-- | classes/urlhelper.php | 33 | ||||
-rw-r--r-- | classes/userhelper.php | 4 |
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(); |