summaryrefslogtreecommitdiff
path: root/classes
diff options
context:
space:
mode:
Diffstat (limited to 'classes')
-rwxr-xr-xclasses/api.php23
-rwxr-xr-xclasses/article.php118
-rw-r--r--classes/auth/base.php15
-rw-r--r--classes/config.php342
-rw-r--r--classes/counters.php60
-rwxr-xr-xclasses/db.php33
-rw-r--r--classes/db/migrations.php198
-rw-r--r--classes/dbupdater.php82
-rw-r--r--classes/debug.php24
-rw-r--r--classes/digest.php20
-rw-r--r--classes/diskcache.php2
-rw-r--r--classes/errors.php5
-rw-r--r--classes/feeditem.php2
-rwxr-xr-xclasses/feeditem/atom.php4
-rwxr-xr-xclasses/feeditem/common.php2
-rwxr-xr-xclasses/feeditem/rss.php4
-rwxr-xr-xclasses/feeds.php393
-rwxr-xr-xclasses/handler/public.php184
-rwxr-xr-xclasses/logger.php10
-rwxr-xr-xclasses/logger/sql.php31
-rw-r--r--classes/mailer.php22
-rw-r--r--classes/opml.php13
-rw-r--r--classes/plugin.php12
-rwxr-xr-xclasses/pluginhost.php75
-rwxr-xr-xclasses/pref/feeds.php253
-rwxr-xr-xclasses/pref/filters.php4
-rw-r--r--classes/pref/labels.php12
-rw-r--r--classes/pref/prefs.php920
-rw-r--r--classes/pref/system.php74
-rw-r--r--classes/pref/users.php151
-rw-r--r--classes/prefs.php6
-rwxr-xr-xclasses/rpc.php70
-rwxr-xr-xclasses/rssutils.php557
-rw-r--r--classes/sanitizer.php10
-rw-r--r--classes/urlhelper.php102
-rw-r--r--classes/userhelper.php250
36 files changed, 2450 insertions, 1633 deletions
diff --git a/classes/api.php b/classes/api.php
index a0ee773c1..a1ed7968c 100755
--- a/classes/api.php
+++ b/classes/api.php
@@ -1,7 +1,7 @@
<?php
class API extends Handler {
- const API_LEVEL = 15;
+ const API_LEVEL = 16;
const STATUS_OK = 0;
const STATUS_ERR = 1;
@@ -49,7 +49,7 @@ class API extends Handler {
}
function getVersion() {
- $rv = array("version" => get_version());
+ $rv = array("version" => Config::get_version());
$this->_wrap(self::STATUS_OK, $rv);
}
@@ -98,8 +98,8 @@ class API extends Handler {
}
function getUnread() {
- $feed_id = clean($_REQUEST["feed_id"]);
- $is_cat = clean($_REQUEST["is_cat"]);
+ $feed_id = clean($_REQUEST["feed_id"] ?? "");
+ $is_cat = clean($_REQUEST["is_cat"] ?? "");
if ($feed_id) {
$this->_wrap(self::STATUS_OK, array("unread" => getFeedUnread($feed_id, $is_cat)));
@@ -188,15 +188,15 @@ class API extends Handler {
if (is_numeric($feed_id)) $feed_id = (int) $feed_id;
- $limit = (int)clean($_REQUEST["limit"]);
+ $limit = (int)clean($_REQUEST["limit"] ?? 0 );
if (!$limit || $limit >= 200) $limit = 200;
- $offset = (int)clean($_REQUEST["skip"]);
+ $offset = (int)clean($_REQUEST["skip"] ?? 0);
$filter = clean($_REQUEST["filter"] ?? "");
$is_cat = self::_param_to_bool(clean($_REQUEST["is_cat"] ?? false));
$show_excerpt = self::_param_to_bool(clean($_REQUEST["show_excerpt"] ?? false));
- $show_content = self::_param_to_bool(clean($_REQUEST["show_content"]));
+ $show_content = self::_param_to_bool(clean($_REQUEST["show_content"] ?? false));
/* all_articles, unread, adaptive, marked, updated */
$view_mode = clean($_REQUEST["view_mode"] ?? null);
$include_attachments = self::_param_to_bool(clean($_REQUEST["include_attachments"] ?? false));
@@ -258,6 +258,10 @@ class API extends Handler {
break;
case 3:
$field = "note";
+ break;
+ case 4:
+ $field = "score";
+ break;
};
switch ($mode) {
@@ -273,6 +277,7 @@ class API extends Handler {
}
if ($field == "note") $set_to = $this->pdo->quote($data);
+ if ($field == "score") $set_to = (int) $data;
if ($field && $set_to && count($article_ids) > 0) {
@@ -363,6 +368,7 @@ class API extends Handler {
}
$this->_wrap(self::STATUS_OK, $articles);
+ // @phpstan-ignore-next-line
} else {
$this->_wrap(self::STATUS_ERR, array("error" => self::E_INCORRECT_USAGE));
}
@@ -786,7 +792,8 @@ class API extends Handler {
list ($flavor_image, $flavor_stream, $flavor_kind) = Article::_get_image($enclosures,
$line["content"], // unsanitized
- $line["site_url"]);
+ $line["site_url"],
+ $headline_row);
$headline_row["flavor_image"] = $flavor_image;
$headline_row["flavor_stream"] = $flavor_stream;
diff --git a/classes/article.php b/classes/article.php
index d8ae97257..04855ac9d 100755
--- a/classes/article.php
+++ b/classes/article.php
@@ -5,26 +5,23 @@ class Article extends Handler_Protected {
const ARTICLE_KIND_YOUTUBE = 3;
function redirect() {
- $id = (int) clean($_REQUEST['id'] ?? 0);
-
- $sth = $this->pdo->prepare("SELECT link FROM ttrss_entries, ttrss_user_entries
- WHERE id = ? AND id = ref_id AND owner_uid = ?
- LIMIT 1");
- $sth->execute([$id, $_SESSION['uid']]);
+ $article = ORM::for_table('ttrss_entries')
+ ->table_alias('e')
+ ->join('ttrss_user_entries', [ 'ref_id', '=', 'e.id'], 'ue')
+ ->where('ue.owner_uid', $_SESSION['uid'])
+ ->find_one((int)$_REQUEST['id']);
- if ($row = $sth->fetch()) {
- $article_url = UrlHelper::validate(str_replace("\n", "", $row['link']));
+ if ($article) {
+ $article_url = UrlHelper::validate($article->link);
if ($article_url) {
header("Location: $article_url");
- } else {
- header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
- print "URL of article $id is blank.";
+ return;
}
-
- } else {
- print_error(__("Article not found."));
}
+
+ header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
+ print "Article not found or has an empty URL.";
}
static function _create_published_article($title, $url, $content, $labels_str,
@@ -182,19 +179,6 @@ class Article extends Handler_Protected {
print json_encode(["id" => $ids, "score" => $score]);
}
- function getScore() {
- $id = clean($_REQUEST['id']);
-
- $sth = $this->pdo->prepare("SELECT score FROM ttrss_user_entries WHERE ref_id = ? AND owner_uid = ?");
- $sth->execute([$id, $_SESSION['uid']]);
- $row = $sth->fetch();
-
- $score = $row['score'];
-
- print json_encode(["id" => $id, "score" => (int)$score]);
- }
-
-
function setArticleTags() {
$id = clean($_REQUEST["id"]);
@@ -433,42 +417,39 @@ class Article extends Handler_Protected {
}
function getmetadatabyid() {
- $id = clean($_REQUEST['id']);
-
- $sth = $this->pdo->prepare("SELECT link, title FROM ttrss_entries, ttrss_user_entries
- WHERE ref_id = ? AND ref_id = id AND owner_uid = ?");
- $sth->execute([$id, $_SESSION['uid']]);
+ $article = ORM::for_table('ttrss_entries')
+ ->join('ttrss_user_entries', ['ref_id', '=', 'id'], 'ue')
+ ->where('ue.owner_uid', $_SESSION['uid'])
+ ->find_one((int)$_REQUEST['id']);
- if ($row = $sth->fetch()) {
- $link = $row['link'];
- $title = $row['title'];
-
- echo json_encode(["link" => $link, "title" => $title]);
+ if ($article) {
+ echo json_encode(["link" => $article->link, "title" => $article->title]);
+ } else {
+ echo json_encode([]);
}
}
static function _get_enclosures($id) {
+ $encs = ORM::for_table('ttrss_enclosures')
+ ->where('post_id', $id)
+ ->find_many();
- $pdo = Db::pdo();
-
- $sth = $pdo->prepare("SELECT * FROM ttrss_enclosures
- WHERE post_id = ? AND content_url != ''");
- $sth->execute([$id]);
-
- $rv = array();
+ $rv = [];
$cache = new DiskCache("images");
- while ($line = $sth->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($encs as $enc) {
+ $cache_key = sha1($enc->content_url);
- if ($cache->exists(sha1($line["content_url"]))) {
- $line["content_url"] = $cache->get_url(sha1($line["content_url"]));
+ if ($cache->exists($cache_key)) {
+ $enc->content_url = $cache->get_url($cache_key);
}
- array_push($rv, $line);
+ array_push($rv, $enc->as_array());
}
return $rv;
+
}
static function _purge_orphans() {
@@ -562,17 +543,20 @@ class Article extends Handler_Protected {
return $rv;
}
- static function _get_image($enclosures, $content, $site_url) {
+ static function _get_image(array $enclosures, string $content, string $site_url, array $headline) {
$article_image = "";
$article_stream = "";
$article_kind = 0;
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_ARTICLE_IMAGE,
- function ($result) use (&$article_image, &$article_stream, &$content) {
+ function ($result, $plugin) use (&$article_image, &$article_stream, &$content) {
list ($article_image, $article_stream, $content) = $result;
+
+ // run until first hard match
+ return !empty($article_image);
},
- $enclosures, $content, $site_url);
+ $enclosures, $content, $site_url, $headline);
if (!$article_image && !$article_stream) {
$tmpdoc = new DOMDocument();
@@ -645,17 +629,16 @@ class Article extends Handler_Protected {
if (count($article_ids) == 0)
return [];
- $id_qmarks = arr_qmarks($article_ids);
-
- $sth = Db::pdo()->prepare("SELECT DISTINCT label_cache FROM ttrss_entries e, ttrss_user_entries ue
- WHERE ue.ref_id = e.id AND id IN ($id_qmarks)");
-
- $sth->execute($article_ids);
+ $entries = ORM::for_table('ttrss_entries')
+ ->table_alias('e')
+ ->join('ttrss_user_entries', ['ref_id', '=', 'id'], 'ue')
+ ->where_in('id', $article_ids)
+ ->find_many();
$rv = [];
- while ($row = $sth->fetch()) {
- $labels = json_decode($row["label_cache"]);
+ foreach ($entries as $entry) {
+ $labels = json_decode($entry->label_cache);
if (isset($labels) && is_array($labels)) {
foreach ($labels as $label) {
@@ -672,19 +655,18 @@ class Article extends Handler_Protected {
if (count($article_ids) == 0)
return [];
- $id_qmarks = arr_qmarks($article_ids);
-
- $sth = Db::pdo()->prepare("SELECT DISTINCT feed_id FROM ttrss_entries e, ttrss_user_entries ue
- WHERE ue.ref_id = e.id AND id IN ($id_qmarks)");
-
- $sth->execute($article_ids);
+ $entries = ORM::for_table('ttrss_entries')
+ ->table_alias('e')
+ ->join('ttrss_user_entries', ['ref_id', '=', 'id'], 'ue')
+ ->where_in('id', $article_ids)
+ ->find_many();
$rv = [];
- while ($row = $sth->fetch()) {
- array_push($rv, $row["feed_id"]);
+ foreach ($entries as $entry) {
+ array_push($rv, $entry->feed_id);
}
- return $rv;
+ return array_unique($rv);
}
}
diff --git a/classes/auth/base.php b/classes/auth/base.php
index f18cc2d2d..82ea06e1b 100644
--- a/classes/auth/base.php
+++ b/classes/auth/base.php
@@ -23,13 +23,14 @@ abstract class Auth_Base extends Plugin implements IAuthModule {
if (!$password) $password = make_password();
- $salt = substr(bin2hex(get_random_bytes(125)), 0, 250);
- $pwd_hash = encrypt_password($password, $salt, true);
-
- $sth = $this->pdo->prepare("INSERT INTO ttrss_users
- (login,access_level,last_login,created,pwd_hash,salt)
- VALUES (LOWER(?), 0, null, NOW(), ?,?)");
- $sth->execute([$login, $pwd_hash, $salt]);
+ $user = ORM::for_table('ttrss_users')->create();
+
+ $user->salt = UserHelper::get_salt();
+ $user->login = mb_strtolower($login);
+ $user->pwd_hash = UserHelper::hash_password($password, $user->salt);
+ $user->access_level = 0;
+ $user->created = Db::NOW();
+ $user->save();
return UserHelper::find_user_by_login($login);
diff --git a/classes/config.php b/classes/config.php
index ee1d3cb4a..567a019c6 100644
--- a/classes/config.php
+++ b/classes/config.php
@@ -6,6 +6,8 @@ class Config {
const T_STRING = 2;
const T_INT = 3;
+ const SCHEMA_VERSION = 144;
+
// override defaults, defined below in _DEFAULTS[], via environment: DB_TYPE becomes TTRSS_DB_TYPE, etc
const DB_TYPE = "DB_TYPE";
@@ -53,6 +55,8 @@ class Config {
const HTTP_PROXY = "HTTP_PROXY";
const FORBID_PASSWORD_CHANGES = "FORBID_PASSWORD_CHANGES";
const SESSION_NAME = "SESSION_NAME";
+ const CHECK_FOR_PLUGIN_UPDATES = "CHECK_FOR_PLUGIN_UPDATES";
+ const ENABLE_PLUGIN_INSTALLER = "ENABLE_PLUGIN_INSTALLER";
private const _DEFAULTS = [
Config::DB_TYPE => [ "pgsql", Config::T_STRING ],
@@ -80,10 +84,10 @@ class Config {
Config::T_STRING ],
Config::CHECK_FOR_UPDATES => [ "true", Config::T_BOOL ],
Config::PLUGINS => [ "auth_internal", Config::T_STRING ],
- Config::LOG_DESTINATION => [ "sql", Config::T_STRING ],
+ Config::LOG_DESTINATION => [ Logger::LOG_DEST_SQL, Config::T_STRING ],
Config::LOCAL_OVERRIDE_STYLESHEET => [ "local-overrides.css",
Config::T_STRING ],
- Config::DAEMON_MAX_CHILD_RUNTIME => [ 1800, 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 ],
Config::FEED_FETCH_NO_CACHE_TIMEOUT => [ 15, Config::T_INT ],
@@ -102,12 +106,18 @@ class Config {
Config::HTTP_PROXY => [ "", Config::T_STRING ],
Config::FORBID_PASSWORD_CHANGES => [ "", Config::T_BOOL ],
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 ],
];
private static $instance;
private $params = [];
private $schema_version = null;
+ private $version = [];
+
+ /** @var Db_Migrations $migrations */
+ private $migrations;
public static function get_instance() : Config {
if (self::$instance == null)
@@ -129,23 +139,109 @@ class Config {
list ($defval, $deftype) = $this::_DEFAULTS[$const];
- $this->params[$cvalue] = [ self::cast_to(!empty($override) ? $override : $defval, $deftype), $deftype ];
+ $this->params[$cvalue] = [ self::cast_to($override !== false ? $override : $defval, $deftype), $deftype ];
+ }
+ }
+ }
+
+ /* package maintainers who don't use git: if version_static.txt exists in tt-rss root
+ directory, its contents are displayed instead of git commit-based version, this could be generated
+ based on source git tree commit used when creating the package */
+
+ static function get_version(bool $as_string = true) {
+ return self::get_instance()->_get_version($as_string);
+ }
+
+ private function _get_version(bool $as_string = true) {
+ $root_dir = dirname(__DIR__);
+
+ if (empty($this->version)) {
+ $this->version["status"] = -1;
+
+ if (PHP_OS === "Darwin") {
+ $ttrss_version["version"] = "UNKNOWN (Unsupported, Darwin)";
+ } else if (file_exists("$root_dir/version_static.txt")) {
+ $this->version["version"] = trim(file_get_contents("$root_dir/version_static.txt")) . " (Unsupported)";
+ } else if (is_dir("$root_dir/.git")) {
+ $this->version = self::get_version_from_git($root_dir);
+
+ if ($this->version["status"] != 0) {
+ user_error("Unable to determine version: " . $this->version["version"], E_USER_WARNING);
+
+ $this->version["version"] = "UNKNOWN (Unsupported, Git error)";
+ }
+ } else {
+ $this->version["version"] = "UNKNOWN (Unsupported)";
}
}
+
+ return $as_string ? $this->version["version"] : $this->version;
}
- static function get_schema_version(bool $nocache = false) {
- return self::get_instance()->_schema_version($nocache);
+ static function get_version_from_git(string $dir) {
+ $descriptorspec = [
+ 1 => ["pipe", "w"], // STDOUT
+ 2 => ["pipe", "w"], // STDERR
+ ];
+
+ $rv = [
+ "status" => -1,
+ "version" => "",
+ "commit" => "",
+ "timestamp" => 0,
+ ];
+
+ $proc = proc_open("git --no-pager log --pretty=\"version-%ct-%h\" -n1 HEAD",
+ $descriptorspec, $pipes, $dir);
+
+ if (is_resource($proc)) {
+ $stdout = trim(stream_get_contents($pipes[1]));
+ $stderr = trim(stream_get_contents($pipes[2]));
+ $status = proc_close($proc);
+
+ $rv["status"] = $status;
+
+ list($check, $timestamp, $commit) = explode("-", $stdout);
+
+ if ($check == "version") {
+
+ $rv["version"] = strftime("%y.%m", (int)$timestamp) . "-$commit";
+ $rv["commit"] = $commit;
+ $rv["timestamp"] = $timestamp;
+
+ // proc_close() may return -1 even if command completed successfully
+ // so if it looks like we got valid data, we ignore it
+
+ if ($rv["status"] == -1)
+ $rv["status"] = 0;
+
+ } else {
+ $rv["version"] = T_sprintf("Git error [RC=%d]: %s", $status, $stderr);
+ }
+ }
+
+ return $rv;
}
- function _schema_version(bool $nocache = false) {
- if (empty($this->schema_version) || $nocache) {
- $row = Db::pdo()->query("SELECT schema_version FROM ttrss_version")->fetch();
+ static function get_migrations() : Db_Migrations {
+ return self::get_instance()->_get_migrations();
+ }
- $this->schema_version = (int) $row["schema_version"];
+ private function _get_migrations() : Db_Migrations {
+ if (empty($this->migrations)) {
+ $this->migrations = new Db_Migrations();
+ $this->migrations->initialize(dirname(__DIR__) . "/sql", "ttrss_version", true, self::SCHEMA_VERSION);
}
- return $this->schema_version;
+ return $this->migrations;
+ }
+
+ static function is_migration_needed() : bool {
+ return self::get_migrations()->is_migration_needed();
+ }
+
+ static function get_schema_version() : int {
+ return self::get_migrations()->get_version();
}
static function cast_to(string $value, int $type_hint) {
@@ -168,7 +264,7 @@ class Config {
private function _add(string $param, string $default, int $type_hint) {
$override = getenv($this::_ENVVAR_PREFIX . $param);
- $this->params[$param] = [ self::cast_to(!empty($override) ? $override : $default, $type_hint), $type_hint ];
+ $this->params[$param] = [ self::cast_to($override !== false ? $override : $default, $type_hint), $type_hint ];
}
static function add(string $param, string $default, int $type_hint = Config::T_STRING) {
@@ -183,4 +279,228 @@ class Config {
return $instance->_get($param);
}
+ /** this returns Config::SELF_URL_PATH sans trailing slash */
+ static function get_self_url() : string {
+ $self_url_path = self::get(Config::SELF_URL_PATH);
+
+ if (substr($self_url_path, -1) === "/") {
+ return substr($self_url_path, 0, -1);
+ } else {
+ return $self_url_path;
+ }
+ }
+
+ static function is_server_https() : bool {
+ return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
+ (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https');
+ }
+
+ /** generates reference self_url_path (no trailing slash) */
+ static function make_self_url() : string {
+ $proto = self::is_server_https() ? 'https' : 'http';
+ $self_url_path = $proto . '://' . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];
+
+ $self_url_path = preg_replace("/\w+\.php(\?.*$)?$/", "", $self_url_path);
+
+ if (substr($self_url_path, -1) === "/") {
+ return substr($self_url_path, 0, -1);
+ } else {
+ return $self_url_path;
+ }
+ }
+
+ /* sanity check stuff */
+
+ private static function check_mysql_tables() {
+ $pdo = Db::pdo();
+
+ $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
+ table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
+ $sth->execute([self::get(Config::DB_NAME)]);
+
+ $bad_tables = [];
+
+ while ($line = $sth->fetch()) {
+ array_push($bad_tables, $line);
+ }
+
+ return $bad_tables;
+ }
+
+ static function sanity_check() {
+
+ /*
+ we don't actually need the DB object right now but some checks below might use ORM which won't be initialized
+ because it is set up in the Db constructor, which is why it's a good idea to invoke it as early as possible
+
+ it is a bit of a hack, maybe ORM should be initialized somewhere else (functions.php?)
+ */
+
+ $pdo = Db::pdo();
+
+ $errors = [];
+
+ if (strpos(self::get(Config::PLUGINS), "auth_") === false) {
+ array_push($errors, "Please enable at least one authentication module via PLUGINS");
+ }
+
+ if (function_exists('posix_getuid') && posix_getuid() == 0) {
+ array_push($errors, "Please don't run this script as root.");
+ }
+
+ if (version_compare(PHP_VERSION, '7.1.0', '<')) {
+ array_push($errors, "PHP version 7.1.0 or newer required. You're using " . PHP_VERSION . ".");
+ }
+
+ if (!class_exists("UConverter")) {
+ array_push($errors, "PHP UConverter class is missing, it's provided by the Internationalization (intl) module.");
+ }
+
+ if (!is_writable(self::get(Config::CACHE_DIR) . "/images")) {
+ array_push($errors, "Image cache is not writable (chmod -R 777 ".self::get(Config::CACHE_DIR)."/images)");
+ }
+
+ if (!is_writable(self::get(Config::CACHE_DIR) . "/upload")) {
+ array_push($errors, "Upload cache is not writable (chmod -R 777 ".self::get(Config::CACHE_DIR)."/upload)");
+ }
+
+ if (!is_writable(self::get(Config::CACHE_DIR) . "/export")) {
+ 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")) {
+ 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.");
+ }
+ }
+
+ if (php_sapi_name() != "cli") {
+
+ if (self::get_schema_version() < 0) {
+ array_push($errors, "Base database schema is missing. Either load it manually or perform a migration (<code>update.php --update-schema</code>)");
+ }
+
+ $ref_self_url_path = self::make_self_url();
+
+ if ($ref_self_url_path) {
+ $ref_self_url_path = preg_replace("/\w+\.php$/", "", $ref_self_url_path);
+ }
+
+ if (self::get_self_url() == "http://example.org/tt-rss") {
+ $hint = $ref_self_url_path ? "(possible value: <b>$ref_self_url_path</b>)" : "";
+ array_push($errors,
+ "Please set SELF_URL_PATH to the correct value for your server: $hint");
+ }
+
+ if (self::get_self_url() != $ref_self_url_path) {
+ array_push($errors,
+ "Please set SELF_URL_PATH to the correct value detected for your server: <b>$ref_self_url_path</b> (you're using: <b>" . self::get_self_url() . "</b>)");
+ }
+ }
+
+ if (!is_writable(self::get(Config::ICONS_DIR))) {
+ array_push($errors, "ICONS_DIR defined in config.php is not writable (chmod -R 777 ".self::get(Config::ICONS_DIR).").\n");
+ }
+
+ if (!is_writable(self::get(Config::LOCK_DIRECTORY))) {
+ array_push($errors, "LOCK_DIRECTORY is not writable (chmod -R 777 ".self::get(Config::LOCK_DIRECTORY).").\n");
+ }
+
+ if (!function_exists("curl_init") && !ini_get("allow_url_fopen")) {
+ array_push($errors, "PHP configuration option allow_url_fopen is disabled, and CURL functions are not present. Either enable allow_url_fopen or install PHP extension for CURL.");
+ }
+
+ if (!function_exists("json_encode")) {
+ array_push($errors, "PHP support for JSON is required, but was not found.");
+ }
+
+ if (!class_exists("PDO")) {
+ array_push($errors, "PHP support for PDO is required but was not found.");
+ }
+
+ if (!function_exists("mb_strlen")) {
+ array_push($errors, "PHP support for mbstring functions is required but was not found.");
+ }
+
+ if (!function_exists("hash")) {
+ array_push($errors, "PHP support for hash() function is required but was not found.");
+ }
+
+ if (ini_get("safe_mode")) {
+ array_push($errors, "PHP safe mode setting is obsolete and not supported by tt-rss.");
+ }
+
+ if (!function_exists("mime_content_type")) {
+ array_push($errors, "PHP function mime_content_type() is missing, try enabling fileinfo module.");
+ }
+
+ if (!class_exists("DOMDocument")) {
+ array_push($errors, "PHP support for DOMDocument is required, but was not found.");
+ }
+
+ if (self::get(Config::DB_TYPE) == "mysql") {
+ $bad_tables = self::check_mysql_tables();
+
+ if (count($bad_tables) > 0) {
+ $bad_tables_fmt = [];
+
+ foreach ($bad_tables as $bt) {
+ array_push($bad_tables_fmt, sprintf("%s (%s)", $bt['table_name'], $bt['engine']));
+ }
+
+ $msg = "<p>The following tables use an unsupported MySQL engine: <b>" .
+ implode(", ", $bad_tables_fmt) . "</b>.</p>";
+
+ $msg .= "<p>The only supported engine on MySQL is InnoDB. MyISAM lacks functionality to run
+ tt-rss.
+ Please backup your data (via OPML) and re-import the schema before continuing.</p>
+ <p><b>WARNING: importing the schema would mean LOSS OF ALL YOUR DATA.</b></p>";
+
+
+ array_push($errors, $msg);
+ }
+ }
+
+ if (count($errors) > 0 && php_sapi_name() != "cli") { ?>
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Startup failed</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="stylesheet" type="text/css" href="themes/light.css">
+ </head>
+ <body class="sanity_failed flat ttrss_utility">
+ <div class="content">
+ <h1>Startup failed</h1>
+
+ <p>Please fix errors indicated by the following messages:</p>
+
+ <?php foreach ($errors as $error) { echo self::format_error($error); } ?>
+
+ <p>You might want to check tt-rss <a target="_blank" href="https://tt-rss.org/wiki.php">wiki</a> or the
+ <a target="_blank" href="https://community.tt-rss.org/">forums</a> for more information. Please search the forums before creating new topic
+ for your question.</p>
+ </div>
+ </body>
+ </html>
+
+ <?php
+ die;
+ } else if (count($errors) > 0) {
+ echo "Please fix errors indicated by the following messages:\n\n";
+
+ foreach ($errors as $error) {
+ echo " * " . strip_tags($error)."\n";
+ }
+
+ echo "\nYou might want to check tt-rss wiki or the forums for more information.\n";
+ echo "Please search the forums before creating new topic for your question.\n";
+
+ exit(1);
+ }
+ }
+
+ private static function format_error($msg) {
+ return "<div class=\"alert alert-danger\">$msg</div>";
+ }
}
diff --git a/classes/counters.php b/classes/counters.php
index b4602825c..8a8b8bc71 100644
--- a/classes/counters.php
+++ b/classes/counters.php
@@ -21,21 +21,20 @@ class Counters {
);
}
- static private function get_cat_children($cat_id, $owner_uid) {
- $pdo = Db::pdo();
-
- $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories WHERE parent_cat = ?
- AND owner_uid = ?");
- $sth->execute([$cat_id, $owner_uid]);
-
+ static private function get_cat_children(int $cat_id, int $owner_uid) {
$unread = 0;
$marked = 0;
- while ($line = $sth->fetch()) {
- list ($tmp_unread, $tmp_marked) = self::get_cat_children($line["id"], $owner_uid);
+ $cats = ORM::for_table('ttrss_feed_categories')
+ ->where('owner_uid', $owner_uid)
+ ->where('parent_cat', $cat_id)
+ ->find_many();
+
+ foreach ($cats as $cat) {
+ list ($tmp_unread, $tmp_marked) = self::get_cat_children($cat->id, $owner_uid);
- $unread += $tmp_unread + Feeds::_get_cat_unread($line["id"], $owner_uid);
- $marked += $tmp_marked + Feeds::_get_cat_marked($line["id"], $owner_uid);
+ $unread += $tmp_unread + Feeds::_get_cat_unread($cat->id, $owner_uid);
+ $marked += $tmp_marked + Feeds::_get_cat_marked($cat->id, $owner_uid);
}
return [$unread, $marked];
@@ -178,7 +177,7 @@ class Counters {
$has_img = false;
}
- // hide default un-updated timestamp i.e. 1980-01-01 (?) -fox
+ // hide default un-updated timestamp i.e. 1970-01-01 (?) -fox
if ((int)date('Y') - (int)date('Y', strtotime($line['last_updated'])) > 2)
$last_updated = '';
@@ -200,35 +199,22 @@ class Counters {
return $ret;
}
- private static function get_global($global_unread = -1) {
- $ret = [];
-
- if ($global_unread == -1) {
- $global_unread = Feeds::_get_global_unread();
- }
-
- $cv = [
- "id" => "global-unread",
- "counter" => (int) $global_unread
+ private static function get_global() {
+ $ret = [
+ [
+ "id" => "global-unread",
+ "counter" => (int) Feeds::_get_global_unread()
+ ]
];
- array_push($ret, $cv);
-
- $pdo = Db::pdo();
-
- $sth = $pdo->prepare("SELECT COUNT(id) AS fn FROM
- ttrss_feeds WHERE owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
- $row = $sth->fetch();
+ $subcribed_feeds = ORM::for_table('ttrss_feeds')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->count();
- $subscribed_feeds = $row["fn"];
-
- $cv = [
+ array_push($ret, [
"id" => "subscribed-feeds",
- "counter" => (int) $subscribed_feeds
- ];
-
- array_push($ret, $cv);
+ "counter" => $subcribed_feeds
+ ]);
return $ret;
}
diff --git a/classes/db.php b/classes/db.php
index a760d4402..008275bca 100755
--- a/classes/db.php
+++ b/classes/db.php
@@ -1,27 +1,42 @@
<?php
class Db
{
- /* @var Db $instance */
+ /** @var Db $instance */
private static $instance;
private $link;
- /* @var PDO $pdo */
+ /** @var PDO $pdo */
private $pdo;
+ function __construct() {
+ ORM::configure(self::get_dsn());
+ ORM::configure('username', Config::get(Config::DB_USER));
+ ORM::configure('password', Config::get(Config::DB_PASS));
+ ORM::configure('return_result_sets', true);
+ }
+
+ static function NOW() {
+ return date("Y-m-d H:i:s", time());
+ }
+
private function __clone() {
//
}
- // this really shouldn't be used unless a separate PDO connection is needed
- // normal usage is Db::pdo()->prepare(...) etc
- public function pdo_connect() {
-
+ 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) : '';
+ return Config::get(Config::DB_TYPE) . ':dbname=' . Config::get(Config::DB_NAME) . $db_host . $db_port;
+ }
+
+ // this really shouldn't be used unless a separate PDO connection is needed
+ // normal usage is Db::pdo()->prepare(...) etc
+ public function pdo_connect() : PDO {
+
try {
- $pdo = new PDO(Config::get(Config::DB_TYPE) . ':dbname=' . Config::get(Config::DB_NAME) . $db_host . $db_port,
+ $pdo = new PDO(self::get_dsn(),
Config::get(Config::DB_USER),
Config::get(Config::DB_PASS));
} catch (Exception $e) {
@@ -49,7 +64,7 @@ class Db
return $pdo;
}
- public static function instance() {
+ public static function instance() : Db {
if (self::$instance == null)
self::$instance = new self();
@@ -60,7 +75,7 @@ class Db
if (self::$instance == null)
self::$instance = new self();
- if (!self::$instance->pdo) {
+ if (empty(self::$instance->pdo)) {
self::$instance->pdo = self::$instance->pdo_connect();
}
diff --git a/classes/db/migrations.php b/classes/db/migrations.php
new file mode 100644
index 000000000..3008af535
--- /dev/null
+++ b/classes/db/migrations.php
@@ -0,0 +1,198 @@
+<?php
+class Db_Migrations {
+
+ private $base_filename = "schema.sql";
+ private $base_path;
+ private $migrations_path;
+ private $migrations_table;
+ private $base_is_latest;
+ private $pdo;
+
+ private $cached_version;
+ private $cached_max_version;
+ private $max_version_override;
+
+ function __construct() {
+ $this->pdo = Db::pdo();
+ }
+
+ function initialize_for_plugin(Plugin $plugin, bool $base_is_latest = true, string $schema_suffix = "sql") {
+ $plugin_dir = PluginHost::getInstance()->get_plugin_dir($plugin);
+ $this->initialize($plugin_dir . "/${schema_suffix}",
+ strtolower("ttrss_migrations_plugin_" . get_class($plugin)),
+ $base_is_latest);
+ }
+
+ function initialize(string $root_path, string $migrations_table, bool $base_is_latest = true, int $max_version_override = 0) {
+ $this->base_path = "$root_path/" . Config::get(Config::DB_TYPE);
+ $this->migrations_path = $this->base_path . "/migrations";
+ $this->migrations_table = $migrations_table;
+ $this->base_is_latest = $base_is_latest;
+ $this->max_version_override = $max_version_override;
+ }
+
+ private function set_version(int $version) {
+ Debug::log("Updating table {$this->migrations_table} with version ${version}...", Debug::LOG_EXTENDED);
+
+ $sth = $this->pdo->query("SELECT * FROM {$this->migrations_table}");
+
+ if ($res = $sth->fetch()) {
+ $sth = $this->pdo->prepare("UPDATE {$this->migrations_table} SET schema_version = ?");
+ } else {
+ $sth = $this->pdo->prepare("INSERT INTO {$this->migrations_table} (schema_version) VALUES (?)");
+ }
+
+ $sth->execute([$version]);
+
+ $this->cached_version = $version;
+ }
+
+ function get_version() : int {
+ if (isset($this->cached_version))
+ return $this->cached_version;
+
+ try {
+ $sth = $this->pdo->query("SELECT * FROM {$this->migrations_table}");
+
+ if ($res = $sth->fetch()) {
+ return (int) $res['schema_version'];
+ } else {
+ return -1;
+ }
+ } catch (PDOException $e) {
+ $this->create_migrations_table();
+
+ return -1;
+ }
+ }
+
+ private function create_migrations_table() {
+ $this->pdo->query("CREATE TABLE IF NOT EXISTS {$this->migrations_table} (schema_version integer not null)");
+ }
+
+ private function migrate_to(int $version) {
+ try {
+ if ($version <= $this->get_version()) {
+ Debug::log("Refusing to apply version $version: current version is higher", Debug::LOG_VERBOSE);
+ return false;
+ }
+
+ if ($version == 0)
+ Debug::log("Loading base database schema...", Debug::LOG_VERBOSE);
+ else
+ Debug::log("Starting migration to $version...", Debug::LOG_VERBOSE);
+
+ $lines = $this->get_lines($version);
+
+ if (count($lines) > 0) {
+ // mysql doesn't support transactions for DDL statements
+ if (Config::get(Config::DB_TYPE) != "mysql")
+ $this->pdo->beginTransaction();
+
+ foreach ($lines as $line) {
+ Debug::log($line, Debug::LOG_EXTENDED);
+ try {
+ $this->pdo->query($line);
+ } catch (PDOException $e) {
+ Debug::log("Failed on line: $line", Debug::LOG_VERBOSE);
+ throw $e;
+ }
+ }
+
+ if ($version == 0 && $this->base_is_latest)
+ $this->set_version($this->get_max_version());
+ else
+ $this->set_version($version);
+
+ if (Config::get(Config::DB_TYPE) != "mysql")
+ $this->pdo->commit();
+
+ Debug::log("Migration finished, current version: " . $this->get_version(), Debug::LOG_VERBOSE);
+
+ Logger::log(E_USER_NOTICE, "Applied migration to version $version for {$this->migrations_table}");
+ } else {
+ Debug::log("Migration failed: schema file is empty or missing.", Debug::LOG_VERBOSE);
+ }
+
+ } catch (PDOException $e) {
+ Debug::log("Migration failed: " . $e->getMessage(), Debug::LOG_VERBOSE);
+ try {
+ $this->pdo->rollback();
+ } catch (PDOException $ie) {
+ //
+ }
+ throw $e;
+ }
+ }
+
+ function get_max_version() : int {
+ if ($this->max_version_override > 0)
+ return $this->max_version_override;
+
+ if (isset($this->cached_max_version))
+ return $this->cached_max_version;
+
+ $migrations = glob("{$this->migrations_path}/*.sql");
+
+ if (count($migrations) > 0) {
+ natsort($migrations);
+
+ $this->cached_max_version = (int) basename(array_pop($migrations), ".sql");
+
+ } else {
+ $this->cached_max_version = 0;
+ }
+
+ return $this->cached_max_version;
+ }
+
+ function is_migration_needed() : bool {
+ return $this->get_version() != $this->get_max_version();
+ }
+
+ function migrate() : bool {
+
+ if ($this->get_version() == -1) {
+ try {
+ $this->migrate_to(0);
+ } catch (PDOException $e) {
+ user_error("Failed to load base schema for {$this->migrations_table}: " . $e->getMessage(), E_USER_WARNING);
+ return false;
+ }
+ }
+
+ for ($i = $this->get_version() + 1; $i <= $this->get_max_version(); $i++) {
+ try {
+ $this->migrate_to($i);
+ } catch (PDOException $e) {
+ user_error("Failed to apply migration ${i} for {$this->migrations_table}: " . $e->getMessage(), E_USER_WARNING);
+ return false;
+ //throw $e;
+ }
+ }
+
+ return !$this->is_migration_needed();
+ }
+
+ private function get_lines(int $version) : array {
+ if ($version > 0)
+ $filename = "{$this->migrations_path}/${version}.sql";
+ else
+ $filename = "{$this->base_path}/{$this->base_filename}";
+
+ if (file_exists($filename)) {
+ $lines = array_filter(preg_split("/[\r\n]/", file_get_contents($filename)),
+ function ($line) {
+ return strlen(trim($line)) > 0 && strpos($line, "--") !== 0;
+ });
+
+ return array_filter(explode(";", implode("", $lines)), function ($line) {
+ return strlen(trim($line)) > 0 && !in_array(strtolower($line), ["begin", "commit"]);
+ });
+
+ } else {
+ user_error("Requested schema file ${filename} not found.", E_USER_ERROR);
+ return [];
+ }
+ }
+}
diff --git a/classes/dbupdater.php b/classes/dbupdater.php
deleted file mode 100644
index d1df31b40..000000000
--- a/classes/dbupdater.php
+++ /dev/null
@@ -1,82 +0,0 @@
-<?php
-class DbUpdater {
-
- private $pdo;
- private $db_type;
- private $need_version;
-
- function __construct($pdo, $db_type, $need_version) {
- $this->pdo = $pdo;
- $this->db_type = $db_type;
- $this->need_version = (int) $need_version;
- }
-
- function get_schema_version() {
- return Config::get_schema_version(true);
- }
-
- function is_update_required() {
- return $this->get_schema_version() < $this->need_version;
- }
-
- function get_schema_lines($version) {
- $filename = "schema/versions/".$this->db_type."/$version.sql";
-
- if (file_exists($filename)) {
- return explode(";", (string)preg_replace("/[\r\n]/", "", (string)file_get_contents($filename)));
- } else {
- user_error("DB Updater: schema file for version $version is not found.");
- return false;
- }
- }
-
- function update_to($version, $html_output = true) {
- if ($this->get_schema_version() == $version - 1) {
-
- $lines = $this->get_schema_lines($version);
-
- if (is_array($lines)) {
-
- $this->pdo->beginTransaction();
-
- foreach ($lines as $line) {
- if (strpos($line, "--") !== 0 && $line) {
-
- if ($html_output)
- print "<pre>$line</pre>";
- else
- Debug::log("> $line");
-
- try {
- $this->pdo->query($line); // PDO returns errors as exceptions now
- } catch (PDOException $e) {
- if ($html_output) {
- print "<div class='text-error'>Error: " . $e->getMessage() . "</div>";
- } else {
- Debug::log("Error: " . $e->getMessage());
- }
-
- $this->pdo->rollBack();
- return false;
- }
- }
- }
-
- $db_version = $this->get_schema_version();
-
- if ($db_version == $version) {
- $this->pdo->commit();
- return true;
- } else {
- $this->pdo->rollBack();
- return false;
- }
- } else {
- return false;
- }
- } else {
- return false;
- }
- }
-
-}
diff --git a/classes/debug.php b/classes/debug.php
index 3061c6893..2ae81e41a 100644
--- a/classes/debug.php
+++ b/classes/debug.php
@@ -1,14 +1,26 @@
<?php
class Debug {
- public static $LOG_DISABLED = -1;
- public static $LOG_NORMAL = 0;
- public static $LOG_VERBOSE = 1;
- public static $LOG_EXTENDED = 2;
+ const LOG_DISABLED = -1;
+ const LOG_NORMAL = 0;
+ const LOG_VERBOSE = 1;
+ const LOG_EXTENDED = 2;
+
+ /** @deprecated */
+ public static $LOG_DISABLED = self::LOG_DISABLED;
+
+ /** @deprecated */
+ public static $LOG_NORMAL = self::LOG_NORMAL;
+
+ /** @deprecated */
+ public static $LOG_VERBOSE = self::LOG_VERBOSE;
+
+ /** @deprecated */
+ public static $LOG_EXTENDED = self::LOG_EXTENDED;
private static $enabled = false;
private static $quiet = false;
private static $logfile = false;
- private static $loglevel = 0;
+ private static $loglevel = self::LOG_NORMAL;
public static function set_logfile($logfile) {
self::$logfile = $logfile;
@@ -34,7 +46,7 @@ class Debug {
return self::$loglevel;
}
- public static function log($message, $level = 0) {
+ public static function log($message, int $level = 0) {
if (!self::$enabled || self::$loglevel < $level) return false;
diff --git a/classes/digest.php b/classes/digest.php
index 26ca5221f..94e5cd1fc 100644
--- a/classes/digest.php
+++ b/classes/digest.php
@@ -78,7 +78,7 @@ class Digest
Debug::log("All done.");
}
- static function prepare_headlines_digest($user_id, $days = 1, $limit = 1000) {
+ static function prepare_headlines_digest(int $user_id, int $days = 1, int $limit = 1000) {
$tpl = new Templator();
$tpl_t = new Templator();
@@ -87,20 +87,22 @@ class Digest
$tpl_t->readTemplateFromFile("digest_template.txt");
$user_tz_string = get_pref(Prefs::USER_TIMEZONE, $user_id);
+
+ if ($user_tz_string == 'Automatic')
+ $user_tz_string = 'GMT';
+
$local_ts = TimeHelper::convert_timestamp(time(), 'UTC', $user_tz_string);
$tpl->setVariable('CUR_DATE', date('Y/m/d', $local_ts));
$tpl->setVariable('CUR_TIME', date('G:i', $local_ts));
- $tpl->setVariable('TTRSS_HOST', Config::get(Config::get(Config::SELF_URL_PATH)));
+ $tpl->setVariable('TTRSS_HOST', Config::get(Config::SELF_URL_PATH));
$tpl_t->setVariable('CUR_DATE', date('Y/m/d', $local_ts));
$tpl_t->setVariable('CUR_TIME', date('G:i', $local_ts));
- $tpl_t->setVariable('TTRSS_HOST', Config::get(Config::get(Config::SELF_URL_PATH)));
+ $tpl_t->setVariable('TTRSS_HOST', Config::get(Config::SELF_URL_PATH));
$affected_ids = array();
- $days = (int) $days;
-
if (Config::get(Config::DB_TYPE) == "pgsql") {
$interval_qpart = "ttrss_entries.date_updated > NOW() - INTERVAL '$days days'";
} else /* if (Config::get(Config::DB_TYPE) == "mysql") */ {
@@ -117,7 +119,7 @@ class Digest
link,
score,
content,
- " . SUBSTRING_FOR_DATE . "(last_updated,1,19) AS last_updated
+ ".SUBSTRING_FOR_DATE."(last_updated,1,19) AS last_updated
FROM
ttrss_user_entries,ttrss_entries,ttrss_feeds
LEFT JOIN
@@ -130,10 +132,8 @@ class Digest
AND unread = true
AND score >= 0
ORDER BY ttrss_feed_categories.title, ttrss_feeds.title, score DESC, date_updated DESC
- LIMIT :limit");
- $sth->bindParam(':user_id', intval($user_id, 10), PDO::PARAM_INT);
- $sth->bindParam(':limit', intval($limit, 10), PDO::PARAM_INT);
- $sth->execute();
+ LIMIT " . (int)$limit);
+ $sth->execute([':user_id' => $user_id]);
$headlines_count = 0;
$headlines = array();
diff --git a/classes/diskcache.php b/classes/diskcache.php
index 9c594acc5..d7ea26d3b 100644
--- a/classes/diskcache.php
+++ b/classes/diskcache.php
@@ -273,7 +273,7 @@ class DiskCache {
}
public function get_url($filename) {
- return get_self_url_prefix() . "/public.php?op=cached&file=" . basename($this->dir) . "/" . basename($filename);
+ return Config::get_self_url() . "/public.php?op=cached&file=" . basename($this->dir) . "/" . basename($filename);
}
// check for locally cached (media) URLs and rewrite to local versions
diff --git a/classes/errors.php b/classes/errors.php
index be175418e..3599c2639 100644
--- a/classes/errors.php
+++ b/classes/errors.php
@@ -5,8 +5,9 @@ class Errors {
const E_UNKNOWN_METHOD = "E_UNKNOWN_METHOD";
const E_UNKNOWN_PLUGIN = "E_UNKNOWN_PLUGIN";
const E_SCHEMA_MISMATCH = "E_SCHEMA_MISMATCH";
+ const E_URL_SCHEME_MISMATCH = "E_URL_SCHEME_MISMATCH";
- static function to_json(string $code) {
- return json_encode(["error" => ["code" => $code]]);
+ static function to_json(string $code, array $params = []) {
+ return json_encode(["error" => ["code" => $code, "params" => $params]]);
}
}
diff --git a/classes/feeditem.php b/classes/feeditem.php
index e30df3086..3a5e5dc09 100644
--- a/classes/feeditem.php
+++ b/classes/feeditem.php
@@ -9,7 +9,7 @@ abstract class FeedItem {
abstract function get_comments_url();
abstract function get_comments_count();
abstract function get_categories();
- abstract function _get_enclosures();
+ abstract function get_enclosures();
abstract function get_author();
abstract function get_language();
}
diff --git a/classes/feeditem/atom.php b/classes/feeditem/atom.php
index 3e092a048..a03080981 100755
--- a/classes/feeditem/atom.php
+++ b/classes/feeditem/atom.php
@@ -119,7 +119,7 @@ class FeedItem_Atom extends FeedItem_Common {
return $this->normalize_categories($cats);
}
- function _get_enclosures() {
+ function get_enclosures() {
$links = $this->elem->getElementsByTagName("link");
$encs = array();
@@ -138,7 +138,7 @@ class FeedItem_Atom extends FeedItem_Common {
}
}
- $encs = array_merge($encs, parent::_get_enclosures());
+ $encs = array_merge($encs, parent::get_enclosures());
return $encs;
}
diff --git a/classes/feeditem/common.php b/classes/feeditem/common.php
index 8f2b9188b..18afeaa94 100755
--- a/classes/feeditem/common.php
+++ b/classes/feeditem/common.php
@@ -78,7 +78,7 @@ abstract class FeedItem_Common extends FeedItem {
}
// this is common for both Atom and RSS types and deals with various media: elements
- function _get_enclosures() {
+ function get_enclosures() {
$encs = [];
$enclosures = $this->xpath->query("media:content", $this->elem);
diff --git a/classes/feeditem/rss.php b/classes/feeditem/rss.php
index f103ad787..1f7953c51 100755
--- a/classes/feeditem/rss.php
+++ b/classes/feeditem/rss.php
@@ -112,7 +112,7 @@ class FeedItem_RSS extends FeedItem_Common {
return $this->normalize_categories($cats);
}
- function _get_enclosures() {
+ function get_enclosures() {
$enclosures = $this->elem->getElementsByTagName("enclosure");
$encs = array();
@@ -129,7 +129,7 @@ class FeedItem_RSS extends FeedItem_Common {
array_push($encs, $enc);
}
- $encs = array_merge($encs, parent::_get_enclosures());
+ $encs = array_merge($encs, parent::get_enclosures());
return $encs;
}
diff --git a/classes/feeds.php b/classes/feeds.php
index b1b19500f..68d535481 100755
--- a/classes/feeds.php
+++ b/classes/feeds.php
@@ -200,17 +200,21 @@ class Feeds extends Handler_Protected {
$feed_id = $line["feed_id"];
- $label_cache = $line["label_cache"];
- $labels = false;
-
- if ($label_cache) {
- $label_cache = json_decode($label_cache, true);
+ if ($line["num_labels"] > 0) {
+ $label_cache = $line["label_cache"];
+ $labels = false;
if ($label_cache) {
- if ($label_cache["no-labels"] ?? false == 1)
- $labels = [];
- else
- $labels = $label_cache;
+ $label_cache = json_decode($label_cache, true);
+
+ if ($label_cache) {
+ if ($label_cache["no-labels"] ?? 0 == 1)
+ $labels = [];
+ else
+ $labels = $label_cache;
+ }
+ } else {
+ $labels = Article::_get_labels($id);
}
$line["labels"] = $labels;
@@ -218,10 +222,6 @@ class Feeds extends Handler_Protected {
$line["labels"] = [];
}
- /*if (!is_array($labels)) $labels = Article::_get_labels($id);
-
- $line["labels"] = Article::_get_labels($id);*/
-
if (count($topmost_article_ids) < 3) {
array_push($topmost_article_ids, $id);
}
@@ -251,21 +251,6 @@ class Feeds extends Handler_Protected {
$this->_mark_timestamp(" sanitize");
- PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_CDM,
- function ($result, $plugin) use (&$line) {
- $line = $result;
- $this->_mark_timestamp(" hook_render_cdm: " . get_class($plugin));
- },
- $line);
-
- $this->_mark_timestamp(" hook_render_cdm");
-
- $line['content'] = DiskCache::rewrite_urls($line['content']);
-
- $this->_mark_timestamp(" disk_cache_rewrite");
-
- $this->_mark_timestamp(" note");
-
if (!get_pref(Prefs::CDM_EXPANDED)) {
$line["cdm_excerpt"] = "<span class='collapse'>
<i class='material-icons' onclick='return Article.cdmUnsetActive(event)'
@@ -330,6 +315,20 @@ class Feeds extends Handler_Protected {
}
$this->_mark_timestamp(" color");
+ $this->_mark_timestamp(" pre-hook_render_cdm");
+
+ PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_RENDER_ARTICLE_CDM,
+ function ($result, $plugin) use (&$line) {
+ $line = $result;
+ $this->_mark_timestamp(" hook: " . get_class($plugin));
+ },
+ $line);
+
+ $this->_mark_timestamp(" hook_render_cdm");
+
+ $line['content'] = DiskCache::rewrite_urls($line['content']);
+
+ $this->_mark_timestamp(" disk_cache_rewrite");
/* we don't need those */
@@ -474,8 +473,9 @@ class Feeds extends Handler_Protected {
/* bump login timestamp if needed */
if (time() - $_SESSION["last_login_update"] > 3600) {
- $sth = $this->pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
- $sth->execute([$_SESSION['uid']]);
+ $user = ORM::for_table('ttrss_users')->find_one($_SESSION["uid"]);
+ $user->last_login = Db::NOW();
+ $user->save();
$_SESSION["last_login_update"] = time();
}
@@ -963,7 +963,7 @@ class Feeds extends Handler_Protected {
function add() {
$feed = clean($_REQUEST['feed']);
- $cat = clean($_REQUEST['cat']);
+ $cat = clean($_REQUEST['cat'] ?? '');
$need_auth = isset($_REQUEST['need_auth']);
$login = $need_auth ? clean($_REQUEST['login']) : '';
$pass = $need_auth ? clean($_REQUEST['pass']) : '';
@@ -985,21 +985,18 @@ class Feeds extends Handler_Protected {
* to get all possible feeds.
* 5 - Couldn't download the URL content.
* 6 - Content is an invalid XML.
+ * 7 - Error while creating feed database entry.
*/
static function _subscribe($url, $cat_id = 0,
- $auth_login = '', $auth_pass = '') {
-
- global $fetch_last_error;
- global $fetch_last_error_content;
- global $fetch_last_content_type;
+ $auth_login = '', $auth_pass = '') : array {
$pdo = Db::pdo();
$url = UrlHelper::validate($url);
- if (!$url) return array("code" => 2);
+ if (!$url) return ["code" => 2];
- $contents = @UrlHelper::fetch($url, false, $auth_login, $auth_pass);
+ $contents = UrlHelper::fetch($url, false, $auth_login, $auth_pass);
PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_SUBSCRIBE_FEED,
function ($result) use (&$contents) {
@@ -1008,14 +1005,14 @@ class Feeds extends Handler_Protected {
$contents, $url, $auth_login, $auth_pass);
if (empty($contents)) {
- if (preg_match("/cloudflare\.com/", $fetch_last_error_content)) {
- $fetch_last_error .= " (feed behind Cloudflare)";
+ if (preg_match("/cloudflare\.com/", UrlHelper::$fetch_last_error_content)) {
+ UrlHelper::$fetch_last_error .= " (feed behind Cloudflare)";
}
- return array("code" => 5, "message" => $fetch_last_error);
+ return array("code" => 5, "message" => UrlHelper::$fetch_last_error);
}
- if (mb_strpos($fetch_last_content_type, "html") !== false && self::_is_html($contents)) {
+ if (mb_strpos(UrlHelper::$fetch_last_content_type, "html") !== false && self::_is_html($contents)) {
$feedUrls = self::_get_feeds_from_html($url, $contents);
if (count($feedUrls) == 0) {
@@ -1027,35 +1024,33 @@ class Feeds extends Handler_Protected {
$url = key($feedUrls);
}
- if (!$cat_id) $cat_id = null;
+ $feed = ORM::for_table('ttrss_feeds')
+ ->where('feed_url', $url)
+ ->where('owner_uid', $_SESSION['uid'])
+ ->find_one();
- $sth = $pdo->prepare("SELECT id FROM ttrss_feeds
- WHERE feed_url = ? AND owner_uid = ?");
- $sth->execute([$url, $_SESSION['uid']]);
-
- if ($row = $sth->fetch()) {
- return array("code" => 0, "feed_id" => (int) $row["id"]);
+ if ($feed) {
+ return ["code" => 0, "feed_id" => $feed->id];
} else {
- $sth = $pdo->prepare(
- "INSERT INTO ttrss_feeds
- (owner_uid,feed_url,title,cat_id, auth_login,auth_pass,update_method,auth_pass_encrypted)
- VALUES (?, ?, ?, ?, ?, ?, 0, false)");
-
- $sth->execute([$_SESSION['uid'], $url, "[Unknown]", $cat_id, (string)$auth_login, (string)$auth_pass]);
-
- $sth = $pdo->prepare("SELECT id FROM ttrss_feeds WHERE feed_url = ?
- AND owner_uid = ?");
- $sth->execute([$url, $_SESSION['uid']]);
- $row = $sth->fetch();
-
- $feed_id = $row["id"];
-
- if ($feed_id) {
- RSSUtils::set_basic_feed_info($feed_id);
+ $feed = ORM::for_table('ttrss_feeds')->create();
+
+ $feed->set([
+ 'owner_uid' => $_SESSION['uid'],
+ 'feed_url' => $url,
+ 'title' => "[Unknown]",
+ 'cat_id' => $cat_id ? $cat_id : null,
+ 'auth_login' => (string)$auth_login,
+ 'auth_pass' => (string)$auth_pass,
+ 'update_method' => 0,
+ 'auth_pass_encrypted' => false,
+ ]);
+
+ if ($feed->save()) {
+ RSSUtils::update_basic_info($feed->id);
+ return ["code" => 1, "feed_id" => (int) $feed->id];
}
- return array("code" => 1, "feed_id" => (int) $feed_id);
-
+ return ["code" => 7];
}
}
@@ -1097,19 +1092,20 @@ class Feeds extends Handler_Protected {
return false;
}
- static function _find_by_url($feed_url, $owner_uid) {
- $sth = Db::pdo()->prepare("SELECT id FROM ttrss_feeds WHERE
- feed_url = ? AND owner_uid = ?");
- $sth->execute([$feed_url, $owner_uid]);
+ static function _find_by_url(string $feed_url, int $owner_uid) {
+ $feed = ORM::for_table('ttrss_feeds')
+ ->where('owner_uid', $owner_uid)
+ ->where('feed_url', $feed_url)
+ ->find_one();
- if ($row = $sth->fetch()) {
- return $row["id"];
+ if ($feed) {
+ return $feed->id;
+ } else {
+ return false;
}
-
- return false;
}
- static function _get_title($id, $cat = false) {
+ static function _get_title($id, bool $cat = false) {
$pdo = Db::pdo();
if ($cat) {
@@ -1156,7 +1152,7 @@ class Feeds extends Handler_Protected {
}
// only real cats
- static function _get_cat_marked($cat, $owner_uid = false) {
+ static function _get_cat_marked(int $cat, int $owner_uid = 0) {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
@@ -1179,7 +1175,7 @@ class Feeds extends Handler_Protected {
}
}
- static function _get_cat_unread($cat, $owner_uid = false) {
+ static function _get_cat_unread(int $cat, int $owner_uid = 0) {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
@@ -1213,7 +1209,7 @@ class Feeds extends Handler_Protected {
}
// only accepts real cats (>= 0)
- static function _get_cat_children_unread($cat, $owner_uid = false) {
+ static function _get_cat_children_unread(int $cat, int $owner_uid = 0) {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
@@ -1232,7 +1228,7 @@ class Feeds extends Handler_Protected {
return $unread;
}
- static function _get_global_unread($user_id = false) {
+ static function _get_global_unread(int $user_id = 0) {
if (!$user_id) $user_id = $_SESSION["uid"];
@@ -1248,29 +1244,27 @@ class Feeds extends Handler_Protected {
return $row["count"];
}
- static function _get_cat_title($cat_id) {
-
- if ($cat_id == -1) {
- return __("Special");
- } else if ($cat_id == -2) {
- return __("Labels");
- } else {
-
- $pdo = Db::pdo();
-
- $sth = $pdo->prepare("SELECT title FROM ttrss_feed_categories WHERE
- id = ?");
- $sth->execute([$cat_id]);
-
- if ($row = $sth->fetch()) {
- return $row["title"];
- } else {
+ static function _get_cat_title(int $cat_id) {
+ switch ($cat_id) {
+ case 0:
return __("Uncategorized");
- }
+ case -1:
+ return __("Special");
+ case -2:
+ return __("Labels");
+ default:
+ $cat = ORM::for_table('ttrss_feed_categories')
+ ->find_one($cat_id);
+
+ if ($cat) {
+ return $cat->title;
+ } else {
+ return "UNKNOWN";
+ }
}
}
- private static function _get_label_unread($label_id, $owner_uid = false) {
+ private static function _get_label_unread($label_id, int $owner_uid = 0) {
if (!$owner_uid) $owner_uid = $_SESSION["uid"];
$pdo = Db::pdo();
@@ -1574,6 +1568,12 @@ class Feeds extends Handler_Protected {
$first_id = 0;
+ if (Config::get(Config::DB_TYPE) == "pgsql") {
+ $yyiw_qpart = "to_char(date_entered, 'IYYY-IW') AS yyiw";
+ } else {
+ $yyiw_qpart = "date_format(date_entered, '%Y-%u') AS yyiw";
+ }
+
if (is_numeric($feed)) {
// proper override_order applied above
if ($vfeed_query_part && !$ignore_vfeed_group && get_pref(Prefs::VFEED_GROUP_BY_FEED, $owner_uid)) {
@@ -1611,18 +1611,16 @@ class Feeds extends Handler_Protected {
if (Config::get(Config::DB_TYPE) == "pgsql") {
$sanity_interval_qpart = "date_entered >= NOW() - INTERVAL '1 hour' AND";
- $yyiw_qpart = "to_char(date_entered, 'IYYY-IW') AS yyiw";
$distinct_columns = str_replace("desc", "", strtolower($order_by));
$distinct_qpart = "DISTINCT ON (id, $distinct_columns)";
} else {
$sanity_interval_qpart = "date_entered >= DATE_SUB(NOW(), INTERVAL 1 hour) AND";
- $yyiw_qpart = "date_format(date_entered, '%Y-%u') AS yyiw";
$distinct_qpart = "DISTINCT"; //fallback
}
// except for Labels category
- if (get_pref(Prefs::HEADLINES_NO_DISTINCT) && !($feed == -2 && $cat_view)) {
+ if (get_pref(Prefs::HEADLINES_NO_DISTINCT, $owner_uid) && !($feed == -2 && $cat_view)) {
$distinct_qpart = "";
}
@@ -1691,6 +1689,7 @@ class Feeds extends Handler_Protected {
$vfeed_query_part
$content_query_part
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
$from_qpart
@@ -1715,41 +1714,46 @@ class Feeds extends Handler_Protected {
} else {
// browsing by tag
- if (Config::get(Config::DB_TYPE) == "pgsql") {
- $distinct_columns = str_replace("desc", "", strtolower($order_by));
- $distinct_qpart = "DISTINCT ON (id, $distinct_columns)";
+ if (get_pref(Prefs::HEADLINES_NO_DISTINCT, $owner_uid)) {
+ $distinct_qpart = "";
} else {
- $distinct_qpart = "DISTINCT"; //fallback
+ if (Config::get(Config::DB_TYPE) == "pgsql") {
+ $distinct_columns = str_replace("desc", "", strtolower($order_by));
+ $distinct_qpart = "DISTINCT ON (id, $distinct_columns)";
+ } else {
+ $distinct_qpart = "DISTINCT"; //fallback
+ }
}
$query = "SELECT $distinct_qpart
+ ttrss_entries.id AS id,
date_entered,
+ $yyiw_qpart,
guid,
- note,
- ttrss_entries.id as id,
- title,
+ ttrss_entries.title,
updated,
- unread,
- feed_id,
- marked,
- published,
+ label_cache,
+ tag_cache,
+ always_display_enclosures,
+ site_url,
+ note,
num_comments,
comments,
int_id,
- tag_cache,
- label_cache,
- link,
- lang,
uuid,
- last_read,
- (SELECT hide_images FROM ttrss_feeds WHERE id = feed_id) AS hide_images,
+ lang,
+ hide_images,
+ unread,feed_id,marked,published,link,last_read,
last_marked, last_published,
$since_id_part
$vfeed_query_part
$content_query_part
- author, score
- FROM ttrss_entries, ttrss_user_entries, ttrss_tags
+ 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
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
@@ -1773,7 +1777,7 @@ class Feeds extends Handler_Protected {
}
- static function _get_parent_cats($cat, $owner_uid) {
+ static function _get_parent_cats(int $cat, int $owner_uid) {
$rv = array();
$pdo = Db::pdo();
@@ -1790,7 +1794,7 @@ class Feeds extends Handler_Protected {
return $rv;
}
- static function _get_child_cats($cat, $owner_uid) {
+ static function _get_child_cats(int $cat, int $owner_uid) {
$rv = array();
$pdo = Db::pdo();
@@ -1835,19 +1839,15 @@ class Feeds extends Handler_Protected {
return $rv;
}
- static function _cat_of_feed($feed) {
- $pdo = Db::pdo();
-
- $sth = $pdo->prepare("SELECT cat_id FROM ttrss_feeds
- WHERE id = ?");
- $sth->execute([$feed]);
+ // returns Uncategorized as 0
+ static function _cat_of(int $feed) : int {
+ $feed = ORM::for_table('ttrss_feeds')->find_one($feed);
- if ($row = $sth->fetch()) {
- return $row["cat_id"];
+ if ($feed) {
+ return (int)$feed->cat_id;
} else {
- return false;
+ return -1;
}
-
}
private function _color_of($name) {
@@ -1898,80 +1898,79 @@ class Feeds extends Handler_Protected {
return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 8192)) !== 0;
}
- static function _add_cat($feed_cat, $parent_cat_id = false, $order_id = 0) {
+ static function _remove_cat(int $id, int $owner_uid) {
+ $cat = ORM::for_table('ttrss_feed_categories')
+ ->where('owner_uid', $owner_uid)
+ ->find_one($id);
- if (!$feed_cat) return false;
-
- $feed_cat = mb_substr($feed_cat, 0, 250);
- if (!$parent_cat_id) $parent_cat_id = null;
-
- $pdo = Db::pdo();
- $tr_in_progress = false;
-
- try {
- $pdo->beginTransaction();
- } catch (Exception $e) {
- $tr_in_progress = true;
- }
+ if ($cat)
+ $cat->delete();
+ }
- $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
- WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
- AND title = :title AND owner_uid = :uid");
- $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
+ static function _add_cat(string $title, int $owner_uid, int $parent_cat = null, int $order_id = 0) {
- if (!$sth->fetch()) {
+ $cat = ORM::for_table('ttrss_feed_categories')
+ ->where('owner_uid', $owner_uid)
+ ->where('parent_cat', $parent_cat)
+ ->where('title', $title)
+ ->find_one();
- $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat,order_id)
- VALUES (?, ?, ?, ?)");
- $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id, (int)$order_id]);
+ if (!$cat) {
+ $cat = ORM::for_table('ttrss_feed_categories')->create();
- if (!$tr_in_progress) $pdo->commit();
+ $cat->set([
+ 'owner_uid' => $owner_uid,
+ 'parent_cat' => $parent_cat,
+ 'order_id' => $order_id,
+ 'title' => $title,
+ ]);
- return true;
+ return $cat->save();
}
- $pdo->commit();
-
return false;
}
- static function _get_access_key($feed_id, $is_cat, $owner_uid = false) {
-
- if (!$owner_uid) $owner_uid = $_SESSION["uid"];
+ static function _clear_access_keys(int $owner_uid) {
+ $key = ORM::for_table('ttrss_access_keys')
+ ->where('owner_uid', $owner_uid)
+ ->delete_many();
+ }
- $is_cat = bool_to_sql_bool($is_cat);
+ static function _update_access_key(string $feed_id, bool $is_cat, int $owner_uid) {
+ $key = ORM::for_table('ttrss_access_keys')
+ ->where('owner_uid', $owner_uid)
+ ->where('feed_id', $feed_id)
+ ->where('is_cat', $is_cat)
+ ->delete_many();
- $pdo = Db::pdo();
+ return self::_get_access_key($feed_id, $is_cat, $owner_uid);
+ }
- $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
- WHERE feed_id = ? AND is_cat = ?
- AND owner_uid = ?");
- $sth->execute([$feed_id, $is_cat, $owner_uid]);
+ static function _get_access_key(string $feed_id, bool $is_cat, int $owner_uid) {
+ $key = ORM::for_table('ttrss_access_keys')
+ ->where('owner_uid', $owner_uid)
+ ->where('feed_id', $feed_id)
+ ->where('is_cat', $is_cat)
+ ->find_one();
- if ($row = $sth->fetch()) {
- return $row["access_key"];
+ if ($key) {
+ return $key->access_key;
} else {
- $key = uniqid_short();
-
- $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
- (access_key, feed_id, is_cat, owner_uid)
- VALUES (?, ?, ?, ?)");
+ $key = ORM::for_table('ttrss_access_keys')->create();
- $sth->execute([$key, $feed_id, $is_cat, $owner_uid]);
+ $key->owner_uid = $owner_uid;
+ $key->feed_id = $feed_id;
+ $key->is_cat = $is_cat;
+ $key->access_key = uniqid_short();
- return $key;
+ if ($key->save()) {
+ return $key->access_key;
+ }
}
}
- /**
- * Purge a feed old posts.
- *
- * @param mixed $feed_id The id of the purged feed.
- * @param mixed $purge_interval Olderness of purged posts.
- * @access public
- * @return mixed
- */
- static function _purge($feed_id, $purge_interval) {
+ static function _purge(int $feed_id, int $purge_interval) {
if (!$purge_interval) $purge_interval = self::_get_purge_interval($feed_id);
@@ -2041,22 +2040,14 @@ class Feeds extends Handler_Protected {
return $rows_deleted;
}
- private static function _get_purge_interval($feed_id) {
-
- $pdo = Db::pdo();
+ private static function _get_purge_interval(int $feed_id) {
+ $feed = ORM::for_table('ttrss_feeds')->find_one($feed_id);
- $sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds
- WHERE id = ?");
- $sth->execute([$feed_id]);
-
- if ($row = $sth->fetch()) {
- $purge_interval = $row["purge_interval"];
- $owner_uid = $row["owner_uid"];
-
- if ($purge_interval == 0)
- $purge_interval = get_pref(Prefs::PURGE_OLD_DAYS, $owner_uid);
-
- return $purge_interval;
+ if ($feed) {
+ if ($feed->purge_interval != 0)
+ return $feed->purge_interval;
+ else
+ return get_pref(Prefs::PURGE_OLD_DAYS, $feed->owner_uid);
} else {
return -1;
}
diff --git a/classes/handler/public.php b/classes/handler/public.php
index e4572382e..2de073cc2 100755
--- a/classes/handler/public.php
+++ b/classes/handler/public.php
@@ -1,9 +1,10 @@
<?php
class Handler_Public extends Handler {
- private function generate_syndicated_feed($owner_uid, $feed, $is_cat,
- $limit, $offset, $search,
- $view_mode = false, $format = 'atom', $order = false, $orig_guid = false, $start_ts = false) {
+ // $feed may be a tag
+ private function generate_syndicated_feed(int $owner_uid, string $feed, bool $is_cat,
+ int $limit, int $offset, string $search, string $view_mode = "",
+ string $format = 'atom', string $order = "", string $orig_guid = "", string $start_ts = "") {
$note_style = "background-color : #fff7d5;
border-width : 1px; ".
@@ -48,10 +49,10 @@ class Handler_Public extends Handler {
//$tmppluginhost->load_data();
$handler = $tmppluginhost->get_feed_handler(
- PluginHost::feed_to_pfeed_id($feed));
+ PluginHost::feed_to_pfeed_id((int)$feed));
if ($handler) {
- $qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id($feed), $params);
+ $qfh_ret = $handler->get_headlines(PluginHost::feed_to_pfeed_id((int)$feed), $params);
}
} else {
@@ -63,7 +64,7 @@ class Handler_Public extends Handler {
$feed_site_url = $qfh_ret[2];
/* $last_error = $qfh_ret[3]; */
- $feed_self_url = get_self_url_prefix() .
+ $feed_self_url = Config::get_self_url() .
"/public.php?op=rss&id=$feed&key=" .
Feeds::_get_access_key($feed, false, $owner_uid);
@@ -75,7 +76,7 @@ class Handler_Public extends Handler {
$tpl->readTemplateFromFile("generated_feed.txt");
$tpl->setVariable('FEED_TITLE', $feed_title, true);
- $tpl->setVariable('VERSION', get_version(), true);
+ $tpl->setVariable('VERSION', Config::get_version(), true);
$tpl->setVariable('FEED_URL', htmlspecialchars($feed_self_url), true);
$tpl->setVariable('SELF_URL', htmlspecialchars(get_self_url_prefix()), true);
@@ -151,7 +152,7 @@ class Handler_Public extends Handler {
$tpl->setVariable('ARTICLE_ENCLOSURE_LENGTH', "", true);
}
- list ($og_image, $og_stream) = Article::_get_image($enclosures, $line['content'], $feed_site_url);
+ list ($og_image, $og_stream) = Article::_get_image($enclosures, $line['content'], $feed_site_url, $line);
$tpl->setVariable('ARTICLE_OG_IMAGE', $og_image, true);
@@ -176,10 +177,8 @@ class Handler_Public extends Handler {
$feed['title'] = $feed_title;
$feed['feed_url'] = $feed_self_url;
-
- $feed['self_url'] = get_self_url_prefix();
-
- $feed['articles'] = array();
+ $feed['self_url'] = Config::get_self_url();
+ $feed['articles'] = [];
while ($line = $result->fetch()) {
@@ -304,7 +303,7 @@ class Handler_Public extends Handler {
$search = clean($_REQUEST["q"] ?? "");
$view_mode = clean($_REQUEST["view-mode"] ?? "");
$order = clean($_REQUEST["order"] ?? "");
- $start_ts = (int)clean($_REQUEST["ts"] ?? 0);
+ $start_ts = clean($_REQUEST["ts"] ?? "");
$format = clean($_REQUEST['format'] ?? "atom");
$orig_guid = clean($_REQUEST["orig_guid"] ?? false);
@@ -402,7 +401,7 @@ class Handler_Public extends Handler {
if ($_REQUEST['return'] && mb_strpos($return, Config::get(Config::SELF_URL_PATH)) === 0) {
header("Location: " . clean($_REQUEST['return']));
} else {
- header("Location: " . get_self_url_prefix());
+ header("Location: " . Config::get_self_url());
}
}
}
@@ -624,33 +623,57 @@ class Handler_Public extends Handler {
<!DOCTYPE html>
<html>
<head>
- <title>Database Updater</title>
+ <title>Tiny Tiny RSS: Database Updater</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
- <?= stylesheet_tag("themes/light.css") ?>
- <link rel="shortcut icon" type="image/png" href="images/favicon.png">
<link rel="icon" type="image/png" sizes="72x72" href="images/favicon-72px.png">
+ <link rel="shortcut icon" type="image/png" href="images/favicon.png">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<?php
- echo stylesheet_tag("themes/light.css");
- echo javascript_tag("lib/dojo/dojo.js");
- echo javascript_tag("lib/dojo/tt-rss-layer.js");
- ?>
+ foreach (["lib/dojo/dojo.js",
+ "lib/dojo/tt-rss-layer.js",
+ "js/common.js",
+ "js/utility.js"] as $jsfile) {
+
+ echo javascript_tag($jsfile);
+
+ } ?>
+
+ <?php if (theme_exists(Config::get(Config::LOCAL_OVERRIDE_STYLESHEET))) {
+ echo stylesheet_tag(get_theme_path(Config::get(Config::LOCAL_OVERRIDE_STYLESHEET)));
+ } ?>
+
<style type="text/css">
- span.ok { color : #009000; font-weight : bold; }
- span.err { color : #ff0000; font-weight : bold; }
+ @media (prefers-color-scheme: dark) {
+ body {
+ background : #303030;
+ }
+ }
+
+ body.css_loading * {
+ display : none;
+ }
</style>
+
+ <script type="text/javascript">
+ require({cache:{}});
+ </script>
</head>
- <body class="flat ttrss_utility">
+ <body class="flat ttrss_utility css_loading">
<script type="text/javascript">
- require(['dojo/parser', "dojo/ready", 'dijit/form/Button','dijit/form/CheckBox', 'dijit/form/Form',
- 'dijit/form/Select','dijit/form/TextBox','dijit/form/ValidationTextBox'],function(parser, ready){
- ready(function() {
- parser.parse();
- });
- });
-
- function confirmOP() {
- return confirm("Update the database?");
+ const UtilityApp = {
+ init: function() {
+ require(['dojo/parser', "dojo/ready", 'dijit/form/Button','dijit/form/CheckBox', 'dijit/form/Form',
+ 'dijit/form/Select','dijit/form/TextBox','dijit/form/ValidationTextBox'],function(parser, ready){
+ ready(function() {
+ parser.parse();
+ });
+ });
+ }
+ }
+
+ function confirmDbUpdate() {
+ return confirm(__("Proceed with update?"));
}
</script>
@@ -661,72 +684,66 @@ class Handler_Public extends Handler {
<?php
@$op = clean($_REQUEST["subop"] ?? "");
- $updater = new DbUpdater(Db::pdo(), Config::get(Config::DB_TYPE), SCHEMA_VERSION);
-
- if ($op == "performupdate") {
- if ($updater->is_update_required()) {
- print "<h2>" . T_sprintf("Performing updates to version %d", SCHEMA_VERSION) . "</h2>";
+ $migrations = Config::get_migrations();
- for ($i = $updater->get_schema_version() + 1; $i <= SCHEMA_VERSION; $i++) {
- print "<ul>";
-
- print "<li class='text-info'>" . T_sprintf("Updating to version %d", $i) . "</li>";
+ if ($op == "performupdate") {
+ if ($migrations->is_migration_needed()) {
+ ?>
- print "<li>";
- $result = $updater->update_to($i, true);
- print "</li>";
+ <h2><?= T_sprintf("Performing updates to version %d", Config::SCHEMA_VERSION) ?></h2>
- if (!$result) {
- print "</ul>";
+ <code><pre class="small pre-wrap"><?php
+ Debug::set_enabled(true);
+ Debug::set_loglevel(Debug::LOG_VERBOSE);
+ $result = $migrations->migrate();
+ Debug::set_loglevel(Debug::LOG_NORMAL);
+ Debug::set_enabled(false);
+ ?></pre></code>
- print_error("One of the updates failed. Either retry the process or perform updates manually.");
+ <?php if (!$result) { ?>
+ <?= format_error("One of migrations failed. Either retry the process or perform updates manually.") ?>
- print "<form method='POST'>
- <input type='hidden' name='subop' value='performupdate'>
- <button type='submit' dojoType='dijit.form.Button' class='alt-danger' onclick='return confirmOP()'>".__("Try again")."</button>
- <a href='index.php'>".__("Return to Tiny Tiny RSS")."</a>
- </form>";
+ <form method="post">
+ <?= \Controls\hidden_tag('subop', 'performupdate') ?>
+ <?= \Controls\submit_tag(__("Update"), ["onclick" => "return confirmDbUpdate()"]) ?>
+ </form>
+ <?php } else { ?>
+ <?= format_notice("Update successful.") ?>
- return;
- } else {
- print "<li class='text-success'>" . __("Completed.") . "</li>";
- print "</ul>";
- }
- }
+ <a href="index.php"><?= __("Return to Tiny Tiny RSS") ?></a>
+ <?php }
- print_notice("Your Tiny Tiny RSS database is now updated to the latest version.");
+ } else { ?>
- print "<a href='index.php'>".__("Return to Tiny Tiny RSS")."</a>";
+ <?= format_notice("Database is already up to date.") ?>
- } else {
- print_notice("Tiny Tiny RSS database is up to date.");
+ <a href="index.php"><?= __("Return to Tiny Tiny RSS") ?></a>
- print "<a href='index.php'>".__("Return to Tiny Tiny RSS")."</a>";
+ <?php
}
} else {
- if ($updater->is_update_required()) {
+ if ($migrations->is_migration_needed()) {
- print "<h2>".T_sprintf("Tiny Tiny RSS database needs update to the latest version (%d to %d).",
- $updater->get_schema_version(), SCHEMA_VERSION)."</h2>";
+ ?>
+ <h2><?= T_sprintf("Database schema needs update to the latest version (%d to %d).",
+ Config::get_schema_version(), Config::SCHEMA_VERSION) ?></h2>
- if (Config::get(Config::DB_TYPE) == "mysql") {
- print_error("<strong>READ THIS:</strong> Due to MySQL limitations, your database is not completely protected while updating. ".
- "Errors may put it in an inconsistent state requiring manual rollback. <strong>BACKUP YOUR DATABASE BEFORE CONTINUING.</strong>");
- } else {
- print_warning("Please backup your database before proceeding.");
- }
+ <?= format_warning("Please backup your database before proceeding.") ?>
- print "<form method='POST'>
- <input type='hidden' name='subop' value='performupdate'>
- <button type='submit' dojoType='dijit.form.Button' class='alt-danger' onclick='return confirmOP()'>".__("Perform updates")."</button>
- </form>";
+ <form method="post">
+ <?= \Controls\hidden_tag('subop', 'performupdate') ?>
+ <?= \Controls\submit_tag(__("Update"), ["onclick" => "return confirmDbUpdate()"]) ?>
+ </form>
- } else {
+ <?php
+ } else { ?>
+
+ <?= format_notice("Database is already up to date.") ?>
- print_notice("Tiny Tiny RSS database is up to date.");
+ <a href="index.php"><?= __("Return to Tiny Tiny RSS") ?></a>
- print "<a href='index.php'>".__("Return to Tiny Tiny RSS")."</a>";
+ <?php
}
}
?>
@@ -779,7 +796,7 @@ class Handler_Public extends Handler {
$timestamp = date("Y-m-d", strtotime($timestamp));
- return "tag:" . parse_url(get_self_url_prefix(), PHP_URL_HOST) . ",$timestamp:/$id";
+ return "tag:" . parse_url(Config::get_self_url(), PHP_URL_HOST) . ",$timestamp:/$id";
}
// this should be used very carefully because this endpoint is exposed to unauthenticated users
@@ -817,9 +834,12 @@ class Handler_Public extends Handler {
}
}
- static function _render_login_form() {
+ static function _render_login_form(string $return_to = "") {
header('Cache-Control: public');
+ if ($return_to)
+ $_REQUEST['return'] = $return_to;
+
require_once "login_form.php";
exit;
}
diff --git a/classes/logger.php b/classes/logger.php
index 864b66743..f8abb5f84 100755
--- a/classes/logger.php
+++ b/classes/logger.php
@@ -3,6 +3,10 @@ class Logger {
private static $instance;
private $adapter;
+ const LOG_DEST_SQL = "sql";
+ const LOG_DEST_STDOUT = "stdout";
+ const LOG_DEST_SYSLOG = "syslog";
+
const ERROR_NAMES = [
1 => 'E_ERROR',
2 => 'E_WARNING',
@@ -51,13 +55,13 @@ class Logger {
function __construct() {
switch (Config::get(Config::LOG_DESTINATION)) {
- case "sql":
+ case self::LOG_DEST_SQL:
$this->adapter = new Logger_SQL();
break;
- case "syslog":
+ case self::LOG_DEST_SYSLOG:
$this->adapter = new Logger_Syslog();
break;
- case "stdout":
+ case self::LOG_DEST_STDOUT:
$this->adapter = new Logger_Stdout();
break;
default:
diff --git a/classes/logger/sql.php b/classes/logger/sql.php
index d21934aa6..784ebef31 100755
--- a/classes/logger/sql.php
+++ b/classes/logger/sql.php
@@ -3,12 +3,18 @@ class Logger_SQL implements Logger_Adapter {
private $pdo;
- function log_error(int $errno, string $errstr, string $file, int $line, $context) {
+ function __construct() {
+ $conn = get_class($this);
- // separate PDO connection object is used for logging
- if (!$this->pdo) $this->pdo = Db::instance()->pdo_connect();
+ ORM::configure(Db::get_dsn(), null, $conn);
+ ORM::configure('username', Config::get(Config::DB_USER), $conn);
+ ORM::configure('password', Config::get(Config::DB_PASS), $conn);
+ ORM::configure('return_result_sets', true, $conn);
+ }
- if ($this->pdo && get_schema_version() > 117) {
+ function log_error(int $errno, string $errstr, string $file, int $line, $context) {
+
+ if (Config::get_schema_version() > 117) {
// limit context length, DOMDocument dumps entire XML in here sometimes, which may be huge
$context = mb_substr($context, 0, 8192);
@@ -36,12 +42,19 @@ class Logger_SQL implements Logger_Adapter {
// this would cause a PDOException on insert below
$owner_uid = !empty($_SESSION["uid"]) ? $_SESSION["uid"] : null;
- $sth = $this->pdo->prepare("INSERT INTO ttrss_error_log
- (errno, errstr, filename, lineno, context, owner_uid, created_at) VALUES
- (?, ?, ?, ?, ?, ?, NOW())");
- $sth->execute([$errno, $errstr, $file, (int)$line, $context, $owner_uid]);
+ $entry = ORM::for_table('ttrss_error_log', get_class($this))->create();
+
+ $entry->set([
+ 'errno' => $errno,
+ 'errstr' => $errstr,
+ 'filename' => $file,
+ 'lineno' => (int)$line,
+ 'context' => $context,
+ 'owner_uid' => $owner_uid,
+ 'created_at' => Db::NOW(),
+ ]);
- return $sth->rowCount();
+ return $entry->save();
}
return false;
diff --git a/classes/mailer.php b/classes/mailer.php
index a4270ba88..564338f69 100644
--- a/classes/mailer.php
+++ b/classes/mailer.php
@@ -1,8 +1,6 @@
<?php
class Mailer {
- // TODO: support HTML mail (i.e. MIME messages)
-
- private $last_error = "Unable to send mail: check local configuration.";
+ private $last_error = "";
function mail($params) {
@@ -10,11 +8,10 @@ class Mailer {
$to_address = $params["to_address"];
$subject = $params["subject"];
$message = $params["message"];
- $message_html = $params["message_html"];
- $from_name = $params["from_name"] ? $params["from_name"] : Config::get(Config::SMTP_FROM_NAME);
- $from_address = $params["from_address"] ? $params["from_address"] : Config::get(Config::SMTP_FROM_ADDRESS);
-
- $additional_headers = $params["headers"] ? $params["headers"] : [];
+ $message_html = $params["message_html"] ?? "";
+ $from_name = $params["from_name"] ?? Config::get(Config::SMTP_FROM_NAME);
+ $from_address = $params["from_address"] ?? Config::get(Config::SMTP_FROM_ADDRESS);
+ $additional_headers = $params["headers"] ?? [];
$from_combined = $from_name ? "$from_name <$from_address>" : $from_address;
$to_combined = $to_name ? "$to_name <$to_address>" : $to_address;
@@ -40,11 +37,18 @@ class Mailer {
$headers = [ "From: $from_combined", "Content-Type: text/plain; charset=UTF-8" ];
- return mail($to_combined, $subject, $message, implode("\r\n", array_merge($headers, $additional_headers)));
+ $rc = mail($to_combined, $subject, $message, implode("\r\n", array_merge($headers, $additional_headers)));
+
+ if (!$rc) {
+ $this->set_error(error_get_last()['message']);
+ }
+
+ return $rc;
}
function set_error($message) {
$this->last_error = $message;
+ user_error("Error sending mail: $message", E_USER_WARNING);
}
function error() {
diff --git a/classes/opml.php b/classes/opml.php
index 6c7cab606..f8e9f6728 100644
--- a/classes/opml.php
+++ b/classes/opml.php
@@ -31,7 +31,7 @@ class OPML extends Handler_Protected {
<body class='claro ttrss_utility'>
<h1>".__('OPML Utility')."</h1><div class='content'>";
- Feeds::_add_cat("Imported feeds");
+ Feeds::_add_cat("Imported feeds", $owner_uid);
$this->opml_notice(__("Importing OPML..."));
@@ -151,7 +151,7 @@ class OPML extends Handler_Protected {
# export tt-rss settings
if ($include_settings) {
- $out .= "<outline text=\"tt-rss-prefs\" schema-version=\"".SCHEMA_VERSION."\">";
+ $out .= "<outline text=\"tt-rss-prefs\" schema-version=\"".Config::SCHEMA_VERSION."\">";
$sth = $this->pdo->prepare("SELECT pref_name, value FROM ttrss_user_prefs2 WHERE
profile IS NULL AND owner_uid = ? ORDER BY pref_name");
@@ -166,7 +166,7 @@ class OPML extends Handler_Protected {
$out .= "</outline>";
- $out .= "<outline text=\"tt-rss-labels\" schema-version=\"".SCHEMA_VERSION."\">";
+ $out .= "<outline text=\"tt-rss-labels\" schema-version=\"".Config::SCHEMA_VERSION."\">";
$sth = $this->pdo->prepare("SELECT * FROM ttrss_labels2 WHERE
owner_uid = ?");
@@ -183,7 +183,7 @@ class OPML extends Handler_Protected {
$out .= "</outline>";
- $out .= "<outline text=\"tt-rss-filters\" schema-version=\"".SCHEMA_VERSION."\">";
+ $out .= "<outline text=\"tt-rss-filters\" schema-version=\"".Config::SCHEMA_VERSION."\">";
$sth = $this->pdo->prepare("SELECT * FROM ttrss_filters2
WHERE owner_uid = ? ORDER BY id");
@@ -521,9 +521,8 @@ class OPML extends Handler_Protected {
if ($cat_id === false) {
$order_id = (int) $root_node->attributes->getNamedItem('ttrssSortOrder')->nodeValue;
- if (!$order_id) $order_id = 0;
- Feeds::_add_cat($cat_title, $parent_id, $order_id);
+ Feeds::_add_cat($cat_title, $_SESSION['uid'], $parent_id ? $parent_id : null, (int)$order_id);
$cat_id = $this->get_feed_category($cat_title, $parent_id);
}
@@ -635,7 +634,7 @@ class OPML extends Handler_Protected {
}
static function get_publish_url(){
- return get_self_url_prefix() .
+ return Config::get_self_url() .
"/public.php?op=publishOpml&key=" .
Feeds::_get_access_key('OPML:Publish', false, $_SESSION["uid"]);
}
diff --git a/classes/plugin.php b/classes/plugin.php
index 6c572467a..ecafa7888 100644
--- a/classes/plugin.php
+++ b/classes/plugin.php
@@ -2,11 +2,10 @@
abstract class Plugin {
const API_VERSION_COMPAT = 1;
- /** @var PDO */
+ /** @var PDO $pdo */
protected $pdo;
- /* @var PluginHost $host */
- abstract function init($host);
+ abstract function init(PluginHost $host);
abstract function about();
// return array(1.0, "plugin", "No description", "No author", false);
@@ -26,6 +25,10 @@ abstract class Plugin {
return false;
}
+ function csrf_ignore($method) {
+ return false;
+ }
+
function get_js() {
return "";
}
@@ -55,7 +58,4 @@ abstract class Plugin {
return vsprintf($this->__($msgid), $args);
}
- function csrf_ignore($method) {
- return false;
- }
}
diff --git a/classes/pluginhost.php b/classes/pluginhost.php
index b6f645a9c..17d1e0c5f 100755
--- a/classes/pluginhost.php
+++ b/classes/pluginhost.php
@@ -61,7 +61,7 @@ class PluginHost {
const HOOK_FEED_BASIC_INFO = "hook_feed_basic_info"; // hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed_id, $auth_login, $auth_pass) (byref)
const HOOK_SEND_LOCAL_FILE = "hook_send_local_file"; // hook_send_local_file($filename)
const HOOK_UNSUBSCRIBE_FEED = "hook_unsubscribe_feed"; // hook_unsubscribe_feed($feed_id, $owner_uid)
- const HOOK_SEND_MAIL = "hook_send_mail"; // hook_send_mail($mailer, $params)
+ const HOOK_SEND_MAIL = "hook_send_mail"; // hook_send_mail(Mailer $mailer, $params)
const HOOK_FILTER_TRIGGERED = "hook_filter_triggered"; // hook_filter_triggered($feed_id, $owner_uid, $article, $matched_filters, $matched_rules, $article_filters)
const HOOK_GET_FULL_TEXT = "hook_get_full_text"; // hook_get_full_text($url)
const HOOK_ARTICLE_IMAGE = "hook_article_image"; // hook_article_image($enclosures, $content, $site_url)
@@ -274,16 +274,14 @@ class PluginHost {
$class = trim($class);
$class_file = strtolower(basename(clean($class)));
- if (!is_dir(__DIR__ . "/../plugins/$class_file") &&
- !is_dir(__DIR__ . "/../plugins.local/$class_file")) continue;
-
// try system plugin directory first
- $file = __DIR__ . "/../plugins/$class_file/init.php";
- $vendor_dir = __DIR__ . "/../plugins/$class_file/vendor";
+ $file = dirname(__DIR__) . "/plugins/$class_file/init.php";
if (!file_exists($file)) {
- $file = __DIR__ . "/../plugins.local/$class_file/init.php";
- $vendor_dir = __DIR__ . "/../plugins.local/$class_file/vendor";
+ $file = dirname(__DIR__) . "/plugins.local/$class_file/init.php";
+
+ if (!file_exists($file))
+ continue;
}
if (!isset($this->plugins[$class])) {
@@ -296,27 +294,7 @@ class PluginHost {
if (class_exists($class) && is_subclass_of($class, "Plugin")) {
- // register plugin autoloader if necessary, for namespaced classes ONLY
- // layout corresponds to tt-rss main /vendor/author/Package/Class.php
-
- if (file_exists($vendor_dir)) {
- spl_autoload_register(function($class) use ($vendor_dir) {
-
- if (strpos($class, '\\') !== false) {
- list ($namespace, $class_name) = explode('\\', $class, 2);
-
- if ($namespace && $class_name) {
- $class_file = "$vendor_dir/$namespace/" . str_replace('\\', '/', $class_name) . ".php";
-
- if (file_exists($class_file))
- require_once $class_file;
- }
- }
- });
- }
-
$plugin = new $class($this);
-
$plugin_api = $plugin->api_version();
if ($plugin_api < self::API_VERSION) {
@@ -491,7 +469,7 @@ class PluginHost {
}
}
- function set(Plugin $sender, string $name, $value, bool $sync = true) {
+ function set(Plugin $sender, string $name, $value) {
$idx = get_class($sender);
if (!isset($this->storage[$idx]))
@@ -499,7 +477,19 @@ class PluginHost {
$this->storage[$idx][$name] = $value;
- if ($sync) $this->save_data(get_class($sender));
+ $this->save_data(get_class($sender));
+ }
+
+ function set_array(Plugin $sender, array $params) {
+ $idx = get_class($sender);
+
+ if (!isset($this->storage[$idx]))
+ $this->storage[$idx] = array();
+
+ foreach ($params as $name => $value)
+ $this->storage[$idx][$name] = $value;
+
+ $this->save_data(get_class($sender));
}
function get(Plugin $sender, string $name, $default_value = false) {
@@ -514,6 +504,14 @@ class PluginHost {
}
}
+ function get_array(Plugin $sender, string $name, array $default_value = []) {
+ $tmp = $this->get($sender, $name);
+
+ if (!is_array($tmp)) $tmp = $default_value;
+
+ return $tmp;
+ }
+
function get_all($sender) {
$idx = get_class($sender);
@@ -601,7 +599,7 @@ class PluginHost {
// handled by classes/pluginhandler.php, requires valid session
function get_method_url(Plugin $sender, string $method, $params = []) {
- return get_self_url_prefix() . "/backend.php?" .
+ return Config::get_self_url() . "/backend.php?" .
http_build_query(
array_merge(
[
@@ -614,7 +612,7 @@ class PluginHost {
// shortcut syntax (disabled for now)
/* function get_method_url(Plugin $sender, string $method, $params) {
- return get_self_url_prefix() . "/backend.php?" .
+ return Config::get_self_url() . "/backend.php?" .
http_build_query(
array_merge(
[
@@ -626,7 +624,7 @@ class PluginHost {
// WARNING: endpoint in public.php, exposed to unauthenticated users
function get_public_method_url(Plugin $sender, string $method, $params = []) {
if ($sender->is_public_method($method)) {
- return get_self_url_prefix() . "/public.php?" .
+ return Config::get_self_url() . "/public.php?" .
http_build_query(
array_merge(
[
@@ -637,4 +635,15 @@ class PluginHost {
user_error("get_public_method_url: requested method '$method' of '" . get_class($sender) . "' is private.");
}
}
+
+ function get_plugin_dir(Plugin $plugin) {
+ $ref = new ReflectionClass(get_class($plugin));
+ return dirname($ref->getFileName());
+ }
+
+ // TODO: use get_plugin_dir()
+ function is_local(Plugin $plugin) {
+ $ref = new ReflectionClass(get_class($plugin));
+ return basename(dirname(dirname($ref->getFileName()))) == "plugins.local";
+ }
}
diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php
index 3b4afab26..788104d38 100755
--- a/classes/pref/feeds.php
+++ b/classes/pref/feeds.php
@@ -1,5 +1,10 @@
<?php
class Pref_Feeds extends Handler_Protected {
+ const E_ICON_FILE_TOO_LARGE = 'E_ICON_FILE_TOO_LARGE';
+ const E_ICON_RENAME_FAILED = 'E_ICON_RENAME_FAILED';
+ const E_ICON_UPLOAD_FAILED = 'E_ICON_UPLOAD_FAILED';
+ const E_ICON_UPLOAD_SUCCESS = 'E_ICON_UPLOAD_SUCCESS';
+
function csrf_ignore($method) {
$csrf_ignored = array("index", "getfeedtree", "savefeedorder");
@@ -22,14 +27,16 @@ class Pref_Feeds extends Handler_Protected {
return $rv;
}
- function renamecat() {
+ function renameCat() {
+ $cat = ORM::for_table("ttrss_feed_categories")
+ ->where("owner_uid", $_SESSION["uid"])
+ ->find_one($_REQUEST['id']);
+
$title = clean($_REQUEST['title']);
- $id = clean($_REQUEST['id']);
- if ($title) {
- $sth = $this->pdo->prepare("UPDATE ttrss_feed_categories SET
- title = ? WHERE id = ? AND owner_uid = ?");
- $sth->execute([$title, $id, $_SESSION['uid']]);
+ if ($cat && $title) {
+ $cat->title = $title;
+ $cat->save();
}
}
@@ -433,78 +440,67 @@ class Pref_Feeds extends Handler_Protected {
}
}
- function removeicon() {
- $feed_id = clean($_REQUEST["feed_id"]);
-
- $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
- WHERE id = ? AND owner_uid = ?");
- $sth->execute([$feed_id, $_SESSION['uid']]);
+ function removeIcon() {
+ $feed_id = (int) $_REQUEST["feed_id"];
+ $icon_file = Config::get(Config::ICONS_DIR) . "/$feed_id.ico";
- if ($row = $sth->fetch()) {
- @unlink(Config::get(Config::ICONS_DIR) . "/$feed_id.ico");
+ $feed = ORM::for_table('ttrss_feeds')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->find_one($feed_id);
- $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET favicon_avg_color = NULL, favicon_last_checked = '1970-01-01'
- where id = ?");
- $sth->execute([$feed_id]);
+ if ($feed && file_exists($icon_file)) {
+ if (unlink($icon_file)) {
+ $feed->set([
+ 'favicon_avg_color' => null,
+ 'favicon_last_checked' => '1970-01-01',
+ 'favicon_is_custom' => false,
+ ]);
+ $feed->save();
+ }
}
}
- function uploadicon() {
- header("Content-type: text/html");
+ function uploadIcon() {
+ $feed_id = (int) $_REQUEST['feed_id'];
+ $tmp_file = tempnam(Config::get(Config::CACHE_DIR) . '/upload', 'icon');
- if (is_uploaded_file($_FILES['icon_file']['tmp_name'])) {
- $tmp_file = tempnam(Config::get(Config::CACHE_DIR) . '/upload', 'icon');
+ // default value
+ $rc = self::E_ICON_UPLOAD_FAILED;
- if (!$tmp_file)
- return;
+ $feed = ORM::for_table('ttrss_feeds')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->find_one($feed_id);
- $result = move_uploaded_file($_FILES['icon_file']['tmp_name'], $tmp_file);
+ if ($feed && $tmp_file && move_uploaded_file($_FILES['icon_file']['tmp_name'], $tmp_file)) {
+ if (filesize($tmp_file) < Config::get(Config::MAX_FAVICON_FILE_SIZE)) {
- if (!$result) {
- return;
- }
- } else {
- return;
- }
+ $new_filename = Config::get(Config::ICONS_DIR) . "/$feed_id.ico";
- $icon_file = $tmp_file;
- $feed_id = clean($_REQUEST["feed_id"]);
- $rc = 2; // failed
+ if (file_exists($new_filename)) unlink($new_filename);
+ if (rename($tmp_file, $new_filename)) {
+ chmod($new_filename, 0644);
- if ($icon_file && is_file($icon_file) && $feed_id) {
- if (filesize($icon_file) < 65535) {
+ $feed->set([
+ 'favicon_avg_color' => null,
+ 'favicon_is_custom' => true,
+ ]);
- $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds
- WHERE id = ? AND owner_uid = ?");
- $sth->execute([$feed_id, $_SESSION['uid']]);
-
- if ($row = $sth->fetch()) {
- $new_filename = Config::get(Config::ICONS_DIR) . "/$feed_id.ico";
-
- if (file_exists($new_filename)) unlink($new_filename);
-
- if (rename($icon_file, $new_filename)) {
- chmod($new_filename, 644);
-
- $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET
- favicon_avg_color = ''
- WHERE id = ?");
- $sth->execute([$feed_id]);
+ if ($feed->save()) {
+ $rc = self::E_ICON_UPLOAD_SUCCESS;
+ }
- $rc = Feeds::_get_icon($feed_id);
+ } else {
+ $rc = self::E_ICON_RENAME_FAILED;
}
- }
} else {
- $rc = 1;
+ $rc = self::E_ICON_FILE_TOO_LARGE;
}
}
- if ($icon_file && is_file($icon_file)) {
- unlink($icon_file);
- }
+ if (file_exists($tmp_file))
+ unlink($tmp_file);
- print $rc;
- return;
+ print json_encode(['rc' => $rc, 'icon_url' => Feeds::_get_icon($feed_id)]);
}
function editfeed() {
@@ -513,11 +509,11 @@ class Pref_Feeds extends Handler_Protected {
$feed_id = (int)clean($_REQUEST["id"]);
- $sth = $this->pdo->prepare("SELECT * FROM ttrss_feeds WHERE id = ? AND
- owner_uid = ?");
- $sth->execute([$feed_id, $_SESSION['uid']]);
+ $row = ORM::for_table('ttrss_feeds')
+ ->where("owner_uid", $_SESSION["uid"])
+ ->find_one($feed_id)->as_array();
- if ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
+ if ($row) {
ob_start();
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_EDIT_FEED, $feed_id);
@@ -694,7 +690,7 @@ class Pref_Feeds extends Handler_Protected {
$purge_intl = (int) clean($_POST["purge_interval"] ?? 0);
$feed_id = (int) clean($_POST["id"] ?? 0); /* editSave */
$feed_ids = explode(",", clean($_POST["ids"] ?? "")); /* batchEditSave */
- $cat_id = (int) clean($_POST["cat_id"]);
+ $cat_id = (int) clean($_POST["cat_id"] ?? 0);
$auth_login = clean($_POST["auth_login"]);
$auth_pass = clean($_POST["auth_pass"]);
$private = checkbox_to_sql_bool(clean($_POST["private"] ?? ""));
@@ -710,7 +706,7 @@ class Pref_Feeds extends Handler_Protected {
$mark_unread_on_update = checkbox_to_sql_bool(
clean($_POST["mark_unread_on_update"] ?? ""));
- $feed_language = clean($_POST["feed_language"]);
+ $feed_language = clean($_POST["feed_language"] ?? "");
if (!$batch) {
@@ -720,48 +716,32 @@ class Pref_Feeds extends Handler_Protected {
$reset_basic_info = $orig_feed_url != $feed_url; */
- $sth = $this->pdo->prepare("UPDATE ttrss_feeds SET
- cat_id = :cat_id,
- title = :title,
- feed_url = :feed_url,
- site_url = :site_url,
- update_interval = :upd_intl,
- purge_interval = :purge_intl,
- auth_login = :auth_login,
- auth_pass = :auth_pass,
- auth_pass_encrypted = false,
- private = :private,
- cache_images = :cache_images,
- hide_images = :hide_images,
- include_in_digest = :include_in_digest,
- always_display_enclosures = :always_display_enclosures,
- mark_unread_on_update = :mark_unread_on_update,
- feed_language = :feed_language
- WHERE id = :id AND owner_uid = :uid");
-
- $sth->execute([":title" => $feed_title,
- ":cat_id" => $cat_id ? $cat_id : null,
- ":feed_url" => $feed_url,
- ":site_url" => $site_url,
- ":upd_intl" => $upd_intl,
- ":purge_intl" => $purge_intl,
- ":auth_login" => $auth_login,
- ":auth_pass" => $auth_pass,
- ":private" => (int)$private,
- ":cache_images" => (int)$cache_images,
- ":hide_images" => (int)$hide_images,
- ":include_in_digest" => (int)$include_in_digest,
- ":always_display_enclosures" => (int)$always_display_enclosures,
- ":mark_unread_on_update" => (int)$mark_unread_on_update,
- ":feed_language" => $feed_language,
- ":id" => $feed_id,
- ":uid" => $_SESSION['uid']]);
-
-/* if ($reset_basic_info) {
- RSSUtils::set_basic_feed_info($feed_id);
- } */
-
- PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_SAVE_FEED, $feed_id);
+ $feed = ORM::for_table('ttrss_feeds')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->find_one($feed_id);
+
+ if ($feed) {
+
+ $feed->title = $feed_title;
+ $feed->cat_id = $cat_id ? $cat_id : null;
+ $feed->feed_url = $feed_url;
+ $feed->site_url = $site_url;
+ $feed->update_interval = $upd_intl;
+ $feed->purge_interval = $purge_intl;
+ $feed->auth_login = $auth_login;
+ $feed->auth_pass = $auth_pass;
+ $feed->private = (int)$private;
+ $feed->cache_images = (int)$cache_images;
+ $feed->hide_images = (int)$hide_images;
+ $feed->feed_language = $feed_language;
+ $feed->include_in_digest = (int)$include_in_digest;
+ $feed->always_display_enclosures = (int)$always_display_enclosures;
+ $feed->mark_unread_on_update = (int)$mark_unread_on_update;
+
+ $feed->save();
+
+ PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_SAVE_FEED, $feed_id);
+ }
} else {
$feed_data = array();
@@ -874,14 +854,14 @@ class Pref_Feeds extends Handler_Protected {
function removeCat() {
$ids = explode(",", clean($_REQUEST["ids"]));
foreach ($ids as $id) {
- $this->remove_feed_category($id, $_SESSION["uid"]);
+ Feeds::_remove_cat((int)$id, $_SESSION["uid"]);
}
}
function addCat() {
$feed_cat = clean($_REQUEST["cat"]);
- Feeds::_add_cat($feed_cat);
+ Feeds::_add_cat($feed_cat, $_SESSION['uid']);
}
function importOpml() {
@@ -1003,10 +983,6 @@ class Pref_Feeds extends Handler_Protected {
private function index_opml() {
?>
- <h3><?= __("Using OPML you can export and import your feeds, filters, labels and Tiny Tiny RSS settings.") ?></h3>
-
- <?php print_notice("Only main settings profile can be migrated using OPML.") ?>
-
<form id='opml_import_form' method='post' enctype='multipart/form-data'>
<label class='dijitButton'><?= __("Choose file...") ?>
<input style='display : none' id='opml_file' name='opml_file' type='file'>
@@ -1015,20 +991,24 @@ class Pref_Feeds extends Handler_Protected {
<input type='hidden' name='csrf_token' value="<?= $_SESSION['csrf_token'] ?>">
<input type='hidden' name='method' value='importOpml'>
<button dojoType='dijit.form.Button' class='alt-primary' onclick="return Helpers.OPML.import()" type="submit">
+ <?= \Controls\icon("file_upload") ?>
<?= __('Import OPML') ?>
</button>
</form>
<hr/>
+ <?php print_notice("Only main settings profile can be migrated using OPML.") ?>
+
<form dojoType='dijit.form.Form' id='opmlExportForm' style='display : inline-block'>
<button dojoType='dijit.form.Button' onclick='Helpers.OPML.export()'>
+ <?= \Controls\icon("file_download") ?>
<?= __('Export OPML') ?>
</button>
<label class='checkbox'>
<?= \Controls\checkbox_tag("include_settings", true, "1") ?>
- <?= __("Include settings") ?>
+ <?= __("Include tt-rss settings") ?>
</label>
</form>
@@ -1036,12 +1016,10 @@ class Pref_Feeds extends Handler_Protected {
<h2><?= __("Published OPML") ?></h2>
- <p>
- <?= __('Your OPML can be published publicly and can be subscribed by anyone who knows the URL below.') ?>
- <?= __("Published OPML does not include your Tiny Tiny RSS settings, feeds that require authentication or feeds hidden from Popular feeds.") ?>
- </p>
+ <?= 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>
@@ -1052,14 +1030,16 @@ class Pref_Feeds extends Handler_Protected {
private function index_shared() {
?>
- <h3><?= __('Published articles can be subscribed by anyone who knows the following URL:') ?></h3>
+ <?= format_notice('Published articles can be subscribed by anyone who knows the following URL:') ?></h3>
<button dojoType='dijit.form.Button' class='alt-primary'
onclick="CommonDialogs.generatedFeed(-2, false)">
+ <?= \Controls\icon('share') ?>
<?= __('Display URL') ?>
</button>
<button class='alt-danger' dojoType='dijit.form.Button' onclick='return Helpers.Feeds.clearFeedAccessKeys()'>
+ <?= \Controls\icon('delete') ?>
<?= __('Clear all generated URLs') ?>
</button>
@@ -1188,12 +1168,6 @@ class Pref_Feeds extends Handler_Protected {
print json_encode($rv);
}
- private function remove_feed_category($id, $owner_uid) {
- $sth = $this->pdo->prepare("DELETE FROM ttrss_feed_categories
- WHERE id = ? AND owner_uid = ?");
- $sth->execute([$id, $owner_uid]);
- }
-
static function remove_feed($id, $owner_uid) {
if (PluginHost::getInstance()->run_hooks_until(PluginHost::HOOK_UNSUBSCRIBE_FEED, true, $id, $owner_uid))
@@ -1273,12 +1247,16 @@ class Pref_Feeds extends Handler_Protected {
}
}
+ function clearKeys() {
+ return Feeds::_clear_access_keys($_SESSION['uid']);
+ }
+
function getOPMLKey() {
print json_encode(["link" => OPML::get_publish_url()]);
}
function regenOPMLKey() {
- $this->update_feed_access_key('OPML:Publish',
+ Feeds::_update_access_key('OPML:Publish',
false, $_SESSION["uid"]);
print json_encode(["link" => OPML::get_publish_url()]);
@@ -1288,17 +1266,17 @@ class Pref_Feeds extends Handler_Protected {
$feed_id = clean($_REQUEST['id']);
$is_cat = clean($_REQUEST['is_cat']);
- $new_key = $this->update_feed_access_key($feed_id, $is_cat, $_SESSION["uid"]);
+ $new_key = Feeds::_update_access_key($feed_id, $is_cat, $_SESSION["uid"]);
print json_encode(["link" => $new_key]);
}
- function getsharedurl() {
+ function getSharedURL() {
$feed_id = clean($_REQUEST['id']);
$is_cat = clean($_REQUEST['is_cat']) == "true";
$search = clean($_REQUEST['search']);
- $link = get_self_url_prefix() . "/public.php?" . http_build_query([
+ $link = Config::get_self_url() . "/public.php?" . http_build_query([
'op' => 'rss',
'id' => $feed_id,
'is_cat' => (int)$is_cat,
@@ -1312,23 +1290,6 @@ class Pref_Feeds extends Handler_Protected {
]);
}
- private function update_feed_access_key($feed_id, $is_cat, $owner_uid) {
-
- // clear old value and generate new one
- $sth = $this->pdo->prepare("DELETE FROM ttrss_access_keys
- WHERE feed_id = ? AND is_cat = ? AND owner_uid = ?");
- $sth->execute([$feed_id, bool_to_sql_bool($is_cat), $owner_uid]);
-
- return Feeds::_get_access_key($feed_id, $is_cat, $owner_uid);
- }
-
- // Silent
- function clearKeys() {
- $sth = $this->pdo->prepare("DELETE FROM ttrss_access_keys WHERE
- owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
- }
-
private function calculate_children_count($cat) {
$c = 0;
diff --git a/classes/pref/filters.php b/classes/pref/filters.php
index a6ea9f982..29d309dbb 100755
--- a/classes/pref/filters.php
+++ b/classes/pref/filters.php
@@ -51,8 +51,8 @@ class Pref_Filters extends Handler_Protected {
$filter = array();
$filter["enabled"] = true;
- $filter["match_any_rule"] = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"]));
- $filter["inverse"] = checkbox_to_sql_bool(clean($_REQUEST["inverse"]));
+ $filter["match_any_rule"] = checkbox_to_sql_bool(clean($_REQUEST["match_any_rule"] ?? false));
+ $filter["inverse"] = checkbox_to_sql_bool(clean($_REQUEST["inverse"] ?? false));
$filter["rules"] = array();
$filter["actions"] = array("dummy-action");
diff --git a/classes/pref/labels.php b/classes/pref/labels.php
index 5bc094d55..2cdb919ce 100644
--- a/classes/pref/labels.php
+++ b/classes/pref/labels.php
@@ -8,14 +8,12 @@ class Pref_Labels extends Handler_Protected {
}
function edit() {
- $label_id = clean($_REQUEST['id']);
+ $label = ORM::for_table('ttrss_labels2')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->find_one($_REQUEST['id']);
- $sth = $this->pdo->prepare("SELECT id, caption, fg_color, bg_color FROM ttrss_labels2 WHERE
- id = ? AND owner_uid = ?");
- $sth->execute([$label_id, $_SESSION['uid']]);
-
- if ($line = $sth->fetch(PDO::FETCH_ASSOC)) {
- print json_encode($line);
+ if ($label) {
+ print json_encode($label->as_array());
}
}
diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php
index ba63d76b3..16c41df9d 100644
--- a/classes/pref/prefs.php
+++ b/classes/pref/prefs.php
@@ -1,4 +1,5 @@
<?php
+use chillerlan\QRCode;
class Pref_Prefs extends Handler_Protected {
@@ -7,6 +8,15 @@ class Pref_Prefs extends Handler_Protected {
private $pref_help_bottom = [];
private $pref_blacklist = [];
+ const PI_RES_ALREADY_INSTALLED = "PI_RES_ALREADY_INSTALLED";
+ const PI_RES_SUCCESS = "PI_RES_SUCCESS";
+ const PI_ERR_NO_CLASS = "PI_ERR_NO_CLASS";
+ const PI_ERR_NO_INIT_PHP = "PI_ERR_NO_INIT_PHP";
+ const PI_ERR_EXEC_FAILED = "PI_ERR_EXEC_FAILED";
+ const PI_ERR_NO_TEMPDIR = "PI_ERR_NO_TEMPDIR";
+ const PI_ERR_PLUGIN_NOT_FOUND = "PI_ERR_PLUGIN_NOT_FOUND";
+ const PI_ERR_NO_WORKDIR = "PI_ERR_NO_WORKDIR";
+
function csrf_ignore($method) {
$csrf_ignored = array("index", "updateself", "otpqrcode");
@@ -64,6 +74,7 @@ class Pref_Prefs extends Handler_Protected {
'BLOCK_SEPARATOR',
Prefs::SSL_CERT_SERIAL,
'BLOCK_SEPARATOR',
+ Prefs::DISABLE_CONDITIONAL_COUNTERS,
Prefs::HEADLINES_NO_DISTINCT,
],
__('Debugging') => [
@@ -105,6 +116,7 @@ class Pref_Prefs extends Handler_Protected {
Prefs::USER_CSS_THEME => array(__("Theme")),
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")),
];
// hidden in the main prefs UI (use to hide things that have description set above)
@@ -209,48 +221,45 @@ class Pref_Prefs extends Handler_Protected {
}
}
- function changeemail() {
+ function changePersonalData() {
+
+ $user = ORM::for_table('ttrss_users')->find_one($_SESSION['uid']);
+ $new_email = clean($_POST['email']);
- $email = clean($_POST["email"]);
- $full_name = clean($_POST["full_name"]);
- $active_uid = $_SESSION["uid"];
+ if ($user) {
+ $user->full_name = clean($_POST['full_name']);
- $sth = $this->pdo->prepare("SELECT email, login, full_name FROM ttrss_users WHERE id = ?");
- $sth->execute([$active_uid]);
+ if ($user->email != $new_email)
+ Logger::log(E_USER_NOTICE, "Email address of user ".$user->login." has been changed to ${new_email}.");
- if ($row = $sth->fetch()) {
- $old_email = $row["email"];
+ if ($user->email && $user->email != $new_email) {
- if ($old_email != $email) {
$mailer = new Mailer();
$tpl = new Templator();
$tpl->readTemplateFromFile("mail_change_template.txt");
- $tpl->setVariable('LOGIN', $row["login"]);
- $tpl->setVariable('NEWMAIL', $email);
+ $tpl->setVariable('LOGIN', $user->login);
+ $tpl->setVariable('NEWMAIL', $new_email);
$tpl->setVariable('TTRSS_HOST', Config::get(Config::SELF_URL_PATH));
$tpl->addBlock('message');
$tpl->generateOutputToString($message);
- $mailer->mail(["to_name" => $row["login"],
- "to_address" => $row["email"],
- "subject" => "[tt-rss] Mail address change notification",
+ $mailer->mail(["to_name" => $user->login,
+ "to_address" => $user->email,
+ "subject" => "[tt-rss] Email address change notification",
"message" => $message]);
+ $user->email = $new_email;
}
- }
- $sth = $this->pdo->prepare("UPDATE ttrss_users SET email = ?,
- full_name = ? WHERE id = ?");
- $sth->execute([$email, $full_name, $active_uid]);
+ $user->save();
+ }
print __("Your personal data has been saved.");
-
- return;
}
function resetconfig() {
@@ -261,21 +270,13 @@ class Pref_Prefs extends Handler_Protected {
private function index_auth_personal() {
- $sth = $this->pdo->prepare("SELECT email,full_name,otp_enabled,
- access_level FROM ttrss_users
- WHERE id = ?");
- $sth->execute([$_SESSION["uid"]]);
- $row = $sth->fetch();
-
- $email = htmlspecialchars($row["email"]);
- $full_name = htmlspecialchars($row["full_name"]);
- $otp_enabled = sql_bool_to_bool($row["otp_enabled"]);
+ $user = ORM::for_table('ttrss_users')->find_one($_SESSION['uid']);
?>
<form dojoType='dijit.form.Form'>
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
- <?= \Controls\hidden_tag("method", "changeemail") ?>
+ <?= \Controls\hidden_tag("method", "changePersonalData") ?>
<script type="dojo/method" event="onSubmit" args="evt">
evt.preventDefault();
@@ -289,18 +290,19 @@ class Pref_Prefs extends Handler_Protected {
<fieldset>
<label><?= __('Full name:') ?></label>
- <input dojoType='dijit.form.ValidationTextBox' name='full_name' required='1' value="<?= $full_name ?>">
+ <input dojoType='dijit.form.ValidationTextBox' name='full_name' required='1' value="<?= htmlspecialchars($user->full_name) ?>">
</fieldset>
<fieldset>
<label><?= __('E-mail:') ?></label>
- <input dojoType='dijit.form.ValidationTextBox' name='email' required='1' value="<?= $email ?>">
+ <input dojoType='dijit.form.ValidationTextBox' name='email' required='1' value="<?= htmlspecialchars($user->email) ?>">
</fieldset>
<hr/>
<button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
- <?= __("Save data") ?>
+ <?= \Controls\icon("save") ?>
+ <?= __("Save") ?>
</button>
</form>
<?php
@@ -313,7 +315,7 @@ class Pref_Prefs extends Handler_Protected {
$authenticator = false;
}
- $otp_enabled = $this->is_otp_enabled();
+ $otp_enabled = UserHelper::is_otp_enabled($_SESSION["uid"]);
if ($authenticator && method_exists($authenticator, "change_password")) {
?>
@@ -350,10 +352,6 @@ class Pref_Prefs extends Handler_Protected {
}
</script>
- <?php if ($otp_enabled) {
- print_notice(__("Changing your current password will disable OTP."));
- } ?>
-
<fieldset>
<label><?= __("Old password:") ?></label>
<input dojoType='dijit.form.ValidationTextBox' type='password' required='1' name='old_password'>
@@ -372,6 +370,7 @@ class Pref_Prefs extends Handler_Protected {
<hr/>
<button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
+ <?= \Controls\icon("security") ?>
<?= __("Change password") ?>
</button>
</form>
@@ -385,7 +384,7 @@ class Pref_Prefs extends Handler_Protected {
}
private function index_auth_app_passwords() {
- print_notice("You can create separate passwords for API clients. Using one is required if you enable OTP.");
+ print_notice("Separate passwords used for API clients. Required if you enable OTP.");
?>
<div id='app_passwords_holder'>
@@ -395,31 +394,21 @@ class Pref_Prefs extends Handler_Protected {
<hr>
<button style='float : left' class='alt-primary' dojoType='dijit.form.Button' onclick="Helpers.AppPasswords.generate()">
- <?= __('Generate new password') ?>
+ <?= \Controls\icon("add") ?>
+ <?= __('Generate password') ?>
</button>
<button style='float : left' class='alt-danger' dojoType='dijit.form.Button'
onclick="Helpers.AppPasswords.removeSelected()">
- <?= __('Remove selected passwords') ?>
+ <?= \Controls\icon("delete") ?>
+ <?= __('Remove selected') ?>
</button>
<?php
}
- private function is_otp_enabled() {
- $sth = $this->pdo->prepare("SELECT otp_enabled FROM ttrss_users
- WHERE id = ?");
- $sth->execute([$_SESSION["uid"]]);
-
- if ($row = $sth->fetch()) {
- return sql_bool_to_bool($row["otp_enabled"]);
- }
-
- return false;
- }
-
private function index_auth_2fa() {
- $otp_enabled = $this->is_otp_enabled();
+ $otp_enabled = UserHelper::is_otp_enabled($_SESSION["uid"]);
if ($_SESSION["auth_module"] == "auth_internal") {
if ($otp_enabled) {
@@ -455,6 +444,7 @@ class Pref_Prefs extends Handler_Protected {
<hr/>
<button dojoType='dijit.form.Button' type='submit' class='alt-danger'>
+ <?= \Controls\icon("lock_open") ?>
<?= __("Disable OTP") ?>
</button>
@@ -464,19 +454,11 @@ class Pref_Prefs extends Handler_Protected {
} else {
- print_warning("You will need a compatible Authenticator to use this. Changing your password would automatically disable OTP.");
- print_notice("You will need to generate app passwords for the API clients if you enable OTP.");
+ print "<img src=".($this->_get_otp_qrcode_img()).">";
- if (function_exists("imagecreatefromstring")) {
- print "<h3>" . __("Scan the following code by the Authenticator application or copy the key manually") . "</h3>";
- $csrf_token_hash = sha1($_SESSION["csrf_token"]);
- print "<img alt='otp qr-code' src='backend.php?op=pref-prefs&method=otpqrcode&csrf_token_hash=$csrf_token_hash'>";
- } else {
- print_error("PHP GD functions are required to generate QR codes.");
- print "<h3>" . __("Use the following OTP key with a compatible Authenticator application") . "</h3>";
- }
+ print_notice("You will need to generate app passwords for API clients if you enable OTP.");
- $otp_secret = $this->otpsecret();
+ $otp_secret = UserHelper::get_otp_secret($_SESSION["uid"]);
?>
<form dojoType='dijit.form.Form'>
@@ -486,7 +468,7 @@ class Pref_Prefs extends Handler_Protected {
<fieldset>
<label><?= __("OTP Key:") ?></label>
- <input dojoType='dijit.form.ValidationTextBox' disabled='disabled' value="<?= $otp_secret ?>" size='32'>
+ <input dojoType='dijit.form.ValidationTextBox' disabled='disabled' value="<?= $otp_secret ?>" style='width : 215px'>
</fieldset>
<!-- TODO: return JSON from the backend call -->
@@ -519,6 +501,7 @@ class Pref_Prefs extends Handler_Protected {
<hr/>
<button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
+ <?= \Controls\icon("lock") ?>
<?= __("Enable OTP") ?>
</button>
@@ -635,30 +618,27 @@ class Pref_Prefs extends Handler_Protected {
} else if ($pref_name == "USER_CSS_THEME") {
- $themes = array_merge(glob("themes/*.php"), glob("themes/*.css"), glob("themes.local/*.css"));
- $themes = array_map("basename", $themes);
- $themes = array_filter($themes, "theme_exists");
- asort($themes);
-
- if (!theme_exists($value)) $value = "";
+ $theme_files = array_map("basename",
+ array_merge(glob("themes/*.php"),
+ glob("themes/*.css"),
+ glob("themes.local/*.css")));
- print "<select name='$pref_name' id='$pref_name' dojoType='fox.form.Select'>";
+ asort($theme_files);
- $issel = $value == "" ? "selected='selected'" : "";
- print "<option $issel value=''>".__("default")."</option>";
+ $themes = [ "" => __("default") ];
- foreach ($themes as $theme) {
- $issel = $value == $theme ? "selected='selected'" : "";
- print "<option $issel value='$theme'>$theme</option>";
+ foreach ($theme_files as $file) {
+ $themes[$file] = basename($file, ".css");
}
+ ?>
- print "</select>";
+ <?= \Controls\select_hash($pref_name, $value, $themes) ?>
+ <?= \Controls\button_tag(\Controls\icon("palette") . " " . __("Customize"), "",
+ ["onclick" => "Helpers.Prefs.customizeCSS()"]) ?>
+ <?= \Controls\button_tag(\Controls\icon("open_in_new") . " " . __("More themes..."), "",
+ ["class" => "alt-info", "onclick" => "window.open(\"https://tt-rss.org/wiki/Themes\")"]) ?>
- print " <button dojoType=\"dijit.form.Button\" class='alt-info'
- onclick=\"Helpers.Prefs.customizeCSS()\">" . __('Customize') . "</button>";
-
- print " <button dojoType='dijit.form.Button' onclick='window.open(\"https://tt-rss.org/wiki/Themes\")'>
- <i class='material-icons'>open_in_new</i> ".__("More themes...")."</button>";
+ <?php
} else if ($pref_name == "DEFAULT_UPDATE_INTERVAL") {
@@ -685,6 +665,11 @@ class Pref_Prefs extends Handler_Protected {
print \Controls\checkbox_tag($pref_name, $is_checked, "true",
["disabled" => $is_disabled], "CB_$pref_name");
+ if ($pref_name == Prefs::DIGEST_ENABLE) {
+ print \Controls\button_tag(\Controls\icon("info") . " " . __('Preview'), '',
+ ['onclick' => 'Helpers.Digest.preview()', 'style' => 'margin-left : 10px']);
+ }
+
} else if (in_array($pref_name, ['FRESH_ARTICLE_MAX_AGE',
'PURGE_OLD_DAYS', 'LONG_DATE_FORMAT', 'SHORT_DATE_FORMAT'])) {
@@ -704,14 +689,14 @@ class Pref_Prefs extends Handler_Protected {
print \Controls\input_tag($pref_name, $value, "text", ["readonly" => true], "SSL_CERT_SERIAL");
- $cert_serial = htmlspecialchars(get_ssl_certificate_id());
+ $cert_serial = htmlspecialchars(self::_get_ssl_certificate_id());
$has_serial = ($cert_serial) ? true : false;
- print \Controls\button_tag(__('Register'), "", [
+ print \Controls\button_tag(\Controls\icon("security") . " " . __('Register'), "", [
"disabled" => !$has_serial,
"onclick" => "dijit.byId('SSL_CERT_SERIAL').attr('value', '$cert_serial')"]);
- print \Controls\button_tag(__('Clear'), "", [
+ print \Controls\button_tag(\Controls\icon("clear") . " " . __('Clear'), "", [
"class" => "alt-danger",
"onclick" => "dijit.byId('SSL_CERT_SERIAL').attr('value', '')"]);
@@ -719,11 +704,10 @@ class Pref_Prefs extends Handler_Protected {
"class" => "alt-info",
"onclick" => "window.open('https://tt-rss.org/wiki/SSL%20Certificate%20Authentication')"]);
- } else if ($pref_name == 'DIGEST_PREFERRED_TIME') {
+ } else if ($pref_name == Prefs::DIGEST_PREFERRED_TIME) {
print "<input dojoType=\"dijit.form.ValidationTextBox\"
id=\"$pref_name\" regexp=\"[012]?\d:\d\d\" placeHolder=\"12:00\"
name=\"$pref_name\" value=\"$value\">";
-
$item['help_text'] .= ". " . T_sprintf("Current server time: %s", date("H:i"));
} else {
$regexp = ($type_hint == Config::T_INT) ? 'regexp="^\d*$"' : '';
@@ -772,19 +756,21 @@ class Pref_Prefs extends Handler_Protected {
<div dojoType="dijit.layout.ContentPane" region="bottom">
<div dojoType="fox.form.ComboButton" type="submit" class="alt-primary">
- <span><?= __('Save configuration') ?></span>
+ <span> <?= __('Save configuration') ?></span>
<div dojoType="dijit.DropDownMenu">
<div dojoType="dijit.MenuItem" onclick="dijit.byId('changeSettingsForm').onSubmit(null, true)">
- <?= __("Save and exit preferences") ?>
+ <?= __("Save and exit") ?>
</div>
</div>
</div>
<button dojoType="dijit.form.Button" onclick="return Helpers.Profiles.edit()">
+ <?= \Controls\icon("settings") ?>
<?= __('Manage profiles') ?>
</button>
<button dojoType="dijit.form.Button" class="alt-danger" onclick="return Helpers.Prefs.confirmReset()">
+ <?= \Controls\icon("clear") ?>
<?= __('Reset to defaults') ?>
</button>
@@ -795,119 +781,73 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
- private function index_plugins_system() {
- print_notice("System plugins are enabled in <strong>config.php</strong> for all users.");
-
- $system_enabled = array_map("trim", explode(",", (string)Config::get(Config::PLUGINS)));
-
- $tmppluginhost = new PluginHost();
- $tmppluginhost->load_all($tmppluginhost::KIND_ALL, $_SESSION["uid"], true);
-
- foreach ($tmppluginhost->get_plugins() as $name => $plugin) {
- $about = $plugin->about();
-
- if ($about[3] ?? false) {
- $is_checked = in_array($name, $system_enabled) ? "checked" : "";
- ?>
- <fieldset class='prefs plugin'>
- <label><?= $name ?>:</label>
- <label class='checkbox description text-muted' id="PLABEL-<?= htmlspecialchars($name) ?>">
- <input disabled='1' dojoType='dijit.form.CheckBox' <?= $is_checked ?> type='checkbox'><?= htmlspecialchars($about[1]) ?>
- </label>
-
- <?php if ($about[4] ?? false) { ?>
- <button dojoType='dijit.form.Button' class='alt-info'
- onclick='window.open("<?= htmlspecialchars($about[4]) ?>")'>
- <i class='material-icons'>open_in_new</i> <?= __("More info...") ?></button>
- <?php } ?>
-
- <div dojoType='dijit.Tooltip' connectId='PLABEL-<?= htmlspecialchars($name) ?>' position='after'>
- <?= htmlspecialchars(T_sprintf("v%.2f, by %s", $about[0], $about[2])) ?>
- </div>
- </fieldset>
- <?php
- }
- }
- }
-
- private function index_plugins_user() {
+ function getPluginsList() {
$system_enabled = array_map("trim", explode(",", (string)Config::get(Config::PLUGINS)));
$user_enabled = array_map("trim", explode(",", get_pref(Prefs::_ENABLED_PLUGINS)));
$tmppluginhost = new PluginHost();
$tmppluginhost->load_all($tmppluginhost::KIND_ALL, $_SESSION["uid"], true);
+ $rv = [];
+
foreach ($tmppluginhost->get_plugins() as $name => $plugin) {
$about = $plugin->about();
+ $is_local = $tmppluginhost->is_local($plugin);
+ $version = htmlspecialchars($this->_get_plugin_version($plugin));
+
+ array_push($rv, [
+ "name" => $name,
+ "is_local" => $is_local,
+ "system_enabled" => in_array($name, $system_enabled),
+ "user_enabled" => in_array($name, $user_enabled),
+ "has_data" => count($tmppluginhost->get_all($plugin)) > 0,
+ "is_system" => (bool)($about[3] ?? false),
+ "version" => $version,
+ "author" => $about[2] ?? "",
+ "description" => $about[1] ?? "",
+ "more_info" => $about[4] ?? "",
+ ]);
+ }
- if (empty($about[3]) || $about[3] == false) {
-
- $is_checked = "";
- $is_disabled = "";
-
- if (in_array($name, $system_enabled)) {
- $is_checked = "checked='1'";
- $is_disabled = "disabled='1'";
- } else if (in_array($name, $user_enabled)) {
- $is_checked = "checked='1'";
- }
-
- ?>
-
- <fieldset class='prefs plugin'>
- <label><?= $name ?>:</label>
- <label class='checkbox description text-muted' id="PLABEL-<?= htmlspecialchars($name) ?>">
- <input name='plugins[]' value="<?= htmlspecialchars($name) ?>"
- dojoType='dijit.form.CheckBox' <?= $is_checked ?> <?= $is_disabled ?> type='checkbox'>
- <?= htmlspecialchars($about[1]) ?>
- </input>
- </label>
-
- <?php if (count($tmppluginhost->get_all($plugin)) > 0) {
- if (in_array($name, $system_enabled) || in_array($name, $user_enabled)) { ?>
- <button dojoType='dijit.form.Button'
- onclick='Helpers.Prefs.clearPluginData("<?= htmlspecialchars($name) ?>")'>
- <i class='material-icons'>clear</i> <?= __("Clear data") ?></button>
- <?php }
- } ?>
-
- <?php if ($about[4] ?? false) { ?>
- <button dojoType='dijit.form.Button' class='alt-info'
- onclick='window.open("<?= htmlspecialchars($about[4]) ?>")'>
- <i class='material-icons'>open_in_new</i> <?= __("More info...") ?></button>
- <?php } ?>
-
- <div dojoType='dijit.Tooltip' connectId="PLABEL-<?= htmlspecialchars($name) ?>" position='after'>
- <?= htmlspecialchars(T_sprintf("v%.2f, by %s", $about[0], $about[2])) ?>
- </div>
+ usort($rv, function($a, $b) { return strcmp($a["name"], $b["name"]); });
- </fieldset>
- <?php
- }
- }
+ print json_encode(['plugins' => $rv, 'is_admin' => $_SESSION['access_level'] >= 10]);
}
function index_plugins() {
?>
<form dojoType="dijit.form.Form" id="changePluginsForm">
- <script type="dojo/method" event="onSubmit" args="evt">
- evt.preventDefault();
- if (this.validate()) {
- xhr.post("backend.php", this.getValues(), () => {
- Notify.close();
- if (confirm(__('Selected plugins have been enabled. Reload?'))) {
- window.location.reload();
- }
- })
- }
- </script>
<?= \Controls\hidden_tag("op", "pref-prefs") ?>
<?= \Controls\hidden_tag("method", "setplugins") ?>
<div dojoType="dijit.layout.BorderContainer" gutters="false">
+ <div region="top" dojoType='fox.Toolbar'>
+ <div class='pull-right'>
+ <input name="search" type="search" onkeyup='Helpers.Plugins.search()' dojoType="dijit.form.TextBox">
+ <button dojoType='dijit.form.Button' onclick='Helpers.Plugins.search()'>
+ <?= __('Search') ?>
+ </button>
+ </div>
+
+ <div dojoType='fox.form.DropDownButton'>
+ <span><?= __('Select') ?></span>
+ <div dojoType='dijit.Menu' style='display: none'>
+ <div onclick="Lists.select('prefs-plugin-list', true)"
+ dojoType='dijit.MenuItem'><?= __('All') ?></div>
+ <div onclick="Lists.select('prefs-plugin-list', false)"
+ dojoType='dijit.MenuItem'><?= __('None') ?></div>
+ </div>
+ </div>
+ </div>
+
<div dojoType="dijit.layout.ContentPane" region="center" style="overflow-y : auto">
- <?php
+
+ <script type="dojo/method" event="onShow">
+ Helpers.Plugins.reload();
+ </script>
+
+ <!-- <?php
if (!empty($_SESSION["safe_mode"])) {
print_error("You have logged in using safe mode, no user plugins will be actually enabled until you login again.");
}
@@ -929,24 +869,41 @@ class Pref_Prefs extends Handler_Protected {
) . " (<a href='https://tt-rss.org/wiki/FeedHandlerPlugins' target='_blank'>".__("More info...")."</a>)"
);
}
- ?>
+ ?> -->
- <h2><?= __("System plugins") ?></h2>
-
- <?php $this->index_plugins_system() ?>
-
- <h2><?= __("User plugins") ?></h2>
-
- <?php $this->index_plugins_user() ?>
+ <ul id="prefs-plugin-list" class="prefs-plugin-list list-unstyled">
+ <li class='text-center'><?= __("Loading, please wait...") ?></li>
+ </ul>
</div>
<div dojoType="dijit.layout.ContentPane" region="bottom">
- <button dojoType='dijit.form.Button' style='float : left' class='alt-info' onclick='window.open("https://tt-rss.org/wiki/Plugins")'>
- <i class='material-icons'>help</i> <?= __("More info...") ?>
- </button>
- <button dojoType='dijit.form.Button' class='alt-primary' type='submit'>
- <?= __("Enable selected plugins") ?>
+
+ <button dojoType='dijit.form.Button' class="alt-info pull-right" onclick='window.open("https://tt-rss.org/wiki/Plugins")'>
+ <i class='material-icons'>help</i>
+ <?= __("More info") ?>
</button>
+
+ <?= \Controls\button_tag(\Controls\icon("check") . " " .__("Enable selected"), "", ["class" => "alt-primary",
+ "onclick" => "Helpers.Plugins.enableSelected()"]) ?>
+
+ <?= \Controls\button_tag(\Controls\icon("refresh"), "", ["title" => __("Reload"), "onclick" => "Helpers.Plugins.reload()"]) ?>
+
+ <?php if ($_SESSION["access_level"] >= 10) { ?>
+ <?php if (Config::get(Config::CHECK_FOR_UPDATES) && Config::get(Config::CHECK_FOR_PLUGIN_UPDATES)) { ?>
+
+ <button class='alt-warning' dojoType='dijit.form.Button' onclick="Helpers.Plugins.update()">
+ <?= \Controls\icon("update") ?>
+ <?= __("Check for updates") ?>
+ </button>
+ <?php } ?>
+
+ <?php if (Config::get(Config::ENABLE_PLUGIN_INSTALLER)) { ?>
+ <button dojoType='dijit.form.Button' onclick="Helpers.Plugins.install()">
+ <?= \Controls\icon("add") ?>
+ <?= __("Install plugin") ?>
+ </button>
+ <?php } ?>
+ <?php } ?>
</div>
</div>
</form>
@@ -970,120 +927,44 @@ class Pref_Prefs extends Handler_Protected {
<div dojoType='dijit.layout.AccordionPane' selected='true' title="<i class='material-icons'>settings</i> <?= __('Preferences') ?>">
<?php $this->index_prefs() ?>
</div>
- <div dojoType='dijit.layout.AccordionPane' title="<i class='material-icons'>extension</i> <?= __('Plugins') ?>">
- <script type='dojo/method' event='onSelected' args='evt'>
- if (this.domNode.querySelector('.loading'))
- window.setTimeout(() => {
- xhr.post("backend.php", {op: 'pref-prefs', method: 'index_plugins'}, (reply) => {
- this.attr('content', reply);
- });
- }, 200);
- </script>
- <span class='loading'><?= __("Loading, please wait...") ?></span>
+ <div dojoType='dijit.layout.AccordionPane' style='padding : 0' title="<i class='material-icons'>extension</i> <?= __('Plugins') ?>">
+ <?php $this->index_plugins() ?>
</div>
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefPrefs") ?>
</div>
<?php
}
- function toggleAdvanced() {
- $_SESSION["prefs_show_advanced"] = !$_SESSION["prefs_show_advanced"];
- }
-
- function otpsecret() {
- $sth = $this->pdo->prepare("SELECT salt, otp_enabled
- FROM ttrss_users
- WHERE id = ?");
- $sth->execute([$_SESSION['uid']]);
+ function _get_otp_qrcode_img() {
+ $secret = UserHelper::get_otp_secret($_SESSION["uid"]);
+ $login = UserHelper::get_login_by_id($_SESSION["uid"]);
- if ($row = $sth->fetch()) {
- $otp_enabled = sql_bool_to_bool($row["otp_enabled"]);
+ if ($secret && $login) {
+ $qrcode = new \chillerlan\QRCode\QRCode();
- if (!$otp_enabled) {
- $base32 = new \OTPHP\Base32();
- $secret = $base32->encode(mb_substr(sha1($row["salt"]), 0, 12), false);
+ $otpurl = "otpauth://totp/".urlencode($login)."?secret=$secret&issuer=".urlencode("Tiny Tiny RSS");
- return $secret;
- }
+ return $qrcode->render($otpurl);
}
return false;
}
- function otpqrcode() {
- $csrf_token_hash = clean($_REQUEST["csrf_token_hash"]);
-
- if (sha1($_SESSION["csrf_token"]) === $csrf_token_hash) {
- require_once "lib/phpqrcode/phpqrcode.php";
-
- $sth = $this->pdo->prepare("SELECT login
- FROM ttrss_users
- WHERE id = ?");
- $sth->execute([$_SESSION['uid']]);
-
- if ($row = $sth->fetch()) {
- $secret = $this->otpsecret();
- $login = $row['login'];
-
- if ($secret) {
- QRcode::png("otpauth://totp/".urlencode($login).
- "?secret=$secret&issuer=".urlencode("Tiny Tiny RSS"));
- }
- }
- } else {
- header("Content-Type: text/json");
- print Errors::to_json(Errors::E_UNAUTHORIZED);
- }
- }
-
function otpenable() {
-
$password = clean($_REQUEST["password"]);
- $otp = clean($_REQUEST["otp"]);
+ $otp_check = clean($_REQUEST["otp"]);
$authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
if ($authenticator->check_password($_SESSION["uid"], $password)) {
-
- $secret = $this->otpsecret();
-
- if ($secret) {
-
- $base32 = new \OTPHP\Base32();
-
- $topt = new \OTPHP\TOTP($secret);
-
- $otp_check = $topt->now();
-
- if ($otp == $otp_check) {
- $sth = $this->pdo->prepare("UPDATE ttrss_users
- SET otp_enabled = true WHERE id = ?");
-
- $sth->execute([$_SESSION['uid']]);
-
- print "OK";
- } else {
- print "ERROR:".__("Incorrect one time password");
- }
+ if (UserHelper::enable_otp($_SESSION["uid"], $otp_check)) {
+ print "OK";
+ } else {
+ print "ERROR:".__("Incorrect one time password");
}
-
} else {
print "ERROR:".__("Incorrect password");
}
-
- }
-
- static function isdefaultpassword() {
- $authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
-
- if ($authenticator &&
- method_exists($authenticator, "check_password") &&
- $authenticator->check_password($_SESSION["uid"], "password")) {
-
- return true;
- }
-
- return false;
}
function otpdisable() {
@@ -1116,9 +997,7 @@ class Pref_Prefs extends Handler_Protected {
"message" => $message]);
}
- $sth = $this->pdo->prepare("UPDATE ttrss_users SET otp_enabled = false WHERE
- id = ?");
- $sth->execute([$_SESSION['uid']]);
+ UserHelper::disable_otp($_SESSION["uid"]);
print "OK";
} else {
@@ -1128,12 +1007,306 @@ class Pref_Prefs extends Handler_Protected {
}
function setplugins() {
- if (is_array(clean($_REQUEST["plugins"])))
- $plugins = join(",", clean($_REQUEST["plugins"]));
- else
- $plugins = "";
+ $plugins = array_filter($_REQUEST["plugins"], 'clean') ?? [];
+
+ set_pref(Prefs::_ENABLED_PLUGINS, implode(",", $plugins));
+ }
+
+ function _get_plugin_version(Plugin $plugin) {
+ $about = $plugin->about();
+
+ if (!empty($about[0])) {
+ return T_sprintf("v%.2f, by %s", $about[0], $about[2]);
+ } else {
+ $ref = new ReflectionClass(get_class($plugin));
+
+ $plugin_dir = dirname($ref->getFileName());
+
+ if (basename($plugin_dir) == "plugins") {
+ return "";
+ }
+
+ if (is_dir("$plugin_dir/.git")) {
+ $ver = Config::get_version_from_git($plugin_dir);
+
+ return $ver["status"] == 0 ? T_sprintf("v%s, by %s", $ver["version"], $about[2]) : $ver["version"];
+ }
+ }
+ }
+
+ static function _get_updated_plugins() {
+ $root_dir = dirname(dirname(__DIR__)); # we're in classes/pref/
+ $plugin_dirs = array_filter(glob("$root_dir/plugins.local/*"), "is_dir");
+
+ $rv = [];
+
+ foreach ($plugin_dirs as $dir) {
+ if (is_dir("$dir/.git")) {
+ $plugin_name = basename($dir);
+
+ array_push($rv, ["plugin" => $plugin_name, "rv" => self::_plugin_needs_update($root_dir, $plugin_name)]);
+ }
+ }
+
+ $rv = array_values(array_filter($rv, function ($item) {
+ return $item["rv"]["need_update"];
+ }));
+
+ return $rv;
+ }
+
+ private static function _plugin_needs_update($root_dir, $plugin_name) {
+ $plugin_dir = "$root_dir/plugins.local/" . basename($plugin_name);
+ $rv = null;
+
+ if (is_dir($plugin_dir) && is_dir("$plugin_dir/.git")) {
+ $pipes = [];
+
+ $descriptorspec = [
+ //0 => ["pipe", "r"], // STDIN
+ 1 => ["pipe", "w"], // STDOUT
+ 2 => ["pipe", "w"], // STDERR
+ ];
+
+ $proc = proc_open("git fetch -q origin -a && git log HEAD..origin/master --oneline", $descriptorspec, $pipes, $plugin_dir);
+
+ if (is_resource($proc)) {
+ $rv = [
+ "stdout" => stream_get_contents($pipes[1]),
+ "stderr" => stream_get_contents($pipes[2]),
+ "git_status" => proc_close($proc),
+ ];
+ $rv["need_update"] = !empty($rv["stdout"]);
+ }
+ }
+
+ return $rv;
+ }
+
+
+ private function _update_plugin($root_dir, $plugin_name) {
+ $plugin_dir = "$root_dir/plugins.local/" . basename($plugin_name);
+ $rv = [];
+
+ if (is_dir($plugin_dir) && is_dir("$plugin_dir/.git")) {
+ $pipes = [];
+
+ $descriptorspec = [
+ //0 => ["pipe", "r"], // STDIN
+ 1 => ["pipe", "w"], // STDOUT
+ 2 => ["pipe", "w"], // STDERR
+ ];
+
+ $proc = proc_open("git fetch origin -a && git log HEAD..origin/master --oneline && git pull --ff-only origin master", $descriptorspec, $pipes, $plugin_dir);
+
+ if (is_resource($proc)) {
+ $rv["stdout"] = stream_get_contents($pipes[1]);
+ $rv["stderr"] = stream_get_contents($pipes[2]);
+ $rv["git_status"] = proc_close($proc);
+ }
+ }
+
+ return $rv;
+ }
+
+ // https://gist.github.com/mindplay-dk/a4aad91f5a4f1283a5e2#gistcomment-2036828
+ private function _recursive_rmdir(string $dir, bool $keep_root = false) {
+ // Handle bad arguments.
+ if (empty($dir) || !file_exists($dir)) {
+ return true; // No such file/dir$dir exists.
+ } elseif (is_file($dir) || is_link($dir)) {
+ return unlink($dir); // Delete file/link.
+ }
+
+ // Delete all children.
+ $files = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ foreach ($files as $fileinfo) {
+ $action = $fileinfo->isDir() ? 'rmdir' : 'unlink';
+ if (!$action($fileinfo->getRealPath())) {
+ return false; // Abort due to the failure.
+ }
+ }
+
+ return $keep_root ? true : rmdir($dir);
+ }
+
+ // https://stackoverflow.com/questions/7153000/get-class-name-from-file
+ private function _get_class_name_from_file($file) {
+ $tokens = token_get_all(file_get_contents($file));
+
+ for ($i = 0; $i < count($tokens); $i++) {
+ if (isset($tokens[$i][0]) && $tokens[$i][0] == T_CLASS) {
+ for ($j = $i+1; $j < count($tokens); $j++) {
+ if (isset($tokens[$j][1]) && $tokens[$j][1] != " ") {
+ return $tokens[$j][1];
+ }
+ }
+ }
+ }
+ }
+
+ function uninstallPlugin() {
+ if ($_SESSION["access_level"] >= 10) {
+ $plugin_name = basename(clean($_REQUEST['plugin']));
+ $status = 0;
+
+ $plugin_dir = dirname(dirname(__DIR__)) . "/plugins.local/$plugin_name";
+
+ if (is_dir($plugin_dir)) {
+ $status = $this->_recursive_rmdir($plugin_dir);
+ }
+
+ print json_encode(['status' => $status]);
+ }
+ }
+
+ function installPlugin() {
+ if ($_SESSION["access_level"] >= 10 && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
+ $plugin_name = basename(clean($_REQUEST['plugin']));
+ $all_plugins = $this->_get_available_plugins();
+ $plugin_dir = dirname(dirname(__DIR__)) . "/plugins.local";
+
+ $work_dir = "$plugin_dir/plugin-installer";
+
+ $rv = [ ];
+
+ if (is_dir($work_dir) || mkdir($work_dir)) {
+ foreach ($all_plugins as $plugin) {
+ if ($plugin['name'] == $plugin_name) {
+
+ $tmp_dir = tempnam($work_dir, $plugin_name);
+
+ if (file_exists($tmp_dir)) {
+ unlink($tmp_dir);
+
+ $pipes = [];
+
+ $descriptorspec = [
+ 1 => ["pipe", "w"], // STDOUT
+ 2 => ["pipe", "w"], // STDERR
+ ];
+
+ $proc = proc_open("git clone " . escapeshellarg($plugin['clone_url']) . " " . $tmp_dir,
+ $descriptorspec, $pipes, sys_get_temp_dir());
+
+ $status = 0;
+
+ if (is_resource($proc)) {
+ $rv["stdout"] = stream_get_contents($pipes[1]);
+ $rv["stderr"] = stream_get_contents($pipes[2]);
+ $status = proc_close($proc);
+ $rv["git_status"] = $status;
+
+ // yeah I know about mysterious RC = -1
+ if (file_exists("$tmp_dir/init.php")) {
+ $class_name = strtolower(basename($this->_get_class_name_from_file("$tmp_dir/init.php")));
+
+ if ($class_name) {
+ $dst_dir = "$plugin_dir/$class_name";
+
+ if (is_dir($dst_dir)) {
+ $rv['result'] = self::PI_RES_ALREADY_INSTALLED;
+ } else {
+ if (rename($tmp_dir, "$plugin_dir/$class_name")) {
+ $rv['result'] = self::PI_RES_SUCCESS;
+ }
+ }
+ } else {
+ $rv['result'] = self::PI_ERR_NO_CLASS;
+ }
+ } else {
+ $rv['result'] = self::PI_ERR_NO_INIT_PHP;
+ }
+
+ } else {
+ $rv['result'] = self::PI_ERR_EXEC_FAILED;
+ }
+ } else {
+ $rv['result'] = self::PI_ERR_NO_TEMPDIR;
+ }
+
+ // cleanup after failure
+ if ($tmp_dir && is_dir($tmp_dir)) {
+ $this->_recursive_rmdir($tmp_dir);
+ }
+
+ break;
+ }
+ }
+
+ if (empty($rv['result']))
+ $rv['result'] = self::PI_ERR_PLUGIN_NOT_FOUND;
+
+ } else {
+ $rv["result"] = self::PI_ERR_NO_WORKDIR;
+ }
+
+ print json_encode($rv);
+ }
+ }
+
+ private function _get_available_plugins() {
+ if ($_SESSION["access_level"] >= 10 && Config::get(Config::ENABLE_PLUGIN_INSTALLER)) {
+ return json_decode(UrlHelper::fetch(['url' => 'https://tt-rss.org/plugins.json']), true);
+ }
+ }
+ function getAvailablePlugins() {
+ if ($_SESSION["access_level"] >= 10) {
+ print json_encode($this->_get_available_plugins());
+ }
+ }
+
+ function checkForPluginUpdates() {
+ if ($_SESSION["access_level"] >= 10 && Config::get(Config::CHECK_FOR_UPDATES) && Config::get(Config::CHECK_FOR_PLUGIN_UPDATES)) {
+ $plugin_name = $_REQUEST["name"] ?? "";
+
+ $root_dir = dirname(dirname(__DIR__)); # we're in classes/pref/
- set_pref(Prefs::_ENABLED_PLUGINS, $plugins);
+ if (!empty($plugin_name)) {
+ $rv = [["plugin" => $plugin_name, "rv" => self::_plugin_needs_update($root_dir, $plugin_name)]];
+ } else {
+ $rv = self::_get_updated_plugins();
+ }
+
+ print json_encode($rv);
+ }
+ }
+
+ function updateLocalPlugins() {
+ if ($_SESSION["access_level"] >= 10) {
+ $plugins = explode(",", $_REQUEST["plugins"] ?? "");
+
+ # we're in classes/pref/
+ $root_dir = dirname(dirname(__DIR__));
+
+ $rv = [];
+
+ if (count($plugins) > 0) {
+ foreach ($plugins as $plugin_name) {
+ array_push($rv, ["plugin" => $plugin_name, "rv" => $this->_update_plugin($root_dir, $plugin_name)]);
+ }
+ // @phpstan-ignore-next-line
+ } else {
+ $plugin_dirs = array_filter(glob("$root_dir/plugins.local/*"), "is_dir");
+
+ foreach ($plugin_dirs as $dir) {
+ if (is_dir("$dir/.git")) {
+ $plugin_name = basename($dir);
+
+ $test = self::_plugin_needs_update($root_dir, $plugin_name);
+
+ if (!empty($test["o"]))
+ array_push($rv, ["plugin" => $plugin_name, "rv" => $this->_update_plugin($root_dir, $plugin_name)]);
+ }
+ }
+ }
+
+ print json_encode($rv);
+ }
}
function clearplugindata() {
@@ -1150,66 +1323,61 @@ class Pref_Prefs extends Handler_Protected {
}
function activateprofile() {
- $_SESSION["profile"] = (int) clean($_REQUEST["id"]);
+ $id = (int) $_REQUEST['id'] ?? 0;
+
+ $profile = ORM::for_table('ttrss_settings_profiles')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->find_one($id);
- // default value
- if (!$_SESSION["profile"]) $_SESSION["profile"] = null;
+ if ($profile) {
+ $_SESSION["profile"] = $id;
+ } else {
+ $_SESSION["profile"] = null;
+ }
}
function remprofiles() {
- $ids = explode(",", clean($_REQUEST["ids"]));
+ $ids = $_REQUEST["ids"] ?? [];
- foreach ($ids as $id) {
- if ($_SESSION["profile"] != $id) {
- $sth = $this->pdo->prepare("DELETE FROM ttrss_settings_profiles WHERE id = ? AND
- owner_uid = ?");
- $sth->execute([$id, $_SESSION['uid']]);
- }
- }
+ ORM::for_table('ttrss_settings_profiles')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->where_in('id', $ids)
+ ->where_not_equal('id', $_SESSION['profile'] ?? 0)
+ ->delete_many();
}
function addprofile() {
$title = clean($_REQUEST["title"]);
if ($title) {
- $this->pdo->beginTransaction();
-
- $sth = $this->pdo->prepare("SELECT id FROM ttrss_settings_profiles
- WHERE title = ? AND owner_uid = ?");
- $sth->execute([$title, $_SESSION['uid']]);
-
- if (!$sth->fetch()) {
+ $profile = ORM::for_table('ttrss_settings_profiles')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->where('title', $title)
+ ->find_one();
- $sth = $this->pdo->prepare("INSERT INTO ttrss_settings_profiles (title, owner_uid)
- VALUES (?, ?)");
+ if (!$profile) {
+ $profile = ORM::for_table('ttrss_settings_profiles')->create();
- $sth->execute([$title, $_SESSION['uid']]);
-
- $sth = $this->pdo->prepare("SELECT id FROM ttrss_settings_profiles WHERE
- title = ? AND owner_uid = ?");
- $sth->execute([$title, $_SESSION['uid']]);
+ $profile->title = $title;
+ $profile->owner_uid = $_SESSION['uid'];
+ $profile->save();
}
-
- $this->pdo->commit();
}
}
function saveprofile() {
- $id = clean($_REQUEST["id"]);
- $title = clean($_REQUEST["title"]);
+ $id = (int)$_REQUEST["id"];
+ $title = clean($_REQUEST["value"]);
- if ($id == 0) {
- print __("Default profile");
- return;
- }
-
- if ($title) {
- $sth = $this->pdo->prepare("UPDATE ttrss_settings_profiles
- SET title = ? WHERE id = ? AND
- owner_uid = ?");
+ if ($title && $id) {
+ $profile = ORM::for_table('ttrss_settings_profiles')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->find_one($id);
- $sth->execute([$title, $id, $_SESSION['uid']]);
- print $title;
+ if ($profile) {
+ $profile->title = $title;
+ $profile->save();
+ }
}
}
@@ -1217,18 +1385,19 @@ class Pref_Prefs extends Handler_Protected {
function getProfiles() {
$rv = [];
- $sth = $this->pdo->prepare("SELECT title,id FROM ttrss_settings_profiles
- WHERE owner_uid = ? ORDER BY title");
- $sth->execute([$_SESSION['uid']]);
+ $profiles = ORM::for_table('ttrss_settings_profiles')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->order_by_expr('title')
+ ->find_many();
array_push($rv, ["title" => __("Default profile"),
"id" => 0,
"active" => empty($_SESSION["profile"])
]);
- while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
- $row["active"] = isset($_SESSION["profile"]) && $_SESSION["profile"] == $row["id"];
- array_push($rv, $row);
+ foreach ($profiles as $profile) {
+ $profile['active'] = ($_SESSION["profile"] ?? 0) == $profile->id;
+ array_push($rv, $profile->as_array());
};
print json_encode($rv);
@@ -1271,23 +1440,25 @@ class Pref_Prefs extends Handler_Protected {
<th align='right'><?= __("Last used") ?></th>
</tr>
<?php
- $sth = $this->pdo->prepare("SELECT id, title, created, last_used
- FROM ttrss_app_passwords WHERE owner_uid = ?");
- $sth->execute([$_SESSION['uid']]);
- while ($row = $sth->fetch()) { ?>
- <tr data-row-id='<?= $row['id'] ?>'>
+ $passwords = ORM::for_table('ttrss_app_passwords')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->order_by_asc('title')
+ ->find_many();
+
+ foreach ($passwords as $pass) { ?>
+ <tr data-row-id='<?= $pass['id'] ?>'>
<td align='center'>
<input onclick='Tables.onRowChecked(this)' dojoType='dijit.form.CheckBox' type='checkbox'>
</td>
<td>
- <?= htmlspecialchars($row["title"]) ?>
+ <?= htmlspecialchars($pass["title"]) ?>
</td>
<td align='right' class='text-muted'>
- <?= TimeHelper::make_local_datetime($row['created'], false) ?>
+ <?= TimeHelper::make_local_datetime($pass['created'], false) ?>
</td>
<td align='right' class='text-muted'>
- <?= TimeHelper::make_local_datetime($row['last_used'], false) ?>
+ <?= TimeHelper::make_local_datetime($pass['last_used'], false) ?>
</td>
</tr>
<?php } ?>
@@ -1296,18 +1467,11 @@ class Pref_Prefs extends Handler_Protected {
<?php
}
- private function _encrypt_app_password($password) {
- $salt = substr(bin2hex(get_random_bytes(24)), 0, 24);
-
- return "SSHA-512:".hash('sha512', $salt . $password). ":$salt";
- }
-
- function deleteAppPassword() {
- $ids = explode(",", clean($_REQUEST['ids']));
- $ids_qmarks = arr_qmarks($ids);
-
- $sth = $this->pdo->prepare("DELETE FROM ttrss_app_passwords WHERE id IN ($ids_qmarks) AND owner_uid = ?");
- $sth->execute(array_merge($ids, [$_SESSION['uid']]));
+ function deleteAppPasswords() {
+ $passwords = ORM::for_table('ttrss_app_passwords')
+ ->where('owner_uid', $_SESSION['uid'])
+ ->where_in('id', $_REQUEST['ids'] ?? [])
+ ->delete_many();
$this->appPasswordList();
}
@@ -1315,17 +1479,41 @@ class Pref_Prefs extends Handler_Protected {
function generateAppPassword() {
$title = clean($_REQUEST['title']);
$new_password = make_password(16);
- $new_password_hash = $this->_encrypt_app_password($new_password);
+ $new_salt = UserHelper::get_salt();
+ $new_password_hash = UserHelper::hash_password($new_password, $new_salt, UserHelper::HASH_ALGOS[0]);
print_warning(T_sprintf("Generated password <strong>%s</strong> for %s. Please remember it for future reference.", $new_password, $title));
- $sth = $this->pdo->prepare("INSERT INTO ttrss_app_passwords
- (title, pwd_hash, service, created, owner_uid)
- VALUES
- (?, ?, ?, NOW(), ?)");
+ $password = ORM::for_table('ttrss_app_passwords')->create();
- $sth->execute([$title, $new_password_hash, Auth_Base::AUTH_SERVICE_API, $_SESSION['uid']]);
+ $password->title = $title;
+ $password->owner_uid = $_SESSION['uid'];
+ $password->pwd_hash = "$new_password_hash:$new_salt";
+ $password->service = Auth_Base::AUTH_SERVICE_API;
+ $password->created = Db::NOW();
+
+ $password->save();
$this->appPasswordList();
}
+
+ function previewDigest() {
+ print json_encode(Digest::prepare_headlines_digest($_SESSION["uid"], 1, 16));
+ }
+
+ static function _get_ssl_certificate_id() {
+ if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] ?? false) {
+ return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
+ $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
+ $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
+ $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
+ }
+ if ($_SERVER["SSL_CLIENT_M_SERIAL"] ?? false) {
+ return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
+ $_SERVER["SSL_CLIENT_V_START"] .
+ $_SERVER["SSL_CLIENT_V_END"] .
+ $_SERVER["SSL_CLIENT_S_DN"]);
+ }
+ return "";
+ }
}
diff --git a/classes/pref/system.php b/classes/pref/system.php
index 85635e753..c79b5095d 100644
--- a/classes/pref/system.php
+++ b/classes/pref/system.php
@@ -14,6 +14,20 @@ class Pref_System extends Handler_Administrative {
$this->pdo->query("DELETE FROM ttrss_error_log");
}
+ function sendTestEmail() {
+ $mail_address = clean($_REQUEST["mail_address"]);
+
+ $mailer = new Mailer();
+
+ $rc = $mailer->mail(["to_name" => "",
+ "to_address" => $mail_address,
+ "subject" => __("Test message from tt-rss"),
+ "message" => ("This message confirms that tt-rss can send outgoing mail.")
+ ]);
+
+ print json_encode(['rc' => $rc, 'error' => $mailer->error()]);
+ }
+
function getphpinfo() {
ob_start();
phpinfo();
@@ -103,12 +117,12 @@ class Pref_System extends Handler_Administrative {
<table width='100%' class='event-log'>
- <tr class='title'>
- <td width='5%'><?= __("Error") ?></td>
- <td><?= __("Filename") ?></td>
- <td><?= __("Message") ?></td>
- <td width='5%'><?= __("User") ?></td>
- <td width='5%'><?= __("Date") ?></td>
+ <tr>
+ <th width='5%'><?= __("Error") ?></th>
+ <th><?= __("Filename") ?></th>
+ <th><?= __("Message") ?></th>
+ <th width='5%'><?= __("User") ?></th>
+ <th width='5%'><?= __("Date") ?></th>
</tr>
<?php
@@ -151,16 +165,48 @@ class Pref_System extends Handler_Administrative {
$page = (int) ($_REQUEST["page"] ?? 0);
?>
<div dojoType='dijit.layout.AccordionContainer' region='center'>
- <div dojoType='dijit.layout.AccordionPane' style='padding : 0' title='<i class="material-icons">report</i> <?= __('Event Log') ?>'>
- <?php
- if (Config::get(Config::LOG_DESTINATION) == "sql") {
+ <?php if (Config::get(Config::LOG_DESTINATION) == Logger::LOG_DEST_SQL) { ?>
+ <div dojoType='dijit.layout.AccordionPane' style='padding : 0' title='<i class="material-icons">report</i> <?= __('Event log') ?>'>
+ <?php
$this->_log_viewer($page, $severity);
- } else {
- print_notice("Please set Config::get(Config::LOG_DESTINATION) to 'sql' in config.php to enable database logging.");
- }
- ?>
+ ?>
+ </div>
+ <?php } ?>
+ <div dojoType='dijit.layout.AccordionPane' style='padding : 0' title='<i class="material-icons">mail</i> <?= __('Mail configuration') ?>'>
+ <div dojoType="dijit.layout.ContentPane">
+
+ <form dojoType="dijit.form.Form">
+ <script type="dojo/method" event="onSubmit" args="evt">
+ evt.preventDefault();
+ if (this.validate()) {
+ xhr.json("backend.php", this.getValues(), (reply) => {
+ const msg = App.byId("mail-test-result");
+
+ if (reply.rc) {
+ msg.innerHTML = __("Mail sent.");
+ msg.className = 'alert alert-success';
+ } else {
+ msg.innerHTML = reply.error;
+ msg.className = 'alert alert-danger';
+ }
+
+ msg.show();
+ })
+ }
+ </script>
+
+ <?= \Controls\hidden_tag("op", "pref-system") ?>
+ <?= \Controls\hidden_tag("method", "sendTestEmail") ?>
+
+ <fieldset>
+ <label><?= __("To:") ?></label>
+ <?= \Controls\input_tag("mail_address", "", "text", ['required' => 1]) ?>
+ <?= \Controls\submit_tag(__("Send test email")) ?>
+ <span style="display: none; margin-left : 10px" class="alert alert-error" id="mail-test-result">...</span>
+ </fieldset>
+ </form>
+ </div>
</div>
-
<div dojoType='dijit.layout.AccordionPane' title='<i class="material-icons">info</i> <?= __('PHP Information') ?>'>
<script type='dojo/method' event='onSelected' args='evt'>
if (this.domNode.querySelector('.loading'))
diff --git a/classes/pref/users.php b/classes/pref/users.php
index 13f808cb3..2e3dc4b67 100644
--- a/classes/pref/users.php
+++ b/classes/pref/users.php
@@ -1,24 +1,22 @@
<?php
class Pref_Users extends Handler_Administrative {
function csrf_ignore($method) {
- $csrf_ignored = array("index");
-
- return array_search($method, $csrf_ignored) !== false;
+ return $method == "index";
}
function edit() {
- global $access_level_names;
-
- $id = (int)clean($_REQUEST["id"]);
+ $user = ORM::for_table('ttrss_users')
+ ->select_expr("id,login,access_level,email,full_name,otp_enabled")
+ ->find_one((int)$_REQUEST["id"])
+ ->as_array();
- $sth = $this->pdo->prepare("SELECT id, login, access_level, email FROM ttrss_users WHERE id = ?");
- $sth->execute([$id]);
+ global $access_level_names;
- if ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
+ if ($user) {
print json_encode([
- "user" => $row,
- "access_level_names" => $access_level_names
- ]);
+ "user" => $user,
+ "access_level_names" => $access_level_names
+ ]);
}
}
@@ -106,31 +104,32 @@ class Pref_Users extends Handler_Administrative {
}
function editSave() {
- $login = clean($_REQUEST["login"]);
- $uid = clean($_REQUEST["id"]);
- $access_level = (int) clean($_REQUEST["access_level"]);
- $email = clean($_REQUEST["email"]);
+ $id = (int)$_REQUEST['id'];
$password = clean($_REQUEST["password"]);
+ $user = ORM::for_table('ttrss_users')->find_one($id);
- // no blank usernames
- if (!$login) return;
+ if ($user) {
+ $login = clean($_REQUEST["login"]);
- // forbid renaming admin
- if ($uid == 1) $login = "admin";
+ if ($id == 1) $login = "admin";
+ if (!$login) return;
- if ($password) {
- $salt = substr(bin2hex(get_random_bytes(125)), 0, 250);
- $pwd_hash = encrypt_password($password, $salt, true);
- $pass_query_part = "pwd_hash = ".$this->pdo->quote($pwd_hash).",
- salt = ".$this->pdo->quote($salt).",";
- } else {
- $pass_query_part = "";
- }
+ $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"]);
- $sth = $this->pdo->prepare("UPDATE ttrss_users SET $pass_query_part login = LOWER(?),
- access_level = ?, email = ?, otp_enabled = false WHERE id = ?");
- $sth->execute([$login, $access_level, $email, $uid]);
+ // force new OTP secret when next enabled
+ if (Config::get_schema_version() >= 143 && !$user->otp_enabled) {
+ $user->otp_secret = null;
+ }
+
+ $user->save();
+ }
+ if ($password) {
+ UserHelper::reset_password($id, false, $password);
+ }
}
function remove() {
@@ -152,24 +151,25 @@ class Pref_Users extends Handler_Administrative {
function add() {
$login = clean($_REQUEST["login"]);
- $tmp_user_pwd = make_password();
- $salt = substr(bin2hex(get_random_bytes(125)), 0, 250);
- $pwd_hash = encrypt_password($tmp_user_pwd, $salt, true);
if (!$login) return; // no blank usernames
if (!UserHelper::find_user_by_login($login)) {
- $sth = $this->pdo->prepare("INSERT INTO ttrss_users
- (login,pwd_hash,access_level,last_login,created, salt)
- VALUES (LOWER(?), ?, 0, null, NOW(), ?)");
- $sth->execute([$login, $pwd_hash, $salt]);
+ $new_password = make_password();
- if ($new_uid = UserHelper::find_user_by_login($login)) {
+ $user = ORM::for_table('ttrss_users')->create();
- print T_sprintf("Added user %s with password %s",
- $login, $tmp_user_pwd);
+ $user->salt = UserHelper::get_salt();
+ $user->login = mb_strtolower($login);
+ $user->pwd_hash = UserHelper::hash_password($new_password, $user->salt);
+ $user->access_level = 0;
+ $user->created = Db::NOW();
+ $user->save();
+ if ($new_uid = UserHelper::find_user_by_login($login)) {
+ print T_sprintf("Added user %s with password %s",
+ $login, $new_password);
} else {
print T_sprintf("Could not create user %s", $login);
}
@@ -200,11 +200,10 @@ class Pref_Users extends Handler_Administrative {
$sort = "login";
}
- $sort = $this->_validate_field($sort,
- ["login", "access_level", "created", "num_feeds", "created", "last_login"], "login");
+ if (!in_array($sort, ["login", "access_level", "created", "num_feeds", "created", "last_login"]))
+ $sort = "login";
if ($sort != "login") $sort = "$sort DESC";
-
?>
<div dojoType='dijit.layout.BorderContainer' gutters='false'>
@@ -249,42 +248,41 @@ class Pref_Users extends Handler_Administrative {
<table width='100%' class='users-list' id='users-list'>
- <tr class='title'>
- <td align='center' width='5%'> </td>
- <td width='20%'><a href='#' onclick="Users.reload('login')"><?= ('Login') ?></a></td>
- <td width='20%'><a href='#' onclick="Users.reload('access_level')"><?= ('Access Level') ?></a></td>
- <td width='10%'><a href='#' onclick="Users.reload('num_feeds')"><?= ('Subscribed feeds') ?></a></td>
- <td width='20%'><a href='#' onclick="Users.reload('created')"><?= ('Registered') ?></a></td>
- <td width='20%'><a href='#' onclick="Users.reload('last_login')"><?= ('Last login') ?></a></td>
+ <tr>
+ <th></th>
+ <th><a href='#' onclick="Users.reload('login')"><?= ('Login') ?></a></th>
+ <th><a href='#' onclick="Users.reload('access_level')"><?= ('Access Level') ?></a></th>
+ <th><a href='#' onclick="Users.reload('num_feeds')"><?= ('Subscribed feeds') ?></a></th>
+ <th><a href='#' onclick="Users.reload('created')"><?= ('Registered') ?></a></th>
+ <th><a href='#' onclick="Users.reload('last_login')"><?= ('Last login') ?></a></th>
</tr>
<?php
- $sth = $this->pdo->prepare("SELECT
- tu.id,
- login,access_level,email,
- ".SUBSTRING_FOR_DATE."(last_login,1,16) as last_login,
- ".SUBSTRING_FOR_DATE."(created,1,16) as created,
- (SELECT COUNT(id) FROM ttrss_feeds WHERE owner_uid = tu.id) AS num_feeds
- FROM
- ttrss_users tu
- WHERE
- (:search = '' OR login LIKE :search) AND tu.id > 0
- ORDER BY $sort");
- $sth->execute([":search" => $user_search ? "%$user_search%" : ""]);
-
- while ($row = $sth->fetch()) { ?>
-
- <tr data-row-id='<?= $row["id"] ?>' onclick='Users.edit(<?= $row["id"] ?>)' title="<?= __('Click to edit') ?>">
- <td align='center'>
+ $users = ORM::for_table('ttrss_users')
+ ->table_alias('u')
+ ->left_outer_join("ttrss_feeds", ["owner_uid", "=", "u.id"], 'f')
+ ->select_expr('u.*,COUNT(f.id) AS num_feeds')
+ ->where_like("login", $user_search ? "%$user_search%" : "%")
+ ->order_by_expr($sort)
+ ->group_by_expr('u.id')
+ ->find_many();
+
+ foreach ($users as $user) { ?>
+
+ <tr data-row-id='<?= $user["id"] ?>' onclick='Users.edit(<?= $user["id"] ?>)' title="<?= __('Click to edit') ?>">
+ <td class='checkbox'>
<input onclick='Tables.onRowChecked(this); event.stopPropagation();'
dojoType='dijit.form.CheckBox' type='checkbox'>
</td>
- <td><i class='material-icons'>person</i> <?= htmlspecialchars($row["login"]) ?></td>
- <td><?= $access_level_names[$row["access_level"]] ?></td>
- <td><?= $row["num_feeds"] ?></td>
- <td><?= TimeHelper::make_local_datetime($row["created"], false) ?></td>
- <td><?= TimeHelper::make_local_datetime($row["last_login"], false) ?></td>
+ <td width='30%'>
+ <i class='material-icons'>person</i>
+ <strong><?= htmlspecialchars($user["login"]) ?></strong>
+ </td>
+ <td><?= $access_level_names[$user["access_level"]] ?></td>
+ <td><?= $user["num_feeds"] ?></td>
+ <td class='text-muted'><?= TimeHelper::make_local_datetime($user["created"], false) ?></td>
+ <td class='text-muted'><?= TimeHelper::make_local_datetime($user["last_login"], false) ?></td>
</tr>
<?php } ?>
</table>
@@ -294,11 +292,4 @@ class Pref_Users extends Handler_Administrative {
<?php
}
- private function _validate_field($string, $allowed, $default = "") {
- if (in_array($string, $allowed))
- return $string;
- else
- return $default;
- }
-
}
diff --git a/classes/prefs.php b/classes/prefs.php
index 8eba40ad5..24f0f7a80 100644
--- a/classes/prefs.php
+++ b/classes/prefs.php
@@ -58,6 +58,8 @@ class Prefs {
const _PREFS_MIGRATED = "_PREFS_MIGRATED";
const HEADLINES_NO_DISTINCT = "HEADLINES_NO_DISTINCT";
const DEBUG_HEADLINE_IDS = "DEBUG_HEADLINE_IDS";
+ const DISABLE_CONDITIONAL_COUNTERS = "DISABLE_CONDITIONAL_COUNTERS";
+ const WIDESCREEN_MODE = "WIDESCREEN_MODE";
private const _DEFAULTS = [
Prefs::PURGE_OLD_DAYS => [ 60, Config::T_INT ],
@@ -76,7 +78,7 @@ class Prefs {
Prefs::DIGEST_ENABLE => [ false, Config::T_BOOL ],
Prefs::CONFIRM_FEED_CATCHUP => [ true, Config::T_BOOL ],
Prefs::CDM_AUTO_CATCHUP => [ false, Config::T_BOOL ],
- Prefs::_DEFAULT_VIEW_MODE => [ "adaptive", Config::T_BOOL ],
+ Prefs::_DEFAULT_VIEW_MODE => [ "adaptive", Config::T_STRING ],
Prefs::_DEFAULT_VIEW_LIMIT => [ 30, Config::T_INT ],
//Prefs::_PREFS_ACTIVE_TAB => [ "", Config::T_STRING ],
//Prefs::STRIP_UNSAFE_TAGS => [ true, Config::T_BOOL ],
@@ -116,6 +118,8 @@ class Prefs {
Prefs::_PREFS_MIGRATED => [ false, Config::T_BOOL ],
Prefs::HEADLINES_NO_DISTINCT => [ false, Config::T_BOOL ],
Prefs::DEBUG_HEADLINE_IDS => [ false, Config::T_BOOL ],
+ Prefs::DISABLE_CONDITIONAL_COUNTERS => [ false, Config::T_BOOL ],
+ Prefs::WIDESCREEN_MODE => [ false, Config::T_BOOL ],
];
const _PROFILE_BLACKLIST = [
diff --git a/classes/rpc.php b/classes/rpc.php
index aaaf4f8d5..35125ae04 100755
--- a/classes/rpc.php
+++ b/classes/rpc.php
@@ -121,7 +121,8 @@ class RPC extends Handler_Protected {
else
$label_ids = array_map("intval", clean($_REQUEST["label_ids"] ?? []));
- $counters = is_array($feed_ids) ? Counters::get_conditional($feed_ids, $label_ids) : Counters::get_all();
+ $counters = is_array($feed_ids) && !get_pref(Prefs::DISABLE_CONDITIONAL_COUNTERS) ?
+ Counters::get_conditional($feed_ids, $label_ids) : Counters::get_all();
$reply = [
'counters' => $counters,
@@ -151,7 +152,9 @@ class RPC extends Handler_Protected {
if (count($ids) > 0)
$this->markArticlesById($ids, $cmode);
- print json_encode(["message" => "UPDATE_COUNTERS", "feeds" => Article::_feeds_of($ids)]);
+ print json_encode(["message" => "UPDATE_COUNTERS",
+ "labels" => Article::_labels_of($ids),
+ "feeds" => Article::_feeds_of($ids)]);
}
function publishSelected() {
@@ -161,17 +164,30 @@ class RPC extends Handler_Protected {
if (count($ids) > 0)
$this->publishArticlesById($ids, $cmode);
- print json_encode(["message" => "UPDATE_COUNTERS", "feeds" => Article::_feeds_of($ids)]);
+ print json_encode(["message" => "UPDATE_COUNTERS",
+ "labels" => Article::_labels_of($ids),
+ "feeds" => Article::_feeds_of($ids)]);
}
function sanityCheck() {
$_SESSION["hasSandbox"] = clean($_REQUEST["hasSandbox"]) === "true";
$_SESSION["clientTzOffset"] = clean($_REQUEST["clientTzOffset"]);
+ $client_location = $_REQUEST["clientLocation"];
+
$error = Errors::E_SUCCESS;
+ $error_params = [];
+
+ $client_scheme = parse_url($client_location, PHP_URL_SCHEME);
+ $server_scheme = parse_url(Config::get_self_url(), PHP_URL_SCHEME);
- if (get_schema_version() != SCHEMA_VERSION) {
+ if (Config::is_migration_needed()) {
$error = Errors::E_SCHEMA_MISMATCH;
+ } else if ($client_scheme != $server_scheme) {
+ $error = Errors::E_URL_SCHEME_MISMATCH;
+ $error_params["client_scheme"] = $client_scheme;
+ $error_params["server_scheme"] = $server_scheme;
+ $error_params["self_url_path"] = Config::get_self_url();
}
if ($error == Errors::E_SUCCESS) {
@@ -183,7 +199,7 @@ class RPC extends Handler_Protected {
print json_encode($reply);
} else {
- print Errors::to_json($error);
+ print Errors::to_json($error, $error_params);
}
}
@@ -219,14 +235,12 @@ class RPC extends Handler_Protected {
//print json_encode(array("message" => "UPDATE_COUNTERS"));
}
- function setpanelmode() {
+ function setWidescreen() {
$wide = (int) clean($_REQUEST["wide"]);
- // FIXME should this use SESSION_COOKIE_LIFETIME and be renewed periodically?
- setcookie("ttrss_widescreen", (string)$wide,
- time() + 86400*365);
+ set_pref(Prefs::WIDESCREEN_MODE, $wide);
- print json_encode(array("wide" => $wide));
+ print json_encode(["wide" => $wide]);
}
static function updaterandomfeed_real() {
@@ -237,27 +251,27 @@ class RPC extends Handler_Protected {
if (Config::get(Config::DB_TYPE) == "pgsql") {
$update_limit_qpart = "AND ((
update_interval = 0
- AND p.value != '-1'
+ AND (p.value IS NULL OR p.value != '-1')
AND last_updated < NOW() - CAST((COALESCE(p.value, '$default_interval') || ' minutes') AS INTERVAL)
) OR (
update_interval > 0
AND last_updated < NOW() - CAST((update_interval || ' minutes') AS INTERVAL)
) OR (
update_interval >= 0
- AND p.value != '-1'
+ AND (p.value IS NULL OR p.value != '-1')
AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL)
))";
} else {
$update_limit_qpart = "AND ((
update_interval = 0
- AND p.value != '-1'
+ AND (p.value IS NULL OR p.value != '-1')
AND last_updated < DATE_SUB(NOW(), INTERVAL CONVERT(COALESCE(p.value, '$default_interval'), SIGNED INTEGER) MINUTE)
) OR (
update_interval > 0
AND last_updated < DATE_SUB(NOW(), INTERVAL update_interval MINUTE)
) OR (
update_interval >= 0
- AND p.value != '-1'
+ AND (p.value IS NULL OR p.value != '-1')
AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL)
))";
}
@@ -368,10 +382,10 @@ class RPC extends Handler_Protected {
}
function log() {
- $msg = clean($_REQUEST['msg']);
- $file = basename(clean($_REQUEST['file']));
- $line = (int) clean($_REQUEST['line']);
- $context = clean($_REQUEST['context']);
+ $msg = clean($_REQUEST['msg'] ?? "");
+ $file = basename(clean($_REQUEST['file'] ?? ""));
+ $line = (int) clean($_REQUEST['line'] ?? 0);
+ $context = clean($_REQUEST['context'] ?? "");
if ($msg) {
Logger::log_error(E_USER_WARNING,
@@ -382,12 +396,12 @@ class RPC extends Handler_Protected {
}
function checkforupdates() {
- $rv = [];
+ $rv = ["changeset" => [], "plugins" => []];
- $git_timestamp = false;
- $git_commit = false;
+ $version = Config::get_version(false);
- get_version($git_commit, $git_timestamp);
+ $git_timestamp = $version["timestamp"] ?? false;
+ $git_commit = $version["commit"] ?? false;
if (Config::get(Config::CHECK_FOR_UPDATES) && $_SESSION["access_level"] >= 10 && $git_timestamp) {
$content = @UrlHelper::fetch(["url" => "https://tt-rss.org/version.json"]);
@@ -399,10 +413,12 @@ class RPC extends Handler_Protected {
if ($git_timestamp < (int)$content["changeset"]["timestamp"] &&
$git_commit != $content["changeset"]["id"]) {
- $rv = $content["changeset"];
+ $rv["changeset"] = $content["changeset"];
}
}
}
+
+ $rv["plugins"] = Pref_Prefs::_get_updated_plugins();
}
print json_encode($rv);
@@ -427,8 +443,8 @@ class RPC extends Handler_Protected {
$params["default_view_mode"] = get_pref(Prefs::_DEFAULT_VIEW_MODE);
$params["default_view_limit"] = (int) get_pref(Prefs::_DEFAULT_VIEW_LIMIT);
$params["default_view_order_by"] = get_pref(Prefs::_DEFAULT_VIEW_ORDER_BY);
- $params["bw_limit"] = (int) $_SESSION["bw_limit"];
- $params["is_default_pw"] = Pref_Prefs::isdefaultpassword();
+ $params["bw_limit"] = (int) ($_SESSION["bw_limit"] ?? false);
+ $params["is_default_pw"] = UserHelper::is_default_password();
$params["label_base_index"] = LABEL_BASE_INDEX;
$theme = get_pref(Prefs::USER_CSS_THEME);
@@ -449,11 +465,11 @@ class RPC extends Handler_Protected {
$max_feed_id = $row["mid"];
$num_feeds = $row["nf"];
- $params["self_url_prefix"] = get_self_url_prefix();
+ $params["self_url_prefix"] = Config::get_self_url();
$params["max_feed_id"] = (int) $max_feed_id;
$params["num_feeds"] = (int) $num_feeds;
$params["hotkeys"] = $this->get_hotkeys_map();
- $params["widescreen"] = (int) ($_COOKIE["ttrss_widescreen"] ?? 0);
+ $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["labels"] = Labels::get_all($_SESSION["uid"]);
diff --git a/classes/rssutils.php b/classes/rssutils.php
index 11a94162c..e6bf08ab1 100755
--- a/classes/rssutils.php
+++ b/classes/rssutils.php
@@ -21,7 +21,7 @@ class RSSUtils {
}
// Strips utf8mb4 characters (i.e. emoji) for mysql
- static function strip_utf8mb4($str) {
+ static function strip_utf8mb4(string $str) {
return preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $str);
}
@@ -52,10 +52,10 @@ class RSSUtils {
}
}
- static function update_daemon_common($limit = null, $options = []) {
+ static function update_daemon_common(int $limit = 0, array $options = []) {
if (!$limit) $limit = Config::get(Config::DAEMON_FEED_LIMIT);
- if (get_schema_version() != SCHEMA_VERSION) {
+ if (Config::get_schema_version() != Config::SCHEMA_VERSION) {
die("Schema version is wrong, please upgrade the database.\n");
}
@@ -78,27 +78,27 @@ class RSSUtils {
if (Config::get(Config::DB_TYPE) == "pgsql") {
$update_limit_qpart = "AND ((
update_interval = 0
- AND p.value != '-1'
+ AND (p.value IS NULL OR p.value != '-1')
AND last_updated < NOW() - CAST((COALESCE(p.value, '$default_interval') || ' minutes') AS INTERVAL)
) OR (
update_interval > 0
AND last_updated < NOW() - CAST((update_interval || ' minutes') AS INTERVAL)
) OR (
update_interval >= 0
- AND p.value != '-1'
+ AND (p.value IS NULL OR p.value != '-1')
AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL)
))";
} else {
$update_limit_qpart = "AND ((
update_interval = 0
- AND p.value != '-1'
+ AND (p.value IS NULL OR p.value != '-1')
AND last_updated < DATE_SUB(NOW(), INTERVAL CONVERT(COALESCE(p.value, '$default_interval'), SIGNED INTEGER) MINUTE)
) OR (
update_interval > 0
AND last_updated < DATE_SUB(NOW(), INTERVAL update_interval MINUTE)
) OR (
update_interval >= 0
- AND p.value != '-1'
+ AND (p.value IS NULL OR p.value != '-1')
AND (last_updated = '1970-01-01 00:00:00' OR last_updated IS NULL)
))";
}
@@ -229,11 +229,9 @@ class RSSUtils {
} else {
try {
if (!self::update_rss_feed($tline["id"], true)) {
- global $fetch_last_error;
-
Logger::log(E_USER_NOTICE,
sprintf("Update request for feed %d (%s, owner UID: %d) failed: %s.",
- $tline["id"], clean($tline["title"]), $tline["owner_uid"], clean($fetch_last_error)));
+ $tline["id"], clean($tline["title"]), $tline["owner_uid"], clean(UrlHelper::$fetch_last_error)));
}
Debug::log(sprintf("<= %.4f (sec) (not using a separate process)", microtime(true) - $fstarted));
@@ -271,8 +269,8 @@ class RSSUtils {
return $nf;
}
- // this is used when subscribing
- static function set_basic_feed_info($feed) {
+ /** this is used when subscribing; TODO: update to ORM */
+ static function update_basic_info(int $feed) {
$pdo = Db::pdo();
@@ -346,215 +344,197 @@ class RSSUtils {
}
}
- static function update_rss_feed($feed, $no_cache = false) {
+ static function update_rss_feed(int $feed, bool $no_cache = false) : bool {
- Debug::log("start", Debug::$LOG_VERBOSE);
+ Debug::log("start", Debug::LOG_VERBOSE);
$pdo = Db::pdo();
- $sth = $pdo->prepare("SELECT title, site_url FROM ttrss_feeds WHERE id = ?");
- $sth->execute([$feed]);
-
- if (!$row = $sth->fetch()) {
- Debug::log("feed $feed not found, skipping.");
- user_error("Attempt to update unknown/invalid feed $feed", E_USER_WARNING);
- return false;
- }
-
- $title = $row["title"];
- $site_url = $row["site_url"];
-
- // feed was batch-subscribed or something, we need to get basic info
- // this is not optimal currently as it fetches stuff separately TODO: optimize
- if ($title == "[Unknown]" || !$title || !$site_url) {
- Debug::log("setting basic feed info for $feed [$title, $site_url]...");
- self::set_basic_feed_info($feed);
+ if (Config::get(Config::DB_TYPE) == "pgsql") {
+ $favicon_interval_qpart = "favicon_last_checked < NOW() - INTERVAL '12 hour'";
+ } else {
+ $favicon_interval_qpart = "favicon_last_checked < DATE_SUB(NOW(), INTERVAL 12 HOUR)";
}
- $sth = $pdo->prepare("SELECT id,update_interval,auth_login,
- feed_url,auth_pass,cache_images,
- mark_unread_on_update, owner_uid,
- auth_pass_encrypted, feed_language,
- last_modified,
- ".SUBSTRING_FOR_DATE."(last_unconditional, 1, 19) AS last_unconditional
- FROM ttrss_feeds WHERE id = ?");
- $sth->execute([$feed]);
+ $feed_obj = ORM::for_table('ttrss_feeds')
+ ->select_expr("ttrss_feeds.*,
+ ".SUBSTRING_FOR_DATE."(last_unconditional, 1, 19) AS last_unconditional,
+ (favicon_is_custom IS NOT TRUE AND
+ (favicon_last_checked IS NULL OR $favicon_interval_qpart)) AS favicon_needs_check")
+ ->find_one($feed);
- if ($row = $sth->fetch()) {
+ if ($feed_obj) {
+ $feed_obj->last_update_started = Db::NOW();
+ $feed_obj->save();
- $owner_uid = $row["owner_uid"];
- $mark_unread_on_update = $row["mark_unread_on_update"];
+ $feed_language = mb_strtolower($feed_obj->feed_language);
- $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_update_started = NOW()
- WHERE id = ?");
- $sth->execute([$feed]);
-
- $auth_login = $row["auth_login"];
- $auth_pass = $row["auth_pass"];
- $stored_last_modified = $row["last_modified"];
- $last_unconditional = $row["last_unconditional"];
- $cache_images = $row["cache_images"];
- $fetch_url = $row["feed_url"];
-
- $feed_language = mb_strtolower($row["feed_language"]);
-
- if (!$feed_language)
- $feed_language = mb_strtolower(get_pref(Prefs::DEFAULT_SEARCH_LANGUAGE, $owner_uid));
-
- if (!$feed_language)
- $feed_language = 'simple';
+ if (!$feed_language) $feed_language = mb_strtolower(get_pref(Prefs::DEFAULT_SEARCH_LANGUAGE, $feed_obj->owner_uid));
+ if (!$feed_language) $feed_language = 'simple';
} else {
+ Debug::log("error: feeds table record not found for feed: $feed");
return false;
}
+ // feed was batch-subscribed or something, we need to get basic info
+ // this is not optimal currently as it fetches stuff separately TODO: optimize
+ if ($feed_obj->title == "[Unknown]" || empty($feed_obj->title) || empty($feed_obj->site_url)) {
+ Debug::log("setting basic feed info for $feed...");
+ self::update_basic_info($feed);
+ }
+
$date_feed_processed = date('Y-m-d H:i');
- $cache_filename = Config::get(Config::CACHE_DIR) . "/feeds/" . sha1($fetch_url) . ".xml";
+ $cache_filename = Config::get(Config::CACHE_DIR) . "/feeds/" . sha1($feed_obj->feed_url) . ".xml";
$pluginhost = new PluginHost();
- $user_plugins = get_pref(Prefs::_ENABLED_PLUGINS, $owner_uid);
+ $user_plugins = get_pref(Prefs::_ENABLED_PLUGINS, $feed_obj->owner_uid);
$pluginhost->load(Config::get(Config::PLUGINS), PluginHost::KIND_ALL);
- $pluginhost->load((string)$user_plugins, PluginHost::KIND_USER, $owner_uid);
- //$pluginhost->load_data();
+ $pluginhost->load((string)$user_plugins, PluginHost::KIND_USER, $feed_obj->owner_uid);
$rss_hash = false;
$force_refetch = isset($_REQUEST["force_refetch"]);
$feed_data = "";
- Debug::log("running HOOK_FETCH_FEED handlers...", Debug::$LOG_VERBOSE);
+ Debug::log("running HOOK_FETCH_FEED handlers...", Debug::LOG_VERBOSE);
$start_ts = microtime(true);
$last_article_timestamp = 0;
+
+ $hff_owner_uid = $feed_obj->owner_uid;
+ $hff_feed_url = $feed_obj->feed_url;
+
$pluginhost->chain_hooks_callback(PluginHost::HOOK_FETCH_FEED,
function ($result, $plugin) use (&$feed_data, $start_ts) {
$feed_data = $result;
- Debug::log(sprintf("=== %.4f (sec) %s", microtime(true) - $start_ts, get_class($plugin)), Debug::$LOG_VERBOSE);
+ Debug::log(sprintf("=== %.4f (sec) %s", microtime(true) - $start_ts, get_class($plugin)), Debug::LOG_VERBOSE);
},
- $feed_data, $fetch_url, $owner_uid, $feed, $last_article_timestamp, $auth_login, $auth_pass);
+ $feed_data, $hff_feed_url, $hff_owner_uid, $feed, $last_article_timestamp, $auth_login, $auth_pass);
if ($feed_data) {
- Debug::log("feed data has been modified by a plugin.", Debug::$LOG_VERBOSE);
+ Debug::log("feed data has been modified by a plugin.", Debug::LOG_VERBOSE);
} else {
- Debug::log("feed data has not been modified by a plugin.", Debug::$LOG_VERBOSE);
+ Debug::log("feed data has not been modified by a plugin.", Debug::LOG_VERBOSE);
}
// try cache
if (!$feed_data &&
- file_exists($cache_filename) &&
is_readable($cache_filename) &&
- !$auth_login && !$auth_pass &&
+ !$feed_obj->auth_login && !$feed_obj->auth_pass &&
filemtime($cache_filename) > time() - 30) {
- Debug::log("using local cache [$cache_filename].", Debug::$LOG_VERBOSE);
+ Debug::log("using local cache: {$cache_filename}.", Debug::LOG_VERBOSE);
- @$feed_data = file_get_contents($cache_filename);
+ $feed_data = file_get_contents($cache_filename);
if ($feed_data) {
$rss_hash = sha1($feed_data);
}
} else {
- Debug::log("local cache will not be used for this feed", Debug::$LOG_VERBOSE);
+ Debug::log("local cache will not be used for this feed", Debug::LOG_VERBOSE);
}
- global $fetch_last_modified;
-
// fetch feed from source
if (!$feed_data) {
- Debug::log("last unconditional update request: $last_unconditional", Debug::$LOG_VERBOSE);
+ Debug::log("last unconditional update request: {$feed_obj->last_unconditional}", Debug::LOG_VERBOSE);
if (ini_get("open_basedir") && function_exists("curl_init")) {
- Debug::log("not using CURL due to open_basedir restrictions", Debug::$LOG_VERBOSE);
+ Debug::log("not using CURL due to open_basedir restrictions", Debug::LOG_VERBOSE);
}
- if (time() - strtotime($last_unconditional) > Config::get(Config::MAX_CONDITIONAL_INTERVAL)) {
- Debug::log("maximum allowed interval for conditional requests exceeded, forcing refetch", Debug::$LOG_VERBOSE);
+ if (time() - strtotime($feed_obj->last_unconditional) > Config::get(Config::MAX_CONDITIONAL_INTERVAL)) {
+ Debug::log("maximum allowed interval for conditional requests exceeded, forcing refetch", Debug::LOG_VERBOSE);
$force_refetch = true;
} else {
- Debug::log("stored last modified for conditional request: $stored_last_modified", Debug::$LOG_VERBOSE);
+ Debug::log("stored last modified for conditional request: {$feed_obj->last_modified}", Debug::LOG_VERBOSE);
}
- Debug::log("fetching [$fetch_url] (force_refetch: $force_refetch)...", Debug::$LOG_VERBOSE);
+ Debug::log("fetching {$feed_obj->feed_url} (force_refetch: $force_refetch)...", Debug::LOG_VERBOSE);
$feed_data = UrlHelper::fetch([
- "url" => $fetch_url,
- "login" => $auth_login,
- "pass" => $auth_pass,
+ "url" => $feed_obj->feed_url,
+ "login" => $feed_obj->auth_login,
+ "pass" => $feed_obj->auth_pass,
"timeout" => $no_cache ? Config::get(Config::FEED_FETCH_NO_CACHE_TIMEOUT) : Config::get(Config::FEED_FETCH_TIMEOUT),
- "last_modified" => $force_refetch ? "" : $stored_last_modified
+ "last_modified" => $force_refetch ? "" : $feed_obj->last_modified
]);
$feed_data = trim($feed_data);
- global $fetch_effective_url;
- global $fetch_effective_ip_addr;
+ Debug::log("fetch done.", Debug::LOG_VERBOSE);
+ Debug::log(sprintf("effective URL (after redirects): %s (IP: %s) ", UrlHelper::$fetch_effective_url, UrlHelper::$fetch_effective_ip_addr), Debug::LOG_VERBOSE);
+ Debug::log("server last modified: " . UrlHelper::$fetch_last_modified, Debug::LOG_VERBOSE);
- Debug::log("fetch done.", Debug::$LOG_VERBOSE);
- Debug::log("effective URL (after redirects): " . clean($fetch_effective_url) . " (IP: $fetch_effective_ip_addr)", Debug::$LOG_VERBOSE);
- Debug::log("source last modified: " . $fetch_last_modified, Debug::$LOG_VERBOSE);
-
- if ($feed_data && $fetch_last_modified != $stored_last_modified) {
- $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_modified = ? WHERE id = ?");
- $sth->execute([substr($fetch_last_modified, 0, 245), $feed]);
+ if ($feed_data && UrlHelper::$fetch_last_modified != $feed_obj->last_modified) {
+ $feed_obj->last_modified = substr(UrlHelper::$fetch_last_modified, 0, 245);
+ $feed_obj->save();
}
// cache vanilla feed data for re-use
- if ($feed_data && !$auth_pass && !$auth_login && is_writable(Config::get(Config::CACHE_DIR) . "/feeds")) {
+ if ($feed_data && !$feed_obj->auth_pass && !$feed_obj->auth_login && is_writable(Config::get(Config::CACHE_DIR) . "/feeds")) {
$new_rss_hash = sha1($feed_data);
if ($new_rss_hash != $rss_hash) {
- Debug::log("saving $cache_filename", Debug::$LOG_VERBOSE);
- @file_put_contents($cache_filename, $feed_data);
+ Debug::log("saving to local cache: $cache_filename", Debug::LOG_VERBOSE);
+ file_put_contents($cache_filename, $feed_data);
}
}
}
if (!$feed_data) {
- global $fetch_last_error;
- global $fetch_last_error_code;
-
- Debug::log("unable to fetch: $fetch_last_error [$fetch_last_error_code]", Debug::$LOG_VERBOSE);
+ Debug::log(sprintf("unable to fetch: %s [%s]", UrlHelper::$fetch_last_error, UrlHelper::$fetch_last_error_code), Debug::LOG_VERBOSE);
// If-Modified-Since
- if ($fetch_last_error_code == 304) {
- Debug::log("source claims data not modified, nothing to do.", Debug::$LOG_VERBOSE);
+ if (UrlHelper::$fetch_last_error_code == 304) {
+ Debug::log("source claims data not modified, nothing to do.", Debug::LOG_VERBOSE);
$error_message = "";
- $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_error = ?,
- last_successful_update = NOW(),
- last_updated = NOW() WHERE id = ?");
+ $feed_obj->set([
+ 'last_error' => '',
+ 'last_successful_update' => Db::NOW(),
+ 'last_updated' => Db::NOW(),
+ ]);
+
+ $feed_obj->save();
} else {
- $error_message = $fetch_last_error;
+ $error_message = UrlHelper::$fetch_last_error;
- $sth = $pdo->prepare("UPDATE ttrss_feeds SET last_error = ?,
- last_updated = NOW() WHERE id = ?");
- }
+ $feed_obj->set([
+ 'last_error' => $error_message,
+ 'last_updated' => Db::NOW(),
+ ]);
- $sth->execute([$error_message, $feed]);
+ $feed_obj->save();
+ }
return $error_message == "";
}
- Debug::log("running HOOK_FEED_FETCHED handlers...", Debug::$LOG_VERBOSE);
+ Debug::log("running HOOK_FEED_FETCHED handlers...", Debug::LOG_VERBOSE);
$feed_data_checksum = md5($feed_data);
+ // because chain_hooks_callback() accepts variables by value
+ $pff_owner_uid = $feed_obj->owner_uid;
+ $pff_feed_url = $feed_obj->feed_url;
+
$start_ts = microtime(true);
$pluginhost->chain_hooks_callback(PluginHost::HOOK_FEED_FETCHED,
function ($result, $plugin) use (&$feed_data, $start_ts) {
$feed_data = $result;
- Debug::log(sprintf("=== %.4f (sec) %s", microtime(true) - $start_ts, get_class($plugin)), Debug::$LOG_VERBOSE);
+ Debug::log(sprintf("=== %.4f (sec) %s", microtime(true) - $start_ts, get_class($plugin)), Debug::LOG_VERBOSE);
},
- $feed_data, $fetch_url, $owner_uid, $feed);
+ $feed_data, $pff_feed_url, $pff_owner_uid, $feed);
if (md5($feed_data) != $feed_data_checksum) {
- Debug::log("feed data has been modified by a plugin.", Debug::$LOG_VERBOSE);
+ Debug::log("feed data has been modified by a plugin.", Debug::LOG_VERBOSE);
} else {
- Debug::log("feed data has not been modified by a plugin.", Debug::$LOG_VERBOSE);
+ Debug::log("feed data has not been modified by a plugin.", Debug::LOG_VERBOSE);
}
$rss = new FeedParser($feed_data);
@@ -562,46 +542,29 @@ class RSSUtils {
if (!$rss->error()) {
- Debug::log("running HOOK_FEED_PARSED handlers...", Debug::$LOG_VERBOSE);
+ Debug::log("running HOOK_FEED_PARSED handlers...", Debug::LOG_VERBOSE);
// We use local pluginhost here because we need to load different per-user feed plugins
$start_ts = microtime(true);
$pluginhost->chain_hooks_callback(PluginHost::HOOK_FEED_PARSED,
function($result, $plugin) use ($start_ts) {
- Debug::log(sprintf("=== %.4f (sec) %s", microtime(true) - $start_ts, get_class($plugin)), Debug::$LOG_VERBOSE);
+ Debug::log(sprintf("=== %.4f (sec) %s", microtime(true) - $start_ts, get_class($plugin)), Debug::LOG_VERBOSE);
},
$rss, $feed);
- Debug::log("language: $feed_language", Debug::$LOG_VERBOSE);
- Debug::log("processing feed data...", Debug::$LOG_VERBOSE);
-
- if (Config::get(Config::DB_TYPE) == "pgsql") {
- $favicon_interval_qpart = "favicon_last_checked < NOW() - INTERVAL '12 hour'";
- } else {
- $favicon_interval_qpart = "favicon_last_checked < DATE_SUB(NOW(), INTERVAL 12 HOUR)";
- }
-
- $sth = $pdo->prepare("SELECT owner_uid,favicon_avg_color,
- (favicon_last_checked IS NULL OR $favicon_interval_qpart) AS
- favicon_needs_check
- FROM ttrss_feeds WHERE id = ?");
- $sth->execute([$feed]);
+ Debug::log("language: $feed_language", Debug::LOG_VERBOSE);
+ Debug::log("processing feed data...", Debug::LOG_VERBOSE);
- if ($row = $sth->fetch()) {
- $favicon_needs_check = $row["favicon_needs_check"];
- $favicon_avg_color = $row["favicon_avg_color"];
- $owner_uid = $row["owner_uid"];
- } else {
- return false;
- }
+ $site_url = mb_substr(rewrite_relative_url($feed_obj->feed_url, clean($rss->get_link())), 0, 245);
- $site_url = mb_substr(rewrite_relative_url($fetch_url, clean($rss->get_link())), 0, 245);
+ Debug::log("site_url: $site_url", Debug::LOG_VERBOSE);
+ Debug::log("feed_title: {$rss->get_title()}", Debug::LOG_VERBOSE);
- Debug::log("site_url: $site_url", Debug::$LOG_VERBOSE);
- Debug::log("feed_title: " . clean($rss->get_title()), Debug::$LOG_VERBOSE);
+ Debug::log("favicon: needs check: {$feed_obj->favicon_needs_check} is custom: {$feed_obj->favicon_is_custom} avg color: {$feed_obj->favicon_avg_color}",
+ Debug::LOG_VERBOSE);
- if ($favicon_needs_check || $force_refetch) {
+ if ($feed_obj->favicon_needs_check || $force_refetch) {
/* terrible hack: if we crash on floicon shit here, we won't check
* the icon avgcolor again (unless the icon got updated) */
@@ -609,70 +572,74 @@ class RSSUtils {
$favicon_file = Config::get(Config::ICONS_DIR) . "/$feed.ico";
$favicon_modified = file_exists($favicon_file) ? filemtime($favicon_file) : -1;
- Debug::log("checking favicon for feed $feed...", Debug::$LOG_VERBOSE);
-
- self::check_feed_favicon($site_url, $feed);
- $favicon_modified_new = file_exists($favicon_file) ? filemtime($favicon_file) : -1;
+ if (!$feed_obj->favicon_is_custom) {
+ Debug::log("favicon: trying to update favicon...", Debug::LOG_VERBOSE);
+ self::update_favicon($site_url, $feed);
- if ($favicon_modified_new > $favicon_modified)
- $favicon_avg_color = '';
+ if ((file_exists($favicon_file) ? filemtime($favicon_file) : -1) > $favicon_modified)
+ $feed_obj->favicon_avg_color = null;
+ }
- $favicon_colorstring = "";
- if (file_exists($favicon_file) && function_exists("imagecreatefromstring") && $favicon_avg_color == '') {
+ if (is_readable($favicon_file) && function_exists("imagecreatefromstring") && empty($feed_obj->favicon_avg_color)) {
require_once "colors.php";
- $sth = $pdo->prepare("UPDATE ttrss_feeds SET favicon_avg_color = 'fail' WHERE
- id = ?");
- $sth->execute([$feed]);
+ Debug::log("favicon: trying to calculate average color...", Debug::LOG_VERBOSE);
- $favicon_color = \Colors\calculate_avg_color($favicon_file);
+ $feed_obj->favicon_avg_color = 'fail';
+ $feed_obj->save();
- $favicon_colorstring = ",favicon_avg_color = " . $pdo->quote($favicon_color);
+ $feed_obj->favicon_avg_color = \Colors\calculate_avg_color($favicon_file);
+ $feed_obj->save();
- } else if ($favicon_avg_color == 'fail') {
- Debug::log("floicon failed on this file, not trying to recalculate avg color", Debug::$LOG_VERBOSE);
- }
+ Debug::log("favicon: avg color: {$feed_obj->favicon_avg_color}", Debug::LOG_VERBOSE);
- $sth = $pdo->prepare("UPDATE ttrss_feeds SET favicon_last_checked = NOW()
- $favicon_colorstring WHERE id = ?");
- $sth->execute([$feed]);
+ } else if ($feed_obj->favicon_avg_color == 'fail') {
+ Debug::log("floicon failed $favicon_file, not trying to recalculate avg color", Debug::LOG_VERBOSE);
+ }
}
- Debug::log("loading filters & labels...", Debug::$LOG_VERBOSE);
+ Debug::log("loading filters & labels...", Debug::LOG_VERBOSE);
- $filters = self::load_filters($feed, $owner_uid);
+ $filters = self::load_filters($feed, $feed_obj->owner_uid);
- if (Debug::get_loglevel() >= Debug::$LOG_EXTENDED) {
+ if (Debug::get_loglevel() >= Debug::LOG_EXTENDED) {
print_r($filters);
}
- Debug::log("" . count($filters) . " filters loaded.", Debug::$LOG_VERBOSE);
+ Debug::log("" . count($filters) . " filters loaded.", Debug::LOG_VERBOSE);
$items = $rss->get_items();
if (!is_array($items)) {
- Debug::log("no articles found.", Debug::$LOG_VERBOSE);
+ Debug::log("no articles found.", Debug::LOG_VERBOSE);
- $sth = $pdo->prepare("UPDATE ttrss_feeds
- SET last_updated = NOW(), last_unconditional = NOW(), last_error = '' WHERE id = ?");
- $sth->execute([$feed]);
+ $feed_obj->set([
+ 'last_updated' => Db::NOW(),
+ 'last_unconditional' => Db::NOW(),
+ 'last_error' => '',
+ ]);
+
+ $feed_obj->save();
return true; // no articles
}
- Debug::log("processing articles...", Debug::$LOG_VERBOSE);
+ Debug::log("processing articles...", Debug::LOG_VERBOSE);
$tstart = time();
foreach ($items as $item) {
$pdo->beginTransaction();
+ Debug::log("=================================================================================================================================",
+ Debug::LOG_VERBOSE);
+
if (Debug::get_loglevel() >= 3) {
print_r($item);
}
if (ini_get("max_execution_time") > 0 && time() - $tstart >= ini_get("max_execution_time") * 0.7) {
- Debug::log("looks like there's too many articles to process at once, breaking out", Debug::$LOG_VERBOSE);
+ Debug::log("looks like there's too many articles to process at once, breaking out.", Debug::LOG_VERBOSE);
$pdo->commit();
break;
}
@@ -686,15 +653,16 @@ class RSSUtils {
continue;
}
- $entry_guid_hashed_compat = 'SHA1:' . sha1("$owner_uid,$entry_guid");
- $entry_guid_hashed = json_encode(["ver" => 2, "uid" => $owner_uid, "hash" => 'SHA1:' . sha1($entry_guid)]);
- $entry_guid = "$owner_uid,$entry_guid";
+ $entry_guid_hashed_compat = 'SHA1:' . sha1("{$feed_obj->owner_uid},$entry_guid");
+ $entry_guid_hashed = json_encode(["ver" => 2, "uid" => $feed_obj->owner_uid, "hash" => 'SHA1:' . sha1($entry_guid)]);
+ $entry_guid = "$feed_obj->owner_uid,$entry_guid";
- Debug::log("guid $entry_guid (hash: $entry_guid_hashed compat: $entry_guid_hashed_compat)", Debug::$LOG_VERBOSE);
+ Debug::log("guid $entry_guid (hash: $entry_guid_hashed compat: $entry_guid_hashed_compat)", Debug::LOG_VERBOSE);
$entry_timestamp = (int)$item->get_date();
- Debug::log("orig date: " . $item->get_date(), Debug::$LOG_VERBOSE);
+ Debug::log(sprintf("orig date: %s (%s)", $item->get_date(), date("Y-m-d H:i:s", $item->get_date())),
+ Debug::LOG_VERBOSE);
$entry_title = strip_tags($item->get_title());
@@ -702,9 +670,9 @@ class RSSUtils {
$entry_language = mb_substr(trim($item->get_language()), 0, 2);
- Debug::log("title $entry_title", Debug::$LOG_VERBOSE);
- Debug::log("link $entry_link", Debug::$LOG_VERBOSE);
- Debug::log("language $entry_language", Debug::$LOG_VERBOSE);
+ Debug::log("title $entry_title", Debug::LOG_VERBOSE);
+ Debug::log("link $entry_link", Debug::LOG_VERBOSE);
+ Debug::log("language $entry_language", Debug::LOG_VERBOSE);
if (!$entry_title) $entry_title = date("Y-m-d H:i:s", $entry_timestamp);;
@@ -723,13 +691,13 @@ class RSSUtils {
$entry_author = strip_tags($item->get_author());
$entry_guid = mb_substr($entry_guid, 0, 245);
- Debug::log("author $entry_author", Debug::$LOG_VERBOSE);
- Debug::log("looking for tags...", Debug::$LOG_VERBOSE);
+ Debug::log("author $entry_author", Debug::LOG_VERBOSE);
+ Debug::log("looking for tags...", Debug::LOG_VERBOSE);
$entry_tags = $item->get_categories();
- Debug::log("tags found: " . join(", ", $entry_tags), Debug::$LOG_VERBOSE);
+ Debug::log("tags found: " . join(", ", $entry_tags), Debug::LOG_VERBOSE);
- Debug::log("done collecting data.", Debug::$LOG_VERBOSE);
+ Debug::log("done collecting data.", Debug::LOG_VERBOSE);
$sth = $pdo->prepare("SELECT id, content_hash, lang FROM ttrss_entries
WHERE guid IN (?, ?, ?)");
@@ -738,9 +706,9 @@ class RSSUtils {
if ($row = $sth->fetch()) {
$base_entry_id = $row["id"];
$entry_stored_hash = $row["content_hash"];
- $article_labels = Article::_get_labels($base_entry_id, $owner_uid);
+ $article_labels = Article::_get_labels($base_entry_id, $feed_obj->owner_uid);
- $existing_tags = Article::_get_tags($base_entry_id, $owner_uid);
+ $existing_tags = Article::_get_tags($base_entry_id, $feed_obj->owner_uid);
$entry_tags = array_unique(array_merge($entry_tags, $existing_tags));
} else {
$base_entry_id = false;
@@ -748,13 +716,13 @@ class RSSUtils {
$article_labels = array();
}
- Debug::log("looking for enclosures...", Debug::$LOG_VERBOSE);
+ Debug::log("looking for enclosures...", Debug::LOG_VERBOSE);
// enclosures
$enclosures = array();
- $encs = $item->_get_enclosures();
+ $encs = $item->get_enclosures();
if (is_array($encs)) {
foreach ($encs as $e) {
@@ -782,7 +750,7 @@ class RSSUtils {
}
}
- $article = array("owner_uid" => $owner_uid, // read only
+ $article = array("owner_uid" => $feed_obj->owner_uid, // read only
"guid" => $entry_guid, // read only
"guid_hashed" => $entry_guid_hashed, // read only
"title" => $entry_title,
@@ -798,33 +766,35 @@ class RSSUtils {
"num_comments" => $num_comments,
"enclosures" => $enclosures,
"feed" => array("id" => $feed,
- "fetch_url" => $fetch_url,
+ "fetch_url" => $feed_obj->feed_url,
"site_url" => $site_url,
- "cache_images" => $cache_images)
+ "cache_images" => $feed_obj->cache_images)
);
$entry_plugin_data = "";
$entry_current_hash = self::calculate_article_hash($article, $pluginhost);
- Debug::log("article hash: $entry_current_hash [stored=$entry_stored_hash]", Debug::$LOG_VERBOSE);
+ Debug::log("article hash: $entry_current_hash [stored=$entry_stored_hash]", Debug::LOG_VERBOSE);
if ($entry_current_hash == $entry_stored_hash && !isset($_REQUEST["force_rehash"])) {
- Debug::log("stored article seems up to date [IID: $base_entry_id], updating timestamp only", Debug::$LOG_VERBOSE);
+ Debug::log("stored article seems up to date [IID: $base_entry_id], updating timestamp only.", Debug::LOG_VERBOSE);
// we keep encountering the entry in feeds, so we need to
// update date_updated column so that we don't get horrible
// dupes when the entry gets purged and reinserted again e.g.
// in the case of SLOW SLOW OMG SLOW updating feeds
- $sth = $pdo->prepare("UPDATE ttrss_entries SET date_updated = NOW()
- WHERE id = ?");
- $sth->execute([$base_entry_id]);
+ $entry_obj = ORM::for_table('ttrss_entries')
+ ->find_one($base_entry_id)
+ ->set('date_updated', Db::NOW())
+ ->save();
$pdo->commit();
+
continue;
}
- Debug::log("hash differs, applying plugin filters:", Debug::$LOG_VERBOSE);
+ Debug::log("hash differs, running HOOK_ARTICLE_FILTER handlers...", Debug::LOG_VERBOSE);
$start_ts = microtime(true);
@@ -835,7 +805,7 @@ class RSSUtils {
$entry_plugin_data .= mb_strtolower(get_class($plugin)) . ",";
Debug::log(sprintf("=== %.4f (sec) %s", microtime(true) - $start_ts, get_class($plugin)),
- Debug::$LOG_VERBOSE);
+ Debug::LOG_VERBOSE);
},
$article);
@@ -845,7 +815,7 @@ class RSSUtils {
print "\n";
}
- Debug::log("plugin data: $entry_plugin_data", Debug::$LOG_VERBOSE);
+ Debug::log("plugin data: {$entry_plugin_data}", Debug::LOG_VERBOSE);
// Workaround: 4-byte unicode requires utf8mb4 in MySQL. See https://tt-rss.org/forum/viewtopic.php?f=1&t=3377&p=20077#p20077
if (Config::get(Config::DB_TYPE) == "mysql" && Config::get(Config::MYSQL_CHARSET) != "UTF8MB4") {
@@ -868,33 +838,35 @@ class RSSUtils {
// $article_filters should be renamed to something like $filter_actions; actual filter objects are in $matched_filters
$pluginhost->run_hooks(PluginHost::HOOK_FILTER_TRIGGERED,
- $feed, $owner_uid, $article, $matched_filters, $matched_rules, $article_filters);
+ $feed, $feed_obj->owner_uid, $article, $matched_filters, $matched_rules, $article_filters);
$matched_filter_ids = array_map(function($f) { return $f['id']; }, $matched_filters);
if (count($matched_filter_ids) > 0) {
- $filter_ids_qmarks = arr_qmarks($matched_filter_ids);
+ $filter_objs = ORM::for_table('ttrss_filters2')
+ ->where('owner_uid', $feed_obj->owner_uid)
+ ->where_in('id', $matched_filter_ids);
- $fsth = $pdo->prepare("UPDATE ttrss_filters2 SET last_triggered = NOW() WHERE
- id IN ($filter_ids_qmarks) AND owner_uid = ?");
-
- $fsth->execute(array_merge($matched_filter_ids, [$owner_uid]));
+ foreach ($filter_objs as $filter_obj) {
+ $filter_obj->set('last_triggered', Db::NOW());
+ $filter_obj->save();
+ }
}
- if (Debug::get_loglevel() >= Debug::$LOG_EXTENDED) {
- Debug::log("matched filters: ", Debug::$LOG_VERBOSE);
+ if (Debug::get_loglevel() >= Debug::LOG_EXTENDED) {
+ Debug::log("matched filters: ", Debug::LOG_VERBOSE);
if (count($matched_filters) != 0) {
print_r($matched_filters);
}
- Debug::log("matched filter rules: ", Debug::$LOG_VERBOSE);
+ Debug::log("matched filter rules: ", Debug::LOG_VERBOSE);
if (count($matched_rules) != 0) {
print_r($matched_rules);
}
- Debug::log("filter actions: ", Debug::$LOG_VERBOSE);
+ Debug::log("filter actions: ", Debug::LOG_VERBOSE);
if (count($article_filters) != 0) {
print_r($article_filters);
@@ -905,7 +877,7 @@ class RSSUtils {
$plugin_filter_actions = $pluginhost->get_filter_actions();
if (count($plugin_filter_names) > 0) {
- Debug::log("applying plugin filter actions...", Debug::$LOG_VERBOSE);
+ Debug::log("applying plugin filter actions...", Debug::LOG_VERBOSE);
foreach ($plugin_filter_names as $pfn) {
list($pfclass,$pfaction) = explode(":", $pfn["param"]);
@@ -913,18 +885,18 @@ class RSSUtils {
if (isset($plugin_filter_actions[$pfclass])) {
$plugin = $pluginhost->get_plugin($pfclass);
- Debug::log("... $pfclass: $pfaction", Debug::$LOG_VERBOSE);
+ Debug::log("... $pfclass: $pfaction", Debug::LOG_VERBOSE);
if ($plugin) {
$start = microtime(true);
$article = $plugin->hook_article_filter_action($article, $pfaction);
- Debug::log(sprintf("=== %.4f (sec)", microtime(true) - $start), Debug::$LOG_VERBOSE);
+ Debug::log(sprintf("=== %.4f (sec)", microtime(true) - $start), Debug::LOG_VERBOSE);
} else {
- Debug::log("??? $pfclass: plugin object not found.", Debug::$LOG_VERBOSE);
+ Debug::log("??? $pfclass: plugin object not found.", Debug::LOG_VERBOSE);
}
} else {
- Debug::log("??? $pfclass: filter plugin not registered.", Debug::$LOG_VERBOSE);
+ Debug::log("??? $pfclass: filter plugin not registered.", Debug::LOG_VERBOSE);
}
}
}
@@ -948,20 +920,20 @@ class RSSUtils {
$entry_timestamp_fmt = strftime("%Y/%m/%d %H:%M:%S", $entry_timestamp);
- Debug::log("date $entry_timestamp [$entry_timestamp_fmt]", Debug::$LOG_VERBOSE);
- Debug::log("num_comments: $num_comments", Debug::$LOG_VERBOSE);
+ Debug::log("date: $entry_timestamp ($entry_timestamp_fmt)", Debug::LOG_VERBOSE);
+ Debug::log("num_comments: $num_comments", Debug::LOG_VERBOSE);
- if (Debug::get_loglevel() >= Debug::$LOG_EXTENDED) {
- Debug::log("article labels:", Debug::$LOG_VERBOSE);
+ if (Debug::get_loglevel() >= Debug::LOG_EXTENDED) {
+ Debug::log("article labels:", Debug::LOG_VERBOSE);
if (count($article_labels) != 0) {
print_r($article_labels);
}
}
- Debug::log("force catchup: $entry_force_catchup", Debug::$LOG_VERBOSE);
+ Debug::log("force catchup: $entry_force_catchup", Debug::LOG_VERBOSE);
- if ($cache_images)
+ if ($feed_obj->cache_images)
self::cache_media($entry_content, $site_url);
$csth = $pdo->prepare("SELECT id FROM ttrss_entries
@@ -970,7 +942,7 @@ class RSSUtils {
if (!$row = $csth->fetch()) {
- Debug::log("base guid [$entry_guid or $entry_guid_hashed] not found, creating...", Debug::$LOG_VERBOSE);
+ Debug::log("base guid [$entry_guid or $entry_guid_hashed] not found, creating...", Debug::LOG_VERBOSE);
// base post entry does not exist, create it
@@ -1018,36 +990,36 @@ class RSSUtils {
if ($row = $csth->fetch()) {
- Debug::log("base guid found, checking for user record", Debug::$LOG_VERBOSE);
+ Debug::log("base guid found, checking for user record", Debug::LOG_VERBOSE);
$ref_id = $row['id'];
$entry_ref_id = $ref_id;
if (self::find_article_filter($article_filters, "filter")) {
- Debug::log("article is filtered out, nothing to do.", Debug::$LOG_VERBOSE);
+ Debug::log("article is filtered out, nothing to do.", Debug::LOG_VERBOSE);
$pdo->commit();
continue;
}
$score = self::calculate_article_score($article_filters) + $entry_score_modifier;
- Debug::log("initial score: $score [including plugin modifier: $entry_score_modifier]", Debug::$LOG_VERBOSE);
+ Debug::log("initial score: $score [including plugin modifier: $entry_score_modifier]", Debug::LOG_VERBOSE);
// check for user post link to main table
$sth = $pdo->prepare("SELECT ref_id, int_id FROM ttrss_user_entries WHERE
ref_id = ? AND owner_uid = ?");
- $sth->execute([$ref_id, $owner_uid]);
+ $sth->execute([$ref_id, $feed_obj->owner_uid]);
// okay it doesn't exist - create user entry
if ($row = $sth->fetch()) {
$entry_ref_id = $row["ref_id"];
$entry_int_id = $row["int_id"];
- Debug::log("user record FOUND: RID: $entry_ref_id, IID: $entry_int_id", Debug::$LOG_VERBOSE);
+ Debug::log("user record FOUND: RID: $entry_ref_id, IID: $entry_int_id", Debug::LOG_VERBOSE);
} else {
- Debug::log("user record not found, creating...", Debug::$LOG_VERBOSE);
+ Debug::log("user record not found, creating...", Debug::LOG_VERBOSE);
if ($score >= -500 && !self::find_article_filter($article_filters, 'catchup') && !$entry_force_catchup) {
$unread = 1;
@@ -1079,20 +1051,20 @@ class RSSUtils {
last_marked, last_published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, '', '', '', ".$last_marked.", ".$last_published.")");
- $sth->execute([$ref_id, $owner_uid, $feed, $unread, $last_read_qpart, $marked,
+ $sth->execute([$ref_id, $feed_obj->owner_uid, $feed, $unread, $last_read_qpart, $marked,
$published, $score]);
$sth = $pdo->prepare("SELECT int_id FROM ttrss_user_entries WHERE
ref_id = ? AND owner_uid = ? AND
feed_id = ? LIMIT 1");
- $sth->execute([$ref_id, $owner_uid, $feed]);
+ $sth->execute([$ref_id, $feed_obj->owner_uid, $feed]);
if ($row = $sth->fetch())
$entry_int_id = $row['int_id'];
}
- Debug::log("resulting RID: $entry_ref_id, IID: $entry_int_id", Debug::$LOG_VERBOSE);
+ Debug::log("resulting RID: $entry_ref_id, IID: $entry_int_id", Debug::LOG_VERBOSE);
if (Config::get(Config::DB_TYPE) == "pgsql")
$tsvector_qpart = "tsvector_combined = to_tsvector(:ts_lang, :ts_content),";
@@ -1134,36 +1106,36 @@ class RSSUtils {
SET score = ? WHERE ref_id = ?");
$sth->execute([$score, $ref_id]);
- if ($mark_unread_on_update &&
+ if ($feed_obj->mark_unread_on_update &&
!$entry_force_catchup &&
!self::find_article_filter($article_filters, 'catchup')) {
- Debug::log("article updated, marking unread as requested.", Debug::$LOG_VERBOSE);
+ Debug::log("article updated, marking unread as requested.", Debug::LOG_VERBOSE);
$sth = $pdo->prepare("UPDATE ttrss_user_entries
SET last_read = null, unread = true WHERE ref_id = ?");
$sth->execute([$ref_id]);
} else {
- Debug::log("article updated, but we're forbidden to mark it unread.", Debug::$LOG_VERBOSE);
+ Debug::log("article updated, but we're forbidden to mark it unread.", Debug::LOG_VERBOSE);
}
}
- Debug::log("assigning labels [other]...", Debug::$LOG_VERBOSE);
+ Debug::log("assigning labels [other]...", Debug::LOG_VERBOSE);
foreach ($article_labels as $label) {
- Labels::add_article($entry_ref_id, $label[1], $owner_uid);
+ Labels::add_article($entry_ref_id, $label[1], $feed_obj->owner_uid);
}
- Debug::log("assigning labels [filters]...", Debug::$LOG_VERBOSE);
+ Debug::log("assigning labels [filters]...", Debug::LOG_VERBOSE);
self::assign_article_to_label_filters($entry_ref_id, $article_filters,
- $owner_uid, $article_labels);
+ $feed_obj->owner_uid, $article_labels);
- if ($cache_images)
+ if ($feed_obj->cache_images)
self::cache_enclosures($enclosures, $site_url);
- if (Debug::get_loglevel() >= Debug::$LOG_EXTENDED) {
- Debug::log("article enclosures:", Debug::$LOG_VERBOSE);
+ if (Debug::get_loglevel() >= Debug::LOG_EXTENDED) {
+ Debug::log("article enclosures:", Debug::LOG_VERBOSE);
print_r($enclosures);
}
@@ -1206,13 +1178,13 @@ class RSSUtils {
$boring_tags = array_map('trim',
explode(",", mb_strtolower(
- get_pref(Prefs::BLACKLISTED_TAGS, $owner_uid))));
+ get_pref(Prefs::BLACKLISTED_TAGS, $feed_obj->owner_uid))));
$entry_tags = FeedItem_Common::normalize_categories(
array_unique(
array_diff($entry_tags, $boring_tags)));
- Debug::log("filtered tags: " . implode(", ", $entry_tags), Debug::$LOG_VERBOSE);
+ Debug::log("filtered tags: " . implode(", ", $entry_tags), Debug::LOG_VERBOSE);
// Save article tags in the database
@@ -1227,10 +1199,10 @@ class RSSUtils {
VALUES (?, ?, ?)");
foreach ($entry_tags as $tag) {
- $tsth->execute([$tag, $entry_int_id, $owner_uid]);
+ $tsth->execute([$tag, $entry_int_id, $feed_obj->owner_uid]);
if (!$tsth->fetch()) {
- $usth->execute([$owner_uid, $tag, $entry_int_id]);
+ $usth->execute([$feed_obj->owner_uid, $tag, $entry_int_id]);
}
}
@@ -1243,52 +1215,58 @@ class RSSUtils {
$tsth->execute([
join(",", $entry_tags),
$entry_ref_id,
- $owner_uid
+ $feed_obj->owner_uid
]);
}
- Debug::log("article processed", Debug::$LOG_VERBOSE);
+ Debug::log("article processed.", Debug::LOG_VERBOSE);
$pdo->commit();
}
- Debug::log("purging feed...", Debug::$LOG_VERBOSE);
+ Debug::log("=================================================================================================================================",
+ Debug::LOG_VERBOSE);
+
+ Debug::log("purging feed...", Debug::LOG_VERBOSE);
Feeds::_purge($feed, 0);
- $sth = $pdo->prepare("UPDATE ttrss_feeds SET
- last_updated = NOW(),
- last_unconditional = NOW(),
- last_successful_update = NOW(),
- last_error = '' WHERE id = ?");
- $sth->execute([$feed]);
+ $feed_obj->set([
+ 'last_updated' => Db::NOW(),
+ 'last_unconditional' => Db::NOW(),
+ 'last_successful_update' => Db::NOW(),
+ 'last_error' => '',
+ ]);
+
+ $feed_obj->save();
} else {
$error_msg = mb_substr($rss->error(), 0, 245);
- Debug::log("fetch error: $error_msg", Debug::$LOG_VERBOSE);
+ Debug::log("fetch error: $error_msg", Debug::LOG_VERBOSE);
if (count($rss->errors()) > 1) {
foreach ($rss->errors() as $error) {
- Debug::log("+ $error", Debug::$LOG_VERBOSE);
+ Debug::log("+ $error", Debug::LOG_VERBOSE);
}
}
- $sth = $pdo->prepare("UPDATE ttrss_feeds SET
- last_error = ?,
- last_updated = NOW(),
- last_unconditional = NOW() WHERE id = ?");
- $sth->execute([$error_msg, $feed]);
+ $feed_obj->set([
+ 'last_updated' => Db::NOW(),
+ 'last_unconditional' => Db::NOW(),
+ 'last_error' => $error_msg,
+ ]);
+
+ $feed_obj->save();
unset($rss);
- Debug::log("update failed.", Debug::$LOG_VERBOSE);
+ Debug::log("update failed.", Debug::LOG_VERBOSE);
return false;
}
- Debug::log("update done.", Debug::$LOG_VERBOSE);
-
+ Debug::log("update done.", Debug::LOG_VERBOSE);
return true;
}
@@ -1304,13 +1282,9 @@ class RSSUtils {
$local_filename = sha1($src);
- Debug::log("cache_enclosures: downloading: $src to $local_filename", Debug::$LOG_VERBOSE);
+ Debug::log("cache_enclosures: downloading: $src to $local_filename", Debug::LOG_VERBOSE);
if (!$cache->exists($local_filename)) {
-
- global $fetch_last_error_code;
- global $fetch_last_error;
-
$file_content = UrlHelper::fetch(array("url" => $src,
"http_referrer" => $src,
"max_size" => Config::get(Config::MAX_CACHE_FILE_SIZE)));
@@ -1318,7 +1292,7 @@ class RSSUtils {
if ($file_content) {
$cache->put($local_filename, $file_content);
} else {
- Debug::log("cache_enclosures: failed with $fetch_last_error_code: $fetch_last_error");
+ Debug::log("cache_enclosures: failed with ".UrlHelper::$fetch_last_error_code.": ".UrlHelper::$fetch_last_error);
}
} else if (is_writable($local_filename)) {
$cache->touch($local_filename);
@@ -1333,13 +1307,10 @@ class RSSUtils {
$url = rewrite_relative_url($site_url, $url);
$local_filename = sha1($url);
- Debug::log("cache_media: checking $url", Debug::$LOG_VERBOSE);
+ Debug::log("cache_media: checking $url", Debug::LOG_VERBOSE);
if (!$cache->exists($local_filename)) {
- Debug::log("cache_media: downloading: $url to $local_filename", Debug::$LOG_VERBOSE);
-
- global $fetch_last_error_code;
- global $fetch_last_error;
+ Debug::log("cache_media: downloading: $url to $local_filename", Debug::LOG_VERBOSE);
$file_content = UrlHelper::fetch(array("url" => $url,
"http_referrer" => $url,
@@ -1348,7 +1319,7 @@ class RSSUtils {
if ($file_content) {
$cache->put($local_filename, $file_content);
} else {
- Debug::log("cache_media: failed with $fetch_last_error_code: $fetch_last_error");
+ Debug::log("cache_media: failed with ".UrlHelper::$fetch_last_error_code.": ".UrlHelper::$fetch_last_error);
}
} else if ($cache->is_writable($local_filename)) {
$cache->touch($local_filename);
@@ -1407,7 +1378,7 @@ class RSSUtils {
}
static function expire_lock_files() {
- Debug::log("Removing old lock files...", Debug::$LOG_VERBOSE);
+ Debug::log("Removing old lock files...", Debug::LOG_VERBOSE);
$num_deleted = 0;
@@ -1657,12 +1628,12 @@ class RSSUtils {
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
}
- static function check_feed_favicon($site_url, $feed) {
+ static function update_favicon(string $site_url, int $feed) {
$icon_file = Config::get(Config::ICONS_DIR) . "/$feed.ico";
$favicon_url = self::get_favicon_url($site_url);
if (!$favicon_url) {
- Debug::log("couldn't find favicon URL in $site_url", Debug::$LOG_VERBOSE);
+ Debug::log("favicon: couldn't find favicon URL in $site_url", Debug::LOG_VERBOSE);
return false;
}
@@ -1673,7 +1644,7 @@ class RSSUtils {
//'type' => 'image',
]);
if (!$contents) {
- Debug::log("fetching favicon $favicon_url failed", Debug::$LOG_VERBOSE);
+ Debug::log("favicon: fetching $favicon_url failed", Debug::LOG_VERBOSE);
return false;
}
@@ -1681,35 +1652,35 @@ class RSSUtils {
// Patterns gleaned from the file(1) source code.
if (preg_match('/^\x00\x00\x01\x00/', $contents)) {
// 0 string \000\000\001\000 MS Windows icon resource
- //error_log("check_feed_favicon: favicon_url=$favicon_url isa MS Windows icon resource");
+ //error_log("update_favicon: favicon_url=$favicon_url isa MS Windows icon resource");
}
elseif (preg_match('/^GIF8/', $contents)) {
// 0 string GIF8 GIF image data
- //error_log("check_feed_favicon: favicon_url=$favicon_url isa GIF image");
+ //error_log("update_favicon: favicon_url=$favicon_url isa GIF image");
}
elseif (preg_match('/^\x89PNG\x0d\x0a\x1a\x0a/', $contents)) {
// 0 string \x89PNG\x0d\x0a\x1a\x0a PNG image data
- //error_log("check_feed_favicon: favicon_url=$favicon_url isa PNG image");
+ //error_log("update_favicon: favicon_url=$favicon_url isa PNG image");
}
elseif (preg_match('/^\xff\xd8/', $contents)) {
// 0 beshort 0xffd8 JPEG image data
- //error_log("check_feed_favicon: favicon_url=$favicon_url isa JPG image");
+ //error_log("update_favicon: favicon_url=$favicon_url isa JPG image");
}
elseif (preg_match('/^BM/', $contents)) {
// 0 string BM PC bitmap (OS2, Windows BMP files)
- //error_log("check_feed_favicon, favicon_url=$favicon_url isa BMP image");
+ //error_log("update_favicon, favicon_url=$favicon_url isa BMP image");
}
else {
- //error_log("check_feed_favicon: favicon_url=$favicon_url isa UNKNOWN type");
- Debug::log("favicon $favicon_url type is unknown (not updating)", Debug::$LOG_VERBOSE);
+ //error_log("update_favicon: favicon_url=$favicon_url isa UNKNOWN type");
+ Debug::log("favicon $favicon_url type is unknown (not updating)", Debug::LOG_VERBOSE);
return false;
}
- Debug::log("setting contents of $icon_file", Debug::$LOG_VERBOSE);
+ Debug::log("favicon: saving to $icon_file", Debug::LOG_VERBOSE);
$fp = @fopen($icon_file, "w");
if (!$fp) {
- Debug::log("failed to open $icon_file for writing", Debug::$LOG_VERBOSE);
+ Debug::log("favicon: failed to open $icon_file for writing", Debug::LOG_VERBOSE);
return false;
}
@@ -1726,13 +1697,13 @@ class RSSUtils {
"\x1f" . "\x8b" . "\x08", 0) === 0;
}
- static function load_filters($feed_id, $owner_uid) {
+ static function load_filters(int $feed_id, int $owner_uid) {
$filters = array();
$feed_id = (int) $feed_id;
- $cat_id = (int)Feeds::_cat_of_feed($feed_id);
+ $cat_id = Feeds::_cat_of($feed_id);
- if ($cat_id == 0)
+ if (!$cat_id)
$null_cat_qpart = "cat_id IS NULL OR";
else
$null_cat_qpart = "";
@@ -1845,7 +1816,7 @@ class RSSUtils {
* @access public
* @return mixed The favicon URL, or false if none was found.
*/
- static function get_favicon_url($url) {
+ static function get_favicon_url(string $url) {
$favicon_url = false;
diff --git a/classes/sanitizer.php b/classes/sanitizer.php
index 52feb5e28..07766dc16 100644
--- a/classes/sanitizer.php
+++ b/classes/sanitizer.php
@@ -49,6 +49,10 @@ class Sanitizer {
return false;
}
+ private static function is_prefix_https() {
+ return parse_url(Config::get(Config::SELF_URL_PATH), PHP_URL_SCHEME) == 'https';
+ }
+
public static function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
if (!$owner && isset($_SESSION["uid"]))
@@ -60,7 +64,9 @@ class Sanitizer {
$doc->loadHTML('<?xml encoding="UTF-8">' . $res);
$xpath = new DOMXPath($doc);
- $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
+ // is it a good idea to possibly rewrite urls to our own prefix?
+ // $rewrite_base_url = $site_url ? $site_url : Config::get_self_url();
+ $rewrite_base_url = $site_url ? $site_url : "http://domain.invalid/";
$entries = $xpath->query('(//a[@href]|//img[@src]|//source[@srcset|@src])');
@@ -125,7 +131,7 @@ class Sanitizer {
if (!self::iframe_whitelisted($entry)) {
$entry->setAttribute('sandbox', 'allow-scripts');
} else {
- if (is_prefix_https()) {
+ if (self::is_prefix_https()) {
$entry->setAttribute("src",
str_replace("http://", "https://",
$entry->getAttribute("src")));
diff --git a/classes/urlhelper.php b/classes/urlhelper.php
index bf2e22a76..55d5d1e6a 100644
--- a/classes/urlhelper.php
+++ b/classes/urlhelper.php
@@ -1,5 +1,14 @@
<?php
class UrlHelper {
+ static $fetch_last_error;
+ static $fetch_last_error_code;
+ static $fetch_last_error_content;
+ static $fetch_last_content_type;
+ static $fetch_last_modified;
+ static $fetch_effective_url;
+ static $fetch_effective_ip_addr;
+ static $fetch_curl_used;
+
static function build_url($parts) {
$tmp = $parts['scheme'] . "://" . $parts['host'];
@@ -158,23 +167,14 @@ class UrlHelper {
public static function fetch($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
- global $fetch_last_error;
- global $fetch_last_error_code;
- global $fetch_last_error_content;
- global $fetch_last_content_type;
- global $fetch_last_modified;
- global $fetch_effective_url;
- global $fetch_effective_ip_addr;
- global $fetch_curl_used;
-
- $fetch_last_error = false;
- $fetch_last_error_code = -1;
- $fetch_last_error_content = "";
- $fetch_last_content_type = "";
- $fetch_curl_used = false;
- $fetch_last_modified = "";
- $fetch_effective_url = "";
- $fetch_effective_ip_addr = "";
+ self::$fetch_last_error = false;
+ self::$fetch_last_error_code = -1;
+ self::$fetch_last_error_content = "";
+ self::$fetch_last_content_type = "";
+ self::$fetch_curl_used = false;
+ self::$fetch_last_modified = "";
+ self::$fetch_effective_url = "";
+ self::$fetch_effective_ip_addr = "";
if (!is_array($options)) {
@@ -219,7 +219,7 @@ class UrlHelper {
$url = self::validate($url, true);
if (!$url) {
- $fetch_last_error = "Requested URL failed extended validation.";
+ self::$fetch_last_error = "Requested URL failed extended validation.";
return false;
}
@@ -227,13 +227,13 @@ class UrlHelper {
$ip_addr = gethostbyname($url_host);
if (!$ip_addr || strpos($ip_addr, "127.") === 0) {
- $fetch_last_error = "URL hostname failed to resolve or resolved to a loopback address ($ip_addr)";
+ self::$fetch_last_error = "URL hostname failed to resolve or resolved to a loopback address ($ip_addr)";
return false;
}
if (function_exists('curl_init') && !ini_get("open_basedir")) {
- $fetch_curl_used = true;
+ self::$fetch_curl_used = true;
$ch = curl_init($url);
@@ -306,13 +306,13 @@ class UrlHelper {
list ($key, $value) = explode(": ", $header);
if (strtolower($key) == "last-modified") {
- $fetch_last_modified = $value;
+ self::$fetch_last_modified = $value;
}
}
if (substr(strtolower($header), 0, 7) == 'http/1.') {
- $fetch_last_error_code = (int) substr($header, 9, 3);
- $fetch_last_error = $header;
+ self::$fetch_last_error_code = (int) substr($header, 9, 3);
+ self::$fetch_last_error = $header;
}
}
@@ -322,39 +322,39 @@ class UrlHelper {
}
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
- $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
+ self::$fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
- $fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
+ self::$fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
- if (!self::validate($fetch_effective_url, true)) {
- $fetch_last_error = "URL received after redirection failed extended validation.";
+ if (!self::validate(self::$fetch_effective_url, true)) {
+ self::$fetch_last_error = "URL received after redirection failed extended validation.";
return false;
}
- $fetch_effective_ip_addr = gethostbyname(parse_url($fetch_effective_url, PHP_URL_HOST));
+ self::$fetch_effective_ip_addr = gethostbyname(parse_url(self::$fetch_effective_url, PHP_URL_HOST));
- if (!$fetch_effective_ip_addr || strpos($fetch_effective_ip_addr, "127.") === 0) {
- $fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address ($fetch_effective_ip_addr)";
+ if (!self::$fetch_effective_ip_addr || strpos(self::$fetch_effective_ip_addr, "127.") === 0) {
+ self::$fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address (".self::$fetch_effective_ip_addr.")";
return false;
}
- $fetch_last_error_code = $http_code;
+ self::$fetch_last_error_code = $http_code;
- if ($http_code != 200 || $type && strpos($fetch_last_content_type, "$type") === false) {
+ if ($http_code != 200 || $type && strpos(self::$fetch_last_content_type, "$type") === false) {
if (curl_errno($ch) != 0) {
- $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
+ self::$fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
}
- $fetch_last_error_content = $contents;
+ self::$fetch_last_error_content = $contents;
curl_close($ch);
return false;
}
if (!$contents) {
- $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
+ self::$fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
curl_close($ch);
return false;
}
@@ -372,7 +372,7 @@ class UrlHelper {
return $contents;
} else {
- $fetch_curl_used = false;
+ self::$fetch_curl_used = false;
if ($login && $pass){
$url_parts = array();
@@ -417,18 +417,18 @@ class UrlHelper {
$old_error = error_get_last();
- $fetch_effective_url = self::resolve_redirects($url, $timeout ? $timeout : Config::get(Config::FILE_FETCH_CONNECT_TIMEOUT));
+ self::$fetch_effective_url = self::resolve_redirects($url, $timeout ? $timeout : Config::get(Config::FILE_FETCH_CONNECT_TIMEOUT));
- if (!self::validate($fetch_effective_url, true)) {
- $fetch_last_error = "URL received after redirection failed extended validation.";
+ if (!self::validate(self::$fetch_effective_url, true)) {
+ self::$fetch_last_error = "URL received after redirection failed extended validation.";
return false;
}
- $fetch_effective_ip_addr = gethostbyname(parse_url($fetch_effective_url, PHP_URL_HOST));
+ self::$fetch_effective_ip_addr = gethostbyname(parse_url(self::$fetch_effective_url, PHP_URL_HOST));
- if (!$fetch_effective_ip_addr || strpos($fetch_effective_ip_addr, "127.") === 0) {
- $fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address ($fetch_effective_ip_addr)";
+ if (!self::$fetch_effective_ip_addr || strpos(self::$fetch_effective_ip_addr, "127.") === 0) {
+ self::$fetch_last_error = "URL hostname received after redirection failed to resolve or resolved to a loopback address (".self::$fetch_effective_ip_addr.")";
return false;
}
@@ -442,30 +442,30 @@ class UrlHelper {
$key = strtolower($key);
if ($key == 'content-type') {
- $fetch_last_content_type = $value;
+ self::$fetch_last_content_type = $value;
// don't abort here b/c there might be more than one
// e.g. if we were being redirected -- last one is the right one
} else if ($key == 'last-modified') {
- $fetch_last_modified = $value;
+ self::$fetch_last_modified = $value;
} else if ($key == 'location') {
- $fetch_effective_url = $value;
+ self::$fetch_effective_url = $value;
}
}
if (substr(strtolower($header), 0, 7) == 'http/1.') {
- $fetch_last_error_code = (int) substr($header, 9, 3);
- $fetch_last_error = $header;
+ self::$fetch_last_error_code = (int) substr($header, 9, 3);
+ self::$fetch_last_error = $header;
}
}
- if ($fetch_last_error_code != 200) {
+ if (self::$fetch_last_error_code != 200) {
$error = error_get_last();
- if ($error['message'] != $old_error['message']) {
- $fetch_last_error .= "; " . $error["message"];
+ if (($error['message'] ?? '') != ($old_error['message'] ?? '')) {
+ self::$fetch_last_error .= "; " . $error["message"];
}
- $fetch_last_error_content = $data;
+ self::$fetch_last_error_content = $data;
return false;
}
diff --git a/classes/userhelper.php b/classes/userhelper.php
index ca673cf58..ce26e6c71 100644
--- a/classes/userhelper.php
+++ b/classes/userhelper.php
@@ -1,6 +1,22 @@
<?php
+use OTPHP\TOTP;
+
class UserHelper {
+ const HASH_ALGO_SSHA512 = 'SSHA-512';
+ const HASH_ALGO_SSHA256 = 'SSHA-256';
+ const HASH_ALGO_MODE2 = 'MODE2';
+ const HASH_ALGO_SHA1X = 'SHA1X';
+ const HASH_ALGO_SHA1 = 'SHA1';
+
+ const HASH_ALGOS = [
+ self::HASH_ALGO_SSHA512,
+ self::HASH_ALGO_SSHA256,
+ self::HASH_ALGO_MODE2,
+ self::HASH_ALGO_SHA1X,
+ self::HASH_ALGO_SHA1
+ ];
+
static function authenticate(string $login = null, string $password = null, bool $check_only = false, string $service = null) {
if (!Config::get(Config::SINGLE_USER_MODE)) {
$user_id = false;
@@ -23,27 +39,24 @@ class UserHelper {
session_regenerate_id(true);
- $_SESSION["uid"] = $user_id;
- $_SESSION["auth_module"] = $auth_module;
+ $user = ORM::for_table('ttrss_users')->find_one($user_id);
- $pdo = Db::pdo();
- $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
- WHERE id = ?");
- $sth->execute([$user_id]);
- $row = $sth->fetch();
-
- $_SESSION["name"] = $row["login"];
- $_SESSION["access_level"] = $row["access_level"];
- $_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
+ if ($user) {
+ $_SESSION["uid"] = $user_id;
+ $_SESSION["auth_module"] = $auth_module;
+ $_SESSION["name"] = $user->login;
+ $_SESSION["access_level"] = $user->access_level;
+ $_SESSION["csrf_token"] = bin2hex(get_random_bytes(16));
+ $_SESSION["ip_address"] = UserHelper::get_user_ip();
+ $_SESSION["pwd_hash"] = $user->pwd_hash;
- $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
- $usth->execute([$user_id]);
+ $user->last_login = Db::NOW();
+ $user->save();
- $_SESSION["ip_address"] = UserHelper::get_user_ip();
- $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
- $_SESSION["pwd_hash"] = $row["pwd_hash"];
+ return true;
+ }
- return true;
+ return false;
}
if ($login && $password && !$user_id && !$check_only)
@@ -75,7 +88,7 @@ class UserHelper {
if (!$pluginhost) $pluginhost = PluginHost::getInstance();
- if ($owner_uid && SCHEMA_VERSION >= 100 && empty($_SESSION["safe_mode"])) {
+ if ($owner_uid && Config::get_schema_version() >= 100 && empty($_SESSION["safe_mode"])) {
$plugins = get_pref(Prefs::_ENABLED_PLUGINS, $owner_uid);
$pluginhost->load((string)$plugins, PluginHost::KIND_USER, $owner_uid);
@@ -117,8 +130,9 @@ class UserHelper {
} else {
/* bump login timestamp */
- $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
- $sth->execute([$_SESSION['uid']]);
+ $user = ORM::for_table('ttrss_users')->find_one($_SESSION["uid"]);
+ $user->last_login = Db::NOW();
+ $user->save();
$_SESSION["last_login_update"] = time();
}
@@ -146,20 +160,29 @@ class UserHelper {
if (isset($_SERVER[$hdr]))
return $_SERVER[$hdr];
}
- }
- static function find_user_by_login(string $login) {
- $pdo = Db::pdo();
+ return null;
+ }
- $sth = $pdo->prepare("SELECT id FROM ttrss_users WHERE
- LOWER(login) = LOWER(?)");
- $sth->execute([$login]);
+ static function get_login_by_id(int $id) {
+ $user = ORM::for_table('ttrss_users')
+ ->find_one($id);
- if ($row = $sth->fetch()) {
- return $row["id"];
- }
+ if ($user)
+ return $user->login;
+ else
+ return null;
+ }
- return false;
+ static function find_user_by_login(string $login) {
+ $user = ORM::for_table('ttrss_users')
+ ->where('login', $login)
+ ->find_one();
+
+ if ($user)
+ return $user->id;
+ else
+ return null;
}
static function logout() {
@@ -173,34 +196,167 @@ class UserHelper {
session_commit();
}
- static function reset_password($uid, $format_output = false) {
+ static function get_salt() {
+ return substr(bin2hex(get_random_bytes(125)), 0, 250);
+ }
- $pdo = Db::pdo();
+ static function reset_password($uid, $format_output = false, $new_password = "") {
- $sth = $pdo->prepare("SELECT login FROM ttrss_users WHERE id = ?");
- $sth->execute([$uid]);
+ $user = ORM::for_table('ttrss_users')->find_one($uid);
+ $message = "";
- if ($row = $sth->fetch()) {
+ if ($user) {
- $login = $row["login"];
+ $login = $user->login;
- $new_salt = substr(bin2hex(get_random_bytes(125)), 0, 250);
- $tmp_user_pwd = make_password();
+ $new_salt = self::get_salt();
+ $tmp_user_pwd = $new_password ? $new_password : make_password();
- $pwd_hash = encrypt_password($tmp_user_pwd, $new_salt, true);
+ $pwd_hash = self::hash_password($tmp_user_pwd, $new_salt, self::HASH_ALGOS[0]);
- $sth = $pdo->prepare("UPDATE ttrss_users
- SET pwd_hash = ?, salt = ?, otp_enabled = false
- WHERE id = ?");
- $sth->execute([$pwd_hash, $new_salt, $uid]);
+ $user->pwd_hash = $pwd_hash;
+ $user->salt = $new_salt;
+ $user->save();
$message = T_sprintf("Changed password of user %s to %s", "<strong>$login</strong>", "<strong>$tmp_user_pwd</strong>");
+ } else {
+ $message = __("User not found");
+ }
- if ($format_output)
- print_notice($message);
- else
- print $message;
+ if ($format_output)
+ print_notice($message);
+ else
+ print $message;
+ }
+ static function check_otp(int $owner_uid, int $otp_check) : bool {
+ $otp = TOTP::create(self::get_otp_secret($owner_uid, true));
+
+ return $otp->now() == $otp_check;
+ }
+
+ static function disable_otp(int $owner_uid) : bool {
+ $user = ORM::for_table('ttrss_users')->find_one($owner_uid);
+
+ if ($user) {
+ $user->otp_enabled = false;
+
+ // force new OTP secret when next enabled
+ if (Config::get_schema_version() >= 143) {
+ $user->otp_secret = null;
+ }
+
+ $user->save();
+
+ return true;
+ } else {
+ return false;
}
}
+
+ static function enable_otp(int $owner_uid, int $otp_check) : bool {
+ $secret = self::get_otp_secret($owner_uid);
+
+ if ($secret) {
+ $otp = TOTP::create($secret);
+ $user = ORM::for_table('ttrss_users')->find_one($owner_uid);
+
+ if ($otp->now() == $otp_check && $user) {
+
+ $user->otp_enabled = true;
+ $user->save();
+
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ static function is_otp_enabled(int $owner_uid) : bool {
+ $user = ORM::for_table('ttrss_users')->find_one($owner_uid);
+
+ if ($user) {
+ return $user->otp_enabled;
+ } else {
+ return false;
+ }
+ }
+
+ static function get_otp_secret(int $owner_uid, bool $show_if_enabled = false) {
+ $user = ORM::for_table('ttrss_users')->find_one($owner_uid);
+
+ if ($user) {
+
+ $salt_based_secret = mb_substr(sha1($user->salt), 0, 12);
+
+ if (Config::get_schema_version() >= 143) {
+ $secret = $user->otp_secret;
+
+ if (empty($secret)) {
+
+ /* migrate secret if OTP is already enabled, otherwise make a new one */
+ if ($user->otp_enabled) {
+ $user->otp_secret = $salt_based_secret;
+ } else {
+ $user->otp_secret = bin2hex(get_random_bytes(6));
+ }
+
+ $user->save();
+
+ $secret = $user->otp_secret;
+ }
+ } else {
+ $secret = $salt_based_secret;
+ }
+
+ if (!$user->otp_enabled || $show_if_enabled) {
+ return \ParagonIE\ConstantTime\Base32::encodeUpperUnpadded($secret);
+ }
+ }
+
+ return null;
+ }
+
+ static function is_default_password() {
+ $authenticator = PluginHost::getInstance()->get_plugin($_SESSION["auth_module"]);
+
+ if ($authenticator &&
+ method_exists($authenticator, "check_password") &&
+ $authenticator->check_password($_SESSION["uid"], "password")) {
+
+ return true;
+ }
+ return false;
+ }
+
+ static function hash_password(string $pass, string $salt, string $algo = "") {
+
+ if (!$algo) $algo = self::HASH_ALGOS[0];
+
+ $pass_hash = "";
+
+ switch ($algo) {
+ case self::HASH_ALGO_SHA1:
+ $pass_hash = sha1($pass);
+ break;
+ case self::HASH_ALGO_SHA1X:
+ $pass_hash = sha1("$salt:$pass");
+ break;
+ case self::HASH_ALGO_MODE2:
+ case self::HASH_ALGO_SSHA256:
+ $pass_hash = hash('sha256', $salt . $pass);
+ break;
+ case self::HASH_ALGO_SSHA512:
+ $pass_hash = hash('sha512', $salt . $pass);
+ break;
+ default:
+ user_error("hash_password: unknown hash algo: $algo", E_USER_ERROR);
+ }
+
+ if ($pass_hash)
+ return "$algo:$pass_hash";
+ else
+ return false;
+ }
}