Browse Source

Feature/new archiving (#2335)

* Change archiving config page layout

I've changed some wording and moved actions into a
maintenance section.

* Update purge action

Now we have more control on the purge action. The configuration allows
us to choose what to keep and what to discard in a more precise way.
At the moment, the configuration applies for all feeds.

* Add purge configuration on feed level

Now the extend purge configuration is available on feed level.
It is stored as attributes and will be used in the purge action.

* Update purge action

Now the purge action uses the feed configuration if it exists and
defaults on user configuration if not.

* Add empty option in period list

* Fix configuration warnings

* Add archiving configuration on categories

See #2369

* Add user info back

* Add explanations in UI

* Fixes for SQLite + error + misc.

* Fix invalid feed reference

* Short array syntax

Only for new code, so far

* Fix prefix error

* Query performance, default values

Work in progress

* Fix default values and confirm before leaving

Form cancel and confirm changes before leaving were broken.
And start taking advantage of the short echo syntax `<?= ?>` as we have
moved to PHP 5.4+

* More work

* Tuning SQL

* Fix MariaDB + performance issue

* SQL performance

* Fix SQLite bug

* Fix some attributes JSON encoding bugs

Especially for SQLite export/import

* More uniform, fix bugs

More uniform between global, category, feed settings

* Drop special cases for old articles during refresh

Instead will use lastSeen date with the new archiving logic.
This was generating problems anyway
https://github.com/FreshRSS/FreshRSS/issues/2154

* Draft drop index keep_history

Not needed anymore

* MySQL typo

Now properly tested with MySQL, PostgreSQL, SQLite

* More work for legacy values

Important to avoid overriding user's preference and risking deleting
data erroneously

* Fix PHP 7.3 / 7.4 warnings

@aledeg "Trying to use values of type null, bool, int, float or resource
as an array (such as $null["key"]) will now generate a notice. "
https://php.net/migration74.incompatible

* Reintroduce min articles and take care of legacy parameters

* A few changes forgotten

* Draft of migration + DROP of feed.keep_history

* Fix several errors

And give up using const for SQL to allow multiple database types (and we
cannot redefine a const)

* Add keep_min to categories + factorise archiving logic

* Legacy fix

* Fix bug yield from

* Minor: Use JSON_UNESCAPED_SLASHE for attributes

And make more uniform

* Fix sign and missing variable

* Fine tune the logic
Alexis Degrugillier 6 years ago
parent
commit
cc0db9af4f
78 changed files with 1062 additions and 277 deletions
  1. 40 3
      app/Controllers/configureController.php
  2. 4 16
      app/Controllers/entryController.php
  3. 2 22
      app/Controllers/feedController.php
  4. 55 3
      app/Controllers/subscriptionController.php
  5. 24 0
      app/Models/Category.php
  6. 109 13
      app/Models/CategoryDAO.php
  7. 17 0
      app/Models/CategoryDAOSQLite.php
  8. 0 10
      app/Models/ConfigurationSetter.php
  9. 19 0
      app/Models/Context.php
  10. 6 6
      app/Models/DatabaseDAO.php
  11. 1 1
      app/Models/DatabaseDAOSQLite.php
  12. 45 20
      app/Models/EntryDAO.php
  13. 7 1
      app/Models/Factory.php
  14. 25 13
      app/Models/Feed.php
  15. 10 10
      app/Models/FeedDAO.php
  16. 1 1
      app/Models/FeedDAOSQLite.php
  17. 1 1
      app/Models/Tag.php
  18. 10 4
      app/Models/TagDAO.php
  19. 5 6
      app/Models/UserDAO.php
  20. 10 11
      app/SQL/install.sql.mysql.php
  21. 9 10
      app/SQL/install.sql.pgsql.php
  22. 8 9
      app/SQL/install.sql.sqlite.php
  23. 10 2
      app/i18n/cz/conf.php
  24. 7 0
      app/i18n/cz/gen.php
  25. 2 1
      app/i18n/cz/sub.php
  26. 10 2
      app/i18n/de/conf.php
  27. 7 0
      app/i18n/de/gen.php
  28. 2 1
      app/i18n/de/sub.php
  29. 10 2
      app/i18n/en/conf.php
  30. 7 0
      app/i18n/en/gen.php
  31. 2 1
      app/i18n/en/sub.php
  32. 10 2
      app/i18n/es/conf.php
  33. 7 0
      app/i18n/es/gen.php
  34. 2 1
      app/i18n/es/sub.php
  35. 10 2
      app/i18n/fr/conf.php
  36. 7 0
      app/i18n/fr/gen.php
  37. 2 1
      app/i18n/fr/sub.php
  38. 10 2
      app/i18n/he/conf.php
  39. 7 0
      app/i18n/he/gen.php
  40. 2 1
      app/i18n/he/sub.php
  41. 10 2
      app/i18n/it/conf.php
  42. 7 0
      app/i18n/it/gen.php
  43. 2 1
      app/i18n/it/sub.php
  44. 10 2
      app/i18n/kr/conf.php
  45. 7 0
      app/i18n/kr/gen.php
  46. 2 1
      app/i18n/kr/sub.php
  47. 12 4
      app/i18n/nl/conf.php
  48. 7 0
      app/i18n/nl/gen.php
  49. 2 1
      app/i18n/nl/sub.php
  50. 10 1
      app/i18n/oc/conf.php
  51. 7 0
      app/i18n/oc/gen.php
  52. 2 1
      app/i18n/oc/sub.php
  53. 10 2
      app/i18n/pt-br/conf.php
  54. 7 0
      app/i18n/pt-br/gen.php
  55. 2 1
      app/i18n/pt-br/sub.php
  56. 11 3
      app/i18n/ru/conf.php
  57. 7 0
      app/i18n/ru/gen.php
  58. 2 1
      app/i18n/ru/sub.php
  59. 1 1
      app/i18n/sk/conf.php
  60. 1 1
      app/i18n/sk/sub.php
  61. 10 2
      app/i18n/tr/conf.php
  62. 7 0
      app/i18n/tr/gen.php
  63. 2 1
      app/i18n/tr/sub.php
  64. 10 2
      app/i18n/zh-cn/conf.php
  65. 7 0
      app/i18n/zh-cn/gen.php
  66. 2 1
      app/i18n/zh-cn/sub.php
  67. 2 18
      app/install.php
  68. 86 24
      app/views/configure/archiving.phtml
  69. 116 0
      app/views/helpers/category/update.phtml
  70. 107 6
      app/views/helpers/feed/update.phtml
  71. 2 2
      cli/_update-or-create-user.php
  72. 8 2
      config-user.default.php
  73. 6 0
      lib/Minz/Request.php
  74. 5 1
      lib/lib_rss.php
  75. 23 0
      p/scripts/category.js
  76. 25 13
      p/scripts/extra.js
  77. 3 0
      p/themes/base-theme/template.css
  78. 0 5
      phpcs.xml

+ 40 - 3
app/Controllers/configureController.php

@@ -196,9 +196,31 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 	 */
 	public function archivingAction() {
 		if (Minz_Request::isPost()) {
-			FreshRSS_Context::$user_conf->old_entries = Minz_Request::param('old_entries', 3);
-			FreshRSS_Context::$user_conf->keep_history_default = Minz_Request::param('keep_history_default', 0);
+			if (!Minz_Request::paramBoolean('enable_keep_max')) {
+				$keepMax = false;
+			} elseif (!$keepMax = Minz_Request::param('keep_max')) {
+				$keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
+			}
+			if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) {
+				$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
+				if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) {
+					$keepPeriod = str_replace('1', Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit'));
+				}
+			} else {
+				$keepPeriod = false;
+			}
+
 			FreshRSS_Context::$user_conf->ttl_default = Minz_Request::param('ttl_default', FreshRSS_Feed::TTL_DEFAULT);
+			FreshRSS_Context::$user_conf->archiving = [
+				'keep_period' => $keepPeriod,
+				'keep_max' => $keepMax,
+				'keep_min' => Minz_Request::param('keep_min_default', 0),
+				'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'),
+				'keep_labels' => Minz_Request::paramBoolean('keep_labels'),
+				'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'),
+			];
+			FreshRSS_Context::$user_conf->keep_history_default = null;	//Legacy < FreshRSS 1.15
+			FreshRSS_Context::$user_conf->old_entries = null;	//Legacy < FreshRSS 1.15
 			FreshRSS_Context::$user_conf->save();
 			invalidateHttpCache();
 
@@ -206,7 +228,20 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			                   array('c' => 'configure', 'a' => 'archiving'));
 		}
 
-		Minz_View::prependTitle(_t('conf.archiving.title') . ' · ');
+		$volatile = [
+				'enable_keep_period' => false,
+				'keep_period_count' => '3',
+				'keep_period_unit' => 'P1M',
+			];
+		$keepPeriod = FreshRSS_Context::$user_conf->archiving['keep_period'];
+		if (preg_match('/^PT?(?P<count>\d+)[YMWDH]$/', $keepPeriod, $matches)) {
+			$volatile = [
+				'enable_keep_period' => true,
+				'keep_period_count' => $matches['count'],
+				'keep_period_unit' => str_replace($matches['count'], 1, $keepPeriod),
+			];
+		}
+		FreshRSS_Context::$user_conf->volatile = $volatile;
 
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 		$this->view->nb_total = $entryDAO->count();
@@ -217,6 +252,8 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 		if (FreshRSS_Auth::hasAccess('admin')) {
 			$this->view->size_total = $databaseDAO->size(true);
 		}
+
+		Minz_View::prependTitle(_t('conf.archiving.title') . ' · ');
 	}
 
 	/**

+ 4 - 16
app/Controllers/entryController.php

@@ -181,32 +181,20 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 	public function purgeAction() {
 		@set_time_limit(300);
 
-		$nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1);
-		$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
-
-		$entryDAO = FreshRSS_Factory::createEntryDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feeds = $feedDAO->listFeeds();
 		$nb_total = 0;
 
 		invalidateHttpCache();
 
-		foreach ($feeds as $feed) {
-			$feed_history = $feed->keepHistory();
-			if (FreshRSS_Feed::KEEP_HISTORY_DEFAULT === $feed_history) {
-				$feed_history = FreshRSS_Context::$user_conf->keep_history_default;
-			}
+		$feedDAO->beginTransaction();
 
-			if ($feed_history >= 0) {
-				$nb = $entryDAO->cleanOldEntries($feed->id(), $date_min, $feed_history);
-				if ($nb > 0) {
-					$nb_total += $nb;
-					Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url(false) . ']');
-				}
-			}
+		foreach ($feeds as $feed) {
+			$nb_total += $feed->cleanOldEntries();
 		}
 
 		$feedDAO->updateCachedValues();
+		$feedDAO->commit();
 
 		$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
 		$databaseDAO->minorDbMaintenance();

+ 2 - 22
app/Controllers/feedController.php

@@ -267,10 +267,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			$maxFeeds = 10;
 		}
 
-		// Calculate date of oldest entries we accept in DB.
-		$nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1);
-		$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
-
 		// WebSub (PubSubHubbub) support
 		$pubsubhubbubEnabledGeneral = FreshRSS_Context::$system_conf->pubsubhubbub_enabled;
 		$pshbMinAge = time() - (3600 * 24);  //TODO: Make a configuration.
@@ -323,12 +319,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				continue;
 			}
 
-			$feed_history = $feed->keepHistory();
-			if ($isNewFeed) {
-				$feed_history = FreshRSS_Feed::KEEP_HISTORY_INFINITE;
-			} elseif (FreshRSS_Feed::KEEP_HISTORY_DEFAULT === $feed_history) {
-				$feed_history = FreshRSS_Context::$user_conf->keep_history_default;
-			}
 			$needFeedCacheRefresh = false;
 
 			// We want chronological order and SimplePie uses reverse order.
@@ -376,15 +366,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 							}
 							$entryDAO->updateEntry($entry->toArray());
 						}
-					} elseif ($feed_history == 0 && $entry_date < $date_min) {
-						// This entry should not be added considering configuration and date.
-						$oldGuids[] = $entry->guid();
 					} else {
 						$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
-						}
 
 						$entry->applyFilterActions();
 
@@ -413,17 +397,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				$entryDAO->updateLastSeen($feed->id(), $oldGuids, $mtime);
 			}
 
-			if ($feed_history >= 0 && mt_rand(0, 30) === 1) {
-				// TODO: move this function in web cron when available (see entry::purge)
-				// Remove old entries once in 30.
+			if (mt_rand(0, 30) === 1) {	// Remove old entries once in 30.
 				if (!$entryDAO->inTransaction()) {
 					$entryDAO->beginTransaction();
 				}
-
-				$nb = $entryDAO->cleanOldEntries($feed->id(), $date_min, max($feed_history, count($entries) + 10));
+				$nb = $feed->cleanOldEntries();
 				if ($nb > 0) {
 					$needFeedCacheRefresh = true;
-					Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url(false) . ']');
 				}
 			}
 

+ 55 - 3
app/Controllers/subscriptionController.php

@@ -121,6 +121,32 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 				$feed->_attributes('timeout', null);
 			}
 
+			if (Minz_Request::paramBoolean('use_default_purge_options')) {
+				$feed->_attributes('archiving', null);
+			} else {
+				if (!Minz_Request::paramBoolean('enable_keep_max')) {
+					$keepMax = false;
+				} elseif (!$keepMax = Minz_Request::param('keep_max')) {
+					$keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
+				}
+				if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) {
+					$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
+					if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) {
+						$keepPeriod = str_replace(1, Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit'));
+					}
+				} else {
+					$keepPeriod = false;
+				}
+				$feed->_attributes('archiving', [
+					'keep_period' => $keepPeriod,
+					'keep_max' => $keepMax,
+					'keep_min' => intval(Minz_Request::param('keep_min', 0)),
+					'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'),
+					'keep_labels' => Minz_Request::paramBoolean('keep_labels'),
+					'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'),
+				]);
+			}
+
 			$feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::param('filteractions_read', '')));
 
 			$values = array(
@@ -132,7 +158,6 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 				'pathEntries' => Minz_Request::param('path_entries', ''),
 				'priority' => intval(Minz_Request::param('priority', FreshRSS_Feed::PRIORITY_MAIN_STREAM)),
 				'httpAuth' => $httpAuth,
-				'keep_history' => intval(Minz_Request::param('keep_history', FreshRSS_Feed::KEEP_HISTORY_DEFAULT)),
 				'ttl' => $ttl * ($mute ? -1 : 1),
 				'attributes' => $feed->attributes(),
 			);
@@ -165,9 +190,36 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 		$this->view->category = $category;
 
 		if (Minz_Request::isPost()) {
-			$values = array(
+			if (Minz_Request::paramBoolean('use_default_purge_options')) {
+				$category->_attributes('archiving', null);
+			} else {
+				if (!Minz_Request::paramBoolean('enable_keep_max')) {
+					$keepMax = false;
+				} elseif (!$keepMax = Minz_Request::param('keep_max')) {
+					$keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
+				}
+				if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) {
+					$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
+					if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) {
+						$keepPeriod = str_replace(1, Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit'));
+					}
+				} else {
+					$keepPeriod = false;
+				}
+				$category->_attributes('archiving', [
+					'keep_period' => $keepPeriod,
+					'keep_max' => $keepMax,
+					'keep_min' => intval(Minz_Request::param('keep_min', 0)),
+					'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'),
+					'keep_labels' => Minz_Request::paramBoolean('keep_labels'),
+					'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'),
+				]);
+			}
+
+			$values = [
 				'name' => Minz_Request::param('name', ''),
-			);
+				'attributes' => $category->attributes(),
+			];
 
 			invalidateHttpCache();
 

+ 24 - 0
app/Models/Category.php

@@ -8,6 +8,7 @@ class FreshRSS_Category extends Minz_Model {
 	private $feeds = null;
 	private $hasFeedsWithError = false;
 	private $isDefault = false;
+	private $attributes = [];
 
 	public function __construct($name = '', $feeds = null) {
 		$this->_name($name);
@@ -68,6 +69,14 @@ class FreshRSS_Category extends Minz_Model {
 		return $this->hasFeedsWithError;
 	}
 
+	public function attributes($key = '') {
+		if ($key == '') {
+			return $this->attributes;
+		} else {
+			return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
+		}
+	}
+
 	public function _id($id) {
 		$this->id = $id;
 		if ($id == FreshRSS_CategoryDAO::DEFAULTCATEGORYID) {
@@ -87,4 +96,19 @@ class FreshRSS_Category extends Minz_Model {
 
 		$this->feeds = $values;
 	}
+
+	public function _attributes($key, $value) {
+		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;
+		}
+	}
 }

+ 109 - 13
app/Models/CategoryDAO.php

@@ -4,15 +4,81 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 
 	const DEFAULTCATEGORYID = 1;
 
+	protected function addColumn($name) {
+		Minz_Log::warning(__method__ . ': ' . $name);
+		try {
+			if ('attributes' === $name) {	//v1.15.0
+				$ok = $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN attributes TEXT') !== false;
+
+				$stm = $this->pdo->query('SELECT * FROM `_feed`');
+				$feeds = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+				$stm = $this->pdo->prepare('UPDATE `_feed` SET attributes = :attributes WHERE id = :id');
+				foreach ($feeds as $feed) {
+					if (empty($feed['keep_history']) || empty($feed['id'])) {
+						continue;
+					}
+					$keepHistory = $feed['keep_history'];
+					$attributes = empty($feed['attributes']) ? [] : json_decode($feed['attributes'], true);
+					if (is_string($attributes)) {	//Legacy risk of double-encoding
+						$attributes = json_decode($attributes, true);
+					}
+					if (!is_array($attributes)) {
+						$attributes = [];
+					}
+					if ($keepHistory > 0) {
+						$attributes['archiving']['keep_min'] = intval($keepHistory);
+					} elseif ($keepHistory == -1) {	//Infinite
+						$attributes['archiving']['keep_period'] = false;
+						$attributes['archiving']['keep_max'] = false;
+						$attributes['archiving']['keep_min'] = false;
+					} else {
+						continue;
+					}
+					$stm->bindValue(':id', $feed['id'], PDO::PARAM_INT);
+					$stm->bindValue(':attributes', json_encode($attributes, JSON_UNESCAPED_SLASHES));
+					$stm->execute();
+				}
+
+				if ($this->pdo->dbType() !== 'sqlite') {	//SQLite does not support DROP COLUMN
+					$this->pdo->exec('ALTER TABLE `_feed` DROP COLUMN keep_history');
+				} else {
+					$this->pdo->exec('DROP INDEX IF EXISTS feed_keep_history_index');	//SQLite at least drop index
+				}
+				return $ok;
+			}
+		} catch (Exception $e) {
+			Minz_Log::error(__method__ . ': ' . $e->getMessage());
+		}
+		return false;
+	}
+
+	protected function autoUpdateDb($errorInfo) {
+		if (isset($errorInfo[0])) {
+			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
+				foreach (['attributes'] as $column) {
+					if (stripos($errorInfo[2], $column) !== false) {
+						return $this->addColumn($column);
+					}
+				}
+			}
+		}
+		return false;
+	}
+
 	public function addCategory($valuesTmp) {
-		$sql = 'INSERT INTO `_category`(name) '
-		     . 'SELECT * FROM (SELECT TRIM(?)) c2 '	//TRIM() to provide a type hint as text for PostgreSQL
+		$sql = 'INSERT INTO `_category`(name, attributes) '
+		     . 'SELECT * FROM (SELECT TRIM(?), ?) c2 '	//TRIM() to provide a type hint as text for PostgreSQL
 		     . 'WHERE NOT EXISTS (SELECT 1 FROM `_tag` WHERE name = TRIM(?))';	//No tag of the same name
 		$stm = $this->pdo->prepare($sql);
 
 		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
+		if (!isset($valuesTmp['attributes'])) {
+			$valuesTmp['attributes'] = [];
+		}
 		$values = array(
 			$valuesTmp['name'],
+			is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
 			$valuesTmp['name'],
 		);
 
@@ -20,7 +86,10 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 			return $this->pdo->lastInsertId('`_category_id_seq`');
 		} else {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
-			Minz_Log::error('SQL error addCategory: ' . $info[2]);
+			if ($this->autoUpdateDb($info)) {
+				return $this->addCategory($valuesTmp);
+			}
+			Minz_Log::error('SQL error addCategory: ' . json_encode($info));
 			return false;
 		}
 	}
@@ -39,13 +108,17 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 	}
 
 	public function updateCategory($id, $valuesTmp) {
-		$sql = 'UPDATE `_category` SET name=? WHERE id=? '
+		$sql = 'UPDATE `_category` SET name=?, attributes=? WHERE id=? '
 		     . 'AND NOT EXISTS (SELECT 1 FROM `_tag` WHERE name = ?)';	//No tag of the same name
 		$stm = $this->pdo->prepare($sql);
 
 		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
+		if (!isset($valuesTmp['attributes'])) {
+			$valuesTmp['attributes'] = [];
+		}
 		$values = array(
 			$valuesTmp['name'],
+			is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
 			$id,
 			$valuesTmp['name'],
 		);
@@ -54,7 +127,10 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 			return $stm->rowCount();
 		} else {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
-			Minz_Log::error('SQL error updateCategory: ' . $info[2]);
+			if ($this->autoUpdateDb($info)) {
+				return $this->updateCategory($valuesTmp);
+			}
+			Minz_Log::error('SQL error updateCategory: ' . json_encode($info));
 			return false;
 		}
 	}
@@ -70,16 +146,27 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 			return $stm->rowCount();
 		} else {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
-			Minz_Log::error('SQL error deleteCategory: ' . $info[2]);
+			Minz_Log::error('SQL error deleteCategory: ' . json_encode($info));
 			return false;
 		}
 	}
 
 	public function selectAll() {
-		$sql = 'SELECT id, name FROM `_category`';
+		$sql = 'SELECT id, name, attributes FROM `_category`';
 		$stm = $this->pdo->query($sql);
-		while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
-			yield $row;
+		if ($stm != false) {
+			while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
+				yield $row;
+			}
+		} else {
+			$info = $this->pdo->errorInfo();
+			if ($this->autoUpdateDb($info)) {
+				foreach ($this->selectAll() as $category) {	// `yield from` requires PHP 7+
+					yield $category;
+				}
+			}
+			Minz_Log::error(__method__ . ' error: ' . json_encode($info));
+			return false;
 		}
 	}
 
@@ -116,7 +203,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 
 	public function listCategories($prePopulateFeeds = true, $details = false) {
 		if ($prePopulateFeeds) {
-			$sql = 'SELECT c.id AS c_id, c.name AS c_name, '
+			$sql = 'SELECT c.id AS c_id, c.name AS c_name, c.attributes AS c_attributes, '
 			     . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.`cache_nbEntries`, f.`cache_nbUnreads`, f.ttl ')
 			     . 'FROM `_category` c '
 			     . 'LEFT OUTER JOIN `_feed` f ON f.category=c.id '
@@ -124,9 +211,17 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 			     . 'GROUP BY f.id, c_id '
 			     . 'ORDER BY c.name, f.name';
 			$stm = $this->pdo->prepare($sql);
-			$stm->bindValue(':priority_normal', FreshRSS_Feed::PRIORITY_NORMAL, PDO::PARAM_INT);
-			$stm->execute();
-			return self::daoToCategoryPrepopulated($stm->fetchAll(PDO::FETCH_ASSOC));
+			$values = [ ':priority_normal' => FreshRSS_Feed::PRIORITY_NORMAL ];
+			if ($stm && $stm->execute($values)) {
+				return self::daoToCategoryPrepopulated($stm->fetchAll(PDO::FETCH_ASSOC));
+			} else {
+				$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
+				if ($this->autoUpdateDb($info)) {
+					return $this->listCategories($prePopulateFeeds, $details);
+				}
+				Minz_Log::error('SQL error listCategories: ' . json_encode($info));
+				return false;
+			}
 		} else {
 			$sql = 'SELECT * FROM `_category` ORDER BY name';
 			$stm = $this->pdo->query($sql);
@@ -282,6 +377,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 				$dao['name']
 			);
 			$cat->_id($dao['id']);
+			$cat->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : '');
 			$cat->_isDefault(static::DEFAULTCATEGORYID === intval($dao['id']));
 			$list[$key] = $cat;
 		}

+ 17 - 0
app/Models/CategoryDAOSQLite.php

@@ -0,0 +1,17 @@
+<?php
+
+class FreshRSS_CategoryDAOSQLite extends FreshRSS_CategoryDAO {
+
+	protected function autoUpdateDb($errorInfo) {
+		if ($tableInfo = $this->pdo->query("PRAGMA table_info('category')")) {
+			$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);
+			foreach (['attributes'] as $column) {
+				if (!in_array($column, $columns)) {
+					return $this->addColumn($column);
+				}
+			}
+		}
+		return false;
+	}
+
+}

+ 0 - 10
app/Models/ConfigurationSetter.php

@@ -79,11 +79,6 @@ class FreshRSS_ConfigurationSetter {
 		$data['html5_notif_timeout'] = $value >= 0 ? $value : 0;
 	}
 
-	private function _keep_history_default(&$data, $value) {
-		$value = intval($value);
-		$data['keep_history_default'] = $value >= FreshRSS_Feed::KEEP_HISTORY_INFINITE ? $value : 0;
-	}
-
 	// It works for system config too!
 	private function _language(&$data, $value) {
 		$value = strtolower($value);
@@ -94,11 +89,6 @@ class FreshRSS_ConfigurationSetter {
 		$data['language'] = $value;
 	}
 
-	private function _old_entries(&$data, $value) {
-		$value = intval($value);
-		$data['old_entries'] = $value > 0 ? $value : 3;
-	}
-
 	private function _passwordHash(&$data, $value) {
 		$data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
 	}

+ 19 - 0
app/Models/Context.php

@@ -51,6 +51,25 @@ class FreshRSS_Context {
 		// Init configuration.
 		self::$system_conf = Minz_Configuration::get('system');
 		self::$user_conf = Minz_Configuration::get('user');
+
+		//Legacy
+		$oldEntries = (int)FreshRSS_Context::$user_conf->param('old_entries', 0);
+		if ($oldEntries > 0) {	//Freshrss < 1.15
+			$archiving['keep_period'] = 'P' . $oldEntries . 'M';
+		}
+
+		$keepMin = (int)FreshRSS_Context::$user_conf->param('keep_history_default', -5);
+		if ($keepMin != 0 && $keepMin > -5) {	//Freshrss < 1.15
+			$archiving = FreshRSS_Context::$user_conf->archiving;
+			if ($keepMin > 0) {
+				$archiving['keep_min'] = $keepMin;
+			} elseif ($keepMin == -1) {	//Infinite
+				$archiving['keep_period'] = false;
+				$archiving['keep_max'] = false;
+				$archiving['keep_min'] = false;
+			}
+			FreshRSS_Context::$user_conf->archiving = $archiving;
+		}
 	}
 
 	/**

+ 6 - 6
app/Models/DatabaseDAO.php

@@ -15,11 +15,11 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 	const LENGTH_INDEX_UNICODE = 191;
 
 	public function create() {
-		require_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
+		require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
 		$db = FreshRSS_Context::$system_conf->db;
 
 		try {
-			$sql = sprintf(SQL_CREATE_DB, empty($db['base']) ? '' : $db['base']);
+			$sql = sprintf($SQL_CREATE_DB, empty($db['base']) ? '' : $db['base']);
 			return $this->pdo->exec($sql) !== false;
 		} catch (PDOException $e) {
 			$_SESSION['bd_error'] = $e->getMessage();
@@ -86,7 +86,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 	public function feedIsCorrect() {
 		return $this->checkTable('feed', array(
 			'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate',
-			'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', 'attributes',
+			'priority', 'pathEntries', 'httpAuth', 'error', 'ttl', 'attributes',
 			'cache_nbEntries', 'cache_nbUnreads',
 		));
 	}
@@ -164,11 +164,11 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 	public function ensureCaseInsensitiveGuids() {
 		$ok = true;
 		if ($this->pdo->dbType() === 'mysql') {
-			include_once(APP_PATH . '/SQL/install.sql.mysql.php');
+			include(APP_PATH . '/SQL/install.sql.mysql.php');
 
 			$ok = false;
 			try {
-				$ok = $this->pdo->exec(SQL_UPDATE_GUID_LATIN1_BIN) !== false;	//FreshRSS 1.12
+				$ok = $this->pdo->exec($SQL_UPDATE_GUID_LATIN1_BIN) !== false;	//FreshRSS 1.12
 			} catch (Exception $e) {
 				$ok = false;
 				Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage());
@@ -243,7 +243,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
 
 		Minz_ModelPdo::clean();
 		$userDAOSQLite = new FreshRSS_UserDAO('', $sqlite);
-		$categoryDAOSQLite = new FreshRSS_CategoryDAO('', $sqlite);
+		$categoryDAOSQLite = new FreshRSS_CategoryDAOSQLite('', $sqlite);
 		$feedDAOSQLite = new FreshRSS_FeedDAOSQLite('', $sqlite);
 		$entryDAOSQLite = new FreshRSS_EntryDAOSQLite('', $sqlite);
 		$tagDAOSQLite = new FreshRSS_TagDAOSQLite('', $sqlite);

+ 1 - 1
app/Models/DatabaseDAOSQLite.php

@@ -66,6 +66,6 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
 	}
 
 	public function optimize() {
-		return $this->exec('VACUUM') !== false;
+		return $this->pdo->exec('VACUUM') !== false;
 	}
 }

+ 45 - 20
app/Models/EntryDAO.php

@@ -26,9 +26,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$this->pdo->commit();
 		}
 		try {
-			require_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
+			require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
 			Minz_Log::warning('SQL CREATE TABLE entrytmp...');
-			$ok = $this->pdo->exec(SQL_CREATE_TABLE_ENTRYTMP . SQL_CREATE_INDEX_ENTRY_1) !== false;
+			$ok = $this->pdo->exec($SQL_CREATE_TABLE_ENTRYTMP . $SQL_CREATE_INDEX_ENTRY_1) !== false;
 		} catch (Exception $ex) {
 			Minz_Log::error(__method__ . ' error: ' . $ex->getMessage());
 		}
@@ -544,32 +544,57 @@ SQL;
 		return $affected;
 	}
 
-	public function cleanOldEntries($id_feed, $date_min, $keep = 15) {	//Remember to call updateCachedValue($id_feed) or updateCachedValues() just after
-		$sql = 'DELETE FROM `_entry` '
-		     . 'WHERE id_feed=:id_feed1 AND id<=:id_max '
-		     . 'AND is_favorite=0 '	//Do not remove favourites
-		     . 'AND `lastSeen` < (SELECT maxLastSeen FROM (SELECT (MAX(e3.`lastSeen`)-99) AS maxLastSeen FROM `_entry` e3 WHERE e3.id_feed=:id_feed2) recent) '	//Do not remove the most newly seen articles, plus a few seconds of tolerance
-		     . 'AND id NOT IN (SELECT id_entry FROM `_entrytag`) '	//Do not purge tagged entries
-		     . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `_entry` e2 WHERE e2.id_feed=:id_feed3 ORDER BY id DESC LIMIT :keep) keep)';	//Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery'
-		$stm = $this->pdo->prepare($sql);
+	public function cleanOldEntries($id_feed, $options = []) { //Remember to call updateCachedValue($id_feed) or updateCachedValues() just after
+		$sql = 'DELETE FROM `_entry` WHERE id_feed = :id_feed1';	//No alias for MySQL / MariaDB
+		$params = [];
+		$params[':id_feed1'] = $id_feed;
 
-		if ($stm) {
-			$id_max = intval($date_min) . '000000';
-			$stm->bindParam(':id_max', $id_max, PDO::PARAM_STR);
-			$stm->bindParam(':id_feed1', $id_feed, PDO::PARAM_INT);
-			$stm->bindParam(':id_feed2', $id_feed, PDO::PARAM_INT);
-			$stm->bindParam(':id_feed3', $id_feed, PDO::PARAM_INT);
-			$stm->bindParam(':keep', $keep, PDO::PARAM_INT);
+		//==Exclusions==
+		if (!empty($options['keep_favourites'])) {
+			$sql .= ' AND is_favorite = 0';
+		}
+		if (!empty($options['keep_unreads'])) {
+			$sql .= ' AND is_read = 1';
+		}
+		if (!empty($options['keep_labels'])) {
+			$sql .= ' AND NOT EXISTS (SELECT 1 FROM `_entrytag` WHERE id_entry = id)';
+		}
+		if (!empty($options['keep_min']) && $options['keep_min'] > 0) {
+			$sql .= ' AND `lastSeen` < (SELECT e2.`lastSeen` FROM `_entry` e2 WHERE e2.id_feed = :id_feed2'
+			      . ' ORDER BY e2.`lastSeen` DESC LIMIT 1 OFFSET :keep_min)';
+			$params[':id_feed2'] = $id_feed;
+			$params[':keep_min'] = (int)$options['keep_min'];
 		}
+		//Keep at least the articles seen at the last refresh
+		$sql .= ' AND `lastSeen` < (SELECT MAX(e3.`lastSeen`) FROM `_entry` e3 WHERE e3.id_feed = :id_feed3)';
+		$params[':id_feed3'] = $id_feed;
+
+		//==Inclusions==
+		$sql .= ' AND (1=0';
+		if (!empty($options['keep_period'])) {
+			$sql .= ' OR `lastSeen` < :max_last_seen';
+			$now = new DateTime('now');
+			$now->sub(new DateInterval($options['keep_period']));
+			$params[':max_last_seen'] = $now->format('U');
+		}
+		if (!empty($options['keep_max']) && $options['keep_max'] > 0) {
+			$sql .= ' OR `lastSeen` <= (SELECT e4.`lastSeen` FROM `_entry` e4 WHERE e4.id_feed = :id_feed4'
+			      . ' ORDER BY e4.`lastSeen` DESC LIMIT 1 OFFSET :keep_max)';
+			$params[':id_feed4'] = $id_feed;
+			$params[':keep_max'] = (int)$options['keep_max'];
+		}
+		$sql .= ')';
+
+		$stm = $this->pdo->prepare($sql);
 
-		if ($stm && $stm->execute()) {
+		if ($stm && $stm->execute($params)) {
 			return $stm->rowCount();
 		} else {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			if ($this->autoUpdateDb($info)) {
-				return $this->cleanOldEntries($id_feed, $date_min, $keep);
+				return $this->cleanOldEntries($id_feed, $options);
 			}
-			Minz_Log::error('SQL error cleanOldEntries: ' . $info[2]);
+			Minz_Log::error(__method__ . ' error:' . json_encode($info));
 			return false;
 		}
 	}

+ 7 - 1
app/Models/Factory.php

@@ -7,7 +7,13 @@ class FreshRSS_Factory {
 	}
 
 	public static function createCategoryDao($username = null) {
-		return new FreshRSS_CategoryDAO($username);
+		$conf = Minz_Configuration::get('system');
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_CategoryDAOSQLite($username);
+			default:
+				return new FreshRSS_CategoryDAO($username);
+		}
 	}
 
 	public static function createFeedDao($username = null) {

+ 25 - 13
app/Models/Feed.php

@@ -7,8 +7,8 @@ class FreshRSS_Feed extends Minz_Model {
 
 	const TTL_DEFAULT = 0;
 
-	const KEEP_HISTORY_DEFAULT = -2;
-	const KEEP_HISTORY_INFINITE = -1;
+	const ARCHIVING_RETENTION_COUNT_LIMIT = 10000;
+	const ARCHIVING_RETENTION_PERIOD = 'P3M';
 
 	private $id = 0;
 	private $url;
@@ -24,9 +24,8 @@ class FreshRSS_Feed extends Minz_Model {
 	private $pathEntries = '';
 	private $httpAuth = '';
 	private $error = false;
-	private $keep_history = self::KEEP_HISTORY_DEFAULT;
 	private $ttl = self::TTL_DEFAULT;
-	private $attributes = array();
+	private $attributes = [];
 	private $mute = false;
 	private $hash = null;
 	private $lockPath = '';
@@ -110,9 +109,6 @@ class FreshRSS_Feed extends Minz_Model {
 	public function inError() {
 		return $this->error;
 	}
-	public function keepHistory() {
-		return $this->keep_history;
-	}
 	public function ttl() {
 		return $this->ttl;
 	}
@@ -230,12 +226,6 @@ class FreshRSS_Feed extends Minz_Model {
 	public function _error($value) {
 		$this->error = (bool)$value;
 	}
-	public function _keepHistory($value) {
-		$value = intval($value);
-		$value = min($value, 1000000);
-		$value = max($value, self::KEEP_HISTORY_DEFAULT);
-		$this->keep_history = $value;
-	}
 	public function _ttl($value) {
 		$value = intval($value);
 		$value = min($value, 100000000);
@@ -469,6 +459,28 @@ class FreshRSS_Feed extends Minz_Model {
 		$this->entries = $entries;
 	}
 
+	public function cleanOldEntries() {	//Remember to call updateCachedValue($id_feed) or updateCachedValues() just after
+		$archiving = $this->attributes('archiving');
+		if ($archiving == null) {
+			$catDAO = FreshRSS_Factory::createCategoryDao();
+			$category = $catDAO->searchById($this->category());
+			$archiving = $category == null ? null : $category->attributes('archiving');
+			if ($archiving == null) {
+				$archiving = FreshRSS_Context::$user_conf->archiving;
+			}
+		}
+		if (is_array($archiving)) {
+			$entryDAO = FreshRSS_Factory::createEntryDao();
+			$nb = $entryDAO->cleanOldEntries($this->id(), $archiving);
+			if ($nb > 0) {
+				$needFeedCacheRefresh = true;
+				Minz_Log::debug($nb . ' entries cleaned in feed [' . $this->url(false) . '] with: ' . json_encode($archiving));
+			}
+			return $nb;
+		}
+		return false;
+	}
+
 	protected function cacheFilename() {
 		return CACHE_PATH . '/' . md5($this->url) . '.spc';
 	}

+ 10 - 10
app/Models/FeedDAO.php

@@ -17,7 +17,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	protected function autoUpdateDb($errorInfo) {
 		if (isset($errorInfo[0])) {
 			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
-				foreach (array('attributes') as $column) {
+				foreach (['attributes'] as $column) {
 					if (stripos($errorInfo[2], $column) !== false) {
 						return $this->addColumn($column);
 					}
@@ -41,12 +41,11 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 					`pathEntries`,
 					`httpAuth`,
 					error,
-					keep_history,
 					ttl,
 					attributes
 				)
 				VALUES
-				(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
+				(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
 		$stm = $this->pdo->prepare($sql);
 
 		$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
@@ -54,6 +53,9 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if (!isset($valuesTmp['pathEntries'])) {
 			$valuesTmp['pathEntries'] = '';
 		}
+		if (!isset($valuesTmp['attributes'])) {
+			$valuesTmp['attributes'] = [];
+		}
 
 		$values = array(
 			substr($valuesTmp['url'], 0, 511),
@@ -66,9 +68,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			mb_strcut($valuesTmp['pathEntries'], 0, 511, 'UTF-8'),
 			base64_encode($valuesTmp['httpAuth']),
 			isset($valuesTmp['error']) ? intval($valuesTmp['error']) : 0,
-			isset($valuesTmp['keep_history']) ? intval($valuesTmp['keep_history']) : FreshRSS_Feed::KEEP_HISTORY_DEFAULT,
 			isset($valuesTmp['ttl']) ? intval($valuesTmp['ttl']) : FreshRSS_Feed::TTL_DEFAULT,
-			isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
+			is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
 		);
 
 		if ($stm && $stm->execute($values)) {
@@ -135,7 +136,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			if ($key === 'httpAuth') {
 				$valuesTmp[$key] = base64_encode($v);
 			} elseif ($key === 'attributes') {
-				$valuesTmp[$key] = json_encode($v);
+				$valuesTmp[$key] = is_string($valuesTmp[$key]) ? $valuesTmp[$key] : json_encode($valuesTmp[$key], JSON_UNESCAPED_SLASHES);
 			}
 		}
 		$set = substr($set, 0, -2);
@@ -246,7 +247,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 	public function selectAll() {
 		$sql = 'SELECT id, url, category, name, website, description, `lastUpdate`, priority, '
-		     . '`pathEntries`, `httpAuth`, error, keep_history, ttl, attributes '
+		     . '`pathEntries`, `httpAuth`, error, ttl, attributes '
 		     . 'FROM `_feed`';
 		$stm = $this->pdo->query($sql);
 		while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
@@ -319,7 +320,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	 */
 	public function listFeedsOrderUpdate($defaultCacheDuration = 3600, $limit = 0) {
 		$this->updateTTL();
-		$sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl, attributes '
+		$sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes '
 		     . 'FROM `_feed` '
 		     . ($defaultCacheDuration < 0 ? '' : 'WHERE ttl >= ' . FreshRSS_Feed::TTL_DEFAULT
 		     . ' AND `lastUpdate` < (' . (time() + 60)
@@ -407,7 +408,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			 . 'SET `cache_nbEntries`=0, `cache_nbUnreads`=0 WHERE id=:id';
 		$stm = $this->pdo->prepare($sql);
 		$stm->bindParam(':id', $id, PDO::PARAM_INT);
-		if (!($stm && $stm->execute($values))) {
+		if (!($stm && $stm->execute())) {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
 			Minz_Log::error('SQL error truncate: ' . $info[2]);
 			$this->pdo->rollBack();
@@ -448,7 +449,6 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$myFeed->_pathEntries(isset($dao['pathEntries']) ? $dao['pathEntries'] : '');
 			$myFeed->_httpAuth(isset($dao['httpAuth']) ? base64_decode($dao['httpAuth']) : '');
 			$myFeed->_error(isset($dao['error']) ? $dao['error'] : 0);
-			$myFeed->_keepHistory(isset($dao['keep_history']) ? $dao['keep_history'] : FreshRSS_Feed::KEEP_HISTORY_DEFAULT);
 			$myFeed->_ttl(isset($dao['ttl']) ? $dao['ttl'] : FreshRSS_Feed::TTL_DEFAULT);
 			$myFeed->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : '');
 			$myFeed->_nbNotRead(isset($dao['cache_nbUnreads']) ? $dao['cache_nbUnreads'] : 0);

+ 1 - 1
app/Models/FeedDAOSQLite.php

@@ -5,7 +5,7 @@ class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
 	protected function autoUpdateDb($errorInfo) {
 		if ($tableInfo = $this->pdo->query("PRAGMA table_info('feed')")) {
 			$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);
-			foreach (array('attributes') as $column) {
+			foreach (['attributes'] as $column) {
 				if (!in_array($column, $columns)) {
 					return $this->addColumn($column);
 				}

+ 1 - 1
app/Models/Tag.php

@@ -3,7 +3,7 @@
 class FreshRSS_Tag extends Minz_Model {
 	private $id = 0;
 	private $name;
-	private $attributes = array();
+	private $attributes = [];
 	private $nbEntries = -1;
 	private $nbUnread = -1;
 

+ 10 - 4
app/Models/TagDAO.php

@@ -13,14 +13,14 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$this->pdo->commit();
 		}
 		try {
-			require_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
+			require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
 
 			Minz_Log::warning('SQL ALTER GUID case sensitivity...');
 			$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
 			$databaseDAO->ensureCaseInsensitiveGuids();
 
 			Minz_Log::warning('SQL CREATE TABLE tag...');
-			$ok = $this->pdo->exec(SQL_CREATE_TABLE_TAGS) !== false;
+			$ok = $this->pdo->exec($SQL_CREATE_TABLE_TAGS) !== false;
 		} catch (Exception $e) {
 			Minz_Log::error('FreshRSS_EntryDAO::createTagTable error: ' . $e->getMessage());
 		}
@@ -48,9 +48,12 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		$stm = $this->pdo->prepare($sql);
 
 		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8');
+		if (!isset($valuesTmp['attributes'])) {
+			$valuesTmp['attributes'] = [];
+		}
 		$values = array(
 			$valuesTmp['name'],
-			isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
+			is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
 			$valuesTmp['name'],
 		);
 
@@ -81,9 +84,12 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		$stm = $this->pdo->prepare($sql);
 
 		$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8');
+		if (!isset($valuesTmp['attributes'])) {
+			$valuesTmp['attributes'] = [];
+		}
 		$values = array(
 			$valuesTmp['name'],
-			isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
+			is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
 			$id,
 			$valuesTmp['name'],
 		);

+ 5 - 6
app/Models/UserDAO.php

@@ -2,14 +2,14 @@
 
 class FreshRSS_UserDAO extends Minz_ModelPdo {
 	public function createUser($insertDefaultFeeds = false) {
-		require_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
+		require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
 
 		try {
-			$sql = SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP . SQL_CREATE_TABLE_TAGS;
+			$sql = $SQL_CREATE_TABLES . $SQL_CREATE_TABLE_ENTRYTMP . $SQL_CREATE_TABLE_TAGS;
 			$ok = $this->pdo->exec($sql) !== false;	//Note: Only exec() can take multiple statements safely.
 			if ($ok && $insertDefaultFeeds) {
 				$default_feeds = FreshRSS_Context::$system_conf->default_feeds;
-				$stm = $this->pdo->prepare(SQL_INSERT_FEED);
+				$stm = $this->pdo->prepare($SQL_INSERT_FEED);
 				foreach ($default_feeds as $feed) {
 					$parameters = [
 						':url' => $feed['url'],
@@ -38,9 +38,8 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
 			fwrite(STDERR, 'Deleting SQL data for user “' . $this->current_user . "”…\n");
 		}
 
-		require_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
-
-		$ok = $this->pdo->exec(SQL_DROP_TABLES) !== false;
+		require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
+		$ok = $this->pdo->exec($SQL_DROP_TABLES) !== false;
 
 		if ($ok) {
 			return true;

+ 10 - 11
app/SQL/install.sql.mysql.php

@@ -1,12 +1,13 @@
 <?php
-const SQL_CREATE_DB = <<<'SQL'
+$SQL_CREATE_DB = <<<'SQL'
 CREATE DATABASE IF NOT EXISTS `%1$s` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
 SQL;
 
-const SQL_CREATE_TABLES = <<<'SQL'
+$SQL_CREATE_TABLES = <<<'SQL'
 CREATE TABLE IF NOT EXISTS `_category` (
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7
 	`name` VARCHAR(191) NOT NULL,	-- Max index length for Unicode is 191 characters (767 bytes) FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE
+	`attributes` TEXT,	-- v1.15.0
 	PRIMARY KEY (`id`),
 	UNIQUE KEY (`name`)	-- v0.7
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
@@ -24,7 +25,6 @@ CREATE TABLE IF NOT EXISTS `_feed` (
 	`pathEntries` VARCHAR(511) DEFAULT NULL,
 	`httpAuth` VARCHAR(511) DEFAULT NULL,
 	`error` BOOLEAN DEFAULT 0,
-	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,	-- v0.7
 	`ttl` INT NOT NULL DEFAULT 0,	-- v0.7.3
 	`attributes` TEXT,	-- v1.11.0
 	`cache_nbEntries` INT DEFAULT 0,	-- v0.7
@@ -33,8 +33,7 @@ CREATE TABLE IF NOT EXISTS `_feed` (
 	FOREIGN KEY (`category`) REFERENCES `_category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
 	UNIQUE KEY (`url`),	-- v0.7
 	INDEX (`name`),	-- v0.7
-	INDEX (`priority`),	-- v0.7
-	INDEX (`keep_history`)	-- v0.7
+	INDEX (`priority`)	-- v0.7
 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
 ENGINE = INNODB;
 
@@ -65,11 +64,11 @@ ENGINE = INNODB;
 INSERT IGNORE INTO `_category` (id, name) VALUES(1, "Uncategorized");
 SQL;
 
-const SQL_CREATE_INDEX_ENTRY_1 = <<<'SQL'
+$SQL_CREATE_INDEX_ENTRY_1 = <<<'SQL'
 CREATE INDEX `entry_feed_read_index` ON `_entry` (`id_feed`,`is_read`);	-- v1.7
 SQL;
 
-const SQL_CREATE_TABLE_ENTRYTMP = <<<'SQL'
+$SQL_CREATE_TABLE_ENTRYTMP = <<<'SQL'
 CREATE TABLE IF NOT EXISTS `_entrytmp` (	-- v1.7
 	`id` BIGINT NOT NULL,
 	`guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
@@ -92,7 +91,7 @@ CREATE TABLE IF NOT EXISTS `_entrytmp` (	-- v1.7
 ENGINE = INNODB;
 SQL;
 
-const SQL_CREATE_TABLE_TAGS = <<<'SQL'
+$SQL_CREATE_TABLE_TAGS = <<<'SQL'
 CREATE TABLE IF NOT EXISTS `_tag` (	-- v1.12
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,
 	`name` VARCHAR(63) NOT NULL,
@@ -113,16 +112,16 @@ CREATE TABLE IF NOT EXISTS `_entrytag` (	-- v1.12
 ENGINE = INNODB;
 SQL;
 
-const SQL_INSERT_FEED = <<<'SQL'
+$SQL_INSERT_FEED = <<<'SQL'
 INSERT IGNORE INTO `_feed` (url, category, name, website, description, ttl)
 	VALUES(:url, 1, :name, :website, :description, 86400);
 SQL;
 
-const SQL_DROP_TABLES = <<<'SQL'
+$SQL_DROP_TABLES = <<<'SQL'
 DROP TABLE IF EXISTS `_entrytag`, `_tag`, `_entrytmp`, `_entry`, `_feed`, `_category`;
 SQL;
 
-const SQL_UPDATE_GUID_LATIN1_BIN = <<<'SQL'
+$SQL_UPDATE_GUID_LATIN1_BIN = <<<'SQL'
 ALTER TABLE `_entrytmp` MODIFY `guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL;	-- v1.12
 ALTER TABLE `_entry` MODIFY `guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL;
 SQL;

+ 9 - 10
app/SQL/install.sql.pgsql.php

@@ -1,12 +1,13 @@
 <?php
-const SQL_CREATE_DB = <<<'SQL'
+$SQL_CREATE_DB = <<<'SQL'
 CREATE DATABASE "%1$s" ENCODING 'UTF8';
 SQL;
 
-const SQL_CREATE_TABLES = <<<'SQL'
+$SQL_CREATE_TABLES = <<<'SQL'
 CREATE TABLE IF NOT EXISTS `_category` (
 	"id" SERIAL PRIMARY KEY,
-	"name" VARCHAR(255) UNIQUE NOT NULL
+	"name" VARCHAR(255) UNIQUE NOT NULL,
+	"attributes" TEXT	-- v1.15.0
 );
 
 CREATE TABLE IF NOT EXISTS `_feed` (
@@ -21,7 +22,6 @@ CREATE TABLE IF NOT EXISTS `_feed` (
 	"pathEntries" VARCHAR(511) DEFAULT NULL,
 	"httpAuth" VARCHAR(511) DEFAULT NULL,
 	"error" SMALLINT DEFAULT 0,
-	"keep_history" INT NOT NULL DEFAULT -2,
 	"ttl" INT NOT NULL DEFAULT 0,
 	"attributes" TEXT,	-- v1.11.0
 	"cache_nbEntries" INT DEFAULT 0,
@@ -30,7 +30,6 @@ CREATE TABLE IF NOT EXISTS `_feed` (
 );
 CREATE INDEX IF NOT EXISTS `_name_index` ON `_feed` ("name");
 CREATE INDEX IF NOT EXISTS `_priority_index` ON `_feed` ("priority");
-CREATE INDEX IF NOT EXISTS `_keep_history_index` ON `_feed` ("keep_history");
 
 CREATE TABLE IF NOT EXISTS `_entry` (
 	"id" BIGINT NOT NULL PRIMARY KEY,
@@ -60,11 +59,11 @@ INSERT INTO `_category` (id, name)
 	RETURNING nextval('`_category_id_seq`');
 SQL;
 
-const SQL_CREATE_INDEX_ENTRY_1 = <<<'SQL'
+$SQL_CREATE_INDEX_ENTRY_1 = <<<'SQL'
 CREATE INDEX IF NOT EXISTS `_entry_feed_read_index` ON `_entry` ("id_feed","is_read");	-- v1.7
 SQL;
 
-const SQL_CREATE_TABLE_ENTRYTMP = <<<'SQL'
+$SQL_CREATE_TABLE_ENTRYTMP = <<<'SQL'
 CREATE TABLE IF NOT EXISTS `_entrytmp` (	-- v1.7
 	"id" BIGINT NOT NULL PRIMARY KEY,
 	"guid" VARCHAR(760) NOT NULL,
@@ -85,7 +84,7 @@ CREATE TABLE IF NOT EXISTS `_entrytmp` (	-- v1.7
 CREATE INDEX IF NOT EXISTS `_entrytmp_date_index` ON `_entrytmp` ("date");
 SQL;
 
-const SQL_CREATE_TABLE_TAGS = <<<'SQL'
+$SQL_CREATE_TABLE_TAGS = <<<'SQL'
 CREATE TABLE IF NOT EXISTS `_tag` (	-- v1.12
 	"id" SERIAL PRIMARY KEY,
 	"name" VARCHAR(63) UNIQUE NOT NULL,
@@ -101,12 +100,12 @@ CREATE TABLE IF NOT EXISTS `_entrytag` (
 CREATE INDEX IF NOT EXISTS `_entrytag_id_entry_index` ON `_entrytag` ("id_entry");
 SQL;
 
-const SQL_INSERT_FEED = <<<'SQL'
+$SQL_INSERT_FEED = <<<'SQL'
 INSERT INTO `_feed` (url, category, name, website, description, ttl)
 	SELECT :url::VARCHAR, 1, :name, :website, :description, 86400
 		WHERE NOT EXISTS (SELECT id FROM `_feed` WHERE url = :url);
 SQL;
 
-const SQL_DROP_TABLES = <<<'SQL'
+$SQL_DROP_TABLES = <<<'SQL'
 DROP TABLE IF EXISTS `_entrytag`, `_tag`, `_entrytmp`, `_entry`, `_feed`, `_category`;
 SQL;

+ 8 - 9
app/SQL/install.sql.sqlite.php

@@ -1,12 +1,13 @@
 <?php
-const SQL_CREATE_DB = <<<'SQL'
+$SQL_CREATE_DB = <<<'SQL'
 SELECT 1;	-- Do nothing for SQLite
 SQL;
 
-const SQL_CREATE_TABLES = <<<'SQL'
+$SQL_CREATE_TABLES = <<<'SQL'
 CREATE TABLE IF NOT EXISTS `category` (
 	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 	`name` VARCHAR(255) NOT NULL,
+	`attributes` TEXT,	-- v1.15.0
 	UNIQUE (`name`)
 );
 
@@ -22,7 +23,6 @@ CREATE TABLE IF NOT EXISTS `feed` (
 	`pathEntries` VARCHAR(511) DEFAULT NULL,
 	`httpAuth` VARCHAR(511) DEFAULT NULL,
 	`error` BOOLEAN DEFAULT 0,
-	`keep_history` MEDIUMINT NOT NULL DEFAULT -2,
 	`ttl` INT NOT NULL DEFAULT 0,
 	`attributes` TEXT,	-- v1.11.0
 	`cache_nbEntries` INT DEFAULT 0,
@@ -32,7 +32,6 @@ CREATE TABLE IF NOT EXISTS `feed` (
 );
 CREATE INDEX IF NOT EXISTS feed_name_index ON `feed`(`name`);
 CREATE INDEX IF NOT EXISTS feed_priority_index ON `feed`(`priority`);
-CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `feed`(`keep_history`);
 
 CREATE TABLE IF NOT EXISTS `entry` (
 	`id` BIGINT NOT NULL,
@@ -60,11 +59,11 @@ CREATE INDEX IF NOT EXISTS entry_feed_read_index ON `entry`(`id_feed`,`is_read`)
 INSERT OR IGNORE INTO `category` (id, name) VALUES(1, "Uncategorized");
 SQL;
 
-const SQL_CREATE_INDEX_ENTRY_1 = <<<'SQL'
+$SQL_CREATE_INDEX_ENTRY_1 = <<<'SQL'
 CREATE INDEX IF NOT EXISTS entry_feed_read_index ON `entry`(`id_feed`,`is_read`);	-- v1.7
 SQL;
 
-const SQL_CREATE_TABLE_ENTRYTMP = <<<'SQL'
+$SQL_CREATE_TABLE_ENTRYTMP = <<<'SQL'
 CREATE TABLE IF NOT EXISTS `entrytmp` (	-- v1.7
 	`id` BIGINT NOT NULL,
 	`guid` VARCHAR(760) NOT NULL,
@@ -86,7 +85,7 @@ CREATE TABLE IF NOT EXISTS `entrytmp` (	-- v1.7
 CREATE INDEX IF NOT EXISTS entrytmp_date_index ON `entrytmp`(`date`);
 SQL;
 
-const SQL_CREATE_TABLE_TAGS = <<<'SQL'
+$SQL_CREATE_TABLE_TAGS = <<<'SQL'
 CREATE TABLE IF NOT EXISTS `tag` (	-- v1.12
 	`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
 	`name` VARCHAR(63) NOT NULL,
@@ -103,12 +102,12 @@ CREATE TABLE IF NOT EXISTS `entrytag` (
 CREATE INDEX IF NOT EXISTS entrytag_id_entry_index ON `entrytag` (`id_entry`);
 SQL;
 
-const SQL_INSERT_FEED = <<<'SQL'
+$SQL_INSERT_FEED = <<<'SQL'
 INSERT OR IGNORE INTO `feed` (url, category, name, website, description, ttl)
 	VALUES(:url, 1, :name, :website, :description, 86400);
 SQL;
 
-const SQL_DROP_TABLES = <<<'SQL'
+$SQL_DROP_TABLES = <<<'SQL'
 DROP TABLE IF EXISTS `entrytag`;
 DROP TABLE IF EXISTS `tag`;
 DROP TABLE IF EXISTS `entrytmp`;

+ 10 - 2
app/i18n/cz/conf.php

@@ -3,13 +3,21 @@
 return array(
 	'archiving' => array(
 		'_' => 'Archivace',
-		'advanced' => 'Pokročilé',
 		'delete_after' => 'Smazat články starší než',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'Více možností je dostupných v nastavení jednotlivých kanálů',
-		'keep_history_by_feed' => 'Zachovat tento minimální počet článků v každém kanálu',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Zachovat tento minimální počet článků v každém kanálu',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'Optimalizovat databázi',
 		'optimize_help' => 'Občasná údržba zmenší velikost databáze',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'Vyčistit nyní',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'Archivace',
 		'ttl' => 'Neaktualizovat častěji než',
 	),

+ 7 - 0
app/i18n/cz/gen.php

@@ -162,6 +162,13 @@ return array(
 		'nothing_to_load' => 'Žádné nové články',
 		'previous' => 'Předchozí',
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',

+ 2 - 1
app/i18n/cz/sub.php

@@ -13,6 +13,7 @@ return array(
 	'category' => array(
 		'_' => 'Kategorie',
 		'add' => 'Přidat kategorii',
+		'archiving' => 'Archivace',
 		'empty' => 'Vyprázdit kategorii',
 		'information' => 'Informace',
 		'new' => 'Nová kategorie',
@@ -40,7 +41,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		'information' => 'Informace',
-		'keep_history' => 'Zachovat tento minimální počet článků',
+		'keep_min' => '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>.',
 		'mute' => 'mute',	//TODO - Translation
 		'no_selected' => 'Nejsou označeny žádné kanály.',

+ 10 - 2
app/i18n/de/conf.php

@@ -3,13 +3,21 @@
 return array(
 	'archiving' => array(
 		'_' => 'Archivierung',
-		'advanced' => 'Erweitert',
 		'delete_after' => 'Entferne Artikel nach',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'Weitere Optionen sind in den Einstellungen der individuellen Feeds verfügbar.',
-		'keep_history_by_feed' => 'Minimale Anzahl an Artikeln, die pro Feed behalten werden',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Minimale Anzahl an Artikeln, die pro Feed behalten werden',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'Datenbank optimieren',
 		'optimize_help' => 'Sollte gelegentlich durchgeführt werden, um die Größe der Datenbank zu reduzieren.',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'Jetzt bereinigen',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'Archivierung',
 		'ttl' => 'Aktualisiere automatisch nicht öfter als',
 	),

+ 7 - 0
app/i18n/de/gen.php

@@ -162,6 +162,13 @@ return array(
 		'nothing_to_load' => 'Es gibt keine weiteren Artikel',
 		'previous' => 'Vorherige',
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',

+ 2 - 1
app/i18n/de/sub.php

@@ -13,6 +13,7 @@ return array(
 	'category' => array(
 		'_' => 'Kategorie',
 		'add' => 'Eine Kategorie hinzufügen',
+		'archiving' => 'Archivierung',
 		'empty' => 'Leere Kategorie',
 		'information' => 'Information',
 		'new' => 'Neue Kategorie',
@@ -40,7 +41,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		'information' => 'Information',
-		'keep_history' => 'Minimale Anzahl an Artikeln, die behalten wird',
+		'keep_min' => '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.',
 		'mute' => 'Stumm schalten',
 		'no_selected' => 'Kein Feed ausgewählt.',

+ 10 - 2
app/i18n/en/conf.php

@@ -3,13 +3,21 @@
 return array(
 	'archiving' => array(
 		'_' => 'Archiving',
-		'advanced' => 'Advanced',
 		'delete_after' => 'Remove articles after',
+		'exception' => 'Purge exception',
 		'help' => 'More options are available in the individual feed settings',
-		'keep_history_by_feed' => 'Minimum number of articles to keep by feed',
+		'keep_favourites' => 'Never delete favourites',
+		'keep_min_by_feed' => 'Minimum number of articles to keep by feed',
+		'keep_labels' => 'Never delete labels',
+		'keep_unreads' => 'Never delete unreads',
+		'maintenance' => 'Maintenance',
 		'optimize' => 'Optimise database',
 		'optimize_help' => 'Do occasionally to reduce the size of the database',
+		'policy' => 'Purge policy',
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',
 		'purge_now' => 'Purge now',
+		'keep_max' => 'Maximum number of articles to keep',
+		'keep_period' => 'Maximum age of articles to keep',
 		'title' => 'Archiving',
 		'ttl' => 'Do not automatically refresh more often than',
 	),

+ 7 - 0
app/i18n/en/gen.php

@@ -163,6 +163,13 @@ return array(
 		'nothing_to_load' => 'There are no more articles',
 		'previous' => 'Previous',
 	),
+	'period' => array(
+		'days' => 'days',
+		'hours' => 'hours',
+		'months' => 'months',
+		'weeks' => 'weeks',
+		'years' => 'years',
+	),
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',

+ 2 - 1
app/i18n/en/sub.php

@@ -13,6 +13,7 @@ return array(
 	'category' => array(
 		'_' => 'Category',
 		'add' => 'Add a category',
+		'archiving' => 'Archiving',
 		'empty' => 'Empty category',
 		'information' => 'Information',
 		'new' => 'New category',
@@ -40,7 +41,7 @@ return array(
 			'help' => 'Write one search filter per line.',
 		),
 		'information' => 'Information',
-		'keep_history' => 'Minimum number of articles to keep',
+		'keep_min' => 'Minimum number of articles to keep',
 		'moved_category_deleted' => 'When you delete a category, its feeds are automatically classified under <em>%s</em>.',
 		'mute' => 'mute',
 		'no_selected' => 'No feed selected.',

+ 10 - 2
app/i18n/es/conf.php

@@ -3,13 +3,21 @@
 return array(
 	'archiving' => array(
 		'_' => 'Archivo',
-		'advanced' => 'Avanzado',
 		'delete_after' => 'Eliminar artículos tras',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'Hay más opciones disponibles en los ajustes de la fuente',
-		'keep_history_by_feed' => 'Número mínimo de artículos a conservar por fuente',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Número mínimo de artículos a conservar por fuente',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'Optimizar la base de datos',
 		'optimize_help' => 'Ejecuta la optimización de vez en cuando para reducir el tamaño de la base de datos',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'Limpiar ahora',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'Archivo',
 		'ttl' => 'No actualizar automáticamente más de',
 	),

+ 7 - 0
app/i18n/es/gen.php

@@ -162,6 +162,13 @@ return array(
 		'nothing_to_load' => 'No hay más artículos',
 		'previous' => 'Anterior',
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',

+ 2 - 1
app/i18n/es/sub.php

@@ -13,6 +13,7 @@ return array(
 	'category' => array(
 		'_' => 'Categoría',
 		'add' => 'Añadir a la categoría',
+		'archiving' => 'Archivo',
 		'empty' => 'Vaciar categoría',
 		'information' => 'Información',
 		'new' => 'Nueva categoría',
@@ -40,7 +41,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		'information' => 'Información',
-		'keep_history' => 'Número mínimo de artículos a conservar',
+		'keep_min' => '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>.',
 		'mute' => 'mute',	//TODO - Translation
 		'no_selected' => 'No hay funentes seleccionadas.',

+ 10 - 2
app/i18n/fr/conf.php

@@ -3,13 +3,21 @@
 return array(
 	'archiving' => array(
 		'_' => 'Archivage',
-		'advanced' => 'Avancé',
 		'delete_after' => 'Supprimer les articles après',
+		'exception' => 'Exception de nettoyage',
 		'help' => 'D’autres options sont disponibles dans la configuration individuelle des flux.',
-		'keep_history_by_feed' => 'Nombre minimum d’articles à conserver par flux',
+		'keep_favourites' => 'Ne jamais supprimer les articles favoris',
+		'keep_min_by_feed' => 'Nombre minimum d’articles à conserver par flux',
+		'keep_labels' => 'Ne jamais supprimer les articles étiquetés',
+		'keep_unreads' => 'Ne jamais supprimer les articles non lus',
+		'maintenance' => 'Maintenance',
 		'optimize' => 'Optimiser la base de données',
 		'optimize_help' => 'À faire de temps en temps pour réduire la taille de la BDD',
+		'policy' => 'Politique de nettoyage',
+		'policy_warning' => 'Si aucune politique de nettoyage n’est sélectionnée, tous les articles seront conservés.',
 		'purge_now' => 'Purger maintenant',
+		'keep_max' => 'Nombre maximum d’articles à conserver',
+		'keep_period' => 'Âge maximum des articles à conserver',
 		'title' => 'Archivage',
 		'ttl' => 'Ne pas automatiquement rafraîchir plus souvent que',
 	),

+ 7 - 0
app/i18n/fr/gen.php

@@ -162,6 +162,13 @@ return array(
 		'nothing_to_load' => 'Fin des articles',
 		'previous' => 'Précédent',
 	),
+	'period' => array(
+		'days' => 'jours',
+		'hours' => 'heures',
+		'months' => 'mois',
+		'weeks' => 'semaines',
+		'years' => 'années',
+	),
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',

+ 2 - 1
app/i18n/fr/sub.php

@@ -13,6 +13,7 @@ return array(
 	'category' => array(
 		'_' => 'Catégorie',
 		'add' => 'Ajouter une catégorie',
+		'archiving' => 'Archivage',
 		'empty' => 'Catégorie vide',
 		'information' => 'Informations',
 		'new' => 'Nouvelle catégorie',
@@ -40,7 +41,7 @@ return array(
 			'help' => 'Écrivez une recherche par ligne.',
 		),
 		'information' => 'Informations',
-		'keep_history' => 'Nombre minimum d’articles à conserver',
+		'keep_min' => '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>.',
 		'mute' => 'muet',
 		'no_selected' => 'Aucun flux sélectionné.',

+ 10 - 2
app/i18n/he/conf.php

@@ -3,13 +3,21 @@
 return array(
 	'archiving' => array(
 		'_' => 'ארכוב',
-		'advanced' => 'מתקדם',
 		'delete_after' => 'מחיקת מאמרים לאחר',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'אפשרויות נוספות זמינות בזרמים ספציפיים',
-		'keep_history_by_feed' => 'Minimum number of articles to keep by feed',	//TODO - Translation
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Minimum number of articles to keep by feed',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'מיטוב בסיס הנתונים',
 		'optimize_help' => 'ביצוע לעיתים קרובות על מנת למטב את בסיס הנתונים',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'ניקוי עכשיו',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'ארכוב',
 		'ttl' => 'אין לרענן אוטומטית יותר מ',
 	),

+ 7 - 0
app/i18n/he/gen.php

@@ -162,6 +162,13 @@ return array(
 		'nothing_to_load' => 'אין מאמרים נוספים',
 		'previous' => 'הקודם',
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',

+ 2 - 1
app/i18n/he/sub.php

@@ -13,6 +13,7 @@ return array(
 	'category' => array(
 		'_' => 'קטגוריה',
 		'add' => 'הוספת קטגוריה',
+		'archiving' => 'ארכוב',
 		'empty' => 'Empty category',	//TODO - Translation
 		'information' => 'מידע',
 		'new' => 'קטגוריה חדשה',
@@ -40,7 +41,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		'information' => 'מידע',
-		'keep_history' => 'מסםר מינימלי של מאמרים לשמור',
+		'keep_min' => 'מסםר מינימלי של מאמרים לשמור',
 		'moved_category_deleted' => 'כאשר הקטגוריה נמחקת ההזנות שבתוכה אוטומטית מקוטלגות תחת  <em>%s</em>.',
 		'mute' => 'mute',	//TODO - Translation
 		'no_selected' => 'אף הזנה לא נבחרה.',

+ 10 - 2
app/i18n/it/conf.php

@@ -3,13 +3,21 @@
 return array(
 	'archiving' => array(
 		'_' => 'Archiviazione',
-		'advanced' => 'Avanzate',
 		'delete_after' => 'Rimuovi articoli dopo',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'Altre opzioni sono disponibili nelle impostazioni dei singoli feed',
-		'keep_history_by_feed' => 'Numero minimo di articoli da mantenere per feed',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Numero minimo di articoli da mantenere per feed',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'Ottimizza database',
 		'optimize_help' => 'Da fare occasionalmente per ridurre le dimensioni del database',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'Cancella ora',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'Archiviazione',
 		'ttl' => 'Non effettuare aggiornamenti per più di',
 	),

+ 7 - 0
app/i18n/it/gen.php

@@ -162,6 +162,13 @@ return array(
 		'nothing_to_load' => 'Non ci sono altri articoli',
 		'previous' => 'Precedente',
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',

+ 2 - 1
app/i18n/it/sub.php

@@ -13,6 +13,7 @@ return array(
 	'category' => array(
 		'_' => 'Categoria',
 		'add' => 'Aggiungi una categoria',
+		'archiving' => 'Archiviazione',
 		'empty' => 'Categoria vuota',
 		'information' => 'Informazioni',
 		'new' => 'Nuova categoria',
@@ -40,7 +41,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		'information' => 'Informazioni',
-		'keep_history' => 'Numero minimo di articoli da mantenere',
+		'keep_min' => 'Numero minimo di articoli da mantenere',
 		'moved_category_deleted' => 'Cancellando una categoria i feed al suo interno verranno classificati automaticamente come <em>%s</em>.',
 		'mute' => 'mute',	//TODO - Translation
 		'no_selected' => 'Nessun feed selezionato.',

+ 10 - 2
app/i18n/kr/conf.php

@@ -3,13 +3,21 @@
 return array(
 	'archiving' => array(
 		'_' => '보관',
-		'advanced' => '고급 설정',
 		'delete_after' => '다음 기간보다 오래된 글 삭제',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => '더 자세한 옵션은 개별 피드 설정에 있습니다',
-		'keep_history_by_feed' => '피드별 최소 유지 글 개수',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => '피드별 최소 유지 글 개수',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => '데이터베이스 최적화',
 		'optimize_help' => '데이터베이스 크기를 줄이기 위해 가끔씩 수행해주세요',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => '지금 삭제',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => '보관',
 		'ttl' => '다음 시간이 지나기 전에 새로고침 금지',
 	),

+ 7 - 0
app/i18n/kr/gen.php

@@ -162,6 +162,13 @@ return array(
 		'nothing_to_load' => '더 이상 글이 없습니다',
 		'previous' => '이전',
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',

+ 2 - 1
app/i18n/kr/sub.php

@@ -13,6 +13,7 @@ return array(
 	'category' => array(
 		'_' => '카테고리',
 		'add' => '카테고리 추가',
+		'archiving' => '보관',
 		'empty' => '빈 카테고리',
 		'information' => '정보',
 		'new' => '새 카테고리',
@@ -40,7 +41,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		'information' => '정보',
-		'keep_history' => '최소 유지 글 개수',
+		'keep_min' => '최소 유지 글 개수',
 		'moved_category_deleted' => '카테고리를 삭제하면, 해당 카테고리 아래에 있던 피드들은 자동적으로 <em>%s</em> 아래로 분류됩니다.',
 		'mute' => '무기한 새로고침 금지',
 		'no_selected' => '선택된 피드가 없습니다.',

+ 12 - 4
app/i18n/nl/conf.php

@@ -1,15 +1,23 @@
 <?php
-/* Dutch translation by Wanabo. http://www.nieuwskop.be */
+
 return array(
 	'archiving' => array(
 		'_' => 'Archivering',
-		'advanced' => 'Geavanceerd',
 		'delete_after' => 'Verwijder artikelen na',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'Meer opties zijn beschikbaar in de persoonlijke stroom instellingen',
-		'keep_history_by_feed' => 'Minimum aantal te behouden artikelen in de feed',
-		'optimize' => 'Optimaliseer database',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Minimum aantal te behouden artikelen in de feed',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
+		'optimize' => 'Optimaliseer database',	//TODO - Translation
 		'optimize_help' => 'Doe dit zo af en toe om de omvang van de database te verkleinen',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'Schoon nu op',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'Archivering',
 		'ttl' => 'Vernieuw niet automatisch meer dan',
 	),

+ 7 - 0
app/i18n/nl/gen.php

@@ -162,6 +162,13 @@ return array(
 		'nothing_to_load' => 'Er zijn geen artikelen meer',
 		'previous' => 'Vorige',
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 		'email' => 'Email',
 		'Known' => 'Known-gebaseerde sites',

+ 2 - 1
app/i18n/nl/sub.php

@@ -13,6 +13,7 @@ return array(
 	'category' => array(
 		'_' => 'Categorie',
 		'add' => 'Voeg categorie toe',
+		'archiving' => 'Archiveren',
 		'empty' => 'Lege categorie',
 		'information' => 'Informatie',
 		'new' => 'Nieuwe categorie',
@@ -40,7 +41,7 @@ return array(
 			'help' => 'Voer één zoekfilter per lijn in.',
 		),
 		'information' => 'Informatie',
-		'keep_history' => 'Minimum aantal artikelen om te houden',
+		'keep_min' => 'Minimum aantal artikelen om te houden',
 		'moved_category_deleted' => 'Als u een categorie verwijderd, worden de feeds automatisch geclassificeerd onder <em>%s</em>.',
 		'mute' => 'demp',
 		'no_selected' => 'Geen feed geselecteerd.',

+ 10 - 1
app/i18n/oc/conf.php

@@ -5,11 +5,20 @@ return array(
 		'_' => 'Archius',
 		'advanced' => 'Avançat',
 		'delete_after' => 'Levar los articles aprèp',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'Mai d’opcions son disponiblas dins la configuracion individuala dels fluxes',
-		'keep_history_by_feed' => 'Nombre minimum d’articles de servar per flux',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Nombre minimum d’articles de servar per flux',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'Optimizar la basa de donada',
 		'optimize_help' => 'De far de temps en temps per redusir la talha de la basa de donadas',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'Purgar ara',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'Archius',
 		'ttl' => 'Actualizar pas automaticament mai sovent que',
 	),

+ 7 - 0
app/i18n/oc/gen.php

@@ -162,6 +162,13 @@ return array(
 		'nothing_to_load' => 'I a pas mai d’articles',
 		'previous' => 'Precedent',
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',

+ 2 - 1
app/i18n/oc/sub.php

@@ -12,6 +12,7 @@ return array(
 	'category' => array(
 		'_' => 'Categoria',
 		'add' => 'Ajustar una categoria',
+		'archiving' => 'Archivar',
 		'empty' => 'Categoria voida',
 		'information' => 'Informacions',
 		'new' => 'Nòva categoria',
@@ -39,7 +40,7 @@ return array(
 			'help' => 'Escrivètz una recèrca per linha.',
 		),
 		'information' => 'Informacions',
-		'keep_history' => 'Nombre minimum d’articles de servar',
+		'keep_min' => 'Nombre minimum d’articles de servar',
 		'moved_category_deleted' => 'Quand escafatz una categoria, sos fluxes son automaticament classats dins <em>%s</em>.',
 		'mute' => 'mut',
 		'no_selected' => 'Cap de flux pas seleccionat.',

+ 10 - 2
app/i18n/pt-br/conf.php

@@ -3,13 +3,21 @@
 return array(
 	'archiving' => array(
 		'_' => 'Arquivar',
-		'advanced' => 'Avançado',
 		'delete_after' => 'Remover artigos depois',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'Mais opções estão disponíveis nas configurações individuais do feed',
-		'keep_history_by_feed' => 'Número mínimo de artigos para deixar no feed',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Número mínimo de artigos para deixar no feed',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'Otimizar banco de dados',
 		'optimize_help' => 'Faça ocasionalmente para reduzir o tamanho do banco de dados',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'Purge agora',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'Arquivar',
 		'ttl' => 'Não atualize automaticamente mais frequente que',
 	),

+ 7 - 0
app/i18n/pt-br/gen.php

@@ -162,6 +162,13 @@ return array(
 		'nothing_to_load' => 'Não há mais artigos',
 		'previous' => 'Anterior',
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',

+ 2 - 1
app/i18n/pt-br/sub.php

@@ -13,6 +13,7 @@ return array(
 	'category' => array(
 		'_' => 'Categoria',
 		'add' => 'Adicionar uma categoria',
+		'archiving' => 'Arquivar',
 		'empty' => 'Categoria vazia',
 		'information' => 'Informações',
 		'new' => 'Nova categoria',
@@ -40,7 +41,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		'information' => 'Informações',
-		'keep_history' => 'Número mínimo de artigos para manter',
+		'keep_min' => '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>.',
 		'mute' => 'mute',	//TODO - Translation
 		'no_selected' => 'Nenhum feed selecionado.',

+ 11 - 3
app/i18n/ru/conf.php

@@ -3,13 +3,21 @@
 return array(
 	'archiving' => array(
 		'_' => 'Архивация',
-		'advanced' => 'Продвинутые настройки',
 		'delete_after' => 'Удалять статьи после',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'Каждую подписку можно настроить более гибко',
-		'keep_history_by_feed' => 'Minimum number of articles to keep by feed', 	//TODO - Translation
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Minimum number of articles to keep by feed',	//TODO - Translation
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'Оптимизировать базу данных',
-		'optimize_help' => 'To do occasionally to reduce the size of the database', 	//TODO - Translation
+		'optimize_help' => 'To do occasionally to reduce the size of the database',	//TODO - Translation
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'Очистить сейчас',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'Архивация',
 		'ttl' => 'Не обновлять чаще чем',
 	),

+ 7 - 0
app/i18n/ru/gen.php

@@ -162,6 +162,13 @@ return array(
 		'nothing_to_load' => 'There are no more articles',	//TODO - Translation
 		'previous' => 'Previous',	//TODO - Translation
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',

+ 2 - 1
app/i18n/ru/sub.php

@@ -13,6 +13,7 @@ return array(
 	'category' => array(
 		'_' => 'Category',	//TODO - Translation
 		'add' => 'Add a category',	//TODO - Translation
+		'archiving' => 'Archivage',	//TODO - Translation
 		'empty' => 'Empty category',	//TODO - Translation
 		'information' => 'Information',	//TODO - Translation
 		'new' => 'New category',	//TODO - Translation
@@ -40,7 +41,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		'information' => 'Information',	//TODO - Translation
-		'keep_history' => 'Minimum number of articles to keep',	//TODO - Translation
+		'keep_min' => '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
 		'mute' => 'mute',	//TODO - Translation
 		'no_selected' => 'No feed selected.',	//TODO - Translation

+ 1 - 1
app/i18n/sk/conf.php

@@ -6,7 +6,7 @@ return array(
 		'advanced' => 'Pokročilé',
 		'delete_after' => 'Vymazať články po',
 		'help' => 'Viac možností nájdete v nastaveniach kanála',
-		'keep_history_by_feed' => 'Minimálny počet článkov kanála na zachovanie',
+		'keep_min_by_feed' => 'Minimálny počet článkov kanála na zachovanie',
 		'optimize' => 'Optimalizovať databázu',
 		'optimize_help' => 'Občas vykonajte na zmenšenie veľkosti databázy',
 		'purge_now' => 'Vyčistiť teraz',

+ 1 - 1
app/i18n/sk/sub.php

@@ -40,7 +40,7 @@ return array(
 			'help' => 'Napíšte jeden výraz hľadania na riadok.',
 		),
 		'information' => 'Informácia',
-		'keep_history' => 'Minimálny počet článkov na uchovanie',
+		'keep_min' => 'Minimálny počet článkov na uchovanie',
 		'moved_category_deleted' => 'Keď vymažete kategóriu, jej kanály sa automaticky zaradia pod <em>%s</em>.',
 		'mute' => 'stíšiť',
 		'no_selected' => 'Nevybrali ste kanál.',

+ 10 - 2
app/i18n/tr/conf.php

@@ -3,13 +3,21 @@
 return array(
 	'archiving' => array(
 		'_' => 'Arşiv',
-		'advanced' => 'Gelişmiş',
 		'delete_after' => 'Makelelerin tutulacağı süre',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => 'Akış ayarlarında daha çok ayar bulabilirsiniz',
-		'keep_history_by_feed' => 'Akışta en az tutulacak makale sayısı',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => 'Akışta en az tutulacak makale sayısı',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => 'Veritabanı optimize et',
 		'optimize_help' => 'Bu işlem bazen veritabanı boyutunu düşürmeye yardımcı olur',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => 'Şimdi temizle',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => 'Arşiv',
 		'ttl' => 'Şu süreden sık otomatik yenileme yapma',
 	),

+ 7 - 0
app/i18n/tr/gen.php

@@ -162,6 +162,13 @@ return array(
 		'nothing_to_load' => 'Başka makale yok',
 		'previous' => 'Önceki',
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',

+ 2 - 1
app/i18n/tr/sub.php

@@ -13,6 +13,7 @@ return array(
 	'category' => array(
 		'_' => 'Kategori',
 		'add' => 'Kategori ekle',
+		'archiving' => 'Arşiv',
 		'empty' => 'Boş kategori',
 		'information' => 'Bilgi',
 		'new' => 'Yeni kategori',
@@ -40,7 +41,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		'information' => 'Bilgi',
-		'keep_history' => 'En az tutulacak makale sayısı',
+		'keep_min' => 'En az tutulacak makale sayısı',
 		'moved_category_deleted' => 'Bir kategoriyi silerseniz, içerisindeki akışlar <em>%s</em> içerisine yerleşir.',
 		'mute' => 'mute',	//TODO - Translation
 		'no_selected' => 'Hiçbir akış seçilmedi.',

+ 10 - 2
app/i18n/zh-cn/conf.php

@@ -3,13 +3,21 @@
 return array(
 	'archiving' => array(
 		'_' => '存档',
-		'advanced' => '高级',
 		'delete_after' => '文章保留',
+		'exception' => 'Purge exception',	//TODO - Translation
 		'help' => '详细选项位于单独的 RSS 源设置',
-		'keep_history_by_feed' => '至少保存的文章数',
+		'keep_favourites' => 'Never delete favourites',	//TODO - Translation
+		'keep_min_by_feed' => '至少保存的文章数',
+		'keep_labels' => 'Never delete labels',	//TODO - Translation
+		'keep_unreads' => 'Never delete unreads',	//TODO - Translation
+		'maintenance' => 'Maintenance',	//TODO - Translation
 		'optimize' => '优化数据库',
 		'optimize_help' => '偶尔执行优化可以减少数据库大小',
+		'policy' => 'Purge policy',	//TODO - Translation
+		'policy_warning' => 'If no purge policy is selected, every article will be kept.',	//TODO - Translation
 		'purge_now' => '立即清除',
+		'keep_max' => 'Maximum number of articles to keep',	//TODO - Translation
+		'keep_period' => 'Maximum age of articles to keep',	//TODO - Translation
 		'title' => '存档',
 		'ttl' => '最小自动更新时间',
 	),

+ 7 - 0
app/i18n/zh-cn/gen.php

@@ -162,6 +162,13 @@ return array(
 		'nothing_to_load' => '没有更多文章了',
 		'previous' => '上一页',
 	),
+	'period' => array(
+		'days' => 'days',	//TODO - Translation
+		'hours' => 'hours',	//TODO - Translation
+		'months' => 'months',	//TODO - Translation
+		'weeks' => 'weeks',	//TODO - Translation
+		'years' => 'years',	//TODO - Translation
+	),
 	'share' => array(
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',

+ 2 - 1
app/i18n/zh-cn/sub.php

@@ -13,6 +13,7 @@ return array(
 	'category' => array(
 		'_' => '分类',
 		'add' => '添加分类',
+		'archiving' => '存档',
 		'empty' => '空分类',
 		'information' => '信息',
 		'new' => '新分类',
@@ -40,7 +41,7 @@ return array(
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
 		'information' => '信息',
-		'keep_history' => '至少保存的文章数',
+		'keep_min' => '至少保存的文章数',
 		'moved_category_deleted' => '删除分类时,其中的 RSS 源会自动归类到 <em>%s</em>',
 		'mute' => '暂停',
 		'no_selected' => '未选择 RSS 源。',

+ 2 - 18
app/install.php

@@ -86,7 +86,6 @@ function saveStep1() {
 		// Then, we set $_SESSION vars
 		$_SESSION['title'] = $system_conf->title;
 		$_SESSION['auth_type'] = $system_conf->auth_type;
-		$_SESSION['old_entries'] = $user_conf->old_entries;
 		$_SESSION['default_user'] = $current_user;
 		$_SESSION['passwordHash'] = $user_conf->passwordHash;
 
@@ -184,14 +183,12 @@ function saveStep3() {
 	if (!empty($_POST)) {
 		$system_default_config = Minz_Configuration::get('default_system');
 		$_SESSION['title'] = $system_default_config->title;
-		$_SESSION['old_entries'] = param('old_entries', $user_default_config->old_entries);
 		$_SESSION['auth_type'] = param('auth_type', 'form');
 		if (FreshRSS_user_Controller::checkUsername(param('default_user', ''))) {
 			$_SESSION['default_user'] = param('default_user', '');
 		}
 
-		if (empty($_SESSION['old_entries']) ||
-		    empty($_SESSION['auth_type']) ||
+		if (empty($_SESSION['auth_type']) ||
 		    empty($_SESSION['default_user'])) {
 			return false;
 		}
@@ -208,10 +205,6 @@ function saveStep3() {
 		FreshRSS_Context::$system_conf->default_user = $_SESSION['default_user'];
 		FreshRSS_Context::$system_conf->save();
 
-		if ((!ctype_digit($_SESSION['old_entries'])) ||($_SESSION['old_entries'] < 1)) {
-			$_SESSION['old_entries'] = $user_default_config->old_entries;
-		}
-
 		// Create default user files but first, we delete previous data to
 		// avoid access right problems.
 		recursive_unlink(USERS_PATH . '/' . $_SESSION['default_user']);
@@ -225,7 +218,6 @@ function saveStep3() {
 				'',
 				[
 					'language' => $_SESSION['language'],
-					'old_entries' => $_SESSION['old_entries'],
 				]
 			);
 		} catch (Exception $e) {
@@ -317,8 +309,7 @@ function checkStep2() {
 }
 
 function checkStep3() {
-	$conf = !empty($_SESSION['old_entries']) &&
-	        !empty($_SESSION['default_user']);
+	$conf = !empty($_SESSION['default_user']);
 
 	$form = isset($_SESSION['auth_type']);
 
@@ -593,13 +584,6 @@ function printStep3() {
 	<form action="index.php?step=3" method="post">
 		<legend><?php echo _t('install.conf'); ?></legend>
 
-		<div class="form-group">
-			<label class="group-name" for="old_entries"><?php echo _t('install.delete_articles_after'); ?></label>
-			<div class="group-controls">
-				<input type="number" id="old_entries" name="old_entries" required="required" min="1" max="1200" value="<?php echo isset($_SESSION['old_entries']) ? $_SESSION['old_entries'] : $user_default_config->old_entries; ?>" tabindex="2" /> <?php echo _t('gen.date.month'); ?>
-			</div>
-		</div>
-
 		<div class="form-group">
 			<label class="group-name" for="default_user"><?php echo _t('install.default_user'); ?></label>
 			<div class="group-controls">

+ 86 - 24
app/views/configure/archiving.phtml

@@ -8,23 +8,6 @@
 		<legend><?php echo _t('conf.archiving'); ?></legend>
 		<p><?php echo _i('help'); ?> <?php echo _t('conf.archiving.help'); ?></p>
 
-		<div class="form-group">
-			<label class="group-name" for="old_entries"><?php echo _t('conf.archiving.delete_after'); ?></label>
-			<div class="group-controls">
-				<input type="number" id="old_entries" name="old_entries" min="1" max="1200" value="<?php echo FreshRSS_Context::$user_conf->old_entries; ?>" data-leave-validation="<?php echo FreshRSS_Context::$user_conf->old_entries; ?>"/> <?php echo _t('gen.date.month'); ?>
-				  <a class="btn confirm" href="<?php echo _url('entry', 'purge'); ?>"><?php echo _t('conf.archiving.purge_now'); ?></a>
-			</div>
-		</div>
-		<div class="form-group">
-			<label class="group-name" for="keep_history_default"><?php echo _t('conf.archiving.keep_history_by_feed'); ?></label>
-			<div class="group-controls">
-				<select class="number" name="keep_history_default" id="keep_history_default" required="required" data-leave-validation="<?php echo FreshRSS_Context::$user_conf->keep_history_default; ?>"><?php
-					foreach (array('' => '', 0 => '0', 10 => '10', 50 => '50', 100 => '100', 500 => '500', 1000 => '1 000', 5000 => '5 000', 10000 => '10 000', FreshRSS_Feed::KEEP_HISTORY_INFINITE => '∞') as $v => $t) {
-						echo '<option value="' . $v . (FreshRSS_Context::$user_conf->keep_history_default == $v ? '" selected="selected' : '') . '">' . $t . ' </option>';
-					}
-				?></select> (<?php echo _t('gen.short.by_default'); ?>)
-			</div>
-		</div>
 		<div class="form-group">
 			<label class="group-name" for="ttl_default"><?php echo _t('conf.archiving.ttl'); ?></label>
 			<div class="group-controls">
@@ -47,6 +30,76 @@
 			</div>
 		</div>
 
+		<p class="alert alert-warn">
+			<?= _t('conf.archiving.policy_warning') ?>
+		</p>
+
+		<div class="form-group">
+			<label class="group-name"><?= _t('conf.archiving.policy') ?><br /><small>(<?= _t('gen.short.by_default') ?>)</small></label>
+			<div class="group-controls">
+				<label class="checkbox" for="enable_keep_max">
+					<input type="checkbox" name="enable_keep_max" id="enable_keep_max" value="1"<?= empty(FreshRSS_Context::$user_conf->archiving['keep_max']) ? '' : ' checked="checked"' ?> data-leave-validation="<?= empty(FreshRSS_Context::$user_conf->archiving['keep_max']) ? 0 : 1 ?>"/>
+					<?= _t('conf.archiving.keep_max') ?>
+					<?php $keepMax = empty(FreshRSS_Context::$user_conf->archiving['keep_max']) ? 200 : FreshRSS_Context::$user_conf->archiving['keep_max']; ?>
+					<input type="number" id="keep_max" name="keep_max" min="0" value="<?= $keepMax ?>" data-leave-validation="<?= $keepMax ?>"/>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="enable_keep_period">
+					<input type="checkbox" name="enable_keep_period" id="enable_keep_period" value="1"<?= FreshRSS_Context::$user_conf->volatile['enable_keep_period'] ? ' checked="checked"' : '' ?> data-leave-validation="<?= FreshRSS_Context::$user_conf->volatile['enable_keep_period'] ? 1 : 0 ?>"/>
+					<?= _t('conf.archiving.keep_period') ?>
+					<input type="number" id="keep_period_count" name="keep_period_count" min="0" value="<?= FreshRSS_Context::$user_conf->volatile['keep_period_count'] ?>" data-leave-validation="<?= FreshRSS_Context::$user_conf->volatile['keep_period_count'] ?>"/>
+					<select class="number" name="keep_period_unit" id="keep_period_unit" data-leave-validation="<?= FreshRSS_Context::$user_conf->volatile['keep_period_unit'] ?>">
+						<option></option>
+						<option value="P1Y" <?= 'P1Y' === FreshRSS_Context::$user_conf->volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.years') ?></option>
+						<option value="P1M" <?= 'P1M' === FreshRSS_Context::$user_conf->volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.months') ?></option>
+						<option value="P1W" <?= 'P1W' === FreshRSS_Context::$user_conf->volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.weeks') ?></option>
+						<option value="P1D" <?= 'P1D' === FreshRSS_Context::$user_conf->volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.days') ?></option>
+						<option value="PT1H" <?= 'PT1H' === FreshRSS_Context::$user_conf->volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.hours') ?></option>
+					</select>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name"><?= _t('conf.archiving.exception') ?><br /><small>(<?= _t('gen.short.by_default') ?>)</small></label>
+			<div class="group-controls">
+				<label class="checkbox" for="keep_favourites">
+					<input type="checkbox" name="keep_favourites" id="keep_favourites" value="1"<?= FreshRSS_Context::$user_conf->archiving['keep_favourites'] !== false ? ' checked="checked"' : '' ?> data-leave-validation="<?= FreshRSS_Context::$user_conf->archiving['keep_favourites'] !== false ? 1 : 0 ?>"/>
+					<?= _t('conf.archiving.keep_favourites') ?>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="keep_labels">
+					<input type="checkbox" name="keep_labels" id="keep_labels" value="1"<?= FreshRSS_Context::$user_conf->archiving['keep_labels'] !== false ? ' checked="checked"' : '' ?> data-leave-validation="<?= FreshRSS_Context::$user_conf->archiving['keep_labels'] !== false ? 1 : 0 ?>"/>
+					<?= _t('conf.archiving.keep_labels') ?>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label class="checkbox" for="keep_unreads">
+					<input type="checkbox" name="keep_unreads" id="keep_unreads" value="1"<?= FreshRSS_Context::$user_conf->archiving['keep_unreads'] ? ' checked="checked"' : '' ?> data-leave-validation="<?= FreshRSS_Context::$user_conf->archiving['keep_unreads'] ? 1 : 0 ?>"/>
+					<?= _t('conf.archiving.keep_unreads') ?>
+				</label>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<label for="keep_min_default"><?php echo _t('conf.archiving.keep_min_by_feed'); ?>
+					<input type="number" id="keep_min_default" name="keep_min_default" min="0" value="<?= FreshRSS_Context::$user_conf->archiving['keep_min'] ?>" data-leave-validation="<?= FreshRSS_Context::$user_conf->archiving['keep_min'] ?>">
+				</label>
+			</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>
@@ -55,9 +108,9 @@
 		</div>
 	</form>
 
-	<form method="post" action="<?php echo _url('entry', 'optimize'); ?>">
+	<legend><?php echo _t('conf.archiving.maintenance'); ?></legend>
+	<form method="post" action="<?php echo _url('entry', 'purge'); ?>">
 		<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
-		<legend><?php echo _t('conf.archiving.advanced'); ?></legend>
 
 		<div class="form-group">
 			<label class="group-name"><?php echo _t('conf.user.current'); ?></label>
@@ -66,21 +119,30 @@
 			</div>
 		</div>
 
-		<?php if (FreshRSS_Auth::hasAccess('admin')) { ?>
 		<div class="form-group">
-			<label class="group-name"><?php echo _t('conf.user.users'); ?></label>
 			<div class="group-controls">
-				<?php echo format_bytes($this->size_total); ?>
+				<button type="submit" class="btn btn-important confirm"><?php echo _t('conf.archiving.purge_now'); ?></button>
 			</div>
 		</div>
+	</form>
 
-		<div class="form-group form-actions">
+	<form method="post" action="<?php echo _url('entry', 'optimize'); ?>">
+		<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
+		<div class="form-group">
 			<div class="group-controls">
 				<input type="hidden" name="optimiseDatabase" value="1" />
 				<button type="submit" class="btn btn-important"><?php echo _t('conf.archiving.optimize'); ?></button>
 				<?php echo _i('help'); ?> <?php echo _t('conf.archiving.optimize_help'); ?>
 			</div>
 		</div>
-		<?php } ?>
 	</form>
+
+	<?php if (FreshRSS_Auth::hasAccess('admin')): ?>
+	<div class="form-group">
+		<label class="group-name"><?php echo _t('conf.user.users'); ?></label>
+		<div class="group-controls">
+			<?php echo format_bytes($this->size_total); ?>
+		</div>
+	</div>
+	<?php endif; ?>
 </div>

+ 116 - 0
app/views/helpers/category/update.phtml

@@ -33,5 +33,121 @@
 				<?php endif;?>
 			</div>
 		</div>
+
+		<legend><?php echo _t('sub.category.archiving'); ?></legend>
+		<?php
+			$archiving = $this->category->attributes('archiving');
+			if (empty($archiving)) {
+				$archiving = [ 'default' => true ];
+			} else {
+				$archiving['default'] = false;
+			}
+			$volatile = [
+				'enable_keep_period' => false,
+				'keep_period_count' => '3',
+				'keep_period_unit' => 'P1M',
+			];
+			if (!empty($archiving['keep_period'])) {
+				if (preg_match('/^PT?(?P<count>\d+)[YMWDH]$/', $archiving['keep_period'], $matches)) {
+					$volatile['enable_keep_period'] = true;
+					$volatile['keep_period_count'] = $matches['count'];
+					$volatile['keep_period_unit'] = str_replace($matches['count'], '1', $archiving['keep_period']);
+				}
+			}
+			//Defaults
+			if (!isset($archiving['keep_max'])) {
+				$archiving['keep_max'] = false;
+			}
+			if (!isset($archiving['keep_favourites'])) {
+				$archiving['keep_favourites'] = true;
+			}
+			if (!isset($archiving['keep_labels'])) {
+				$archiving['keep_labels'] = true;
+			}
+			if (!isset($archiving['keep_unreads'])) {
+				$archiving['keep_unreads'] = false;
+			}
+			if (!isset($archiving['keep_min'])) {
+				$archiving['keep_min'] = 50;
+			}
+		?>
+
+		<p class="alert alert-warn">
+			<?= _t('conf.archiving.policy_warning') ?>
+		</p>
+
+		<div class="form-group">
+			<label class="group-name"><?= _t('conf.archiving.policy') ?></label>
+			<div class="group-controls">
+				<label class="checkbox">
+					<input type="checkbox" name="use_default_purge_options" id="use_default_purge_options" value="1"<?= $archiving['default'] ? ' checked="checked"' : '' ?> data-leave-validation="<?= $archiving['default'] ? 1 : 0 ?>" />
+					<?= _t('gen.short.by_default') ?>
+				</label>
+			</div>
+		</div>
+		<div class="form-group archiving"<?= $archiving['default'] ? ' hidden="hidden"' : '' ?>>
+			<div class="group-controls">
+				<label class="checkbox" for="enable_keep_max">
+					<input type="checkbox" name="enable_keep_max" id="enable_keep_max" value="1"<?= empty($archiving['keep_max']) ? '' : ' checked="checked"' ?> data-leave-validation="<?= empty($archiving['keep_max']) ? 0 : 1 ?>"/>
+					<?= _t('conf.archiving.keep_max') ?>
+					<input type="number" id="keep_max" name="keep_max" min="0" value="<?= empty($archiving['keep_max']) ? 200 : $archiving['keep_max'] ?>" data-leave-validation="<?= empty($archiving['keep_max']) ? 200 : $archiving['keep_max'] ?>"/>
+				</label>
+			</div>
+		</div>
+		<div class="form-group archiving"<?= $archiving['default'] ? ' hidden="hidden"' : '' ?>>
+			<div class="group-controls">
+				<label class="checkbox" for="enable_keep_period">
+					<input type="checkbox" name="enable_keep_period" id="enable_keep_period" value="1"<?= $volatile['enable_keep_period'] ? ' checked="checked"' : '' ?> data-leave-validation="<?= $volatile['enable_keep_period'] ? 1 : 0 ?>"/>
+					<?= _t('conf.archiving.keep_period') ?>
+					<input type="number" id="keep_period_count" name="keep_period_count" min="0" value="<?= $volatile['keep_period_count'] ?>" data-leave-validation="<?= $volatile['keep_period_count'] ?>"/>
+					<select class="number" name="keep_period_unit" id="keep_period_unit" data-leave-validation="<?= $volatile['keep_period_unit'] ?>">
+						<option></option>
+						<option value="P1Y" <?= 'P1Y' === $volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.years') ?></option>
+						<option value="P1M" <?= 'P1M' === $volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.months') ?></option>
+						<option value="P1W" <?= 'P1W' === $volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.weeks') ?></option>
+						<option value="P1D" <?= 'P1D' === $volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.days') ?></option>
+						<option value="PT1H" <?= 'PT1H' === $volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.hours') ?></option>
+					</select>
+				</label>
+			</div>
+		</div>
+		<div class="form-group archiving"<?= $archiving['default'] ? ' hidden="hidden"' : '' ?>>
+			<label class="group-name"><?= _t('conf.archiving.exception') ?></label>
+			<div class="group-controls">
+				<label class="checkbox" for="keep_favourites">
+					<input type="checkbox" name="keep_favourites" id="keep_favourites" value="1"<?= $archiving['keep_favourites'] ? ' checked="checked"' : '' ?> data-leave-validation="<?= $archiving['keep_favourites'] ? 1 : 0 ?>"/>
+					<?= _t('conf.archiving.keep_favourites') ?>
+				</label>
+			</div>
+		</div>
+		<div class="form-group archiving"<?= $archiving['default'] ? ' hidden="hidden"' : '' ?>>
+			<div class="group-controls">
+				<label class="checkbox" for="keep_labels">
+					<input type="checkbox" name="keep_labels" id="keep_labels" value="1"<?= $archiving['keep_labels'] ? ' checked="checked"' : '' ?> data-leave-validation="<?= $archiving['keep_labels'] ? 1 : 0 ?>"/>
+					<?= _t('conf.archiving.keep_labels') ?>
+				</label>
+			</div>
+		</div>
+		<div class="form-group archiving"<?= $archiving['default'] ? ' hidden="hidden"' : '' ?>>
+			<div class="group-controls">
+				<label class="checkbox" for="keep_unreads">
+					<input type="checkbox" name="keep_unreads" id="keep_unreads" value="1"<?= $archiving['keep_unreads'] ? ' checked="checked"' : '' ?> data-leave-validation="<?= $archiving['keep_unreads'] ? 1 : 0 ?>"/>
+					<?= _t('conf.archiving.keep_unreads') ?>
+				</label>
+			</div>
+		</div>
+		<div class="form-group archiving"<?= $archiving['default'] ? ' hidden="hidden"' : '' ?>>
+			<div class="group-controls">
+				<label for="keep_min"><?php echo _t('sub.feed.keep_min'); ?>
+					<input type="number" id="keep_min" name="keep_min" min="0" value="<?= $archiving['keep_min'] ?>" data-leave-validation="<?= $archiving['keep_min'] ?>">
+				</label>
+			</div>
+		</div>
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button class="btn btn-important"><?= _t('gen.action.submit') ?></button>
+				<button type="reset" class="btn"><?= _t('gen.action.cancel') ?></button>
+			</div>
+		</div>
 	</form>
 </div>

+ 107 - 6
app/views/helpers/feed/update.phtml

@@ -78,6 +78,7 @@
 		<div class="form-group form-actions">
 			<div class="group-controls">
 				<button class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>
+				<button type="reset" class="btn"><?php echo _t('gen.action.cancel'); ?></button>
 				<button class="btn btn-attention confirm"
 				        data-str-confirm="<?php echo _t('gen.js.confirm_action_feed_cat'); ?>"
 				        formaction="<?php echo _url('feed', 'delete', 'id', $this->feed->id()); ?>"
@@ -97,16 +98,115 @@
 				</div>
 			</div>
 		</div>
+		<?php
+			$archiving = $this->feed->attributes('archiving');
+			if (empty($archiving)) {
+				$archiving = [ 'default' => true ];
+			} else {
+				$archiving['default'] = false;
+			}
+			$volatile = [
+				'enable_keep_period' => false,
+				'keep_period_count' => '3',
+				'keep_period_unit' => 'P1M',
+			];
+			if (!empty($archiving['keep_period'])) {
+				if (preg_match('/^PT?(?P<count>\d+)[YMWDH]$/', $archiving['keep_period'], $matches)) {
+					$volatile['enable_keep_period'] = true;
+					$volatile['keep_period_count'] = $matches['count'];
+					$volatile['keep_period_unit'] = str_replace($matches['count'], '1', $archiving['keep_period']);
+				}
+			}
+			//Defaults
+			if (!isset($archiving['keep_max'])) {
+				$archiving['keep_max'] = false;
+			}
+			if (!isset($archiving['keep_min'])) {
+				$archiving['keep_min'] = 50;
+			}
+			if (!isset($archiving['keep_favourites'])) {
+				$archiving['keep_favourites'] = true;
+			}
+			if (!isset($archiving['keep_labels'])) {
+				$archiving['keep_labels'] = true;
+			}
+			if (!isset($archiving['keep_unreads'])) {
+				$archiving['keep_unreads'] = false;
+			}
+		?>
+
+		<p class="alert alert-warn">
+			<?= _t('conf.archiving.policy_warning') ?>
+		</p>
+
 		<div class="form-group">
-			<label class="group-name" for="keep_history"><?php echo _t('sub.feed.keep_history'); ?></label>
+			<label class="group-name"><?= _t('conf.archiving.policy') ?></label>
 			<div class="group-controls">
-				<select class="number" name="keep_history" id="keep_history" required="required"><?php
-					foreach (array('' => '', FreshRSS_Feed::KEEP_HISTORY_DEFAULT => _t('gen.short.by_default'), 0 => '0', 10 => '10', 50 => '50', 100 => '100', 500 => '500', 1000 => '1 000', 5000 => '5 000', 10000 => '10 000', FreshRSS_Feed::KEEP_HISTORY_INFINITE => '∞') as $v => $t) {
-						echo '<option value="' . $v . ($this->feed->keepHistory() === $v ? '" selected="selected' : '') . '">' . $t . '</option>';
-					}
-				?></select>
+				<label class="checkbox">
+					<input type="checkbox" name="use_default_purge_options" id="use_default_purge_options" value="1"<?= $archiving['default'] ? ' checked="checked"' : '' ?> data-leave-validation="<?= $archiving['default'] ? 1 : 0 ?>" />
+					<?= _t('gen.short.by_default') ?>
+				</label>
 			</div>
 		</div>
+		<div class="form-group archiving"<?= $archiving['default'] ? ' hidden="hidden"' : '' ?>>
+			<div class="group-controls">
+				<label class="checkbox" for="enable_keep_max">
+					<input type="checkbox" name="enable_keep_max" id="enable_keep_max" value="1"<?= empty($archiving['keep_max']) ? '' : ' checked="checked"' ?> data-leave-validation="<?= empty($archiving['keep_max']) ? 0 : 1 ?>"/>
+					<?= _t('conf.archiving.keep_max') ?>
+					<input type="number" id="keep_max" name="keep_max" min="0" value="<?= empty($archiving['keep_max']) ? 200 : $archiving['keep_max'] ?>" data-leave-validation="<?= empty($archiving['keep_max']) ? 200 : $archiving['keep_max'] ?>"/>
+				</label>
+			</div>
+		</div>
+		<div class="form-group archiving"<?= $archiving['default'] ? ' hidden="hidden"' : '' ?>>
+			<div class="group-controls">
+				<label class="checkbox" for="enable_keep_period">
+					<input type="checkbox" name="enable_keep_period" id="enable_keep_period" value="1"<?= $volatile['enable_keep_period'] ? ' checked="checked"' : '' ?> data-leave-validation="<?= $volatile['enable_keep_period'] ? 1 : 0 ?>"/>
+					<?= _t('conf.archiving.keep_period') ?>
+					<input type="number" id="keep_period_count" name="keep_period_count" min="0" value="<?= $volatile['keep_period_count'] ?>" data-leave-validation="<?= $volatile['keep_period_count'] ?>"/>
+					<select class="number" name="keep_period_unit" id="keep_period_unit" data-leave-validation="<?= $volatile['keep_period_unit'] ?>">
+						<option></option>
+						<option value="P1Y" <?= 'P1Y' === $volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.years') ?></option>
+						<option value="P1M" <?= 'P1M' === $volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.months') ?></option>
+						<option value="P1W" <?= 'P1W' === $volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.weeks') ?></option>
+						<option value="P1D" <?= 'P1D' === $volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.days') ?></option>
+						<option value="PT1H" <?= 'PT1H' === $volatile['keep_period_unit'] ? 'selected="selected"' : '' ?>><?= _t('gen.period.hours') ?></option>
+					</select>
+				</label>
+			</div>
+		</div>
+		<div class="form-group archiving"<?= $archiving['default'] ? ' hidden="hidden"' : '' ?>>
+			<label class="group-name"><?= _t('conf.archiving.exception') ?></label>
+			<div class="group-controls">
+				<label class="checkbox" for="keep_favourites">
+					<input type="checkbox" name="keep_favourites" id="keep_favourites" value="1"<?= $archiving['keep_favourites'] ? ' checked="checked"' : '' ?> data-leave-validation="<?= $archiving['keep_favourites'] ? 1 : 0 ?>"/>
+					<?= _t('conf.archiving.keep_favourites') ?>
+				</label>
+			</div>
+		</div>
+		<div class="form-group archiving"<?= $archiving['default'] ? ' hidden="hidden"' : '' ?>>
+			<div class="group-controls">
+				<label class="checkbox" for="keep_labels">
+					<input type="checkbox" name="keep_labels" id="keep_labels" value="1"<?= $archiving['keep_labels'] ? ' checked="checked"' : '' ?> data-leave-validation="<?= $archiving['keep_labels'] ? 1 : 0 ?>"/>
+					<?= _t('conf.archiving.keep_labels') ?>
+				</label>
+			</div>
+		</div>
+		<div class="form-group archiving"<?= $archiving['default'] ? ' hidden="hidden"' : '' ?>>
+			<div class="group-controls">
+				<label class="checkbox" for="keep_unreads">
+					<input type="checkbox" name="keep_unreads" id="keep_unreads" value="1"<?= $archiving['keep_unreads'] ? ' checked="checked"' : '' ?> data-leave-validation="<?= $archiving['keep_unreads'] ?>"/>
+					<?= _t('conf.archiving.keep_unreads') ?>
+				</label>
+			</div>
+		</div>
+		<div class="form-group archiving"<?= $archiving['default'] ? ' hidden="hidden"' : '' ?>>
+			<div class="group-controls">
+				<label for="keep_min"><?php echo _t('sub.feed.keep_min'); ?>
+					<input type="number" id="keep_min" name="keep_min" min="0" value="<?= $archiving['keep_min'] ?>" data-leave-validation="<?= $archiving['keep_min'] ?>">
+				</label>
+			</div>
+		</div>
+
 		<div class="form-group">
 			<label class="group-name" for="ttl"><?php echo _t('sub.feed.ttl'); ?></label>
 			<div class="group-controls">
@@ -143,6 +243,7 @@
 		<div class="form-group form-actions">
 			<div class="group-controls">
 				<button class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>
+				<button type="reset" class="btn"><?php echo _t('gen.action.cancel'); ?></button>
 				<button class="btn btn-attention confirm" formmethod="post" formaction="<?php echo _url('feed', 'truncate', 'id', $this->feed->id()); ?>"><?php echo _t('gen.action.truncate'); ?></button>
 			</div>
 		</div>

+ 2 - 2
cli/_update-or-create-user.php

@@ -45,8 +45,8 @@ $values = array(
 		'language' => strParam('language'),
 		'mail_login' => strParam('email'),
 		'token' => strParam('token'),
-		'old_entries' => intParam('purge_after_months'),
-		'keep_history_default' => intParam('feed_min_articles_default'),
+		'old_entries' => intParam('purge_after_months'),	//TODO: Update with new mechanism
+		'keep_history_default' => intParam('feed_min_articles_default'),	//TODO: Update with new mechanism
 		'ttl_default' => intParam('feed_ttl_default'),
 		'since_hours_posts_per_rss' => intParam('since_hours_posts_per_rss'),
 		'min_posts_per_rss' => intParam('min_posts_per_rss'),

+ 8 - 2
config-user.default.php

@@ -5,8 +5,14 @@
 # override.
 return array (
 	'language' => 'en',
-	'old_entries' => 3,
-	'keep_history_default' => 50,
+	'archiving' => [
+		'keep_period' => 'P3M',
+		'keep_max' => 200,
+		'keep_min' => 50,
+		'keep_favourites' => true,
+		'keep_labels' => true,
+		'keep_unreads' => false,
+	],
 	'ttl_default' => 3600,
 	'mail_login' => '',
 	'email_validation_token' => '',

+ 6 - 0
lib/Minz/Request.php

@@ -52,6 +52,12 @@ class Minz_Request {
 		}
 		return null;
 	}
+	public static function paramBoolean($key) {
+		if (null === $value = self::paramTernary($key)) {
+			return false;
+		}
+		return $value;
+	}
 	public static function defaultControllerName() {
 		return self::$default_controller_name;
 	}

+ 5 - 1
lib/lib_rss.php

@@ -300,7 +300,11 @@ function invalidateHttpCache($username = '') {
 		Minz_Session::_param('touch', uTimeString());
 		$username = Minz_Session::param('currentUser', '_');
 	}
-	return touch(join_path(DATA_PATH, 'users', $username, 'log.txt'));
+	$ok = @touch(DATA_PATH . '/users/' . $username . '/log.txt');
+	if (!$ok) {
+		//TODO: Display notification error on front-end
+	}
+	return $ok;
 }
 
 function listUsers() {

+ 23 - 0
p/scripts/category.js

@@ -137,11 +137,34 @@ function init_draggable() {
 		};
 }
 
+function archiving() {
+	const slider = document.getElementById('slider');
+	slider.addEventListener('change', function (e) {
+		if (e.target.id === 'use_default_purge_options') {
+			slider.querySelectorAll('.archiving').forEach(function (element) {
+				element.hidden = e.target.checked;
+			});
+		}
+	});
+	slider.addEventListener('click', function (e) {
+		if (e.target.closest('button[type=reset]')) {
+			const archiving = document.getElementById('use_default_purge_options');
+			if (archiving) {
+				slider.querySelectorAll('.archiving').forEach(function (element) {
+					element.hidden = archiving.getAttribute('data-leave-validation') == 1;
+				});
+			}
+		}
+	});
+}
+
 if (document.readyState && document.readyState !== 'loading') {
 	init_draggable();
+	archiving();
 } else if (document.addEventListener) {
 	document.addEventListener('DOMContentLoaded', function () {
 		init_draggable();
+		archiving();
 	}, false);
 }
 // @license-end

+ 25 - 13
p/scripts/extra.js

@@ -184,12 +184,32 @@ function init_slider_observers() {
 		};
 
 	closer.onclick = function (ev) {
-			closer.classList.remove('active');
-			slider.classList.remove('active');
-			return false;
+			if (data_leave_validation() || confirm(context.i18n.confirmation_default)) {
+				slider.querySelectorAll('form').forEach(function (f) { f.reset(); });
+				closer.classList.remove('active');
+				slider.classList.remove('active');
+				return true;
+			} else {
+				return false;
+			}
 		};
 }
 
+function data_leave_validation() {
+	const ds = document.querySelectorAll('[data-leave-validation]');
+	for (let i = ds.length - 1; i >= 0; i--) {
+		const input = ds[i];
+		if (input.type === 'checkbox' || input.type === 'radio') {
+			if (input.checked != input.getAttribute('data-leave-validation')) {
+				return false;
+			}
+		} else if (input.value != input.getAttribute('data-leave-validation')) {
+			return false;
+		}
+	}
+	return true;
+}
+
 function init_configuration_alert() {
 	window.onsubmit = function (e) {
 			window.hasSubmit = true;
@@ -198,16 +218,8 @@ function init_configuration_alert() {
 			if (window.hasSubmit) {
 				return;
 			}
-			const ds = document.querySelectorAll('[data-leave-validation]');
-			for (let i = ds.length - 1; i >= 0; i--) {
-				const input = ds[i];
-				if (input.type === 'checkbox' || input.type === 'radio') {
-					if (input.checked != input.getAttribute('data-leave-validation')) {
-						return false;
-					}
-				} else if (input.value != input.getAttribute('data-leave-validation')) {
-					return false;
-				}
+			if (!data_leave_validation()) {
+				return false;
 			}
 		};
 }

+ 3 - 0
p/themes/base-theme/template.css

@@ -101,6 +101,9 @@ label {
 input {
 	width: 180px;
 }
+input[type=number] {
+	width: 6em;
+}
 
 textarea,
 input[type="file"],

+ 0 - 5
phpcs.xml

@@ -3,8 +3,6 @@
 	<description>Created with the PHP Coding Standard Generator. https://edorian.github.com/php-coding-standard-generator/</description>
 	<!-- to circumvent https://github.com/squizlabs/PHP_CodeSniffer/pull/1404 -->
 	<arg name="tab-width" value="40"/>
-	<exclude-pattern>./static</exclude-pattern>
-	<exclude-pattern>./vendor</exclude-pattern>
 	<exclude-pattern>./lib/SimplePie/</exclude-pattern>
 	<exclude-pattern>./lib/PHPMailer/</exclude-pattern>
 	<exclude-pattern>./lib/http-conditional.php</exclude-pattern>
@@ -28,9 +26,6 @@
 		<exclude-pattern>./app/install.php</exclude-pattern>
 		<!-- @todo remove test exclusion -->
 		<exclude-pattern>./tests/app/</exclude-pattern>
-		<!-- @todo remove SQL exclusion -->
-		<exclude-pattern>./app/SQL/install.sql.mysql.php</exclude-pattern>
-		<exclude-pattern>./app/SQL/install.sql.pgsql.php</exclude-pattern>
 		<properties>
 			<property name="lineLimit" value="100"/>
 			<!-- needs to be large to accomodate extra large tab width to circumvent https://github.com/squizlabs/PHP_CodeSniffer/pull/1404 -->