diff options
| author | 2014-09-26 14:36:30 +0200 | |
|---|---|---|
| committer | 2014-09-26 14:36:30 +0200 | |
| commit | 147a3d21bbe78ae66c7134ac0c355472a92e1159 (patch) | |
| tree | 7f8ca64ccc0939c8c9aef145a8fd86cc2e52640b /app/Models | |
| parent | 7e949d50320317b5c3b5a2da2bdaf324e794b2f7 (diff) | |
| parent | c14162221365077bcaeecde7127806190490dd58 (diff) | |
Merge branch 'dev'
Diffstat (limited to 'app/Models')
| -rw-r--r-- | app/Models/Category.php | 2 | ||||
| -rw-r--r-- | app/Models/CategoryDAO.php | 35 | ||||
| -rw-r--r-- | app/Models/Configuration.php | 152 | ||||
| -rw-r--r-- | app/Models/Entry.php | 10 | ||||
| -rw-r--r-- | app/Models/EntryDAO.php | 690 | ||||
| -rw-r--r-- | app/Models/EntryDAOSQLite.php | 129 | ||||
| -rw-r--r-- | app/Models/Factory.php | 32 | ||||
| -rw-r--r-- | app/Models/Feed.php | 217 | ||||
| -rw-r--r-- | app/Models/FeedDAO.php | 346 | ||||
| -rw-r--r-- | app/Models/FeedDAOSQLite.php | 19 | ||||
| -rw-r--r-- | app/Models/Share.php | 44 | ||||
| -rw-r--r-- | app/Models/StatsDAO.php | 281 | ||||
| -rw-r--r-- | app/Models/StatsDAOSQLite.php | 64 | ||||
| -rw-r--r-- | app/Models/Themes.php | 17 | ||||
| -rw-r--r-- | app/Models/UserDAO.php | 52 |
15 files changed, 1437 insertions, 653 deletions
diff --git a/app/Models/Category.php b/app/Models/Category.php index 328bae799..0a0dbd3ca 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -44,7 +44,7 @@ class FreshRSS_Category extends Minz_Model { } public function feeds () { if ($this->feeds === null) { - $feedDAO = new FreshRSS_FeedDAO (); + $feedDAO = FreshRSS_Factory::createFeedDao(); $this->feeds = $feedDAO->listByCategory ($this->id ()); $this->nbFeed = 0; $this->nbNotRead = 0; diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index 5355228a5..f11f87f47 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -12,12 +12,25 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if ($stm && $stm->execute ($values)) { return $this->bd->lastInsertId(); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error addCategory: ' . $info[2], Minz_Log::ERROR); return false; } } + public function addCategoryObject($category) { + $cat = $this->searchByName($category->name()); + if (!$cat) { + // Category does not exist yet in DB so we add it before continue + $values = array( + 'name' => $category->name(), + ); + return $this->addCategory($values); + } + + return $cat->id(); + } + public function updateCategory ($id, $valuesTmp) { $sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=?'; $stm = $this->bd->prepare ($sql); @@ -30,8 +43,8 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if ($stm && $stm->execute ($values)) { return $stm->rowCount(); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error updateCategory: ' . $info[2], Minz_Log::ERROR); return false; } } @@ -45,8 +58,8 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if ($stm && $stm->execute ($values)) { return $stm->rowCount(); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error deleteCategory: ' . $info[2], Minz_Log::ERROR); return false; } } @@ -64,7 +77,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if (isset ($cat[0])) { return $cat[0]; } else { - return false; + return null; } } public function searchByName ($name) { @@ -80,7 +93,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if (isset ($cat[0])) { return $cat[0]; } else { - return false; + return null; } } @@ -89,7 +102,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { $sql = 'SELECT c.id AS c_id, c.name AS c_name, ' . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.cache_nbEntries, f.cache_nbUnreads ') . 'FROM `' . $this->prefix . 'category` c ' - . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category = c.id ' + . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category=c.id ' . 'GROUP BY f.id ' . 'ORDER BY c.name, f.name'; $stm = $this->bd->prepare ($sql); @@ -120,7 +133,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { public function checkDefault () { $def_cat = $this->searchById (1); - if ($def_cat === false) { + if ($def_cat == null) { $cat = new FreshRSS_Category (Minz_Translate::t ('default_category')); $cat->_id (1); @@ -153,7 +166,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { } public function countNotRead ($id) { - $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id WHERE category=? AND e.is_read=0'; + $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE category=? AND e.is_read=0'; $stm = $this->bd->prepare ($sql); $values = array ($id); $stm->execute ($values); diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php index 2b719c370..95f819779 100644 --- a/app/Models/Configuration.php +++ b/app/Models/Configuration.php @@ -7,16 +7,22 @@ class FreshRSS_Configuration { 'language' => 'en', 'old_entries' => 3, 'keep_history_default' => 0, + 'ttl_default' => 3600, 'mail_login' => '', 'token' => '', 'passwordHash' => '', //CRYPT_BLOWFISH + 'apiPasswordHash' => '', //CRYPT_BLOWFISH 'posts_per_page' => 20, 'view_mode' => 'normal', - 'default_view' => 'not_read', + 'default_view' => FreshRSS_Entry::STATE_NOT_READ, 'auto_load_more' => true, 'display_posts' => false, + 'display_categories' => false, + 'hide_read_feeds' => true, 'onread_jump_next' => true, 'lazyload' => true, + 'sticky_post' => true, + 'reading_confirm' => false, 'sort_order' => 'DESC', 'anon_access' => false, 'mark_when' => array( @@ -26,6 +32,7 @@ class FreshRSS_Configuration { 'reception' => false, ), 'theme' => 'Origine', + 'content_width' => 'thin', 'shortcuts' => array( 'mark_read' => 'r', 'mark_favorite' => 'f', @@ -37,6 +44,9 @@ class FreshRSS_Configuration { 'collapse_entry' => 'c', 'load_more' => 'm', 'auto_share' => 's', + 'focus_search' => 'a', + 'user_filter' => 'u', + 'help' => 'f1', ), 'topline_read' => true, 'topline_favorite' => true, @@ -48,16 +58,9 @@ class FreshRSS_Configuration { 'bottomline_tags' => true, 'bottomline_date' => true, 'bottomline_link' => true, - 'sharing' => array( - 'shaarli' => '', - 'wallabag' => '', - 'diaspora' => '', - 'twitter' => true, - 'g+' => true, - 'facebook' => true, - 'email' => true, - 'print' => true, - ), + 'sharing' => array(), + 'queries' => array(), + 'html5_notif_timeout' => 0, ); private $available_languages = array( @@ -65,8 +68,10 @@ class FreshRSS_Configuration { 'fr' => 'Français', ); - public function __construct ($user) { - $this->filename = DATA_PATH . '/' . $user . '_user.php'; + private $shares; + + public function __construct($user) { + $this->filename = DATA_PATH . DIRECTORY_SEPARATOR . $user . '_user.php'; $data = @include($this->filename); if (!is_array($data)) { @@ -80,10 +85,20 @@ class FreshRSS_Configuration { } } $this->data['user'] = $user; + + $this->shares = DATA_PATH . DIRECTORY_SEPARATOR . 'shares.php'; + + $shares = @include($this->shares); + if (!is_array($shares)) { + throw new Minz_PermissionDeniedException($this->shares); + } + + $this->data['shares'] = $shares; } public function save() { @rename($this->filename, $this->filename . '.bak.php'); + unset($this->data['shares']); // Remove shares because it is not intended to be stored in user configuration if (file_put_contents($this->filename, "<?php\n return " . var_export($this->data, true) . ';', LOCK_EX) === false) { throw new Minz_PermissionDeniedException($this->filename); } @@ -104,20 +119,20 @@ class FreshRSS_Configuration { } } - public function sharing($key = false) { - if ($key === false) { - return $this->data['sharing']; - } - if (isset($this->data['sharing'][$key])) { - return $this->data['sharing'][$key]; - } - return false; - } - public function availableLanguages() { return $this->available_languages; } + public function remove_query_by_get($get) { + $final_queries = array(); + foreach ($this->queries as $key => $query) { + if (empty($query['get']) || $query['get'] !== $get) { + $final_queries[$key] = $query; + } + } + $this->_queries($final_queries); + } + public function _language($value) { if (!isset($this->available_languages[$value])) { $value = 'en'; @@ -136,17 +151,40 @@ class FreshRSS_Configuration { } } public function _default_view ($value) { - $this->data['default_view'] = $value === 'all' ? 'all' : 'not_read'; + switch ($value) { + case FreshRSS_Entry::STATE_ALL: + // left blank on purpose + case FreshRSS_Entry::STATE_NOT_READ: + // left blank on purpose + case FreshRSS_Entry::STATE_STRICT + FreshRSS_Entry::STATE_NOT_READ: + $this->data['default_view'] = $value; + break; + default: + $this->data['default_view'] = FreshRSS_Entry::STATE_ALL; + break; + } } public function _display_posts ($value) { $this->data['display_posts'] = ((bool)$value) && $value !== 'no'; } + public function _display_categories ($value) { + $this->data['display_categories'] = ((bool)$value) && $value !== 'no'; + } + public function _hide_read_feeds($value) { + $this->data['hide_read_feeds'] = (bool)$value; + } public function _onread_jump_next ($value) { $this->data['onread_jump_next'] = ((bool)$value) && $value !== 'no'; } public function _lazyload ($value) { $this->data['lazyload'] = ((bool)$value) && $value !== 'no'; } + public function _sticky_post($value) { + $this->data['sticky_post'] = ((bool)$value) && $value !== 'no'; + } + public function _reading_confirm($value) { + $this->data['reading_confirm'] = ((bool)$value) && $value !== 'no'; + } public function _sort_order ($value) { $this->data['sort_order'] = $value === 'ASC' ? 'ASC' : 'DESC'; } @@ -158,6 +196,10 @@ class FreshRSS_Configuration { $value = intval($value); $this->data['keep_history_default'] = $value >= -1 ? $value : 0; } + public function _ttl_default($value) { + $value = intval($value); + $this->data['ttl_default'] = $value >= -1 ? $value : 3600; + } public function _shortcuts ($values) { foreach ($values as $key => $value) { if (isset($this->data['shortcuts'][$key])) { @@ -168,6 +210,9 @@ class FreshRSS_Configuration { public function _passwordHash ($value) { $this->data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; } + public function _apiPasswordHash ($value) { + $this->data['apiPasswordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; + } public function _mail_login ($value) { $value = filter_var($value, FILTER_VALIDATE_EMAIL); if ($value) { @@ -187,29 +232,70 @@ class FreshRSS_Configuration { } } public function _sharing ($values) { - $are_url = array ('shaarli', 'wallabag', 'diaspora'); - foreach ($values as $key => $value) { - if (in_array($key, $are_url)) { + $this->data['sharing'] = array(); + $unique = array(); + foreach ($values as $value) { + if (!is_array($value)) { + continue; + } + + // Verify URL and add default value when needed + if (isset($value['url'])) { $is_url = ( - filter_var ($value, FILTER_VALIDATE_URL) || + filter_var ($value['url'], FILTER_VALIDATE_URL) || (version_compare(PHP_VERSION, '5.3.3', '<') && (strpos($value, '-') > 0) && ($value === filter_var($value, FILTER_SANITIZE_URL))) ); //PHP bug #51192 - if (!$is_url) { - $value = ''; + continue; } - } elseif (!is_bool($value)) { - $value = true; + } else { + $value['url'] = null; + } + + // Add a default name + if (empty($value['name'])) { + $value['name'] = $value['type']; } - $this->data['sharing'][$key] = $value; + $json_value = json_encode($value); + if (!in_array($json_value, $unique)) { + $unique[] = $json_value; + $this->data['sharing'][] = $value; + } + } + } + public function _queries ($values) { + $this->data['queries'] = array(); + foreach ($values as $value) { + $value = array_filter($value); + $params = $value; + unset($params['name']); + unset($params['url']); + $value['url'] = Minz_Url::display(array('params' => $params)); + + $this->data['queries'][] = $value; } } public function _theme($value) { $this->data['theme'] = $value; } + public function _content_width($value) { + if ($value === 'medium' || + $value === 'large' || + $value === 'no_limit') { + $this->data['content_width'] = $value; + } else { + $this->data['content_width'] = 'thin'; + } + } + + public function _html5_notif_timeout ($value) { + $value = intval($value); + $this->data['html5_notif_timeout'] = $value >= 0 ? $value : 0; + } + public function _token($value) { $this->data['token'] = $value; } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index a6c67221b..9d7dd5dc4 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -1,6 +1,12 @@ <?php class FreshRSS_Entry extends Minz_Model { + const STATE_ALL = 0; + const STATE_READ = 1; + const STATE_NOT_READ = 2; + const STATE_FAVORITE = 4; + const STATE_NOT_FAVORITE = 8; + const STATE_STRICT = 16; private $id = 0; private $guid; @@ -69,7 +75,7 @@ class FreshRSS_Entry extends Minz_Model { } public function feed ($object = false) { if ($object) { - $feedDAO = new FreshRSS_FeedDAO (); + $feedDAO = FreshRSS_Factory::createFeedDao(); return $feedDAO->searchById ($this->feed); } else { return $this->feed; @@ -149,7 +155,7 @@ class FreshRSS_Entry extends Minz_Model { // Gestion du contenu // On cherche à récupérer les articles en entier... même si le flux ne le propose pas if ($pathEntries) { - $entryDAO = new FreshRSS_EntryDAO(); + $entryDAO = FreshRSS_Factory::createEntryDao(); $entry = $entryDAO->searchByGuid($this->feed, $this->guid); if($entry) { diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index aaf4dcf6a..c1f87ee34 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -1,12 +1,27 @@ <?php class FreshRSS_EntryDAO extends Minz_ModelPdo { - public function addEntry ($valuesTmp) { - $sql = 'INSERT INTO `' . $this->prefix . 'entry`(id, guid, title, author, content_bin, link, date, is_read, is_favorite, id_feed, tags) ' - . 'VALUES(?, ?, ?, ?, COMPRESS(?), ?, ?, ?, ?, ?, ?)'; - $stm = $this->bd->prepare ($sql); - $values = array ( + public function isCompressed() { + return parent::$sharedDbType !== 'sqlite'; + } + + public function addEntryPrepare() { + $sql = 'INSERT INTO `' . $this->prefix . 'entry`(id, guid, title, author, ' + . ($this->isCompressed() ? 'content_bin' : 'content') + . ', link, date, is_read, is_favorite, id_feed, tags) ' + . 'VALUES(?, ?, ?, ?, ' + . ($this->isCompressed() ? 'COMPRESS(?)' : '?') + . ', ?, ?, ?, ?, ?, ?)'; + return $this->bd->prepare($sql); + } + + public function addEntry($valuesTmp, $preparedStatement = null) { + $stm = $preparedStatement === null ? + FreshRSS_EntryDAO::addEntryPrepare() : + $preparedStatement; + + $values = array( $valuesTmp['id'], substr($valuesTmp['guid'], 0, 760), substr($valuesTmp['title'], 0, 255), @@ -20,12 +35,12 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { substr($valuesTmp['tags'], 0, 1023), ); - if ($stm && $stm->execute ($values)) { + if ($stm && $stm->execute($values)) { return $this->bd->lastInsertId(); } else { - $info = $stm->errorInfo(); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); if ((int)($info[0] / 1000) !== 23) { //Filter out "SQLSTATE Class code 23: Constraint Violation" because of expected duplicate entries - Minz_Log::record ('SQL error ' . $info[0] . ': ' . $info[1] . ' ' . $info[2] + Minz_Log::record('SQL error addEntry: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2] . ' while adding entry in feed ' . $valuesTmp['id_feed'] . ' with title: ' . $valuesTmp['title'], Minz_Log::ERROR); } /*else { Minz_Log::record ('SQL error ' . $info[0] . ': ' . $info[1] . ' ' . $info[2] @@ -35,232 +50,259 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { } } - public function markFavorite ($id, $is_favorite = true) { - $sql = 'UPDATE `' . $this->prefix . 'entry` e ' - . 'SET e.is_favorite = ? ' - . 'WHERE e.id=?'; - $values = array ($is_favorite ? 1 : 0, $id); - $stm = $this->bd->prepare ($sql); - if ($stm && $stm->execute ($values)) { + public function addEntryObject($entry, $conf, $feedHistory) { + $existingGuids = array_fill_keys( + $this->listLastGuidsByFeed($entry->feed(), 20), 1 + ); + + $nb_month_old = max($conf->old_entries, 1); + $date_min = time() - (3600 * 24 * 30 * $nb_month_old); + + $eDate = $entry->date(true); + + if ($feedHistory == -2) { + $feedHistory = $conf->keep_history_default; + } + + if (!isset($existingGuids[$entry->guid()]) && + ($feedHistory != 0 || $eDate >= $date_min || $entry->isFavorite())) { + $values = $entry->toArray(); + + $useDeclaredDate = empty($existingGuids); + $values['id'] = ($useDeclaredDate || $eDate < $date_min) ? + min(time(), $eDate) . uSecString() : + uTimeString(); + + return $this->addEntry($values); + } + + // We don't return Entry object to avoid a research in DB + return -1; + } + + public function markFavorite($ids, $is_favorite = true) { + if (!is_array($ids)) { + $ids = array($ids); + } + $sql = 'UPDATE `' . $this->prefix . 'entry` ' + . 'SET is_favorite=? ' + . 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)'; + $values = array($is_favorite ? 1 : 0); + $values = array_merge($values, $ids); + $stm = $this->bd->prepare($sql); + if ($stm && $stm->execute($values)) { return $stm->rowCount(); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error markFavorite: ' . $info[2], Minz_Log::ERROR); return false; } } - public function markRead ($id, $is_read = true) { - $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' - . 'SET e.is_read = ?,' - . 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 ' - . 'WHERE e.id=?'; - $values = array ($is_read ? 1 : 0, $id); - $stm = $this->bd->prepare ($sql); - if ($stm && $stm->execute ($values)) { - return $stm->rowCount(); + + protected function updateCacheUnreads($catId = false, $feedId = false) { + $sql = 'UPDATE `' . $this->prefix . 'feed` f ' + . 'LEFT OUTER JOIN (' + . 'SELECT e.id_feed, ' + . 'COUNT(*) AS nbUnreads ' + . 'FROM `' . $this->prefix . 'entry` e ' + . 'WHERE e.is_read=0 ' + . 'GROUP BY e.id_feed' + . ') x ON x.id_feed=f.id ' + . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) ' + . 'WHERE 1'; + $values = array(); + if ($feedId !== false) { + $sql .= ' AND f.id=?'; + $values[] = $id; + } + if ($catId !== false) { + $sql .= ' AND f.category=?'; + $values[] = $catId; + } + $stm = $this->bd->prepare($sql); + if ($stm && $stm->execute($values)) { + return true; } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error updateCacheUnreads: ' . $info[2], Minz_Log::ERROR); return false; } } - public function markReadEntries ($idMax = 0, $favorites = false) { - if ($idMax === 0) { - $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' - . 'SET e.is_read = 1, f.cache_nbUnreads=0 ' - . 'WHERE e.is_read = 0 AND '; - if ($favorites) { - $sql .= 'e.is_favorite = 1'; - } else { - $sql .= 'f.priority > 0'; - } - $stm = $this->bd->prepare ($sql); - if ($stm && $stm->execute ()) { - return $stm->rowCount(); - } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - return false; - } - } else { - $this->bd->beginTransaction (); - $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' - . 'SET e.is_read = 1 ' - . 'WHERE e.is_read = 0 AND e.id <= ? AND '; - if ($favorites) { - $sql .= 'e.is_favorite = 1'; - } else { - $sql .= 'f.priority > 0'; + public function markRead($ids, $is_read = true) { + if (is_array($ids)) { //Many IDs at once (used by API) + if (count($ids) < 6) { //Speed heuristics + $affected = 0; + foreach ($ids as $id) { + $affected += $this->markRead($id, $is_read); + } + return $affected; } - $values = array ($idMax); - $stm = $this->bd->prepare ($sql); - if (!($stm && $stm->execute ($values))) { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - $this->bd->rollBack (); + + $sql = 'UPDATE `' . $this->prefix . 'entry` ' + . 'SET is_read=? ' + . 'WHERE id IN (' . str_repeat('?,', count($ids) - 1). '?)'; + $values = array($is_read ? 1 : 0); + $values = array_merge($values, $ids); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error markRead: ' . $info[2], Minz_Log::ERROR); return false; } $affected = $stm->rowCount(); - - if ($affected > 0) { - $sql = 'UPDATE `' . $this->prefix . 'feed` f ' - . 'LEFT OUTER JOIN (' - . 'SELECT e.id_feed, ' - . 'COUNT(*) AS nbUnreads ' - . 'FROM `' . $this->prefix . 'entry` e ' - . 'WHERE e.is_read = 0 ' - . 'GROUP BY e.id_feed' - . ') x ON x.id_feed=f.id ' - . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0)'; - $stm = $this->bd->prepare ($sql); - if (!($stm && $stm->execute ())) { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - $this->bd->rollBack (); - return false; - } + if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) { + return false; } - - $this->bd->commit (); return $affected; - } - } - public function markReadCat ($id, $idMax = 0) { - if ($idMax === 0) { - $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' - . 'SET e.is_read = 1, f.cache_nbUnreads=0 ' - . 'WHERE f.category = ? AND e.is_read = 0'; - $values = array ($id); - $stm = $this->bd->prepare ($sql); - if ($stm && $stm->execute ($values)) { + } else { + $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id ' + . 'SET e.is_read=?,' + . 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 ' + . 'WHERE e.id=? AND e.is_read=?'; + $values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1); + $stm = $this->bd->prepare($sql); + if ($stm && $stm->execute($values)) { return $stm->rowCount(); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error markRead: ' . $info[2], Minz_Log::ERROR); return false; } - } else { - $this->bd->beginTransaction (); - - $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' - . 'SET e.is_read = 1 ' - . 'WHERE f.category = ? AND e.is_read = 0 AND e.id <= ?'; - $values = array ($id, $idMax); - $stm = $this->bd->prepare ($sql); - if (!($stm && $stm->execute ($values))) { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - $this->bd->rollBack (); - return false; - } - $affected = $stm->rowCount(); + } + } - if ($affected > 0) { - $sql = 'UPDATE `' . $this->prefix . 'feed` f ' - . 'LEFT OUTER JOIN (' - . 'SELECT e.id_feed, ' - . 'COUNT(*) AS nbUnreads ' - . 'FROM `' . $this->prefix . 'entry` e ' - . 'WHERE e.is_read = 0 ' - . 'GROUP BY e.id_feed' - . ') x ON x.id_feed=f.id ' - . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) ' - . 'WHERE f.category = ?'; - $values = array ($id); - $stm = $this->bd->prepare ($sql); - if (!($stm && $stm->execute ($values))) { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - $this->bd->rollBack (); - return false; - } - } + public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) { + if ($idMax == 0) { + $idMax = time() . '000000'; + Minz_Log::record('Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG); + } - $this->bd->commit (); - return $affected; + $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id ' + . 'SET e.is_read=1 ' + . 'WHERE e.is_read=0 AND e.id <= ?'; + if ($onlyFavorites) { + $sql .= ' AND e.is_favorite=1'; + } elseif ($priorityMin >= 0) { + $sql .= ' AND f.priority > ' . intval($priorityMin); + } + $values = array($idMax); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error markReadEntries: ' . $info[2], Minz_Log::ERROR); + return false; } + $affected = $stm->rowCount(); + if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) { + return false; + } + return $affected; } - public function markReadFeed ($id, $idMax = 0) { - if ($idMax === 0) { - $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' - . 'SET e.is_read = 1, f.cache_nbUnreads=0 ' - . 'WHERE f.id=? AND e.is_read = 0'; - $values = array ($id); - $stm = $this->bd->prepare ($sql); - if ($stm && $stm->execute ($values)) { - return $stm->rowCount(); - } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - return false; - } - } else { - $this->bd->beginTransaction (); - - $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' - . 'SET e.is_read = 1 ' - . 'WHERE f.id=? AND e.is_read = 0 AND e.id <= ?'; - $values = array ($id, $idMax); - $stm = $this->bd->prepare ($sql); - if (!($stm && $stm->execute ($values))) { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - $this->bd->rollBack (); - return false; - } - $affected = $stm->rowCount(); - if ($affected > 0) { - $sql = 'UPDATE `' . $this->prefix . 'feed` f ' - . 'SET f.cache_nbUnreads=f.cache_nbUnreads-' . $affected - . ' WHERE f.id=?'; - $values = array ($id); - $stm = $this->bd->prepare ($sql); - if (!($stm && $stm->execute ($values))) { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - $this->bd->rollBack (); - return false; - } - } + public function markReadCat($id, $idMax = 0) { + if ($idMax == 0) { + $idMax = time() . '000000'; + Minz_Log::record('Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG); + } - $this->bd->commit (); - return $affected; + $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id ' + . 'SET e.is_read=1 ' + . 'WHERE f.category=? AND e.is_read=0 AND e.id <= ?'; + $values = array($id, $idMax); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error markReadCat: ' . $info[2], Minz_Log::ERROR); + return false; + } + $affected = $stm->rowCount(); + if (($affected > 0) && (!$this->updateCacheUnreads($id, false))) { + return false; + } + return $affected; + } + + public function markReadFeed($id, $idMax = 0) { + if ($idMax == 0) { + $idMax = time() . '000000'; + Minz_Log::record('Calling markReadFeed(0) is deprecated!', Minz_Log::DEBUG); + } + $this->bd->beginTransaction(); + + $sql = 'UPDATE `' . $this->prefix . 'entry` ' + . 'SET is_read=1 ' + . 'WHERE id_feed=? AND is_read=0 AND id <= ?'; + $values = array($id, $idMax); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error markReadFeed: ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); + return false; + } + $affected = $stm->rowCount(); + + if ($affected > 0) { + $sql = 'UPDATE `' . $this->prefix . 'feed` ' + . 'SET cache_nbUnreads=cache_nbUnreads-' . $affected + . ' WHERE id=?'; + $values = array($id); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error markReadFeed: ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); + return false; + } } + + $this->bd->commit(); + return $affected; } - public function searchByGuid ($feed_id, $id) { + public function searchByGuid($feed_id, $id) { // un guid est unique pour un flux donné - $sql = 'SELECT id, guid, title, author, UNCOMPRESS(content_bin) AS content, link, date, is_read, is_favorite, id_feed, tags ' + $sql = 'SELECT id, guid, title, author, ' + . ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content') + . ', link, date, is_read, is_favorite, id_feed, tags ' . 'FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid=?'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); - $values = array ( + $values = array( $feed_id, $id ); - $stm->execute ($values); - $res = $stm->fetchAll (PDO::FETCH_ASSOC); - $entries = self::daoToEntry ($res); - return isset ($entries[0]) ? $entries[0] : false; + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + $entries = self::daoToEntry($res); + return isset($entries[0]) ? $entries[0] : null; } - public function searchById ($id) { - $sql = 'SELECT id, guid, title, author, UNCOMPRESS(content_bin) AS content, link, date, is_read, is_favorite, id_feed, tags ' + public function searchById($id) { + $sql = 'SELECT id, guid, title, author, ' + . ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content') + . ', link, date, is_read, is_favorite, id_feed, tags ' . 'FROM `' . $this->prefix . 'entry` WHERE id=?'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); + + $values = array($id); - $values = array ($id); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + $entries = self::daoToEntry($res); + return isset($entries[0]) ? $entries[0] : null; + } - $stm->execute ($values); - $res = $stm->fetchAll (PDO::FETCH_ASSOC); - $entries = self::daoToEntry ($res); - return isset ($entries[0]) ? $entries[0] : false; + protected function sqlConcat($s1, $s2) { + return 'CONCAT(' . $s1 . ',' . $s2 . ')'; //MySQL } - public function listWhere($type = 'a', $id = '', $state = 'all', $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $keepHistoryDefault = 0) { + private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) { + if (!$state) { + $state = FreshRSS_Entry::STATE_ALL; + } $where = ''; $joinFeed = false; $values = array(); @@ -269,185 +311,237 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $where .= 'f.priority > 0 '; $joinFeed = true; break; - case 's': - $where .= 'e1.is_favorite = 1 '; + case 's': //Deprecated: use $state instead + $where .= 'e1.is_favorite=1 '; break; case 'c': - $where .= 'f.category = ? '; + $where .= 'f.category=? '; $values[] = intval($id); $joinFeed = true; break; case 'f': - $where .= 'e1.id_feed = ? '; + $where .= 'e1.id_feed=? '; $values[] = intval($id); break; - default: - throw new FreshRSS_EntriesGetter_Exception ('Bad type in Entry->listByType: [' . $type . ']!'); - } - switch ($state) { - case 'all': - break; - case 'not_read': - $where .= 'AND e1.is_read = 0 '; - break; - case 'read': - $where .= 'AND e1.is_read = 1 '; - break; - case 'favorite': - $where .= 'AND e1.is_favorite = 1 '; + case 'A': + $where .= '1 '; break; default: - throw new FreshRSS_EntriesGetter_Exception ('Bad state in Entry->listByType: [' . $state . ']!'); + throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!'); + } + + if ($state & FreshRSS_Entry::STATE_NOT_READ) { + if (!($state & FreshRSS_Entry::STATE_READ)) { + $where .= 'AND e1.is_read=0 '; + } elseif ($state & FreshRSS_Entry::STATE_STRICT) { + $where .= 'AND e1.is_read=0 '; + } + } + elseif ($state & FreshRSS_Entry::STATE_READ) { + $where .= 'AND e1.is_read=1 '; } + if ($state & FreshRSS_Entry::STATE_FAVORITE) { + if (!($state & FreshRSS_Entry::STATE_NOT_FAVORITE)) { + $where .= 'AND e1.is_favorite=1 '; + } + } + elseif ($state & FreshRSS_Entry::STATE_NOT_FAVORITE) { + $where .= 'AND e1.is_favorite=0 '; + } + switch ($order) { case 'DESC': case 'ASC': break; default: - throw new FreshRSS_EntriesGetter_Exception ('Bad order in Entry->listByType: [' . $order . ']!'); + throw new FreshRSS_EntriesGetter_Exception('Bad order in Entry->listByType: [' . $order . ']!'); + } + if ($firstId === '' && parent::$sharedDbType === 'mysql') { + $firstId = $order === 'DESC' ? '9000000000'. '000000' : '0'; //MySQL optimization. Tested on MySQL 5.5 with 150k articles } if ($firstId !== '') { $where .= 'AND e1.id ' . ($order === 'DESC' ? '<=' : '>=') . $firstId . ' '; } if (($date_min > 0) && ($type !== 's')) { - $where .= 'AND (e1.id >= ' . $date_min . '000000 OR e1.is_read = 0 OR e1.is_favorite = 1 OR (f.keep_history <> 0'; - if (intval($keepHistoryDefault) === 0) { - $where .= ' AND f.keep_history <> -2'; //default + $where .= 'AND (e1.id >= ' . $date_min . '000000'; + if ($showOlderUnreadsorFavorites) { //Lax date constraint + $where .= ' OR e1.is_read=0 OR e1.is_favorite=1 OR (f.keep_history <> 0'; + if (intval($keepHistoryDefault) === 0) { + $where .= ' AND f.keep_history <> -2'; //default + } + $where .= ')'; } - $where .= ')) '; + $where .= ') '; $joinFeed = true; } $search = ''; if ($filter !== '') { + require_once(LIB_PATH . '/lib_date.php'); $filter = trim($filter); $filter = addcslashes($filter, '\\%_'); - if (stripos($filter, 'intitle:') === 0) { - $filter = substr($filter, strlen('intitle:')); - $intitle = true; - } else { - $intitle = false; - } - if (stripos($filter, 'inurl:') === 0) { - $filter = substr($filter, strlen('inurl:')); - $inurl = true; - } else { - $inurl = false; - } - if (stripos($filter, 'author:') === 0) { - $filter = substr($filter, strlen('author:')); - $author = true; - } else { - $author = false; - } $terms = array_unique(explode(' ', $filter)); - sort($terms); //Put #tags first + //sort($terms); //Put #tags first //TODO: Put the cheapest filters first foreach ($terms as $word) { $word = trim($word); - if (strlen($word) > 0) { - if ($intitle) { - $search .= 'AND e1.title LIKE ? '; - $values[] = '%' . $word .'%'; - } elseif ($inurl) { - $search .= 'AND CONCAT(e1.link, e1.guid) LIKE ? '; - $values[] = '%' . $word .'%'; - } elseif ($author) { - $search .= 'AND e1.author LIKE ? '; + if (stripos($word, 'intitle:') === 0) { + $word = substr($word, strlen('intitle:')); + $search .= 'AND e1.title LIKE ? '; + $values[] = '%' . $word .'%'; + } elseif (stripos($word, 'inurl:') === 0) { + $word = substr($word, strlen('inurl:')); + $search .= 'AND CONCAT(e1.link, e1.guid) LIKE ? '; + $values[] = '%' . $word .'%'; + } elseif (stripos($word, 'author:') === 0) { + $word = substr($word, strlen('author:')); + $search .= 'AND e1.author LIKE ? '; + $values[] = '%' . $word .'%'; + } elseif (stripos($word, 'date:') === 0) { + $word = substr($word, strlen('date:')); + list($minDate, $maxDate) = parseDateInterval($word); + if ($minDate) { + $search .= 'AND e1.id >= ' . $minDate . '000000 '; + } + if ($maxDate) { + $search .= 'AND e1.id <= ' . $maxDate . '000000 '; + } + } elseif (stripos($word, 'pubdate:') === 0) { + $word = substr($word, strlen('pubdate:')); + list($minDate, $maxDate) = parseDateInterval($word); + if ($minDate) { + $search .= 'AND e1.date >= ' . $minDate . ' '; + } + if ($maxDate) { + $search .= 'AND e1.date <= ' . $maxDate . ' '; + } + } else { + if ($word[0] === '#' && isset($word[1])) { + $search .= 'AND e1.tags LIKE ? '; $values[] = '%' . $word .'%'; } else { - if ($word[0] === '#' && isset($word[1])) { - $search .= 'AND e1.tags LIKE ? '; - $values[] = '%' . $word .'%'; - } else { - $search .= 'AND CONCAT(e1.title, UNCOMPRESS(e1.content_bin)) LIKE ? '; - $values[] = '%' . $word .'%'; - } + $search .= 'AND ' . $this->sqlconcat('e1.title', $this->isCompressed() ? 'UNCOMPRESS(content_bin)' : 'content') . ' LIKE ? '; + $values[] = '%' . $word .'%'; } } } } - $sql = 'SELECT e.id, e.guid, e.title, e.author, UNCOMPRESS(e.content_bin) AS content, e.link, e.date, e.is_read, e.is_favorite, e.id_feed, e.tags ' + return array($values, + 'SELECT e1.id FROM `' . $this->prefix . 'entry` e1 ' + . ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e1.id_feed=f.id ' : '') + . 'WHERE ' . $where + . $search + . 'ORDER BY e1.id ' . $order + . ($limit > 0 ? ' LIMIT ' . $limit : '')); //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ + } + + public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) { + list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min, $showOlderUnreadsorFavorites, $keepHistoryDefault); + + $sql = 'SELECT e.id, e.guid, e.title, e.author, ' + . ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content') + . ', e.link, e.date, e.is_read, e.is_favorite, e.id_feed, e.tags ' . 'FROM `' . $this->prefix . 'entry` e ' - . 'INNER JOIN (SELECT e1.id FROM `' . $this->prefix . 'entry` e1 ' - . ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e1.id_feed = f.id ' : '') - . 'WHERE ' . $where - . $search - . 'ORDER BY e1.id ' . $order - . ($limit > 0 ? ' LIMIT ' . $limit : '') //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ - . ') e2 ON e2.id = e.id ' + . 'INNER JOIN (' + . $sql + . ') e2 ON e2.id=e.id ' . 'ORDER BY e.id ' . $order; - $stm = $this->bd->prepare ($sql); - $stm->execute ($values); + $stm = $this->bd->prepare($sql); + $stm->execute($values); - return self::daoToEntry ($stm->fetchAll (PDO::FETCH_ASSOC)); + return self::daoToEntry($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, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) { //For API + list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min, $showOlderUnreadsorFavorites, $keepHistoryDefault); + + $stm = $this->bd->prepare($sql); + $stm->execute($values); + + return $stm->fetchAll(PDO::FETCH_COLUMN, 0); } public function listLastGuidsByFeed($id, $n) { $sql = 'SELECT guid FROM `' . $this->prefix . 'entry` WHERE id_feed=? ORDER BY id DESC LIMIT ' . intval($n); - $stm = $this->bd->prepare ($sql); - $values = array ($id); - $stm->execute ($values); - return $stm->fetchAll (PDO::FETCH_COLUMN, 0); + $stm = $this->bd->prepare($sql); + $values = array($id); + $stm->execute($values); + return $stm->fetchAll(PDO::FETCH_COLUMN, 0); } - public function countUnreadRead () { - $sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id WHERE priority > 0' - . ' UNION SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id WHERE priority > 0 AND is_read = 0'; - $stm = $this->bd->prepare ($sql); - $stm->execute (); - $res = $stm->fetchAll (PDO::FETCH_COLUMN, 0); + public function countUnreadRead() { + $sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE priority > 0' + . ' UNION SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE priority > 0 AND is_read=0'; + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetchAll(PDO::FETCH_COLUMN, 0); $all = empty($res[0]) ? 0 : $res[0]; $unread = empty($res[1]) ? 0 : $res[1]; return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread); } - public function count ($minPriority = null) { - $sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id'; + public function count($minPriority = null) { + $sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id'; if ($minPriority !== null) { $sql = ' WHERE priority > ' . intval($minPriority); } - $stm = $this->bd->prepare ($sql); - $stm->execute (); - $res = $stm->fetchAll (PDO::FETCH_COLUMN, 0); + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetchAll(PDO::FETCH_COLUMN, 0); return $res[0]; } - public function countNotRead ($minPriority = null) { - $sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id WHERE is_read = 0'; + public function countNotRead($minPriority = null) { + $sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id WHERE is_read=0'; if ($minPriority !== null) { $sql = ' AND priority > ' . intval($minPriority); } - $stm = $this->bd->prepare ($sql); - $stm->execute (); - $res = $stm->fetchAll (PDO::FETCH_COLUMN, 0); + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetchAll(PDO::FETCH_COLUMN, 0); return $res[0]; } - public function countUnreadReadFavorites () { - $sql = 'SELECT COUNT(id) FROM `' . $this->prefix . 'entry` WHERE is_favorite=1' - . ' UNION SELECT COUNT(id) FROM `' . $this->prefix . 'entry` WHERE is_favorite=1 AND is_read = 0'; - $stm = $this->bd->prepare ($sql); - $stm->execute (); - $res = $stm->fetchAll (PDO::FETCH_COLUMN, 0); + public function countUnreadReadFavorites() { + $sql = 'SELECT c FROM (' + . 'SELECT COUNT(id) AS c, 1 as o FROM `' . $this->prefix . 'entry` WHERE is_favorite=1 ' + . 'UNION SELECT COUNT(id) AS c, 2 AS o FROM `' . $this->prefix . 'entry` WHERE is_favorite=1 AND is_read=0' + . ') u ORDER BY o'; + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetchAll(PDO::FETCH_COLUMN, 0); $all = empty($res[0]) ? 0 : $res[0]; $unread = empty($res[1]) ? 0 : $res[1]; return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread); } public function optimizeTable() { - $sql = 'OPTIMIZE TABLE `' . $this->prefix . 'entry`'; - $stm = $this->bd->prepare ($sql); - $stm->execute (); + $sql = 'OPTIMIZE TABLE `' . $this->prefix . 'entry`'; //MySQL + $stm = $this->bd->prepare($sql); + $stm->execute(); + } + + public function size($all = false) { + $db = Minz_Configuration::dataBase(); + $sql = 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema=?'; //MySQL + $values = array($db['base']); + if (!$all) { + $sql .= ' AND table_name LIKE ?'; + $values[] = $this->prefix . '%'; + } + $stm = $this->bd->prepare($sql); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_COLUMN, 0); + return $res[0]; } - public static function daoToEntry ($listDAO) { - $list = array (); + public static function daoToEntry($listDAO) { + $list = array(); - if (!is_array ($listDAO)) { - $listDAO = array ($listDAO); + if (!is_array($listDAO)) { + $listDAO = array($listDAO); } foreach ($listDAO as $key => $dao) { - $entry = new FreshRSS_Entry ( + $entry = new FreshRSS_Entry( $dao['id_feed'], $dao['guid'], $dao['title'], @@ -459,13 +553,13 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $dao['is_favorite'], $dao['tags'] ); - if (isset ($dao['id'])) { - $entry->_id ($dao['id']); + if (isset($dao['id'])) { + $entry->_id($dao['id']); } $list[] = $entry; } - unset ($listDAO); + unset($listDAO); return $list; } diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php new file mode 100644 index 000000000..9dc395c3c --- /dev/null +++ b/app/Models/EntryDAOSQLite.php @@ -0,0 +1,129 @@ +<?php + +class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { + + protected function sqlConcat($s1, $s2) { + return $s1 . '||' . $s2; + } + + protected function updateCacheUnreads($catId = false, $feedId = false) { + $sql = 'UPDATE `' . $this->prefix . 'feed` ' + . 'SET cache_nbUnreads=(' + . 'SELECT COUNT(*) AS nbUnreads FROM `' . $this->prefix . 'entry` e ' + . 'WHERE e.id_feed=`' . $this->prefix . 'feed`.id AND e.is_read=0) ' + . 'WHERE 1'; + $values = array(); + if ($feedId !== false) { + $sql .= ' AND id=?'; + $values[] = $feedId; + } + if ($catId !== false) { + $sql .= ' AND category=?'; + $values[] = $catId; + } + $stm = $this->bd->prepare($sql); + if ($stm && $stm->execute($values)) { + return true; + } else { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error updateCacheUnreads: ' . $info[2], Minz_Log::ERROR); + return false; + } + } + + public function markRead($ids, $is_read = true) { + if (is_array($ids)) { //Many IDs at once (used by API) + if (true) { //Speed heuristics //TODO: Not implemented yet for SQLite (so always call IDs one by one) + $affected = 0; + foreach ($ids as $id) { + $affected += $this->markRead($id, $is_read); + } + return $affected; + } + } else { + $this->bd->beginTransaction(); + $sql = 'UPDATE `' . $this->prefix . 'entry` SET is_read=? WHERE id=? AND is_read=?'; + $values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error markRead 1: ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); + return false; + } + $affected = $stm->rowCount(); + if ($affected > 0) { + $sql = 'UPDATE `' . $this->prefix . 'feed` SET cache_nbUnreads=cache_nbUnreads' . ($is_read ? '-' : '+') . '1 ' + . 'WHERE id=(SELECT e.id_feed FROM `' . $this->prefix . 'entry` e WHERE e.id=?)'; + $values = array($ids); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error markRead 2: ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); + return false; + } + } + $this->bd->commit(); + return $affected; + } + } + + public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0) { + if ($idMax == 0) { + $idMax = time() . '000000'; + Minz_Log::record('Calling markReadEntries(0) is deprecated!', Minz_Log::DEBUG); + } + + $sql = 'UPDATE `' . $this->prefix . 'entry` SET is_read=1 WHERE is_read=0 AND id <= ?'; + if ($onlyFavorites) { + $sql .= ' AND is_favorite=1'; + } elseif ($priorityMin >= 0) { + $sql .= ' AND id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.priority > ' . intval($priorityMin) . ')'; + } + $values = array($idMax); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error markReadEntries: ' . $info[2], Minz_Log::ERROR); + return false; + } + $affected = $stm->rowCount(); + if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) { + return false; + } + return $affected; + } + + public function markReadCat($id, $idMax = 0) { + if ($idMax == 0) { + $idMax = time() . '000000'; + Minz_Log::record('Calling markReadCat(0) is deprecated!', Minz_Log::DEBUG); + } + + $sql = 'UPDATE `' . $this->prefix . 'entry` ' + . 'SET is_read=1 ' + . 'WHERE is_read=0 AND id <= ? AND ' + . 'id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.category=?)'; + $values = array($idMax, $id); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error markReadCat: ' . $info[2], Minz_Log::ERROR); + return false; + } + $affected = $stm->rowCount(); + if (($affected > 0) && (!$this->updateCacheUnreads($id, false))) { + return false; + } + return $affected; + } + + public function optimizeTable() { + //TODO: Search for an equivalent in SQLite + } + + public function size($all = false) { + return @filesize(DATA_PATH . '/' . Minz_Session::param('currentUser', '_') . '.sqlite'); + } +} diff --git a/app/Models/Factory.php b/app/Models/Factory.php new file mode 100644 index 000000000..08569b2e2 --- /dev/null +++ b/app/Models/Factory.php @@ -0,0 +1,32 @@ +<?php + +class FreshRSS_Factory { + + public static function createFeedDao() { + $db = Minz_Configuration::dataBase(); + if ($db['type'] === 'sqlite') { + return new FreshRSS_FeedDAOSQLite(); + } else { + return new FreshRSS_FeedDAO(); + } + } + + public static function createEntryDao() { + $db = Minz_Configuration::dataBase(); + if ($db['type'] === 'sqlite') { + return new FreshRSS_EntryDAOSQLite(); + } else { + return new FreshRSS_EntryDAO(); + } + } + + public static function createStatsDAO() { + $db = Minz_Configuration::dataBase(); + if ($db['type'] === 'sqlite') { + return new FreshRSS_StatsDAOSQLite(); + } else { + return new FreshRSS_StatsDAO(); + } + } + +} diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 73f9c32fb..2a5ea45ac 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -16,17 +16,25 @@ class FreshRSS_Feed extends Minz_Model { private $httpAuth = ''; private $error = false; private $keep_history = -2; + private $ttl = -2; private $hash = null; + private $lockPath = ''; - public function __construct ($url, $validate=true) { + public function __construct($url, $validate=true) { if ($validate) { - $this->_url ($url); + $this->_url($url); } else { $this->url = $url; } } - public function id () { + public static function example() { + $f = new FreshRSS_Feed('http://example.net/', false); + $f->faviconPrepare(); + return $f; + } + + public function id() { return $this->id; } @@ -37,74 +45,77 @@ class FreshRSS_Feed extends Minz_Model { return $this->hash; } - public function url () { + public function url() { return $this->url; } - public function category () { + public function category() { return $this->category; } - public function entries () { + public function entries() { return $this->entries === null ? array() : $this->entries; } - public function name () { + public function name() { return $this->name; } - public function website () { + public function website() { return $this->website; } - public function description () { + public function description() { return $this->description; } - public function lastUpdate () { + public function lastUpdate() { return $this->lastUpdate; } - public function priority () { + public function priority() { return $this->priority; } - public function pathEntries () { + public function pathEntries() { return $this->pathEntries; } - public function httpAuth ($raw = true) { + public function httpAuth($raw = true) { if ($raw) { return $this->httpAuth; } else { - $pos_colon = strpos ($this->httpAuth, ':'); - $user = substr ($this->httpAuth, 0, $pos_colon); - $pass = substr ($this->httpAuth, $pos_colon + 1); + $pos_colon = strpos($this->httpAuth, ':'); + $user = substr($this->httpAuth, 0, $pos_colon); + $pass = substr($this->httpAuth, $pos_colon + 1); - return array ( + return array( 'username' => $user, 'password' => $pass ); } } - public function inError () { + public function inError() { return $this->error; } - public function keepHistory () { + public function keepHistory() { return $this->keep_history; } - public function nbEntries () { + public function ttl() { + return $this->ttl; + } + public function nbEntries() { if ($this->nbEntries < 0) { - $feedDAO = new FreshRSS_FeedDAO (); - $this->nbEntries = $feedDAO->countEntries ($this->id ()); + $feedDAO = FreshRSS_Factory::createFeedDao(); + $this->nbEntries = $feedDAO->countEntries($this->id()); } return $this->nbEntries; } - public function nbNotRead () { + public function nbNotRead() { if ($this->nbNotRead < 0) { - $feedDAO = new FreshRSS_FeedDAO (); - $this->nbNotRead = $feedDAO->countNotRead ($this->id ()); + $feedDAO = FreshRSS_Factory::createFeedDao(); + $this->nbNotRead = $feedDAO->countNotRead($this->id()); } return $this->nbNotRead; } public function faviconPrepare() { $file = DATA_PATH . '/favicons/' . $this->hash() . '.txt'; - if (!file_exists ($file)) { + if (!file_exists($file)) { $t = $this->website; - if (empty($t)) { + if ($t == '') { $t = $this->url; } file_put_contents($file, $t); @@ -115,109 +126,123 @@ class FreshRSS_Feed extends Minz_Model { @unlink($path . '.ico'); @unlink($path . '.txt'); } - public function favicon () { - return Minz_Url::display ('/f.php?' . $this->hash()); + public function favicon() { + return Minz_Url::display('/f.php?' . $this->hash()); } - public function _id ($value) { + public function _id($value) { $this->id = $value; } - public function _url ($value, $validate=true) { + public function _url($value, $validate=true) { + $this->hash = null; if ($validate) { $value = checkUrl($value); } - if (empty ($value)) { - throw new FreshRSS_BadUrl_Exception ($value); + if (empty($value)) { + throw new FreshRSS_BadUrl_Exception($value); } $this->url = $value; } - public function _category ($value) { + public function _category($value) { $value = intval($value); $this->category = $value >= 0 ? $value : 0; } - public function _name ($value) { + public function _name($value) { $this->name = $value === null ? '' : $value; } - public function _website ($value, $validate=true) { + public function _website($value, $validate=true) { if ($validate) { $value = checkUrl($value); } - if (empty ($value)) { + if (empty($value)) { $value = ''; } $this->website = $value; } - public function _description ($value) { + public function _description($value) { $this->description = $value === null ? '' : $value; } - public function _lastUpdate ($value) { + public function _lastUpdate($value) { $this->lastUpdate = $value; } - public function _priority ($value) { + public function _priority($value) { $value = intval($value); $this->priority = $value >= 0 ? $value : 10; } - public function _pathEntries ($value) { + public function _pathEntries($value) { $this->pathEntries = $value; } - public function _httpAuth ($value) { + public function _httpAuth($value) { $this->httpAuth = $value; } - public function _error ($value) { + public function _error($value) { $this->error = (bool)$value; } - public function _keepHistory ($value) { + public function _keepHistory($value) { $value = intval($value); $value = min($value, 1000000); $value = max($value, -2); $this->keep_history = $value; } - public function _nbNotRead ($value) { + public function _ttl($value) { + $value = intval($value); + $value = min($value, 100000000); + $value = max($value, -2); + $this->ttl = $value; + } + public function _nbNotRead($value) { $this->nbNotRead = intval($value); } - public function _nbEntries ($value) { + public function _nbEntries($value) { $this->nbEntries = intval($value); } - public function load ($loadDetails = false) { + public function load($loadDetails = false) { if ($this->url !== null) { if (CACHE_PATH === false) { - throw new Minz_FileNotExistException ( + throw new Minz_FileNotExistException( 'CACHE_PATH', Minz_Exception::ERROR ); } else { - $url = htmlspecialchars_decode ($this->url, ENT_QUOTES); + $url = htmlspecialchars_decode($this->url, ENT_QUOTES); if ($this->httpAuth != '') { - $url = preg_replace ('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url); + $url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url); } $feed = customSimplePie(); - $feed->set_feed_url ($url); + $feed->set_feed_url($url); + if (!$loadDetails) { //Only activates auto-discovery when adding a new feed + $feed->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE); + } $mtime = $feed->init(); if ((!$mtime) || $feed->error()) { - throw new FreshRSS_Feed_Exception ($feed->error() . ' [' . $url . ']'); - } - - // si on a utilisé l'auto-discover, notre url va avoir changé - $subscribe_url = $feed->subscribe_url (); - if ($subscribe_url !== null && $subscribe_url !== $this->url) { - if ($this->httpAuth != '') { - // on enlève les id si authentification HTTP - $subscribe_url = preg_replace ('#((.+)://)((.+)@)(.+)#', '${1}${5}', $subscribe_url); - } - $this->_url ($subscribe_url); + throw new FreshRSS_Feed_Exception($feed->error() . ' [' . $url . ']'); } if ($loadDetails) { - $title = htmlspecialchars(html_only_entity_decode($feed->get_title()), ENT_COMPAT, 'UTF-8'); - $this->_name ($title === null ? $this->url : $title); + // si on a utilisé l'auto-discover, notre url va avoir changé + $subscribe_url = $feed->subscribe_url(false); + + $title = strtr(html_only_entity_decode($feed->get_title()), array('<' => '<', '>' => '>', '"' => '"')); //HTML to HTML-PRE //ENT_COMPAT except & + $this->_name($title == '' ? $this->url : $title); $this->_website(html_only_entity_decode($feed->get_link())); $this->_description(html_only_entity_decode($feed->get_description())); + } else { + //The case of HTTP 301 Moved Permanently + $subscribe_url = $feed->subscribe_url(true); } - if (($mtime === true) || ($mtime > $this->lastUpdate)) { + if ($subscribe_url !== null && $subscribe_url !== $this->url) { + if ($this->httpAuth != '') { + // on enlève les id si authentification HTTP + $subscribe_url = preg_replace('#((.+)://)((.+)@)(.+)#', '${1}${5}', $subscribe_url); + } + $this->_url($subscribe_url); + } + + if (($mtime === true) ||($mtime > $this->lastUpdate)) { syslog(LOG_DEBUG, 'FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $subscribe_url); $this->loadEntries($feed); // et on charge les articles du flux } else { @@ -231,47 +256,52 @@ class FreshRSS_Feed extends Minz_Model { } } - private function loadEntries ($feed) { - $entries = array (); + private function loadEntries($feed) { + $entries = array(); - foreach ($feed->get_items () as $item) { - $title = html_only_entity_decode (strip_tags ($item->get_title ())); - $author = $item->get_author (); - $link = $item->get_permalink (); - $date = @strtotime ($item->get_date ()); + foreach ($feed->get_items() as $item) { + $title = html_only_entity_decode(strip_tags($item->get_title())); + $author = $item->get_author(); + $link = $item->get_permalink(); + $date = @strtotime($item->get_date()); // gestion des tags (catégorie == tag) - $tags_tmp = $item->get_categories (); - $tags = array (); + $tags_tmp = $item->get_categories(); + $tags = array(); if ($tags_tmp !== null) { foreach ($tags_tmp as $tag) { - $tags[] = html_only_entity_decode ($tag->get_label ()); + $tags[] = html_only_entity_decode($tag->get_label()); } } - $content = html_only_entity_decode ($item->get_content ()); + $content = html_only_entity_decode($item->get_content()); $elinks = array(); foreach ($item->get_enclosures() as $enclosure) { $elink = $enclosure->get_link(); - if (array_key_exists($elink, $elinks)) continue; - $elinks[$elink] = '1'; - $mime = strtolower($enclosure->get_type()); - if (strpos($mime, 'image/') === 0) { - $content .= '<br /><img src="' . $elink . '" alt="" />'; + if (empty($elinks[$elink])) { + $elinks[$elink] = '1'; + $mime = strtolower($enclosure->get_type()); + if (strpos($mime, 'image/') === 0) { + $content .= '<br /><img lazyload="" postpone="" src="' . $elink . '" alt="" />'; + } elseif (strpos($mime, 'audio/') === 0) { + $content .= '<br /><audio lazyload="" postpone="" preload="none" src="' . $elink . '" controls="controls" />'; + } elseif (strpos($mime, 'video/') === 0) { + $content .= '<br /><video lazyload="" postpone="" preload="none" src="' . $elink . '" controls="controls" />'; + } } } - $entry = new FreshRSS_Entry ( - $this->id (), - $item->get_id (), + $entry = new FreshRSS_Entry( + $this->id(), + $item->get_id(), $title === null ? '' : $title, - $author === null ? '' : html_only_entity_decode ($author->name), + $author === null ? '' : html_only_entity_decode($author->name), $content === null ? '' : $content, $link === null ? '' : $link, - $date ? $date : time () + $date ? $date : time() ); - $entry->_tags ($tags); + $entry->_tags($tags); // permet de récupérer le contenu des flux tronqués $entry->loadCompleteContent($this->pathEntries()); @@ -283,20 +313,19 @@ class FreshRSS_Feed extends Minz_Model { } function lock() { - $lock = TMP_PATH . '/' . md5(Minz_Configuration::salt() . $this->url) . '.freshrss.lock'; - if (file_exists($lock) && ((time() - @filemtime($lock)) > 3600)) { - @unlink($lock); + $this->lockPath = TMP_PATH . '/' . $this->hash() . '.freshrss.lock'; + if (file_exists($this->lockPath) && ((time() - @filemtime($this->lockPath)) > 3600)) { + @unlink($this->lockPath); } - if (($handle = @fopen($lock, 'x')) === false) { + if (($handle = @fopen($this->lockPath, 'x')) === false) { return false; } - //register_shutdown_function('unlink', $lock); + //register_shutdown_function('unlink', $this->lockPath); @fclose($handle); return true; } function unlock() { - $lock = TMP_PATH . '/' . md5(Minz_Configuration::salt() . $this->url) . '.freshrss.lock'; - @unlink($lock); + @unlink($this->lockPath); } } diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 7ebe68d2b..b89ae2045 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -1,244 +1,279 @@ <?php class FreshRSS_FeedDAO extends Minz_ModelPdo { - public function addFeed ($valuesTmp) { - $sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2)'; - $stm = $this->bd->prepare ($sql); + public function addFeed($valuesTmp) { + $sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)'; + $stm = $this->bd->prepare($sql); - $values = array ( + $values = array( substr($valuesTmp['url'], 0, 511), $valuesTmp['category'], substr($valuesTmp['name'], 0, 255), substr($valuesTmp['website'], 0, 255), substr($valuesTmp['description'], 0, 1023), $valuesTmp['lastUpdate'], - base64_encode ($valuesTmp['httpAuth']), + base64_encode($valuesTmp['httpAuth']), ); - if ($stm && $stm->execute ($values)) { + if ($stm && $stm->execute($values)) { return $this->bd->lastInsertId(); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error addFeed: ' . $info[2], Minz_Log::ERROR); return false; } } - public function updateFeed ($id, $valuesTmp) { + public function addFeedObject($feed) { + // TODO: not sure if we should write this method in DAO since DAO + // should not be aware about feed class + + // Add feed only if we don't find it in DB + $feed_search = $this->searchByUrl($feed->url()); + if (!$feed_search) { + $values = array( + 'id' => $feed->id(), + 'url' => $feed->url(), + 'category' => $feed->category(), + 'name' => $feed->name(), + 'website' => $feed->website(), + 'description' => $feed->description(), + 'lastUpdate' => 0, + 'httpAuth' => $feed->httpAuth() + ); + + $id = $this->addFeed($values); + if ($id) { + $feed->_id($id); + $feed->faviconPrepare(); + } + + return $id; + } + + return $feed_search->id(); + } + + public function updateFeed($id, $valuesTmp) { $set = ''; foreach ($valuesTmp as $key => $v) { $set .= $key . '=?, '; if ($key == 'httpAuth') { - $valuesTmp[$key] = base64_encode ($v); + $valuesTmp[$key] = base64_encode($v); } } - $set = substr ($set, 0, -2); + $set = substr($set, 0, -2); $sql = 'UPDATE `' . $this->prefix . 'feed` SET ' . $set . ' WHERE id=?'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); foreach ($valuesTmp as $v) { $values[] = $v; } $values[] = $id; - if ($stm && $stm->execute ($values)) { + if ($stm && $stm->execute($values)) { return $stm->rowCount(); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error updateFeed: ' . $info[2], Minz_Log::ERROR); return false; } } - public function updateLastUpdate ($id, $inError = 0, $updateCache = true) { + public function updateLastUpdate($id, $inError = 0, $updateCache = true) { if ($updateCache) { - $sql = 'UPDATE `' . $this->prefix . 'feed` f ' //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE - . 'SET f.cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=f.id),' - . 'f.cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=f.id AND e2.is_read=0),' + $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 f.id=?'; + . 'WHERE id=?'; } else { - $sql = 'UPDATE `' . $this->prefix . 'feed` f ' + $sql = 'UPDATE `' . $this->prefix . 'feed` ' . 'SET lastUpdate=?, error=? ' - . 'WHERE f.id=?'; + . 'WHERE id=?'; } - $values = array ( + $values = array( time(), $inError, $id, ); - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); - if ($stm && $stm->execute ($values)) { + if ($stm && $stm->execute($values)) { return $stm->rowCount(); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error updateLastUpdate: ' . $info[2], Minz_Log::ERROR); return false; } } - public function changeCategory ($idOldCat, $idNewCat) { - $catDAO = new FreshRSS_CategoryDAO (); - $newCat = $catDAO->searchById ($idNewCat); + public function changeCategory($idOldCat, $idNewCat) { + $catDAO = new FreshRSS_CategoryDAO(); + $newCat = $catDAO->searchById($idNewCat); if (!$newCat) { - $newCat = $catDAO->getDefault (); + $newCat = $catDAO->getDefault(); } $sql = 'UPDATE `' . $this->prefix . 'feed` SET category=? WHERE category=?'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); - $values = array ( - $newCat->id (), + $values = array( + $newCat->id(), $idOldCat ); - if ($stm && $stm->execute ($values)) { + if ($stm && $stm->execute($values)) { return $stm->rowCount(); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error changeCategory: ' . $info[2], Minz_Log::ERROR); return false; } } - public function deleteFeed ($id) { - /*//For MYISAM (MySQL 5.5-) without FOREIGN KEY - $sql = 'DELETE FROM `' . $this->prefix . 'entry` WHERE id_feed=?'; - $stm = $this->bd->prepare ($sql); - $values = array ($id); - if (!($stm && $stm->execute ($values))) { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - return false; - }*/ - + public function deleteFeed($id) { $sql = 'DELETE FROM `' . $this->prefix . 'feed` WHERE id=?'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); - $values = array ($id); + $values = array($id); - if ($stm && $stm->execute ($values)) { + if ($stm && $stm->execute($values)) { return $stm->rowCount(); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error deleteFeed: ' . $info[2], Minz_Log::ERROR); return false; } } - public function deleteFeedByCategory ($id) { - /*//For MYISAM (MySQL 5.5-) without FOREIGN KEY - $sql = 'DELETE FROM `' . $this->prefix . 'entry` e ' - . 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' - . 'WHERE f.category=?'; - $stm = $this->bd->prepare ($sql); - $values = array ($id); - if (!($stm && $stm->execute ($values))) { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - return false; - }*/ - + public function deleteFeedByCategory($id) { $sql = 'DELETE FROM `' . $this->prefix . 'feed` WHERE category=?'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); - $values = array ($id); + $values = array($id); - if ($stm && $stm->execute ($values)) { + if ($stm && $stm->execute($values)) { return $stm->rowCount(); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error deleteFeedByCategory: ' . $info[2], Minz_Log::ERROR); return false; } } - public function searchById ($id) { + public function searchById($id) { $sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE id=?'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); - $values = array ($id); + $values = array($id); - $stm->execute ($values); - $res = $stm->fetchAll (PDO::FETCH_ASSOC); - $feed = self::daoToFeed ($res); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + $feed = self::daoToFeed($res); - if (isset ($feed[$id])) { + if (isset($feed[$id])) { return $feed[$id]; } else { - return false; + return null; } } - public function searchByUrl ($url) { + public function searchByUrl($url) { $sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE url=?'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); - $values = array ($url); + $values = array($url); - $stm->execute ($values); - $res = $stm->fetchAll (PDO::FETCH_ASSOC); - $feed = current (self::daoToFeed ($res)); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + $feed = current(self::daoToFeed($res)); - if (isset ($feed)) { + if (isset($feed)) { return $feed; } else { - return false; + return null; } } - public function listFeeds () { + public function listFeeds() { $sql = 'SELECT * FROM `' . $this->prefix . 'feed` ORDER BY name'; - $stm = $this->bd->prepare ($sql); - $stm->execute (); + $stm = $this->bd->prepare($sql); + $stm->execute(); - return self::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC)); + return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC)); } - public function listFeedsOrderUpdate ($cacheDuration = 1500) { - $sql = 'SELECT id, name, url, lastUpdate, pathEntries, httpAuth, keep_history ' + public function arrayFeedCategoryNames() { //For API + $sql = 'SELECT f.id, f.name, c.name as c_name FROM `' . $this->prefix . 'feed` f ' + . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category'; + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + $feedCategoryNames = array(); + foreach ($res as $line) { + $feedCategoryNames[$line['id']] = array( + 'name' => $line['name'], + 'c_name' => $line['c_name'], + ); + } + return $feedCategoryNames; + } + + public function listFeedsOrderUpdate($defaultCacheDuration = 3600) { + if ($defaultCacheDuration < 0) { + $defaultCacheDuration = 2147483647; + } + $sql = 'SELECT id, url, name, website, lastUpdate, pathEntries, httpAuth, keep_history, ttl ' . 'FROM `' . $this->prefix . 'feed` ' - . 'WHERE lastUpdate < ' . (time() - intval($cacheDuration)) - . ' ORDER BY lastUpdate'; - $stm = $this->bd->prepare ($sql); - $stm->execute (); + . 'WHERE ttl <> -1 AND lastUpdate < (' . (time() + 60) . '-(CASE WHEN ttl=-2 THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ' + . 'ORDER BY lastUpdate'; + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute())) { + $sql2 = 'ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN ttl INT NOT NULL DEFAULT -2'; //v0.7.3 + $stm = $this->bd->prepare($sql2); + $stm->execute(); + $stm = $this->bd->prepare($sql); + $stm->execute(); + } - return self::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC)); + return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC)); } - public function listByCategory ($cat) { + public function listByCategory($cat) { $sql = 'SELECT * FROM `' . $this->prefix . 'feed` WHERE category=? ORDER BY name'; - $stm = $this->bd->prepare ($sql); + $stm = $this->bd->prepare($sql); - $values = array ($cat); + $values = array($cat); - $stm->execute ($values); + $stm->execute($values); - return self::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC)); + return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC)); } - public function countEntries ($id) { + public function countEntries($id) { $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` WHERE id_feed=?'; - $stm = $this->bd->prepare ($sql); - $values = array ($id); - $stm->execute ($values); - $res = $stm->fetchAll (PDO::FETCH_ASSOC); + $stm = $this->bd->prepare($sql); + $values = array($id); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); return $res[0]['count']; } - public function countNotRead ($id) { + + public function countNotRead($id) { $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND is_read=0'; - $stm = $this->bd->prepare ($sql); - $values = array ($id); - $stm->execute ($values); - $res = $stm->fetchAll (PDO::FETCH_ASSOC); + $stm = $this->bd->prepare($sql); + $values = array($id); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); return $res[0]['count']; } - public function updateCachedValues () { //For one single feed, call updateLastUpdate($id) + + public function updateCachedValues() { //For one single feed, call updateLastUpdate($id) $sql = 'UPDATE `' . $this->prefix . 'feed` f ' . 'INNER JOIN (' . 'SELECT e.id_feed, ' @@ -248,80 +283,78 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { . 'GROUP BY e.id_feed' . ') x ON x.id_feed=f.id ' . 'SET f.cache_nbEntries=x.nbEntries, f.cache_nbUnreads=x.nbUnreads'; - $stm = $this->bd->prepare ($sql); - - $values = array ($feed_id); + $stm = $this->bd->prepare($sql); - if ($stm && $stm->execute ($values)) { + if ($stm && $stm->execute()) { return $stm->rowCount(); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error updateCachedValues: ' . $info[2], Minz_Log::ERROR); return false; } } - public function truncate ($id) { - $sql = 'DELETE e.* FROM `' . $this->prefix . 'entry` e WHERE e.id_feed=?'; + public function truncate($id) { + $sql = 'DELETE FROM `' . $this->prefix . 'entry` WHERE id_feed=?'; $stm = $this->bd->prepare($sql); $values = array($id); - $this->bd->beginTransaction (); - if (!($stm && $stm->execute ($values))) { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - $this->bd->rollBack (); - return false; - } + $this->bd->beginTransaction(); + if (!($stm && $stm->execute($values))) { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error truncate: ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); + return false; + } $affected = $stm->rowCount(); - $sql = 'UPDATE `' . $this->prefix . 'feed` f ' - . 'SET f.cache_nbEntries=0, f.cache_nbUnreads=0 WHERE f.id=?'; - $values = array ($id); - $stm = $this->bd->prepare ($sql); - if (!($stm && $stm->execute ($values))) { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - $this->bd->rollBack (); + $sql = 'UPDATE `' . $this->prefix . 'feed` ' + . 'SET cache_nbEntries=0, cache_nbUnreads=0 WHERE id=?'; + $values = array($id); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error truncate: ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); return false; } - $this->bd->commit (); + $this->bd->commit(); return $affected; } - public function cleanOldEntries ($id, $date_min, $keep = 15) { //Remember to call updateLastUpdate($id) just after - $sql = 'DELETE e.* FROM `' . $this->prefix . 'entry` e ' - . 'WHERE e.id_feed = :id_feed AND e.id <= :id_max AND e.is_favorite = 0 AND e.id NOT IN ' - . '(SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed = :id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select because of: MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery' - $stm = $this->bd->prepare ($sql); + public function cleanOldEntries($id, $date_min, $keep = 15) { //Remember to call updateLastUpdate($id) just after + $sql = 'DELETE FROM `' . $this->prefix . 'entry` ' + . 'WHERE id_feed = :id_feed AND id <= :id_max AND is_favorite=0 AND id NOT IN ' + . '(SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed = :id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery' + $stm = $this->bd->prepare($sql); $id_max = intval($date_min) . '000000'; $stm->bindParam(':id_feed', $id, PDO::PARAM_INT); - $stm->bindParam(':id_max', $id_max, PDO::PARAM_INT); + $stm->bindParam(':id_max', $id_max, PDO::PARAM_STR); $stm->bindParam(':keep', $keep, PDO::PARAM_INT); - if ($stm && $stm->execute ()) { + if ($stm && $stm->execute()) { return $stm->rowCount(); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record('SQL error cleanOldEntries: ' . $info[2], Minz_Log::ERROR); return false; } } - public static function daoToFeed ($listDAO, $catID = null) { - $list = array (); + public static function daoToFeed($listDAO, $catID = null) { + $list = array(); - if (!is_array ($listDAO)) { - $listDAO = array ($listDAO); + if (!is_array($listDAO)) { + $listDAO = array($listDAO); } foreach ($listDAO as $key => $dao) { - if (!isset ($dao['name'])) { + if (!isset($dao['name'])) { continue; } - if (isset ($dao['id'])) { + if (isset($dao['id'])) { $key = $dao['id']; } if ($catID === null) { @@ -338,13 +371,14 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { $myFeed->_lastUpdate(isset($dao['lastUpdate']) ? $dao['lastUpdate'] : 0); $myFeed->_priority(isset($dao['priority']) ? $dao['priority'] : 10); $myFeed->_pathEntries(isset($dao['pathEntries']) ? $dao['pathEntries'] : ''); - $myFeed->_httpAuth(isset($dao['httpAuth']) ? base64_decode ($dao['httpAuth']) : ''); + $myFeed->_httpAuth(isset($dao['httpAuth']) ? base64_decode($dao['httpAuth']) : ''); $myFeed->_error(isset($dao['error']) ? $dao['error'] : 0); $myFeed->_keepHistory(isset($dao['keep_history']) ? $dao['keep_history'] : -2); + $myFeed->_ttl(isset($dao['ttl']) ? $dao['ttl'] : -2); $myFeed->_nbNotRead(isset($dao['cache_nbUnreads']) ? $dao['cache_nbUnreads'] : 0); $myFeed->_nbEntries(isset($dao['cache_nbEntries']) ? $dao['cache_nbEntries'] : 0); - if (isset ($dao['id'])) { - $myFeed->_id ($dao['id']); + if (isset($dao['id'])) { + $myFeed->_id($dao['id']); } $list[$key] = $myFeed; } diff --git a/app/Models/FeedDAOSQLite.php b/app/Models/FeedDAOSQLite.php new file mode 100644 index 000000000..0d1872389 --- /dev/null +++ b/app/Models/FeedDAOSQLite.php @@ -0,0 +1,19 @@ +<?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::record('SQL error updateCachedValues: ' . $info[2], Minz_Log::ERROR); + return false; + } + } + +} diff --git a/app/Models/Share.php b/app/Models/Share.php new file mode 100644 index 000000000..b146db722 --- /dev/null +++ b/app/Models/Share.php @@ -0,0 +1,44 @@ +<?php + +class FreshRSS_Share { + + static public function generateUrl($options, $selected, $link, $title) { + $share = $options[$selected['type']]; + $matches = array( + '~URL~', + '~TITLE~', + '~LINK~', + ); + $replaces = array( + $selected['url'], + self::transformData($title, self::getTransform($share, 'title')), + self::transformData($link, self::getTransform($share, 'link')), + ); + $url = str_replace($matches, $replaces, $share['url']); + return $url; + } + + static private function transformData($data, $transform) { + if (!is_array($transform)) { + return $data; + } + if (count($transform) === 0) { + return $data; + } + foreach ($transform as $action) { + $data = call_user_func($action, $data); + } + return $data; + } + + static private function getTransform($options, $type) { + $transform = $options['transform']; + + if (array_key_exists($type, $transform)) { + return $transform[$type]; + } + + return $transform; + } + +} diff --git a/app/Models/StatsDAO.php b/app/Models/StatsDAO.php index 60cec7847..40505ab3e 100644 --- a/app/Models/StatsDAO.php +++ b/app/Models/StatsDAO.php @@ -2,6 +2,8 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo { + const ENTRY_COUNT_PERIOD = 30; + /** * Calculates entry repartition for all feeds and for main stream. * The repartition includes: @@ -9,7 +11,7 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo { * - read entries * - unread entries * - favorite entries - * + * * @return type */ public function calculateEntryRepartition() { @@ -50,68 +52,190 @@ SQL; /** * Calculates entry count per day on a 30 days period. * Returns the result as a JSON string. - * + * * @return string */ public function calculateEntryCount() { - $count = array(); + $count = $this->initEntryCountArray(); + $period = self::ENTRY_COUNT_PERIOD; - // Generates a list of 30 last day to be sure we always have 30 days. - // If we do not do that kind of thing, we'll end up with holes in the - // days if the user do not have a lot of feeds. + // Get stats per day for the last 30 days $sql = <<<SQL -SELECT - (tens.val + units.val + 1) AS day -FROM ( - SELECT 0 AS val - UNION ALL SELECT 1 - UNION ALL SELECT 2 - UNION ALL SELECT 3 - UNION ALL SELECT 4 - UNION ALL SELECT 5 - UNION ALL SELECT 6 - UNION ALL SELECT 7 - UNION ALL SELECT 8 - UNION ALL SELECT 9 -) AS units -CROSS JOIN ( - SELECT 0 AS val - UNION ALL SELECT 10 - UNION ALL SELECT 20 -) AS tens +SELECT DATEDIFF(FROM_UNIXTIME(e.date), NOW()) AS day, +COUNT(1) AS count +FROM {$this->prefix}entry AS e +WHERE FROM_UNIXTIME(e.date, '%Y%m%d') BETWEEN DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -{$period} DAY), '%Y%m%d') AND DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -1 DAY), '%Y%m%d') +GROUP BY day ORDER BY day ASC SQL; $stm = $this->bd->prepare($sql); $stm->execute(); $res = $stm->fetchAll(PDO::FETCH_ASSOC); + foreach ($res as $value) { - $count[$value['day']] = 0; + $count[$value['day']] = (int) $value['count']; } - // Get stats per day for the last 30 days and applies the result on - // the array created with the last query. + return $this->convertToSerie($count); + } + + /** + * Initialize an array for the entry count. + * + * @return array + */ + protected function initEntryCountArray() { + return $this->initStatsArray(-self::ENTRY_COUNT_PERIOD, -1); + } + + /** + * Calculates the number of article per hour of the day per feed + * + * @param integer $feed id + * @return string + */ + public function calculateEntryRepartitionPerFeedPerHour($feed = null) { + return $this->calculateEntryRepartitionPerFeedPerPeriod('%H', $feed); + } + + /** + * Calculates the number of article per day of week per feed + * + * @param integer $feed id + * @return string + */ + public function calculateEntryRepartitionPerFeedPerDayOfWeek($feed = null) { + return $this->calculateEntryRepartitionPerFeedPerPeriod('%w', $feed); + } + + /** + * Calculates the number of article per month per feed + * + * @param integer $feed + * @return string + */ + public function calculateEntryRepartitionPerFeedPerMonth($feed = null) { + return $this->calculateEntryRepartitionPerFeedPerPeriod('%m', $feed); + } + + /** + * Calculates the number of article per period per feed + * + * @param string $period format string to use for grouping + * @param integer $feed id + * @return string + */ + protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) { + if ($feed) { + $restrict = "WHERE e.id_feed = {$feed}"; + } else { + $restrict = ''; + } $sql = <<<SQL -SELECT DATEDIFF(FROM_UNIXTIME(e.date), NOW()) AS day, -COUNT(1) AS count +SELECT DATE_FORMAT(FROM_UNIXTIME(e.date), '{$period}') AS period +, COUNT(1) AS count FROM {$this->prefix}entry AS e -WHERE FROM_UNIXTIME(e.date, '%Y%m%d') BETWEEN DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -30 DAY), '%Y%m%d') AND DATE_FORMAT(DATE_ADD(NOW(), INTERVAL -1 DAY), '%Y%m%d') -GROUP BY day -ORDER BY day ASC +{$restrict} +GROUP BY period +ORDER BY period ASC SQL; + $stm = $this->bd->prepare($sql); $stm->execute(); - $res = $stm->fetchAll(PDO::FETCH_ASSOC); + $res = $stm->fetchAll(PDO::FETCH_NAMED); foreach ($res as $value) { - $count[$value['day']] = (int) $value['count']; + $repartition[(int) $value['period']] = (int) $value['count']; } - return $this->convertToSerie($count); + return $this->convertToSerie($repartition); + } + + /** + * Calculates the average number of article per hour per feed + * + * @param integer $feed id + * @return integer + */ + public function calculateEntryAveragePerFeedPerHour($feed = null) { + return $this->calculateEntryAveragePerFeedPerPeriod(1/24, $feed); + } + + /** + * Calculates the average number of article per day of week per feed + * + * @param integer $feed id + * @return integer + */ + public function calculateEntryAveragePerFeedPerDayOfWeek($feed = null) { + return $this->calculateEntryAveragePerFeedPerPeriod(7, $feed); + } + + /** + * Calculates the average number of article per month per feed + * + * @param integer $feed id + * @return integer + */ + public function calculateEntryAveragePerFeedPerMonth($feed = null) { + return $this->calculateEntryAveragePerFeedPerPeriod(30, $feed); + } + + /** + * Calculates the average number of article per feed + * + * @param float $period number used to divide the number of day in the period + * @param integer $feed id + * @return integer + */ + protected function calculateEntryAveragePerFeedPerPeriod($period, $feed = null) { + if ($feed) { + $restrict = "WHERE e.id_feed = {$feed}"; + } else { + $restrict = ''; + } + $sql = <<<SQL +SELECT COUNT(1) AS count +, MIN(date) AS date_min +, MAX(date) AS date_max +FROM {$this->prefix}entry AS e +{$restrict} +SQL; + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetch(PDO::FETCH_NAMED); + $date_min = new \DateTime(); + $date_min->setTimestamp($res['date_min']); + $date_max = new \DateTime(); + $date_max->setTimestamp($res['date_max']); + $interval = $date_max->diff($date_min, true); + $interval_in_days = $interval->format('%a'); + if ($interval_in_days <= 0) { + // Surely only one article. + // We will return count / (period/period) == count. + $interval_in_days = $period; + } + + return round($res['count'] / ($interval_in_days / $period), 2); + } + + /** + * Initialize an array for statistics depending on a range + * + * @param integer $min + * @param integer $max + * @return array + */ + protected function initStatsArray($min, $max) { + return array_map(function () { + return 0; + }, array_flip(range($min, $max))); } /** * Calculates feed count per category. * Returns the result as a JSON string. - * + * * @return string */ public function calculateFeedByCategory() { @@ -134,7 +258,7 @@ SQL; /** * Calculates entry count per category. * Returns the result as a JSON string. - * + * * @return string */ public function calculateEntryByCategory() { @@ -158,7 +282,7 @@ SQL; /** * Calculates the 10 top feeds based on their number of entries - * + * * @return array */ public function calculateTopFeed() { @@ -172,7 +296,7 @@ FROM {$this->prefix}category AS c, {$this->prefix}entry AS e WHERE c.id = f.category AND f.id = e.id_feed -GROUP BY id +GROUP BY f.id ORDER BY count DESC LIMIT 10 SQL; @@ -181,7 +305,29 @@ SQL; return $stm->fetchAll(PDO::FETCH_ASSOC); } - private function convertToSerie($data) { + /** + * Calculates the last publication date for each feed + * + * @return array + */ + public function calculateFeedLastDate() { + $sql = <<<SQL +SELECT MAX(f.id) as id +, MAX(f.name) AS name +, MAX(date) AS last_date +, COUNT(*) AS nb_articles +FROM {$this->prefix}feed AS f, +{$this->prefix}entry AS e +WHERE f.id = e.id_feed +GROUP BY f.id +ORDER BY name +SQL; + $stm = $this->bd->prepare($sql); + $stm->execute(); + return $stm->fetchAll(PDO::FETCH_ASSOC); + } + + protected function convertToSerie($data) { $serie = array(); foreach ($data as $key => $value) { @@ -191,7 +337,7 @@ SQL; return json_encode($serie); } - private function convertToPieSerie($data) { + protected function convertToPieSerie($data) { $serie = array(); foreach ($data as $value) { @@ -202,4 +348,57 @@ SQL; return json_encode($serie); } + /** + * Gets days ready for graphs + * + * @return string + */ + public function getDays() { + return $this->convertToTranslatedJson(array( + 'sun', + 'mon', + 'tue', + 'wed', + 'thu', + 'fri', + 'sat', + )); + } + + /** + * Gets months ready for graphs + * + * @return string + */ + public function getMonths() { + return $this->convertToTranslatedJson(array( + 'jan', + 'feb', + 'mar', + 'apr', + 'may', + 'jun', + 'jul', + 'aug', + 'sep', + 'oct', + 'nov', + 'dec', + )); + } + + /** + * Translates array content and encode it as JSON + * + * @param array $data + * @return string + */ + private function convertToTranslatedJson($data = array()) { + $translated = array_map(function ($a) { + return Minz_Translate::t($a); + }, $data); + + return json_encode($translated); + } + } diff --git a/app/Models/StatsDAOSQLite.php b/app/Models/StatsDAOSQLite.php new file mode 100644 index 000000000..3b1256de1 --- /dev/null +++ b/app/Models/StatsDAOSQLite.php @@ -0,0 +1,64 @@ +<?php + +class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO { + + /** + * Calculates entry count per day on a 30 days period. + * Returns the result as a JSON string. + * + * @return string + */ + public function calculateEntryCount() { + $count = $this->initEntryCountArray(); + $period = parent::ENTRY_COUNT_PERIOD; + + // Get stats per day for the last 30 days + $sql = <<<SQL +SELECT round(julianday(e.date, 'unixepoch') - julianday('now')) AS day, +COUNT(1) AS count +FROM {$this->prefix}entry AS e +WHERE strftime('%Y%m%d', e.date, 'unixepoch') + BETWEEN strftime('%Y%m%d', 'now', '-{$period} days') + AND strftime('%Y%m%d', 'now', '-1 day') +GROUP BY day +ORDER BY day ASC +SQL; + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + + foreach ($res as $value) { + $count[(int) $value['day']] = (int) $value['count']; + } + + return $this->convertToSerie($count); + } + + protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) { + if ($feed) { + $restrict = "WHERE e.id_feed = {$feed}"; + } else { + $restrict = ''; + } + $sql = <<<SQL +SELECT strftime('{$period}', e.date, 'unixepoch') AS period +, COUNT(1) AS count +FROM {$this->prefix}entry AS e +{$restrict} +GROUP BY period +ORDER BY period ASC +SQL; + + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetchAll(PDO::FETCH_NAMED); + + $repartition = array(); + foreach ($res as $value) { + $repartition[(int) $value['period']] = (int) $value['count']; + } + + return $this->convertToSerie($repartition); + } + +} diff --git a/app/Models/Themes.php b/app/Models/Themes.php index c7099a1df..68fc17a2b 100644 --- a/app/Models/Themes.php +++ b/app/Models/Themes.php @@ -31,7 +31,10 @@ class FreshRSS_Themes extends Minz_Model { if (file_exists($json_filename)) { $content = file_get_contents($json_filename); $res = json_decode($content, true); - if ($res && isset($res['files']) && is_array($res['files'])) { + if ($res && + !empty($res['name']) && + isset($res['files']) && + is_array($res['files'])) { $res['id'] = $theme_id; return $res; } @@ -70,6 +73,7 @@ class FreshRSS_Themes extends Minz_Model { 'add' => '✚', 'all' => '☰', 'bookmark' => '★', + 'bookmark-add' => '✚', 'category' => '☷', 'category-white' => '☷', 'close' => '❌', @@ -77,6 +81,8 @@ class FreshRSS_Themes extends Minz_Model { 'down' => '▽', 'favorite' => '★', 'help' => 'ⓘ', + 'icon' => '⊚', + 'key' => '⚿', 'link' => '↗', 'login' => '🔒', 'logout' => '🔓', @@ -84,13 +90,18 @@ class FreshRSS_Themes extends Minz_Model { 'non-starred' => '☆', 'prev' => '⏪', 'read' => '☑', + 'rss' => '☄', 'unread' => '☐', 'refresh' => '🔃', //↻ 'search' => '🔍', 'share' => '♺', 'starred' => '★', + 'stats' => '%', 'tag' => '⚐', 'up' => '△', + 'view-normal' => '☰', + 'view-global' => '☷', + 'view-reader' => '☕', ); if (!isset($alts[$name])) { return ''; @@ -104,3 +115,7 @@ class FreshRSS_Themes extends Minz_Model { '<img class="icon" src="' . Minz_Url::display($url) . '" alt="' . $alts[$name] . '" />'; } } + +function _i($icon, $url_only = false) { + return FreshRSS_Themes::icon($icon, $url_only); +} diff --git a/app/Models/UserDAO.php b/app/Models/UserDAO.php index a25b57f89..9f64fb4a7 100644 --- a/app/Models/UserDAO.php +++ b/app/Models/UserDAO.php @@ -2,35 +2,55 @@ class FreshRSS_UserDAO extends Minz_ModelPdo { public function createUser($username) { - require_once(APP_PATH . '/sql.php'); $db = Minz_Configuration::dataBase(); + require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); - $sql = sprintf(SQL_CREATE_TABLES, $db['prefix'] . $username . '_'); - $stm = $this->bd->prepare($sql, array(PDO::ATTR_EMULATE_PREPARES => true)); - $values = array( - 'catName' => Minz_Translate::t('default_category'), - ); - if ($stm && $stm->execute($values)) { + $userPDO = new Minz_ModelPdo($username); + + $ok = false; + if (defined('SQL_CREATE_TABLES')) { //E.g. MySQL + $sql = sprintf(SQL_CREATE_TABLES, $db['prefix'] . $username . '_', Minz_Translate::t('default_category')); + $stm = $userPDO->bd->prepare($sql); + $ok = $stm && $stm->execute(); + } else { //E.g. SQLite + global $SQL_CREATE_TABLES; + if (is_array($SQL_CREATE_TABLES)) { + $ok = true; + foreach ($SQL_CREATE_TABLES as $instruction) { + $sql = sprintf($instruction, '', Minz_Translate::t('default_category')); + $stm = $userPDO->bd->prepare($sql); + $ok &= ($stm && $stm->execute()); + } + } + } + + if ($ok) { return true; } else { - $info = $stm->errorInfo(); + $info = empty($stm) ? array(2 => 'syntax error') : $stm->errorInfo(); Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); return false; } } public function deleteUser($username) { - require_once(APP_PATH . '/sql.php'); $db = Minz_Configuration::dataBase(); + require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); - $sql = sprintf(SQL_DROP_TABLES, $db['prefix'] . $username . '_'); - $stm = $this->bd->prepare($sql); - if ($stm && $stm->execute()) { - return true; + if ($db['type'] === 'sqlite') { + return unlink(DATA_PATH . '/' . $username . '.sqlite'); } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - return false; + $userPDO = new Minz_ModelPdo($username); + + $sql = sprintf(SQL_DROP_TABLES, $db['prefix'] . $username . '_'); + $stm = $userPDO->bd->prepare($sql); + if ($stm && $stm->execute()) { + return true; + } else { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); + return false; + } } } } |
