From 4888f919f104b2d170302565e481a0b731eb4145 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Tue, 25 Dec 2018 01:30:28 +0100 Subject: Prepare for batch mark as read --- app/Models/EntryDAO.php | 2 +- app/Models/TagDAO.php | 48 +++++++++++++++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 18 deletions(-) (limited to 'app/Models') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 6d77a33cd..9ae1ed797 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -383,7 +383,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { */ public function markRead($ids, $is_read = true) { FreshRSS_UserDAO::touch(); - if (is_array($ids)) { //Many IDs at once (used by API) + if (is_array($ids)) { //Many IDs at once if (count($ids) < 6) { //Speed heuristics $affected = 0; foreach ($ids as $id) { diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php index b55d2b35d..0b4428f17 100644 --- a/app/Models/TagDAO.php +++ b/app/Models/TagDAO.php @@ -256,42 +256,56 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } } - //For API - public function getEntryIdsTagNames($entries) { - $sql = 'SELECT et.id_entry, t.name ' + public function getTagsForEntries($entries) { + $sql = 'SELECT et.id_entry, et.id_tag, t.name ' . 'FROM `' . $this->prefix . 'tag` t ' . 'INNER JOIN `' . $this->prefix . 'entrytag` et ON et.id_tag = t.id'; $values = array(); if (is_array($entries) && count($entries) > 0) { $sql .= ' AND et.id_entry IN (' . str_repeat('?,', count($entries) - 1). '?)'; - foreach ($entries as $entry) { - $values[] = is_array($entry) ? $entry['id'] : $entry->id(); + if (is_array($entries[0])) { + foreach ($entries as $entry) { + $values[] = $entry['id']; + } + } elseif (is_object($entries[0])) { + foreach ($entries as $entry) { + $values[] = $entry->id(); + } + } else { + foreach ($entries as $entry) { + $values[] = $entry; + } } } $stm = $this->bd->prepare($sql); if ($stm && $stm->execute($values)) { - $result = array(); - foreach ($stm->fetchAll(PDO::FETCH_ASSOC) as $line) { - $entryId = 'e_' . $line['id_entry']; - $tagName = $line['name']; - if (empty($result[$entryId])) { - $result[$entryId] = array(); - } - $result[$entryId][] = $tagName; - } - return $result; + return $stm->fetchAll(PDO::FETCH_ASSOC); } else { $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); if ($this->autoUpdateDb($info)) { - return $this->getTagNamesEntryIds($id_entry); + return $this->getTagsForEntries($entries); } - Minz_Log::error('SQL error getTagNamesEntryIds: ' . $info[2]); + Minz_Log::error('SQL error getTagsForEntries: ' . $info[2]); return false; } } + //For API + public function getEntryIdsTagNames($entries) { + $result = array(); + foreach ($this->getTagsForEntries($entries) as $line) { + $entryId = 'e_' . $line['id_entry']; + $tagName = $line['name']; + if (empty($result[$entryId])) { + $result[$entryId] = array(); + } + $result[$entryId][] = $tagName; + } + return $result; + } + public static function daoToTag($listDAO) { $list = array(); if (!is_array($listDAO)) { -- cgit v1.2.3 From 9cc72c0c4860c96d9d82310e4bb76a9233fd2961 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 3 Feb 2019 21:24:06 +0100 Subject: Fix EntryDAO tags warning for Fever API https://github.com/FreshRSS/FreshRSS/issues/2239 --- app/Models/EntryDAO.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/Models') diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 21f17c097..93d1183c9 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -1065,7 +1065,7 @@ SQL; $dao['date'], $dao['is_read'], $dao['is_favorite'], - $dao['tags'] + isset($dao['tags']) ? $dao['tags'] : '' ); if (isset($dao['id'])) { $entry->_id($dao['id']); -- cgit v1.2.3 From 566799075155546392316b1e4e352bdfc9e3602b Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 9 Mar 2019 15:43:44 +0100 Subject: Change nextGet behaviour Fixes https://github.com/FreshRSS/FreshRSS/issues/2206 Related to https://github.com/FreshRSS/FreshRSS/pull/2255 --- app/Models/Context.php | 42 +++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 25 deletions(-) (limited to 'app/Models') diff --git a/app/Models/Context.php b/app/Models/Context.php index 60ec6ff77..95dc47c8c 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -252,37 +252,29 @@ class FreshRSS_Context { $found_current_get = false; switch ($get[0]) { case 'f': - // We search the next feed with at least one unread article in - // same category as the currend feed. + // We search the next unread feed with the following priorities: next in same category, or previous in same category, or next, or previous. foreach (self::$categories as $cat) { - if ($cat->id() != self::$current_get['category']) { - // We look into the category of the current feed! - continue; - } - + $sameCat = false; foreach ($cat->feeds() as $feed) { - if ($feed->id() == self::$current_get['feed']) { - // Here is our current feed! Fine, the next one will - // be a potential candidate. + if ($found_current_get) { + if ($feed->nbNotRead() > 0) { + $another_unread_id = $feed->id(); + break 2; + } + } elseif ($feed->id() == self::$current_get['feed']) { $found_current_get = true; - continue; - } - - if ($feed->nbNotRead() > 0) { + } elseif ($feed->nbNotRead() > 0) { $another_unread_id = $feed->id(); - if ($found_current_get) { - // We have found our current feed and now we - // have an feed with unread articles. Leave the - // loop! - break; - } + $sameCat = true; } } - break; + if ($found_current_get && $sameCat) { + break; + } } - // If no feed have been found, next_get is the current category. - self::$next_get = empty($another_unread_id) ? 'c_' . self::$current_get['category'] : 'f_' . $another_unread_id; + // If there is no more unread feed, show main stream + self::$next_get = $another_unread_id == '' ? 'a' : 'f_' . $another_unread_id; break; case 'c': // We search the next category with at least one unread article. @@ -304,8 +296,8 @@ class FreshRSS_Context { } } - // No unread category? The main stream will be our destination! - self::$next_get = empty($another_unread_id) ? 'a' : 'c_' . $another_unread_id; + // If there is no more unread category, show main stream + self::$next_get = $another_unread_id == '' ? 'a' : 'c_' . $another_unread_id; break; } } -- cgit v1.2.3 From 834ffacce22ff6a2c0f1459476dc4a45e8ea06f9 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Tue, 19 Mar 2019 20:14:31 +0100 Subject: No old ID (#2276) * No old ID https://github.com/FreshRSS/FreshRSS/issues/2273 * PostgreSQL insert or ignore --- app/Controllers/feedController.php | 10 +++------- app/Controllers/importExportController.php | 2 +- app/Models/EntryDAOPGSQL.php | 4 +++- lib/lib_rss.php | 7 +------ 4 files changed, 8 insertions(+), 15 deletions(-) (limited to 'app/Models') diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index d5ae235ad..7f36e0388 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -377,17 +377,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $read_upon_reception = $feed->attributes('read_upon_reception') !== null ? ( $feed->attributes('read_upon_reception') ) : FreshRSS_Context::$user_conf->mark_when['reception']; - if ($isNewFeed) { - $id = min(time(), $entry_date) . uSecString(); - $entry->_isRead($read_upon_reception); - } elseif ($entry_date < $date_min) { - $id = min(time(), $entry_date) . uSecString(); + $id = uTimeString(); + $entry->_id($id); + if ($entry_date < $date_min) { $entry->_isRead(true); //Old article that was not in database. Probably an error, so mark as read } else { - $id = uTimeString(); $entry->_isRead($read_upon_reception); } - $entry->_id($id); $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); if ($entry === null) { diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 80b9868ec..1d7176929 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -585,7 +585,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $feed_id, $item['id'], $item['title'], $author, $content, $url, $published, $is_read, $is_starred ); - $entry->_id(min(time(), $entry->date(true)) . uSecString()); + $entry->_id(uTimeString()); $entry->_tags($tags); if (isset($newGuids[$entry->guid()])) { diff --git a/app/Models/EntryDAOPGSQL.php b/app/Models/EntryDAOPGSQL.php index aef258b6f..e571e457f 100644 --- a/app/Models/EntryDAOPGSQL.php +++ b/app/Models/EntryDAOPGSQL.php @@ -37,7 +37,9 @@ BEGIN INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) (SELECT rank + row_number() OVER(ORDER BY date) AS id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `' . $this->prefix . 'entrytmp` AS etmp - WHERE NOT EXISTS (SELECT 1 FROM `' . $this->prefix . 'entry` AS ereal WHERE etmp.id_feed = ereal.id_feed AND etmp.guid = ereal.guid) + WHERE NOT EXISTS ( + SELECT 1 FROM `' . $this->prefix . 'entry` AS ereal + WHERE (etmp.id = ereal.id) OR (etmp.id_feed = ereal.id_feed AND etmp.guid = ereal.guid)) ORDER BY date); DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= maxrank; END $$;'; diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 89e9ddfea..bff59d5cc 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -303,12 +303,7 @@ function lazyimg($content) { function uTimeString() { $t = @gettimeofday(); - return $t['sec'] . str_pad($t['usec'], 6, '0'); -} - -function uSecString() { - $t = @gettimeofday(); - return str_pad($t['usec'], 6, '0'); + return $t['sec'] . str_pad($t['usec'], 6, '0', STR_PAD_LEFT); } function invalidateHttpCache($username = '') { -- cgit v1.2.3 From e84a90943ab1e4a254b2d33c7cabef18b718b456 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Wed, 20 Mar 2019 17:52:31 +0100 Subject: Session fix when form + HTTP auth are used (#2286) https://github.com/Alkarex/FreshRSS/commit/bf51c82d55f6bf1af2a6464ca4f148d6c613d28f https://github.com/FreshRSS/FreshRSS/issues/2125#issuecomment-473873922 --- app/Models/Auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/Models') diff --git a/app/Models/Auth.php b/app/Models/Auth.php index 513a9cb2f..16a506f00 100644 --- a/app/Models/Auth.php +++ b/app/Models/Auth.php @@ -13,7 +13,7 @@ class FreshRSS_Auth { * This method initializes authentication system. */ public static function init() { - if (Minz_Session::param('REMOTE_USER', '') !== httpAuthUser()) { + if (isset($_SESSION['REMOTE_USER']) && $_SESSION['REMOTE_USER'] !== httpAuthUser()) { //HTTP REMOTE_USER has changed self::removeAccess(); } -- cgit v1.2.3 From ebd8c31c0272f135b1b55f0480d1c8c3875935fe Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Fri, 22 Mar 2019 19:05:38 +0100 Subject: Rework CSRF interaction with sessions (#2290) * Rework CSRF interaction with sessions Fix https://github.com/FreshRSS/FreshRSS/issues/2288 Improve security in some edge cases Maybe relevant for https://github.com/FreshRSS/FreshRSS/issues/2125#issuecomment-474992671 * Forgotten mime type --- app/Controllers/authController.php | 6 +++++- app/Controllers/userController.php | 1 + app/FreshRSS.php | 32 ++++++++++++++++++++------------ app/Models/Auth.php | 8 ++++---- p/scripts/main.js | 28 ++++++++++++++++++++++++++-- 5 files changed, 56 insertions(+), 19 deletions(-) (limited to 'app/Models') diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php index 75d4acae0..ca44b1a96 100644 --- a/app/Controllers/authController.php +++ b/app/Controllers/authController.php @@ -69,7 +69,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { * the user is already connected. */ public function loginAction() { - if (FreshRSS_Auth::hasAccess()) { + if (FreshRSS_Auth::hasAccess() && Minz_Request::param('u', '') == '') { Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); } @@ -133,6 +133,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { // Set session parameter to give access to the user. Minz_Session::_param('currentUser', $username); Minz_Session::_param('passwordHash', $conf->passwordHash); + Minz_Session::_param('csrf'); FreshRSS_Auth::giveAccess(); // Set cookie parameter if nedded. @@ -161,6 +162,8 @@ class FreshRSS_auth_Controller extends Minz_ActionController { return; } + FreshRSS_FormAuth::deleteCookie(); + $conf = get_user_configuration($username); if ($conf == null) { return; @@ -176,6 +179,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { if ($ok) { Minz_Session::_param('currentUser', $username); Minz_Session::_param('passwordHash', $s); + Minz_Session::_param('csrf'); FreshRSS_Auth::giveAccess(); Minz_Request::good(_t('feedback.auth.login.success'), diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index 71172b9ef..be3787561 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -247,6 +247,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { $user_conf = get_user_configuration($new_user_name); Minz_Session::_param('currentUser', $new_user_name); Minz_Session::_param('passwordHash', $user_conf->passwordHash); + Minz_Session::_param('csrf'); FreshRSS_Auth::giveAccess(); } diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 1dc80718e..ecf13e4cf 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -57,18 +57,26 @@ class FreshRSS extends Minz_FrontController { private static function initAuth() { FreshRSS_Auth::init(); - if (Minz_Request::isPost() && !(is_referer_from_same_domain() && FreshRSS_Auth::isCsrfOk())) { - // Basic protection against XSRF attacks - FreshRSS_Auth::removeAccess(); - $http_referer = empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER']; - Minz_Translate::init('en'); //TODO: Better choice of fallback language - Minz_Error::error( - 403, - array('error' => array( - _t('feedback.access.denied'), - ' [HTTP_REFERER=' . htmlspecialchars($http_referer, ENT_NOQUOTES, 'UTF-8') . ']' - )) - ); + if (Minz_Request::isPost()) { + if (!is_referer_from_same_domain()) { + // Basic protection against XSRF attacks + FreshRSS_Auth::removeAccess(); + $http_referer = empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER']; + Minz_Translate::init('en'); //TODO: Better choice of fallback language + Minz_Error::error(403, array('error' => array( + _t('feedback.access.denied'), + ' [HTTP_REFERER=' . htmlspecialchars($http_referer, ENT_NOQUOTES, 'UTF-8') . ']' + ))); + } + if ((!FreshRSS_Auth::isCsrfOk()) && + (Minz_Request::controllerName() !== 'auth' || Minz_Request::actionName() !== 'login')) { + // Token-based protection against XSRF attacks, except for the login form itself + Minz_Translate::init('en'); //TODO: Better choice of fallback language + Minz_Error::error(403, array('error' => array( + _t('feedback.access.denied'), + ' [CSRF]' + ))); + } } } diff --git a/app/Models/Auth.php b/app/Models/Auth.php index 16a506f00..6d079a01f 100644 --- a/app/Models/Auth.php +++ b/app/Models/Auth.php @@ -24,6 +24,7 @@ class FreshRSS_Auth { $conf = Minz_Configuration::get('system'); $current_user = $conf->default_user; Minz_Session::_param('currentUser', $current_user); + Minz_Session::_param('csrf'); } if (self::$login_ok) { @@ -56,6 +57,7 @@ class FreshRSS_Auth { $current_user = trim($credentials[0]); Minz_Session::_param('currentUser', $current_user); Minz_Session::_param('passwordHash', trim($credentials[1])); + Minz_Session::_param('csrf'); } return $current_user != ''; case 'http_auth': @@ -63,6 +65,7 @@ class FreshRSS_Auth { $login_ok = $current_user != '' && FreshRSS_UserDAO::exists($current_user); if ($login_ok) { Minz_Session::_param('currentUser', $current_user); + Minz_Session::_param('csrf'); } return $login_ok; case 'none': @@ -196,13 +199,10 @@ class FreshRSS_Auth { } public static function isCsrfOk($token = null) { $csrf = Minz_Session::param('csrf'); - if ($csrf == '') { - return true; //Not logged in yet - } if ($token === null) { $token = Minz_Request::fetchPOST('_csrf'); } - return $token === $csrf; + return $token != '' && $token === $csrf; } } diff --git a/p/scripts/main.js b/p/scripts/main.js index 521adc839..212bf804b 100644 --- a/p/scripts/main.js +++ b/p/scripts/main.js @@ -44,6 +44,12 @@ var context; }()); // +function badAjax() { + openNotification(context.i18n.notif_request_failed, 'bad'); + location.reload(); + return true; +} + function needsScroll(elem) { const winBottom = document.documentElement.scrollTop + document.documentElement.clientHeight, elemTop = elem.offsetParent.offsetTop + elem.offsetTop, @@ -165,6 +171,9 @@ function send_mark_read_queue(queue, asRead) { for (let i = queue.length - 1; i >= 0; i--) { delete pending_entries['flux_' + queue[i]]; } + if (this.status == 403) { + badAjax(); + } }; req.onload = function (e) { if (this.status != 200) { @@ -269,6 +278,9 @@ function mark_favorite(div) { req.onerror = function (e) { openNotification(context.i18n.notif_request_failed, 'bad'); delete pending_entries[div.id]; + if (this.status == 403) { + badAjax(); + } }; req.onload = function (e) { if (this.status != 200) { @@ -918,6 +930,9 @@ function init_stream(stream) { req.responseType = 'json'; req.onerror = function (e) { checkboxTag.checked = !isChecked; + if (this.status == 403) { + badAjax(); + } }; req.onload = function (e) { if (this.status != 200) { @@ -1008,6 +1023,9 @@ function updateFeed(feeds, feeds_count) { const req = new XMLHttpRequest(); req.open('POST', feed.url, true); req.onloadend = function (e) { + if (this.status != 200) { + return badAjax(); + } feed_processed++; const div = document.getElementById('actualizeProgress'); div.querySelector('.progress').innerHTML = feed_processed + ' / ' + feeds_count; @@ -1045,9 +1063,12 @@ function init_actualize() { context.ajax_loading = true; const req = new XMLHttpRequest(); - req.open('GET', './?c=javascript&a=actualize', true); + req.open('POST', './?c=javascript&a=actualize', true); req.responseType = 'json'; req.onload = function (e) { + if (this.status != 200) { + return badAjax(); + } const json = xmlHttpRequestJson(this); if (auto && json.feeds.length < 1) { auto = false; @@ -1078,7 +1099,10 @@ function init_actualize() { updateFeed(json.feeds, feeds_count); } }; - req.send(); + req.setRequestHeader('Content-Type', 'application/json'); + req.send(JSON.stringify({ + _csrf: context.csrf, + })); return false; }; -- cgit v1.2.3 From e7a57915f9c90c144d95918048d2523418866921 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 23 Mar 2019 12:08:35 +0100 Subject: CLI user-info fixes (#2292) * CLI missing exit codes https://github.com/FreshRSS/FreshRSS/issues/2291 * CLI catch for outdated databases https://github.com/FreshRSS/FreshRSS/issues/2291#issuecomment-475856890 --- app/Models/TagDAO.php | 14 +++++++++++--- cli/list-users.php | 2 ++ cli/user-info.php | 2 ++ 3 files changed, 15 insertions(+), 3 deletions(-) (limited to 'app/Models') diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php index 0b4428f17..297d24c96 100644 --- a/app/Models/TagDAO.php +++ b/app/Models/TagDAO.php @@ -187,9 +187,17 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable { public function count() { $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'tag`'; $stm = $this->bd->prepare($sql); - $stm->execute(); - $res = $stm->fetchAll(PDO::FETCH_ASSOC); - return $res[0]['count']; + if ($stm && $stm->execute()) { + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + return $res[0]['count']; + } else { + $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); + if ($this->autoUpdateDb($info)) { + return $this->count(); + } + Minz_Log::error('SQL error TagDAO::count: ' . $info[2]); + return false; + } } public function countEntries($id) { diff --git a/cli/list-users.php b/cli/list-users.php index 758bbdb46..b3ce9936f 100755 --- a/cli/list-users.php +++ b/cli/list-users.php @@ -13,3 +13,5 @@ if (FreshRSS_Context::$system_conf->default_user !== '' foreach ($users as $user) { echo $user, "\n"; } + +done(); diff --git a/cli/user-info.php b/cli/user-info.php index 043bebf7c..125408c10 100755 --- a/cli/user-info.php +++ b/cli/user-info.php @@ -51,3 +51,5 @@ foreach ($users as $username) { "\n"; } } + +done(); -- cgit v1.2.3 From 1804c0e0bc095487b9a1ad13cbc9f55f6cef2a2a Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 23 Mar 2019 22:52:47 +0100 Subject: Filter actions (#2275) * Draft of filter actions * Travis * Implement UI + finish logic * Travis --- app/Controllers/feedController.php | 27 +++---- app/Controllers/subscriptionController.php | 4 +- app/Models/Entry.php | 113 +++++++++++++++++++++++++++++ app/Models/Feed.php | 104 ++++++++++++++++++++++++++ app/Models/FilterAction.php | 45 ++++++++++++ app/i18n/cz/sub.php | 4 + app/i18n/de/sub.php | 4 + app/i18n/en/sub.php | 4 + app/i18n/es/sub.php | 4 + app/i18n/fr/sub.php | 4 + app/i18n/he/sub.php | 4 + app/i18n/it/sub.php | 4 + app/i18n/kr/sub.php | 4 + app/i18n/nl/sub.php | 4 + app/i18n/oc/sub.php | 4 + app/i18n/pt-br/sub.php | 4 + app/i18n/ru/sub.php | 4 + app/i18n/tr/sub.php | 4 + app/i18n/zh-cn/sub.php | 4 + app/views/helpers/feed/update.phtml | 13 ++++ 20 files changed, 345 insertions(+), 17 deletions(-) create mode 100644 app/Models/FilterAction.php (limited to 'app/Models') diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 7f36e0388..0aed9b0a1 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -289,7 +289,8 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } $ttl = $feed->ttl(); if ((!$simplePiePush) && (!$feed_id) && - ($feed->lastUpdate() + 10 >= time() - ($ttl == FreshRSS_Feed::TTL_DEFAULT ? FreshRSS_Context::$user_conf->ttl_default : $ttl))) { + ($feed->lastUpdate() + 10 >= time() - ( + $ttl == FreshRSS_Feed::TTL_DEFAULT ? FreshRSS_Context::$user_conf->ttl_default : $ttl))) { //Too early to refresh from source, but check whether the feed was updated by another user $mtime = $feed->cacheModifiedTime(); if ($feed->lastUpdate() + 10 >= $mtime) { @@ -347,8 +348,8 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $entry_date = $entry->date(true); if (isset($existingHashForGuids[$entry->guid()])) { $existingHash = $existingHashForGuids[$entry->guid()]; - if (strcasecmp($existingHash, $entry->hash()) === 0 || trim($existingHash, '0') == '') { - //This entry already exists and is unchanged. TODO: Remove the test with the zero'ed hash in FreshRSS v1.3 + if (strcasecmp($existingHash, $entry->hash()) === 0) { + //This entry already exists and is unchanged. $oldGuids[] = $entry->guid(); } else { //This entry already exists but has been updated //Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->url(false) . @@ -374,17 +375,14 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // This entry should not be added considering configuration and date. $oldGuids[] = $entry->guid(); } else { - $read_upon_reception = $feed->attributes('read_upon_reception') !== null ? ( - $feed->attributes('read_upon_reception') - ) : FreshRSS_Context::$user_conf->mark_when['reception']; $id = uTimeString(); $entry->_id($id); if ($entry_date < $date_min) { $entry->_isRead(true); //Old article that was not in database. Probably an error, so mark as read - } else { - $entry->_isRead($read_upon_reception); } + $entry->applyFilterActions(); + $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); if ($entry === null) { // An extension has returned a null value, there is nothing to insert. @@ -392,7 +390,8 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } if ($pubSubHubbubEnabled && !$simplePiePush) { //We use push, but have discovered an article by pull! - $text = 'An article was discovered by pull although we use PubSubHubbub!: Feed ' . $url . ' GUID ' . $entry->guid(); + $text = 'An article was discovered by pull although we use PubSubHubbub!: Feed ' . $url . + ' GUID ' . $entry->guid(); Minz_Log::warning($text, PSHB_LOG); Minz_Log::warning($text); $pubSubHubbubEnabled = false; @@ -416,9 +415,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $entryDAO->beginTransaction(); } - $nb = $entryDAO->cleanOldEntries($feed->id(), - $date_min, - max($feed_history, count($entries) + 10)); + $nb = $entryDAO->cleanOldEntries($feed->id(), $date_min, max($feed_history, count($entries) + 10)); if ($nb > 0) { $needFeedCacheRefresh = true; Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url(false) . ']'); @@ -598,11 +595,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController { if (self::moveFeed($feed_id, $cat_id)) { // TODO: return something useful // Log a notice to prevent "Empty IF statement" warning in PHP_CodeSniffer - Minz_Log::notice('Moved feed `' . $feed_id . '` ' . - 'in the category `' . $cat_id . '`');; + Minz_Log::notice('Moved feed `' . $feed_id . '` in the category `' . $cat_id . '`'); } else { - Minz_Log::warning('Cannot move feed `' . $feed_id . '` ' . - 'in the category `' . $cat_id . '`'); + Minz_Log::warning('Cannot move feed `' . $feed_id . '` in the category `' . $cat_id . '`'); Minz_Error::error(404); } } diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php index 62fb3d384..9cf41ed0b 100644 --- a/app/Controllers/subscriptionController.php +++ b/app/Controllers/subscriptionController.php @@ -110,6 +110,8 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { $feed->_attributes('timeout', null); } + $feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::param('filteractions_read', ''))); + $values = array( 'name' => Minz_Request::param('name', ''), 'description' => sanitizeHTML(Minz_Request::param('description', '', true)), @@ -121,7 +123,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { 'httpAuth' => $httpAuth, 'keep_history' => intval(Minz_Request::param('keep_history', FreshRSS_Feed::KEEP_HISTORY_DEFAULT)), 'ttl' => $ttl * ($mute ? -1 : 1), - 'attributes' => $feed->attributes() + 'attributes' => $feed->attributes(), ); invalidateHttpCache(); diff --git a/app/Models/Entry.php b/app/Models/Entry.php index f2f3d08fe..3bb977283 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -185,6 +185,119 @@ class FreshRSS_Entry extends Minz_Model { $this->tags = $value; } + public function matches($booleanSearch) { + if (!$booleanSearch || count($booleanSearch->searches()) <= 0) { + return true; + } + foreach ($booleanSearch->searches() as $filter) { + $ok = true; + if ($ok && $filter->getMinPubdate()) { + $ok &= $this->date >= $filter->getMinPubdate(); + } + if ($ok && $filter->getMaxPubdate()) { + $ok &= $this->date <= $filter->getMaxPubdate(); + } + if ($ok && $filter->getMinDate()) { + $ok &= strnatcmp($this->id, $filter->getMinDate() . '000000') >= 0; + } + if ($ok && $filter->getMaxDate()) { + $ok &= strnatcmp($this->id, $filter->getMaxDate() . '000000') <= 0; + } + if ($ok && $filter->getInurl()) { + foreach ($filter->getInurl() as $url) { + $ok &= stripos($this->link, $url) !== false; + } + } + if ($ok && $filter->getNotInurl()) { + foreach ($filter->getNotInurl() as $url) { + $ok &= stripos($this->link, $url) === false; + } + } + if ($ok && $filter->getAuthor()) { + foreach ($filter->getAuthor() as $author) { + $ok &= stripos($this->authors, $author) !== false; + } + } + if ($ok && $filter->getNotAuthor()) { + foreach ($filter->getNotAuthor() as $author) { + $ok &= stripos($this->authors, $author) === false; + } + } + if ($ok && $filter->getIntitle()) { + foreach ($filter->getIntitle() as $title) { + $ok &= stripos($this->title, $title) !== false; + } + } + if ($ok && $filter->getNotIntitle()) { + foreach ($filter->getNotIntitle() as $title) { + $ok &= stripos($this->title, $title) === false; + } + } + if ($ok && $filter->getTags()) { + foreach ($filter->getTags() as $tag2) { + $found = false; + foreach ($this->tags as $tag1) { + if (strcasecmp($tag1, $tag2) === 0) { + $found = true; + } + } + $ok &= $found; + } + } + if ($ok && $filter->getNotTags()) { + foreach ($filter->getNotTags() as $tag2) { + $found = false; + foreach ($this->tags as $tag1) { + if (strcasecmp($tag1, $tag2) === 0) { + $found = true; + } + } + $ok &= !$found; + } + } + if ($ok && $filter->getSearch()) { + foreach ($filter->getSearch() as $needle) { + $ok &= (stripos($this->title, $needle) !== false || stripos($this->content, $needle) !== false); + } + } + if ($ok && $filter->getNotSearch()) { + foreach ($filter->getNotSearch() as $needle) { + $ok &= (stripos($this->title, $needle) === false && stripos($this->content, $needle) === false); + } + } + if ($ok) { + return true; + } + } + return false; + } + + public function applyFilterActions() { + if ($this->feed != null) { + if ($this->feed->attributes('read_upon_reception') || + ($this->feed->attributes('read_upon_reception') === null && FreshRSS_Context::$user_conf->mark_when['reception'])) { + $this->_isRead(true); + } + foreach ($this->feed->filterActions() as $filterAction) { + if ($this->matches($filterAction->booleanSearch())) { + foreach ($filterAction->actions() as $action => $params) { + switch ($action) { + case 'read': + $this->_isRead(true); + break; + case 'star': + $this->_is_favorite(true); + break; + case 'label': + //TODO: Implement more actions + break; + } + } + } + } + } + } + public function isDay($day, $today) { $date = $this->dateAdded(true); switch ($day) { diff --git a/app/Models/Feed.php b/app/Models/Feed.php index b21a8bbbe..89989236c 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -32,6 +32,7 @@ class FreshRSS_Feed extends Minz_Model { private $lockPath = ''; private $hubUrl = ''; private $selfUrl = ''; + private $filterActions = null; public function __construct($url, $validate = true) { if ($validate) { @@ -498,6 +499,109 @@ class FreshRSS_Feed extends Minz_Model { @unlink($this->lockPath); } + public function filterActions() { + if ($this->filterActions == null) { + $this->filterActions = array(); + $filters = $this->attributes('filters'); + if (is_array($filters)) { + foreach ($filters as $filter) { + $filterAction = FreshRSS_FilterAction::fromJSON($filter); + if ($filterAction != null) { + $this->filterActions[] = $filterAction; + } + } + } + } + return $this->filterActions; + } + + private function _filterActions($filterActions) { + $this->filterActions = $filterActions; + if (is_array($this->filterActions) && !empty($this->filterActions)) { + $this->_attributes('filters', array_map(function ($af) { + return $af == null ? null : $af->toJSON(); + }, $this->filterActions)); + } else { + $this->_attributes('filters', null); + } + } + + public function filtersAction($action) { + $action = trim($action); + if ($action == '') { + return array(); + } + $filters = array(); + $filterActions = $this->filterActions(); + for ($i = count($filterActions) - 1; $i >= 0; $i--) { + $filterAction = $filterActions[$i]; + if ($filterAction != null && $filterAction->booleanSearch() != null && + $filterAction->actions() != null && in_array($action, $filterAction->actions(), true)) { + $filters[] = $filterAction->booleanSearch(); + } + } + return $filters; + } + + public function _filtersAction($action, $filters) { + $action = trim($action); + if ($action == '' || !is_array($filters)) { + return false; + } + $filters = array_unique(array_map('trim', $filters)); + $filterActions = $this->filterActions(); + + //Check existing filters + for ($i = count($filterActions) - 1; $i >= 0; $i--) { + $filterAction = $filterActions[$i]; + if ($filterAction == null || !is_array($filterAction->actions()) || + $filterAction->booleanSearch() == null || trim($filterAction->booleanSearch()->getRawInput()) == '') { + array_splice($filterAction, $i, 1); + continue; + } + $actions = $filterAction->actions(); + //Remove existing rules with same action + for ($j = count($actions) - 1; $j >= 0; $j--) { + if ($actions[$j] === $action) { + array_splice($actions, $j, 1); + } + } + //Update existing filter with new action + for ($k = count($filters) - 1; $k >= 0; $k --) { + $filter = $filters[$k]; + if ($filter === $filterAction->booleanSearch()->getRawInput()) { + $actions[] = $action; + array_splice($filters, $k, 1); + } + } + //Save result + if (empty($actions)) { + array_splice($filterActions, $i, 1); + } else { + $filterAction->_actions($actions); + } + } + + //Add new filters + for ($k = count($filters) - 1; $k >= 0; $k --) { + $filter = $filters[$k]; + if ($filter != '') { + $filterAction = FreshRSS_FilterAction::fromJSON(array( + 'search' => $filter, + 'actions' => array($action), + )); + if ($filterAction != null) { + $filterActions[] = $filterAction; + } + } + } + + if (empty($filterActions)) { + $filterActions = null; + } + $this->_filterActions($filterActions); + } + // public function pubSubHubbubEnabled() { diff --git a/app/Models/FilterAction.php b/app/Models/FilterAction.php new file mode 100644 index 000000000..23a45d14e --- /dev/null +++ b/app/Models/FilterAction.php @@ -0,0 +1,45 @@ +booleanSearch = $booleanSearch; + $this->_actions($actions); + } + + public function booleanSearch() { + return $this->booleanSearch; + } + + public function actions() { + return $this->actions; + } + + public function _actions($actions) { + if (is_array($actions)) { + $this->actions = array_unique($actions); + } else { + $this->actions = null; + } + } + + public function toJSON() { + if (is_array($this->actions) && $this->booleanSearch != null) { + return array( + 'search' => $this->booleanSearch->getRawInput(), + 'actions' => $this->actions, + ); + } + return ''; + } + + public static function fromJSON($json) { + if (!empty($json['search']) && !empty($json['actions']) && is_array($json['actions'])) { + return new FreshRSS_FilterAction(new FreshRSS_BooleanSearch($json['search']), $json['actions']); + } + return null; + } +} diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php index 5b5634fed..2e81c928d 100644 --- a/app/i18n/cz/sub.php +++ b/app/i18n/cz/sub.php @@ -33,6 +33,10 @@ return array( 'description' => 'Popis', 'empty' => 'Kanál je prázdný. Ověřte prosím zda je ještě autorem udržován.', 'error' => 'Vyskytl se problém s kanálem. Ověřte že je vždy dostupný, prosím, a poté jej aktualizujte.', + 'filteractions' => array( + '_' => 'Filter actions', //TODO - Translation + 'help' => 'Write one search filter per line.', //TODO - Translation + ), 'informations' => 'Informace', 'keep_history' => 'Zachovat tento minimální počet článků', 'moved_category_deleted' => 'Po smazání kategorie budou v ní obsažené kanály automaticky přesunuty do %s.', diff --git a/app/i18n/de/sub.php b/app/i18n/de/sub.php index 27e893177..bd050671e 100644 --- a/app/i18n/de/sub.php +++ b/app/i18n/de/sub.php @@ -33,6 +33,10 @@ return array( 'description' => 'Beschreibung', 'empty' => 'Dieser Feed ist leer. Bitte stellen Sie sicher, dass er noch gepflegt wird.', 'error' => 'Dieser Feed ist auf ein Problem gestoßen. Bitte stellen Sie sicher, dass er immer lesbar ist und aktualisieren Sie ihn dann.', + 'filteractions' => array( + '_' => 'Filter actions', //TODO - Translation + 'help' => 'Write one search filter per line.', //TODO - Translation + ), 'informations' => 'Information', 'keep_history' => 'Minimale Anzahl an Artikeln, die behalten wird', 'moved_category_deleted' => 'Wenn Sie eine Kategorie entfernen, werden deren Feeds automatisch in die Kategorie %s eingefügt.', diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php index 4efd81ba4..f11eb9b99 100644 --- a/app/i18n/en/sub.php +++ b/app/i18n/en/sub.php @@ -33,6 +33,10 @@ return array( 'description' => 'Description', 'empty' => 'This feed is empty. Please verify that it is still maintained.', 'error' => 'This feed has encountered a problem. Please verify that it is always reachable then update it.', + 'filteractions' => array( + '_' => 'Filter actions', + 'help' => 'Write one search filter per line.', + ), 'informations' => 'Information', 'keep_history' => 'Minimum number of articles to keep', 'moved_category_deleted' => 'When you delete a category, its feeds are automatically classified under %s.', diff --git a/app/i18n/es/sub.php b/app/i18n/es/sub.php index 854984891..c0526106f 100755 --- a/app/i18n/es/sub.php +++ b/app/i18n/es/sub.php @@ -33,6 +33,10 @@ return array( 'description' => 'Descripción', 'empty' => 'La fuente está vacía. Por favor, verifica que siga activa.', 'error' => 'Hay un problema con esta fuente. Por favor, veritica que esté disponible y prueba de nuevo.', + 'filteractions' => array( + '_' => 'Filter actions', //TODO - Translation + 'help' => 'Write one search filter per line.', //TODO - Translation + ), 'informations' => 'Información', 'keep_history' => 'Número mínimo de artículos a conservar', 'moved_category_deleted' => 'Al borrar una categoría todas sus fuentes pasan automáticamente a la categoría %s.', diff --git a/app/i18n/fr/sub.php b/app/i18n/fr/sub.php index d9964ac6e..b71019faa 100644 --- a/app/i18n/fr/sub.php +++ b/app/i18n/fr/sub.php @@ -33,6 +33,10 @@ return array( 'description' => 'Description', 'empty' => 'Ce flux est vide. Veuillez vérifier qu’il est toujours maintenu.', 'error' => 'Ce flux a rencontré un problème. Veuillez vérifier qu’il est toujours accessible puis actualisez-le.', + 'filteractions' => array( + '_' => 'Filtres d’action', + 'help' => 'Écrivez une recherche par ligne.', + ), 'informations' => 'Informations', 'keep_history' => 'Nombre minimum d’articles à conserver', 'moved_category_deleted' => 'Lors de la suppression d’une catégorie, ses flux seront automatiquement classés dans %s.', diff --git a/app/i18n/he/sub.php b/app/i18n/he/sub.php index 6d824e349..bb2025bc3 100644 --- a/app/i18n/he/sub.php +++ b/app/i18n/he/sub.php @@ -33,6 +33,10 @@ return array( 'description' => 'תיאור', 'empty' => 'הזנה זו ריקה. אנא ודאו שהיא עדיין מתוחזקת.', 'error' => 'הזנה זו נתקלה בשגיאה, אנא ודאו שהיא תקינה ואז נסו שנית.', + 'filteractions' => array( + '_' => 'Filter actions', //TODO - Translation + 'help' => 'Write one search filter per line.', //TODO - Translation + ), 'informations' => 'מידע', 'keep_history' => 'מסםר מינימלי של מאמרים לשמור', 'moved_category_deleted' => 'כאשר הקטגוריה נמחקת ההזנות שבתוכה אוטומטית מקוטלגות תחת %s.', diff --git a/app/i18n/it/sub.php b/app/i18n/it/sub.php index ff7fa6f1d..bf279e059 100644 --- a/app/i18n/it/sub.php +++ b/app/i18n/it/sub.php @@ -33,6 +33,10 @@ return array( 'description' => 'Descrizione', 'empty' => 'Questo feed non contiene articoli. Per favore verifica il sito direttamente.', 'error' => 'Questo feed ha generato un errore. Per favore verifica se ancora disponibile.', + 'filteractions' => array( + '_' => 'Filter actions', //TODO - Translation + 'help' => 'Write one search filter per line.', //TODO - Translation + ), 'informations' => 'Informazioni', 'keep_history' => 'Numero minimo di articoli da mantenere', 'moved_category_deleted' => 'Cancellando una categoria i feed al suo interno verranno classificati automaticamente come %s.', diff --git a/app/i18n/kr/sub.php b/app/i18n/kr/sub.php index 9edd85818..151775c1c 100644 --- a/app/i18n/kr/sub.php +++ b/app/i18n/kr/sub.php @@ -33,6 +33,10 @@ return array( 'description' => '설명', 'empty' => '이 피드는 비어있습니다. 피드가 계속 운영되고 있는지 확인하세요.', 'error' => '이 피드에 문제가 발생했습니다. 이 피드에 접근 권한이 있는지 확인하세요.', + 'filteractions' => array( + '_' => 'Filter actions', //TODO - Translation + 'help' => 'Write one search filter per line.', //TODO - Translation + ), 'informations' => '정보', 'keep_history' => '최소 유지 글 개수', 'moved_category_deleted' => '카테고리를 삭제하면, 해당 카테고리 아래에 있던 피드들은 자동적으로 %s 아래로 분류됩니다.', diff --git a/app/i18n/nl/sub.php b/app/i18n/nl/sub.php index 1d6c9f806..8ba9c020d 100644 --- a/app/i18n/nl/sub.php +++ b/app/i18n/nl/sub.php @@ -33,6 +33,10 @@ return array( 'description' => 'Omschrijving', 'empty' => 'Deze feed is leeg. Controleer of deze nog actueel is.', 'error' => 'Deze feed heeft problemen. Verifieer a.u.b het doeladres en actualiseer het.', + 'filteractions' => array( + '_' => 'Filter actions', //TODO - Translation + 'help' => 'Write one search filter per line.', //TODO - Translation + ), 'informations' => 'Informatie', 'keep_history' => 'Minimum aantal artikelen om te houden', 'moved_category_deleted' => 'Als u een categorie verwijderd, worden de feeds automatisch geclassificeerd onder %s.', diff --git a/app/i18n/oc/sub.php b/app/i18n/oc/sub.php index fc5a0cc1f..51ee0d43f 100644 --- a/app/i18n/oc/sub.php +++ b/app/i18n/oc/sub.php @@ -32,6 +32,10 @@ return array( 'description' => 'Descripcion', 'empty' => 'Aqueste flux es void. Assegurats-vos qu’es totjorn mantengut.', 'error' => 'Aqueste flux a rescontrat un problèma. Volgatz verificar que siá totjorn accessible puèi actualizatz-lo.', + 'filteractions' => array( + '_' => 'Filter actions', //TODO - Translation + 'help' => 'Write one search filter per line.', //TODO - Translation + ), 'informations' => 'Informacions', 'keep_history' => 'Nombre minimum d’articles de servar', 'moved_category_deleted' => 'Quand escafatz una categoria, sos fluxes son automaticament classats dins %s.', diff --git a/app/i18n/pt-br/sub.php b/app/i18n/pt-br/sub.php index 58b2fc1f9..fc26e89e7 100644 --- a/app/i18n/pt-br/sub.php +++ b/app/i18n/pt-br/sub.php @@ -33,6 +33,10 @@ return array( 'description' => 'Descrição', 'empty' => 'Este feed está vazio. Por favor verifique ele ainda é mantido.', 'error' => 'Este feed encontra-se com problema. Por favor verifique se ele ainda está disponível e atualize-o.', + 'filteractions' => array( + '_' => 'Filter actions', //TODO - Translation + 'help' => 'Write one search filter per line.', //TODO - Translation + ), 'informations' => 'Informações', 'keep_history' => 'Número mínimo de artigos para manter', 'moved_category_deleted' => 'Quando você deleta uma categoria, seus feeds são automaticamente classificados como %s.', diff --git a/app/i18n/ru/sub.php b/app/i18n/ru/sub.php index 62f8a8e3a..e125d549e 100644 --- a/app/i18n/ru/sub.php +++ b/app/i18n/ru/sub.php @@ -33,6 +33,10 @@ return array( 'description' => 'Description', //TODO - Translation 'empty' => 'This feed is empty. Please verify that it is still maintained.', //TODO - Translation 'error' => 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.', //TODO - Translation + 'filteractions' => array( + '_' => 'Filter actions', //TODO - Translation + 'help' => 'Write one search filter per line.', //TODO - Translation + ), 'informations' => 'Information', //TODO - Translation 'keep_history' => 'Minimum number of articles to keep', //TODO - Translation 'moved_category_deleted' => 'When you delete a category, its feeds are automatically classified under %s.', //TODO - Translation diff --git a/app/i18n/tr/sub.php b/app/i18n/tr/sub.php index 7f29633be..9f4945c0a 100644 --- a/app/i18n/tr/sub.php +++ b/app/i18n/tr/sub.php @@ -33,6 +33,10 @@ return array( 'description' => 'Tanım', 'empty' => 'Bu akış boş. Lütfen akışın aktif olduğuna emin olun.', 'error' => 'Bu akışda bir hatayla karşılaşıldı. Lütfen akışın sürekli ulaşılabilir olduğuna emin olun.', + 'filteractions' => array( + '_' => 'Filter actions', //TODO - Translation + 'help' => 'Write one search filter per line.', //TODO - Translation + ), 'informations' => 'Bilgi', 'keep_history' => 'En az tutulacak makale sayısı', 'moved_category_deleted' => 'Bir kategoriyi silerseniz, içerisindeki akışlar %s içerisine yerleşir.', diff --git a/app/i18n/zh-cn/sub.php b/app/i18n/zh-cn/sub.php index f84d08cdf..90f9fd942 100644 --- a/app/i18n/zh-cn/sub.php +++ b/app/i18n/zh-cn/sub.php @@ -33,6 +33,10 @@ return array( 'description' => '描述', 'empty' => '此源为空。请确认它是否正常更新。', 'error' => '此源遇到一些问题。请在确认是否能正常访问后重试。', + 'filteractions' => array( + '_' => 'Filter actions', //TODO - Translation + 'help' => 'Write one search filter per line.', //TODO - Translation + ), 'informations' => '信息', 'keep_history' => '至少保存的文章数', 'moved_category_deleted' => '删除分类时,其中的 RSS 源会自动归类到 %s', diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml index bc90ba456..be8034c0d 100644 --- a/app/views/helpers/feed/update.phtml +++ b/app/views/helpers/feed/update.phtml @@ -234,6 +234,19 @@ + +
+ +
+ + +
+
+
-- cgit v1.2.3