aboutsummaryrefslogtreecommitdiff
path: root/app/Models
diff options
context:
space:
mode:
authorGravatar Alexis Degrugillier <aledeg@users.noreply.github.com> 2019-10-23 00:52:15 +0200
committerGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2019-10-23 00:52:15 +0200
commitcc0db9af4f980829faa4bf0960617807b32fb4fa (patch)
tree8a418edeee15e878a12dbff02e241d2d76820be6 /app/Models
parentd7f888392678d711478ed64f4a52f43021d143af (diff)
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
Diffstat (limited to 'app/Models')
-rw-r--r--app/Models/Category.php24
-rw-r--r--app/Models/CategoryDAO.php122
-rw-r--r--app/Models/CategoryDAOSQLite.php17
-rw-r--r--app/Models/ConfigurationSetter.php10
-rw-r--r--app/Models/Context.php19
-rw-r--r--app/Models/DatabaseDAO.php12
-rw-r--r--app/Models/DatabaseDAOSQLite.php2
-rw-r--r--app/Models/EntryDAO.php65
-rw-r--r--app/Models/Factory.php8
-rw-r--r--app/Models/Feed.php38
-rw-r--r--app/Models/FeedDAO.php20
-rw-r--r--app/Models/FeedDAOSQLite.php2
-rw-r--r--app/Models/Tag.php2
-rw-r--r--app/Models/TagDAO.php14
-rw-r--r--app/Models/UserDAO.php11
15 files changed, 280 insertions, 86 deletions
diff --git a/app/Models/Category.php b/app/Models/Category.php
index 29c0e586b..a0ee1ddaa 100644
--- a/app/Models/Category.php
+++ b/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;
+ }
+ }
}
diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php
index dd49b542d..1b8717e83 100644
--- a/app/Models/CategoryDAO.php
+++ b/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;
}
diff --git a/app/Models/CategoryDAOSQLite.php b/app/Models/CategoryDAOSQLite.php
new file mode 100644
index 000000000..e32545c90
--- /dev/null
+++ b/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;
+ }
+
+}
diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php
index 963d37e2b..b1d271f41 100644
--- a/app/Models/ConfigurationSetter.php
+++ b/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 : '';
}
diff --git a/app/Models/Context.php b/app/Models/Context.php
index 95dc47c8c..878b72c69 100644
--- a/app/Models/Context.php
+++ b/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;
+ }
}
/**
diff --git a/app/Models/DatabaseDAO.php b/app/Models/DatabaseDAO.php
index f6cd2f756..a36b469b1 100644
--- a/app/Models/DatabaseDAO.php
+++ b/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);
diff --git a/app/Models/DatabaseDAOSQLite.php b/app/Models/DatabaseDAOSQLite.php
index b1473ab09..413e7ee09 100644
--- a/app/Models/DatabaseDAOSQLite.php
+++ b/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;
}
}
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index 5ff3a5b70..6a8a25b3e 100644
--- a/app/Models/EntryDAO.php
+++ b/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;
}
}
diff --git a/app/Models/Factory.php b/app/Models/Factory.php
index 6f2ca2217..69885c205 100644
--- a/app/Models/Factory.php
+++ b/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) {
diff --git a/app/Models/Feed.php b/app/Models/Feed.php
index 8aee9d62f..0a45a1f4c 100644
--- a/app/Models/Feed.php
+++ b/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';
}
diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php
index d4a91c145..fa0001df7 100644
--- a/app/Models/FeedDAO.php
+++ b/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);
diff --git a/app/Models/FeedDAOSQLite.php b/app/Models/FeedDAOSQLite.php
index c56447df6..0f685867a 100644
--- a/app/Models/FeedDAOSQLite.php
+++ b/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);
}
diff --git a/app/Models/Tag.php b/app/Models/Tag.php
index 3eb989cc1..0d50e356c 100644
--- a/app/Models/Tag.php
+++ b/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;
diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php
index 9c0f591c9..5882eee76 100644
--- a/app/Models/TagDAO.php
+++ b/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'],
);
diff --git a/app/Models/UserDAO.php b/app/Models/UserDAO.php
index 8e7e977d0..4e824cf01 100644
--- a/app/Models/UserDAO.php
+++ b/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;