diff options
| author | 2023-12-15 23:04:29 +0100 | |
|---|---|---|
| committer | 2023-12-15 23:04:29 +0100 | |
| commit | 6bb45a87268157aab961a6a4a728d9a9bbe043b0 (patch) | |
| tree | d1c36638d5ee61e2e663d214d724a71f07a89354 /app/Models | |
| parent | a3ed8269132303eebc03d3e6df822f1f101fa95b (diff) | |
Add filter actions (auto mark read) at category and global levels (#5942)
* Add filter actions (auto mark read) at category level
fix https://github.com/FreshRSS/FreshRSS/issues/3497
* Add filter actions (auto mark read) at global level
fix https://github.com/FreshRSS/FreshRSS/issues/2788
* Fix feed category ID
* Minor comment
Diffstat (limited to 'app/Models')
| -rw-r--r-- | app/Models/AttributesTrait.php | 40 | ||||
| -rw-r--r-- | app/Models/Category.php | 35 | ||||
| -rw-r--r-- | app/Models/CategoryDAO.php | 11 | ||||
| -rw-r--r-- | app/Models/Entry.php | 80 | ||||
| -rw-r--r-- | app/Models/Feed.php | 153 | ||||
| -rw-r--r-- | app/Models/FeedDAO.php | 2 | ||||
| -rw-r--r-- | app/Models/FilterActionsTrait.php | 147 | ||||
| -rw-r--r-- | app/Models/Tag.php | 33 | ||||
| -rw-r--r-- | app/Models/UserConfiguration.php | 21 |
9 files changed, 244 insertions, 278 deletions
diff --git a/app/Models/AttributesTrait.php b/app/Models/AttributesTrait.php new file mode 100644 index 000000000..39154182b --- /dev/null +++ b/app/Models/AttributesTrait.php @@ -0,0 +1,40 @@ +<?php +declare(strict_types=1); + +/** + * Logic to work with (JSON) attributes (for entries, feeds, categories, tags...). + */ +trait FreshRSS_AttributesTrait { + /** + * @var array<string,mixed> + */ + private array $attributes = []; + + /** + * @phpstan-return ($key is non-empty-string ? mixed : array<string,mixed>) + * @return array<string,mixed>|mixed|null + */ + public function attributes(string $key = '') { + if ($key === '') { + return $this->attributes; + } else { + return $this->attributes[$key] ?? null; + } + } + + /** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */ + public function _attributes(string $key, $value = null): void { + if ($key == '') { + if (is_string($value)) { + $value = json_decode($value, true); + } + if (is_array($value)) { + $this->attributes = $value; + } + } elseif ($value === null) { + unset($this->attributes[$key]); + } else { + $this->attributes[$key] = $value; + } + } +} diff --git a/app/Models/Category.php b/app/Models/Category.php index ab08a5b74..b1e35650a 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -2,6 +2,7 @@ declare(strict_types=1); class FreshRSS_Category extends Minz_Model { + use FreshRSS_AttributesTrait, FreshRSS_FilterActionsTrait; /** * Normal @@ -22,21 +23,21 @@ class FreshRSS_Category extends Minz_Model { private ?array $feeds = null; /** @var bool|int */ private $hasFeedsWithError = false; - /** @var array<string,mixed> */ - private array $attributes = []; private int $lastUpdate = 0; private bool $error = false; /** * @param array<FreshRSS_Feed>|null $feeds */ - public function __construct(string $name = '', ?array $feeds = null) { + public function __construct(string $name = '', int $id = 0, ?array $feeds = null) { + $this->_id($id); $this->_name($name); if ($feeds !== null) { $this->_feeds($feeds); $this->nbFeeds = 0; $this->nbNotRead = 0; foreach ($feeds as $feed) { + $feed->_category($this); $this->nbFeeds++; $this->nbNotRead += $feed->nbNotRead(); $this->hasFeedsWithError |= $feed->inError(); @@ -120,18 +121,6 @@ class FreshRSS_Category extends Minz_Model { return (bool)($this->hasFeedsWithError); } - /** - * @phpstan-return ($key is non-empty-string ? mixed : array<string,mixed>) - * @return array<string,mixed>|mixed|null - */ - public function attributes(string $key = '') { - if ($key === '') { - return $this->attributes; - } else { - return $this->attributes[$key] ?? null; - } - } - public function _id(int $id): void { $this->id = $id; if ($id === FreshRSS_CategoryDAO::DEFAULTCATEGORYID) { @@ -169,22 +158,6 @@ class FreshRSS_Category extends Minz_Model { $this->sortFeeds(); } - /** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */ - public function _attributes(string $key, $value): void { - if ('' === $key) { - if (is_string($value)) { - $value = json_decode($value, true); - } - if (is_array($value)) { - $this->attributes = $value; - } - } elseif (null === $value) { - unset($this->attributes[$key]); - } else { - $this->attributes[$key] = $value; - } - } - /** * @param array<string> $attributes * @throws FreshRSS_Context_Exception diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index 2477d0ea2..20347e4f2 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -351,8 +351,7 @@ SQL; $def_cat = $this->searchById(self::DEFAULTCATEGORYID); if ($def_cat == null) { - $cat = new FreshRSS_Category(_t('gen.short.default_category')); - $cat->_id(self::DEFAULTCATEGORYID); + $cat = new FreshRSS_Category(_t('gen.short.default_category'), self::DEFAULTCATEGORYID); $sql = 'INSERT INTO `_category`(id, name) VALUES(?, ?)'; if ($this->pdo->dbType() === 'pgsql') { @@ -441,9 +440,9 @@ SQL; // End of the current category, we add it to the $list $cat = new FreshRSS_Category( $previousLine['c_name'], + $previousLine['c_id'], $feedDao::daoToFeed($feedsDao, $previousLine['c_id']) ); - $cat->_id($previousLine['c_id']); $cat->_kind($previousLine['c_kind']); $cat->_attributes('', $previousLine['c_attributes'] ?? '[]'); $list[(int)$previousLine['c_id']] = $cat; @@ -459,9 +458,9 @@ SQL; if ($previousLine != null) { $cat = new FreshRSS_Category( $previousLine['c_name'], + $previousLine['c_id'], $feedDao::daoToFeed($feedsDao, $previousLine['c_id']) ); - $cat->_id($previousLine['c_id']); $cat->_kind($previousLine['c_kind']); $cat->_lastUpdate($previousLine['c_last_update'] ?? 0); $cat->_error($previousLine['c_error'] ?? 0); @@ -482,9 +481,9 @@ SQL; foreach ($listDAO as $dao) { FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'kind', 'lastUpdate', 'error']); $cat = new FreshRSS_Category( - $dao['name'] + $dao['name'], + $dao['id'] ); - $cat->_id($dao['id']); $cat->_kind($dao['kind']); $cat->_lastUpdate($dao['lastUpdate'] ?? 0); $cat->_error($dao['error'] ?? 0); diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 476c5a8cf..186b1f166 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -2,6 +2,8 @@ declare(strict_types=1); class FreshRSS_Entry extends Minz_Model { + use FreshRSS_AttributesTrait; + public const STATE_READ = 1; public const STATE_NOT_READ = 2; public const STATE_ALL = 3; @@ -26,8 +28,6 @@ class FreshRSS_Entry extends Minz_Model { private ?FreshRSS_Feed $feed; /** @var array<string> */ private array $tags = []; - /** @var array<string,mixed> */ - private array $attributes = []; /** * @param int|string $pubdate @@ -396,34 +396,6 @@ HTML; } } - /** - * @phpstan-return ($key is non-empty-string ? mixed : array<string,mixed>) - * @return array<string,mixed>|mixed|null - */ - public function attributes(string $key = '') { - if ($key === '') { - return $this->attributes; - } else { - return $this->attributes[$key] ?? null; - } - } - - /** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */ - public function _attributes(string $key, $value): void { - if ($key == '') { - if (is_string($value)) { - $value = json_decode($value, true); - } - if (is_array($value)) { - $this->attributes = $value; - } - } elseif ($value === null) { - unset($this->attributes[$key]); - } else { - $this->attributes[$key] = $value; - } - } - public function hash(): string { if ($this->hash == '') { //Do not include $this->date because it may be automatically generated when lacking @@ -660,40 +632,26 @@ HTML; /** @param array<string,bool> $titlesAsRead */ public function applyFilterActions(array $titlesAsRead = []): void { - if ($this->feed != null) { - if (!$this->isRead()) { - if ($this->feed->attributes('read_upon_reception') || - ($this->feed->attributes('read_upon_reception') === null && FreshRSS_Context::$user_conf->mark_when['reception'])) { - $this->_isRead(true); - Minz_ExtensionManager::callHook('entry_auto_read', $this, 'upon_reception'); - } - if (!empty($titlesAsRead[$this->title()])) { - Minz_Log::debug('Mark title as read: ' . $this->title()); - $this->_isRead(true); - Minz_ExtensionManager::callHook('entry_auto_read', $this, 'same_title_in_feed'); - } + if ($this->feed === null) { + return; + } + if (!$this->isRead()) { + if ($this->feed->attributes('read_upon_reception') || + ($this->feed->attributes('read_upon_reception') === null && FreshRSS_Context::$user_conf->mark_when['reception'])) { + $this->_isRead(true); + Minz_ExtensionManager::callHook('entry_auto_read', $this, 'upon_reception'); } - foreach ($this->feed->filterActions() as $filterAction) { - if ($this->matches($filterAction->booleanSearch())) { - foreach ($filterAction->actions() as $action) { - switch ($action) { - case 'read': - if (!$this->isRead()) { - $this->_isRead(true); - Minz_ExtensionManager::callHook('entry_auto_read', $this, 'filter'); - } - break; - case 'star': - $this->_isFavorite(true); - break; - case 'label': - //TODO: Implement more actions - break; - } - } - } + if (!empty($titlesAsRead[$this->title()])) { + Minz_Log::debug('Mark title as read: ' . $this->title()); + $this->_isRead(true); + Minz_ExtensionManager::callHook('entry_auto_read', $this, 'same_title_in_feed'); } } + FreshRSS_Context::$user_conf->applyFilterActions($this); + if ($this->feed->category() !== null) { + $this->feed->category()->applyFilterActions($this); + } + $this->feed->applyFilterActions($this); } public function isDay(int $day, int $today): bool { diff --git a/app/Models/Feed.php b/app/Models/Feed.php index a379f5016..dbe6aaa73 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -2,6 +2,7 @@ declare(strict_types=1); class FreshRSS_Feed extends Minz_Model { + use FreshRSS_AttributesTrait, FreshRSS_FilterActionsTrait; /** * Normal RSS or Atom feed @@ -42,7 +43,7 @@ class FreshRSS_Feed extends Minz_Model { private int $id = 0; private string $url = ''; private int $kind = 0; - private int $categoryId = 1; + private int $categoryId = 0; private ?FreshRSS_Category $category; private int $nbEntries = -1; private int $nbNotRead = -1; @@ -55,15 +56,11 @@ class FreshRSS_Feed extends Minz_Model { private string $httpAuth = ''; private bool $error = false; private int $ttl = self::TTL_DEFAULT; - /** @var array<string,mixed> */ - private array $attributes = []; private bool $mute = false; private string $hash = ''; private string $lockPath = ''; private string $hubUrl = ''; private string $selfUrl = ''; - /** @var array<FreshRSS_FilterAction>|null $filterActions */ - private ?array $filterActions = null; public function __construct(string $url, bool $validate = true) { if ($validate) { @@ -105,7 +102,7 @@ class FreshRSS_Feed extends Minz_Model { } public function category(): ?FreshRSS_Category { - if ($this->category === null) { + if ($this->category === null && $this->categoryId > 0) { $catDAO = FreshRSS_Factory::createCategoryDao(); $this->category = $catDAO->searchById($this->categoryId); } @@ -113,6 +110,9 @@ class FreshRSS_Feed extends Minz_Model { } public function categoryId(): int { + if ($this->category !== null) { + return $this->category->id() ?: $this->categoryId; + } return $this->categoryId; } @@ -186,18 +186,6 @@ class FreshRSS_Feed extends Minz_Model { return $this->ttl; } - /** - * @phpstan-return ($key is non-empty-string ? mixed : array<string,mixed>) - * @return array<string,mixed>|mixed|null - */ - public function attributes(string $key = '') { - if ($key === '') { - return $this->attributes; - } else { - return $this->attributes[$key] ?? null; - } - } - public function mute(): bool { return $this->mute; } @@ -325,22 +313,6 @@ class FreshRSS_Feed extends Minz_Model { $this->mute = $value < self::TTL_DEFAULT; } - /** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */ - public function _attributes(string $key, $value): void { - if ($key == '') { - if (is_string($value)) { - $value = json_decode($value, true); - } - if (is_array($value)) { - $this->attributes = $value; - } - } elseif ($value === null) { - unset($this->attributes[$key]); - } else { - $this->attributes[$key] = $value; - } - } - public function _nbNotRead(int $value): void { $this->nbNotRead = $value; } @@ -871,119 +843,6 @@ class FreshRSS_Feed extends Minz_Model { return @unlink($this->lockPath); } - /** - * @return array<FreshRSS_FilterAction> - */ - public function filterActions(): array { - if (empty($this->filterActions)) { - $this->filterActions = []; - $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; - } - - /** - * @param array<FreshRSS_FilterAction>|null $filterActions - */ - private function _filterActions(?array $filterActions): void { - $this->filterActions = $filterActions; - if (is_array($this->filterActions) && !empty($this->filterActions)) { - $this->_attributes('filters', array_map(static function (?FreshRSS_FilterAction $af) { - return $af == null ? null : $af->toJSON(); - }, $this->filterActions)); - } else { - $this->_attributes('filters', null); - } - } - - /** @return array<FreshRSS_BooleanSearch> */ - public function filtersAction(string $action): array { - $action = trim($action); - if ($action == '') { - return []; - } - $filters = []; - $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; - } - - /** - * @param array<string> $filters - */ - public function _filtersAction(string $action, array $filters): void { - $action = trim($action); - if ($action == '') { - return; - } - $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($filterActions, $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([ - 'search' => $filter, - 'actions' => [$action], - ]); - if ($filterAction != null) { - $filterActions[] = $filterAction; - } - } - } - - if (empty($filterActions)) { - $filterActions = null; - } - $this->_filterActions($filterActions); - } - //<WebSub> public function pubSubHubbubEnabled(): bool { diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 055ec60e4..ac844217a 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -373,7 +373,7 @@ SQL; * @return array<FreshRSS_Feed> */ public function listFeedsOrderUpdate(int $defaultCacheDuration = 3600, int $limit = 0): array { - $sql = 'SELECT id, url, kind, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes, `cache_nbEntries`, `cache_nbUnreads` ' + $sql = 'SELECT id, url, kind, category, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes, `cache_nbEntries`, `cache_nbUnreads` ' . 'FROM `_feed` ' . ($defaultCacheDuration < 0 ? '' : 'WHERE ttl >= ' . FreshRSS_Feed::TTL_DEFAULT . ' AND `lastUpdate` < (' . (time() + 60) diff --git a/app/Models/FilterActionsTrait.php b/app/Models/FilterActionsTrait.php new file mode 100644 index 000000000..869992b21 --- /dev/null +++ b/app/Models/FilterActionsTrait.php @@ -0,0 +1,147 @@ +<?php +declare(strict_types=1); + +/** + * Logic to apply filter actions (for feeds, categories, user configuration...). + */ +trait FreshRSS_FilterActionsTrait { + + /** @var array<FreshRSS_FilterAction>|null $filterActions */ + private ?array $filterActions = null; + + /** + * @return array<FreshRSS_FilterAction> + */ + private function filterActions(): array { + if (empty($this->filterActions)) { + $this->filterActions = []; + $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; + } + + /** + * @param array<FreshRSS_FilterAction>|null $filterActions + */ + private function _filterActions(?array $filterActions): void { + $this->filterActions = $filterActions; + if (is_array($this->filterActions) && !empty($this->filterActions)) { + $this->_attributes('filters', array_map(static function (?FreshRSS_FilterAction $af) { + return $af == null ? null : $af->toJSON(); + }, $this->filterActions)); + } else { + $this->_attributes('filters', null); + } + } + + /** @return array<FreshRSS_BooleanSearch> */ + public function filtersAction(string $action): array { + $action = trim($action); + if ($action == '') { + return []; + } + $filters = []; + $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; + } + + /** + * @param array<string> $filters + */ + public function _filtersAction(string $action, array $filters): void { + $action = trim($action); + if ($action === '') { + return; + } + $filters = array_unique(array_map('trim', $filters), SORT_STRING); + $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($filterActions, $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([ + 'search' => $filter, + 'actions' => [$action], + ]); + if ($filterAction != null) { + $filterActions[] = $filterAction; + } + } + } + + if (empty($filterActions)) { + $filterActions = null; + } + $this->_filterActions($filterActions); + } + + public function applyFilterActions(FreshRSS_Entry $entry): void { + foreach ($this->filterActions() as $filterAction) { + if ($entry->matches($filterAction->booleanSearch())) { + foreach ($filterAction->actions() as $action) { + switch ($action) { + case 'read': + if (!$entry->isRead()) { + $entry->_isRead(true); + Minz_ExtensionManager::callHook('entry_auto_read', $entry, 'filter'); + } + break; + case 'star': + $entry->_isFavorite(true); + break; + case 'label': + //TODO: Implement more actions + break; + } + } + } + } + } +} diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 0e50763d0..cedc4647d 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -2,13 +2,10 @@ declare(strict_types=1); class FreshRSS_Tag extends Minz_Model { + use FreshRSS_AttributesTrait; private int $id = 0; private string $name; - /** - * @var array<string,mixed> - */ - private array $attributes = []; private int $nbEntries = -1; private int $nbUnread = -1; @@ -35,34 +32,6 @@ class FreshRSS_Tag extends Minz_Model { $this->name = trim($value); } - /** - * @phpstan-return ($key is non-empty-string ? mixed : array<string,mixed>) - * @return array<string,mixed>|mixed|null - */ - public function attributes(string $key = '') { - if ($key === '') { - return $this->attributes; - } else { - return $this->attributes[$key] ?? null; - } - } - - /** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */ - public function _attributes(string $key, $value = null): void { - if ($key == '') { - if (is_string($value)) { - $value = json_decode($value, true); - } - if (is_array($value)) { - $this->attributes = $value; - } - } elseif ($value === null) { - unset($this->attributes[$key]); - } else { - $this->attributes[$key] = $value; - } - } - public function nbEntries(): int { if ($this->nbEntries < 0) { $tagDAO = FreshRSS_Factory::createTagDao(); diff --git a/app/Models/UserConfiguration.php b/app/Models/UserConfiguration.php index d75c76bcb..0aec3a05f 100644 --- a/app/Models/UserConfiguration.php +++ b/app/Models/UserConfiguration.php @@ -72,10 +72,31 @@ declare(strict_types=1); * @property array<string,mixed> $volatile */ final class FreshRSS_UserConfiguration extends Minz_Configuration { + use FreshRSS_FilterActionsTrait; /** @throws Minz_ConfigurationNamespaceException */ public static function init(string $config_filename, ?string $default_filename = null): FreshRSS_UserConfiguration { parent::register('user', $config_filename, $default_filename); return parent::get('user'); } + + /** + * @phpstan-return ($key is non-empty-string ? mixed : array<string,mixed>) + * @return array<string,mixed>|mixed|null + */ + public function attributes(string $key = '') { + if ($key === '') { + return []; // Not implemented for user configuration + } else { + return parent::param($key, null); + } + } + + /** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */ + public function _attributes(string $key, $value = null): void { + if ($key == '') { + return; // Not implemented for user configuration + } + parent::_param($key, $value); + } } |
