aboutsummaryrefslogtreecommitdiff
path: root/app/Models
diff options
context:
space:
mode:
Diffstat (limited to 'app/Models')
-rw-r--r--app/Models/Auth.php27
-rw-r--r--app/Models/ConfigurationSetter.php4
-rw-r--r--app/Models/Entry.php8
-rw-r--r--app/Models/EntryDAO.php189
-rw-r--r--app/Models/EntryDAOPGSQL.php26
-rw-r--r--app/Models/EntryDAOSQLite.php39
-rw-r--r--app/Models/Factory.php9
-rw-r--r--app/Models/Feed.php21
-rw-r--r--app/Models/FeedDAO.php56
-rw-r--r--app/Models/FeedDAOSQLite.php19
-rw-r--r--app/Models/Search.php168
-rw-r--r--app/Models/UserDAO.php10
12 files changed, 413 insertions, 163 deletions
diff --git a/app/Models/Auth.php b/app/Models/Auth.php
index 476627e10..4de058999 100644
--- a/app/Models/Auth.php
+++ b/app/Models/Auth.php
@@ -74,6 +74,10 @@ class FreshRSS_Auth {
public static function giveAccess() {
$current_user = Minz_Session::param('currentUser');
$user_conf = get_user_configuration($current_user);
+ if ($user_conf == null) {
+ self::$login_ok = false;
+ return;
+ }
$system_conf = Minz_Configuration::get('system');
switch ($system_conf->auth_type) {
@@ -120,13 +124,28 @@ class FreshRSS_Auth {
* Removes all accesses for the current user.
*/
public static function removeAccess() {
- Minz_Session::_param('loginOk');
self::$login_ok = false;
- $conf = Minz_Configuration::get('system');
- Minz_Session::_param('currentUser', $conf->default_user);
+ Minz_Session::_param('loginOk');
Minz_Session::_param('csrf');
+ $system_conf = Minz_Configuration::get('system');
- switch ($conf->auth_type) {
+ $username = '';
+ $token_param = Minz_Request::param('token', '');
+ if ($token_param != '') {
+ $username = trim(Minz_Request::param('user', ''));
+ if ($username != '') {
+ $conf = get_user_configuration($username);
+ if ($conf == null) {
+ $username = '';
+ }
+ }
+ }
+ if ($username == '') {
+ $username = $system_conf->default_user;
+ }
+ Minz_Session::_param('currentUser', $username);
+
+ switch ($system_conf->auth_type) {
case 'form':
Minz_Session::_param('passwordHash');
FreshRSS_FormAuth::deleteCookie();
diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php
index 046f54955..70e1dea2e 100644
--- a/app/Models/ConfigurationSetter.php
+++ b/app/Models/ConfigurationSetter.php
@@ -197,6 +197,10 @@ class FreshRSS_ConfigurationSetter {
$data['hide_read_feeds'] = $this->handleBool($value);
}
+ private function _sides_close_article(&$data, $value) {
+ $data['sides_close_article'] = $this->handleBool($value);
+ }
+
private function _lazyload(&$data, $value) {
$data['lazyload'] = $this->handleBool($value);
}
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index a562a963a..26cd24797 100644
--- a/app/Models/Entry.php
+++ b/app/Models/Entry.php
@@ -22,7 +22,6 @@ class FreshRSS_Entry extends Minz_Model {
public function __construct($feed = '', $guid = '', $title = '', $author = '', $content = '',
$link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') {
- $this->_guid($guid);
$this->_title($title);
$this->_author($author);
$this->_content($content);
@@ -32,6 +31,7 @@ class FreshRSS_Entry extends Minz_Model {
$this->_isFavorite($is_favorite);
$this->_feed($feed);
$this->_tags(preg_split('/[\s#]/', $tags));
+ $this->_guid($guid);
}
public function id() {
@@ -101,6 +101,12 @@ class FreshRSS_Entry extends Minz_Model {
$this->id = $value;
}
public function _guid($value) {
+ if ($value == '') {
+ $value = $this->link;
+ if ($value == '') {
+ $value = $this->hash();
+ }
+ }
$this->guid = $value;
}
public function _title($value) {
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index afcde3d7f..7e836097a 100644
--- a/app/Models/EntryDAO.php
+++ b/app/Models/EntryDAO.php
@@ -88,6 +88,38 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return false;
}
+ protected function createEntryTempTable() {
+ $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 CREATE TABLE entrytmp...');
+ if (defined('SQL_CREATE_TABLE_ENTRYTMP')) {
+ $sql = sprintf(SQL_CREATE_TABLE_ENTRYTMP, $this->prefix);
+ $stm = $this->bd->prepare($sql);
+ $ok = $stm && $stm->execute();
+ } else {
+ global $SQL_CREATE_TABLE_ENTRYTMP;
+ $ok = !empty($SQL_CREATE_TABLE_ENTRYTMP);
+ foreach ($SQL_CREATE_TABLE_ENTRYTMP as $instruction) {
+ $sql = sprintf($instruction, $this->prefix);
+ $stm = $this->bd->prepare($sql);
+ $ok &= $stm && $stm->execute();
+ }
+ }
+ } catch (Exception $e) {
+ Minz_Log::error('FreshRSS_EntryDAO::createEntryTempTable error: ' . $e->getMessage());
+ }
+ if ($hadTransaction) {
+ $this->bd->beginTransaction();
+ }
+ return $ok;
+ }
+
protected function autoUpdateDb($errorInfo) {
if (isset($errorInfo[0])) {
if ($errorInfo[0] === '42S22') { //ER_BAD_FIELD_ERROR
@@ -97,6 +129,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return $this->addColumn($column);
}
}
+ } elseif ($errorInfo[0] === '42S02' && stripos($errorInfo[2], 'entrytmp') !== false) { //ER_BAD_TABLE_ERROR
+ return $this->createEntryTempTable(); //v1.7
}
}
if (isset($errorInfo[1])) {
@@ -110,8 +144,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
private $addEntryPrepared = null;
public function addEntry($valuesTmp) {
- if ($this->addEntryPrepared === null) {
- $sql = 'INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, '
+ if ($this->addEntryPrepared == null) {
+ $sql = 'INSERT INTO `' . $this->prefix . 'entrytmp` (id, guid, title, author, '
. ($this->isCompressed() ? 'content_bin' : 'content')
. ', link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) '
. 'VALUES(:id, :guid, :title, :author, '
@@ -121,43 +155,45 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
. ', :is_read, :is_favorite, :id_feed, :tags)';
$this->addEntryPrepared = $this->bd->prepare($sql);
}
- $this->addEntryPrepared->bindParam(':id', $valuesTmp['id']);
- $valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
- $valuesTmp['guid'] = safe_ascii($valuesTmp['guid']);
- $this->addEntryPrepared->bindParam(':guid', $valuesTmp['guid']);
- $valuesTmp['title'] = substr($valuesTmp['title'], 0, 255);
- $this->addEntryPrepared->bindParam(':title', $valuesTmp['title']);
- $valuesTmp['author'] = substr($valuesTmp['author'], 0, 255);
- $this->addEntryPrepared->bindParam(':author', $valuesTmp['author']);
- $this->addEntryPrepared->bindParam(':content', $valuesTmp['content']);
- $valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023);
- $valuesTmp['link'] = safe_ascii($valuesTmp['link']);
- $this->addEntryPrepared->bindParam(':link', $valuesTmp['link']);
- $this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
- $valuesTmp['lastSeen'] = time();
- $this->addEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
- $valuesTmp['is_read'] = $valuesTmp['is_read'] ? 1 : 0;
- $this->addEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'], PDO::PARAM_INT);
- $valuesTmp['is_favorite'] = $valuesTmp['is_favorite'] ? 1 : 0;
- $this->addEntryPrepared->bindParam(':is_favorite', $valuesTmp['is_favorite'], PDO::PARAM_INT);
- $this->addEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT);
- $valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023);
- $this->addEntryPrepared->bindParam(':tags', $valuesTmp['tags']);
-
- if ($this->hasNativeHex()) {
- $this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
- } else {
- $valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']); //hex2bin() is PHP5.4+
- $this->addEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
+ if ($this->addEntryPrepared) {
+ $this->addEntryPrepared->bindParam(':id', $valuesTmp['id']);
+ $valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
+ $valuesTmp['guid'] = safe_ascii($valuesTmp['guid']);
+ $this->addEntryPrepared->bindParam(':guid', $valuesTmp['guid']);
+ $valuesTmp['title'] = substr($valuesTmp['title'], 0, 255);
+ $this->addEntryPrepared->bindParam(':title', $valuesTmp['title']);
+ $valuesTmp['author'] = substr($valuesTmp['author'], 0, 255);
+ $this->addEntryPrepared->bindParam(':author', $valuesTmp['author']);
+ $this->addEntryPrepared->bindParam(':content', $valuesTmp['content']);
+ $valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023);
+ $valuesTmp['link'] = safe_ascii($valuesTmp['link']);
+ $this->addEntryPrepared->bindParam(':link', $valuesTmp['link']);
+ $this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
+ $valuesTmp['lastSeen'] = time();
+ $this->addEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
+ $valuesTmp['is_read'] = $valuesTmp['is_read'] ? 1 : 0;
+ $this->addEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'], PDO::PARAM_INT);
+ $valuesTmp['is_favorite'] = $valuesTmp['is_favorite'] ? 1 : 0;
+ $this->addEntryPrepared->bindParam(':is_favorite', $valuesTmp['is_favorite'], PDO::PARAM_INT);
+ $this->addEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT);
+ $valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023);
+ $this->addEntryPrepared->bindParam(':tags', $valuesTmp['tags']);
+
+ if ($this->hasNativeHex()) {
+ $this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
+ } else {
+ $valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']); //hex2bin() is PHP5.4+
+ $this->addEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
+ }
}
-
if ($this->addEntryPrepared && $this->addEntryPrepared->execute()) {
- return $this->bd->lastInsertId();
+ return true;
} else {
$info = $this->addEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->addEntryPrepared->errorInfo();
if ($this->autoUpdateDb($info)) {
+ $this->addEntryPrepared = null;
return $this->addEntry($valuesTmp);
- } elseif ((int)($info[0] / 1000) !== 23) { //Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries
+ } elseif ((int)((int)$info[0] / 1000) !== 23) { //Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries
Minz_Log::error('SQL error addEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
. ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title']);
}
@@ -165,6 +201,22 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
}
+ public function commitNewEntries() {
+ $sql = 'SET @rank=(SELECT MAX(id) - COUNT(*) FROM `' . $this->prefix . 'entrytmp`); ' . //MySQL-specific
+ 'INSERT IGNORE INTO `' . $this->prefix . 'entry` (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 FROM `' . $this->prefix . 'entrytmp` ORDER BY date; ' .
+ 'DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= @rank;';
+ $hadTransaction = $this->bd->inTransaction();
+ if (!$hadTransaction) {
+ $this->bd->beginTransaction();
+ }
+ $result = $this->bd->exec($sql) !== false;
+ if (!$hadTransaction) {
+ $this->bd->commit();
+ }
+ return $result;
+ }
+
private $updateEntryPrepared = null;
public function updateEntry($valuesTmp) {
@@ -212,7 +264,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute()) {
- return $this->bd->lastInsertId();
+ return true;
} else {
$info = $this->updateEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->updateEntryPrepared->errorInfo();
if ($this->autoUpdateDb($info)) {
@@ -578,18 +630,6 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$search .= 'AND ' . $alias . 'id >= ' . $date_min . '000000 ';
}
if ($filter) {
- if ($filter->getIntitle()) {
- $search .= 'AND ' . $alias . 'title LIKE ? ';
- $values[] = "%{$filter->getIntitle()}%";
- }
- if ($filter->getInurl()) {
- $search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? ';
- $values[] = "%{$filter->getInurl()}%";
- }
- if ($filter->getAuthor()) {
- $search .= 'AND ' . $alias . 'author LIKE ? ';
- $values[] = "%{$filter->getAuthor()}%";
- }
if ($filter->getMinDate()) {
$search .= 'AND ' . $alias . 'id >= ? ';
$values[] = "{$filter->getMinDate()}000000";
@@ -606,20 +646,69 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
$search .= 'AND ' . $alias . 'date <= ? ';
$values[] = $filter->getMaxPubdate();
}
+
+ if ($filter->getAuthor()) {
+ foreach ($filter->getAuthor() as $author) {
+ $search .= 'AND ' . $alias . 'author LIKE ? ';
+ $values[] = "%{$author}%";
+ }
+ }
+ if ($filter->getIntitle()) {
+ foreach ($filter->getIntitle() as $title) {
+ $search .= 'AND ' . $alias . 'title LIKE ? ';
+ $values[] = "%{$title}%";
+ }
+ }
if ($filter->getTags()) {
- $tags = $filter->getTags();
- foreach ($tags as $tag) {
+ foreach ($filter->getTags() as $tag) {
$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->getNotAuthor()) {
+ foreach ($filter->getNotAuthor() as $author) {
+ $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->getNotTags()) {
+ foreach ($filter->getNotTags() as $tag) {
+ $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->getSearch()) {
- $search_values = $filter->getSearch();
- foreach ($search_values as $search_value) {
+ 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->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}%";
+ }
+ }
}
return array($values, $search);
}
diff --git a/app/Models/EntryDAOPGSQL.php b/app/Models/EntryDAOPGSQL.php
index b96a62ebc..b25993c47 100644
--- a/app/Models/EntryDAOPGSQL.php
+++ b/app/Models/EntryDAOPGSQL.php
@@ -11,6 +11,11 @@ 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();
+ }
+ }
return false;
}
@@ -18,6 +23,27 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
return false;
}
+ public function commitNewEntries() {
+ $sql = 'DO $$
+DECLARE
+maxrank bigint := (SELECT MAX(id) FROM `' . $this->prefix . 'entrytmp`);
+rank bigint := (SELECT maxrank - COUNT(*) FROM `' . $this->prefix . 'entrytmp`);
+BEGIN
+ INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags)
+ (SELECT rank + row_number() OVER(ORDER BY date) AS id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags FROM `' . $this->prefix . 'entrytmp` ORDER BY date);
+ DELETE FROM `' . $this->prefix . 'entrytmp` WHERE id <= maxrank;
+END $$;';
+ $hadTransaction = $this->bd->inTransaction();
+ if (!$hadTransaction) {
+ $this->bd->beginTransaction();
+ }
+ $result = $this->bd->exec($sql) !== false;
+ if (!$hadTransaction) {
+ $this->bd->commit();
+ }
+ return $result;
+ }
+
public function size($all = true) {
$db = FreshRSS_Context::$system_conf->db;
$sql = 'SELECT pg_size_pretty(pg_database_size(?))';
diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php
index 34e854608..ad7bcd865 100644
--- a/app/Models/EntryDAOSQLite.php
+++ b/app/Models/EntryDAOSQLite.php
@@ -7,21 +7,42 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
}
protected function autoUpdateDb($errorInfo) {
- if (empty($errorInfo[0]) || $errorInfo[0] == '42S22') { //ER_BAD_FIELD_ERROR
- //autoAddColumn
- if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) {
- $showCreate = $tableInfo->fetchColumn();
- Minz_Log::debug('FreshRSS_EntryDAOSQLite::autoUpdateDb: ' . $showCreate);
- foreach (array('lastSeen', 'hash') as $column) {
- if (stripos($showCreate, $column) === false) {
- return $this->addColumn($column);
- }
+ 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) {
+ return $this->createEntryTempTable();
+ }
+ }
+ if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) {
+ $showCreate = $tableInfo->fetchColumn();
+ foreach (array('lastSeen', 'hash') as $column) {
+ if (stripos($showCreate, $column) === false) {
+ return $this->addColumn($column);
}
}
}
return false;
}
+ 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`;';
+ $hadTransaction = $this->bd->inTransaction();
+ if (!$hadTransaction) {
+ $this->bd->beginTransaction();
+ }
+ $result = $this->bd->exec($sql) !== false;
+ if (!$hadTransaction) {
+ $this->bd->commit();
+ }
+ return $result;
+ }
+
protected function sqlConcat($s1, $s2) {
return $s1 . '||' . $s2;
}
diff --git a/app/Models/Factory.php b/app/Models/Factory.php
index 6502c38b7..dfccc883e 100644
--- a/app/Models/Factory.php
+++ b/app/Models/Factory.php
@@ -3,14 +3,7 @@
class FreshRSS_Factory {
public static function createFeedDao($username = null) {
- $conf = Minz_Configuration::get('system');
- switch ($conf->db['type']) {
- case 'sqlite':
- case 'pgsql':
- return new FreshRSS_FeedDAOSQLite($username);
- default:
- return new FreshRSS_FeedDAO($username);
- }
+ return new FreshRSS_FeedDAO($username);
}
public static function createEntryDao($username = null) {
diff --git a/app/Models/Feed.php b/app/Models/Feed.php
index 7a9cf8612..52d49db6e 100644
--- a/app/Models/Feed.php
+++ b/app/Models/Feed.php
@@ -318,7 +318,7 @@ class FreshRSS_Feed extends Minz_Model {
$elinks = array();
foreach ($item->get_enclosures() as $enclosure) {
$elink = $enclosure->get_link();
- if (empty($elinks[$elink])) {
+ if ($elink != '' && empty($elinks[$elink])) {
$elinks[$elink] = '1';
$mime = strtolower($enclosure->get_type());
if (strpos($mime, 'image/') === 0) {
@@ -327,6 +327,8 @@ class FreshRSS_Feed extends Minz_Model {
$content .= '<p class="enclosure"><audio preload="none" src="' . $elink . '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>';
} elseif (strpos($mime, 'video/') === 0) {
$content .= '<p class="enclosure"><video preload="none" src="' . $elink . '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>';
+ } elseif (strpos($mime, 'application/') === 0 || strpos($mime, 'text/') === 0) {
+ $content .= '<p class="enclosure"><a download="" href="' . $elink . '">💾</a></p>';
} else {
unset($elinks[$elink]);
}
@@ -335,7 +337,7 @@ class FreshRSS_Feed extends Minz_Model {
$entry = new FreshRSS_Entry(
$this->id(),
- $item->get_id(),
+ $item->get_id(false, false),
$title === null ? '' : $title,
$author === null ? '' : html_only_entity_decode($author->name),
$content === null ? '' : $content,
@@ -429,7 +431,7 @@ class FreshRSS_Feed extends Minz_Model {
}
} else {
@mkdir($path, 0777, true);
- $key = sha1($path . FreshRSS_Context::$system_conf->salt . uniqid(mt_rand(), true));
+ $key = sha1($path . FreshRSS_Context::$system_conf->salt);
$hubJson = array(
'hub' => $this->hubUrl,
'key' => $key,
@@ -451,15 +453,16 @@ class FreshRSS_Feed extends Minz_Model {
//Parameter true to subscribe, false to unsubscribe.
function pubSubHubbubSubscribe($state) {
- if (FreshRSS_Context::$system_conf->base_url && $this->hubUrl && $this->selfUrl) {
- $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($this->selfUrl) . '/!hub.json';
+ $url = $this->selfUrl ? $this->selfUrl : $this->url;
+ if (FreshRSS_Context::$system_conf->base_url && $url) {
+ $hubFilename = PSHB_PATH . '/feeds/' . base64url_encode($url) . '/!hub.json';
$hubFile = @file_get_contents($hubFilename);
if ($hubFile === false) {
Minz_Log::warning('JSON not found for PubSubHubbub: ' . $this->url);
return false;
}
$hubJson = json_decode($hubFile, true);
- if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) {
+ if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key']) || empty($hubJson['hub'])) {
Minz_Log::warning('Invalid JSON for PubSubHubbub: ' . $this->url);
return false;
}
@@ -474,13 +477,13 @@ class FreshRSS_Feed extends Minz_Model {
}
$ch = curl_init();
curl_setopt_array($ch, array(
- CURLOPT_URL => $this->hubUrl,
+ CURLOPT_URL => $hubJson['hub'],
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERAGENT => 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')',
CURLOPT_POSTFIELDS => 'hub.verify=sync'
. '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe')
- . '&hub.topic=' . urlencode($this->selfUrl)
+ . '&hub.topic=' . urlencode($url)
. '&hub.callback=' . urlencode($callbackUrl)
)
);
@@ -488,7 +491,7 @@ class FreshRSS_Feed extends Minz_Model {
$info = curl_getinfo($ch);
file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" .
- 'PubSubHubbub ' . ($state ? 'subscribe' : 'unsubscribe') . ' to ' . $this->selfUrl .
+ 'PubSubHubbub ' . ($state ? 'subscribe' : 'unsubscribe') . ' to ' . $url .
' with callback ' . $callbackUrl . ': ' . $info['http_code'] . ' ' . $response . "\n", FILE_APPEND);
if (substr($info['http_code'], 0, 1) == '2') {
diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php
index 0168aebd9..d278122e3 100644
--- a/app/Models/FeedDAO.php
+++ b/app/Models/FeedDAO.php
@@ -92,29 +92,15 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
}
}
- public function updateLastUpdate($id, $inError = false, $updateCache = true, $mtime = 0) {
- if ($updateCache) {
- $sql = 'UPDATE `' . $this->prefix . 'feed` ' //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
- . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
- . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),'
- . '`lastUpdate`=?, error=? '
- . 'WHERE id=?';
- } else {
- $sql = 'UPDATE `' . $this->prefix . 'feed` '
- . 'SET `lastUpdate`=?, error=? '
- . 'WHERE id=?';
- }
-
- if ($mtime <= 0) {
- $mtime = time();
- }
-
+ public function updateLastUpdate($id, $inError = false, $mtime = 0) { //See also updateCachedValue()
+ $sql = 'UPDATE `' . $this->prefix . 'feed` '
+ . 'SET `lastUpdate`=?, error=? '
+ . 'WHERE id=?';
$values = array(
- $mtime,
+ $mtime <= 0 ? time() : $mtime,
$inError ? 1 : 0,
$id,
);
-
$stm = $this->bd->prepare($sql);
if ($stm && $stm->execute($values)) {
@@ -294,18 +280,28 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return $res[0]['count'];
}
- public function updateCachedValues() { //For one single feed, call updateLastUpdate($id)
- $sql = 'UPDATE `' . $this->prefix . 'feed` f '
- . 'INNER JOIN ('
- . 'SELECT e.id_feed, '
- . 'COUNT(CASE WHEN e.is_read = 0 THEN 1 END) AS nbUnreads, '
- . 'COUNT(e.id) AS nbEntries '
- . 'FROM `' . $this->prefix . 'entry` e '
- . 'GROUP BY e.id_feed'
- . ') x ON x.id_feed=f.id '
- . 'SET f.`cache_nbEntries`=x.nbEntries, f.`cache_nbUnreads`=x.nbUnreads';
+ public function updateCachedValue($id) { //For multiple feeds, call updateCachedValues()
+ $sql = 'UPDATE `' . $this->prefix . 'feed` ' //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
+ . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+ . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0) '
+ . 'WHERE id=?';
+ $values = array($id);
$stm = $this->bd->prepare($sql);
+ if ($stm && $stm->execute($values)) {
+ return $stm->rowCount();
+ } else {
+ $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+ Minz_Log::error('SQL error updateCachedValue: ' . $info[2]);
+ return false;
+ }
+ }
+
+ public function updateCachedValues() { //For one single feed, call updateCachedValue($id)
+ $sql = 'UPDATE `' . $this->prefix . 'feed` '
+ . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+ . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
+ $stm = $this->bd->prepare($sql);
if ($stm && $stm->execute()) {
return $stm->rowCount();
} else {
@@ -343,7 +339,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
return $affected;
}
- public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateLastUpdate($id) or updateCachedValues() just after
+ public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateCachedValue($id) 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
diff --git a/app/Models/FeedDAOSQLite.php b/app/Models/FeedDAOSQLite.php
deleted file mode 100644
index 440ae74da..000000000
--- a/app/Models/FeedDAOSQLite.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
-
- public function updateCachedValues() { //For one single feed, call updateLastUpdate($id)
- $sql = 'UPDATE `' . $this->prefix . 'feed` '
- . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
- . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
- $stm = $this->bd->prepare($sql);
- if ($stm && $stm->execute()) {
- return $stm->rowCount();
- } else {
- $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
- Minz_Log::error('SQL error updateCachedValues: ' . $info[2]);
- return false;
- }
- }
-
-}
diff --git a/app/Models/Search.php b/app/Models/Search.php
index 575a9a2cb..5cc7f8e8d 100644
--- a/app/Models/Search.php
+++ b/app/Models/Search.php
@@ -23,18 +23,35 @@ class FreshRSS_Search {
private $tags;
private $search;
+ private $not_intitle;
+ private $not_inurl;
+ private $not_author;
+ private $not_tags;
+ private $not_search;
+
public function __construct($input) {
- if (strcmp($input, '') == 0) {
+ if ($input == '') {
return;
}
$this->raw_input = $input;
+
+ $input = preg_replace('/:&quot;(.*?)&quot;/', ':"\1"', $input);
+
+ $input = $this->parseNotIntitleSearch($input);
+ $input = $this->parseNotAuthorSearch($input);
+ $input = $this->parseNotInurlSearch($input);
+ $input = $this->parseNotTagsSeach($input);
+
+ $input = $this->parsePubdateSearch($input);
+ $input = $this->parseDateSearch($input);
+
$input = $this->parseIntitleSearch($input);
$input = $this->parseAuthorSearch($input);
$input = $this->parseInurlSearch($input);
- $input = $this->parsePubdateSearch($input);
- $input = $this->parseDateSearch($input);
$input = $this->parseTagsSeach($input);
- $this->parseSearch($input);
+
+ $input = $this->parseNotSearch($input);
+ $input = $this->parseSearch($input);
}
public function __toString() {
@@ -48,6 +65,9 @@ class FreshRSS_Search {
public function getIntitle() {
return $this->intitle;
}
+ public function getNotIntitle() {
+ return $this->not_intitle;
+ }
public function getMinDate() {
return $this->min_date;
@@ -68,18 +88,34 @@ class FreshRSS_Search {
public function getInurl() {
return $this->inurl;
}
+ public function getNotInurl() {
+ return $this->not_inurl;
+ }
public function getAuthor() {
return $this->author;
}
+ public function getNotAuthor() {
+ return $this->not_author;
+ }
public function getTags() {
return $this->tags;
}
+ public function getNotTags() {
+ return $this->not_tags;
+ }
public function getSearch() {
return $this->search;
}
+ public function getNotSearch() {
+ return $this->not_search;
+ }
+
+ private static function removeEmptyValues($anArray) {
+ return is_array($anArray) ? array_filter($anArray, function($value) { return $value !== ''; }) : array();
+ }
/**
* Parse the search string to find intitle keyword and the search related
@@ -90,14 +126,28 @@ class FreshRSS_Search {
* @return string
*/
private function parseIntitleSearch($input) {
- if (preg_match('/intitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+ if (preg_match_all('/\bintitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
$this->intitle = $matches['search'];
- return str_replace($matches[0], '', $input);
+ $input = str_replace($matches[0], '', $input);
}
- if (preg_match('/intitle:(?P<search>\w*)/', $input, $matches)) {
- $this->intitle = $matches['search'];
- return str_replace($matches[0], '', $input);
+ if (preg_match_all('/\bintitle:(?P<search>\w*)/', $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);
+ return $input;
+ }
+
+ private function parseNotIntitleSearch($input) {
+ if (preg_match_all('/[!-]intitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+ $this->not_intitle = $matches['search'];
+ $input = str_replace($matches[0], '', $input);
+ }
+ if (preg_match_all('/[!-]intitle:(?P<search>\w*)/', $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);
return $input;
}
@@ -112,30 +162,54 @@ class FreshRSS_Search {
* @return string
*/
private function parseAuthorSearch($input) {
- if (preg_match('/author:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+ if (preg_match_all('/\bauthor:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
$this->author = $matches['search'];
- return str_replace($matches[0], '', $input);
+ $input = str_replace($matches[0], '', $input);
}
- if (preg_match('/author:(?P<search>\w*)/', $input, $matches)) {
- $this->author = $matches['search'];
- return str_replace($matches[0], '', $input);
+ if (preg_match_all('/\bauthor:(?P<search>\w*)/', $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);
+ return $input;
+ }
+
+ private function parseNotAuthorSearch($input) {
+ if (preg_match_all('/[!-]author:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+ $this->not_author = $matches['search'];
+ $input = str_replace($matches[0], '', $input);
+ }
+ if (preg_match_all('/[!-]author:(?P<search>\w*)/', $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);
return $input;
}
/**
* Parse the search string to find inurl keyword and the search related
* to it.
- * The search is the first word following the keyword except.
+ * The search is the first word following the keyword.
*
* @param string $input
* @return string
*/
private function parseInurlSearch($input) {
- if (preg_match('/inurl:(?P<search>[^\s]*)/', $input, $matches)) {
+ if (preg_match_all('/\binurl:(?P<search>[^\s]*)/', $input, $matches)) {
$this->inurl = $matches['search'];
- return str_replace($matches[0], '', $input);
+ $input = str_replace($matches[0], '', $input);
+ }
+ $this->inurl = self::removeEmptyValues($this->inurl);
+ return $input;
+ }
+
+ private function parseNotInurlSearch($input) {
+ if (preg_match_all('/[!-]inurl:(?P<search>[^\s]*)/', $input, $matches)) {
+ $this->not_inurl = $matches['search'];
+ $input = str_replace($matches[0], '', $input);
}
+ $this->not_inurl = self::removeEmptyValues($this->not_inurl);
return $input;
}
@@ -148,9 +222,12 @@ class FreshRSS_Search {
* @return string
*/
private function parseDateSearch($input) {
- if (preg_match('/date:(?P<search>[^\s]*)/', $input, $matches)) {
- list($this->min_date, $this->max_date) = parseDateInterval($matches['search']);
- return str_replace($matches[0], '', $input);
+ if (preg_match_all('/\bdate:(?P<search>[^\s]*)/', $input, $matches)) {
+ $input = str_replace($matches[0], '', $input);
+ $dates = self::removeEmptyValues($matches['search']);
+ if (!empty($dates[0])) {
+ list($this->min_date, $this->max_date) = parseDateInterval($dates[0]);
+ }
}
return $input;
}
@@ -164,9 +241,12 @@ class FreshRSS_Search {
* @return string
*/
private function parsePubdateSearch($input) {
- if (preg_match('/pubdate:(?P<search>[^\s]*)/', $input, $matches)) {
- list($this->min_pubdate, $this->max_pubdate) = parseDateInterval($matches['search']);
- return str_replace($matches[0], '', $input);
+ if (preg_match_all('/\bpubdate:(?P<search>[^\s]*)/', $input, $matches)) {
+ $input = str_replace($matches[0], '', $input);
+ $dates = self::removeEmptyValues($matches['search']);
+ if (!empty($dates[0])) {
+ list($this->min_pubdate, $this->max_pubdate) = parseDateInterval($dates[0]);
+ }
}
return $input;
}
@@ -182,8 +262,18 @@ class FreshRSS_Search {
private function parseTagsSeach($input) {
if (preg_match_all('/#(?P<search>[^\s]+)/', $input, $matches)) {
$this->tags = $matches['search'];
- return str_replace($matches[0], '', $input);
+ $input = str_replace($matches[0], '', $input);
}
+ $this->tags = self::removeEmptyValues($this->tags);
+ return $input;
+ }
+
+ private function parseNotTagsSeach($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);
return $input;
}
@@ -196,16 +286,16 @@ class FreshRSS_Search {
* @return string
*/
private function parseSearch($input) {
- $input = $this->cleanSearch($input);
- if (strcmp($input, '') == 0) {
+ $input = self::cleanSearch($input);
+ if ($input == '') {
return;
}
if (preg_match_all('/(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
$this->search = $matches['search'];
$input = str_replace($matches[0], '', $input);
}
- $input = $this->cleanSearch($input);
- if (strcmp($input, '') == 0) {
+ $input = self::cleanSearch($input);
+ if ($input == '') {
return;
}
if (is_array($this->search)) {
@@ -215,13 +305,33 @@ class FreshRSS_Search {
}
}
+ private function parseNotSearch($input) {
+ $input = self::cleanSearch($input);
+ if ($input == '') {
+ return;
+ }
+ if (preg_match_all('/[!-](?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+ $this->not_search = $matches['search'];
+ $input = str_replace($matches[0], '', $input);
+ }
+ if ($input == '') {
+ return;
+ }
+ if (preg_match_all('/[!-](?P<search>[^\s]+)/', $input, $matches)) {
+ $this->not_search = array_merge(is_array($this->not_search) ? $this->not_search : array(), $matches['search']);
+ $input = str_replace($matches[0], '', $input);
+ }
+ $this->not_search = self::removeEmptyValues($this->not_search);
+ return $input;
+ }
+
/**
* Remove all unnecessary spaces in the search
*
* @param string $input
* @return string
*/
- private function cleanSearch($input) {
+ private static function cleanSearch($input) {
$input = preg_replace('/\s+/', ' ', $input);
return trim($input);
}
diff --git a/app/Models/UserDAO.php b/app/Models/UserDAO.php
index a60caf395..310c7c096 100644
--- a/app/Models/UserDAO.php
+++ b/app/Models/UserDAO.php
@@ -14,21 +14,23 @@ 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, $bd_prefix_user, _t('gen.short.default_category'));
+ $sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP, $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;
if (is_array($SQL_CREATE_TABLES)) {
- $ok = true;
- foreach ($SQL_CREATE_TABLES as $instruction) {
+ $instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP);
+ $ok = !empty($instructions);
+ foreach ($instructions as $instruction) {
$sql = sprintf($instruction, $bd_prefix_user, _t('gen.short.default_category'));
$stm = $userPDO->bd->prepare($sql);
$ok &= ($stm && $stm->execute());
}
}
}
- if ($insertDefaultFeeds) {
+ if ($ok && $insertDefaultFeeds) {
if (defined('SQL_INSERT_FEEDS')) { //E.g. MySQL
$sql = sprintf(SQL_INSERT_FEEDS, $bd_prefix_user);
$stm = $userPDO->bd->prepare($sql);