diff options
| author | 2022-07-19 15:17:08 +0200 | |
|---|---|---|
| committer | 2022-07-19 15:17:08 +0200 | |
| commit | 0866fdaee85bc8530437436abe7f8536b8b0f4f0 (patch) | |
| tree | fd73e2e55740423bfab64581c4934b6d50dd3162 | |
| parent | 2d807e06b1abffdbc40a60c3623e22e3c6b818c6 (diff) | |
Add database field attributes for entries (#4444)
* Add database field attributes for entries
Just like we already have for categories, feeds, etc.
No core use yet, but allows in particular extensions to save per-entry data
* Fix PHPStand
* Fix wrong variable
| -rwxr-xr-x | app/Controllers/feedController.php | 2 | ||||
| -rw-r--r-- | app/Controllers/importExportController.php | 1 | ||||
| -rw-r--r-- | app/Models/Entry.php | 28 | ||||
| -rw-r--r-- | app/Models/EntryDAO.php | 70 | ||||
| -rw-r--r-- | app/Models/EntryDAOPGSQL.php | 16 | ||||
| -rw-r--r-- | app/Models/EntryDAOSQLite.php | 14 | ||||
| -rw-r--r-- | app/SQL/install.sql.mysql.php | 2 | ||||
| -rw-r--r-- | app/SQL/install.sql.pgsql.php | 2 | ||||
| -rw-r--r-- | app/SQL/install.sql.sqlite.php | 2 |
9 files changed, 116 insertions, 21 deletions
diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 8621cb535..0699844d8 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -426,6 +426,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController { //', old hash ' . $existingHash . ', new hash ' . $entry->hash()); $entry->_isRead($mark_updated_article_unread ? false : null); //Change is_read according to policy. + /** @var FreshRSS_Entry|null */ $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); if ($entry === null) { // An extension has returned a null value, there is nothing to insert. @@ -454,6 +455,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController { $titlesAsRead[$entry->title()] = true; } + /** @var FreshRSS_Entry|null */ $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); if ($entry === null) { // An extension has returned a null value, there is nothing to insert. diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index c7b2d579f..8402e840d 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -440,6 +440,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController { } $newGuids[$entry->guid()] = true; + /** @var FreshRSS_Entry|null */ $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); if ($entry == null) { // An extension has returned a null value, there is nothing to insert. diff --git a/app/Models/Entry.php b/app/Models/Entry.php index fb17268b3..72e59e38c 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -44,6 +44,7 @@ class FreshRSS_Entry extends Minz_Model { private $feed; private $tags; + private $attributes = []; public function __construct(int $feedId = 0, string $guid = '', string $title = '', string $authors = '', string $content = '', string $link = '', $pubdate = 0, bool $is_read = false, bool $is_favorite = false, string $tags = '') { @@ -88,6 +89,9 @@ class FreshRSS_Entry extends Minz_Model { if (!empty($dao['categories'])) { $entry->_tags($dao['categories']); } + if (!empty($dao['attributes'])) { + $entry->_attributes('', $dao['attributes']); + } return $entry; } @@ -228,6 +232,29 @@ class FreshRSS_Entry extends Minz_Model { } } + public function attributes($key = '') { + if ($key == '') { + return $this->attributes; + } else { + return isset($this->attributes[$key]) ? $this->attributes[$key] : null; + } + } + + public function _attributes(string $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; + } + } + public function hash(): string { if ($this->hash == '') { //Do not include $this->date because it may be automatically generated when lacking @@ -588,6 +615,7 @@ class FreshRSS_Entry extends Minz_Model { 'is_favorite' => $this->isFavorite(), 'id_feed' => $this->feed(), 'tags' => $this->tags(true), + 'attributes' => $this->attributes(), ); } } diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index d87f3b7c4..5faaac1fb 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -65,9 +65,33 @@ SQL; return $ok; } + protected function addColumn(string $name) { + Minz_Log::warning(__method__ . ': ' . $name); + try { + if ($name === 'attributes') { //v1.20.0 + $sql = <<<'SQL' +ALTER TABLE `_entry` ADD COLUMN attributes TEXT; +ALTER TABLE `_entrytmp` ADD COLUMN attributes TEXT; +SQL; + return $this->pdo->exec($sql) !== false; + } + } catch (Exception $e) { + Minz_Log::error(__method__ . ' error: ' . $e->getMessage()); + } + return false; + } + //TODO: Move the database auto-updates to DatabaseDAO protected function autoUpdateDb(array $errorInfo) { if (isset($errorInfo[0])) { + if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) { + $errorLines = explode("\n", $errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise + foreach (['attributes'] as $column) { + if (stripos($errorLines[0], $column) !== false) { + return $this->addColumn($column); + } + } + } if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_TABLE_ERROR) { if (stripos($errorInfo[2], 'tag') !== false) { $tagDAO = FreshRSS_Factory::createTagDao(); @@ -97,12 +121,12 @@ SQL; $sql = static::sqlIgnoreConflict( 'INSERT INTO `_' . ($useTmpTable ? 'entrytmp' : 'entry') . '` (id, guid, title, author, ' . (static::isCompressed() ? 'content_bin' : 'content') - . ', link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) ' + . ', link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags, attributes) ' . 'VALUES(:id, :guid, :title, :author, ' . (static::isCompressed() ? 'COMPRESS(:content)' : ':content') . ', :link, :date, :last_seen, ' . static::sqlHexDecode(':hash') - . ', :is_read, :is_favorite, :id_feed, :tags)'); + . ', :is_read, :is_favorite, :id_feed, :tags, :attributes)'); $this->addEntryPrepared = $this->pdo->prepare($sql); } if ($this->addEntryPrepared) { @@ -135,6 +159,11 @@ SQL; $valuesTmp['tags'] = mb_strcut($valuesTmp['tags'], 0, 1023, 'UTF-8'); $valuesTmp['tags'] = safe_utf8($valuesTmp['tags']); $this->addEntryPrepared->bindParam(':tags', $valuesTmp['tags']); + if (!isset($valuesTmp['attributes'])) { + $valuesTmp['attributes'] = []; + } + $this->addEntryPrepared->bindValue(':attributes', is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : + json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES)); if (static::hasNativeHex()) { $this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']); @@ -164,9 +193,9 @@ SET @rank=(SELECT MAX(id) - COUNT(*) FROM `_entrytmp`); INSERT IGNORE INTO `_entry` ( id, guid, title, author, content_bin, link, date, `lastSeen`, - hash, is_read, is_favorite, id_feed, tags + hash, is_read, is_favorite, id_feed, tags, attributes ) -SELECT @rank:=@rank+1 AS id, guid, title, author, content_bin, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags +SELECT @rank:=@rank+1 AS id, guid, title, author, content_bin, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags, attributes FROM `_entrytmp` ORDER BY date, id; @@ -197,7 +226,7 @@ SQL; . ', link=:link, date=:date, `lastSeen`=:last_seen' . ', hash=' . static::sqlHexDecode(':hash') . ', is_read=COALESCE(:is_read, is_read)' - . ', tags=:tags ' + . ', tags=:tags, attributes=:attributes ' . 'WHERE id_feed=:id_feed AND guid=:guid'; $this->updateEntryPrepared = $this->pdo->prepare($sql); } @@ -229,6 +258,11 @@ SQL; $valuesTmp['tags'] = mb_strcut($valuesTmp['tags'], 0, 1023, 'UTF-8'); $valuesTmp['tags'] = safe_utf8($valuesTmp['tags']); $this->updateEntryPrepared->bindParam(':tags', $valuesTmp['tags']); + if (!isset($valuesTmp['attributes'])) { + $valuesTmp['attributes'] = []; + } + $this->updateEntryPrepared->bindValue(':attributes', is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : + json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES)); if (static::hasNativeHex()) { $this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hash']); @@ -654,11 +688,20 @@ SQL; public function selectAll() { $sql = 'SELECT id, guid, title, author, ' . (static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content') - . ', link, date, `lastSeen`, ' . static::sqlHexEncode('hash') . ' AS hash, is_read, is_favorite, id_feed, tags ' + . ', link, date, `lastSeen`, ' . static::sqlHexEncode('hash') . ' AS hash, is_read, is_favorite, id_feed, tags, attributes ' . 'FROM `_entry`'; $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)) { + yield from $this->selectAll(); + } + Minz_Log::error(__method__ . ' error: ' . json_encode($info)); + yield false; } } @@ -667,7 +710,7 @@ SQL; // un guid est unique pour un flux donné $sql = 'SELECT id, guid, title, author, ' . (static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content') - . ', link, date, is_read, is_favorite, id_feed, tags ' + . ', link, date, is_read, is_favorite, id_feed, tags, attributes ' . 'FROM `_entry` WHERE id_feed=:id_feed AND guid=:guid'; $stm = $this->pdo->prepare($sql); $stm->bindParam(':id_feed', $id_feed, PDO::PARAM_INT); @@ -681,7 +724,7 @@ SQL; public function searchById($id) { $sql = 'SELECT id, guid, title, author, ' . (static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content') - . ', link, date, is_read, is_favorite, id_feed, tags ' + . ', link, date, is_read, is_favorite, id_feed, tags, attributes ' . 'FROM `_entry` WHERE id=:id'; $stm = $this->pdo->prepare($sql); $stm->bindParam(':id', $id, PDO::PARAM_INT); @@ -1065,7 +1108,7 @@ SQL; $sql = 'SELECT e0.id, e0.guid, e0.title, e0.author, ' . (static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content') - . ', e0.link, e0.date, e0.is_read, e0.is_favorite, e0.id_feed, e0.tags ' + . ', e0.link, e0.date, e0.is_read, e0.is_favorite, e0.id_feed, e0.tags, e0.attributes ' . 'FROM `_entry` e0 ' . 'INNER JOIN (' . $sql @@ -1077,6 +1120,9 @@ SQL; return $stm; } else { $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo(); + if ($this->autoUpdateDb($info)) { + return $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min); + } Minz_Log::error('SQL error listWhereRaw: ' . $info[2]); return false; } @@ -1110,7 +1156,7 @@ SQL; $sql = 'SELECT id, guid, title, author, ' . (static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content') - . ', link, date, is_read, is_favorite, id_feed, tags ' + . ', link, date, is_read, is_favorite, id_feed, tags, attributes ' . 'FROM `_entry` ' . 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?) ' . 'ORDER BY id ' . $order; diff --git a/app/Models/EntryDAOPGSQL.php b/app/Models/EntryDAOPGSQL.php index 13bf8b41f..8702db706 100644 --- a/app/Models/EntryDAOPGSQL.php +++ b/app/Models/EntryDAOPGSQL.php @@ -20,6 +20,14 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite { protected function autoUpdateDb(array $errorInfo) { if (isset($errorInfo[0])) { + if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) { + $errorLines = explode("\n", $errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise + foreach (['attributes'] as $column) { + if (stripos($errorLines[0], $column) !== false) { + return $this->addColumn($column); + } + } + } if ($errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_TABLE) { if (stripos($errorInfo[2], 'tag') !== false) { $tagDAO = FreshRSS_Factory::createTagDao(); @@ -32,10 +40,6 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite { return false; } - protected function addColumn(string $name) { - return false; - } - public function commitNewEntries() { //TODO: Update to PostgreSQL 9.5+ syntax with ON CONFLICT DO NOTHING $sql = 'DO $$ @@ -44,9 +48,9 @@ maxrank bigint := (SELECT MAX(id) FROM `_entrytmp`); rank bigint := (SELECT maxrank - COUNT(*) FROM `_entrytmp`); BEGIN INSERT INTO `_entry` - (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) + (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags, attributes) (SELECT rank + row_number() OVER(ORDER BY date, id) AS id, guid, title, author, content, - link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags + link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags, attributes FROM `_entrytmp` AS etmp WHERE NOT EXISTS ( SELECT 1 FROM `_entry` AS ereal diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index e4e079959..27a7c3560 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -23,6 +23,14 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { } protected function autoUpdateDb(array $errorInfo) { + if ($tableInfo = $this->pdo->query("PRAGMA table_info('entry')")) { + $columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1); + foreach (['attributes'] as $column) { + if (!in_array($column, $columns)) { + return $this->addColumn($column); + } + } + } if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='tag'")) { $showCreate = $tableInfo->fetchColumn(); if (stripos($showCreate, 'tag') === false) { @@ -43,13 +51,13 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { $sql = ' DROP TABLE IF EXISTS `tmp`; CREATE TEMP TABLE `tmp` AS - SELECT id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags + SELECT id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags, attributes FROM `_entrytmp` ORDER BY date, id; INSERT OR IGNORE INTO `_entry` - (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) + (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags, attributes) SELECT rowid + (SELECT MAX(id) - COUNT(*) FROM `tmp`) AS id, - guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags + guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags, attributes FROM `tmp` ORDER BY date, id; DELETE FROM `_entrytmp` WHERE id <= (SELECT MAX(id) FROM `tmp`); diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php index 226cc0346..d85bd3dc3 100644 --- a/app/SQL/install.sql.mysql.php +++ b/app/SQL/install.sql.mysql.php @@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS `_entry` ( `is_favorite` BOOLEAN NOT NULL DEFAULT 0, `id_feed` INT, -- 1.20.0 `tags` VARCHAR(1023), + `attributes` TEXT, -- v1.20.0 PRIMARY KEY (`id`), FOREIGN KEY (`id_feed`) REFERENCES `_feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE KEY (`id_feed`,`guid`), -- v0.7 @@ -87,6 +88,7 @@ CREATE TABLE IF NOT EXISTS `_entrytmp` ( -- v1.7 `is_favorite` BOOLEAN NOT NULL DEFAULT 0, `id_feed` INT, -- 1.20.0 `tags` VARCHAR(1023), + `attributes` TEXT, -- v1.20.0 PRIMARY KEY (`id`), FOREIGN KEY (`id_feed`) REFERENCES `_feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE KEY (`id_feed`,`guid`), diff --git a/app/SQL/install.sql.pgsql.php b/app/SQL/install.sql.pgsql.php index d453d65fb..c4da2afad 100644 --- a/app/SQL/install.sql.pgsql.php +++ b/app/SQL/install.sql.pgsql.php @@ -49,6 +49,7 @@ CREATE TABLE IF NOT EXISTS `_entry` ( "is_favorite" SMALLINT NOT NULL DEFAULT 0, "id_feed" INT, -- 1.20.0 "tags" VARCHAR(1023), + "attributes" TEXT, -- v1.20.0 FOREIGN KEY ("id_feed") REFERENCES `_feed` ("id") ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE ("id_feed","guid") ); @@ -82,6 +83,7 @@ CREATE TABLE IF NOT EXISTS `_entrytmp` ( -- v1.7 "is_favorite" SMALLINT NOT NULL DEFAULT 0, "id_feed" INT, -- 1.20.0 "tags" VARCHAR(1023), + "attributes" TEXT, -- v1.20.0 FOREIGN KEY ("id_feed") REFERENCES `_feed` ("id") ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE ("id_feed","guid") ); diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php index dd2cca708..ccf256d6a 100644 --- a/app/SQL/install.sql.sqlite.php +++ b/app/SQL/install.sql.sqlite.php @@ -51,6 +51,7 @@ CREATE TABLE IF NOT EXISTS `entry` ( `is_favorite` BOOLEAN NOT NULL DEFAULT 0, `id_feed` INTEGER, -- 1.20.0 `tags` VARCHAR(1023), + `attributes` TEXT, -- v1.20.0 PRIMARY KEY (`id`), FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE (`id_feed`,`guid`) @@ -82,6 +83,7 @@ CREATE TABLE IF NOT EXISTS `entrytmp` ( -- v1.7 `is_favorite` BOOLEAN NOT NULL DEFAULT 0, `id_feed` INTEGER, -- 1.20.0 `tags` VARCHAR(1023), + `attributes` TEXT, -- v1.20.0 PRIMARY KEY (`id`), FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE (`id_feed`,`guid`) |
