diff options
-rw-r--r-- | classes/config.php | 4 | ||||
-rwxr-xr-x | classes/db.php | 8 | ||||
-rw-r--r-- | classes/debug.php | 20 | ||||
-rwxr-xr-x | classes/feeds.php | 2 | ||||
-rwxr-xr-x | classes/handler/public.php | 14 | ||||
-rw-r--r-- | classes/mailer.php | 9 | ||||
-rwxr-xr-x | classes/pref/feeds.php | 10 | ||||
-rwxr-xr-x | classes/pref/filters.php | 8 | ||||
-rw-r--r-- | classes/prefs.php | 6 | ||||
-rwxr-xr-x | classes/rssutils.php | 20 | ||||
-rw-r--r-- | classes/userhelper.php | 160 | ||||
-rw-r--r-- | js/PrefFeedTree.js | 13 | ||||
-rw-r--r-- | js/PrefFilterTree.js | 11 | ||||
-rw-r--r-- | phpstan.neon | 2 | ||||
-rw-r--r-- | plugins/auth_internal/init.php | 12 | ||||
-rw-r--r-- | themes/compact.css | 3 | ||||
-rw-r--r-- | themes/compact_night.css | 3 | ||||
-rw-r--r-- | themes/light-high-contrast.css | 3 | ||||
-rw-r--r-- | themes/light.css | 3 | ||||
-rw-r--r-- | themes/light/cdm.less | 1 | ||||
-rw-r--r-- | themes/light/tt-rss.less | 2 | ||||
-rw-r--r-- | themes/night.css | 3 | ||||
-rw-r--r-- | themes/night_blue.css | 3 | ||||
-rwxr-xr-x | update.php | 172 | ||||
-rwxr-xr-x | update_daemon2.php | 4 |
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); } } |