diff options
31 files changed, 340 insertions, 155 deletions
diff --git a/classes/config.php b/classes/config.php index 1c65fc76f..2c78b908d 100644 --- a/classes/config.php +++ b/classes/config.php @@ -6,7 +6,7 @@ class Config { const T_STRING = 2; const T_INT = 3; - const SCHEMA_VERSION = 145; + const SCHEMA_VERSION = 146; /** override default values, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX: * diff --git a/classes/errors.php b/classes/errors.php index 31be558cf..aa626d017 100644 --- a/classes/errors.php +++ b/classes/errors.php @@ -14,4 +14,27 @@ class Errors { static function to_json(string $code, array $params = []): string { return json_encode(["error" => ["code" => $code, "params" => $params]]); } + + static function libxml_last_error() : string { + $error = libxml_get_last_error(); + $error_formatted = ""; + + if ($error) { + foreach (libxml_get_errors() as $error) { + if ($error->level == LIBXML_ERR_FATAL) { + // currently only the first error is reported + $error_formatted = self::format_libxml_error($error); + break; + } + } + } + + return UConverter::transcode($error_formatted, 'UTF-8', 'UTF-8'); + } + + static function format_libxml_error(LibXMLError $error) : string { + return sprintf("LibXML error %s at line %d (column %d): %s", + $error->code, $error->line, $error->column, + $error->message); + } } diff --git a/classes/feedparser.php b/classes/feedparser.php index 6ce69cc89..3ed0647d2 100644 --- a/classes/feedparser.php +++ b/classes/feedparser.php @@ -193,10 +193,9 @@ class FeedParser { } } + /** @deprecated use Errors::format_libxml_error() instead */ function format_error(LibXMLError $error) : string { - return sprintf("LibXML error %s at line %d (column %d): %s", - $error->code, $error->line, $error->column, - $error->message); + return Errors::format_libxml_error($error); } // libxml may have invalid unicode data in error messages diff --git a/classes/feeds.php b/classes/feeds.php index a9afb70f2..2c37d659a 100755 --- a/classes/feeds.php +++ b/classes/feeds.php @@ -133,7 +133,7 @@ class Feeds extends Handler_Protected { $reply['vfeed_group_enabled'] = $vfeed_group_enabled; $plugin_menu_items = ""; - PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM, + PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2, function ($result) use (&$plugin_menu_items) { $plugin_menu_items .= $result; }, @@ -254,6 +254,10 @@ class Feeds extends Handler_Protected { $line["buttons_left"] .= $button_doc->saveXML($button_doc->firstChild); } + } else if ($result) { + user_error(get_class($plugin) . + " plugin: content provided in HOOK_ARTICLE_LEFT_BUTTON is not valid XML: " . + Errors::libxml_last_error() . " $result", E_USER_WARNING); } }, $line); @@ -273,6 +277,10 @@ class Feeds extends Handler_Protected { $line["buttons"] .= $button_doc->saveXML($button_doc->firstChild); } + } else if ($result) { + user_error(get_class($plugin) . + " plugin: content provided in HOOK_ARTICLE_BUTTON is not valid XML: " . + Errors::libxml_last_error() . " $result", E_USER_WARNING); } }, $line); @@ -718,7 +726,7 @@ class Feeds extends Handler_Protected { <fieldset> <label> <?= \Controls\select_hash("xdebug", $xdebug, - [Debug::$LOG_VERBOSE => "LOG_VERBOSE", Debug::$LOG_EXTENDED => "LOG_EXTENDED"]); + [Debug::LOG_VERBOSE => "LOG_VERBOSE", Debug::LOG_EXTENDED => "LOG_EXTENDED"]); ?></label> </fieldset> @@ -955,7 +963,8 @@ class Feeds extends Handler_Protected { $sth->execute([$owner_uid, $feed]); $row = $sth->fetch(); - return $row["count"]; + // Handle 'SUM()' returning null if there are no results + return $row["count"] ?? 0; } else if ($n_feed == -1) { $match_part = "marked = true"; @@ -1359,7 +1368,8 @@ class Feeds extends Handler_Protected { $sth->execute([$user_id]); $row = $sth->fetch(); - return $row["count"]; + // Handle 'SUM()' returning null if there are no articles/results (e.g. admin user with no feeds) + return $row["count"] ?? 0; } static function _get_cat_title(int $cat_id): string { @@ -2132,7 +2142,7 @@ class Feeds extends Handler_Protected { $owner_uid = $row["owner_uid"]; if (Config::get(Config::FORCE_ARTICLE_PURGE) != 0) { - Debug::log("purge_feed: FORCE_ARTICLE_PURGE is set, overriding interval to " . Config::get(Config::FORCE_ARTICLE_PURGE), Debug::$LOG_VERBOSE); + Debug::log("purge_feed: FORCE_ARTICLE_PURGE is set, overriding interval to " . Config::get(Config::FORCE_ARTICLE_PURGE), Debug::LOG_VERBOSE); $purge_unread = true; $purge_interval = Config::get(Config::FORCE_ARTICLE_PURGE); } else { @@ -2141,10 +2151,10 @@ class Feeds extends Handler_Protected { $purge_interval = (int) $purge_interval; - Debug::log("purge_feed: interval $purge_interval days for feed $feed_id, owner: $owner_uid, purge unread: $purge_unread", Debug::$LOG_VERBOSE); + Debug::log("purge_feed: interval $purge_interval days for feed $feed_id, owner: $owner_uid, purge unread: $purge_unread", Debug::LOG_VERBOSE); if ($purge_interval <= 0) { - Debug::log("purge_feed: purging disabled for this feed, nothing to do.", Debug::$LOG_VERBOSE); + Debug::log("purge_feed: purging disabled for this feed, nothing to do.", Debug::LOG_VERBOSE); return null; } @@ -2177,10 +2187,10 @@ class Feeds extends Handler_Protected { $rows_deleted = $sth->rowCount(); - Debug::log("purge_feed: deleted $rows_deleted articles.", Debug::$LOG_VERBOSE); + Debug::log("purge_feed: deleted $rows_deleted articles.", Debug::LOG_VERBOSE); } else { - Debug::log("purge_feed: owner of $feed_id not found", Debug::$LOG_VERBOSE); + Debug::log("purge_feed: owner of $feed_id not found", Debug::LOG_VERBOSE); } return $rows_deleted; diff --git a/classes/plugin.php b/classes/plugin.php index be8376925..39af6a9a1 100644 --- a/classes/plugin.php +++ b/classes/plugin.php @@ -98,7 +98,7 @@ abstract class Plugin { /* GLOBAL hooks are invoked in global context, only available to system plugins (loaded via .env for all users) */ - /** Adds buttons for article (on the right) - e.g. mail, share, add note. + /** Adds buttons for article (on the right) - e.g. mail, share, add note. Generated markup must be valid XML. * @param array<string,mixed> $line * @return string * @see PluginHost::HOOK_ARTICLE_BUTTON @@ -307,7 +307,7 @@ abstract class Plugin { return []; } - /** Adds per-article buttons on the left side + /** Adds per-article buttons on the left side. Generated markup must be valid XML. * @param array<string,mixed> $row * @return string * @see PluginHost::HOOK_ARTICLE_LEFT_BUTTON @@ -647,6 +647,7 @@ abstract class Plugin { } /** Allows adding custom elements to headlines Select... dropdown + * @deprecated removed, see Plugin::hook_headline_toolbar_select_menu_item2() * @param int $feed_id * @param int $is_cat * @return string @@ -658,6 +659,18 @@ abstract class Plugin { return ""; } + /** Allows adding custom elements to headlines Select... select dropdown (<option> format) + * @param int $feed_id + * @param int $is_cat + * @return string + * @see PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2 + */ + function hook_headline_toolbar_select_menu_item2($feed_id, $is_cat) { + user_error("Dummy method invoked.", E_USER_ERROR); + + return ""; + } + /** Invoked when user tries to subscribe to feed, may override information (i.e. feed URL) used afterwards * @param string $url * @param string $auth_login diff --git a/classes/pluginhost.php b/classes/pluginhost.php index a3a389def..952d4df77 100755 --- a/classes/pluginhost.php +++ b/classes/pluginhost.php @@ -189,9 +189,14 @@ class PluginHost { /** @see Plugin::hook_headlines_custom_sort_override() */ const HOOK_HEADLINES_CUSTOM_SORT_OVERRIDE = "hook_headlines_custom_sort_override"; - /** @see Plugin::hook_headline_toolbar_select_menu_item() */ + /** @see Plugin::hook_headline_toolbar_select_menu_item() + * @deprecated removed, see PluginHost::HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2 + */ const HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM = "hook_headline_toolbar_select_menu_item"; + /** @see Plugin::hook_headline_toolbar_select_menu_item() */ + const HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2 = "hook_headline_toolbar_select_menu_item2"; + /** @see Plugin::hook_pre_subscribe() */ const HOOK_PRE_SUBSCRIBE = "hook_pre_subscribe"; @@ -270,9 +275,10 @@ class PluginHost { * @param mixed $args */ function run_hooks(string $hook, ...$args): void { - $method = strtolower($hook); - foreach ($this->get_hooks($hook) as $plugin) { + $method = strtolower((string)$hook); + + foreach ($this->get_hooks((string)$hook) as $plugin) { //Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE); try { @@ -291,9 +297,9 @@ class PluginHost { * @param mixed $check */ function run_hooks_until(string $hook, $check, ...$args): bool { - $method = strtolower($hook); + $method = strtolower((string)$hook); - foreach ($this->get_hooks($hook) as $plugin) { + foreach ($this->get_hooks((string)$hook) as $plugin) { try { $result = $plugin->$method(...$args); @@ -315,9 +321,9 @@ class PluginHost { * @param mixed $args */ function run_hooks_callback(string $hook, Closure $callback, ...$args): void { - $method = strtolower($hook); + $method = strtolower((string)$hook); - foreach ($this->get_hooks($hook) as $plugin) { + foreach ($this->get_hooks((string)$hook) as $plugin) { //Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE); try { @@ -336,9 +342,9 @@ class PluginHost { * @param mixed $args */ function chain_hooks_callback(string $hook, Closure $callback, &...$args): void { - $method = strtolower($hook); + $method = strtolower((string)$hook); - foreach ($this->get_hooks($hook) as $plugin) { + foreach ($this->get_hooks((string)$hook) as $plugin) { //Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE); try { @@ -358,7 +364,7 @@ class PluginHost { function add_hook(string $type, Plugin $sender, int $priority = 50): void { $priority = (int) $priority; - if (!method_exists($sender, strtolower($type))) { + if (!method_exists($sender, strtolower((string)$type))) { user_error( sprintf("Plugin %s tried to register a hook without implementation: %s", get_class($sender), $type), @@ -422,7 +428,7 @@ class PluginHost { asort($plugins); - $this->load(join(",", $plugins), $kind, $owner_uid, $skip_init); + $this->load(join(",", $plugins), (int)$kind, $owner_uid, $skip_init); } /** diff --git a/classes/pref/filters.php b/classes/pref/filters.php index 6e6e3d9ee..04178f1a6 100755 --- a/classes/pref/filters.php +++ b/classes/pref/filters.php @@ -1,6 +1,15 @@ <?php class Pref_Filters extends Handler_Protected { + const ACTION_TAG = 4; + const ACTION_SCORE = 6; + const ACTION_LABEL = 7; + const ACTION_PLUGIN = 9; + const ACTION_REMOVE_TAG = 10; + + const PARAM_ACTIONS = [self::ACTION_TAG, self::ACTION_SCORE, + self::ACTION_LABEL, self::ACTION_PLUGIN, self::ACTION_REMOVE_TAG]; + function csrf_ignore(string $method): bool { $csrf_ignored = array("index", "getfiltertree", "savefilterorder"); @@ -274,7 +283,7 @@ class Pref_Filters extends Handler_Protected { } } - if ($line['action_id'] == 7) { + if ($line['action_id'] == self::ACTION_LABEL) { $label_sth = $this->pdo->prepare("SELECT fg_color, bg_color FROM ttrss_labels2 WHERE caption = ? AND owner_uid = ?"); @@ -474,11 +483,7 @@ class Pref_Filters extends Handler_Protected { $title = __($row["description"]); - if ($action["action_id"] == 4 || $action["action_id"] == 6 || - $action["action_id"] == 7) - $title .= ": " . $action["action_param"]; - - if ($action["action_id"] == 9) { + if ($action["action_id"] == self::ACTION_PLUGIN) { list ($pfclass, $pfaction) = explode(":", $action["action_param"]); $filter_actions = PluginHost::getInstance()->get_filter_actions(); @@ -491,6 +496,8 @@ class Pref_Filters extends Handler_Protected { } } } + } else if (in_array($action["action_id"], self::PARAM_ACTIONS)) { + $title .= ": " . $action["action_param"]; } } @@ -596,14 +603,19 @@ class Pref_Filters extends Handler_Protected { $action_param = $action["action_param"]; $action_param_label = $action["action_param_label"]; - if ($action_id == 7) { + if ($action_id == self::ACTION_LABEL) { $action_param = $action_param_label; } - if ($action_id == 6) { + if ($action_id == self::ACTION_SCORE) { $action_param = (int)str_replace("+", "", $action_param); } + if (in_array($action_id, [self::ACTION_TAG, self::ACTION_REMOVE_TAG])) { + $action_param = implode(", ", FeedItem_Common::normalize_categories( + explode(",", $action_param))); + } + $asth->execute([$filter_id, $action_id, $action_param]); } } diff --git a/classes/rssutils.php b/classes/rssutils.php index b886a060c..e826a32f8 100755 --- a/classes/rssutils.php +++ b/classes/rssutils.php @@ -557,7 +557,7 @@ class RSSUtils { Debug::log("language: $feed_language", Debug::LOG_VERBOSE); Debug::log("processing feed data...", Debug::LOG_VERBOSE); - $site_url = mb_substr(rewrite_relative_url($feed_obj->feed_url, clean($rss->get_link())), 0, 245); + $site_url = mb_substr(UrlHelper::rewrite_relative($feed_obj->feed_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); @@ -736,7 +736,7 @@ class RSSUtils { // TODO: Just use FeedEnclosure (and modify it to cover whatever justified this)? $e_item = array( - rewrite_relative_url($site_url, $e->link), + UrlHelper::rewrite_relative($site_url, $e->link), $e->type, $e->length, $e->title, $e->width, $e->height); // Yet another episode of "mysql utf8_general_ci is gimped" @@ -1164,32 +1164,30 @@ class RSSUtils { } // check for manual tags (we have to do it here since they're loaded from filters) - foreach ($article_filters as $f) { if ($f["type"] == "tag") { + $entry_tags = array_merge($entry_tags, + FeedItem_Common::normalize_categories(explode(",", $f["param"]))); + } + } - $manual_tags = array_map('trim', explode(",", mb_strtolower($f["param"]))); - - foreach ($manual_tags as $tag) { - array_push($entry_tags, $tag); - } + // like boring tags, but filter-based + foreach ($article_filters as $f) { + if ($f["type"] == "ignore-tag") { + $entry_tags = array_diff($entry_tags, + FeedItem_Common::normalize_categories(explode(",", $f["param"]))); } } // Skip boring tags - - $boring_tags = array_map('trim', - explode(",", mb_strtolower( - get_pref(Prefs::BLACKLISTED_TAGS, $feed_obj->owner_uid)))); - $entry_tags = FeedItem_Common::normalize_categories( - array_unique( - array_diff($entry_tags, $boring_tags))); + array_diff($entry_tags, + FeedItem_Common::normalize_categories(explode(",", + get_pref(Prefs::BLACKLISTED_TAGS, $feed_obj->owner_uid))))); - Debug::log("filtered tags: " . implode(", ", $entry_tags), Debug::LOG_VERBOSE); + Debug::log("resulting article tags: " . implode(", ", $entry_tags), Debug::LOG_VERBOSE); // Save article tags in the database - if (count($entry_tags) > 0) { $tsth = $pdo->prepare("SELECT id FROM ttrss_tags @@ -1286,7 +1284,7 @@ class RSSUtils { foreach ($enclosures as $enc) { if (preg_match("/(image|audio|video)/", $enc[1])) { - $src = rewrite_relative_url($site_url, $enc[0]); + $src = UrlHelper::rewrite_relative($site_url, $enc[0]); $local_filename = sha1($src); @@ -1312,7 +1310,7 @@ class RSSUtils { /* TODO: move to DiskCache? */ static function cache_media_url(DiskCache $cache, string $url, string $site_url): void { - $url = rewrite_relative_url($site_url, $url); + $url = UrlHelper::rewrite_relative($site_url, $url); $local_filename = sha1($url); Debug::log("cache_media: checking $url", Debug::LOG_VERBOSE); @@ -1874,14 +1872,14 @@ class RSSUtils { $base = $xpath->query('/html/head/base[@href]'); foreach ($base as $b) { - $url = rewrite_relative_url($url, $b->getAttribute("href")); + $url = UrlHelper::rewrite_relative($url, $b->getAttribute("href")); break; } $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]'); if (count($entries) > 0) { foreach ($entries as $entry) { - $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href")); + $favicon_url = UrlHelper::rewrite_relative($url, $entry->getAttribute("href")); break; } } @@ -1889,7 +1887,7 @@ class RSSUtils { } if (!$favicon_url) - $favicon_url = rewrite_relative_url($url, "/favicon.ico"); + $favicon_url = UrlHelper::rewrite_relative($url, "/favicon.ico"); return $favicon_url; } diff --git a/classes/urlhelper.php b/classes/urlhelper.php index 9696c16db..9ac7781ef 100644 --- a/classes/urlhelper.php +++ b/classes/urlhelper.php @@ -419,6 +419,8 @@ class UrlHelper { if (curl_errno($ch) != 0) { self::$fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch); + } else { + self::$fetch_last_error = "HTTP Code: $http_code "; } self::$fetch_last_error_content = $contents; @@ -215,20 +215,13 @@ ?> </select> - <div class="catchup-button" dojoType="fox.form.ComboButton" onclick="Feeds.catchupCurrent()"> - <span><?= __('Mark as read') ?></span> - <div dojoType="dijit.DropDownMenu"> - <div dojoType="dijit.MenuItem" onclick="Feeds.catchupCurrent('1day')"> - <?= __('Older than one day') ?> - </div> - <div dojoType="dijit.MenuItem" onclick="Feeds.catchupCurrent('1week')"> - <?= __('Older than one week') ?> - </div> - <div dojoType="dijit.MenuItem" onclick="Feeds.catchupCurrent('2week')"> - <?= __('Older than two weeks') ?> - </div> - </div> - </div> + <select class="catchup-button" id="main-catchup-dropdown" dojoType="fox.form.Select" + data-prevent-value-change="true"> + <option value=""><?= __('Mark as read') ?></option> + <option value="1day"><?= __('Older than one day') ?></option> + <option value="1week"><?= __('Older than one week') ?></option> + <option value="2week"><?= __('Older than two weeks') ?></option> + </select> </form> diff --git a/js/CommonFilters.js b/js/CommonFilters.js index 8a20480f0..434ee72c7 100644 --- a/js/CommonFilters.js +++ b/js/CommonFilters.js @@ -16,7 +16,8 @@ const Filters = { ACTION_SCORE: 6, ACTION_LABEL: 7, ACTION_PLUGIN: 9, - PARAM_ACTIONS: [4, 6, 7, 9], + ACTION_REMOVE_TAG: 10, + PARAM_ACTIONS: [4, 6, 7, 9, 10], filter_info: {}, test: function() { const test_dialog = new fox.SingleUseDialog({ diff --git a/js/Feeds.js b/js/Feeds.js index 5ef554af0..714eb77d2 100644 --- a/js/Feeds.js +++ b/js/Feeds.js @@ -282,6 +282,10 @@ const Feeds = { CommonDialogs.safeModeWarning(); } + dojo.connect(dijit.byId("main-catchup-dropdown"), 'onItemClick', + (item) => Feeds.catchupCurrent(item.option.value) + ); + // bw_limit disables timeout() so we request initial counters separately if (App.getInitParam("bw_limit")) { this.requestCounters(); diff --git a/js/Headlines.js b/js/Headlines.js index 18e47d740..2be3cd697 100755 --- a/js/Headlines.js +++ b/js/Headlines.js @@ -626,6 +626,12 @@ const Headlines = { const search_query = Feeds._search_query ? Feeds._search_query.query : ""; const target = dijit.byId('toolbar-headlines'); + // TODO: is this needed? destroyDescendants() below might take care of it (?) + if (this._headlinesSelectClickHandle) + dojo.disconnect(this._headlinesSelectClickHandle); + + target.destroyDescendants(); + if (tb && typeof tb == 'object') { target.attr('innerHTML', ` @@ -646,27 +652,37 @@ const Headlines = { </span> <span class='right'> <span id='selected_prompt'></span> - <div class='select-articles-dropdown' dojoType='fox.form.DropDownButton' title='"${__('Select articles')}'> - <span>${__("Select...")}</span> - <div dojoType='dijit.Menu' style='display: none;'> - <div dojoType='dijit.MenuItem' onclick='Headlines.select("all")'>${__('All')}</div> - <div dojoType='dijit.MenuItem' onclick='Headlines.select("unread")'>${__('Unread')}</div> - <div dojoType='dijit.MenuItem' onclick='Headlines.select("invert")'>${__('Invert')}</div> - <div dojoType='dijit.MenuItem' onclick='Headlines.select("none")'>${__('None')}</div> - <div dojoType='dijit.MenuSeparator'></div> - <div dojoType='dijit.MenuItem' onclick='Headlines.selectionToggleUnread()'>${__('Toggle unread')}</div> - <div dojoType='dijit.MenuItem' onclick='Headlines.selectionToggleMarked()'>${__('Toggle starred')}</div> - <div dojoType='dijit.MenuItem' onclick='Headlines.selectionTogglePublished()'>${__('Toggle published')}</div> - <div dojoType='dijit.MenuSeparator'></div> - <div dojoType='dijit.MenuItem' onclick='Headlines.catchupSelection()'>${__('Mark as read')}</div> - <div dojoType='dijit.MenuItem' onclick='Article.selectionSetScore()'>${__('Set score')}</div> - ${tb.plugin_menu_items} + + <select class='select-articles-dropdown' + id='headlines-select-articles-dropdown' + data-prevent-value-change="true" + data-dropdown-skip-first="true" + dojoType="fox.form.Select" + title="${__('Show articles')}"> + <option value='' selected="selected">${__("Select...")}</option> + <option value='headlines_select_all'>${__('All')}</option> + <option value='headlines_select_unread'>${__('Unread')}</option> + <option value='headlines_select_invert'>${__('Invert')}</option> + <option value='headlines_select_none'>${__('None')}</option> + <option></option> + <option value='headlines_selectionToggleUnread'>${__('Toggle unread')}</option> + <option value='headlines_selectionToggleMarked'>${__('Toggle starred')}</option> + <option value='headlines_selectionTogglePublished'>${__('Toggle published')}</option> + <option></option> + <option value='headlines_catchupSelection'>${__('Mark as read')}</option> + <option value='article_selectionSetScore'>${__('Set score')}</option> + ${tb.plugin_menu_items != '' ? + ` + <option></option> + ${tb.plugin_menu_items} + ` : ''} ${headlines.id === 0 && !headlines.is_cat ? ` - <div dojoType='dijit.MenuSeparator'></div> - <div dojoType='dijit.MenuItem' class='text-error' onclick='Headlines.deleteSelection()'>${__('Delete permanently')}</div> + <option></option> + <option class='text-error' value='headlines_deleteSelection'>${__('Delete permanently')}</option> ` : ''} - </div> + </select> + ${tb.plugin_buttons} </span> `); @@ -675,6 +691,48 @@ const Headlines = { } dojo.parser.parse(target.domNode); + + this._headlinesSelectClickHandle = dojo.connect(dijit.byId("headlines-select-articles-dropdown"), 'onItemClick', + (item) => { + const action = item.option.value; + + switch (action) { + case 'headlines_select_all': + Headlines.select('all'); + break; + case 'headlines_select_unread': + Headlines.select('unread'); + break; + case 'headlines_select_invert': + Headlines.select('invert'); + break; + case 'headlines_select_none': + Headlines.select('none'); + break; + case 'headlines_selectionToggleUnread': + Headlines.selectionToggleUnread(); + break; + case 'headlines_selectionToggleMarked': + Headlines.selectionToggleMarked(); + break; + case 'headlines_selectionTogglePublished': + Headlines.selectionTogglePublished(); + break; + case 'headlines_catchupSelection': + Headlines.catchupSelection(); + break; + case 'article_selectionSetScore': + Article.selectionSetScore(); + break; + case 'headlines_deleteSelection': + Headlines.deleteSelection(); + break; + default: + if (!PluginHost.run_until(PluginHost.HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2, true, action)) + console.warn('unknown headlines action', action); + } + } + ); }, onLoaded: function (reply, offset, append) { console.log("Headlines.onLoaded: offset=", offset, "append=", append); diff --git a/js/PluginHost.js b/js/PluginHost.js index deb7c0645..513429e4a 100644 --- a/js/PluginHost.js +++ b/js/PluginHost.js @@ -21,6 +21,7 @@ const PluginHost = { HOOK_HEADLINE_MUTATIONS_SYNCED: 16, HOOK_HEADLINES_RENDERED: 17, HOOK_HEADLINES_SCROLL_HANDLER: 18, + HOOK_HEADLINE_TOOLBAR_SELECT_MENU_ITEM2: 19, hooks: [], register: function (name, callback) { if (typeof(this.hooks[name]) == 'undefined') @@ -36,6 +37,17 @@ const PluginHost = { this.hooks[name][i](args); } }, + run_until: function (name, check, ...args) { + //console.warn('PluginHost.run_until', name, check, args); + + if (typeof(this.hooks[name]) != 'undefined') + for (let i = 0; i < this.hooks[name].length; i++) { + if (this.hooks[name][i](args) == check) + return true; + } + + return false; + }, unregister: function (name, callback) { for (let i = 0; i < this.hooks[name].length; i++) if (this.hooks[name][i] == callback) diff --git a/js/form/Select.js b/js/form/Select.js index 530880e2d..0c73cd52c 100755 --- a/js/form/Select.js +++ b/js/form/Select.js @@ -1,8 +1,66 @@ -/* global dijit, define */ -define(["dojo/_base/declare", "dijit/form/Select"], function (declare) { - return declare("fox.form.Select", dijit.form.Select, { +/* eslint-disable prefer-rest-params */ +/* global define */ +// FIXME: there probably is a better, more dojo-like notation for custom data- properties +define(["dojo/_base/declare", + "dijit/form/Select", + "dojo/_base/lang", // lang.hitch + "dijit/MenuItem", + "dijit/MenuSeparator", + "dojo/aspect", + ], function (declare, select, lang, MenuItem, MenuSeparator, aspect) { + return declare("fox.form.Select", select, { focus: function() { return; // Stop dijit.form.Select from keeping focus after closing the menu }, + startup: function() { + this.inherited(arguments); + + if (this.attr('data-dropdown-skip-first') == 'true') { + aspect.before(this, "_loadChildren", () => { + this.options = this.options.splice(1); + }); + } + }, + // hook invoked when dropdown MenuItem is clicked + onItemClick: function(/*item, menu*/) { + // + }, + _setValueAttr: function(/*anything*/ newValue, /*Boolean?*/ priorityChange){ + if (this.attr('data-prevent-value-change') == 'true' && newValue != '') + return; + + this.inherited(arguments); + }, + // the only difference from dijit/form/Select is _onItemClicked() handler + _getMenuItemForOption: function(/*_FormSelectWidget.__SelectOption*/ option){ + // summary: + // For the given option, return the menu item that should be + // used to display it. This can be overridden as needed + if (!option.value && !option.label){ + // We are a separator (no label set for it) + return new MenuSeparator({ownerDocument: this.ownerDocument}); + } else { + // Just a regular menu option + const click = lang.hitch(this, "_setValueAttr", option); + const item = new MenuItem({ + option: option, + label: (this.labelType === 'text' ? (option.label || '').toString() + .replace(/&/g, '&').replace(/</g, '<') : + option.label) || this.emptyLabel, + onClick: () => { + this.onItemClick(item, this.dropDown); + + click(); + }, + ownerDocument: this.ownerDocument, + dir: this.dir, + textDir: this.textDir, + disabled: option.disabled || false + }); + item.focusNode.setAttribute("role", "option"); + + return item; + } + }, }); }); diff --git a/locale/cs_CZ/LC_MESSAGES/messages.mo b/locale/cs_CZ/LC_MESSAGES/messages.mo Binary files differindex 470547241..31ff2c923 100644 --- a/locale/cs_CZ/LC_MESSAGES/messages.mo +++ b/locale/cs_CZ/LC_MESSAGES/messages.mo diff --git a/locale/cs_CZ/LC_MESSAGES/messages.po b/locale/cs_CZ/LC_MESSAGES/messages.po index f716401ec..a8dca4355 100644 --- a/locale/cs_CZ/LC_MESSAGES/messages.po +++ b/locale/cs_CZ/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: TT-RSS CZech\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-06-18 13:34+0300\n" -"PO-Revision-Date: 2021-03-12 06:52+0000\n" +"PO-Revision-Date: 2021-11-24 07:41+0000\n" "Last-Translator: Marek Pavelka <[email protected]>\n" "Language-Team: Czech <https://weblate.tt-rss.org/projects/tt-rss/messages/cs/" ">\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -"X-Generator: Weblate 4.5.1\n" +"X-Generator: Weblate 4.9.1\n" #: backend.php:60 msgid "Use default" @@ -280,20 +280,16 @@ msgid "Open next feed" msgstr "Otevřít další kanál" #: classes/rpc.php:567 -#, fuzzy -#| msgid "Open next feed" msgid "Open next unread feed" -msgstr "Otevřít další kanál" +msgstr "Otevřít další nepřečtený kanál" #: classes/rpc.php:568 msgid "Open previous feed" msgstr "Otevřít předchozí kanál" #: classes/rpc.php:569 -#, fuzzy -#| msgid "Open previous feed" msgid "Open previous unread feed" -msgstr "Otevřít předchozí kanál" +msgstr "Otevřít předchozí nepřečtený kanál" #: classes/rpc.php:570 msgid "Open next article (in combined mode, scroll down)" @@ -461,10 +457,8 @@ msgid "Toggle headline grouping" msgstr "Přepnout seskupování nadpisů" #: classes/rpc.php:613 -#, fuzzy -#| msgid "Toggle sidebar" msgid "Toggle grid view" -msgstr "Přepnout postranní panel" +msgstr "Přepnout zobrazení v mřížce" #: classes/rpc.php:614 msgid "Debug feed update" @@ -851,14 +845,12 @@ msgid "May increase server load" msgstr "Může zvýšit zatížení serveru" #: classes/pref/prefs.php:121 -#, fuzzy -#| msgid "Preview" msgid "Grid view" -msgstr "Náhled" +msgstr "Zobrazení v mřížce" #: classes/pref/prefs.php:121 msgid "On wider screens, if always expanded" -msgstr "" +msgstr "Na širších obrazovkách, pokud je vždy rozbaleno" #: classes/pref/prefs.php:222 msgid "The configuration was saved." @@ -927,14 +919,12 @@ msgid "Disable OTP" msgstr "Zakázat jednorázové heslo" #: classes/pref/prefs.php:472 -#, fuzzy -#| msgid "OTP Key:" msgid "OTP secret:" -msgstr "Klíč jednorázového hesla:" +msgstr "Tajné jednorázové heslo:" #: classes/pref/prefs.php:499 msgid "Verification code:" -msgstr "" +msgstr "Ověřovací kód:" #: classes/pref/prefs.php:507 msgid "Enable OTP" @@ -1609,10 +1599,8 @@ msgid "%d min" msgstr "%d min" #: plugins/auth_internal/init.php:112 -#, fuzzy -#| msgid "Please enter label caption:" msgid "Please enter verification code (OTP):" -msgstr "Zadejte titulek štítku:" +msgstr "Zadejte ověřovací kód (jednorázové heslo):" #: plugins/auth_internal/init.php:114 msgid "Continue" @@ -1620,7 +1608,7 @@ msgstr "Pokračovat" #: plugins/auth_internal/init.php:166 msgid "Too many authentication attempts, throttled." -msgstr "" +msgstr "Příliš mnoho pokusů o ověření, omezeno." #: plugins/auth_internal/init.php:247 msgid "Password has been changed." @@ -1833,8 +1821,6 @@ msgid "The following comics are currently supported:" msgstr "Nyní jsou podporovány následující komiksy:" #: plugins/nsfw/init.php:39 -#, fuzzy -#| msgid "Not work safe (click to toggle)" msgid "Not safe for work (click to toggle)" msgstr "Neotvírat v práci (kliknutím přepnout)" @@ -2331,13 +2317,11 @@ msgstr "Vymazat protokol událostí?" #: js/PrefHelpers.js:135 msgid "Name for cloned profile:" -msgstr "" +msgstr "Název pro klonovaný profil:" #: js/PrefHelpers.js:145 -#, fuzzy -#| msgid "Please select an image file." msgid "Please select a single profile to clone." -msgstr "Vyberte soubor obrázku." +msgstr "Vyberte jeden profil pro klonování." #: js/PrefHelpers.js:153 msgid "" @@ -2362,7 +2346,7 @@ msgstr "(aktivní)" #: js/PrefHelpers.js:219 msgid "(empty)" -msgstr "" +msgstr "(prázdný)" #: js/PrefHelpers.js:242 msgid "Activate selected profile?" @@ -2749,10 +2733,8 @@ msgid "Looking for articles (%d processed, %f found)..." msgstr "Hledání článků (%d zpracováno, %f nalezeno)..." #: js/CommonFilters.js:72 -#, fuzzy -#| msgid "Found %d articles matching this filter:" msgid "Articles matching this filter:" -msgstr "Nalezeno %d článků odpovídajících tomuto filtru:" +msgstr "Články odpovídající tomuto filtru:" #: js/CommonFilters.js:74 msgid "Found %d articles matching this filter:" @@ -2763,10 +2745,8 @@ msgid "Error while trying to get filter test results." msgstr "Chyba při pokusu o získání výsledků testu filtru." #: js/CommonFilters.js:95 -#, fuzzy -#| msgid "Looking for plugins..." msgid "Looking for articles..." -msgstr "Vyhledávání modulů..." +msgstr "Vyhledávání článků..." #: js/CommonFilters.js:174 msgid "Edit rule" @@ -2829,10 +2809,8 @@ msgid "Unable to fetch full text for this article" msgstr "Nelze načíst úplný text pro tento článek" #: plugins/shorten_expanded/init.js:32 -#, fuzzy -#| msgid "Email article" msgid "Expand article" -msgstr "Odeslat článek e-mailem" +msgstr "Rozbalit článek" #: plugins/share/share.js:7 msgid "Share article by URL" @@ -2896,13 +2874,11 @@ msgstr "URL stránky:" #: js/PrefHelpers.js:229 msgid "Clone" -msgstr "" +msgstr "Klonovat" #: js/PrefHelpers.js:231 -#, fuzzy -#| msgid "Activate profile" msgid "Activate" -msgstr "Aktivovat profil" +msgstr "Aktivovat" #: js/PrefHelpers.js:299 msgid "Apply" diff --git a/locale/zh_TW/LC_MESSAGES/messages.mo b/locale/zh_TW/LC_MESSAGES/messages.mo Binary files differindex 6ec886518..74a2f11b3 100644 --- a/locale/zh_TW/LC_MESSAGES/messages.mo +++ b/locale/zh_TW/LC_MESSAGES/messages.mo diff --git a/locale/zh_TW/LC_MESSAGES/messages.po b/locale/zh_TW/LC_MESSAGES/messages.po index 0ba915e8b..83316edc1 100644 --- a/locale/zh_TW/LC_MESSAGES/messages.po +++ b/locale/zh_TW/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: Tiny Tiny RSS\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-06-18 13:34+0300\n" -"PO-Revision-Date: 2021-09-22 14:10+0000\n" +"PO-Revision-Date: 2021-11-21 17:41+0000\n" "Last-Translator: TonyRL <[email protected]>\n" "Language-Team: Chinese (Traditional) <https://weblate.tt-rss.org/projects/" "tt-rss/messages/zh_Hant/>\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Weblate 4.8.1\n" +"X-Generator: Weblate 4.9.1\n" #: backend.php:60 msgid "Use default" @@ -149,7 +149,7 @@ msgstr "未讀" #: index.php:192 msgid "With Note" -msgstr "筆記" +msgstr "附註記" #: index.php:195 msgid "Sort articles" @@ -650,7 +650,7 @@ msgstr "進階" #: classes/pref/prefs.php:81 msgid "Debugging" -msgstr "除錯中" +msgstr "除錯" #: classes/pref/prefs.php:87 msgid "Never apply these tags automatically (comma-separated list)." diff --git a/plugins/af_redditimgur/init.php b/plugins/af_redditimgur/init.php index 8ec947b86..f2a04ce24 100755 --- a/plugins/af_redditimgur/init.php +++ b/plugins/af_redditimgur/init.php @@ -158,7 +158,24 @@ class Af_RedditImgur extends Plugin { private function process_post_media(array $data, DOMDocument $doc, DOMXPath $xpath, DOMElement $anchor) : bool { $found = 0; - if (isset($data["media_metadata"])) { + // process galleries in the right order + if (isset($data["gallery_data"]) && isset($data["media_metadata"])) { + foreach ($data["gallery_data"]["items"] as $gal_item) { + $media_id = $gal_item["media_id"] ?? null; + + if ($media_id) { + $media_url = htmlspecialchars_decode($data["media_metadata"][$media_id]["s"]["u"] ?? ""); + + if ($media_url) { + Debug::log("found gallery item: $media_id, url: $media_url", Debug::LOG_EXTENDED); + + $this->handle_as_image($doc, $anchor, $media_url); + $found = 1; + } + } + } + // i'm not sure if this is a thing, but if there's no gallery just process any possible attaches in the random order... + } else if (isset($data["media_metadata"])) { foreach ($data["media_metadata"] as $media) { if (!empty($media["s"]["u"])) { $media_url = htmlspecialchars_decode($media["s"]["u"]); @@ -205,7 +222,7 @@ class Af_RedditImgur extends Plugin { Debug::log("found hosted video url: $media_url / poster $poster_url, looking up fallback url...", Debug::LOG_VERBOSE); - $fallback_url = $data["media"]["reddit_video"]["fallback_url"]; + $fallback_url = $data["media"]["reddit_video"]["fallback_url"] ?? null; if ($fallback_url) { Debug::log("found video fallback_url: $fallback_url", Debug::LOG_VERBOSE); diff --git a/sql/mysql/migrations/146.sql b/sql/mysql/migrations/146.sql new file mode 100644 index 000000000..6d4824727 --- /dev/null +++ b/sql/mysql/migrations/146.sql @@ -0,0 +1,2 @@ +insert into ttrss_filter_actions (id,name,description) values (10, 'ignore-tag', + 'Ignore tags'); diff --git a/sql/mysql/schema.sql b/sql/mysql/schema.sql index ff6ff4797..589d1013a 100644 --- a/sql/mysql/schema.sql +++ b/sql/mysql/schema.sql @@ -249,6 +249,9 @@ insert into ttrss_filter_actions (id,name,description) values (8, 'stop', insert into ttrss_filter_actions (id,name,description) values (9, 'plugin', 'Invoke plugin'); +insert into ttrss_filter_actions (id,name,description) values (10, 'ignore-tag', + 'Ignore tags'); + create table ttrss_filters2(id integer primary key auto_increment, owner_uid integer not null, match_any_rule boolean not null default false, diff --git a/sql/pgsql/migrations/146.sql b/sql/pgsql/migrations/146.sql new file mode 100644 index 000000000..6d4824727 --- /dev/null +++ b/sql/pgsql/migrations/146.sql @@ -0,0 +1,2 @@ +insert into ttrss_filter_actions (id,name,description) values (10, 'ignore-tag', + 'Ignore tags'); diff --git a/sql/pgsql/schema.sql b/sql/pgsql/schema.sql index b539419b6..938ccc905 100644 --- a/sql/pgsql/schema.sql +++ b/sql/pgsql/schema.sql @@ -245,6 +245,9 @@ insert into ttrss_filter_actions (id,name,description) values (8, 'stop', insert into ttrss_filter_actions (id,name,description) values (9, 'plugin', 'Invoke plugin'); +insert into ttrss_filter_actions (id,name,description) values (10, 'ignore-tag', + 'Ignore tags'); + create table ttrss_filters2(id serial not null primary key, owner_uid integer not null references ttrss_users(id) on delete cascade, match_any_rule boolean not null default false, diff --git a/themes/compact.css b/themes/compact.css index ccd6ef76c..d462892db 100644 --- a/themes/compact.css +++ b/themes/compact.css @@ -1475,8 +1475,7 @@ body.ttrss_utility hr { box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.1); border: 0 solid #ddd; border-bottom-width: 1px; - background: white ! important; - opacity: 0.9; + background: rgba(255, 255, 255, 0.9) ! important; backdrop-filter: blur(6px); } body.ttrss_prefs { diff --git a/themes/compact_night.css b/themes/compact_night.css index 6b072e510..8b1cd17bc 100644 --- a/themes/compact_night.css +++ b/themes/compact_night.css @@ -1475,8 +1475,7 @@ body.ttrss_utility hr { box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.1); border: 0 solid #222; border-bottom-width: 1px; - background: #333 ! important; - opacity: 0.9; + background: rgba(51, 51, 51, 0.9) ! important; backdrop-filter: blur(6px); } body.ttrss_prefs { diff --git a/themes/light-high-contrast.css b/themes/light-high-contrast.css index 18fc67f6a..77f3def7e 100644 --- a/themes/light-high-contrast.css +++ b/themes/light-high-contrast.css @@ -1475,8 +1475,7 @@ body.ttrss_utility hr { box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.1); border: 0 solid #ddd; border-bottom-width: 1px; - background: white ! important; - opacity: 0.9; + background: rgba(255, 255, 255, 0.9) ! important; backdrop-filter: blur(6px); } body.ttrss_prefs { diff --git a/themes/light.css b/themes/light.css index 475e4dbbf..8367b07cc 100644 --- a/themes/light.css +++ b/themes/light.css @@ -1475,8 +1475,7 @@ body.ttrss_utility hr { box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.1); border: 0 solid #ddd; border-bottom-width: 1px; - background: white ! important; - opacity: 0.9; + background: rgba(255, 255, 255, 0.9) ! important; backdrop-filter: blur(6px); } body.ttrss_prefs { diff --git a/themes/light/cdm.less b/themes/light/cdm.less index 4cbfa1d28..6bb3378c1 100644 --- a/themes/light/cdm.less +++ b/themes/light/cdm.less @@ -325,8 +325,7 @@ box-shadow : 0 1px 1px -1px rgba(0,0,0,0.1); border: 0 solid @border-default; border-bottom-width: 1px; - background : @default-bg ! important; - opacity: 0.9; + background : fade(@default-bg, 90%) ! important; backdrop-filter: blur(6px); } } diff --git a/themes/night.css b/themes/night.css index 24288e149..447ca6f7f 100644 --- a/themes/night.css +++ b/themes/night.css @@ -1476,8 +1476,7 @@ body.ttrss_utility hr { box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.1); border: 0 solid #222; border-bottom-width: 1px; - background: #333 ! important; - opacity: 0.9; + background: rgba(51, 51, 51, 0.9) ! important; backdrop-filter: blur(6px); } body.ttrss_prefs { diff --git a/themes/night_blue.css b/themes/night_blue.css index 209484935..7a6ce2b69 100644 --- a/themes/night_blue.css +++ b/themes/night_blue.css @@ -1476,8 +1476,7 @@ body.ttrss_utility hr { box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.1); border: 0 solid #222; border-bottom-width: 1px; - background: #333 ! important; - opacity: 0.9; + background: rgba(51, 51, 51, 0.9) ! important; backdrop-filter: blur(6px); } body.ttrss_prefs { |