summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--classes/config.php4
-rwxr-xr-xclasses/db.php8
-rw-r--r--classes/debug.php20
-rwxr-xr-xclasses/feeds.php2
-rwxr-xr-xclasses/handler/public.php14
-rw-r--r--classes/mailer.php9
-rwxr-xr-xclasses/pref/feeds.php10
-rwxr-xr-xclasses/pref/filters.php8
-rw-r--r--classes/prefs.php6
-rwxr-xr-xclasses/rssutils.php20
-rw-r--r--classes/userhelper.php160
-rw-r--r--js/PrefFeedTree.js13
-rw-r--r--js/PrefFilterTree.js11
-rw-r--r--phpstan.neon2
-rw-r--r--plugins/auth_internal/init.php12
-rw-r--r--themes/compact.css3
-rw-r--r--themes/compact_night.css3
-rw-r--r--themes/light-high-contrast.css3
-rw-r--r--themes/light.css3
-rw-r--r--themes/light/cdm.less1
-rw-r--r--themes/light/tt-rss.less2
-rw-r--r--themes/night.css3
-rw-r--r--themes/night_blue.css3
-rwxr-xr-xupdate.php172
-rwxr-xr-xupdate_daemon2.php4
25 files changed, 416 insertions, 80 deletions
diff --git a/classes/config.php b/classes/config.php
index cc089b7ba..74546e3f2 100644
--- a/classes/config.php
+++ b/classes/config.php
@@ -189,6 +189,9 @@ class Config {
/** http user agent (changing this is not recommended) */
const HTTP_USER_AGENT = "HTTP_USER_AGENT";
+ /** delay updates for this feed if received HTTP 429 (Too Many Requests) for this amount of seconds (base value, actual delay is base...base*2) */
+ const HTTP_429_THROTTLE_INTERVAL = "HTTP_429_THROTTLE_INTERVAL";
+
/** default values for all global configuration options */
private const _DEFAULTS = [
Config::DB_TYPE => [ "pgsql", Config::T_STRING ],
@@ -245,6 +248,7 @@ class Config {
Config::AUTH_MIN_INTERVAL => [ 5, Config::T_INT ],
Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://tt-rss.org/)',
Config::T_STRING ],
+ Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ],
];
/** @var Config|null */
diff --git a/classes/db.php b/classes/db.php
index 2cc89f5ba..4331b662e 100755
--- a/classes/db.php
+++ b/classes/db.php
@@ -17,8 +17,12 @@ class Db
}
}
- static function NOW(): string {
- return date("Y-m-d H:i:s", time());
+ /**
+ * @param int $delta adjust generated timestamp by this value in seconds (either positive or negative)
+ * @return string
+ */
+ static function NOW(int $delta = 0): string {
+ return date("Y-m-d H:i:s", time() + $delta);
}
private function __clone() {
diff --git a/classes/debug.php b/classes/debug.php
index fbdf260e0..4777e8c74 100644
--- a/classes/debug.php
+++ b/classes/debug.php
@@ -68,9 +68,9 @@ class Debug {
}
/**
- * @param int $level Debug::LOG_*
+ * @param Debug::LOG_* $level
*/
- public static function set_loglevel(int $level): void {
+ public static function set_loglevel($level): void {
self::$loglevel = $level;
}
@@ -82,7 +82,21 @@ class Debug {
}
/**
- * @param int $level Debug::LOG_*
+ * @param int $level integer loglevel value
+ * @return Debug::LOG_* if valid, warn and return LOG_DISABLED otherwise
+ */
+ public static function map_loglevel(int $level) : int {
+ if (in_array($level, self::ALL_LOG_LEVELS)) {
+ /** @phpstan-ignore-next-line */
+ return $level;
+ } else {
+ user_error("Passed invalid debug log level: $level", E_USER_WARNING);
+ return self::LOG_DISABLED;
+ }
+ }
+
+ /**
+ * @param Debug::LOG_* $level log level
*/
public static function log(string $message, int $level = Debug::LOG_NORMAL): bool {
diff --git a/classes/feeds.php b/classes/feeds.php
index a06486883..197caeedc 100755
--- a/classes/feeds.php
+++ b/classes/feeds.php
@@ -665,7 +665,7 @@ class Feeds extends Handler_Protected {
}
Debug::set_enabled(true);
- Debug::set_loglevel($xdebug);
+ Debug::set_loglevel(Debug::map_loglevel($xdebug));
$feed_id = (int)$_REQUEST["feed_id"];
$do_update = ($_REQUEST["action"] ?? "") == "do_update";
diff --git a/classes/handler/public.php b/classes/handler/public.php
index 3fef4c2b9..499cf8db2 100755
--- a/classes/handler/public.php
+++ b/classes/handler/public.php
@@ -76,7 +76,7 @@ class Handler_Public extends Handler {
"/public.php?op=rss&id=$feed&key=" .
Feeds::_get_access_key($feed, false, $owner_uid);
- if (!$feed_site_url) $feed_site_url = get_self_url_prefix();
+ if (!$feed_site_url) $feed_site_url = Config::get_self_url();
if ($format == 'atom') {
$tpl = new Templator();
@@ -87,7 +87,7 @@ class Handler_Public extends Handler {
$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);
+ $tpl->setVariable('SELF_URL', htmlspecialchars(Config::get_self_url()), true);
while ($line = $result->fetch()) {
$line["content_preview"] = Sanitizer::sanitize(truncate_string(strip_tags($line["content"]), 100, '...'));
@@ -134,7 +134,7 @@ class Handler_Public extends Handler {
$tpl->setVariable('ARTICLE_AUTHOR', htmlspecialchars($line['author']), true);
- $tpl->setVariable('ARTICLE_SOURCE_LINK', htmlspecialchars($line['site_url'] ? $line["site_url"] : get_self_url_prefix()), true);
+ $tpl->setVariable('ARTICLE_SOURCE_LINK', htmlspecialchars($line['site_url'] ? $line["site_url"] : Config::get_self_url()), true);
$tpl->setVariable('ARTICLE_SOURCE_TITLE', htmlspecialchars($line['feed_title'] ?? $feed_title), true);
foreach ($line["tags"] as $tag) {
@@ -312,7 +312,7 @@ class Handler_Public extends Handler {
$login, $user_id);
if (!$redirect_url)
- $redirect_url = get_self_url_prefix() . "/index.php";
+ $redirect_url = Config::get_self_url() . "/index.php";
header("Location: " . $redirect_url);
} else {
@@ -389,11 +389,11 @@ class Handler_Public extends Handler {
if (UserHelper::authenticate($login, $password)) {
$_POST["password"] = "";
- if (get_schema_version() >= 120) {
+ if (Config::get_schema_version() >= 120) {
$_SESSION["language"] = get_pref(Prefs::USER_LANGUAGE, $_SESSION["uid"]);
}
- $_SESSION["ref_schema_version"] = get_schema_version();
+ $_SESSION["ref_schema_version"] = Config::get_schema_version();
$_SESSION["bw_limit"] = !!clean($_POST["bw_limit"] ?? false);
$_SESSION["safe_mode"] = $safe_mode;
@@ -563,7 +563,7 @@ class Handler_Public extends Handler {
print_notice("Password reset instructions are being sent to your email address.");
$resetpass_token = sha1(get_random_bytes(128));
- $resetpass_link = get_self_url_prefix() . "/public.php?op=forgotpass&hash=" . $resetpass_token .
+ $resetpass_link = Config::get_self_url() . "/public.php?op=forgotpass&hash=" . $resetpass_token .
"&login=" . urlencode($login);
$tpl = new Templator();
diff --git a/classes/mailer.php b/classes/mailer.php
index a15c8546b..60b1ce4fd 100644
--- a/classes/mailer.php
+++ b/classes/mailer.php
@@ -45,14 +45,7 @@ class Mailer {
++$hooks_tried;
}
- $headers = [ "From: $from_combined" ];
-
- if ($message_html) {
- $headers[] = "MIME-Version: 1.0";
- $headers[] = "Content-Type: text/html; charset=UTF-8";
- } else {
- $headers[] = "Content-Type: text/plain; charset=UTF-8";
- }
+ $headers = [ "From: $from_combined", "Content-Type: text/plain; charset=UTF-8" ];
$rc = mail($to_combined, $subject, $message, implode("\r\n", array_merge($headers, $additional_headers)));
diff --git a/classes/pref/feeds.php b/classes/pref/feeds.php
index 03b70580b..bb638f166 100755
--- a/classes/pref/feeds.php
+++ b/classes/pref/feeds.php
@@ -973,16 +973,6 @@ class Pref_Feeds extends Handler_Protected {
persist="true"
model="feedModel"
openOnClick="false">
- <script type="dojo/method" event="onClick" args="item">
- var id = String(item.id);
- var bare_id = id.substr(id.indexOf(':')+1);
-
- if (id.match('FEED:')) {
- CommonDialogs.editFeed(bare_id);
- } else if (id.match('CAT:')) {
- dijit.byId('feedTree').editCategory(bare_id, item);
- }
- </script>
</div>
</div>
</div>
diff --git a/classes/pref/filters.php b/classes/pref/filters.php
index 79dd78993..8f1c578b6 100755
--- a/classes/pref/filters.php
+++ b/classes/pref/filters.php
@@ -703,14 +703,6 @@ class Pref_Filters extends Handler_Protected {
</div>
<div dojoType="fox.PrefFilterTree" id="filterTree" dndController="dijit.tree.dndSource"
betweenThreshold="5" model="filterModel" openOnClick="true">
- <script type="dojo/method" event="onClick" args="item">
- var id = String(item.id);
- var bare_id = id.substr(id.indexOf(':')+1);
-
- if (id.match('FILTER:')) {
- Filters.edit(bare_id);
- }
- </script>
</div>
</div>
<?php PluginHost::getInstance()->run_hooks(PluginHost::HOOK_PREFS_TAB, "prefFilters") ?>
diff --git a/classes/prefs.php b/classes/prefs.php
index 7e6033f4d..378fea293 100644
--- a/classes/prefs.php
+++ b/classes/prefs.php
@@ -230,7 +230,7 @@ class Prefs {
}
}
- if (get_schema_version() >= 141) {
+ if (Config::get_schema_version() >= 141) {
// fill in any overrides from the database
$sth = $this->pdo->prepare("SELECT pref_name, value FROM ttrss_user_prefs2
WHERE owner_uid = :uid AND
@@ -265,7 +265,7 @@ class Prefs {
if ($this->_is_cached($pref_name, $owner_uid, $profile_id)) {
$cached_value = $this->_get_cache($pref_name, $owner_uid, $profile_id);
return Config::cast_to($cached_value, $type_hint);
- } else if (get_schema_version() >= 141) {
+ } else if (Config::get_schema_version() >= 141) {
$sth = $this->pdo->prepare("SELECT value FROM ttrss_user_prefs2
WHERE pref_name = :name AND owner_uid = :uid AND
(profile = :profile OR (:profile IS NULL AND profile IS NULL))");
@@ -390,7 +390,7 @@ class Prefs {
}
function migrate(int $owner_uid, ?int $profile_id): void {
- if (get_schema_version() < 141)
+ if (Config::get_schema_version() < 141)
return;
if (!$profile_id) $profile_id = null;
diff --git a/classes/rssutils.php b/classes/rssutils.php
index aec17d538..1d87e73d6 100755
--- a/classes/rssutils.php
+++ b/classes/rssutils.php
@@ -492,7 +492,7 @@ class RSSUtils {
// If-Modified-Since
if (UrlHelper::$fetch_last_error_code == 304) {
- Debug::log("source claims data not modified, nothing to do.", Debug::LOG_VERBOSE);
+ Debug::log("source claims data not modified (304), nothing to do.", Debug::LOG_VERBOSE);
$error_message = "";
$feed_obj->set([
@@ -503,6 +503,24 @@ class RSSUtils {
$feed_obj->save();
+ } else if (UrlHelper::$fetch_last_error_code == 429) {
+
+ // randomize interval using Config::HTTP_429_THROTTLE_INTERVAL as a base value (1-2x)
+ $http_429_throttle_interval = rand(Config::get(Config::HTTP_429_THROTTLE_INTERVAL),
+ Config::get(Config::HTTP_429_THROTTLE_INTERVAL)*2);
+
+ $error_message = UrlHelper::$fetch_last_error;
+
+ Debug::log("source claims we're requesting too often (429), throttling updates for $http_429_throttle_interval seconds.",
+ Debug::LOG_VERBOSE);
+
+ $feed_obj->set([
+ 'last_error' => $error_message . " (updates throttled for $http_429_throttle_interval seconds.)",
+ 'last_successful_update' => Db::NOW($http_429_throttle_interval),
+ 'last_updated' => Db::NOW($http_429_throttle_interval),
+ ]);
+
+ $feed_obj->save();
} else {
$error_message = UrlHelper::$fetch_last_error;
diff --git a/classes/userhelper.php b/classes/userhelper.php
index 91e40665d..228bb14fb 100644
--- a/classes/userhelper.php
+++ b/classes/userhelper.php
@@ -17,6 +17,15 @@ class UserHelper {
self::HASH_ALGO_SHA1
];
+ const ACCESS_LEVELS = [
+ self::ACCESS_LEVEL_DISABLED,
+ self::ACCESS_LEVEL_READONLY,
+ self::ACCESS_LEVEL_USER,
+ self::ACCESS_LEVEL_POWERUSER,
+ self::ACCESS_LEVEL_ADMIN,
+ self::ACCESS_LEVEL_KEEP_CURRENT
+ ];
+
/** forbidden to login */
const ACCESS_LEVEL_DISABLED = -2;
@@ -32,6 +41,23 @@ class UserHelper {
/** has administrator permissions */
const ACCESS_LEVEL_ADMIN = 10;
+ /** used by self::user_modify() to keep current access level */
+ const ACCESS_LEVEL_KEEP_CURRENT = -1024;
+
+ /**
+ * @param int $level integer loglevel value
+ * @return UserHelper::ACCESS_LEVEL_* if valid, warn and return ACCESS_LEVEL_KEEP_CURRENT otherwise
+ */
+ public static function map_access_level(int $level) : int {
+ if (in_array($level, self::ACCESS_LEVELS)) {
+ /** @phpstan-ignore-next-line */
+ return $level;
+ } else {
+ user_error("Passed invalid user access level: $level", E_USER_WARNING);
+ return self::ACCESS_LEVEL_KEEP_CURRENT;
+ }
+ }
+
static function authenticate(string $login = null, string $password = null, bool $check_only = false, string $service = null): bool {
if (!Config::get(Config::SINGLE_USER_MODE)) {
$user_id = false;
@@ -133,7 +159,7 @@ class UserHelper {
if (empty($_SESSION["uid"])) {
if (Config::get(Config::AUTH_AUTO_LOGIN) && self::authenticate(null, null)) {
- $_SESSION["ref_schema_version"] = get_schema_version();
+ $_SESSION["ref_schema_version"] = Config::get_schema_version();
} else {
self::authenticate(null, null, true);
}
@@ -217,6 +243,7 @@ class UserHelper {
return substr(bin2hex(get_random_bytes(125)), 0, 250);
}
+ /** TODO: this should invoke UserHelper::user_modify() */
static function reset_password(int $uid, bool $format_output = false, string $new_password = ""): void {
$user = ORM::for_table('ttrss_users')->find_one($uid);
@@ -335,18 +362,14 @@ class UserHelper {
return null;
}
- static function is_default_password(): bool {
-
- /** @var Auth_Internal|false $authenticator -- this is only here to make check_password() visible to static analyzer */
- $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;
+ /**
+ * @param null|int $owner_uid if null, checks current user via session-specific auth module, if set works on internal database only
+ * @return bool
+ * @throws PDOException
+ * @throws Exception
+ */
+ static function is_default_password(?int $owner_uid = null): bool {
+ return self::user_has_password($owner_uid, 'password');
}
/**
@@ -380,4 +403,115 @@ class UserHelper {
else
return false;
}
+
+ /**
+ * @param string $login Login for new user (case-insensitive)
+ * @param string $password Password for new user (may not be blank)
+ * @param UserHelper::ACCESS_LEVEL_* $access_level Access level for new user
+ * @return bool true if user has been created
+ */
+ static function user_add(string $login, string $password, int $access_level) : bool {
+ $login = clean($login);
+
+ if ($login &&
+ $password &&
+ !self::find_user_by_login($login) &&
+ self::map_access_level((int)$access_level) != self::ACCESS_LEVEL_KEEP_CURRENT) {
+
+ $user = ORM::for_table('ttrss_users')->create();
+
+ $user->salt = self::get_salt();
+ $user->login = mb_strtolower($login);
+ $user->pwd_hash = self::hash_password($password, $user->salt);
+ $user->access_level = $access_level;
+ $user->created = Db::NOW();
+
+ return $user->save();
+ }
+
+ return false;
+ }
+
+ /**
+ * @param int $uid User ID to modify
+ * @param string $new_password set password to this value if its not blank
+ * @param UserHelper::ACCESS_LEVEL_* $access_level set user access level to this value if it is set (default ACCESS_LEVEL_KEEP_CURRENT)
+ * @return bool true if user record has been saved
+ *
+ * NOTE: $access_level is of mixed type because of intellephense
+ */
+ static function user_modify(int $uid, string $new_password = '', $access_level = self::ACCESS_LEVEL_KEEP_CURRENT) : bool {
+ $user = ORM::for_table('ttrss_users')->find_one($uid);
+
+ if ($user) {
+ if ($new_password != '') {
+ $new_salt = self::get_salt();
+ $pwd_hash = self::hash_password($new_password, $new_salt, self::HASH_ALGOS[0]);
+
+ $user->pwd_hash = $pwd_hash;
+ $user->salt = $new_salt;
+ }
+
+ if ($access_level != self::ACCESS_LEVEL_KEEP_CURRENT) {
+ $user->access_level = (int)$access_level;
+ }
+
+ return $user->save();
+ }
+
+ return false;
+ }
+
+ /**
+ * @param int $uid user ID to delete (this won't delete built-in admin user with UID 1)
+ * @return bool true if user has been deleted
+ */
+ static function user_delete(int $uid) : bool {
+ if ($uid != 1) {
+
+ $user = ORM::for_table('ttrss_users')->find_one($uid);
+
+ if ($user) {
+ // TODO: is it still necessary to split those queries?
+
+ ORM::for_table('ttrss_tags')
+ ->where('owner_uid', $uid)
+ ->delete_many();
+
+ ORM::for_table('ttrss_feeds')
+ ->where('owner_uid', $uid)
+ ->delete_many();
+
+ return $user->delete();
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param null|int $owner_uid if null, checks current user via session-specific auth module, if set works on internal database only
+ * @param string $password password to compare hash against
+ * @return bool
+ */
+ static function user_has_password(?int $owner_uid, string $password) : bool {
+ if ($owner_uid) {
+ $authenticator = new Auth_Internal();
+
+ return $authenticator->check_password($owner_uid, $password);
+ } else {
+ /** @var Auth_Internal|false $authenticator -- this is only here to make check_password() visible to static analyzer */
+ $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;
+ }
+
}
diff --git a/js/PrefFeedTree.js b/js/PrefFeedTree.js
index 0585173c9..85b262b6d 100644
--- a/js/PrefFeedTree.js
+++ b/js/PrefFeedTree.js
@@ -1,5 +1,5 @@
/* eslint-disable prefer-rest-params */
-/* global __, lib, dijit, define, dojo, CommonDialogs, Notify, Tables, xhrPost, xhr, fox, App */
+/* global __, lib, dijit, define, dojo, CommonDialogs, Notify, Tables, xhr, fox, App */
define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_base/array", "dojo/cookie"],
function (declare, domConstruct, checkBoxTree, array, cookie) {
@@ -14,6 +14,17 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
this.checkInactiveFeeds();
this.checkErrorFeeds();
});
+
+ dojo.connect(this, 'onClick', (item) => {
+ const id = String(item.id);
+ const bare_id = id.substr(id.indexOf(':')+1);
+
+ if (id.match('FEED:')) {
+ CommonDialogs.editFeed(bare_id);
+ } else if (id.match('CAT:')) {
+ dijit.byId('feedTree').editCategory(bare_id, item);
+ }
+ });
},
// save state in localStorage instead of cookies
// reference: https://stackoverflow.com/a/27968996
diff --git a/js/PrefFilterTree.js b/js/PrefFilterTree.js
index d4496b647..149261abd 100644
--- a/js/PrefFilterTree.js
+++ b/js/PrefFilterTree.js
@@ -1,5 +1,5 @@
/* eslint-disable prefer-rest-params */
-/* global __, define, lib, dijit, dojo, xhr, App, Notify */
+/* global __, define, lib, dijit, dojo, xhr, App, Notify, Filters */
define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], function (declare, domConstruct) {
@@ -10,6 +10,15 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
dijit.byId('filterTree').hideOrShowFilterRules(
parseInt(localStorage.getItem("ttrss:hide-filter-rules"))
);
+
+ dojo.connect(this, 'onClick', (item) => {
+ const id = String(item.id);
+ const bare_id = id.substr(id.indexOf(':')+1);
+
+ if (id.match('FILTER:')) {
+ Filters.edit(bare_id);
+ }
+ });
},
_createTreeNode: function(args) {
const tnode = this.inherited(arguments);
diff --git a/phpstan.neon b/phpstan.neon
index 66c6caa50..bca85c19b 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,5 +1,7 @@
parameters:
level: 6
+ parallel:
+ maximumNumberOfProcesses: 4
reportUnmatchedIgnoredErrors: false
ignoreErrors:
- '#Constant.*\b(SUBSTRING_FOR_DATE|SCHEMA_VERSION|SELF_USER_AGENT|LABEL_BASE_INDEX|PLUGIN_FEED_BASE_INDEX)\b.*not found#'
diff --git a/plugins/auth_internal/init.php b/plugins/auth_internal/init.php
index 688a0f5d8..882b5506a 100644
--- a/plugins/auth_internal/init.php
+++ b/plugins/auth_internal/init.php
@@ -18,14 +18,14 @@ class Auth_Internal extends Auth_Base {
$otp = (int) ($_REQUEST["otp"] ?? 0);
// don't bother with null/null logins for auth_external etc
- if ($login && get_schema_version() > 96) {
+ if ($login && Config::get_schema_version() > 96) {
$user_id = UserHelper::find_user_by_login($login);
if ($user_id && UserHelper::is_otp_enabled($user_id)) {
// only allow app passwords for service logins if OTP is enabled
- if ($service && get_schema_version() > 138) {
+ if ($service && Config::get_schema_version() > 138) {
return $this->check_app_password($login, $password, $service);
}
@@ -106,7 +106,7 @@ class Auth_Internal extends Auth_Base {
// service logins: check app passwords first but allow regular password
// as a fallback if OTP is not enabled
- if ($service && get_schema_version() > 138) {
+ if ($service && Config::get_schema_version() > 138) {
$user_id = $this->check_app_password($login, $password, $service);
if ($user_id)
@@ -119,7 +119,7 @@ class Auth_Internal extends Auth_Base {
->find_one();
if ($user) {
- if (get_schema_version() >= 145) {
+ if (Config::get_schema_version() >= 145) {
if ($user->last_auth_attempt) {
$last_auth_attempt = strtotime($user->last_auth_attempt);
@@ -145,7 +145,7 @@ class Auth_Internal extends Auth_Base {
if ($auth_result) {
return $auth_result;
} else {
- if (get_schema_version() >= 145) {
+ if (Config::get_schema_version() >= 145) {
$user->last_auth_attempt = Db::NOW();
$user->save();
}
@@ -176,7 +176,7 @@ class Auth_Internal extends Auth_Base {
list ($pwd_algo, $raw_hash) = explode(":", $pwd_hash, 2);
// check app password only if service is specified
- if ($service && get_schema_version() > 138) {
+ if ($service && Config::get_schema_version() > 138) {
return $this->check_app_password($login, $password, $service);
}
diff --git a/themes/compact.css b/themes/compact.css
index 76a532b15..c58d4c419 100644
--- a/themes/compact.css
+++ b/themes/compact.css
@@ -57,6 +57,7 @@ body.ttrss_main .post .header .comments {
}
body.ttrss_main .post .header .date {
white-space: nowrap;
+ margin-left: 4px;
}
body.ttrss_main .post .header img,
body.ttrss_main .post .header i.material-icons {
@@ -70,6 +71,7 @@ body.ttrss_main .post .header .title {
font-weight: 600;
text-rendering: optimizelegibility;
font-family: system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
+ word-break: break-all;
}
body.ttrss_main .post div.content {
padding: 10px;
@@ -1339,6 +1341,7 @@ body.ttrss_utility hr {
}
.cdm.expanded .titleWrap {
white-space: normal;
+ word-break: break-all;
}
.cdm.expanded .footer {
border: 0px solid #ddd;
diff --git a/themes/compact_night.css b/themes/compact_night.css
index 16097ec6e..761294d86 100644
--- a/themes/compact_night.css
+++ b/themes/compact_night.css
@@ -57,6 +57,7 @@ body.ttrss_main .post .header .comments {
}
body.ttrss_main .post .header .date {
white-space: nowrap;
+ margin-left: 4px;
}
body.ttrss_main .post .header img,
body.ttrss_main .post .header i.material-icons {
@@ -70,6 +71,7 @@ body.ttrss_main .post .header .title {
font-weight: 600;
text-rendering: optimizelegibility;
font-family: system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
+ word-break: break-all;
}
body.ttrss_main .post div.content {
padding: 10px;
@@ -1339,6 +1341,7 @@ body.ttrss_utility hr {
}
.cdm.expanded .titleWrap {
white-space: normal;
+ word-break: break-all;
}
.cdm.expanded .footer {
border: 0px solid #222;
diff --git a/themes/light-high-contrast.css b/themes/light-high-contrast.css
index 8f4729176..12fcb877d 100644
--- a/themes/light-high-contrast.css
+++ b/themes/light-high-contrast.css
@@ -57,6 +57,7 @@ body.ttrss_main .post .header .comments {
}
body.ttrss_main .post .header .date {
white-space: nowrap;
+ margin-left: 4px;
}
body.ttrss_main .post .header img,
body.ttrss_main .post .header i.material-icons {
@@ -70,6 +71,7 @@ body.ttrss_main .post .header .title {
font-weight: 600;
text-rendering: optimizelegibility;
font-family: system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
+ word-break: break-all;
}
body.ttrss_main .post div.content {
padding: 10px;
@@ -1339,6 +1341,7 @@ body.ttrss_utility hr {
}
.cdm.expanded .titleWrap {
white-space: normal;
+ word-break: break-all;
}
.cdm.expanded .footer {
border: 0px solid #ddd;
diff --git a/themes/light.css b/themes/light.css
index bfd221eef..82a84c604 100644
--- a/themes/light.css
+++ b/themes/light.css
@@ -57,6 +57,7 @@ body.ttrss_main .post .header .comments {
}
body.ttrss_main .post .header .date {
white-space: nowrap;
+ margin-left: 4px;
}
body.ttrss_main .post .header img,
body.ttrss_main .post .header i.material-icons {
@@ -70,6 +71,7 @@ body.ttrss_main .post .header .title {
font-weight: 600;
text-rendering: optimizelegibility;
font-family: system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
+ word-break: break-all;
}
body.ttrss_main .post div.content {
padding: 10px;
@@ -1339,6 +1341,7 @@ body.ttrss_utility hr {
}
.cdm.expanded .titleWrap {
white-space: normal;
+ word-break: break-all;
}
.cdm.expanded .footer {
border: 0px solid #ddd;
diff --git a/themes/light/cdm.less b/themes/light/cdm.less
index 6bb3378c1..05ba3c99a 100644
--- a/themes/light/cdm.less
+++ b/themes/light/cdm.less
@@ -143,6 +143,7 @@
.titleWrap {
white-space : normal;
+ word-break : break-all;
}
.footer {
diff --git a/themes/light/tt-rss.less b/themes/light/tt-rss.less
index 6a1f45d28..1f1242f6b 100644
--- a/themes/light/tt-rss.less
+++ b/themes/light/tt-rss.less
@@ -39,6 +39,7 @@ body.ttrss_main {
.date {
white-space : nowrap;
+ margin-left : 4px;
}
img, i.material-icons {
@@ -53,6 +54,7 @@ body.ttrss_main {
font-weight : 600;
text-rendering: optimizelegibility;
font-family : @fonts-ui;
+ word-break : break-all;
}
}
diff --git a/themes/night.css b/themes/night.css
index 18c370ee4..0bb02e2eb 100644
--- a/themes/night.css
+++ b/themes/night.css
@@ -58,6 +58,7 @@ body.ttrss_main .post .header .comments {
}
body.ttrss_main .post .header .date {
white-space: nowrap;
+ margin-left: 4px;
}
body.ttrss_main .post .header img,
body.ttrss_main .post .header i.material-icons {
@@ -71,6 +72,7 @@ body.ttrss_main .post .header .title {
font-weight: 600;
text-rendering: optimizelegibility;
font-family: system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
+ word-break: break-all;
}
body.ttrss_main .post div.content {
padding: 10px;
@@ -1340,6 +1342,7 @@ body.ttrss_utility hr {
}
.cdm.expanded .titleWrap {
white-space: normal;
+ word-break: break-all;
}
.cdm.expanded .footer {
border: 0px solid #222;
diff --git a/themes/night_blue.css b/themes/night_blue.css
index 22ba4fd17..5f9f6b0e6 100644
--- a/themes/night_blue.css
+++ b/themes/night_blue.css
@@ -58,6 +58,7 @@ body.ttrss_main .post .header .comments {
}
body.ttrss_main .post .header .date {
white-space: nowrap;
+ margin-left: 4px;
}
body.ttrss_main .post .header img,
body.ttrss_main .post .header i.material-icons {
@@ -71,6 +72,7 @@ body.ttrss_main .post .header .title {
font-weight: 600;
text-rendering: optimizelegibility;
font-family: system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
+ word-break: break-all;
}
body.ttrss_main .post div.content {
padding: 10px;
@@ -1340,6 +1342,7 @@ body.ttrss_utility hr {
}
.cdm.expanded .titleWrap {
white-space: normal;
+ word-break: break-all;
}
.cdm.expanded .footer {
border: 0px solid #222;
diff --git a/update.php b/update.php
index f33458c2e..5e31c805b 100755
--- a/update.php
+++ b/update.php
@@ -99,8 +99,13 @@
"opml-export:" => ["USER:FILE", "export OPML of USER to FILE"],
"opml-import:" => ["USER:FILE", "import OPML for USER from FILE"],
"user-list" => "list all users",
-# "user-add:" => ["USER[:PASSWORD]", "add USER, optionally without prompting for PASSWORD"],
-# "user-remove:" => ["USERNAME", "remove specified user"],
+ "user-add:" => ["USER[:PASSWORD[:ACCESS_LEVEL=0]]", "add USER, prompts for password if unset"],
+ "user-remove:" => ["USERNAME", "remove USER"],
+ "user-check-password:" => ["USER:PASSWORD", "returns 0 if user has specified PASSWORD"],
+ "user-set-password:" => ["USER:PASSWORD", "sets PASSWORD of specified USER"],
+ "user-set-access-level:" => ["USER:LEVEL", "sets access LEVEL of specified USER"],
+ "user-exists:" => ["USER", "returns 0 if specified USER exists in the database"],
+ "force-yes" => "assume 'yes' to all queries",
"help" => "",
];
@@ -136,7 +141,7 @@
printf(" %s %s\n", str_pad($option, $max_key_len + 5), $help_text);
}
- return;
+ exit(0);
}
if (!isset($options['daemon'])) {
@@ -150,7 +155,7 @@
Debug::set_enabled(true);
if (isset($options["log-level"])) {
- Debug::set_loglevel((int)$options["log-level"]);
+ Debug::set_loglevel(Debug::map_loglevel((int)$options["log-level"]));
}
if (isset($options["log"])) {
@@ -159,7 +164,7 @@
Debug::log("Logging to " . $options["log"]);
} else {
if (isset($options['quiet'])) {
- Debug::set_loglevel(Debug::$LOG_DISABLED);
+ Debug::set_loglevel(Debug::LOG_DISABLED);
}
}
@@ -177,7 +182,6 @@
if (isset($options["pidlock"])) {
$my_pid = $options["pidlock"];
$lock_filename = "update_daemon-$my_pid.lock";
-
}
Debug::log("Lock: $lock_filename");
@@ -247,6 +251,7 @@
if (isset($options["daemon-loop"])) {
if (!make_stampfile('update_daemon.stamp')) {
Debug::log("warning: unable to create stampfile\n");
+ exit(1);
}
RSSUtils::update_daemon_common(isset($options["pidlock"]) ? 50 : Config::get(Config::DAEMON_FEED_LIMIT), $options);
@@ -265,21 +270,23 @@
if (isset($options["update-schema"])) {
if (Config::is_migration_needed()) {
- if ($options["update-schema"] != "force-yes") {
+ if (!isset($options['force-yes']) && $options["update-schema"] != "force-yes") {
Debug::log("Type 'yes' to continue.");
if (read_stdin() != 'yes')
- exit;
+ exit(1);
} else {
Debug::log("Proceeding to update without confirmation.");
}
if (!isset($options["log-level"])) {
- Debug::set_loglevel(Debug::$LOG_VERBOSE);
+ Debug::set_loglevel(Debug::LOG_VERBOSE);
}
$migrations = Config::get_migrations();
- $migrations->migrate();
+ $rc = $migrations->migrate();
+
+ exit($rc ? 0 : 1);
} else {
Debug::log("Database schema is already at latest version.");
@@ -343,7 +350,6 @@
}
echo "Plugins marked by * are currently enabled for all users.\n";
-
}
if (isset($options["debug-feed"])) {
@@ -352,11 +358,11 @@
if (isset($options["force-refetch"])) $_REQUEST["force_refetch"] = true;
if (isset($options["force-rehash"])) $_REQUEST["force_rehash"] = true;
- Debug::set_loglevel(Debug::$LOG_EXTENDED);
+ Debug::set_loglevel(Debug::LOG_EXTENDED);
- $rc = RSSUtils::update_rss_feed($feed) != false ? 0 : 1;
+ $rc = RSSUtils::update_rss_feed($feed);
- exit($rc);
+ exit($rc ? 0 : 1);
}
if (isset($options["send-digests"])) {
@@ -385,8 +391,10 @@
$rc = $opml->opml_export($filename, $owner_uid, false, true, true);
Debug::log($rc ? "Success." : "Failed.");
+ exit($rc ? 0 : 1);
} else {
Debug::log("User not found: $user");
+ exit(1);
}
}
@@ -401,10 +409,146 @@
$rc = $opml->opml_import($owner_uid, $filename);
Debug::log($rc ? "Success." : "Failed.");
+ exit($rc ? 0 : 1);
} else {
Debug::log("User not found: $user");
+ exit(1);
+ }
+
+ }
+
+ if (isset($options["user-add"])) {
+ list ($login, $password, $access_level) = explode(":", $options["user-add"], 3);
+
+ $uid = UserHelper::find_user_by_login($login);
+
+ if ($uid) {
+ Debug::log("Error: User already exists: $login");
+ exit(1);
+ }
+
+ if (!$access_level)
+ $access_level = UserHelper::ACCESS_LEVEL_USER;
+
+ if (!in_array($access_level, UserHelper::ACCESS_LEVELS)) {
+ Debug::log("Error: Invalid access level value: $access_level");
+ exit(1);
+ }
+
+ if (!$password) {
+ Debug::log("Please enter password for user $login: ");
+ $password = read_stdin();
+
+ if (!$password) {
+ Debug::log("Error: password may not be blank.");
+ exit(1);
+ }
}
+ Debug::log("Adding user $login with access level $access_level...");
+
+ if (UserHelper::user_add($login, $password, $access_level)) {
+ Debug::log("Success.");
+ } else {
+ Debug::log("Operation failed, check the logs for more information.");
+ exit(1);
+ }
+ }
+
+ if (isset($options["user-set-password"])) {
+ list ($login, $password) = explode(":", $options["user-set-password"], 2);
+
+ $uid = UserHelper::find_user_by_login($login);
+
+ if (!$uid) {
+ Debug::log("Error: User not found: $login");
+ exit(1);
+ }
+
+ Debug::log("Changing password of user $login...");
+
+ if (UserHelper::user_modify($uid, $password)) {
+ Debug::log("Success.");
+ } else {
+ Debug::log("Operation failed, check the logs for more information.");
+ exit(1);
+ }
+ }
+
+ if (isset($options["user-set-access-level"])) {
+ list ($login, $access_level) = explode(":", $options["user-set-access-level"], 2);
+
+ $uid = UserHelper::find_user_by_login($login);
+
+ if (!$uid) {
+ Debug::log("Error: User not found: $login");
+ exit(1);
+ }
+
+ if (!in_array($access_level, UserHelper::ACCESS_LEVELS)) {
+ Debug::log("Error: Invalid access level value: $access_level");
+ exit(1);
+ }
+
+ Debug::log("Changing access level of user $login...");
+
+ if (UserHelper::user_modify($uid, '', UserHelper::map_access_level((int)$access_level))) {
+ Debug::log("Success.");
+ } else {
+ Debug::log("Operation failed, check the logs for more information.");
+ exit(1);
+ }
+ }
+
+ if (isset($options["user-remove"])) {
+ $login = $options["user-remove"];
+
+ $uid = UserHelper::find_user_by_login($login);
+
+ if (!$uid) {
+ Debug::log("Error: User not found: $login");
+ exit(1);
+ }
+
+ if (!isset($options['force-yes'])) {
+ Debug::log("About to remove user $login. Type 'yes' to continue.");
+
+ if (read_stdin() != 'yes')
+ exit(1);
+ }
+
+ Debug::log("Removing user $login...");
+
+ if (UserHelper::user_delete($uid)) {
+ Debug::log("Success.");
+ } else {
+ Debug::log("Operation failed, check the logs for more information.");
+ exit(1);
+ }
+ }
+
+ if (isset($options["user-exists"])) {
+ $login = $options["user-exists"];
+
+ if (UserHelper::find_user_by_login($login))
+ exit(0);
+ else
+ exit(1);
+ }
+
+ if (isset($options["user-check-password"])) {
+ list ($login, $password) = explode(":", $options["user-check-password"], 2);
+
+ $uid = UserHelper::find_user_by_login($login);
+
+ if (!$uid) {
+ Debug::log("Error: User not found: $login");
+ exit(1);
+ }
+
+ $rc = UserHelper::user_has_password($uid, $password);
+
+ exit($rc ? 0 : 1);
}
PluginHost::getInstance()->run_commands($options);
diff --git a/update_daemon2.php b/update_daemon2.php
index eea790c8b..72c13c874 100755
--- a/update_daemon2.php
+++ b/update_daemon2.php
@@ -146,7 +146,7 @@
Debug::set_enabled(true);
if (isset($options["log-level"])) {
- Debug::set_loglevel((int)$options["log-level"]);
+ Debug::set_loglevel(Debug::map_loglevel((int)$options["log-level"]));
}
if (isset($options["log"])) {
@@ -155,7 +155,7 @@
Debug::log("Logging to " . $options["log"]);
} else {
if (isset($options['quiet'])) {
- Debug::set_loglevel(Debug::$LOG_DISABLED);
+ Debug::set_loglevel(Debug::LOG_DISABLED);
}
}