aboutsummaryrefslogtreecommitdiff
path: root/app/Models
diff options
context:
space:
mode:
Diffstat (limited to 'app/Models')
-rw-r--r--app/Models/BooleanSearch.php55
-rw-r--r--app/Models/CategoryDAO.php26
-rw-r--r--app/Models/DatabaseDAO.php2
-rw-r--r--app/Models/Entry.php3
-rw-r--r--app/Models/EntryDAO.php188
-rw-r--r--app/Models/EntryDAOSQLite.php78
-rw-r--r--app/Models/Factory.php8
-rw-r--r--app/Models/Feed.php65
-rw-r--r--app/Models/FeedDAO.php80
-rw-r--r--app/Models/FeedDAOSQLite.php17
-rw-r--r--app/Models/UserQuery.php2
11 files changed, 353 insertions, 171 deletions
diff --git a/app/Models/BooleanSearch.php b/app/Models/BooleanSearch.php
new file mode 100644
index 000000000..6e016f7e9
--- /dev/null
+++ b/app/Models/BooleanSearch.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * Contains Boolean search from the search form.
+ */
+class FreshRSS_BooleanSearch {
+
+ private $raw_input = '';
+ private $searches = array();
+
+ public function __construct($input) {
+ $input = trim($input);
+ if ($input == '') {
+ return;
+ }
+ $this->raw_input = $input;
+
+ $input = preg_replace('/:&quot;(.*?)&quot;/', ':"\1"', $input);
+ $splits = preg_split('/\b(OR)\b/i', $input, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+ $segment = '';
+ $ns = count($splits);
+ for ($i = 0; $i < $ns; $i++) {
+ $segment = $segment . $splits[$i];
+ if (trim($segment) == '' || strcasecmp($segment, 'OR') === 0) {
+ $segment = '';
+ } else {
+ $quotes = substr_count($segment, '"') + substr_count($segment, '&quot;');
+ if ($quotes % 2 === 0) {
+ $segment = trim($segment);
+ if ($segment != '') {
+ $this->searches[] = new FreshRSS_Search($segment);
+ }
+ $segment = '';
+ }
+ }
+ }
+ $segment = trim($segment);
+ if ($segment != '') {
+ $this->searches[] = new FreshRSS_Search($segment);
+ }
+ }
+
+ public function searches() {
+ return $this->searches;
+ }
+
+ public function __toString() {
+ return $this->getRawInput();
+ }
+
+ public function getRawInput() {
+ return $this->raw_input;
+ }
+}
diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php
index 68db17db3..ef2c402a0 100644
--- a/app/Models/CategoryDAO.php
+++ b/app/Models/CategoryDAO.php
@@ -134,7 +134,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
if (isset($cat[0])) {
return $cat[0];
} else {
- return false;
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS database error: Default category not found!' . "\n");
+ }
+ Minz_Log::error('FreshRSS database error: Default category not found!');
+ return null;
}
}
public function checkDefault() {
@@ -144,13 +148,27 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
$cat = new FreshRSS_Category(_t('gen.short.default_category'));
$cat->_id(self::DEFAULTCATEGORYID);
+ $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');";
+ }
+ $stm = $this->bd->prepare($sql);
+
$values = array(
- 'id' => $cat->id(),
- 'name' => $cat->name(),
+ $cat->id(),
+ $cat->name(),
);
- $this->addCategory($values);
+ if ($stm && $stm->execute($values)) {
+ return $this->bd->lastInsertId('"' . $this->prefix . 'category_id_seq"');
+ } else {
+ $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+ Minz_Log::error('SQL error check default category: ' . json_encode($info));
+ return false;
+ }
}
+ return true;
}
public function count() {
diff --git a/app/Models/DatabaseDAO.php b/app/Models/DatabaseDAO.php
index f5469f2b7..b8e5577e4 100644
--- a/app/Models/DatabaseDAO.php
+++ b/app/Models/DatabaseDAO.php
@@ -50,7 +50,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',
+ 'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', 'attributes',
'cache_nbEntries', 'cache_nbUnreads'
));
}
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index 0ad3781e5..c6b26a7cc 100644
--- a/app/Models/Entry.php
+++ b/app/Models/Entry.php
@@ -193,7 +193,8 @@ class FreshRSS_Entry extends Minz_Model {
try {
// l'article n'est pas en BDD, on va le chercher sur le site
$this->content = get_content_by_parsing(
- htmlspecialchars_decode($this->link(), ENT_QUOTES), $pathEntries
+ htmlspecialchars_decode($this->link(), ENT_QUOTES), $pathEntries,
+ $this->feed->attributes()
);
} catch (Exception $e) {
// rien à faire, on garde l'ancien contenu(requête a échoué)
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index 8cdebedc5..a3bca3727 100644
--- a/app/Models/EntryDAO.php
+++ b/app/Models/EntryDAO.php
@@ -437,7 +437,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
* @param integer $priorityMin
* @return integer affected rows
*/
- public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filter = null, $state = 0) {
+ public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filters = null, $state = 0) {
FreshRSS_UserDAO::touch();
if ($idMax == 0) {
$idMax = time() . '000000';
@@ -454,7 +454,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
$values = array($idMax);
- list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state);
+ list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
$stm = $this->bd->prepare($sql . $search);
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@@ -480,7 +480,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
* @param integer $idMax fail safe article ID
* @return integer affected rows
*/
- public function markReadCat($id, $idMax = 0, $filter = null, $state = 0) {
+ public function markReadCat($id, $idMax = 0, $filters = null, $state = 0) {
FreshRSS_UserDAO::touch();
if ($idMax == 0) {
$idMax = time() . '000000';
@@ -492,7 +492,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
. 'WHERE f.category=? AND e.is_read=0 AND e.id <= ?';
$values = array($id, $idMax);
- list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state);
+ list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
$stm = $this->bd->prepare($sql . $search);
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@@ -518,7 +518,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
* @param integer $idMax fail safe article ID
* @return integer affected rows
*/
- public function markReadFeed($id_feed, $idMax = 0, $filter = null, $state = 0) {
+ public function markReadFeed($id_feed, $idMax = 0, $filters = null, $state = 0) {
FreshRSS_UserDAO::touch();
if ($idMax == 0) {
$idMax = time() . '000000';
@@ -531,7 +531,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
. 'WHERE id_feed=? AND is_read=0 AND id <= ?';
$values = array($id_feed, $idMax);
- list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
+ list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
$stm = $this->bd->prepare($sql . $search);
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@@ -625,7 +625,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return 'CONCAT(' . $s1 . ',' . $s2 . ')'; //MySQL
}
- protected function sqlListEntriesWhere($alias = '', $filter = null, $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $firstId = '', $date_min = 0) {
+ protected function sqlListEntriesWhere($alias = '', $filters = null, $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $firstId = '', $date_min = 0) {
$search = ' ';
$values = array();
if ($state & FreshRSS_Entry::STATE_NOT_READ) {
@@ -650,10 +650,6 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
default:
throw new FreshRSS_EntriesGetter_Exception('Bad order in Entry->listByType: [' . $order . ']!');
}
- /*if ($firstId === '' && parent::$sharedDbType === 'mysql') {
- //MySQL optimization. TODO: check if this is needed again, after the filtering for old articles has been removed in 0.9-dev
- $firstId = $order === 'DESC' ? '9000000000'. '000000' : '0';
- }*/
if ($firstId !== '') {
$search .= 'AND ' . $alias . 'id ' . ($order === 'DESC' ? '<=' : '>=') . ' ? ';
$values[] = $firstId;
@@ -662,91 +658,111 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$search .= 'AND ' . $alias . 'id >= ? ';
$values[] = $date_min . '000000';
}
- if ($filter) {
- if ($filter->getMinDate()) {
- $search .= 'AND ' . $alias . 'id >= ? ';
- $values[] = "{$filter->getMinDate()}000000";
- }
- if ($filter->getMaxDate()) {
- $search .= 'AND ' . $alias . 'id <= ? ';
- $values[] = "{$filter->getMaxDate()}000000";
- }
- if ($filter->getMinPubdate()) {
- $search .= 'AND ' . $alias . 'date >= ? ';
- $values[] = $filter->getMinPubdate();
- }
- if ($filter->getMaxPubdate()) {
- $search .= 'AND ' . $alias . 'date <= ? ';
- $values[] = $filter->getMaxPubdate();
- }
+ if ($filters && count($filters->searches()) > 0) {
+ $isOpen = false;
+ foreach ($filters->searches() as $filter) {
+ if ($filter == null) {
+ continue;
+ }
+ $sub_search = '';
+ if ($filter->getMinDate()) {
+ $sub_search .= 'AND ' . $alias . 'id >= ? ';
+ $values[] = "{$filter->getMinDate()}000000";
+ }
+ if ($filter->getMaxDate()) {
+ $sub_search .= 'AND ' . $alias . 'id <= ? ';
+ $values[] = "{$filter->getMaxDate()}000000";
+ }
+ if ($filter->getMinPubdate()) {
+ $sub_search .= 'AND ' . $alias . 'date >= ? ';
+ $values[] = $filter->getMinPubdate();
+ }
+ if ($filter->getMaxPubdate()) {
+ $sub_search .= 'AND ' . $alias . 'date <= ? ';
+ $values[] = $filter->getMaxPubdate();
+ }
- if ($filter->getAuthor()) {
- foreach ($filter->getAuthor() as $author) {
- $search .= 'AND ' . $alias . 'author LIKE ? ';
- $values[] = "%{$author}%";
+ if ($filter->getAuthor()) {
+ foreach ($filter->getAuthor() as $author) {
+ $sub_search .= 'AND ' . $alias . 'author LIKE ? ';
+ $values[] = "%{$author}%";
+ }
}
- }
- if ($filter->getIntitle()) {
- foreach ($filter->getIntitle() as $title) {
- $search .= 'AND ' . $alias . 'title LIKE ? ';
- $values[] = "%{$title}%";
+ if ($filter->getIntitle()) {
+ foreach ($filter->getIntitle() as $title) {
+ $sub_search .= 'AND ' . $alias . 'title LIKE ? ';
+ $values[] = "%{$title}%";
+ }
}
- }
- if ($filter->getTags()) {
- foreach ($filter->getTags() as $tag) {
- $search .= 'AND ' . $alias . 'tags LIKE ? ';
- $values[] = "%{$tag}%";
+ if ($filter->getTags()) {
+ foreach ($filter->getTags() as $tag) {
+ $sub_search .= 'AND ' . $alias . 'tags LIKE ? ';
+ $values[] = "%{$tag}%";
+ }
}
- }
- if ($filter->getInurl()) {
- foreach ($filter->getInurl() as $url) {
- $search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? ';
- $values[] = "%{$url}%";
+ if ($filter->getInurl()) {
+ foreach ($filter->getInurl() as $url) {
+ $sub_search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? ';
+ $values[] = "%{$url}%";
+ }
}
- }
- if ($filter->getNotAuthor()) {
- foreach ($filter->getNotAuthor() as $author) {
- $search .= 'AND (NOT ' . $alias . 'author LIKE ?) ';
- $values[] = "%{$author}%";
+ if ($filter->getNotAuthor()) {
+ foreach ($filter->getNotAuthor() as $author) {
+ $sub_search .= 'AND (NOT ' . $alias . 'author LIKE ?) ';
+ $values[] = "%{$author}%";
+ }
}
- }
- if ($filter->getNotIntitle()) {
- foreach ($filter->getNotIntitle() as $title) {
- $search .= 'AND (NOT ' . $alias . 'title LIKE ?) ';
- $values[] = "%{$title}%";
+ if ($filter->getNotIntitle()) {
+ foreach ($filter->getNotIntitle() as $title) {
+ $sub_search .= 'AND (NOT ' . $alias . 'title LIKE ?) ';
+ $values[] = "%{$title}%";
+ }
}
- }
- if ($filter->getNotTags()) {
- foreach ($filter->getNotTags() as $tag) {
- $search .= 'AND (NOT ' . $alias . 'tags LIKE ?) ';
- $values[] = "%{$tag}%";
+ if ($filter->getNotTags()) {
+ foreach ($filter->getNotTags() as $tag) {
+ $sub_search .= 'AND (NOT ' . $alias . 'tags LIKE ?) ';
+ $values[] = "%{$tag}%";
+ }
}
- }
- if ($filter->getNotInurl()) {
- foreach ($filter->getNotInurl() as $url) {
- $search .= 'AND (NOT CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ?) ';
- $values[] = "%{$url}%";
+ if ($filter->getNotInurl()) {
+ foreach ($filter->getNotInurl() as $url) {
+ $sub_search .= 'AND (NOT CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ?) ';
+ $values[] = "%{$url}%";
+ }
}
- }
- if ($filter->getSearch()) {
- foreach ($filter->getSearch() as $search_value) {
- $search .= 'AND ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ? ';
- $values[] = "%{$search_value}%";
+ if ($filter->getSearch()) {
+ foreach ($filter->getSearch() as $search_value) {
+ $sub_search .= 'AND ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ? ';
+ $values[] = "%{$search_value}%";
+ }
}
- }
- if ($filter->getNotSearch()) {
- foreach ($filter->getNotSearch() as $search_value) {
- $search .= 'AND (NOT ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ?) ';
- $values[] = "%{$search_value}%";
+ if ($filter->getNotSearch()) {
+ foreach ($filter->getNotSearch() as $search_value) {
+ $sub_search .= 'AND (NOT ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ?) ';
+ $values[] = "%{$search_value}%";
+ }
}
+
+ if ($sub_search != '') {
+ if ($isOpen) {
+ $search .= 'OR ';
+ } else {
+ $search .= 'AND (';
+ $isOpen = true;
+ }
+ $search .= '(' . substr($sub_search, 4) . ') ';
+ }
+ }
+ if ($isOpen) {
+ $search .= ') ';
}
}
return array($values, $search);
}
- private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {
+ private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
if (!$state) {
$state = FreshRSS_Entry::STATE_ALL;
}
@@ -777,7 +793,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!');
}
- list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state, $order, $firstId, $date_min);
+ 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 '
@@ -785,11 +801,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
. 'WHERE ' . $where
. $search
. 'ORDER BY e.id ' . $order
- . ($limit > 0 ? ' LIMIT ' . $limit : '')); //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
+ . ($limit > 0 ? ' LIMIT ' . intval($limit) : '')); //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
}
- public function listWhereRaw($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {
- list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min);
+ public function listWhereRaw($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
+ list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
$sql = 'SELECT e0.id, e0.guid, e0.title, e0.author, '
. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
@@ -805,8 +821,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return $stm;
}
- public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {
- $stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filter, $date_min);
+ 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));
}
@@ -827,8 +843,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC));
}
- public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) { //For API
- list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min);
+ public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) { //For API
+ list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
$stm = $this->bd->prepare($sql);
$stm->execute($values);
diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php
index 0f57dc1ba..cca970e36 100644
--- a/app/Models/EntryDAOSQLite.php
+++ b/app/Models/EntryDAOSQLite.php
@@ -7,7 +7,6 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
}
protected function autoUpdateDb($errorInfo) {
- Minz_Log::error('FreshRSS_EntryDAO::autoUpdateDb error: ' . print_r($errorInfo, true));
if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entrytmp'")) {
$showCreate = $tableInfo->fetchColumn();
if (stripos($showCreate, 'entrytmp') === false) {
@@ -27,63 +26,28 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
public function commitNewEntries() {
$sql = '
- CREATE TEMP TABLE `tmp` AS
- SELECT
- id,
- guid,
- title,
- author,
- content,
- link,
- date,
- `lastSeen`,
- hash, is_read,
- is_favorite,
- id_feed,
- tags
- FROM `' . $this->prefix . 'entrytmp`
- ORDER BY date;
- INSERT OR IGNORE INTO `' . $this->prefix . 'entry`
- (
- id,
- guid,
- title,
- author,
- content,
- link,
- date,
- `lastSeen`,
- hash,
- is_read,
- is_favorite,
- id_feed,
- tags
- )
- 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
- FROM `tmp`
- ORDER BY date;
- DELETE FROM `' . $this->prefix . 'entrytmp`
- WHERE id <= (SELECT MAX(id)
- FROM `tmp`);
- DROP TABLE `tmp`;';
+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
+ FROM `' . $this->prefix . 'entrytmp`
+ ORDER BY date;
+INSERT OR IGNORE INTO `' . $this->prefix . 'entry`
+ (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags)
+ 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
+ FROM `tmp`
+ ORDER BY date;
+DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= (SELECT MAX(id) FROM `tmp`);
+DROP TABLE IF EXISTS `tmp`;
+';
$hadTransaction = $this->bd->inTransaction();
if (!$hadTransaction) {
$this->bd->beginTransaction();
}
$result = $this->bd->exec($sql) !== false;
+ if (!$result) {
+ Minz_Log::error('SQL error commitNewEntries: ' . json_encode($this->bd->errorInfo()));
+ }
if (!$hadTransaction) {
$this->bd->commit();
}
@@ -195,7 +159,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
* @param integer $priorityMin
* @return integer affected rows
*/
- public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filter = null, $state = 0) {
+ public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filters = null, $state = 0) {
if ($idMax == 0) {
$idMax = time() . '000000';
Minz_Log::debug('Calling markReadEntries(0) is deprecated!');
@@ -209,7 +173,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
}
$values = array($idMax);
- list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
+ list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
$stm = $this->bd->prepare($sql . $search);
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@@ -235,7 +199,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
* @param integer $idMax fail safe article ID
* @return integer affected rows
*/
- public function markReadCat($id, $idMax = 0, $filter = null, $state = 0) {
+ public function markReadCat($id, $idMax = 0, $filters = null, $state = 0) {
if ($idMax == 0) {
$idMax = time() . '000000';
Minz_Log::debug('Calling markReadCat(0) is deprecated!');
@@ -247,7 +211,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
. 'id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.category=?)';
$values = array($idMax, $id);
- list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
+ list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
$stm = $this->bd->prepare($sql . $search);
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
diff --git a/app/Models/Factory.php b/app/Models/Factory.php
index dfccc883e..764987c46 100644
--- a/app/Models/Factory.php
+++ b/app/Models/Factory.php
@@ -3,7 +3,13 @@
class FreshRSS_Factory {
public static function createFeedDao($username = null) {
- return new FreshRSS_FeedDAO($username);
+ $conf = Minz_Configuration::get('system');
+ switch ($conf->db['type']) {
+ case 'sqlite':
+ return new FreshRSS_FeedDAOSQLite($username);
+ default:
+ return new FreshRSS_FeedDAO($username);
+ }
}
public static function createEntryDao($username = null) {
diff --git a/app/Models/Feed.php b/app/Models/Feed.php
index 196d94931..7eb079f15 100644
--- a/app/Models/Feed.php
+++ b/app/Models/Feed.php
@@ -26,6 +26,7 @@ class FreshRSS_Feed extends Minz_Model {
private $error = false;
private $keep_history = self::KEEP_HISTORY_DEFAULT;
private $ttl = self::TTL_DEFAULT;
+ private $attributes = array();
private $mute = false;
private $hash = null;
private $lockPath = '';
@@ -114,6 +115,13 @@ class FreshRSS_Feed extends Minz_Model {
public function ttl() {
return $this->ttl;
}
+ public function attributes($key = '') {
+ if ($key == '') {
+ return $this->attributes;
+ } else {
+ return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
+ }
+ }
public function mute() {
return $this->mute;
}
@@ -234,6 +242,22 @@ class FreshRSS_Feed extends Minz_Model {
$this->ttl = abs($value);
$this->mute = $value < self::TTL_DEFAULT;
}
+
+ 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 _nbNotRead($value) {
$this->nbNotRead = intval($value);
}
@@ -253,7 +277,7 @@ class FreshRSS_Feed extends Minz_Model {
if ($this->httpAuth != '') {
$url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url);
}
- $feed = customSimplePie();
+ $feed = customSimplePie($this->attributes());
if (substr($url, -11) === '#force_feed') {
$feed->force_feed(true);
$url = substr($url, 0, -11);
@@ -311,6 +335,8 @@ class FreshRSS_Feed extends Minz_Model {
public function loadEntries($feed) {
$entries = array();
+ $guids = array();
+ $hasUniqueGuids = true;
foreach ($feed->get_items() as $item) {
$title = html_only_entity_decode(strip_tags($item->get_title()));
@@ -351,9 +377,13 @@ class FreshRSS_Feed extends Minz_Model {
}
}
+ $guid = $item->get_id(false, false);
+ $hasUniqueGuids &= empty($guids['_' . $guid]);
+ $guids['_' . $guid] = true;
+
$entry = new FreshRSS_Entry(
$this->id(),
- $item->get_id(false, false),
+ $guid,
$title === null ? '' : $title,
$author === null ? '' : html_only_entity_decode(strip_tags($author->name == null ? $author->email : $author->name)),
$content === null ? '' : $content,
@@ -368,14 +398,31 @@ class FreshRSS_Feed extends Minz_Model {
unset($item);
}
+ $hasBadGuids = $this->attributes('hasBadGuids');
+ if ($hasBadGuids != !$hasUniqueGuids) {
+ $hasBadGuids = !$hasUniqueGuids;
+ if ($hasBadGuids) {
+ Minz_Log::warning('Feed has invalid GUIDs: ' . $this->url);
+ } else {
+ Minz_Log::warning('Feed has valid GUIDs again: ' . $this->url);
+ }
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $feedDAO->updateFeedAttribute($this, 'hasBadGuids', $hasBadGuids);
+ }
+ if (!$hasUniqueGuids) {
+ foreach ($entries as $entry) {
+ $entry->_guid('');
+ }
+ }
+
$this->entries = $entries;
}
- function cacheModifiedTime() {
+ public function cacheModifiedTime() {
return @filemtime(CACHE_PATH . '/' . md5($this->url) . '.spc');
}
- function lock() {
+ public function lock() {
$this->lockPath = TMP_PATH . '/' . $this->hash() . '.freshrss.lock';
if (file_exists($this->lockPath) && ((time() - @filemtime($this->lockPath)) > 3600)) {
@unlink($this->lockPath);
@@ -388,13 +435,13 @@ class FreshRSS_Feed extends Minz_Model {
return true;
}
- function unlock() {
+ public function unlock() {
@unlink($this->lockPath);
}
//<PubSubHubbub>
- function pubSubHubbubEnabled() {
+ public function pubSubHubbubEnabled() {
$url = $this->selfUrl ? $this->selfUrl : $this->url;
$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';
if ($hubFile = @file_get_contents($hubFilename)) {
@@ -407,7 +454,7 @@ class FreshRSS_Feed extends Minz_Model {
return false;
}
- function pubSubHubbubError($error = true) {
+ public function pubSubHubbubError($error = true) {
$url = $this->selfUrl ? $this->selfUrl : $this->url;
$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';
$hubFile = @file_get_contents($hubFilename);
@@ -420,7 +467,7 @@ class FreshRSS_Feed extends Minz_Model {
return false;
}
- function pubSubHubbubPrepare() {
+ public function pubSubHubbubPrepare() {
$key = '';
if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl && @is_dir(PSHB_PATH)) {
$path = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl);
@@ -467,7 +514,7 @@ class FreshRSS_Feed extends Minz_Model {
}
//Parameter true to subscribe, false to unsubscribe.
- function pubSubHubbubSubscribe($state) {
+ public function pubSubHubbubSubscribe($state) {
$url = $this->selfUrl ? $this->selfUrl : $this->url;
if (FreshRSS_Context::$system_conf->base_url && $url) {
$hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';
diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php
index 0c25ab0ba..9d980c139 100644
--- a/app/Models/FeedDAO.php
+++ b/app/Models/FeedDAO.php
@@ -1,6 +1,33 @@
<?php
class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
+
+ protected function addColumn($name) {
+ Minz_Log::warning('FreshRSS_FeedDAO::addColumn: ' . $name);
+ try {
+ if ($name === 'attributes') { //v1.11.0
+ $stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN attributes TEXT');
+ return $stm && $stm->execute();
+ }
+ } catch (Exception $e) {
+ Minz_Log::error('FreshRSS_FeedDAO::addColumn error: ' . $e->getMessage());
+ }
+ return false;
+ }
+
+ protected function autoUpdateDb($errorInfo) {
+ if (isset($errorInfo[0])) {
+ if ($errorInfo[0] === '42S22' || $errorInfo[0] === '42703') { //ER_BAD_FIELD_ERROR (Mysql), undefined_column (PostgreSQL)
+ foreach (array('attributes') as $column) {
+ if (stripos($errorInfo[2], $column) !== false) {
+ return $this->addColumn($column);
+ }
+ }
+ }
+ }
+ return false;
+ }
+
public function addFeed($valuesTmp) {
$sql = '
INSERT INTO `' . $this->prefix . 'feed`
@@ -15,10 +42,11 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
`httpAuth`,
error,
keep_history,
- ttl
+ ttl,
+ attributes
)
VALUES
- (?, ?, ?, ?, ?, ?, 10, ?, 0, ?, ?)';
+ (?, ?, ?, ?, ?, ?, 10, ?, 0, ?, ?, ?)';
$stm = $this->bd->prepare($sql);
$valuesTmp['url'] = safe_ascii($valuesTmp['url']);
@@ -34,12 +62,16 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
base64_encode($valuesTmp['httpAuth']),
FreshRSS_Feed::KEEP_HISTORY_DEFAULT,
FreshRSS_Feed::TTL_DEFAULT,
+ isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
);
if ($stm && $stm->execute($values)) {
return $this->bd->lastInsertId('"' . $this->prefix . 'feed_id_seq"');
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+ if ($this->autoUpdateDb($info)) {
+ return $this->addFeed($valuesTmp);
+ }
Minz_Log::error('SQL error addFeed: ' . $info[2]);
return false;
}
@@ -60,7 +92,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
'website' => $feed->website(),
'description' => $feed->description(),
'lastUpdate' => 0,
- 'httpAuth' => $feed->httpAuth()
+ 'httpAuth' => $feed->httpAuth(),
+ 'attributes' => $feed->attributes(),
);
$id = $this->addFeed($values);
@@ -87,8 +120,10 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
foreach ($valuesTmp as $key => $v) {
$set .= '`' . $key . '`=?, ';
- if ($key == 'httpAuth') {
+ if ($key === 'httpAuth') {
$valuesTmp[$key] = base64_encode($v);
+ } elseif ($key === 'attributes') {
+ $valuesTmp[$key] = json_encode($v);
}
}
$set = substr($set, 0, -2);
@@ -105,11 +140,25 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return $stm->rowCount();
} else {
$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
- Minz_Log::error('SQL error updateFeed: ' . $info[2]);
+ if ($this->autoUpdateDb($info)) {
+ return $this->updateFeed($id, $valuesTmp);
+ }
+ Minz_Log::error('SQL error updateFeed: ' . $info[2] . ' for feed ' . $id);
return false;
}
}
+ public function updateFeedAttribute($feed, $key, $value) {
+ if ($feed instanceof FreshRSS_Feed) {
+ $feed->_attributes($key, $value);
+ return $this->updateFeed(
+ $feed->id(),
+ array('attributes' => $feed->attributes())
+ );
+ }
+ return false;
+ }
+
public function updateLastUpdate($id, $inError = false, $mtime = 0) { //See also updateCachedValue()
$sql = 'UPDATE `' . $this->prefix . 'feed` '
. 'SET `lastUpdate`=?, error=? '
@@ -250,17 +299,25 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
/**
* Use $defaultCacheDuration == -1 to return all feeds, without filtering them by TTL.
*/
- public function listFeedsOrderUpdate($defaultCacheDuration = 3600) {
+ public function listFeedsOrderUpdate($defaultCacheDuration = 3600, $limit = 0) {
$this->updateTTL();
- $sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl '
+ $sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl, attributes '
. 'FROM `' . $this->prefix . 'feed` '
. ($defaultCacheDuration < 0 ? '' : 'WHERE ttl >= ' . FreshRSS_Feed::TTL_DEFAULT
. ' AND `lastUpdate` < (' . (time() + 60) . '-(CASE WHEN ttl=' . FreshRSS_Feed::TTL_DEFAULT . ' THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ')
- . 'ORDER BY `lastUpdate`';
+ . 'ORDER BY `lastUpdate` '
+ . ($limit < 1 ? '' : 'LIMIT ' . intval($limit));
$stm = $this->bd->prepare($sql);
- $stm->execute();
-
- return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
+ if ($stm && $stm->execute()) {
+ return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
+ } else {
+ $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+ if ($this->autoUpdateDb($info)) {
+ return $this->listFeedsOrderUpdate($defaultCacheDuration);
+ }
+ Minz_Log::error('SQL error listFeedsOrderUpdate: ' . $info[2]);
+ return array();
+ }
}
public function listByCategory($cat) {
@@ -385,6 +442,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$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);
$myFeed->_nbEntries(isset($dao['cache_nbEntries']) ? $dao['cache_nbEntries'] : 0);
if (isset($dao['id'])) {
diff --git a/app/Models/FeedDAOSQLite.php b/app/Models/FeedDAOSQLite.php
new file mode 100644
index 000000000..3c203b378
--- /dev/null
+++ b/app/Models/FeedDAOSQLite.php
@@ -0,0 +1,17 @@
+<?php
+
+class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
+
+ protected function autoUpdateDb($errorInfo) {
+ if ($tableInfo = $this->bd->query("PRAGMA table_info('feed')")) {
+ $columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);
+ foreach (array('attributes') as $column) {
+ if (!in_array($column, $columns)) {
+ return $this->addColumn($column);
+ }
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php
index 52747f538..ef94fdaf6 100644
--- a/app/Models/UserQuery.php
+++ b/app/Models/UserQuery.php
@@ -41,7 +41,7 @@ class FreshRSS_UserQuery {
$query['search'] = '';
}
// linked to deeply with the search object, need to use dependency injection
- $this->search = new FreshRSS_Search($query['search']);
+ $this->search = new FreshRSS_BooleanSearch($query['search']);
if (isset($query['state'])) {
$this->state = $query['state'];
}