Explorar o código

Filter actions (#2275)

* Draft of filter actions

* Travis

* Implement UI + finish logic

* Travis
Alexandre Alapetite %!s(int64=7) %!d(string=hai) anos
pai
achega
1804c0e0bc

+ 11 - 16
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);
 		}
 	}

+ 3 - 1
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();

+ 113 - 0
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) {

+ 104 - 0
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);
+	}
+
 	//<WebSub>
 
 	public function pubSubHubbubEnabled() {

+ 45 - 0
app/Models/FilterAction.php

@@ -0,0 +1,45 @@
+<?php
+
+class FreshRSS_FilterAction {
+
+	private $booleanSearch = null;
+	private $actions = null;
+
+	private function __construct($booleanSearch, $actions) {
+		$this->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;
+	}
+}

+ 4 - 0
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 <em>%s</em>.',

+ 4 - 0
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 <em>%s</em> eingefügt.',

+ 4 - 0
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 <em>%s</em>.',

+ 4 - 0
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 <em>%s</em>.',

+ 4 - 0
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 <em>%s</em>.',

+ 4 - 0
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' => 'כאשר הקטגוריה נמחקת ההזנות שבתוכה אוטומטית מקוטלגות תחת  <em>%s</em>.',

+ 4 - 0
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 <em>%s</em>.',

+ 4 - 0
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' => '카테고리를 삭제하면, 해당 카테고리 아래에 있던 피드들은 자동적으로 <em>%s</em> 아래로 분류됩니다.',

+ 4 - 0
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 <em>%s</em>.',

+ 4 - 0
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 <em>%s</em>.',

+ 4 - 0
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 <em>%s</em>.',

+ 4 - 0
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 <em>%s</em>.',	//TODO - Translation

+ 4 - 0
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 <em>%s</em> içerisine yerleşir.',

+ 4 - 0
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 源会自动归类到 <em>%s</em>',

+ 13 - 0
app/views/helpers/feed/update.phtml

@@ -234,6 +234,19 @@
 		</div>
 		<?php } ?>
 
+		<legend><?php echo _t('sub.feed.filteractions'); ?></legend>
+		<div class="form-group">
+			<label class="group-name" for="filteractions_read"><?php echo _t('conf.reading.read.when'); ?></label>
+			<div class="group-controls">
+				<textarea name="filteractions_read" id="filteractions_read"><?php
+					foreach ($this->feed->filtersAction('read') as $filterRead) {
+						echo htmlspecialchars($filterRead->getRawInput(), ENT_NOQUOTES, 'UTF-8'), "\n\n";
+					}
+				?></textarea>
+				<?php echo _i('help'); ?> <?php echo _t('sub.feed.filteractions.help'); ?>
+			</div>
+		</div>
+
 		<div class="form-group form-actions">
 			<div class="group-controls">
 				<button type="submit" class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>