diff options
Diffstat (limited to 'app/Models')
| -rw-r--r-- | app/Models/Auth.php | 27 | ||||
| -rw-r--r-- | app/Models/ConfigurationSetter.php | 4 | ||||
| -rw-r--r-- | app/Models/Entry.php | 8 | ||||
| -rw-r--r-- | app/Models/EntryDAO.php | 189 | ||||
| -rw-r--r-- | app/Models/EntryDAOPGSQL.php | 26 | ||||
| -rw-r--r-- | app/Models/EntryDAOSQLite.php | 39 | ||||
| -rw-r--r-- | app/Models/Factory.php | 9 | ||||
| -rw-r--r-- | app/Models/Feed.php | 21 | ||||
| -rw-r--r-- | app/Models/FeedDAO.php | 56 | ||||
| -rw-r--r-- | app/Models/FeedDAOSQLite.php | 19 | ||||
| -rw-r--r-- | app/Models/Search.php | 168 | ||||
| -rw-r--r-- | app/Models/UserDAO.php | 10 |
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('/:"(.*?)"/', ':"\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); |
