summaryrefslogtreecommitdiff
path: root/app/Models
diff options
context:
space:
mode:
Diffstat (limited to 'app/Models')
-rw-r--r--app/Models/Category.php6
-rw-r--r--app/Models/CategoryDAO.php17
-rw-r--r--app/Models/Context.php46
-rw-r--r--app/Models/DatabaseDAO.php89
-rw-r--r--app/Models/DatabaseDAOPGSQL.php38
-rw-r--r--app/Models/DatabaseDAOSQLite.php14
-rw-r--r--app/Models/Entry.php53
-rw-r--r--app/Models/EntryDAO.php103
-rw-r--r--app/Models/EntryDAOPGSQL.php9
-rw-r--r--app/Models/EntryDAOSQLite.php48
-rw-r--r--app/Models/Factory.php16
-rw-r--r--app/Models/Feed.php34
-rw-r--r--app/Models/FeedDAO.php9
-rw-r--r--app/Models/Search.php37
-rw-r--r--app/Models/Tag.php76
-rw-r--r--app/Models/TagDAO.php315
-rw-r--r--app/Models/TagDAOPGSQL.php9
-rw-r--r--app/Models/TagDAOSQLite.php19
-rw-r--r--app/Models/Themes.php12
-rw-r--r--app/Models/UserDAO.php7
-rw-r--r--app/Models/UserQuery.php26
21 files changed, 855 insertions, 128 deletions
diff --git a/app/Models/Category.php b/app/Models/Category.php
index 197faf942..240dbca73 100644
--- a/app/Models/Category.php
+++ b/app/Models/Category.php
@@ -30,7 +30,7 @@ class FreshRSS_Category extends Minz_Model {
}
public function nbFeed() {
if ($this->nbFeed < 0) {
- $catDAO = new FreshRSS_CategoryDAO();
+ $catDAO = FreshRSS_Factory::createCategoryDao();
$this->nbFeed = $catDAO->countFeed($this->id());
}
@@ -38,7 +38,7 @@ class FreshRSS_Category extends Minz_Model {
}
public function nbNotRead() {
if ($this->nbNotRead < 0) {
- $catDAO = new FreshRSS_CategoryDAO();
+ $catDAO = FreshRSS_Factory::createCategoryDao();
$this->nbNotRead = $catDAO->countNotRead($this->id());
}
@@ -68,7 +68,7 @@ class FreshRSS_Category extends Minz_Model {
$this->id = $value;
}
public function _name($value) {
- $this->name = mb_strcut(trim($value), 0, 255, 'UTF-8');
+ $this->name = trim($value);
}
public function _feeds($values) {
if (!is_array($values)) {
diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php
index cf6b3bae3..ba7eb765e 100644
--- a/app/Models/CategoryDAO.php
+++ b/app/Models/CategoryDAO.php
@@ -5,11 +5,15 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
const DEFAULTCATEGORYID = 1;
public function addCategory($valuesTmp) {
- $sql = 'INSERT INTO `' . $this->prefix . 'category`(name) VALUES(?)';
+ $sql = 'INSERT INTO `' . $this->prefix . 'category`(name) '
+ . 'SELECT * FROM (SELECT TRIM(?)) c2 ' //TRIM() to provide a type hint as text for PostgreSQL
+ . 'WHERE NOT EXISTS (SELECT 1 FROM `' . $this->prefix . 'tag` WHERE name = TRIM(?))'; //No tag of the same name
$stm = $this->bd->prepare($sql);
+ $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
$values = array(
- mb_strcut($valuesTmp['name'], 0, 255, 'UTF-8'),
+ $valuesTmp['name'],
+ $valuesTmp['name'],
);
if ($stm && $stm->execute($values)) {
@@ -35,12 +39,15 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
}
public function updateCategory($id, $valuesTmp) {
- $sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=?';
+ $sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=? '
+ . 'AND NOT EXISTS (SELECT 1 FROM `' . $this->prefix . 'tag` WHERE name = ?)'; //No tag of the same name
$stm = $this->bd->prepare($sql);
+ $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
$values = array(
$valuesTmp['name'],
- $id
+ $id,
+ $valuesTmp['name'],
);
if ($stm && $stm->execute($values)) {
@@ -151,7 +158,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
$sql = 'INSERT INTO `' . $this->prefix . 'category`(id, name) VALUES(?, ?)';
if (parent::$sharedDbType === 'pgsql') {
//Force call to nextval()
- $sql .= " RETURNING nextval('" . $this->prefix . "category_id_seq');";
+ $sql .= ' RETURNING nextval(\'"' . $this->prefix . 'category_id_seq"\');';
}
$stm = $this->bd->prepare($sql);
diff --git a/app/Models/Context.php b/app/Models/Context.php
index 2ca8f80b0..60ec6ff77 100644
--- a/app/Models/Context.php
+++ b/app/Models/Context.php
@@ -8,6 +8,7 @@ class FreshRSS_Context {
public static $user_conf = null;
public static $system_conf = null;
public static $categories = array();
+ public static $tags = array();
public static $name = '';
public static $description = '';
@@ -25,6 +26,8 @@ class FreshRSS_Context {
'starred' => false,
'feed' => false,
'category' => false,
+ 'tag' => false,
+ 'tags' => false,
);
public static $next_get = 'a';
@@ -91,6 +94,14 @@ class FreshRSS_Context {
} else {
return 'c_' . self::$current_get['category'];
}
+ } elseif (self::$current_get['tag']) {
+ if ($array) {
+ return array('t', self::$current_get['tag']);
+ } else {
+ return 't_' . self::$current_get['tag'];
+ }
+ } elseif (self::$current_get['tags']) {
+ return 'T';
}
}
@@ -117,6 +128,10 @@ class FreshRSS_Context {
return self::$current_get['feed'] == $id;
case 'c':
return self::$current_get['category'] == $id;
+ case 't':
+ return self::$current_get['tag'] == $id;
+ case 'T':
+ return self::$current_get['tags'] || self::$current_get['tag'];
default:
return false;
}
@@ -130,6 +145,7 @@ class FreshRSS_Context {
* - s
* - f_<feed id>
* - c_<category id>
+ * - t_<tag id>
*
* $name and $get_unread attributes are also updated as $next_get
* Raise an exception if id or $get is invalid.
@@ -140,7 +156,7 @@ class FreshRSS_Context {
$nb_unread = 0;
if (empty(self::$categories)) {
- $catDAO = new FreshRSS_CategoryDAO();
+ $catDAO = FreshRSS_Factory::createCategoryDao();
self::$categories = $catDAO->listCategories();
}
@@ -166,12 +182,10 @@ class FreshRSS_Context {
if ($feed === null) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$feed = $feedDAO->searchById($id);
-
if (!$feed) {
throw new FreshRSS_Context_Exception('Invalid feed: ' . $id);
}
}
-
self::$current_get['feed'] = $id;
self::$current_get['category'] = $feed->category();
self::$name = $feed->name();
@@ -182,19 +196,37 @@ class FreshRSS_Context {
// We try to find the corresponding category.
self::$current_get['category'] = $id;
if (!isset(self::$categories[$id])) {
- $catDAO = new FreshRSS_CategoryDAO();
+ $catDAO = FreshRSS_Factory::createCategoryDao();
$cat = $catDAO->searchById($id);
-
if (!$cat) {
throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
}
} else {
$cat = self::$categories[$id];
}
-
self::$name = $cat->name();
self::$get_unread = $cat->nbNotRead();
break;
+ case 't':
+ // We try to find the corresponding tag.
+ self::$current_get['tag'] = $id;
+ if (!isset(self::$tags[$id])) {
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ $tag = $tagDAO->searchById($id);
+ if (!$tag) {
+ throw new FreshRSS_Context_Exception('Invalid tag: ' . $id);
+ }
+ } else {
+ $tag = self::$tags[$id];
+ }
+ self::$name = $tag->name();
+ self::$get_unread = $tag->nbUnread();
+ break;
+ case 'T':
+ self::$current_get['tags'] = true;
+ self::$name = _t('index.menu.tags');
+ self::$get_unread = 0;
+ break;
default:
throw new FreshRSS_Context_Exception('Invalid getter: ' . $get);
}
@@ -211,7 +243,7 @@ class FreshRSS_Context {
self::$next_get = $get;
if (empty(self::$categories)) {
- $catDAO = new FreshRSS_CategoryDAO();
+ $catDAO = FreshRSS_Factory::createCategoryDao();
self::$categories = $catDAO->listCategories();
}
diff --git a/app/Models/DatabaseDAO.php b/app/Models/DatabaseDAO.php
index b8e5577e4..b331eccc3 100644
--- a/app/Models/DatabaseDAO.php
+++ b/app/Models/DatabaseDAO.php
@@ -4,6 +4,16 @@
* This class is used to test database is well-constructed.
*/
class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
+
+ //MySQL error codes
+ const ER_BAD_FIELD_ERROR = '42S22';
+ const ER_BAD_TABLE_ERROR = '42S02';
+ const ER_TRUNCATED_WRONG_VALUE_FOR_FIELD = '1366';
+
+ //MySQL InnoDB maximum index length for UTF8MB4
+ //https://dev.mysql.com/doc/refman/8.0/en/innodb-restrictions.html
+ const LENGTH_INDEX_UNICODE = 191;
+
public function tablesAreCorrect() {
$sql = 'SHOW TABLES';
$stm = $this->bd->prepare($sql);
@@ -14,6 +24,9 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
$this->prefix . 'category' => false,
$this->prefix . 'feed' => false,
$this->prefix . 'entry' => false,
+ $this->prefix . 'entrytmp' => false,
+ $this->prefix . 'tag' => false,
+ $this->prefix . 'entrytag' => false,
);
foreach ($res as $value) {
$tables[array_pop($value)] = true;
@@ -43,7 +56,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
public function categoryIsCorrect() {
return $this->checkTable('category', array(
- 'id', 'name'
+ 'id', 'name',
));
}
@@ -51,14 +64,33 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
return $this->checkTable('feed', array(
'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate',
'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', 'attributes',
- 'cache_nbEntries', 'cache_nbUnreads'
+ 'cache_nbEntries', 'cache_nbUnreads',
));
}
public function entryIsCorrect() {
return $this->checkTable('entry', array(
- 'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'is_read',
- 'is_favorite', 'id_feed', 'tags'
+ 'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'lastSeen', 'hash', 'is_read',
+ 'is_favorite', 'id_feed', 'tags',
+ ));
+ }
+
+ public function entrytmpIsCorrect() {
+ return $this->checkTable('entrytmp', array(
+ 'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'lastSeen', 'hash', 'is_read',
+ 'is_favorite', 'id_feed', 'tags',
+ ));
+ }
+
+ public function tagIsCorrect() {
+ return $this->checkTable('tag', array(
+ 'id', 'name', 'attributes',
+ ));
+ }
+
+ public function entrytagIsCorrect() {
+ return $this->checkTable('entrytag', array(
+ 'id_tag', 'id_entry',
));
}
@@ -97,28 +129,39 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
public function optimize() {
$ok = true;
-
- $sql = 'OPTIMIZE TABLE `' . $this->prefix . 'entry`'; //MySQL
- $stm = $this->bd->prepare($sql);
- $ok &= $stm != false;
- if ($stm) {
- $ok &= $stm->execute();
- }
-
- $sql = 'OPTIMIZE TABLE `' . $this->prefix . 'feed`'; //MySQL
- $stm = $this->bd->prepare($sql);
- $ok &= $stm != false;
- if ($stm) {
- $ok &= $stm->execute();
+ $tables = array('category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag');
+
+ foreach ($tables as $table) {
+ $sql = 'OPTIMIZE TABLE `' . $this->prefix . $table . '`'; //MySQL
+ $stm = $this->bd->prepare($sql);
+ $ok &= $stm != false;
+ if ($stm) {
+ $ok &= $stm->execute();
+ }
}
+ return $ok;
+ }
- $sql = 'OPTIMIZE TABLE `' . $this->prefix . 'category`'; //MySQL
- $stm = $this->bd->prepare($sql);
- $ok &= $stm != false;
- if ($stm) {
- $ok &= $stm->execute();
+ public function ensureCaseInsensitiveGuids() {
+ $ok = true;
+ $db = FreshRSS_Context::$system_conf->db;
+ if ($db['type'] === 'mysql') {
+ include_once(APP_PATH . '/SQL/install.sql.mysql.php');
+ if (defined('SQL_UPDATE_GUID_LATIN1_BIN')) { //FreshRSS 1.12
+ try {
+ $sql = sprintf(SQL_UPDATE_GUID_LATIN1_BIN, $this->prefix);
+ $stm = $this->bd->prepare($sql);
+ $ok = $stm->execute();
+ } catch (Exception $e) {
+ $ok = false;
+ Minz_Log::error('FreshRSS_DatabaseDAO::ensureCaseInsensitiveGuids error: ' . $e->getMessage());
+ }
+ }
}
-
return $ok;
}
+
+ public function minorDbMaintenance() {
+ $this->ensureCaseInsensitiveGuids();
+ }
}
diff --git a/app/Models/DatabaseDAOPGSQL.php b/app/Models/DatabaseDAOPGSQL.php
index 1b3f7408d..8582b5719 100644
--- a/app/Models/DatabaseDAOPGSQL.php
+++ b/app/Models/DatabaseDAOPGSQL.php
@@ -3,7 +3,12 @@
/**
* This class is used to test database is well-constructed.
*/
-class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO {
+class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
+
+ //PostgreSQL error codes
+ const UNDEFINED_COLUMN = '42703';
+ const UNDEFINED_TABLE = '42P01';
+
public function tablesAreCorrect() {
$db = FreshRSS_Context::$system_conf->db;
$dbowner = $db['user'];
@@ -17,6 +22,9 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO {
$this->prefix . 'category' => false,
$this->prefix . 'feed' => false,
$this->prefix . 'entry' => false,
+ $this->prefix . 'entrytmp' => false,
+ $this->prefix . 'tag' => false,
+ $this->prefix . 'entrytag' => false,
);
foreach ($res as $value) {
$tables[array_pop($value)] = true;
@@ -53,28 +61,16 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO {
public function optimize() {
$ok = true;
+ $tables = array('category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag');
- $sql = 'VACUUM `' . $this->prefix . 'entry`';
- $stm = $this->bd->prepare($sql);
- $ok &= $stm != false;
- if ($stm) {
- $ok &= $stm->execute();
- }
-
- $sql = 'VACUUM `' . $this->prefix . 'feed`';
- $stm = $this->bd->prepare($sql);
- $ok &= $stm != false;
- if ($stm) {
- $ok &= $stm->execute();
+ foreach ($tables as $table) {
+ $sql = 'VACUUM `' . $this->prefix . $table . '`';
+ $stm = $this->bd->prepare($sql);
+ $ok &= $stm != false;
+ if ($stm) {
+ $ok &= $stm->execute();
+ }
}
-
- $sql = 'VACUUM `' . $this->prefix . 'category`';
- $stm = $this->bd->prepare($sql);
- $ok &= $stm != false;
- if ($stm) {
- $ok &= $stm->execute();
- }
-
return $ok;
}
}
diff --git a/app/Models/DatabaseDAOSQLite.php b/app/Models/DatabaseDAOSQLite.php
index d3aedb3c0..a93a209b2 100644
--- a/app/Models/DatabaseDAOSQLite.php
+++ b/app/Models/DatabaseDAOSQLite.php
@@ -14,6 +14,9 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
'category' => false,
'feed' => false,
'entry' => false,
+ 'entrytmp' => false,
+ 'tag' => false,
+ 'entrytag' => false,
);
foreach ($res as $value) {
$tables[$value['name']] = true;
@@ -32,8 +35,15 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
public function entryIsCorrect() {
return $this->checkTable('entry', array(
- 'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'is_read',
- 'is_favorite', 'id_feed', 'tags'
+ 'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'lastSeen', 'hash', 'is_read',
+ 'is_favorite', 'id_feed', 'tags',
+ ));
+ }
+
+ public function entrytmpIsCorrect() {
+ return $this->checkTable('entrytmp', array(
+ 'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'lastSeen', 'hash', 'is_read',
+ 'is_favorite', 'id_feed', 'tags',
));
}
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index ccbad5724..985276734 100644
--- a/app/Models/Entry.php
+++ b/app/Models/Entry.php
@@ -10,7 +10,7 @@ class FreshRSS_Entry extends Minz_Model {
private $id = 0;
private $guid;
private $title;
- private $author;
+ private $authors;
private $content;
private $link;
private $date;
@@ -21,18 +21,17 @@ class FreshRSS_Entry extends Minz_Model {
private $feed;
private $tags;
- public function __construct($feedId = '', $guid = '', $title = '', $author = '', $content = '',
+ public function __construct($feedId = '', $guid = '', $title = '', $authors = '', $content = '',
$link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') {
$this->_title($title);
- $this->_author($author);
+ $this->_authors($authors);
$this->_content($content);
$this->_link($link);
$this->_date($pubdate);
$this->_isRead($is_read);
$this->_isFavorite($is_favorite);
$this->_feedId($feedId);
- $tags = mb_strcut($tags, 0, 1023, 'UTF-8');
- $this->_tags(preg_split('/[\s#]/', $tags));
+ $this->_tags($tags);
$this->_guid($guid);
}
@@ -46,7 +45,15 @@ class FreshRSS_Entry extends Minz_Model {
return $this->title;
}
public function author() {
- return $this->author === null ? '' : $this->author;
+ //Deprecated
+ return $this->authors(true);
+ }
+ public function authors($asString = false) {
+ if ($asString) {
+ return $this->authors == null ? '' : ';' . implode('; ', $this->authors);
+ } else {
+ return $this->authors;
+ }
}
public function content() {
return $this->content;
@@ -86,9 +93,9 @@ class FreshRSS_Entry extends Minz_Model {
return $this->feedId;
}
}
- public function tags($inString = false) {
- if ($inString) {
- return empty($this->tags) ? '' : '#' . implode(' #', $this->tags);
+ public function tags($asString = false) {
+ if ($asString) {
+ return $this->tags == null ? '' : '#' . implode(' #', $this->tags);
} else {
return $this->tags;
}
@@ -97,7 +104,7 @@ class FreshRSS_Entry extends Minz_Model {
public function hash() {
if ($this->hash === null) {
//Do not include $this->date because it may be automatically generated when lacking
- $this->hash = md5($this->link . $this->title . $this->author . $this->content . $this->tags(true));
+ $this->hash = md5($this->link . $this->title . $this->authors(true) . $this->content . $this->tags(true));
}
return $this->hash;
}
@@ -124,11 +131,22 @@ class FreshRSS_Entry extends Minz_Model {
}
public function _title($value) {
$this->hash = null;
- $this->title = mb_strcut($value, 0, 255, 'UTF-8');
+ $this->title = $value;
}
public function _author($value) {
+ //Deprecated
+ $this->_authors($value);
+ }
+ public function _authors($value) {
$this->hash = null;
- $this->author = mb_strcut($value, 0, 255, 'UTF-8');
+ if (!is_array($value)) {
+ if (strpos($value, ';') !== false) {
+ $value = preg_split('/\s*[;]\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
+ } else {
+ $value = preg_split('/\s*[,]\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
+ }
+ }
+ $this->authors = $value;
}
public function _content($value) {
$this->hash = null;
@@ -162,15 +180,8 @@ class FreshRSS_Entry extends Minz_Model {
public function _tags($value) {
$this->hash = null;
if (!is_array($value)) {
- $value = array($value);
- }
-
- foreach ($value as $key => $t) {
- if (!$t) {
- unset($value[$key]);
- }
+ $value = preg_split('/\s*[#,]\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
}
-
$this->tags = $value;
}
@@ -287,7 +298,7 @@ class FreshRSS_Entry extends Minz_Model {
'id' => $this->id(),
'guid' => $this->guid(),
'title' => $this->title(),
- 'author' => $this->author(),
+ 'author' => $this->authors(true),
'content' => $this->content(),
'link' => $this->link(),
'date' => $this->date(true),
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index f0e164995..a01c2227b 100644
--- a/app/Models/EntryDAO.php
+++ b/app/Models/EntryDAO.php
@@ -18,6 +18,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return 'hex(' . $x . ')';
}
+ //TODO: Move the database auto-updates to DatabaseDAO
protected function addColumn($name) {
Minz_Log::warning('FreshRSS_EntryDAO::addColumn: ' . $name);
$hasTransaction = false;
@@ -56,6 +57,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
private $triedUpdateToUtf8mb4 = false;
+ //TODO: Move the database auto-updates to DatabaseDAO
protected function updateToUtf8mb4() {
if ($this->triedUpdateToUtf8mb4) {
return false;
@@ -65,7 +67,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
if ($db['type'] === 'mysql') {
include_once(APP_PATH . '/SQL/install.sql.mysql.php');
if (defined('SQL_UPDATE_UTF8MB4')) {
- Minz_Log::warning('Updating MySQL to UTF8MB4...');
+ Minz_Log::warning('Updating MySQL to UTF8MB4...'); //v1.5.0
$hadTransaction = $this->bd->inTransaction();
if ($hadTransaction) {
$this->bd->commit();
@@ -88,6 +90,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return false;
}
+ //TODO: Move the database auto-updates to DatabaseDAO
protected function createEntryTempTable() {
$ok = false;
$hadTransaction = $this->bd->inTransaction();
@@ -120,22 +123,28 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return $ok;
}
+ //TODO: Move the database auto-updates to DatabaseDAO
protected function autoUpdateDb($errorInfo) {
if (isset($errorInfo[0])) {
- if ($errorInfo[0] === '42S22') { //ER_BAD_FIELD_ERROR
+ if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR) {
//autoAddColumn
foreach (array('lastSeen', 'hash') as $column) {
if (stripos($errorInfo[2], $column) !== false) {
return $this->addColumn($column);
}
}
- } elseif ($errorInfo[0] === '42S02' && stripos($errorInfo[2], 'entrytmp') !== false) { //ER_BAD_TABLE_ERROR
- return $this->createEntryTempTable(); //v1.7
+ } elseif ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_TABLE_ERROR) {
+ if (stripos($errorInfo[2], 'tag') !== false) {
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ return $tagDAO->createTagTable(); //v1.12.0
+ } elseif (stripos($errorInfo[2], 'entrytmp') !== false) {
+ return $this->createEntryTempTable(); //v1.7.0
+ }
}
}
if (isset($errorInfo[1])) {
- if ($errorInfo[1] == '1366') { //ER_TRUNCATED_WRONG_VALUE_FOR_FIELD
- return $this->updateToUtf8mb4();
+ if ($errorInfo[1] == FreshRSS_DatabaseDAO::ER_TRUNCATED_WRONG_VALUE_FOR_FIELD) {
+ return $this->updateToUtf8mb4(); //v1.5.0
}
}
return false;
@@ -560,11 +569,52 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return $affected;
}
+ /**
+ * Mark all the articles in a tag as read.
+ * @param integer $id tag ID, or empty for targetting any tag
+ * @param integer $idMax max article ID
+ * @return integer affected rows
+ */
+ public function markReadTag($id = '', $idMax = 0, $filters = null, $state = 0, $is_read = true) {
+ FreshRSS_UserDAO::touch();
+ if ($idMax == 0) {
+ $idMax = time() . '000000';
+ Minz_Log::debug('Calling markReadTag(0) is deprecated!');
+ }
+
+ $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'entrytag` et ON et.id_entry = e.id '
+ . 'SET e.is_read = ? '
+ . 'WHERE '
+ . ($id == '' ? '' : 'et.id_tag = ? AND ')
+ . 'e.is_read <> ? AND e.id <= ?';
+ $values = array($is_read ? 1 : 0);
+ if ($id != '') {
+ $values[] = $id;
+ }
+ $values[] = $is_read ? 1 : 0;
+ $values[] = $idMax;
+
+ list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
+
+ $stm = $this->bd->prepare($sql . $search);
+ if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
+ $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+ Minz_Log::error('SQL error markReadTag: ' . $info[2]);
+ return false;
+ }
+ $affected = $stm->rowCount();
+ if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+ return false;
+ }
+ return $affected;
+ }
+
public function cleanOldEntries($id_feed, $date_min, $keep = 15) { //Remember to call updateCachedValue($id_feed) or updateCachedValues() just after
$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
. 'WHERE id_feed=:id_feed 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 `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) ' //Do not remove the most newly seen articles, plus a few seconds of tolerance
+ . 'AND id NOT IN (SELECT id_entry FROM `' . $this->prefix . 'entrytag`) ' //Do not purge tagged entries
. 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=:id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery'
$stm = $this->bd->prepare($sql);
@@ -770,24 +820,31 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$joinFeed = false;
$values = array();
switch ($type) {
- case 'a':
+ case 'a': //All PRIORITY_MAIN_STREAM
$where .= 'f.priority > ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
break;
- case 's': //Deprecated: use $state instead
+ case 'A': //All except PRIORITY_ARCHIVED
+ $where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
+ break;
+ case 's': //Starred. Deprecated: use $state instead
$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
$where .= 'AND e.is_favorite=1 ';
break;
- case 'c':
+ case 'c': //Category
$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
$where .= 'AND f.category=? ';
$values[] = intval($id);
break;
- case 'f':
+ case 'f': //Feed
$where .= 'e.id_feed=? ';
$values[] = intval($id);
break;
- case 'A':
- $where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
+ case 't': //Tag
+ $where .= 'et.id_tag=? ';
+ $values[] = intval($id);
+ break;
+ case 'T': //Any tag
+ $where .= '1=1 ';
break;
default:
throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!');
@@ -796,8 +853,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state, $order, $firstId, $date_min);
return array(array_merge($values, $searchValues),
- 'SELECT e.id FROM `' . $this->prefix . 'entry` e '
+ 'SELECT '
+ . ($type === 'T' ? 'DISTINCT ' : '')
+ . 'e.id FROM `' . $this->prefix . 'entry` e '
. 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id '
+ . ($type === 't' || $type === 'T' ? 'INNER JOIN `' . $this->prefix . 'entrytag` et ON et.id_entry = e.id ' : '')
. 'WHERE ' . $where
. $search
. 'ORDER BY e.id ' . $order
@@ -817,13 +877,22 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
. 'ORDER BY e0.id ' . $order;
$stm = $this->bd->prepare($sql);
- $stm->execute($values);
- return $stm;
+ if ($stm && $stm->execute($values)) {
+ return $stm;
+ } else {
+ $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+ Minz_Log::error('SQL error listWhereRaw: ' . $info[2]);
+ return false;
+ }
}
public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
$stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
- return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC));
+ if ($stm) {
+ return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC));
+ } else {
+ return false;
+ }
}
public function listByIds($ids, $order = 'DESC') {
@@ -923,7 +992,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$stm = $this->bd->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
- return $res[0];
+ return isset($res[0]) ? $res[0] : 0;
}
public function countNotRead($minPriority = null) {
$sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e';
diff --git a/app/Models/EntryDAOPGSQL.php b/app/Models/EntryDAOPGSQL.php
index f09fe8e75..aef258b6f 100644
--- a/app/Models/EntryDAOPGSQL.php
+++ b/app/Models/EntryDAOPGSQL.php
@@ -12,8 +12,13 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
protected function autoUpdateDb($errorInfo) {
if (isset($errorInfo[0])) {
- if ($errorInfo[0] === '42P01' && stripos($errorInfo[2], 'entrytmp') !== false) { //undefined_table
- return $this->createEntryTempTable();
+ if ($errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_TABLE) {
+ if (stripos($errorInfo[2], 'tag') !== false) {
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ return $tagDAO->createTagTable(); //v1.12.0
+ } elseif (stripos($errorInfo[2], 'entrytmp') !== false) {
+ return $this->createEntryTempTable(); //v1.7.0
+ }
}
}
return false;
diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php
index 944de8470..f8cd14fe6 100644
--- a/app/Models/EntryDAOSQLite.php
+++ b/app/Models/EntryDAOSQLite.php
@@ -7,10 +7,17 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
}
protected function autoUpdateDb($errorInfo) {
+ if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='tag'")) {
+ $showCreate = $tableInfo->fetchColumn();
+ if (stripos($showCreate, 'tag') === false) {
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ return $tagDAO->createTagTable(); //v1.12.0
+ }
+ }
if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entrytmp'")) {
$showCreate = $tableInfo->fetchColumn();
if (stripos($showCreate, 'entrytmp') === false) {
- return $this->createEntryTempTable();
+ return $this->createEntryTempTable(); //v1.7.0
}
}
if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) {
@@ -228,4 +235,43 @@ DROP TABLE IF EXISTS `tmp`;
}
return $affected;
}
+
+ /**
+ * Mark all the articles in a tag as read.
+ * @param integer $id tag ID, or empty for targetting any tag
+ * @param integer $idMax max article ID
+ * @return integer affected rows
+ */
+ public function markReadTag($id = '', $idMax = 0, $filters = null, $state = 0, $is_read = true) {
+ FreshRSS_UserDAO::touch();
+ if ($idMax == 0) {
+ $idMax = time() . '000000';
+ Minz_Log::debug('Calling markReadTag(0) is deprecated!');
+ }
+
+ $sql = 'UPDATE `' . $this->prefix . 'entry` e '
+ . 'SET e.is_read = ? '
+ . 'WHERE e.is_read <> ? AND e.id <= ? AND '
+ . 'e.id IN (SELECT et.id_entry FROM `' . $this->prefix . 'entrytag` et '
+ . ($id == '' ? '' : 'WHERE et.id = ?')
+ . ')';
+ $values = array($is_read ? 1 : 0, $is_read ? 1 : 0, $idMax);
+ if ($id != '') {
+ $values[] = $id;
+ }
+
+ list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
+
+ $stm = $this->bd->prepare($sql . $search);
+ if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
+ $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+ Minz_Log::error('SQL error markReadTag: ' . $info[2]);
+ return false;
+ }
+ $affected = $stm->rowCount();
+ if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
+ return false;
+ }
+ return $affected;
+ }
}
diff --git a/app/Models/Factory.php b/app/Models/Factory.php
index 764987c46..1accb491c 100644
--- a/app/Models/Factory.php
+++ b/app/Models/Factory.php
@@ -2,6 +2,10 @@
class FreshRSS_Factory {
+ public static function createCategoryDao($username = null) {
+ return new FreshRSS_CategoryDAO($username);
+ }
+
public static function createFeedDao($username = null) {
$conf = Minz_Configuration::get('system');
switch ($conf->db['type']) {
@@ -24,6 +28,18 @@ class FreshRSS_Factory {
}
}
+ public static function createTagDao($username = null) {
+ $conf = Minz_Configuration::get('system');
+ switch ($conf->db['type']) {
+ case 'sqlite':
+ return new FreshRSS_TagDAOSQLite($username);
+ case 'pgsql':
+ return new FreshRSS_TagDAOPGSQL($username);
+ default:
+ return new FreshRSS_TagDAO($username);
+ }
+ }
+
public static function createStatsDAO($username = null) {
$conf = Minz_Configuration::get('system');
switch ($conf->db['type']) {
diff --git a/app/Models/Feed.php b/app/Models/Feed.php
index ed381a867..e1dd2990d 100644
--- a/app/Models/Feed.php
+++ b/app/Models/Feed.php
@@ -286,6 +286,10 @@ class FreshRSS_Feed extends Minz_Model {
if (!$loadDetails) { //Only activates auto-discovery when adding a new feed
$feed->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE);
}
+ if ($this->attributes('clear_cache')) {
+ // Do not use `$simplePie->enable_cache(false);` as it would prevent caching in multiuser context
+ $this->clearCache();
+ }
Minz_ExtensionManager::callHook('simplepie_before_init', $feed, $this);
$mtime = $feed->init();
@@ -345,13 +349,21 @@ class FreshRSS_Feed extends Minz_Model {
$link = $item->get_permalink();
$date = @strtotime($item->get_date());
- // gestion des tags (catégorie == tag)
- $tags_tmp = $item->get_categories();
+ //Tag processing (tag == category)
+ $categories = $item->get_categories();
$tags = array();
- if ($tags_tmp !== null) {
- foreach ($tags_tmp as $tag) {
- $tags[] = html_only_entity_decode($tag->get_label());
+ if (is_array($categories)) {
+ foreach ($categories as $category) {
+ $text = html_only_entity_decode($category->get_label());
+ //Some feeds use a single category with comma-separated tags
+ $labels = explode(',', $text);
+ if (is_array($labels)) {
+ foreach ($labels as $label) {
+ $tags[] = trim($label);
+ }
+ }
}
+ $tags = array_unique($tags);
}
$content = html_only_entity_decode($item->get_content());
@@ -412,7 +424,7 @@ class FreshRSS_Feed extends Minz_Model {
$author_names = '';
if (is_array($authors)) {
foreach ($authors as $author) {
- $author_names .= html_only_entity_decode(strip_tags($author->name == '' ? $author->email : $author->name)) . ', ';
+ $author_names .= html_only_entity_decode(strip_tags($author->name == '' ? $author->email : $author->name)) . '; ';
}
}
$author_names = substr($author_names, 0, -2);
@@ -457,8 +469,16 @@ class FreshRSS_Feed extends Minz_Model {
$this->entries = $entries;
}
+ protected function cacheFilename() {
+ return CACHE_PATH . '/' . md5($this->url) . '.spc';
+ }
+
+ public function clearCache() {
+ return @unlink($this->cacheFilename());
+ }
+
public function cacheModifiedTime() {
- return @filemtime(CACHE_PATH . '/' . md5($this->url) . '.spc');
+ return @filemtime($this->cacheFilename());
}
public function lock() {
diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php
index 285f17193..e579f5881 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] === '42S22' || $errorInfo[0] === '42703') { //ER_BAD_FIELD_ERROR (Mysql), undefined_column (PostgreSQL)
+ if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
foreach (array('attributes') as $column) {
if (stripos($errorInfo[2], $column) !== false) {
return $this->addColumn($column);
@@ -55,7 +55,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$values = array(
substr($valuesTmp['url'], 0, 511),
$valuesTmp['category'],
- mb_strcut($valuesTmp['name'], 0, 255, 'UTF-8'),
+ mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'),
substr($valuesTmp['website'], 0, 255),
mb_strcut($valuesTmp['description'], 0, 1023, 'UTF-8'),
$valuesTmp['lastUpdate'],
@@ -109,6 +109,9 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
public function updateFeed($id, $valuesTmp) {
+ if (isset($valuesTmp['name'])) {
+ $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
+ }
if (isset($valuesTmp['url'])) {
$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
}
@@ -180,7 +183,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
public function changeCategory($idOldCat, $idNewCat) {
- $catDAO = new FreshRSS_CategoryDAO();
+ $catDAO = FreshRSS_Factory::createCategoryDao();
$newCat = $catDAO->searchById($idNewCat);
if (!$newCat) {
$newCat = $catDAO->getDefault();
diff --git a/app/Models/Search.php b/app/Models/Search.php
index 5cc7f8e8d..c52e391fa 100644
--- a/app/Models/Search.php
+++ b/app/Models/Search.php
@@ -40,7 +40,7 @@ class FreshRSS_Search {
$input = $this->parseNotIntitleSearch($input);
$input = $this->parseNotAuthorSearch($input);
$input = $this->parseNotInurlSearch($input);
- $input = $this->parseNotTagsSeach($input);
+ $input = $this->parseNotTagsSearch($input);
$input = $this->parsePubdateSearch($input);
$input = $this->parseDateSearch($input);
@@ -48,7 +48,7 @@ class FreshRSS_Search {
$input = $this->parseIntitleSearch($input);
$input = $this->parseAuthorSearch($input);
$input = $this->parseInurlSearch($input);
- $input = $this->parseTagsSeach($input);
+ $input = $this->parseTagsSearch($input);
$input = $this->parseNotSearch($input);
$input = $this->parseSearch($input);
@@ -117,6 +117,17 @@ class FreshRSS_Search {
return is_array($anArray) ? array_filter($anArray, function($value) { return $value !== ''; }) : array();
}
+ private static function decodeSpaces($value) {
+ if (is_array($value)) {
+ for ($i = count($value) - 1; $i >= 0; $i--) {
+ $value[$i] = self::decodeSpaces($value[$i]);
+ }
+ } else {
+ $value = trim(str_replace('+', ' ', $value));
+ }
+ return $value;
+ }
+
/**
* Parse the search string to find intitle keyword and the search related
* to it.
@@ -130,11 +141,12 @@ class FreshRSS_Search {
$this->intitle = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
- if (preg_match_all('/\bintitle:(?P<search>\w*)/', $input, $matches)) {
+ if (preg_match_all('/\bintitle:(?P<search>[^\s"]*)/', $input, $matches)) {
$this->intitle = array_merge($this->intitle ? $this->intitle : array(), $matches['search']);
$input = str_replace($matches[0], '', $input);
}
$this->intitle = self::removeEmptyValues($this->intitle);
+ $this->intitle = self::decodeSpaces($this->intitle);
return $input;
}
@@ -143,11 +155,12 @@ class FreshRSS_Search {
$this->not_intitle = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
- if (preg_match_all('/[!-]intitle:(?P<search>\w*)/', $input, $matches)) {
+ if (preg_match_all('/[!-]intitle:(?P<search>[^\s"]*)/', $input, $matches)) {
$this->not_intitle = array_merge($this->not_intitle ? $this->not_intitle : array(), $matches['search']);
$input = str_replace($matches[0], '', $input);
}
$this->not_intitle = self::removeEmptyValues($this->not_intitle);
+ $this->not_intitle = self::decodeSpaces($this->not_intitle);
return $input;
}
@@ -166,11 +179,12 @@ class FreshRSS_Search {
$this->author = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
- if (preg_match_all('/\bauthor:(?P<search>\w*)/', $input, $matches)) {
+ if (preg_match_all('/\bauthor:(?P<search>[^\s"]*)/', $input, $matches)) {
$this->author = array_merge($this->author ? $this->author : array(), $matches['search']);
$input = str_replace($matches[0], '', $input);
}
$this->author = self::removeEmptyValues($this->author);
+ $this->author = self::decodeSpaces($this->author);
return $input;
}
@@ -179,11 +193,12 @@ class FreshRSS_Search {
$this->not_author = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
- if (preg_match_all('/[!-]author:(?P<search>\w*)/', $input, $matches)) {
+ if (preg_match_all('/[!-]author:(?P<search>[^\s"]*)/', $input, $matches)) {
$this->not_author = array_merge($this->not_author ? $this->not_author : array(), $matches['search']);
$input = str_replace($matches[0], '', $input);
}
$this->not_author = self::removeEmptyValues($this->not_author);
+ $this->not_author = self::decodeSpaces($this->not_author);
return $input;
}
@@ -201,6 +216,7 @@ class FreshRSS_Search {
$input = str_replace($matches[0], '', $input);
}
$this->inurl = self::removeEmptyValues($this->inurl);
+ $this->inurl = self::decodeSpaces($this->inurl);
return $input;
}
@@ -210,6 +226,7 @@ class FreshRSS_Search {
$input = str_replace($matches[0], '', $input);
}
$this->not_inurl = self::removeEmptyValues($this->not_inurl);
+ $this->not_inurl = self::decodeSpaces($this->not_inurl);
return $input;
}
@@ -259,21 +276,23 @@ class FreshRSS_Search {
* @param string $input
* @return string
*/
- private function parseTagsSeach($input) {
+ private function parseTagsSearch($input) {
if (preg_match_all('/#(?P<search>[^\s]+)/', $input, $matches)) {
$this->tags = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
$this->tags = self::removeEmptyValues($this->tags);
+ $this->tags = self::decodeSpaces($this->tags);
return $input;
}
- private function parseNotTagsSeach($input) {
+ private function parseNotTagsSearch($input) {
if (preg_match_all('/[!-]#(?P<search>[^\s]+)/', $input, $matches)) {
$this->not_tags = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
$this->not_tags = self::removeEmptyValues($this->not_tags);
+ $this->not_tags = self::decodeSpaces($this->not_tags);
return $input;
}
@@ -303,6 +322,7 @@ class FreshRSS_Search {
} else {
$this->search = explode(' ', $input);
}
+ $this->search = self::decodeSpaces($this->search);
}
private function parseNotSearch($input) {
@@ -322,6 +342,7 @@ class FreshRSS_Search {
$input = str_replace($matches[0], '', $input);
}
$this->not_search = self::removeEmptyValues($this->not_search);
+ $this->not_search = self::decodeSpaces($this->not_search);
return $input;
}
diff --git a/app/Models/Tag.php b/app/Models/Tag.php
new file mode 100644
index 000000000..3eb989cc1
--- /dev/null
+++ b/app/Models/Tag.php
@@ -0,0 +1,76 @@
+<?php
+
+class FreshRSS_Tag extends Minz_Model {
+ private $id = 0;
+ private $name;
+ private $attributes = array();
+ private $nbEntries = -1;
+ private $nbUnread = -1;
+
+ public function __construct($name = '') {
+ $this->_name($name);
+ }
+
+ public function id() {
+ return $this->id;
+ }
+
+ public function _id($value) {
+ $this->id = (int)$value;
+ }
+
+ public function name() {
+ return $this->name;
+ }
+
+ public function _name($value) {
+ $this->name = trim($value);
+ }
+
+ public function attributes($key = '') {
+ if ($key == '') {
+ return $this->attributes;
+ } else {
+ return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
+ }
+ }
+
+ 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;
+ }
+ }
+
+ public function nbEntries() {
+ if ($this->nbEntries < 0) {
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ $this->nbEntries = $tagDAO->countEntries($this->id());
+ }
+ return $this->nbFeed;
+ }
+
+ public function _nbEntries($value) {
+ $this->nbEntries = (int)$value;
+ }
+
+ public function nbUnread() {
+ if ($this->nbUnread < 0) {
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ $this->nbUnread = $tagDAO->countNotRead($this->id());
+ }
+ return $this->nbUnread;
+ }
+
+ public function _nbUnread($value) {
+ $this->nbUnread = (int)$value;
+ }
+}
diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php
new file mode 100644
index 000000000..1b59c8971
--- /dev/null
+++ b/app/Models/TagDAO.php
@@ -0,0 +1,315 @@
+<?php
+
+class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
+
+ public function sqlIgnore() {
+ return 'IGNORE';
+ }
+
+ public function createTagTable() {
+ $ok = false;
+ $hadTransaction = $this->bd->inTransaction();
+ if ($hadTransaction) {
+ $this->bd->commit();
+ }
+ try {
+ $db = FreshRSS_Context::$system_conf->db;
+ require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
+
+ Minz_Log::warning('SQL ALTER GUID case sensitivity...');
+ $databaseDAO = FreshRSS_Factory::createDatabaseDAO();
+ $databaseDAO->ensureCaseInsensitiveGuids();
+
+ Minz_Log::warning('SQL CREATE TABLE tag...');
+ if (defined('SQL_CREATE_TABLE_TAGS')) {
+ $sql = sprintf(SQL_CREATE_TABLE_TAGS, $this->prefix);
+ $stm = $this->bd->prepare($sql);
+ $ok = $stm && $stm->execute();
+ } else {
+ global $SQL_CREATE_TABLE_TAGS;
+ $ok = !empty($SQL_CREATE_TABLE_TAGS);
+ foreach ($SQL_CREATE_TABLE_TAGS as $instruction) {
+ $sql = sprintf($instruction, $this->prefix);
+ $stm = $this->bd->prepare($sql);
+ $ok &= $stm && $stm->execute();
+ }
+ }
+ } catch (Exception $e) {
+ Minz_Log::error('FreshRSS_EntryDAO::createTagTable error: ' . $e->getMessage());
+ }
+ if ($hadTransaction) {
+ $this->bd->beginTransaction();
+ }
+ return $ok;
+ }
+
+ protected function autoUpdateDb($errorInfo) {
+ if (isset($errorInfo[0])) {
+ if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_TABLE_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_TABLE) {
+ if (stripos($errorInfo[2], 'tag') !== false) {
+ return $this->createTagTable(); //v1.12.0
+ }
+ }
+ }
+ return false;
+ }
+
+ public function addTag($valuesTmp) {
+ $sql = 'INSERT INTO `' . $this->prefix . 'tag`(name, attributes) '
+ . 'SELECT * FROM (SELECT TRIM(?), TRIM(?)) t2 ' //TRIM() to provide a type hint as text for PostgreSQL
+ . 'WHERE NOT EXISTS (SELECT 1 FROM `' . $this->prefix . 'category` WHERE name = TRIM(?))'; //No category of the same name
+ $stm = $this->bd->prepare($sql);
+
+ $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8');
+ $values = array(
+ $valuesTmp['name'],
+ isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
+ $valuesTmp['name'],
+ );
+
+ if ($stm && $stm->execute($values)) {
+ return $this->bd->lastInsertId('"' . $this->prefix . 'tag_id_seq"');
+ } else {
+ $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+ Minz_Log::error('SQL error addTag: ' . $info[2]);
+ return false;
+ }
+ }
+
+ public function addTagObject($tag) {
+ $tag = $this->searchByName($tag->name());
+ if (!$tag) {
+ $values = array(
+ 'name' => $tag->name(),
+ 'attributes' => $tag->attributes(),
+ );
+ return $this->addTag($values);
+ }
+ return $tag->id();
+ }
+
+ public function updateTag($id, $valuesTmp) {
+ $sql = 'UPDATE `' . $this->prefix . 'tag` SET name=?, attributes=? WHERE id=? '
+ . 'AND NOT EXISTS (SELECT 1 FROM `' . $this->prefix . 'category` WHERE name = ?)'; //No category of the same name
+ $stm = $this->bd->prepare($sql);
+
+ $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8');
+ $values = array(
+ $valuesTmp['name'],
+ isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
+ $id,
+ $valuesTmp['name'],
+ );
+
+ if ($stm && $stm->execute($values)) {
+ return $stm->rowCount();
+ } else {
+ $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+ Minz_Log::error('SQL error updateTag: ' . $info[2]);
+ return false;
+ }
+ }
+
+ public function updateTagAttribute($tag, $key, $value) {
+ if ($tag instanceof FreshRSS_Tag) {
+ $tag->_attributes($key, $value);
+ return $this->updateFeed(
+ $tag->id(),
+ array('attributes' => $feed->attributes())
+ );
+ }
+ return false;
+ }
+
+ public function deleteTag($id) {
+ if ($id <= 0) {
+ return false;
+ }
+ $sql = 'DELETE FROM `' . $this->prefix . 'tag` WHERE id=?';
+ $stm = $this->bd->prepare($sql);
+
+ $values = array($id);
+
+ if ($stm && $stm->execute($values)) {
+ return $stm->rowCount();
+ } else {
+ $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+ Minz_Log::error('SQL error deleteTag: ' . $info[2]);
+ return false;
+ }
+ }
+
+ public function searchById($id) {
+ $sql = 'SELECT * FROM `' . $this->prefix . 'tag` WHERE id=?';
+ $stm = $this->bd->prepare($sql);
+ $values = array($id);
+ $stm->execute($values);
+ $res = $stm->fetchAll(PDO::FETCH_ASSOC);
+ $tag = self::daoToTag($res);
+ return isset($tag[0]) ? $tag[0] : null;
+ }
+
+ public function searchByName($name) {
+ $sql = 'SELECT * FROM `' . $this->prefix . 'tag` WHERE name=?';
+ $stm = $this->bd->prepare($sql);
+ $values = array($name);
+ $stm->execute($values);
+ $res = $stm->fetchAll(PDO::FETCH_ASSOC);
+ $tag = self::daoToTag($res);
+ return isset($tag[0]) ? $tag[0] : null;
+ }
+
+ public function listTags($precounts = false) {
+ if ($precounts) {
+ $sql = 'SELECT t.id, t.name, count(e.id) AS unreads '
+ . 'FROM `' . $this->prefix . 'tag` t '
+ . 'LEFT OUTER JOIN `' . $this->prefix . 'entrytag` et ON et.id_tag = t.id '
+ . 'LEFT OUTER JOIN `' . $this->prefix . 'entry` e ON et.id_entry = e.id AND e.is_read = 0 '
+ . 'GROUP BY t.id '
+ . 'ORDER BY t.name';
+ } else {
+ $sql = 'SELECT * FROM `' . $this->prefix . 'tag` ORDER BY name';
+ }
+
+ $stm = $this->bd->prepare($sql);
+ if ($stm && $stm->execute()) {
+ return self::daoToTag($stm->fetchAll(PDO::FETCH_ASSOC));
+ } else {
+ $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+ if ($this->autoUpdateDb($info)) {
+ return $this->listTags($precounts);
+ }
+ Minz_Log::error('SQL error listTags: ' . $info[2]);
+ return false;
+ }
+ }
+
+ public function count() {
+ $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'tag`';
+ $stm = $this->bd->prepare($sql);
+ $stm->execute();
+ $res = $stm->fetchAll(PDO::FETCH_ASSOC);
+ return $res[0]['count'];
+ }
+
+ public function countEntries($id) {
+ $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entrytag` WHERE id_tag=?';
+ $stm = $this->bd->prepare($sql);
+ $values = array($id);
+ $stm->execute($values);
+ $res = $stm->fetchAll(PDO::FETCH_ASSOC);
+ return $res[0]['count'];
+ }
+
+ public function countNotRead($id) {
+ $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entrytag` et '
+ . 'INNER JOIN `' . $this->prefix . 'entry` e ON et.id_entry=e.id '
+ . 'WHERE et.id_tag=? AND e.is_read=0';
+ $stm = $this->bd->prepare($sql);
+ $values = array($id);
+ $stm->execute($values);
+ $res = $stm->fetchAll(PDO::FETCH_ASSOC);
+ return $res[0]['count'];
+ }
+
+ public function tagEntry($id_tag, $id_entry, $checked = true) {
+ if ($checked) {
+ $sql = 'INSERT ' . $this->sqlIgnore() . ' INTO `' . $this->prefix . 'entrytag`(id_tag, id_entry) VALUES(?, ?)';
+ } else {
+ $sql = 'DELETE FROM `' . $this->prefix . 'entrytag` WHERE id_tag=? AND id_entry=?';
+ }
+ $stm = $this->bd->prepare($sql);
+ $values = array($id_tag, $id_entry);
+
+ if ($stm && $stm->execute($values)) {
+ return true;
+ } else {
+ $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+ Minz_Log::error('SQL error tagEntry: ' . $info[2]);
+ return false;
+ }
+ }
+
+ public function getTagsForEntry($id_entry) {
+ $sql = 'SELECT t.id, t.name, et.id_entry IS NOT NULL as checked '
+ . 'FROM `' . $this->prefix . 'tag` t '
+ . 'LEFT OUTER JOIN `' . $this->prefix . 'entrytag` et ON et.id_tag = t.id AND et.id_entry=? '
+ . 'ORDER BY t.name';
+
+ $stm = $this->bd->prepare($sql);
+ $values = array($id_entry);
+
+ if ($stm && $stm->execute($values)) {
+ $lines = $stm->fetchAll(PDO::FETCH_ASSOC);
+ for ($i = count($lines) - 1; $i >= 0; $i--) {
+ $lines[$i]['id'] = intval($lines[$i]['id']);
+ $lines[$i]['checked'] = !empty($lines[$i]['checked']);
+ }
+ return $lines;
+ } else {
+ $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+ if ($this->autoUpdateDb($info)) {
+ return $this->getTagsForEntry($id_entry);
+ }
+ Minz_Log::error('SQL error getTagsForEntry: ' . $info[2]);
+ return false;
+ }
+ }
+
+ //For API
+ public function getEntryIdsTagNames($entries) {
+ $sql = 'SELECT et.id_entry, t.name '
+ . 'FROM `' . $this->prefix . 'tag` t '
+ . 'INNER JOIN `' . $this->prefix . 'entrytag` et ON et.id_tag = t.id';
+
+ $values = array();
+ if (is_array($entries) && count($entries) > 0) {
+ $sql .= ' AND et.id_entry IN (' . str_repeat('?,', count($entries) - 1). '?)';
+ foreach ($entries as $entry) {
+ $values[] = $entry->id();
+ }
+ }
+ $stm = $this->bd->prepare($sql);
+
+ if ($stm && $stm->execute($values)) {
+ $result = array();
+ foreach ($stm->fetchAll(PDO::FETCH_ASSOC) as $line) {
+ $entryId = 'e_' . $line['id_entry'];
+ $tagName = $line['name'];
+ if (empty($result[$entryId])) {
+ $result[$entryId] = array();
+ }
+ $result[$entryId][] = $tagName;
+ }
+ return $result;
+ } else {
+ $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+ if ($this->autoUpdateDb($info)) {
+ return $this->getTagNamesEntryIds($id_entry);
+ }
+ Minz_Log::error('SQL error getTagNamesEntryIds: ' . $info[2]);
+ return false;
+ }
+ }
+
+ public static function daoToTag($listDAO) {
+ $list = array();
+ if (!is_array($listDAO)) {
+ $listDAO = array($listDAO);
+ }
+ foreach ($listDAO as $key => $dao) {
+ $tag = new FreshRSS_Tag(
+ $dao['name']
+ );
+ $tag->_id($dao['id']);
+ if (!empty($dao['attributes'])) {
+ $tag->_attributes('', $dao['attributes']);
+ }
+ if (isset($dao['unreads'])) {
+ $tag->_nbUnread($dao['unreads']);
+ }
+ $list[$key] = $tag;
+ }
+ return $list;
+ }
+}
diff --git a/app/Models/TagDAOPGSQL.php b/app/Models/TagDAOPGSQL.php
new file mode 100644
index 000000000..56a28e294
--- /dev/null
+++ b/app/Models/TagDAOPGSQL.php
@@ -0,0 +1,9 @@
+<?php
+
+class FreshRSS_TagDAOPGSQL extends FreshRSS_TagDAO {
+
+ public function sqlIgnore() {
+ return ''; //TODO
+ }
+
+}
diff --git a/app/Models/TagDAOSQLite.php b/app/Models/TagDAOSQLite.php
new file mode 100644
index 000000000..b1deb6c65
--- /dev/null
+++ b/app/Models/TagDAOSQLite.php
@@ -0,0 +1,19 @@
+<?php
+
+class FreshRSS_TagDAOSQLite extends FreshRSS_TagDAO {
+
+ public function sqlIgnore() {
+ return 'OR IGNORE';
+ }
+
+ protected function autoUpdateDb($errorInfo) {
+ if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='tag'")) {
+ $showCreate = $tableInfo->fetchColumn();
+ if (stripos($showCreate, 'tag') === false) {
+ return $this->createTagTable(); //v1.12.0
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/app/Models/Themes.php b/app/Models/Themes.php
index 8920fbf7e..235269e39 100644
--- a/app/Models/Themes.php
+++ b/app/Models/Themes.php
@@ -68,7 +68,7 @@ class FreshRSS_Themes extends Minz_Model {
return $infos;
}
- public static function icon($name, $urlOnly = false) {
+ public static function alt($name) {
static $alts = array(
'add' => '✚',
'all' => '☰',
@@ -84,6 +84,7 @@ class FreshRSS_Themes extends Minz_Model {
'icon' => '⊚',
'import' => '⤓',
'key' => '⚿',
+ 'label' => '🏷️',
'link' => '↗',
'login' => '🔒',
'logout' => '🔓',
@@ -104,13 +105,18 @@ class FreshRSS_Themes extends Minz_Model {
'view-global' => '☷',
'view-reader' => '☕',
);
- if (!isset($alts[$name])) {
+ return isset($name) ? $alts[$name] : '';
+ }
+
+ public static function icon($name, $urlOnly = false, $altOnly = false) {
+ $alt = self::alt($name);
+ if ($alt == '') {
return '';
}
$url = $name . '.svg';
$url = isset(self::$themeIcons[$url]) ? (self::$themeIconsUrl . $url) : (self::$defaultIconsUrl . $url);
- return $urlOnly ? Minz_Url::display($url) : '<img class="icon" src="' . Minz_Url::display($url) . '" alt="' . $alts[$name] . '" />';
+ return $urlOnly ? Minz_Url::display($url) : '<img class="icon" src="' . Minz_Url::display($url) . '" alt="' . $alt . '" />';
}
}
diff --git a/app/Models/UserDAO.php b/app/Models/UserDAO.php
index c921d54c9..5fb46c947 100644
--- a/app/Models/UserDAO.php
+++ b/app/Models/UserDAO.php
@@ -14,14 +14,13 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
$ok = false;
$bd_prefix_user = $db['prefix'] . $username . '_';
if (defined('SQL_CREATE_TABLES')) { //E.g. MySQL
- $sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP, $bd_prefix_user, _t('gen.short.default_category'));
+ $sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP . SQL_CREATE_TABLE_TAGS, $bd_prefix_user, _t('gen.short.default_category'));
$stm = $userPDO->bd->prepare($sql);
$ok = $stm && $stm->execute();
} else { //E.g. SQLite
- global $SQL_CREATE_TABLES;
- global $SQL_CREATE_TABLE_ENTRYTMP;
+ global $SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_CREATE_TABLE_TAGS;
if (is_array($SQL_CREATE_TABLES)) {
- $instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP);
+ $instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_CREATE_TABLE_TAGS);
$ok = !empty($instructions);
foreach ($instructions as $instruction) {
$sql = sprintf($instruction, $bd_prefix_user, _t('gen.short.default_category'));
diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php
index ef94fdaf6..f607084f8 100644
--- a/app/Models/UserQuery.php
+++ b/app/Models/UserQuery.php
@@ -19,15 +19,17 @@ class FreshRSS_UserQuery {
private $url;
private $feed_dao;
private $category_dao;
+ private $tag_dao;
/**
* @param array $query
* @param FreshRSS_Searchable $feed_dao
* @param FreshRSS_Searchable $category_dao
*/
- public function __construct($query, FreshRSS_Searchable $feed_dao = null, FreshRSS_Searchable $category_dao = null) {
+ public function __construct($query, FreshRSS_Searchable $feed_dao = null, FreshRSS_Searchable $category_dao = null, FreshRSS_Searchable $tag_dao = null) {
$this->category_dao = $category_dao;
$this->feed_dao = $feed_dao;
+ $this->tag_dao = $tag_dao;
if (isset($query['get'])) {
$this->parseGet($query['get']);
}
@@ -88,6 +90,9 @@ class FreshRSS_UserQuery {
case 's':
$this->parseFavorite();
break;
+ case 't':
+ $this->parseTag($matches['id']);
+ break;
}
}
}
@@ -139,6 +144,25 @@ class FreshRSS_UserQuery {
}
/**
+ * Parse the query string when it is a "tag" query
+ *
+ * @param integer $id
+ * @throws FreshRSS_DAO_Exception
+ */
+ private function parseTag($id) {
+ if ($this->tag_dao == null) {
+ throw new FreshRSS_DAO_Exception('Tag DAO is not loaded in UserQuery');
+ }
+ $category = $this->category_dao->searchById($id);
+ if ($tag) {
+ $this->get_name = $tag->name();
+ } else {
+ $this->deprecated = true;
+ }
+ $this->get_type = 'tag';
+ }
+
+ /**
* Parse the query string when it is a "favorite" query
*/
private function parseFavorite() {