|
|
@@ -119,6 +119,7 @@ SQL;
|
|
|
$this->pdo->commit();
|
|
|
}
|
|
|
Minz_Log::warning(__METHOD__ . ': ' . $name);
|
|
|
+ require APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php';
|
|
|
try {
|
|
|
if ($name === 'attributes') { //v1.20.0
|
|
|
$sql = <<<'SQL'
|
|
|
@@ -127,6 +128,13 @@ ALTER TABLE `_entrytmp` ADD COLUMN attributes TEXT;
|
|
|
SQL;
|
|
|
return $this->pdo->exec($sql) !== false;
|
|
|
}
|
|
|
+ if ($name === 'lastUserModified') { //v1.28.0
|
|
|
+ $sql = $GLOBALS['ALTER_TABLE_ENTRY_LAST_USER_MODIFIED'];
|
|
|
+ if (!is_string($sql)) {
|
|
|
+ throw new Exception('ALTER_TABLE_ENTRY_LAST_USER_MODIFIED is not a string!');
|
|
|
+ }
|
|
|
+ return $this->pdo->exec($sql) !== false;
|
|
|
+ }
|
|
|
} catch (Exception $e) {
|
|
|
Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage());
|
|
|
}
|
|
|
@@ -261,8 +269,11 @@ SQL;
|
|
|
|
|
|
private PDOStatement|null|false $updateEntryPrepared = null;
|
|
|
|
|
|
- /** @param array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'lastSeen':int,'hash':string,
|
|
|
- * 'is_read':bool|int|null,'is_favorite':bool|int|null,'id_feed':int,'tags':string,'attributes':array<string,mixed>} $valuesTmp */
|
|
|
+ /**
|
|
|
+ * @param array{id:string,guid:string,title:string,author:string,content:string,link:string,
|
|
|
+ * date:int,lastSeen:int,lastUserModified?:int,hash:string,
|
|
|
+ * is_read:bool|int|null,is_favorite:bool|int|null,id_feed:int,tags:string,attributes:array<string,mixed>} $valuesTmp
|
|
|
+ */
|
|
|
public function updateEntry(array $valuesTmp): bool {
|
|
|
if (!isset($valuesTmp['is_read'])) {
|
|
|
$valuesTmp['is_read'] = null;
|
|
|
@@ -270,12 +281,16 @@ SQL;
|
|
|
if (!isset($valuesTmp['is_favorite'])) {
|
|
|
$valuesTmp['is_favorite'] = null;
|
|
|
}
|
|
|
+ if (empty($valuesTmp['lastUserModified'])) {
|
|
|
+ $valuesTmp['lastUserModified'] = 0;
|
|
|
+ }
|
|
|
|
|
|
if ($this->updateEntryPrepared == null) {
|
|
|
$sql = 'UPDATE `_entry` '
|
|
|
. 'SET title=:title, author=:author, '
|
|
|
. (static::isCompressed() ? 'content_bin=COMPRESS(:content)' : 'content=:content')
|
|
|
. ', link=:link, date=:date, `lastSeen`=:last_seen'
|
|
|
+ . ', `lastUserModified`=MAX(:last_user_modified, `lastUserModified`)'
|
|
|
. ', hash=' . static::sqlHexDecode(':hash')
|
|
|
. ', is_read=COALESCE(:is_read, is_read)'
|
|
|
. ', is_favorite=COALESCE(:is_favorite, is_favorite)'
|
|
|
@@ -300,6 +315,7 @@ SQL;
|
|
|
$this->updateEntryPrepared->bindParam(':link', $valuesTmp['link']);
|
|
|
$this->updateEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
|
|
|
$this->updateEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
|
|
|
+ $this->updateEntryPrepared->bindParam(':last_user_modified', $valuesTmp['lastUserModified'], PDO::PARAM_INT);
|
|
|
if ($valuesTmp['is_read'] === null) {
|
|
|
$this->updateEntryPrepared->bindValue(':is_read', null, PDO::PARAM_NULL);
|
|
|
} else {
|
|
|
@@ -332,7 +348,8 @@ SQL;
|
|
|
return true;
|
|
|
} else {
|
|
|
$info = $this->updateEntryPrepared == false ? $this->pdo->errorInfo() : $this->updateEntryPrepared->errorInfo();
|
|
|
- /** @var array{id:string,guid:string,title:string,author:string,content:string,link:string,date:int,lastSeen:int,hash:string,
|
|
|
+ /** @var array{id:string,guid:string,title:string,author:string,content:string,link:string,
|
|
|
+ * date:int,lastSeen:int,lastUserModified:int,hash:string,
|
|
|
* is_read:bool|int|null,is_favorite:bool|int|null,id_feed:int,tags:string,attributes:array<string,mixed>} $valuesTmp */
|
|
|
/** @var array{0:string,1:int,2:string} $info */
|
|
|
if ($this->autoUpdateDb($info)) {
|
|
|
@@ -381,9 +398,10 @@ SQL;
|
|
|
return $affected;
|
|
|
}
|
|
|
$sql = 'UPDATE `_entry` '
|
|
|
- . 'SET is_favorite=? '
|
|
|
+ . 'SET is_favorite=?, `lastUserModified`=? '
|
|
|
. 'WHERE id IN (' . str_repeat('?,', count($ids) - 1) . '?)';
|
|
|
$values = [$is_favorite ? 1 : 0];
|
|
|
+ $values[] = time();
|
|
|
$values = array_merge($values, $ids);
|
|
|
$stm = $this->pdo->prepare($sql);
|
|
|
if ($stm !== false && $stm->execute($values)) {
|
|
|
@@ -462,9 +480,9 @@ SQL;
|
|
|
|
|
|
FreshRSS_UserDAO::touch();
|
|
|
$sql = 'UPDATE `_entry` '
|
|
|
- . 'SET is_read=? '
|
|
|
- . 'WHERE id IN (' . str_repeat('?,', count($ids) - 1) . '?)';
|
|
|
- $values = [$is_read ? 1 : 0];
|
|
|
+ . 'SET is_read=?, `lastUserModified`=? '
|
|
|
+ . 'WHERE is_read<>? AND id IN (' . str_repeat('?,', count($ids) - 1) . '?)';
|
|
|
+ $values = [$is_read ? 1 : 0, time(), $is_read ? 1 : 0];
|
|
|
$values = array_merge($values, $ids);
|
|
|
$stm = $this->pdo->prepare($sql);
|
|
|
if ($stm === false || !$stm->execute($values)) {
|
|
|
@@ -480,10 +498,10 @@ SQL;
|
|
|
} else {
|
|
|
FreshRSS_UserDAO::touch();
|
|
|
$sql = 'UPDATE `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id '
|
|
|
- . 'SET e.is_read=?,'
|
|
|
+ . 'SET e.is_read=?,`lastUserModified`=?,'
|
|
|
. 'f.`cache_nbUnreads`=f.`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 '
|
|
|
. 'WHERE e.id=? AND e.is_read=?';
|
|
|
- $values = [$is_read ? 1 : 0, $ids, $is_read ? 0 : 1];
|
|
|
+ $values = [$is_read ? 1 : 0, time(), $ids, $is_read ? 0 : 1];
|
|
|
$stm = $this->pdo->prepare($sql);
|
|
|
if ($stm !== false && $stm->execute($values)) {
|
|
|
return $stm->rowCount();
|
|
|
@@ -516,8 +534,8 @@ SQL;
|
|
|
Minz_Log::debug('Calling markReadEntries(0) is deprecated!');
|
|
|
}
|
|
|
|
|
|
- $sql = 'UPDATE `_entry` SET is_read = ? WHERE is_read <> ? AND id <= ?';
|
|
|
- $values = [$is_read ? 1 : 0, $is_read ? 1 : 0, $idMax];
|
|
|
+ $sql = 'UPDATE `_entry` SET is_read = ?, `lastUserModified`=? WHERE is_read <> ? AND id <= ?';
|
|
|
+ $values = [$is_read ? 1 : 0, time(), $is_read ? 1 : 0, $idMax];
|
|
|
if ($onlyFavorites) {
|
|
|
$sql .= ' AND is_favorite=1';
|
|
|
}
|
|
|
@@ -569,11 +587,11 @@ SQL;
|
|
|
|
|
|
$sql = <<<'SQL'
|
|
|
UPDATE `_entry`
|
|
|
-SET is_read = ?
|
|
|
+SET is_read = ?, `lastUserModified` = ?
|
|
|
WHERE is_read <> ? AND id <= ?
|
|
|
AND id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category=? AND f.priority >= ? AND f.priority < ?)
|
|
|
SQL;
|
|
|
- $values = [$is_read ? 1 : 0, $is_read ? 1 : 0, $idMax, $id, FreshRSS_Feed::PRIORITY_CATEGORY, FreshRSS_Feed::PRIORITY_IMPORTANT];
|
|
|
+ $values = [$is_read ? 1 : 0, time(), $is_read ? 1 : 0, $idMax, $id, FreshRSS_Feed::PRIORITY_CATEGORY, FreshRSS_Feed::PRIORITY_IMPORTANT];
|
|
|
|
|
|
[$searchValues, $search] = $this->sqlListEntriesWhere(alias: '', state: $state, filters: $filters);
|
|
|
|
|
|
@@ -613,9 +631,9 @@ SQL;
|
|
|
}
|
|
|
|
|
|
$sql = 'UPDATE `_entry` '
|
|
|
- . 'SET is_read=? '
|
|
|
+ . 'SET is_read=?, `lastUserModified`=? '
|
|
|
. 'WHERE id_feed=? AND is_read <> ? AND id <= ?';
|
|
|
- $values = [$is_read ? 1 : 0, $id_feed, $is_read ? 1 : 0, $idMax];
|
|
|
+ $values = [$is_read ? 1 : 0, time(), $id_feed, $is_read ? 1 : 0, $idMax];
|
|
|
|
|
|
[$searchValues, $search] = $this->sqlListEntriesWhere(alias: '', state: $state, filters: $filters);
|
|
|
|
|
|
@@ -664,11 +682,11 @@ SQL;
|
|
|
}
|
|
|
|
|
|
$sql = 'UPDATE `_entry` e INNER JOIN `_entrytag` et ON et.id_entry = e.id '
|
|
|
- . 'SET e.is_read = ? '
|
|
|
+ . 'SET e.is_read = ?, `lastUserModified` = ? '
|
|
|
. 'WHERE '
|
|
|
. ($id == 0 ? '' : 'et.id_tag = ? AND ')
|
|
|
. 'e.is_read <> ? AND e.id <= ?';
|
|
|
- $values = [$is_read ? 1 : 0];
|
|
|
+ $values = [$is_read ? 1 : 0, time()];
|
|
|
if ($id != 0) {
|
|
|
$values[] = $id;
|
|
|
}
|
|
|
@@ -756,8 +774,9 @@ SQL;
|
|
|
|
|
|
/**
|
|
|
* @param 'ASC'|'DESC' $order
|
|
|
- * @return Traversable<array{id:string,guid:string,title:string,author:string,content:string,link:string,date:int,lastSeen:int,
|
|
|
- * hash:string,is_read:bool,is_favorite:bool,id_feed:int,tags:string,attributes:?string}>
|
|
|
+ * @return Traversable<array{id:string,guid:string,title:string,author:string,content:string,link:string,
|
|
|
+ * date:int,lastSeen:int,lastUserModified:int,
|
|
|
+ * hash:string,is_read:bool,is_favorite:bool,id_feed:int,tags:string,attributes:?string}>
|
|
|
*/
|
|
|
public function selectAll(string $order = 'ASC', int $limit = -1, int $offset = 0): Traversable {
|
|
|
$content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content';
|
|
|
@@ -765,14 +784,14 @@ SQL;
|
|
|
$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'ASC';
|
|
|
$sqlLimit = static::sqlLimit($limit, $offset);
|
|
|
$sql = <<<SQL
|
|
|
-SELECT id, guid, title, author, {$content}, link, date, `lastSeen`, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes
|
|
|
+SELECT id, guid, title, author, {$content}, link, date, `lastSeen`, `lastUserModified`, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes
|
|
|
FROM `_entry`
|
|
|
ORDER BY id {$order} {$sqlLimit}
|
|
|
SQL;
|
|
|
$stm = $this->pdo->query($sql);
|
|
|
if ($stm !== false) {
|
|
|
while (is_array($row = $stm->fetch(PDO::FETCH_ASSOC))) {
|
|
|
- /** @var array{id:string,guid:string,title:string,author:string,content:string,link:string,date:int,lastSeen:int,
|
|
|
+ /** @var array{id:string,guid:string,title:string,author:string,content:string,link:string,date:int,lastSeen:int,lastUserModified:int,
|
|
|
* hash:string,is_read:bool,is_favorite:bool,id_feed:int,tags:string,attributes:?string} $row */
|
|
|
yield $row;
|
|
|
}
|
|
|
@@ -791,7 +810,7 @@ SQL;
|
|
|
$content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content';
|
|
|
$hash = static::sqlHexEncode('hash');
|
|
|
$sql = <<<SQL
|
|
|
-SELECT id, guid, title, author, link, date, is_read, is_favorite, {$hash} AS hash, id_feed, tags, attributes, {$content}
|
|
|
+SELECT id, guid, title, author, {$content}, link, date, `lastSeen`, `lastUserModified`, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes
|
|
|
FROM `_entry` WHERE id_feed=:id_feed AND guid=:guid
|
|
|
SQL;
|
|
|
$res = $this->fetchAssoc($sql, [':id_feed' => $id_feed, ':guid' => $guid]);
|
|
|
@@ -804,7 +823,7 @@ SQL;
|
|
|
$content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content';
|
|
|
$hash = static::sqlHexEncode('hash');
|
|
|
$sql = <<<SQL
|
|
|
-SELECT id, guid, title, author, link, date, is_read, is_favorite, {$hash} AS hash, id_feed, tags, attributes, {$content}
|
|
|
+SELECT id, guid, title, author, {$content}, link, date, `lastSeen`, `lastUserModified`, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes
|
|
|
FROM `_entry` WHERE id=:id
|
|
|
SQL;
|
|
|
$res = $this->fetchAssoc($sql, [':id' => $id]);
|
|
|
@@ -1210,7 +1229,7 @@ SQL;
|
|
|
/**
|
|
|
* @param numeric-string $id_min
|
|
|
* @param numeric-string $id_max
|
|
|
- * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' $sort
|
|
|
+ * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified' $sort
|
|
|
* @param 'ASC'|'DESC' $order
|
|
|
* @param numeric-string $continuation_id
|
|
|
* @param list<string|int> $continuation_values
|
|
|
@@ -1278,11 +1297,12 @@ SQL;
|
|
|
$values[] = $id_min;
|
|
|
}
|
|
|
|
|
|
- if ($continuation_id !== '0' && in_array($sort, ['c.name', 'date', 'f.name', 'link', 'title'], true)) {
|
|
|
+ if ($continuation_id !== '0' && in_array($sort, ['c.name', 'date', 'f.name', 'link', 'title', 'lastUserModified'], true)) {
|
|
|
$sign = $order === 'ASC' ? '>' : '<';
|
|
|
$orderBy = match ($sort) {
|
|
|
'c.name' => 'c.name',
|
|
|
'f.name' => 'f.name',
|
|
|
+ 'lastUserModified' => $alias . '`lastUserModified`',
|
|
|
default => $alias . $sort,
|
|
|
};
|
|
|
// Keyset pagination (Compatibility syntax due to poor performance of tuple syntax in MySQL https://bugs.mysql.com/bug.php?id=104128)
|
|
|
@@ -1320,7 +1340,7 @@ SQL;
|
|
|
* @param int $id category/feed/tag ID
|
|
|
* @param numeric-string $id_min
|
|
|
* @param numeric-string $id_max
|
|
|
- * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' $sort
|
|
|
+ * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified' $sort
|
|
|
* @param 'ASC'|'DESC' $order
|
|
|
* @param numeric-string $continuation_id
|
|
|
* @param list<string|int> $continuation_values
|
|
|
@@ -1379,10 +1399,11 @@ SQL;
|
|
|
}
|
|
|
|
|
|
$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC';
|
|
|
- $sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand'], true) ? $sort : 'id';
|
|
|
+ $sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand', 'lastUserModified'], true) ? $sort : 'id';
|
|
|
$orderBy = match ($sort) {
|
|
|
'c.name' => 'c.name',
|
|
|
'f.name' => 'f.name',
|
|
|
+ 'lastUserModified' => 'e.`lastUserModified`',
|
|
|
'rand' => static::sqlRandom(),
|
|
|
default => 'e.' . $sort,
|
|
|
};
|
|
|
@@ -1412,7 +1433,7 @@ SQL;
|
|
|
* @param int $id category/feed/tag ID
|
|
|
* @param numeric-string $id_min
|
|
|
* @param numeric-string $id_max
|
|
|
- * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' $sort
|
|
|
+ * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified' $sort
|
|
|
* @param 'ASC'|'DESC' $order
|
|
|
* @param numeric-string $continuation_id
|
|
|
* @param list<string|int> $continuation_values
|
|
|
@@ -1422,7 +1443,7 @@ SQL;
|
|
|
string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC',
|
|
|
string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0): PDOStatement|false {
|
|
|
$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC';
|
|
|
- $sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand'], true) ? $sort : 'id';
|
|
|
+ $sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand', 'lastUserModified'], true) ? $sort : 'id';
|
|
|
|
|
|
[$values, $sql] = $this->sqlListWhere($type, $id, $state, $filters, id_min: $id_min, id_max: $id_max, sort: $sort, order: $order,
|
|
|
continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset);
|
|
|
@@ -1430,15 +1451,16 @@ SQL;
|
|
|
$orderBy = match ($sort) {
|
|
|
'c.name' => 'c0.name',
|
|
|
'f.name' => 'f0.name',
|
|
|
+ 'lastUserModified' => 'e0.`lastUserModified`',
|
|
|
'rand' => static::sqlRandom(),
|
|
|
default => 'e0.' . $sort,
|
|
|
};
|
|
|
$content = static::isCompressed() ? 'UNCOMPRESS(e0.content_bin) AS content' : 'e0.content';
|
|
|
$hash = static::sqlHexEncode('e0.hash');
|
|
|
$sql = <<<SQL
|
|
|
-SELECT e0.id, e0.guid, e0.title, e0.author, {$content}, e0.link, e0.date, {$hash} AS hash, e0.is_read, e0.is_favorite, e0.id_feed, e0.tags, e0.attributes
|
|
|
-FROM `_entry` e0
|
|
|
-INNER JOIN ({$sql}) e2 ON e2.id=e0.id
|
|
|
+SELECT e0.id, e0.guid, e0.title, e0.author, {$content}, e0.link,
|
|
|
+ e0.date, e0.`lastSeen`, e0.`lastUserModified`, {$hash} AS hash, e0.is_read, e0.is_favorite, e0.id_feed, e0.tags, e0.attributes
|
|
|
+FROM `_entry` e0 INNER JOIN ({$sql}) e2 ON e2.id=e0.id
|
|
|
SQL;
|
|
|
if ($sort === 'f.name' || $sort === 'c.name') {
|
|
|
$sql .= ' INNER JOIN `_feed` f0 ON f0.id = e0.id_feed ';
|
|
|
@@ -1474,7 +1496,7 @@ SQL;
|
|
|
* @param int $id category/feed/tag ID
|
|
|
* @param numeric-string $id_min
|
|
|
* @param numeric-string $id_max
|
|
|
- * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' $sort
|
|
|
+ * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified' $sort
|
|
|
* @param 'ASC'|'DESC' $order
|
|
|
* @param numeric-string $continuation_id
|
|
|
* @param list<string|int> $continuation_values
|
|
|
@@ -1520,7 +1542,7 @@ SQL;
|
|
|
$hash = static::sqlHexEncode('hash');
|
|
|
$repeats = str_repeat('?,', count($ids) - 1) . '?';
|
|
|
$sql = <<<SQL
|
|
|
-SELECT id, guid, title, author, link, date, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes, {$content}
|
|
|
+SELECT id, guid, title, author, link, date, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes, {$content}, lastUserModified
|
|
|
FROM `_entry`
|
|
|
WHERE id IN ({$repeats})
|
|
|
ORDER BY id {$order}
|