diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index b2b670b04..ca100c7f7 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -81,6 +81,7 @@ public function index(): TemplateResponse 'oldestFirst', 'showAll', 'disableRefresh', + 'titleFilterRegex', 'displaymode', 'splitmode', 'starredOpenState' diff --git a/lib/Service/FeedServiceV2.php b/lib/Service/FeedServiceV2.php index 8c78fb268..1a955080b 100644 --- a/lib/Service/FeedServiceV2.php +++ b/lib/Service/FeedServiceV2.php @@ -29,6 +29,7 @@ use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\DoesNotExistException; use OCP\IAppConfig; +use OCP\Config\IUserConfig; use OCA\News\Db\Feed; use OCA\News\Db\Item; @@ -84,6 +85,7 @@ class FeedServiceV2 extends Service * @param HtmlSanitizer $purifier HTML Sanitizer * @param LoggerInterface $logger Logger * @param IAppConfig $config App config + * @param IUserConfig $userConfig User config */ public function __construct( FeedMapperV2 $mapper, @@ -93,6 +95,7 @@ public function __construct( HtmlSanitizer $purifier, LoggerInterface $logger, IAppConfig $config, + IUserConfig $userConfig, AppData $appData ) { parent::__construct($mapper, $logger); @@ -102,6 +105,7 @@ public function __construct( $this->explorer = $explorer; $this->purifier = $purifier; $this->config = $config; + $this->userConfig = $userConfig; $this->appData = $appData; } @@ -282,6 +286,25 @@ public function create( return $this->mapper->insert($feed); } + /** + * Get regex pattern for filtering article titles + * + * @param String $userId UserId for configuration access + * + * @return valid regex pattern or empty string if invalid + */ + private function getTitleFilterRegex(String $userId): String + { + $pattern = $this->userConfig->getValueString($userId, 'news', 'titleFilterRegex'); + if (empty($pattern)) { + return ''; + } + if (@preg_match($pattern, '') === false) { + $this->logger->warning('Pattern in titleFilterRegex is not valid: {pattern}', [ 'pattern' => $pattern ]); + return ''; + } + return $pattern; + } /** * Update a feed @@ -384,7 +407,14 @@ public function fetch(Entity $feed): Entity $feed->setFaviconLink($fetchedFavicon); } + $filterBy = $this->getTitleFilterRegex($feed->getUserId()); foreach (array_reverse($items) as &$item) { + if ($item->getTitle() !== null && !empty($filterBy) && preg_match($filterBy, $item->getTitle())) { + $this->logger->info('Item filtered: matched by = {filterBy} title = {title}', [ 'title' => $item->getTitle(), 'filterBy' => $filterBy ]); + continue; + } + + $item->setFeedId($feed->getId()) ->setBody($this->purifier->purify($item->getBody())); diff --git a/src/components/modals/AppSettingsDialog.vue b/src/components/modals/AppSettingsDialog.vue index cdab2ad26..9709e334d 100644 --- a/src/components/modals/AppSettingsDialog.vue +++ b/src/components/modals/AppSettingsDialog.vue @@ -22,6 +22,10 @@ v-model="disableRefresh" :label="t('news', 'Disable automatic refresh')" /> + @@ -218,6 +222,7 @@ import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' import NcRadioGroup from '@nextcloud/vue/components/NcRadioGroup' import NcRadioGroupButton from '@nextcloud/vue/components/NcRadioGroupButton' +import NcTextField from '@nextcloud/vue/components/NcTextField' import DownloadIcon from 'vue-material-design-icons/Download.vue' import UploadIcon from 'vue-material-design-icons/Upload.vue' import { DISPLAY_MODE, SPLIT_MODE } from '../../enums/index.ts' @@ -240,6 +245,7 @@ export default defineComponent({ NcNoteCard, NcRadioGroup, NcRadioGroupButton, + NcTextField, DownloadIcon, UploadIcon, }, @@ -353,6 +359,16 @@ export default defineComponent({ }, }, + titleFilterRegex: { + get() { + return this.$store.getters.titleFilterRegex + }, + + set(newValue) { + this.saveSetting('titleFilterRegex', newValue) + }, + }, + uploadOpmlStatusMessage() { return this.$store.getters.lastOpmlImportMessage?.message }, diff --git a/src/store/app.ts b/src/store/app.ts index 8901432bb..a251f8ec3 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -18,6 +18,7 @@ export type AppInfoState = { preventReadOnScroll: boolean showAll: boolean disableRefresh: boolean + titleFilterRegex: string lastViewedFeedId: string lastViewedFeedType: string starredOpenState: boolean @@ -34,6 +35,7 @@ const state: AppInfoState = reactive({ preventReadOnScroll: loadState('news', 'preventReadOnScroll', null) === '1', showAll: loadState('news', 'showAll', null) === '1', disableRefresh: loadState('news', 'disableRefresh', null) === '1', + titleFilterRegex: loadState('news', 'titleFilterRegex', ''), lastViewedFeedId: loadState('news', 'lastViewedFeedId', '0'), lastViewedFeedType: loadState('news', 'lastViewedFeedType', '6'), starredOpenState: loadState('news', 'starredOpenState', null) === '1', @@ -71,6 +73,9 @@ const getters = { disableRefresh(state: AppInfoState) { return state.disableRefresh }, + titleFilterRegex(state: AppInfoState) { + return state.titleFilterRegex + }, lastViewedFeedId(state: AppInfoState) { return state.lastViewedFeedId }, @@ -149,6 +154,12 @@ export const mutations = { ) { state.disableRefresh = value }, + titleFilterRegex( + state: AppInfoState, + { value }: { value: string }, + ) { + state.titleFilterRegex = value + }, starredOpenState( state: AppInfoState, { value }: { value: boolean }, diff --git a/tests/Unit/Service/FeedServiceTest.php b/tests/Unit/Service/FeedServiceTest.php index 786dad4d4..2010527e5 100644 --- a/tests/Unit/Service/FeedServiceTest.php +++ b/tests/Unit/Service/FeedServiceTest.php @@ -27,6 +27,7 @@ use OCA\News\Utility\HtmlSanitizer; use OCP\AppFramework\Db\DoesNotExistException; use OCP\IAppConfig; +use OCP\IUserConfig; use OCA\News\Db\Feed; use OCA\News\Db\Item; @@ -129,11 +130,15 @@ protected function setUp(): void ->getMockBuilder(IAppConfig::class) ->disableOriginalConstructor() ->getMock(); + $this->userConfig = $this + ->getMockBuilder(IUserConfig::class) + ->disableOriginalConstructor() + ->getMock(); $this->appData = $this ->getMockBuilder(AppData::class) ->disableOriginalConstructor() ->getMock(); - + $this->class = new FeedServiceV2( $this->mapper, $this->fetcher, @@ -142,6 +147,7 @@ protected function setUp(): void $this->purifier, $this->logger, $this->config, + $this->userConfig, $this->appData ); $this->uid = 'jack';