diff options
Diffstat (limited to 'app')
106 files changed, 1441 insertions, 413 deletions
diff --git a/app/Controllers/categoryController.php b/app/Controllers/categoryController.php index f3b35a323..2551a79d4 100644 --- a/app/Controllers/categoryController.php +++ b/app/Controllers/categoryController.php @@ -16,7 +16,7 @@ class FreshRSS_category_Controller extends Minz_ActionController { Minz_Error::error(403); } - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); $catDAO->checkDefault(); } @@ -27,7 +27,7 @@ class FreshRSS_category_Controller extends Minz_ActionController { * - new-category */ public function createAction() { - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); $url_redirect = array('c' => 'subscription', 'a' => 'index'); $limits = FreshRSS_Context::$system_conf->limits; @@ -75,7 +75,7 @@ class FreshRSS_category_Controller extends Minz_ActionController { * - name */ public function updateAction() { - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); $url_redirect = array('c' => 'subscription', 'a' => 'index'); if (Minz_Request::isPost()) { @@ -116,7 +116,7 @@ class FreshRSS_category_Controller extends Minz_ActionController { */ public function deleteAction() { $feedDAO = FreshRSS_Factory::createFeedDao(); - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); $url_redirect = array('c' => 'subscription', 'a' => 'index'); if (Minz_Request::isPost()) { diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index d34b5d59d..20bcd2e76 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -243,8 +243,9 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * checking if categories and feeds are still in use. */ public function queriesAction() { - $category_dao = new FreshRSS_CategoryDAO(); + $category_dao = FreshRSS_Factory::createCategoryDao(); $feed_dao = FreshRSS_Factory::createFeedDao(); + $tag_dao = FreshRSS_Factory::createTagDao(); if (Minz_Request::isPost()) { $params = Minz_Request::param('queries', array()); @@ -277,16 +278,17 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * lean data. */ public function addQueryAction() { - $category_dao = new FreshRSS_CategoryDAO(); + $category_dao = FreshRSS_Factory::createCategoryDao(); $feed_dao = FreshRSS_Factory::createFeedDao(); + $tag_dao = FreshRSS_Factory::createTagDao(); $queries = array(); foreach (FreshRSS_Context::$user_conf->queries as $key => $query) { - $queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao); + $queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao); } $params = Minz_Request::fetchGET(); $params['url'] = Minz_Url::display(array('params' => $params)); $params['name'] = _t('conf.query.number', count($queries) + 1); - $queries[] = new FreshRSS_UserQuery($params, $feed_dao, $category_dao); + $queries[] = new FreshRSS_UserQuery($params, $feed_dao, $category_dao, $tag_dao); FreshRSS_Context::$user_conf->queries = $queries; FreshRSS_Context::$user_conf->save(); diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php index 16a15c447..fc0af0639 100755 --- a/app/Controllers/entryController.php +++ b/app/Controllers/entryController.php @@ -53,6 +53,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController { } $params = array(); + $this->view->tags = array(); $entryDAO = FreshRSS_Factory::createEntryDao(); if ($id === false) { @@ -81,6 +82,12 @@ class FreshRSS_entry_Controller extends Minz_ActionController { case 'a': $entryDAO->markReadEntries($id_max, false, 0, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read); break; + case 't': + $entryDAO->markReadTag($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read); + break; + case 'T': + $entryDAO->markReadTag('', $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read); + break; } if ($next_get !== 'a') { @@ -91,6 +98,13 @@ class FreshRSS_entry_Controller extends Minz_ActionController { } } else { $entryDAO->markRead($id, $is_read); + + $tagDAO = FreshRSS_Factory::createTagDao(); + foreach ($tagDAO->getTagsForEntry($id) as $tag) { + if (!empty($tag['checked'])) { + $this->view->tags[] = $tag['id']; + } + } } if (!$this->ajax) { @@ -193,6 +207,9 @@ class FreshRSS_entry_Controller extends Minz_ActionController { $feedDAO->updateCachedValues(); + $databaseDAO = FreshRSS_Factory::createDatabaseDAO(); + $databaseDAO->minorDbMaintenance(); + invalidateHttpCache(); Minz_Request::good(_t('feedback.sub.purge_completed', $nb_total), array( 'c' => 'configure', diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 2f7495884..f2b1b8960 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -43,7 +43,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { FreshRSS_UserDAO::touch(); @set_time_limit(300); - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); $url = trim($url); @@ -192,7 +192,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // GET request: we must ask confirmation to user before adding feed. Minz_View::prependTitle(_t('sub.feed.title_add') . ' · '); - $this->catDAO = new FreshRSS_CategoryDAO(); + $this->catDAO = FreshRSS_Factory::createCategoryDao(); $this->view->categories = $this->catDAO->listCategories(false); $this->view->feed = new FreshRSS_Feed($url); try { @@ -481,6 +481,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController { if ($entryDAO->inTransaction()) { $entryDAO->commit(); } + + $databaseDAO = FreshRSS_Factory::createDatabaseDAO(); + $databaseDAO->minorDbMaintenance(); } return array($updated_feeds, reset($feeds), $nb_new_articles); } @@ -511,6 +514,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $entryDAO->commitNewEntries(); $feedDAO->updateCachedValues(); $entryDAO->commit(); + + $databaseDAO = FreshRSS_Factory::createDatabaseDAO(); + $databaseDAO->minorDbMaintenance(); } else { list($updated_feeds, $feed, $nb_new_articles) = self::actualizeFeed($id, $url, $force, null, false, $noCommit); } @@ -556,7 +562,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } FreshRSS_UserDAO::touch(); - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); if ($cat_id > 0) { $cat = $catDAO->searchById($cat_id); $cat_id = $cat == null ? 0 : $cat->id(); diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index ddffdba73..fa914ef87 100755 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -32,7 +32,29 @@ class FreshRSS_index_Controller extends Minz_ActionController { Minz_Error::error(404); } - $this->view->callbackBeforeContent = function($view) { + $this->view->categories = FreshRSS_Context::$categories; + + $this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title(); + $title = FreshRSS_Context::$name; + if (FreshRSS_Context::$get_unread > 0) { + $title = '(' . FreshRSS_Context::$get_unread . ') ' . $title; + } + Minz_View::prependTitle($title . ' · '); + + $this->view->callbackBeforeFeeds = function ($view) { + try { + $tagDAO = FreshRSS_Factory::createTagDao(); + $view->tags = $tagDAO->listTags(true); + $view->nbUnreadTags = 0; + foreach ($view->tags as $tag) { + $view->nbUnreadTags += $tag->nbUnread(); + } + } catch (Exception $e) { + Minz_Log::notice($e->getMessage()); + } + }; + + $this->view->callbackBeforeEntries = function ($view) { try { FreshRSS_Context::$number++; //+1 for pagination $entries = FreshRSS_index_Controller::listEntriesByContext(); @@ -60,15 +82,6 @@ class FreshRSS_index_Controller extends Minz_ActionController { Minz_Log::notice($e->getMessage()); Minz_Error::error(404); } - - $view->categories = FreshRSS_Context::$categories; - - $view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title(); - $title = FreshRSS_Context::$name; - if (FreshRSS_Context::$get_unread > 0) { - $title = '(' . FreshRSS_Context::$get_unread . ') ' . $title; - } - Minz_View::prependTitle($title . ' · '); }; } @@ -158,7 +171,7 @@ class FreshRSS_index_Controller extends Minz_ActionController { */ private function updateContext() { if (empty(FreshRSS_Context::$categories)) { - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); FreshRSS_Context::$categories = $catDAO->listCategories(); } diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php index 9d7acf647..d56da9cbb 100755 --- a/app/Controllers/javascriptController.php +++ b/app/Controllers/javascriptController.php @@ -7,14 +7,17 @@ class FreshRSS_javascript_Controller extends Minz_ActionController { public function actualizeAction() { header('Content-Type: application/json; charset=UTF-8'); + Minz_Session::_param('actualize_feeds', false); $feedDAO = FreshRSS_Factory::createFeedDao(); $this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default); } public function nbUnreadsPerFeedAction() { header('Content-Type: application/json; charset=UTF-8'); - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); $this->view->categories = $catDAO->listCategories(true, false); + $tagDAO = FreshRSS_Factory::createTagDao(); + $this->view->tags = $tagDAO->listTags(true); } //For Web-form login diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php index 5d1dee72c..acfacb890 100644 --- a/app/Controllers/statsController.php +++ b/app/Controllers/statsController.php @@ -131,7 +131,7 @@ class FreshRSS_stats_Controller extends Minz_ActionController { */ public function repartitionAction() { $statsDAO = FreshRSS_Factory::createStatsDAO(); - $categoryDAO = new FreshRSS_CategoryDAO(); + $categoryDAO = FreshRSS_Factory::createCategoryDao(); $feedDAO = FreshRSS_Factory::createFeedDao(); Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js'))); $id = Minz_Request::param('id', null); diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php index 701a588e0..0b1439ba5 100644 --- a/app/Controllers/subscriptionController.php +++ b/app/Controllers/subscriptionController.php @@ -14,7 +14,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { Minz_Error::error(403); } - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); $feedDAO = FreshRSS_Factory::createFeedDao(); $catDAO->checkDefault(); @@ -98,6 +98,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { $feed->_attributes('mark_updated_article_unread', Minz_Request::paramTernary('mark_updated_article_unread')); $feed->_attributes('read_upon_reception', Minz_Request::paramTernary('read_upon_reception')); + $feed->_attributes('clear_cache', Minz_Request::paramTernary('clear_cache')); if (FreshRSS_Auth::hasAccess('admin')) { $feed->_attributes('ssl_verify', Minz_Request::paramTernary('ssl_verify')); diff --git a/app/Controllers/tagController.php b/app/Controllers/tagController.php new file mode 100644 index 000000000..106e0afa8 --- /dev/null +++ b/app/Controllers/tagController.php @@ -0,0 +1,80 @@ +<?php + +/** + * Controller to handle every tag actions. + */ +class FreshRSS_tag_Controller extends Minz_ActionController { + /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ + public function firstAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + // If ajax request, we do not print layout + $this->ajax = Minz_Request::param('ajax'); + if ($this->ajax) { + $this->view->_useLayout(false); + Minz_Request::_param('ajax'); + } + } + + /** + * This action adds (checked=true) or removes (checked=false) a tag to an entry. + */ + public function tagEntryAction() { + if (Minz_Request::isPost()) { + $id_tag = Minz_Request::param('id_tag'); + $name_tag = trim(Minz_Request::param('name_tag')); + $id_entry = Minz_Request::param('id_entry'); + $checked = Minz_Request::paramTernary('checked'); + if ($id_entry != false) { + $tagDAO = FreshRSS_Factory::createTagDao(); + if ($id_tag == 0 && $name_tag != '' && $checked) { + //Create new tag + $id_tag = $tagDAO->addTag(array('name' => $name_tag)); + } + if ($id_tag != 0) { + $tagDAO->tagEntry($id_tag, $id_entry, $checked); + } + } + } else { + Minz_Error::error(405); + } + if (!$this->ajax) { + Minz_Request::forward(array( + 'c' => 'index', + 'a' => 'index', + ), true); + } + } + + public function deleteAction() { + if (Minz_Request::isPost()) { + $id_tag = Minz_Request::param('id_tag'); + if ($id_tag != false) { + $tagDAO = FreshRSS_Factory::createTagDao(); + $tagDAO->deleteTag($id_tag); + } + } else { + Minz_Error::error(405); + } + if (!$this->ajax) { + Minz_Request::forward(array( + 'c' => 'index', + 'a' => 'index', + ), true); + } + } + + public function getTagsForEntryAction() { + $this->view->_useLayout(false); + header('Content-Type: application/json; charset=UTF-8'); + header('Cache-Control: private, no-cache, no-store, must-revalidate'); + $id_entry = Minz_Request::param('id_entry', 0); + $tagDAO = FreshRSS_Factory::createTagDao(); + $this->view->tags = $tagDAO->getTagsForEntry($id_entry); + } +} diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php index c67b358bb..2be644c85 100644 --- a/app/Controllers/updateController.php +++ b/app/Controllers/updateController.php @@ -32,7 +32,13 @@ class FreshRSS_update_Controller extends Minz_ActionController { $output = array(); $return = 1; try { - exec('git pull --ff-only', $output, $return); + exec('git clean -f -d -f', $output, $return); + if ($return == 0) { + exec('git pull --ff-only', $output, $return); + } else { + $line = is_array($output) ? implode('; ', $output) : '' . $output; + Minz_Log::warning('git clean warning:' . $line); + } } catch (Exception $e) { Minz_Log::warning('git pull error:' . $e->getMessage()); } diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index 75a4303d6..2338c8b2a 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -38,7 +38,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { * The username is also used as folder name, file name, and part of SQL table name. * '_' is a reserved internal username. */ - const USERNAME_PATTERN = '[0-9a-zA-Z_]{2,38}|[0-9a-zA-Z]'; + const USERNAME_PATTERN = '[0-9a-zA-Z_][0-9a-zA-Z_.]{1,38}|[0-9a-zA-Z]'; public static function checkUsername($username) { return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1; @@ -91,6 +91,10 @@ class FreshRSS_user_Controller extends Minz_ActionController { } public function updateAction() { + if (!FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + if (Minz_Request::isPost()) { $passwordPlain = Minz_Request::param('newPasswordPlain', '', true); Minz_Request::_param('newPasswordPlain'); //Discard plain-text password ASAP @@ -104,8 +108,12 @@ class FreshRSS_user_Controller extends Minz_ActionController { )); if ($ok) { - Minz_Request::good(_t('feedback.user.updated', $username), - array('c' => 'user', 'a' => 'manage')); + $isSelfUpdate = Minz_Session::param('currentUser', '_') === $username; + if ($passwordPlain == '' || !$isSelfUpdate) { + Minz_Request::good(_t('feedback.user.updated', $username), array('c' => 'user', 'a' => 'manage')); + } else { + Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'index', 'a' => 'index')); + } } else { Minz_Request::bad(_t('feedback.user.updated.error', $username), array('c' => 'user', 'a' => 'manage')); @@ -138,8 +146,11 @@ class FreshRSS_user_Controller extends Minz_ActionController { Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash); if ($ok) { - Minz_Request::good(_t('feedback.profile.updated'), - array('c' => 'user', 'a' => 'profile')); + if ($passwordPlain == '') { + Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'profile')); + } else { + Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'index', 'a' => 'index')); + } } else { Minz_Request::bad(_t('feedback.profile.error'), array('c' => 'user', 'a' => 'profile')); @@ -166,7 +177,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { $entryDAO = FreshRSS_Factory::createEntryDao($this->view->current_user); $this->view->nb_articles = $entryDAO->count(); - $databaseDAO = FreshRSS_Factory::createDatabaseDAO(); + $databaseDAO = FreshRSS_Factory::createDatabaseDAO($this->view->current_user); $this->view->size_user = $databaseDAO->size(); } } diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 2bd5135a9..dec446a8e 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -90,7 +90,6 @@ class FreshRSS extends Minz_FrontController { } $filetime = @filemtime(PUBLIC_PATH . '/themes/' . $theme_id . '/' . $filename); $url = '/themes/' . $theme_id . '/' . $filename . '?' . $filetime; - header('Link: <' . Minz_Url::display($url, '', 'root') . '>;rel=preload', false); //HTTP2 Minz_View::prependStyle(Minz_Url::display($url)); } } diff --git a/app/Models/Category.php b/app/Models/Category.php index 197faf942..240dbca73 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -30,7 +30,7 @@ class FreshRSS_Category extends Minz_Model { } public function nbFeed() { if ($this->nbFeed < 0) { - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); $this->nbFeed = $catDAO->countFeed($this->id()); } @@ -38,7 +38,7 @@ class FreshRSS_Category extends Minz_Model { } public function nbNotRead() { if ($this->nbNotRead < 0) { - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); $this->nbNotRead = $catDAO->countNotRead($this->id()); } @@ -68,7 +68,7 @@ class FreshRSS_Category extends Minz_Model { $this->id = $value; } public function _name($value) { - $this->name = mb_strcut(trim($value), 0, 255, 'UTF-8'); + $this->name = trim($value); } public function _feeds($values) { if (!is_array($values)) { diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index cf6b3bae3..ba7eb765e 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -5,11 +5,15 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable const DEFAULTCATEGORYID = 1; public function addCategory($valuesTmp) { - $sql = 'INSERT INTO `' . $this->prefix . 'category`(name) VALUES(?)'; + $sql = 'INSERT INTO `' . $this->prefix . 'category`(name) ' + . 'SELECT * FROM (SELECT TRIM(?)) c2 ' //TRIM() to provide a type hint as text for PostgreSQL + . 'WHERE NOT EXISTS (SELECT 1 FROM `' . $this->prefix . 'tag` WHERE name = TRIM(?))'; //No tag of the same name $stm = $this->bd->prepare($sql); + $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'); $values = array( - mb_strcut($valuesTmp['name'], 0, 255, 'UTF-8'), + $valuesTmp['name'], + $valuesTmp['name'], ); if ($stm && $stm->execute($values)) { @@ -35,12 +39,15 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable } public function updateCategory($id, $valuesTmp) { - $sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=?'; + $sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=? ' + . 'AND NOT EXISTS (SELECT 1 FROM `' . $this->prefix . 'tag` WHERE name = ?)'; //No tag of the same name $stm = $this->bd->prepare($sql); + $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'); $values = array( $valuesTmp['name'], - $id + $id, + $valuesTmp['name'], ); if ($stm && $stm->execute($values)) { @@ -151,7 +158,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable $sql = 'INSERT INTO `' . $this->prefix . 'category`(id, name) VALUES(?, ?)'; if (parent::$sharedDbType === 'pgsql') { //Force call to nextval() - $sql .= " RETURNING nextval('" . $this->prefix . "category_id_seq');"; + $sql .= ' RETURNING nextval(\'"' . $this->prefix . 'category_id_seq"\');'; } $stm = $this->bd->prepare($sql); diff --git a/app/Models/Context.php b/app/Models/Context.php index 2ca8f80b0..60ec6ff77 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -8,6 +8,7 @@ class FreshRSS_Context { public static $user_conf = null; public static $system_conf = null; public static $categories = array(); + public static $tags = array(); public static $name = ''; public static $description = ''; @@ -25,6 +26,8 @@ class FreshRSS_Context { 'starred' => false, 'feed' => false, 'category' => false, + 'tag' => false, + 'tags' => false, ); public static $next_get = 'a'; @@ -91,6 +94,14 @@ class FreshRSS_Context { } else { return 'c_' . self::$current_get['category']; } + } elseif (self::$current_get['tag']) { + if ($array) { + return array('t', self::$current_get['tag']); + } else { + return 't_' . self::$current_get['tag']; + } + } elseif (self::$current_get['tags']) { + return 'T'; } } @@ -117,6 +128,10 @@ class FreshRSS_Context { return self::$current_get['feed'] == $id; case 'c': return self::$current_get['category'] == $id; + case 't': + return self::$current_get['tag'] == $id; + case 'T': + return self::$current_get['tags'] || self::$current_get['tag']; default: return false; } @@ -130,6 +145,7 @@ class FreshRSS_Context { * - s * - f_<feed id> * - c_<category id> + * - t_<tag id> * * $name and $get_unread attributes are also updated as $next_get * Raise an exception if id or $get is invalid. @@ -140,7 +156,7 @@ class FreshRSS_Context { $nb_unread = 0; if (empty(self::$categories)) { - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); self::$categories = $catDAO->listCategories(); } @@ -166,12 +182,10 @@ class FreshRSS_Context { if ($feed === null) { $feedDAO = FreshRSS_Factory::createFeedDao(); $feed = $feedDAO->searchById($id); - if (!$feed) { throw new FreshRSS_Context_Exception('Invalid feed: ' . $id); } } - self::$current_get['feed'] = $id; self::$current_get['category'] = $feed->category(); self::$name = $feed->name(); @@ -182,19 +196,37 @@ class FreshRSS_Context { // We try to find the corresponding category. self::$current_get['category'] = $id; if (!isset(self::$categories[$id])) { - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); $cat = $catDAO->searchById($id); - if (!$cat) { throw new FreshRSS_Context_Exception('Invalid category: ' . $id); } } else { $cat = self::$categories[$id]; } - self::$name = $cat->name(); self::$get_unread = $cat->nbNotRead(); break; + case 't': + // We try to find the corresponding tag. + self::$current_get['tag'] = $id; + if (!isset(self::$tags[$id])) { + $tagDAO = FreshRSS_Factory::createTagDao(); + $tag = $tagDAO->searchById($id); + if (!$tag) { + throw new FreshRSS_Context_Exception('Invalid tag: ' . $id); + } + } else { + $tag = self::$tags[$id]; + } + self::$name = $tag->name(); + self::$get_unread = $tag->nbUnread(); + break; + case 'T': + self::$current_get['tags'] = true; + self::$name = _t('index.menu.tags'); + self::$get_unread = 0; + break; default: throw new FreshRSS_Context_Exception('Invalid getter: ' . $get); } @@ -211,7 +243,7 @@ class FreshRSS_Context { self::$next_get = $get; if (empty(self::$categories)) { - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); self::$categories = $catDAO->listCategories(); } diff --git a/app/Models/DatabaseDAO.php b/app/Models/DatabaseDAO.php index b8e5577e4..b331eccc3 100644 --- a/app/Models/DatabaseDAO.php +++ b/app/Models/DatabaseDAO.php @@ -4,6 +4,16 @@ * This class is used to test database is well-constructed. */ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { + + //MySQL error codes + const ER_BAD_FIELD_ERROR = '42S22'; + const ER_BAD_TABLE_ERROR = '42S02'; + const ER_TRUNCATED_WRONG_VALUE_FOR_FIELD = '1366'; + + //MySQL InnoDB maximum index length for UTF8MB4 + //https://dev.mysql.com/doc/refman/8.0/en/innodb-restrictions.html + const LENGTH_INDEX_UNICODE = 191; + public function tablesAreCorrect() { $sql = 'SHOW TABLES'; $stm = $this->bd->prepare($sql); @@ -14,6 +24,9 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { $this->prefix . 'category' => false, $this->prefix . 'feed' => false, $this->prefix . 'entry' => false, + $this->prefix . 'entrytmp' => false, + $this->prefix . 'tag' => false, + $this->prefix . 'entrytag' => false, ); foreach ($res as $value) { $tables[array_pop($value)] = true; @@ -43,7 +56,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { public function categoryIsCorrect() { return $this->checkTable('category', array( - 'id', 'name' + 'id', 'name', )); } @@ -51,14 +64,33 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { return $this->checkTable('feed', array( 'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate', 'priority', 'pathEntries', 'httpAuth', 'error', 'keep_history', 'ttl', 'attributes', - 'cache_nbEntries', 'cache_nbUnreads' + 'cache_nbEntries', 'cache_nbUnreads', )); } public function entryIsCorrect() { return $this->checkTable('entry', array( - 'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'is_read', - 'is_favorite', 'id_feed', 'tags' + 'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'lastSeen', 'hash', 'is_read', + 'is_favorite', 'id_feed', 'tags', + )); + } + + public function entrytmpIsCorrect() { + return $this->checkTable('entrytmp', array( + 'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'lastSeen', 'hash', 'is_read', + 'is_favorite', 'id_feed', 'tags', + )); + } + + public function tagIsCorrect() { + return $this->checkTable('tag', array( + 'id', 'name', 'attributes', + )); + } + + public function entrytagIsCorrect() { + return $this->checkTable('entrytag', array( + 'id_tag', 'id_entry', )); } @@ -97,28 +129,39 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { public function optimize() { $ok = true; - - $sql = 'OPTIMIZE TABLE `' . $this->prefix . 'entry`'; //MySQL - $stm = $this->bd->prepare($sql); - $ok &= $stm != false; - if ($stm) { - $ok &= $stm->execute(); - } - - $sql = 'OPTIMIZE TABLE `' . $this->prefix . 'feed`'; //MySQL - $stm = $this->bd->prepare($sql); - $ok &= $stm != false; - if ($stm) { - $ok &= $stm->execute(); + $tables = array('category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag'); + + foreach ($tables as $table) { + $sql = 'OPTIMIZE TABLE `' . $this->prefix . $table . '`'; //MySQL + $stm = $this->bd->prepare($sql); + $ok &= $stm != false; + if ($stm) { + $ok &= $stm->execute(); + } } + return $ok; + } - $sql = 'OPTIMIZE TABLE `' . $this->prefix . 'category`'; //MySQL - $stm = $this->bd->prepare($sql); - $ok &= $stm != false; - if ($stm) { - $ok &= $stm->execute(); + public function ensureCaseInsensitiveGuids() { + $ok = true; + $db = FreshRSS_Context::$system_conf->db; + if ($db['type'] === 'mysql') { + include_once(APP_PATH . '/SQL/install.sql.mysql.php'); + if (defined('SQL_UPDATE_GUID_LATIN1_BIN')) { //FreshRSS 1.12 + try { + $sql = sprintf(SQL_UPDATE_GUID_LATIN1_BIN, $this->prefix); + $stm = $this->bd->prepare($sql); + $ok = $stm->execute(); + } catch (Exception $e) { + $ok = false; + Minz_Log::error('FreshRSS_DatabaseDAO::ensureCaseInsensitiveGuids error: ' . $e->getMessage()); + } + } } - return $ok; } + + public function minorDbMaintenance() { + $this->ensureCaseInsensitiveGuids(); + } } diff --git a/app/Models/DatabaseDAOPGSQL.php b/app/Models/DatabaseDAOPGSQL.php index 1b3f7408d..8582b5719 100644 --- a/app/Models/DatabaseDAOPGSQL.php +++ b/app/Models/DatabaseDAOPGSQL.php @@ -3,7 +3,12 @@ /** * This class is used to test database is well-constructed. */ -class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO { +class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite { + + //PostgreSQL error codes + const UNDEFINED_COLUMN = '42703'; + const UNDEFINED_TABLE = '42P01'; + public function tablesAreCorrect() { $db = FreshRSS_Context::$system_conf->db; $dbowner = $db['user']; @@ -17,6 +22,9 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO { $this->prefix . 'category' => false, $this->prefix . 'feed' => false, $this->prefix . 'entry' => false, + $this->prefix . 'entrytmp' => false, + $this->prefix . 'tag' => false, + $this->prefix . 'entrytag' => false, ); foreach ($res as $value) { $tables[array_pop($value)] = true; @@ -53,28 +61,16 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO { public function optimize() { $ok = true; + $tables = array('category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag'); - $sql = 'VACUUM `' . $this->prefix . 'entry`'; - $stm = $this->bd->prepare($sql); - $ok &= $stm != false; - if ($stm) { - $ok &= $stm->execute(); - } - - $sql = 'VACUUM `' . $this->prefix . 'feed`'; - $stm = $this->bd->prepare($sql); - $ok &= $stm != false; - if ($stm) { - $ok &= $stm->execute(); + foreach ($tables as $table) { + $sql = 'VACUUM `' . $this->prefix . $table . '`'; + $stm = $this->bd->prepare($sql); + $ok &= $stm != false; + if ($stm) { + $ok &= $stm->execute(); + } } - - $sql = 'VACUUM `' . $this->prefix . 'category`'; - $stm = $this->bd->prepare($sql); - $ok &= $stm != false; - if ($stm) { - $ok &= $stm->execute(); - } - return $ok; } } diff --git a/app/Models/DatabaseDAOSQLite.php b/app/Models/DatabaseDAOSQLite.php index d3aedb3c0..a93a209b2 100644 --- a/app/Models/DatabaseDAOSQLite.php +++ b/app/Models/DatabaseDAOSQLite.php @@ -14,6 +14,9 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO { 'category' => false, 'feed' => false, 'entry' => false, + 'entrytmp' => false, + 'tag' => false, + 'entrytag' => false, ); foreach ($res as $value) { $tables[$value['name']] = true; @@ -32,8 +35,15 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO { public function entryIsCorrect() { return $this->checkTable('entry', array( - 'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'is_read', - 'is_favorite', 'id_feed', 'tags' + 'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'lastSeen', 'hash', 'is_read', + 'is_favorite', 'id_feed', 'tags', + )); + } + + public function entrytmpIsCorrect() { + return $this->checkTable('entrytmp', array( + 'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'lastSeen', 'hash', 'is_read', + 'is_favorite', 'id_feed', 'tags', )); } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index ccbad5724..985276734 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -10,7 +10,7 @@ class FreshRSS_Entry extends Minz_Model { private $id = 0; private $guid; private $title; - private $author; + private $authors; private $content; private $link; private $date; @@ -21,18 +21,17 @@ class FreshRSS_Entry extends Minz_Model { private $feed; private $tags; - public function __construct($feedId = '', $guid = '', $title = '', $author = '', $content = '', + public function __construct($feedId = '', $guid = '', $title = '', $authors = '', $content = '', $link = '', $pubdate = 0, $is_read = false, $is_favorite = false, $tags = '') { $this->_title($title); - $this->_author($author); + $this->_authors($authors); $this->_content($content); $this->_link($link); $this->_date($pubdate); $this->_isRead($is_read); $this->_isFavorite($is_favorite); $this->_feedId($feedId); - $tags = mb_strcut($tags, 0, 1023, 'UTF-8'); - $this->_tags(preg_split('/[\s#]/', $tags)); + $this->_tags($tags); $this->_guid($guid); } @@ -46,7 +45,15 @@ class FreshRSS_Entry extends Minz_Model { return $this->title; } public function author() { - return $this->author === null ? '' : $this->author; + //Deprecated + return $this->authors(true); + } + public function authors($asString = false) { + if ($asString) { + return $this->authors == null ? '' : ';' . implode('; ', $this->authors); + } else { + return $this->authors; + } } public function content() { return $this->content; @@ -86,9 +93,9 @@ class FreshRSS_Entry extends Minz_Model { return $this->feedId; } } - public function tags($inString = false) { - if ($inString) { - return empty($this->tags) ? '' : '#' . implode(' #', $this->tags); + public function tags($asString = false) { + if ($asString) { + return $this->tags == null ? '' : '#' . implode(' #', $this->tags); } else { return $this->tags; } @@ -97,7 +104,7 @@ class FreshRSS_Entry extends Minz_Model { public function hash() { if ($this->hash === null) { //Do not include $this->date because it may be automatically generated when lacking - $this->hash = md5($this->link . $this->title . $this->author . $this->content . $this->tags(true)); + $this->hash = md5($this->link . $this->title . $this->authors(true) . $this->content . $this->tags(true)); } return $this->hash; } @@ -124,11 +131,22 @@ class FreshRSS_Entry extends Minz_Model { } public function _title($value) { $this->hash = null; - $this->title = mb_strcut($value, 0, 255, 'UTF-8'); + $this->title = $value; } public function _author($value) { + //Deprecated + $this->_authors($value); + } + public function _authors($value) { $this->hash = null; - $this->author = mb_strcut($value, 0, 255, 'UTF-8'); + if (!is_array($value)) { + if (strpos($value, ';') !== false) { + $value = preg_split('/\s*[;]\s*/', $value, -1, PREG_SPLIT_NO_EMPTY); + } else { + $value = preg_split('/\s*[,]\s*/', $value, -1, PREG_SPLIT_NO_EMPTY); + } + } + $this->authors = $value; } public function _content($value) { $this->hash = null; @@ -162,15 +180,8 @@ class FreshRSS_Entry extends Minz_Model { public function _tags($value) { $this->hash = null; if (!is_array($value)) { - $value = array($value); - } - - foreach ($value as $key => $t) { - if (!$t) { - unset($value[$key]); - } + $value = preg_split('/\s*[#,]\s*/', $value, -1, PREG_SPLIT_NO_EMPTY); } - $this->tags = $value; } @@ -287,7 +298,7 @@ class FreshRSS_Entry extends Minz_Model { 'id' => $this->id(), 'guid' => $this->guid(), 'title' => $this->title(), - 'author' => $this->author(), + 'author' => $this->authors(true), 'content' => $this->content(), 'link' => $this->link(), 'date' => $this->date(true), diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index f0e164995..a01c2227b 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -18,6 +18,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return 'hex(' . $x . ')'; } + //TODO: Move the database auto-updates to DatabaseDAO protected function addColumn($name) { Minz_Log::warning('FreshRSS_EntryDAO::addColumn: ' . $name); $hasTransaction = false; @@ -56,6 +57,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { private $triedUpdateToUtf8mb4 = false; + //TODO: Move the database auto-updates to DatabaseDAO protected function updateToUtf8mb4() { if ($this->triedUpdateToUtf8mb4) { return false; @@ -65,7 +67,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { if ($db['type'] === 'mysql') { include_once(APP_PATH . '/SQL/install.sql.mysql.php'); if (defined('SQL_UPDATE_UTF8MB4')) { - Minz_Log::warning('Updating MySQL to UTF8MB4...'); + Minz_Log::warning('Updating MySQL to UTF8MB4...'); //v1.5.0 $hadTransaction = $this->bd->inTransaction(); if ($hadTransaction) { $this->bd->commit(); @@ -88,6 +90,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return false; } + //TODO: Move the database auto-updates to DatabaseDAO protected function createEntryTempTable() { $ok = false; $hadTransaction = $this->bd->inTransaction(); @@ -120,22 +123,28 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $ok; } + //TODO: Move the database auto-updates to DatabaseDAO protected function autoUpdateDb($errorInfo) { if (isset($errorInfo[0])) { - if ($errorInfo[0] === '42S22') { //ER_BAD_FIELD_ERROR + if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR) { //autoAddColumn foreach (array('lastSeen', 'hash') as $column) { if (stripos($errorInfo[2], $column) !== false) { return $this->addColumn($column); } } - } elseif ($errorInfo[0] === '42S02' && stripos($errorInfo[2], 'entrytmp') !== false) { //ER_BAD_TABLE_ERROR - return $this->createEntryTempTable(); //v1.7 + } elseif ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_TABLE_ERROR) { + if (stripos($errorInfo[2], 'tag') !== false) { + $tagDAO = FreshRSS_Factory::createTagDao(); + return $tagDAO->createTagTable(); //v1.12.0 + } elseif (stripos($errorInfo[2], 'entrytmp') !== false) { + return $this->createEntryTempTable(); //v1.7.0 + } } } if (isset($errorInfo[1])) { - if ($errorInfo[1] == '1366') { //ER_TRUNCATED_WRONG_VALUE_FOR_FIELD - return $this->updateToUtf8mb4(); + if ($errorInfo[1] == FreshRSS_DatabaseDAO::ER_TRUNCATED_WRONG_VALUE_FOR_FIELD) { + return $this->updateToUtf8mb4(); //v1.5.0 } } return false; @@ -560,11 +569,52 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return $affected; } + /** + * Mark all the articles in a tag as read. + * @param integer $id tag ID, or empty for targetting any tag + * @param integer $idMax max article ID + * @return integer affected rows + */ + public function markReadTag($id = '', $idMax = 0, $filters = null, $state = 0, $is_read = true) { + FreshRSS_UserDAO::touch(); + if ($idMax == 0) { + $idMax = time() . '000000'; + Minz_Log::debug('Calling markReadTag(0) is deprecated!'); + } + + $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'entrytag` et ON et.id_entry = e.id ' + . 'SET e.is_read = ? ' + . 'WHERE ' + . ($id == '' ? '' : 'et.id_tag = ? AND ') + . 'e.is_read <> ? AND e.id <= ?'; + $values = array($is_read ? 1 : 0); + if ($id != '') { + $values[] = $id; + } + $values[] = $is_read ? 1 : 0; + $values[] = $idMax; + + list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state); + + $stm = $this->bd->prepare($sql . $search); + if (!($stm && $stm->execute(array_merge($values, $searchValues)))) { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::error('SQL error markReadTag: ' . $info[2]); + return false; + } + $affected = $stm->rowCount(); + if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) { + return false; + } + return $affected; + } + public function cleanOldEntries($id_feed, $date_min, $keep = 15) { //Remember to call updateCachedValue($id_feed) or updateCachedValues() just after $sql = 'DELETE FROM `' . $this->prefix . 'entry` ' . 'WHERE id_feed=:id_feed AND id<=:id_max ' . 'AND is_favorite=0 ' //Do not remove favourites . 'AND `lastSeen` < (SELECT maxLastSeen FROM (SELECT (MAX(e3.`lastSeen`)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) ' //Do not remove the most newly seen articles, plus a few seconds of tolerance + . 'AND id NOT IN (SELECT id_entry FROM `' . $this->prefix . 'entrytag`) ' //Do not purge tagged entries . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=:id_feed ORDER BY id DESC LIMIT :keep) keep)'; //Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery' $stm = $this->bd->prepare($sql); @@ -770,24 +820,31 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $joinFeed = false; $values = array(); switch ($type) { - case 'a': + case 'a': //All PRIORITY_MAIN_STREAM $where .= 'f.priority > ' . FreshRSS_Feed::PRIORITY_NORMAL . ' '; break; - case 's': //Deprecated: use $state instead + case 'A': //All except PRIORITY_ARCHIVED + $where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' '; + break; + case 's': //Starred. Deprecated: use $state instead $where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' '; $where .= 'AND e.is_favorite=1 '; break; - case 'c': + case 'c': //Category $where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' '; $where .= 'AND f.category=? '; $values[] = intval($id); break; - case 'f': + case 'f': //Feed $where .= 'e.id_feed=? '; $values[] = intval($id); break; - case 'A': - $where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' '; + case 't': //Tag + $where .= 'et.id_tag=? '; + $values[] = intval($id); + break; + case 'T': //Any tag + $where .= '1=1 '; break; default: throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!'); @@ -796,8 +853,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state, $order, $firstId, $date_min); return array(array_merge($values, $searchValues), - 'SELECT e.id FROM `' . $this->prefix . 'entry` e ' + 'SELECT ' + . ($type === 'T' ? 'DISTINCT ' : '') + . 'e.id FROM `' . $this->prefix . 'entry` e ' . 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' + . ($type === 't' || $type === 'T' ? 'INNER JOIN `' . $this->prefix . 'entrytag` et ON et.id_entry = e.id ' : '') . 'WHERE ' . $where . $search . 'ORDER BY e.id ' . $order @@ -817,13 +877,22 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { . 'ORDER BY e0.id ' . $order; $stm = $this->bd->prepare($sql); - $stm->execute($values); - return $stm; + if ($stm && $stm->execute($values)) { + return $stm; + } else { + $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::error('SQL error listWhereRaw: ' . $info[2]); + return false; + } } public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) { $stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min); - return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC)); + if ($stm) { + return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC)); + } else { + return false; + } } public function listByIds($ids, $order = 'DESC') { @@ -923,7 +992,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $stm = $this->bd->prepare($sql); $stm->execute(); $res = $stm->fetchAll(PDO::FETCH_COLUMN, 0); - return $res[0]; + return isset($res[0]) ? $res[0] : 0; } public function countNotRead($minPriority = null) { $sql = 'SELECT COUNT(e.id) AS count FROM `' . $this->prefix . 'entry` e'; diff --git a/app/Models/EntryDAOPGSQL.php b/app/Models/EntryDAOPGSQL.php index f09fe8e75..aef258b6f 100644 --- a/app/Models/EntryDAOPGSQL.php +++ b/app/Models/EntryDAOPGSQL.php @@ -12,8 +12,13 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite { protected function autoUpdateDb($errorInfo) { if (isset($errorInfo[0])) { - if ($errorInfo[0] === '42P01' && stripos($errorInfo[2], 'entrytmp') !== false) { //undefined_table - return $this->createEntryTempTable(); + if ($errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_TABLE) { + if (stripos($errorInfo[2], 'tag') !== false) { + $tagDAO = FreshRSS_Factory::createTagDao(); + return $tagDAO->createTagTable(); //v1.12.0 + } elseif (stripos($errorInfo[2], 'entrytmp') !== false) { + return $this->createEntryTempTable(); //v1.7.0 + } } } return false; diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index 944de8470..f8cd14fe6 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -7,10 +7,17 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { } protected function autoUpdateDb($errorInfo) { + if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='tag'")) { + $showCreate = $tableInfo->fetchColumn(); + if (stripos($showCreate, 'tag') === false) { + $tagDAO = FreshRSS_Factory::createTagDao(); + return $tagDAO->createTagTable(); //v1.12.0 + } + } if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entrytmp'")) { $showCreate = $tableInfo->fetchColumn(); if (stripos($showCreate, 'entrytmp') === false) { - return $this->createEntryTempTable(); + return $this->createEntryTempTable(); //v1.7.0 } } if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='entry'")) { @@ -228,4 +235,43 @@ DROP TABLE IF EXISTS `tmp`; } return $affected; } + + /** + * Mark all the articles in a tag as read. + * @param integer $id tag ID, or empty for targetting any tag + * @param integer $idMax max article ID + * @return integer affected rows + */ + public function markReadTag($id = '', $idMax = 0, $filters = null, $state = 0, $is_read = true) { + FreshRSS_UserDAO::touch(); + if ($idMax == 0) { + $idMax = time() . '000000'; + Minz_Log::debug('Calling markReadTag(0) is deprecated!'); + } + + $sql = 'UPDATE `' . $this->prefix . 'entry` e ' + . 'SET e.is_read = ? ' + . 'WHERE e.is_read <> ? AND e.id <= ? AND ' + . 'e.id IN (SELECT et.id_entry FROM `' . $this->prefix . 'entrytag` et ' + . ($id == '' ? '' : 'WHERE et.id = ?') + . ')'; + $values = array($is_read ? 1 : 0, $is_read ? 1 : 0, $idMax); + if ($id != '') { + $values[] = $id; + } + + list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state); + + $stm = $this->bd->prepare($sql . $search); + if (!($stm && $stm->execute(array_merge($values, $searchValues)))) { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::error('SQL error markReadTag: ' . $info[2]); + return false; + } + $affected = $stm->rowCount(); + if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) { + return false; + } + return $affected; + } } diff --git a/app/Models/Factory.php b/app/Models/Factory.php index 764987c46..1accb491c 100644 --- a/app/Models/Factory.php +++ b/app/Models/Factory.php @@ -2,6 +2,10 @@ class FreshRSS_Factory { + public static function createCategoryDao($username = null) { + return new FreshRSS_CategoryDAO($username); + } + public static function createFeedDao($username = null) { $conf = Minz_Configuration::get('system'); switch ($conf->db['type']) { @@ -24,6 +28,18 @@ class FreshRSS_Factory { } } + public static function createTagDao($username = null) { + $conf = Minz_Configuration::get('system'); + switch ($conf->db['type']) { + case 'sqlite': + return new FreshRSS_TagDAOSQLite($username); + case 'pgsql': + return new FreshRSS_TagDAOPGSQL($username); + default: + return new FreshRSS_TagDAO($username); + } + } + public static function createStatsDAO($username = null) { $conf = Minz_Configuration::get('system'); switch ($conf->db['type']) { diff --git a/app/Models/Feed.php b/app/Models/Feed.php index ed381a867..e1dd2990d 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -286,6 +286,10 @@ class FreshRSS_Feed extends Minz_Model { if (!$loadDetails) { //Only activates auto-discovery when adding a new feed $feed->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE); } + if ($this->attributes('clear_cache')) { + // Do not use `$simplePie->enable_cache(false);` as it would prevent caching in multiuser context + $this->clearCache(); + } Minz_ExtensionManager::callHook('simplepie_before_init', $feed, $this); $mtime = $feed->init(); @@ -345,13 +349,21 @@ class FreshRSS_Feed extends Minz_Model { $link = $item->get_permalink(); $date = @strtotime($item->get_date()); - // gestion des tags (catégorie == tag) - $tags_tmp = $item->get_categories(); + //Tag processing (tag == category) + $categories = $item->get_categories(); $tags = array(); - if ($tags_tmp !== null) { - foreach ($tags_tmp as $tag) { - $tags[] = html_only_entity_decode($tag->get_label()); + if (is_array($categories)) { + foreach ($categories as $category) { + $text = html_only_entity_decode($category->get_label()); + //Some feeds use a single category with comma-separated tags + $labels = explode(',', $text); + if (is_array($labels)) { + foreach ($labels as $label) { + $tags[] = trim($label); + } + } } + $tags = array_unique($tags); } $content = html_only_entity_decode($item->get_content()); @@ -412,7 +424,7 @@ class FreshRSS_Feed extends Minz_Model { $author_names = ''; if (is_array($authors)) { foreach ($authors as $author) { - $author_names .= html_only_entity_decode(strip_tags($author->name == '' ? $author->email : $author->name)) . ', '; + $author_names .= html_only_entity_decode(strip_tags($author->name == '' ? $author->email : $author->name)) . '; '; } } $author_names = substr($author_names, 0, -2); @@ -457,8 +469,16 @@ class FreshRSS_Feed extends Minz_Model { $this->entries = $entries; } + protected function cacheFilename() { + return CACHE_PATH . '/' . md5($this->url) . '.spc'; + } + + public function clearCache() { + return @unlink($this->cacheFilename()); + } + public function cacheModifiedTime() { - return @filemtime(CACHE_PATH . '/' . md5($this->url) . '.spc'); + return @filemtime($this->cacheFilename()); } public function lock() { diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 285f17193..e579f5881 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -17,7 +17,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { protected function autoUpdateDb($errorInfo) { if (isset($errorInfo[0])) { - if ($errorInfo[0] === '42S22' || $errorInfo[0] === '42703') { //ER_BAD_FIELD_ERROR (Mysql), undefined_column (PostgreSQL) + if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) { foreach (array('attributes') as $column) { if (stripos($errorInfo[2], $column) !== false) { return $this->addColumn($column); @@ -55,7 +55,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { $values = array( substr($valuesTmp['url'], 0, 511), $valuesTmp['category'], - mb_strcut($valuesTmp['name'], 0, 255, 'UTF-8'), + mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'), substr($valuesTmp['website'], 0, 255), mb_strcut($valuesTmp['description'], 0, 1023, 'UTF-8'), $valuesTmp['lastUpdate'], @@ -109,6 +109,9 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } public function updateFeed($id, $valuesTmp) { + if (isset($valuesTmp['name'])) { + $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'); + } if (isset($valuesTmp['url'])) { $valuesTmp['url'] = safe_ascii($valuesTmp['url']); } @@ -180,7 +183,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } public function changeCategory($idOldCat, $idNewCat) { - $catDAO = new FreshRSS_CategoryDAO(); + $catDAO = FreshRSS_Factory::createCategoryDao(); $newCat = $catDAO->searchById($idNewCat); if (!$newCat) { $newCat = $catDAO->getDefault(); diff --git a/app/Models/Search.php b/app/Models/Search.php index 5cc7f8e8d..c52e391fa 100644 --- a/app/Models/Search.php +++ b/app/Models/Search.php @@ -40,7 +40,7 @@ class FreshRSS_Search { $input = $this->parseNotIntitleSearch($input); $input = $this->parseNotAuthorSearch($input); $input = $this->parseNotInurlSearch($input); - $input = $this->parseNotTagsSeach($input); + $input = $this->parseNotTagsSearch($input); $input = $this->parsePubdateSearch($input); $input = $this->parseDateSearch($input); @@ -48,7 +48,7 @@ class FreshRSS_Search { $input = $this->parseIntitleSearch($input); $input = $this->parseAuthorSearch($input); $input = $this->parseInurlSearch($input); - $input = $this->parseTagsSeach($input); + $input = $this->parseTagsSearch($input); $input = $this->parseNotSearch($input); $input = $this->parseSearch($input); @@ -117,6 +117,17 @@ class FreshRSS_Search { return is_array($anArray) ? array_filter($anArray, function($value) { return $value !== ''; }) : array(); } + private static function decodeSpaces($value) { + if (is_array($value)) { + for ($i = count($value) - 1; $i >= 0; $i--) { + $value[$i] = self::decodeSpaces($value[$i]); + } + } else { + $value = trim(str_replace('+', ' ', $value)); + } + return $value; + } + /** * Parse the search string to find intitle keyword and the search related * to it. @@ -130,11 +141,12 @@ class FreshRSS_Search { $this->intitle = $matches['search']; $input = str_replace($matches[0], '', $input); } - if (preg_match_all('/\bintitle:(?P<search>\w*)/', $input, $matches)) { + if (preg_match_all('/\bintitle:(?P<search>[^\s"]*)/', $input, $matches)) { $this->intitle = array_merge($this->intitle ? $this->intitle : array(), $matches['search']); $input = str_replace($matches[0], '', $input); } $this->intitle = self::removeEmptyValues($this->intitle); + $this->intitle = self::decodeSpaces($this->intitle); return $input; } @@ -143,11 +155,12 @@ class FreshRSS_Search { $this->not_intitle = $matches['search']; $input = str_replace($matches[0], '', $input); } - if (preg_match_all('/[!-]intitle:(?P<search>\w*)/', $input, $matches)) { + if (preg_match_all('/[!-]intitle:(?P<search>[^\s"]*)/', $input, $matches)) { $this->not_intitle = array_merge($this->not_intitle ? $this->not_intitle : array(), $matches['search']); $input = str_replace($matches[0], '', $input); } $this->not_intitle = self::removeEmptyValues($this->not_intitle); + $this->not_intitle = self::decodeSpaces($this->not_intitle); return $input; } @@ -166,11 +179,12 @@ class FreshRSS_Search { $this->author = $matches['search']; $input = str_replace($matches[0], '', $input); } - if (preg_match_all('/\bauthor:(?P<search>\w*)/', $input, $matches)) { + if (preg_match_all('/\bauthor:(?P<search>[^\s"]*)/', $input, $matches)) { $this->author = array_merge($this->author ? $this->author : array(), $matches['search']); $input = str_replace($matches[0], '', $input); } $this->author = self::removeEmptyValues($this->author); + $this->author = self::decodeSpaces($this->author); return $input; } @@ -179,11 +193,12 @@ class FreshRSS_Search { $this->not_author = $matches['search']; $input = str_replace($matches[0], '', $input); } - if (preg_match_all('/[!-]author:(?P<search>\w*)/', $input, $matches)) { + if (preg_match_all('/[!-]author:(?P<search>[^\s"]*)/', $input, $matches)) { $this->not_author = array_merge($this->not_author ? $this->not_author : array(), $matches['search']); $input = str_replace($matches[0], '', $input); } $this->not_author = self::removeEmptyValues($this->not_author); + $this->not_author = self::decodeSpaces($this->not_author); return $input; } @@ -201,6 +216,7 @@ class FreshRSS_Search { $input = str_replace($matches[0], '', $input); } $this->inurl = self::removeEmptyValues($this->inurl); + $this->inurl = self::decodeSpaces($this->inurl); return $input; } @@ -210,6 +226,7 @@ class FreshRSS_Search { $input = str_replace($matches[0], '', $input); } $this->not_inurl = self::removeEmptyValues($this->not_inurl); + $this->not_inurl = self::decodeSpaces($this->not_inurl); return $input; } @@ -259,21 +276,23 @@ class FreshRSS_Search { * @param string $input * @return string */ - private function parseTagsSeach($input) { + private function parseTagsSearch($input) { if (preg_match_all('/#(?P<search>[^\s]+)/', $input, $matches)) { $this->tags = $matches['search']; $input = str_replace($matches[0], '', $input); } $this->tags = self::removeEmptyValues($this->tags); + $this->tags = self::decodeSpaces($this->tags); return $input; } - private function parseNotTagsSeach($input) { + private function parseNotTagsSearch($input) { if (preg_match_all('/[!-]#(?P<search>[^\s]+)/', $input, $matches)) { $this->not_tags = $matches['search']; $input = str_replace($matches[0], '', $input); } $this->not_tags = self::removeEmptyValues($this->not_tags); + $this->not_tags = self::decodeSpaces($this->not_tags); return $input; } @@ -303,6 +322,7 @@ class FreshRSS_Search { } else { $this->search = explode(' ', $input); } + $this->search = self::decodeSpaces($this->search); } private function parseNotSearch($input) { @@ -322,6 +342,7 @@ class FreshRSS_Search { $input = str_replace($matches[0], '', $input); } $this->not_search = self::removeEmptyValues($this->not_search); + $this->not_search = self::decodeSpaces($this->not_search); return $input; } diff --git a/app/Models/Tag.php b/app/Models/Tag.php new file mode 100644 index 000000000..3eb989cc1 --- /dev/null +++ b/app/Models/Tag.php @@ -0,0 +1,76 @@ +<?php + +class FreshRSS_Tag extends Minz_Model { + private $id = 0; + private $name; + private $attributes = array(); + private $nbEntries = -1; + private $nbUnread = -1; + + public function __construct($name = '') { + $this->_name($name); + } + + public function id() { + return $this->id; + } + + public function _id($value) { + $this->id = (int)$value; + } + + public function name() { + return $this->name; + } + + public function _name($value) { + $this->name = trim($value); + } + + public function attributes($key = '') { + if ($key == '') { + return $this->attributes; + } else { + return isset($this->attributes[$key]) ? $this->attributes[$key] : null; + } + } + + public function _attributes($key, $value) { + if ($key == '') { + if (is_string($value)) { + $value = json_decode($value, true); + } + if (is_array($value)) { + $this->attributes = $value; + } + } elseif ($value === null) { + unset($this->attributes[$key]); + } else { + $this->attributes[$key] = $value; + } + } + + public function nbEntries() { + if ($this->nbEntries < 0) { + $tagDAO = FreshRSS_Factory::createTagDao(); + $this->nbEntries = $tagDAO->countEntries($this->id()); + } + return $this->nbFeed; + } + + public function _nbEntries($value) { + $this->nbEntries = (int)$value; + } + + public function nbUnread() { + if ($this->nbUnread < 0) { + $tagDAO = FreshRSS_Factory::createTagDao(); + $this->nbUnread = $tagDAO->countNotRead($this->id()); + } + return $this->nbUnread; + } + + public function _nbUnread($value) { + $this->nbUnread = (int)$value; + } +} diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php new file mode 100644 index 000000000..1b59c8971 --- /dev/null +++ b/app/Models/TagDAO.php @@ -0,0 +1,315 @@ +<?php + +class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable { + + public function sqlIgnore() { + return 'IGNORE'; + } + + public function createTagTable() { + $ok = false; + $hadTransaction = $this->bd->inTransaction(); + if ($hadTransaction) { + $this->bd->commit(); + } + try { + $db = FreshRSS_Context::$system_conf->db; + require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); + + Minz_Log::warning('SQL ALTER GUID case sensitivity...'); + $databaseDAO = FreshRSS_Factory::createDatabaseDAO(); + $databaseDAO->ensureCaseInsensitiveGuids(); + + Minz_Log::warning('SQL CREATE TABLE tag...'); + if (defined('SQL_CREATE_TABLE_TAGS')) { + $sql = sprintf(SQL_CREATE_TABLE_TAGS, $this->prefix); + $stm = $this->bd->prepare($sql); + $ok = $stm && $stm->execute(); + } else { + global $SQL_CREATE_TABLE_TAGS; + $ok = !empty($SQL_CREATE_TABLE_TAGS); + foreach ($SQL_CREATE_TABLE_TAGS as $instruction) { + $sql = sprintf($instruction, $this->prefix); + $stm = $this->bd->prepare($sql); + $ok &= $stm && $stm->execute(); + } + } + } catch (Exception $e) { + Minz_Log::error('FreshRSS_EntryDAO::createTagTable error: ' . $e->getMessage()); + } + if ($hadTransaction) { + $this->bd->beginTransaction(); + } + return $ok; + } + + protected function autoUpdateDb($errorInfo) { + if (isset($errorInfo[0])) { + if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_TABLE_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_TABLE) { + if (stripos($errorInfo[2], 'tag') !== false) { + return $this->createTagTable(); //v1.12.0 + } + } + } + return false; + } + + public function addTag($valuesTmp) { + $sql = 'INSERT INTO `' . $this->prefix . 'tag`(name, attributes) ' + . 'SELECT * FROM (SELECT TRIM(?), TRIM(?)) t2 ' //TRIM() to provide a type hint as text for PostgreSQL + . 'WHERE NOT EXISTS (SELECT 1 FROM `' . $this->prefix . 'category` WHERE name = TRIM(?))'; //No category of the same name + $stm = $this->bd->prepare($sql); + + $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8'); + $values = array( + $valuesTmp['name'], + isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '', + $valuesTmp['name'], + ); + + if ($stm && $stm->execute($values)) { + return $this->bd->lastInsertId('"' . $this->prefix . 'tag_id_seq"'); + } else { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::error('SQL error addTag: ' . $info[2]); + return false; + } + } + + public function addTagObject($tag) { + $tag = $this->searchByName($tag->name()); + if (!$tag) { + $values = array( + 'name' => $tag->name(), + 'attributes' => $tag->attributes(), + ); + return $this->addTag($values); + } + return $tag->id(); + } + + public function updateTag($id, $valuesTmp) { + $sql = 'UPDATE `' . $this->prefix . 'tag` SET name=?, attributes=? WHERE id=? ' + . 'AND NOT EXISTS (SELECT 1 FROM `' . $this->prefix . 'category` WHERE name = ?)'; //No category of the same name + $stm = $this->bd->prepare($sql); + + $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8'); + $values = array( + $valuesTmp['name'], + isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '', + $id, + $valuesTmp['name'], + ); + + if ($stm && $stm->execute($values)) { + return $stm->rowCount(); + } else { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::error('SQL error updateTag: ' . $info[2]); + return false; + } + } + + public function updateTagAttribute($tag, $key, $value) { + if ($tag instanceof FreshRSS_Tag) { + $tag->_attributes($key, $value); + return $this->updateFeed( + $tag->id(), + array('attributes' => $feed->attributes()) + ); + } + return false; + } + + public function deleteTag($id) { + if ($id <= 0) { + return false; + } + $sql = 'DELETE FROM `' . $this->prefix . 'tag` WHERE id=?'; + $stm = $this->bd->prepare($sql); + + $values = array($id); + + if ($stm && $stm->execute($values)) { + return $stm->rowCount(); + } else { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::error('SQL error deleteTag: ' . $info[2]); + return false; + } + } + + public function searchById($id) { + $sql = 'SELECT * FROM `' . $this->prefix . 'tag` WHERE id=?'; + $stm = $this->bd->prepare($sql); + $values = array($id); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + $tag = self::daoToTag($res); + return isset($tag[0]) ? $tag[0] : null; + } + + public function searchByName($name) { + $sql = 'SELECT * FROM `' . $this->prefix . 'tag` WHERE name=?'; + $stm = $this->bd->prepare($sql); + $values = array($name); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + $tag = self::daoToTag($res); + return isset($tag[0]) ? $tag[0] : null; + } + + public function listTags($precounts = false) { + if ($precounts) { + $sql = 'SELECT t.id, t.name, count(e.id) AS unreads ' + . 'FROM `' . $this->prefix . 'tag` t ' + . 'LEFT OUTER JOIN `' . $this->prefix . 'entrytag` et ON et.id_tag = t.id ' + . 'LEFT OUTER JOIN `' . $this->prefix . 'entry` e ON et.id_entry = e.id AND e.is_read = 0 ' + . 'GROUP BY t.id ' + . 'ORDER BY t.name'; + } else { + $sql = 'SELECT * FROM `' . $this->prefix . 'tag` ORDER BY name'; + } + + $stm = $this->bd->prepare($sql); + if ($stm && $stm->execute()) { + return self::daoToTag($stm->fetchAll(PDO::FETCH_ASSOC)); + } else { + $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); + if ($this->autoUpdateDb($info)) { + return $this->listTags($precounts); + } + Minz_Log::error('SQL error listTags: ' . $info[2]); + return false; + } + } + + public function count() { + $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'tag`'; + $stm = $this->bd->prepare($sql); + $stm->execute(); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + return $res[0]['count']; + } + + public function countEntries($id) { + $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entrytag` WHERE id_tag=?'; + $stm = $this->bd->prepare($sql); + $values = array($id); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + return $res[0]['count']; + } + + public function countNotRead($id) { + $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entrytag` et ' + . 'INNER JOIN `' . $this->prefix . 'entry` e ON et.id_entry=e.id ' + . 'WHERE et.id_tag=? AND e.is_read=0'; + $stm = $this->bd->prepare($sql); + $values = array($id); + $stm->execute($values); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + return $res[0]['count']; + } + + public function tagEntry($id_tag, $id_entry, $checked = true) { + if ($checked) { + $sql = 'INSERT ' . $this->sqlIgnore() . ' INTO `' . $this->prefix . 'entrytag`(id_tag, id_entry) VALUES(?, ?)'; + } else { + $sql = 'DELETE FROM `' . $this->prefix . 'entrytag` WHERE id_tag=? AND id_entry=?'; + } + $stm = $this->bd->prepare($sql); + $values = array($id_tag, $id_entry); + + if ($stm && $stm->execute($values)) { + return true; + } else { + $info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo(); + Minz_Log::error('SQL error tagEntry: ' . $info[2]); + return false; + } + } + + public function getTagsForEntry($id_entry) { + $sql = 'SELECT t.id, t.name, et.id_entry IS NOT NULL as checked ' + . 'FROM `' . $this->prefix . 'tag` t ' + . 'LEFT OUTER JOIN `' . $this->prefix . 'entrytag` et ON et.id_tag = t.id AND et.id_entry=? ' + . 'ORDER BY t.name'; + + $stm = $this->bd->prepare($sql); + $values = array($id_entry); + + if ($stm && $stm->execute($values)) { + $lines = $stm->fetchAll(PDO::FETCH_ASSOC); + for ($i = count($lines) - 1; $i >= 0; $i--) { + $lines[$i]['id'] = intval($lines[$i]['id']); + $lines[$i]['checked'] = !empty($lines[$i]['checked']); + } + return $lines; + } else { + $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); + if ($this->autoUpdateDb($info)) { + return $this->getTagsForEntry($id_entry); + } + Minz_Log::error('SQL error getTagsForEntry: ' . $info[2]); + return false; + } + } + + //For API + public function getEntryIdsTagNames($entries) { + $sql = 'SELECT et.id_entry, t.name ' + . 'FROM `' . $this->prefix . 'tag` t ' + . 'INNER JOIN `' . $this->prefix . 'entrytag` et ON et.id_tag = t.id'; + + $values = array(); + if (is_array($entries) && count($entries) > 0) { + $sql .= ' AND et.id_entry IN (' . str_repeat('?,', count($entries) - 1). '?)'; + foreach ($entries as $entry) { + $values[] = $entry->id(); + } + } + $stm = $this->bd->prepare($sql); + + if ($stm && $stm->execute($values)) { + $result = array(); + foreach ($stm->fetchAll(PDO::FETCH_ASSOC) as $line) { + $entryId = 'e_' . $line['id_entry']; + $tagName = $line['name']; + if (empty($result[$entryId])) { + $result[$entryId] = array(); + } + $result[$entryId][] = $tagName; + } + return $result; + } else { + $info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo(); + if ($this->autoUpdateDb($info)) { + return $this->getTagNamesEntryIds($id_entry); + } + Minz_Log::error('SQL error getTagNamesEntryIds: ' . $info[2]); + return false; + } + } + + public static function daoToTag($listDAO) { + $list = array(); + if (!is_array($listDAO)) { + $listDAO = array($listDAO); + } + foreach ($listDAO as $key => $dao) { + $tag = new FreshRSS_Tag( + $dao['name'] + ); + $tag->_id($dao['id']); + if (!empty($dao['attributes'])) { + $tag->_attributes('', $dao['attributes']); + } + if (isset($dao['unreads'])) { + $tag->_nbUnread($dao['unreads']); + } + $list[$key] = $tag; + } + return $list; + } +} diff --git a/app/Models/TagDAOPGSQL.php b/app/Models/TagDAOPGSQL.php new file mode 100644 index 000000000..56a28e294 --- /dev/null +++ b/app/Models/TagDAOPGSQL.php @@ -0,0 +1,9 @@ +<?php + +class FreshRSS_TagDAOPGSQL extends FreshRSS_TagDAO { + + public function sqlIgnore() { + return ''; //TODO + } + +} diff --git a/app/Models/TagDAOSQLite.php b/app/Models/TagDAOSQLite.php new file mode 100644 index 000000000..b1deb6c65 --- /dev/null +++ b/app/Models/TagDAOSQLite.php @@ -0,0 +1,19 @@ +<?php + +class FreshRSS_TagDAOSQLite extends FreshRSS_TagDAO { + + public function sqlIgnore() { + return 'OR IGNORE'; + } + + protected function autoUpdateDb($errorInfo) { + if ($tableInfo = $this->bd->query("SELECT sql FROM sqlite_master where name='tag'")) { + $showCreate = $tableInfo->fetchColumn(); + if (stripos($showCreate, 'tag') === false) { + return $this->createTagTable(); //v1.12.0 + } + } + return false; + } + +} diff --git a/app/Models/Themes.php b/app/Models/Themes.php index 8920fbf7e..235269e39 100644 --- a/app/Models/Themes.php +++ b/app/Models/Themes.php @@ -68,7 +68,7 @@ class FreshRSS_Themes extends Minz_Model { return $infos; } - public static function icon($name, $urlOnly = false) { + public static function alt($name) { static $alts = array( 'add' => '✚', 'all' => '☰', @@ -84,6 +84,7 @@ class FreshRSS_Themes extends Minz_Model { 'icon' => '⊚', 'import' => '⤓', 'key' => '⚿', + 'label' => '🏷️', 'link' => '↗', 'login' => '🔒', 'logout' => '🔓', @@ -104,13 +105,18 @@ class FreshRSS_Themes extends Minz_Model { 'view-global' => '☷', 'view-reader' => '☕', ); - if (!isset($alts[$name])) { + return isset($name) ? $alts[$name] : ''; + } + + public static function icon($name, $urlOnly = false, $altOnly = false) { + $alt = self::alt($name); + if ($alt == '') { return ''; } $url = $name . '.svg'; $url = isset(self::$themeIcons[$url]) ? (self::$themeIconsUrl . $url) : (self::$defaultIconsUrl . $url); - return $urlOnly ? Minz_Url::display($url) : '<img class="icon" src="' . Minz_Url::display($url) . '" alt="' . $alts[$name] . '" />'; + return $urlOnly ? Minz_Url::display($url) : '<img class="icon" src="' . Minz_Url::display($url) . '" alt="' . $alt . '" />'; } } diff --git a/app/Models/UserDAO.php b/app/Models/UserDAO.php index c921d54c9..5fb46c947 100644 --- a/app/Models/UserDAO.php +++ b/app/Models/UserDAO.php @@ -14,14 +14,13 @@ class FreshRSS_UserDAO extends Minz_ModelPdo { $ok = false; $bd_prefix_user = $db['prefix'] . $username . '_'; if (defined('SQL_CREATE_TABLES')) { //E.g. MySQL - $sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP, $bd_prefix_user, _t('gen.short.default_category')); + $sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP . SQL_CREATE_TABLE_TAGS, $bd_prefix_user, _t('gen.short.default_category')); $stm = $userPDO->bd->prepare($sql); $ok = $stm && $stm->execute(); } else { //E.g. SQLite - global $SQL_CREATE_TABLES; - global $SQL_CREATE_TABLE_ENTRYTMP; + global $SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_CREATE_TABLE_TAGS; if (is_array($SQL_CREATE_TABLES)) { - $instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP); + $instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_CREATE_TABLE_TAGS); $ok = !empty($instructions); foreach ($instructions as $instruction) { $sql = sprintf($instruction, $bd_prefix_user, _t('gen.short.default_category')); diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php index ef94fdaf6..f607084f8 100644 --- a/app/Models/UserQuery.php +++ b/app/Models/UserQuery.php @@ -19,15 +19,17 @@ class FreshRSS_UserQuery { private $url; private $feed_dao; private $category_dao; + private $tag_dao; /** * @param array $query * @param FreshRSS_Searchable $feed_dao * @param FreshRSS_Searchable $category_dao */ - public function __construct($query, FreshRSS_Searchable $feed_dao = null, FreshRSS_Searchable $category_dao = null) { + public function __construct($query, FreshRSS_Searchable $feed_dao = null, FreshRSS_Searchable $category_dao = null, FreshRSS_Searchable $tag_dao = null) { $this->category_dao = $category_dao; $this->feed_dao = $feed_dao; + $this->tag_dao = $tag_dao; if (isset($query['get'])) { $this->parseGet($query['get']); } @@ -88,6 +90,9 @@ class FreshRSS_UserQuery { case 's': $this->parseFavorite(); break; + case 't': + $this->parseTag($matches['id']); + break; } } } @@ -139,6 +144,25 @@ class FreshRSS_UserQuery { } /** + * Parse the query string when it is a "tag" query + * + * @param integer $id + * @throws FreshRSS_DAO_Exception + */ + private function parseTag($id) { + if ($this->tag_dao == null) { + throw new FreshRSS_DAO_Exception('Tag DAO is not loaded in UserQuery'); + } + $category = $this->category_dao->searchById($id); + if ($tag) { + $this->get_name = $tag->name(); + } else { + $this->deprecated = true; + } + $this->get_type = 'tag'; + } + + /** * Parse the query string when it is a "favorite" query */ private function parseFavorite() { diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php index 747a0a6b3..b3353ac95 100644 --- a/app/SQL/install.sql.mysql.php +++ b/app/SQL/install.sql.mysql.php @@ -1,10 +1,10 @@ <?php -define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); +define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS `%1$s` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); define('SQL_CREATE_TABLES', ' CREATE TABLE IF NOT EXISTS `%1$scategory` ( `id` SMALLINT NOT NULL AUTO_INCREMENT, -- v0.7 - `name` varchar(191) NOT NULL, + `name` VARCHAR(' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') NOT NULL, -- Max index length for Unicode is 191 characters (767 bytes) PRIMARY KEY (`id`), UNIQUE KEY (`name`) -- v0.7 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci @@ -12,21 +12,21 @@ ENGINE = INNODB; CREATE TABLE IF NOT EXISTS `%1$sfeed` ( `id` SMALLINT NOT NULL AUTO_INCREMENT, -- v0.7 - `url` varchar(511) CHARACTER SET latin1 NOT NULL, + `url` VARCHAR(511) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, `category` SMALLINT DEFAULT 0, -- v0.7 - `name` varchar(191) NOT NULL, - `website` varchar(255) CHARACTER SET latin1, - `description` text, - `lastUpdate` int(11) DEFAULT 0, -- Until year 2038 - `priority` tinyint(2) NOT NULL DEFAULT 10, - `pathEntries` varchar(511) DEFAULT NULL, - `httpAuth` varchar(511) DEFAULT NULL, - `error` boolean DEFAULT 0, + `name` VARCHAR(' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') NOT NULL, + `website` VARCHAR(255) CHARACTER SET latin1 COLLATE latin1_bin, + `description` TEXT, + `lastUpdate` INT(11) DEFAULT 0, -- Until year 2038 + `priority` TINYINT(2) NOT NULL DEFAULT 10, + `pathEntries` VARCHAR(511) DEFAULT NULL, + `httpAuth` VARCHAR(511) DEFAULT NULL, + `error` BOOLEAN DEFAULT 0, `keep_history` MEDIUMINT NOT NULL DEFAULT -2, -- v0.7 `ttl` INT NOT NULL DEFAULT 0, -- v0.7.3 `attributes` TEXT, -- v1.11.0 - `cache_nbEntries` int DEFAULT 0, -- v0.7 - `cache_nbUnreads` int DEFAULT 0, -- v0.7 + `cache_nbEntries` INT DEFAULT 0, -- v0.7 + `cache_nbUnreads` INT DEFAULT 0, -- v0.7 PRIMARY KEY (`id`), FOREIGN KEY (`category`) REFERENCES `%1$scategory`(`id`) ON DELETE SET NULL ON UPDATE CASCADE, UNIQUE KEY (`url`), -- v0.7 @@ -37,19 +37,19 @@ CREATE TABLE IF NOT EXISTS `%1$sfeed` ( ENGINE = INNODB; CREATE TABLE IF NOT EXISTS `%1$sentry` ( - `id` bigint NOT NULL, -- v0.7 - `guid` varchar(760) CHARACTER SET latin1 NOT NULL, -- Maximum for UNIQUE is 767B - `title` varchar(255) NOT NULL, - `author` varchar(255), - `content_bin` blob, -- v0.7 - `link` varchar(1023) CHARACTER SET latin1 NOT NULL, - `date` int(11), -- Until year 2038 + `id` BIGINT NOT NULL, -- v0.7 + `guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, -- Maximum for UNIQUE is 767B + `title` VARCHAR(255) NOT NULL, + `author` VARCHAR(255), + `content_bin` BLOB, -- v0.7 + `link` VARCHAR(1023) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `date` INT(11), -- Until year 2038 `lastSeen` INT(11) DEFAULT 0, -- v1.1.1, Until year 2038 `hash` BINARY(16), -- v1.1.1 - `is_read` boolean NOT NULL DEFAULT 0, - `is_favorite` boolean NOT NULL DEFAULT 0, + `is_read` BOOLEAN NOT NULL DEFAULT 0, + `is_favorite` BOOLEAN NOT NULL DEFAULT 0, `id_feed` SMALLINT, -- v0.7 - `tags` varchar(1023), + `tags` VARCHAR(1023), PRIMARY KEY (`id`), FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE KEY (`id_feed`,`guid`), -- v0.7 @@ -65,19 +65,19 @@ INSERT IGNORE INTO `%1$scategory` (id, name) VALUES(1, "%2$s"); define('SQL_CREATE_TABLE_ENTRYTMP', ' CREATE TABLE IF NOT EXISTS `%1$sentrytmp` ( -- v1.7 - `id` bigint NOT NULL, - `guid` varchar(760) CHARACTER SET latin1 NOT NULL, - `title` varchar(255) NOT NULL, - `author` varchar(255), - `content_bin` blob, - `link` varchar(1023) CHARACTER SET latin1 NOT NULL, - `date` int(11), + `id` BIGINT NOT NULL, + `guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `title` VARCHAR(255) NOT NULL, + `author` VARCHAR(255), + `content_bin` BLOB, + `link` VARCHAR(1023) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `date` INT(11), `lastSeen` INT(11) DEFAULT 0, `hash` BINARY(16), - `is_read` boolean NOT NULL DEFAULT 0, - `is_favorite` boolean NOT NULL DEFAULT 0, + `is_read` BOOLEAN NOT NULL DEFAULT 0, + `is_favorite` BOOLEAN NOT NULL DEFAULT 0, `id_feed` SMALLINT, - `tags` varchar(1023), + `tags` VARCHAR(1023), PRIMARY KEY (`id`), FOREIGN KEY (`id_feed`) REFERENCES `%1$sfeed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE KEY (`id_feed`,`guid`), @@ -88,25 +88,46 @@ ENGINE = INNODB; CREATE INDEX `entry_feed_read_index` ON `%1$sentry`(`id_feed`,`is_read`); -- v1.7 Located here to be auto-added '); +define('SQL_CREATE_TABLE_TAGS', ' +CREATE TABLE IF NOT EXISTS `%1$stag` ( -- v1.12 + `id` SMALLINT NOT NULL AUTO_INCREMENT, + `name` VARCHAR(63) NOT NULL, + `attributes` TEXT, + PRIMARY KEY (`id`), + UNIQUE KEY (`name`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci +ENGINE = INNODB; + +CREATE TABLE IF NOT EXISTS `%1$sentrytag` ( -- v1.12 + `id_tag` SMALLINT, + `id_entry` BIGINT, + PRIMARY KEY (`id_tag`,`id_entry`), + FOREIGN KEY (`id_tag`) REFERENCES `%1$stag`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (`id_entry`) REFERENCES `%1$sentry`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, + INDEX (`id_entry`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci +ENGINE = INNODB; +'); + define('SQL_INSERT_FEEDS', ' -INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400); +INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("https://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "https://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400); INSERT IGNORE INTO `%1$sfeed` (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS @ GitHub", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400); '); -define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `%1$sentrytmp`, `%1$sentry`, `%1$sfeed`, `%1$scategory`'); +define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `%1$sentrytag`, `%1$stag`, `%1$sentrytmp`, `%1$sentry`, `%1$sfeed`, `%1$scategory`'); define('SQL_UPDATE_UTF8MB4', ' -ALTER DATABASE `%2$s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER DATABASE `%2$s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- v1.5.0 ALTER TABLE `%1$scategory` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -UPDATE `%1$scategory` SET name=SUBSTRING(name,1,190) WHERE LENGTH(name) > 191; -ALTER TABLE `%1$scategory` MODIFY `name` VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL; +UPDATE `%1$scategory` SET name=SUBSTRING(name,1,' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') WHERE LENGTH(name) > ' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . '; +ALTER TABLE `%1$scategory` MODIFY `name` VARCHAR(' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL; OPTIMIZE TABLE `%1$scategory`; ALTER TABLE `%1$sfeed` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -UPDATE `%1$sfeed` SET name=SUBSTRING(name,1,190) WHERE LENGTH(name) > 191; -ALTER TABLE `%1$sfeed` MODIFY `name` VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL; -ALTER TABLE `%1$sfeed` MODIFY `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +UPDATE `%1$sfeed` SET name=SUBSTRING(name,1,' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') WHERE LENGTH(name) > ' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . '; +ALTER TABLE `%1$sfeed` MODIFY `name` VARCHAR(' . FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE . ') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL; +ALTER TABLE `%1$sfeed` MODIFY `description` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; OPTIMIZE TABLE `%1$sfeed`; ALTER TABLE `%1$sentry` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -115,3 +136,8 @@ ALTER TABLE `%1$sentry` MODIFY `author` VARCHAR(255) CHARACTER SET utf8mb4 COLLA ALTER TABLE `%1$sentry` MODIFY `tags` VARCHAR(1023) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; OPTIMIZE TABLE `%1$sentry`; '); + +define('SQL_UPDATE_GUID_LATIN1_BIN', ' -- v1.12 +ALTER TABLE `%1$sentrytmp` MODIFY `guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL; +ALTER TABLE `%1$sentry` MODIFY `guid` VARCHAR(760) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL; +'); diff --git a/app/SQL/install.sql.pgsql.php b/app/SQL/install.sql.pgsql.php index b80fbf1e7..e68e6f3be 100644 --- a/app/SQL/install.sql.pgsql.php +++ b/app/SQL/install.sql.pgsql.php @@ -1,5 +1,5 @@ <?php -define('SQL_CREATE_DB', 'CREATE DATABASE %1$s ENCODING \'UTF8\';'); +define('SQL_CREATE_DB', 'CREATE DATABASE "%1$s" ENCODING \'UTF8\';'); global $SQL_CREATE_TABLES; $SQL_CREATE_TABLES = array( @@ -10,16 +10,16 @@ $SQL_CREATE_TABLES = array( 'CREATE TABLE IF NOT EXISTS "%1$sfeed" ( "id" SERIAL PRIMARY KEY, - "url" varchar(511) UNIQUE NOT NULL, + "url" VARCHAR(511) UNIQUE NOT NULL, "category" SMALLINT DEFAULT 0, "name" VARCHAR(255) NOT NULL, "website" VARCHAR(255), - "description" text, + "description" TEXT, "lastUpdate" INT DEFAULT 0, "priority" SMALLINT NOT NULL DEFAULT 10, "pathEntries" VARCHAR(511) DEFAULT NULL, "httpAuth" VARCHAR(511) DEFAULT NULL, - "error" smallint DEFAULT 0, + "error" SMALLINT DEFAULT 0, "keep_history" INT NOT NULL DEFAULT -2, "ttl" INT NOT NULL DEFAULT 0, "attributes" TEXT, -- v1.11.0 @@ -27,9 +27,9 @@ $SQL_CREATE_TABLES = array( "cache_nbUnreads" INT DEFAULT 0, FOREIGN KEY ("category") REFERENCES "%1$scategory" ("id") ON DELETE SET NULL ON UPDATE CASCADE );', -'CREATE INDEX %1$sname_index ON "%1$sfeed" ("name");', -'CREATE INDEX %1$spriority_index ON "%1$sfeed" ("priority");', -'CREATE INDEX %1$skeep_history_index ON "%1$sfeed" ("keep_history");', +'CREATE INDEX "%1$sname_index" ON "%1$sfeed" ("name");', +'CREATE INDEX "%1$spriority_index" ON "%1$sfeed" ("priority");', +'CREATE INDEX "%1$skeep_history_index" ON "%1$sfeed" ("keep_history");', 'CREATE TABLE IF NOT EXISTS "%1$sentry" ( "id" BIGINT NOT NULL PRIMARY KEY, @@ -48,11 +48,14 @@ $SQL_CREATE_TABLES = array( FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE ("id_feed","guid") );', -'CREATE INDEX %1$sis_favorite_index ON "%1$sentry" ("is_favorite");', -'CREATE INDEX %1$sis_read_index ON "%1$sentry" ("is_read");', -'CREATE INDEX %1$sentry_lastSeen_index ON "%1$sentry" ("lastSeen");', +'CREATE INDEX "%1$sis_favorite_index" ON "%1$sentry" ("is_favorite");', +'CREATE INDEX "%1$sis_read_index" ON "%1$sentry" ("is_read");', +'CREATE INDEX "%1$sentry_lastSeen_index" ON "%1$sentry" ("lastSeen");', -'INSERT INTO "%1$scategory" (id, name) SELECT 1, \'%2$s\' WHERE NOT EXISTS (SELECT id FROM "%1$scategory" WHERE id = 1) RETURNING nextval(\'%1$scategory_id_seq\');', +'INSERT INTO "%1$scategory" (id, name) + SELECT 1, \'%2$s\' + WHERE NOT EXISTS (SELECT id FROM "%1$scategory" WHERE id = 1) + RETURNING nextval(\'"%1$scategory_id_seq"\');', ); global $SQL_CREATE_TABLE_ENTRYTMP; @@ -74,15 +77,36 @@ $SQL_CREATE_TABLE_ENTRYTMP = array( FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE ("id_feed","guid") );', -'CREATE INDEX %1$sentrytmp_date_index ON "%1$sentrytmp" ("date");', +'CREATE INDEX "%1$sentrytmp_date_index" ON "%1$sentrytmp" ("date");', -'CREATE INDEX %1$sentry_feed_read_index ON "%1$sentry" ("id_feed","is_read");', //v1.7 +'CREATE INDEX "%1$sentry_feed_read_index" ON "%1$sentry" ("id_feed","is_read");', //v1.7 +); + +global $SQL_CREATE_TABLE_TAGS; +$SQL_CREATE_TABLE_TAGS = array( +'CREATE TABLE IF NOT EXISTS "%1$stag" ( -- v1.12 + "id" SERIAL PRIMARY KEY, + "name" VARCHAR(63) UNIQUE NOT NULL, + "attributes" TEXT +);', +'CREATE TABLE IF NOT EXISTS "%1$sentrytag" ( + "id_tag" SMALLINT, + "id_entry" BIGINT, + PRIMARY KEY ("id_tag","id_entry"), + FOREIGN KEY ("id_tag") REFERENCES "%1$stag" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY ("id_entry") REFERENCES "%1$sentry" ("id") ON DELETE CASCADE ON UPDATE CASCADE +);', +'CREATE INDEX "%1$sentrytag_id_entry_index" ON "%1$sentrytag" ("id_entry");', ); global $SQL_INSERT_FEEDS; $SQL_INSERT_FEEDS = array( -'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'http://freshrss.org/feeds/all.atom.xml\', 1, \'FreshRSS.org\', \'http://freshrss.org/\', \'FreshRSS, a free, self-hostable aggregator…\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'http://freshrss.org/feeds/all.atom.xml\');', -'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) SELECT \'https://github.com/FreshRSS/FreshRSS/releases.atom\', 1, \'FreshRSS @ GitHub\', \'https://github.com/FreshRSS/FreshRSS/\', \'FreshRSS releases @ GitHub\', 86400 WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'https://github.com/FreshRSS/FreshRSS/releases.atom\');', +'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) + SELECT \'https://freshrss.org/feeds/all.atom.xml\', 1, \'FreshRSS.org\', \'https://freshrss.org/\', \'FreshRSS, a free, self-hostable aggregator…\', 86400 + WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'https://freshrss.org/feeds/all.atom.xml\');', +'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) + SELECT \'https://github.com/FreshRSS/FreshRSS/releases.atom\', 1, \'FreshRSS @ GitHub\', \'https://github.com/FreshRSS/FreshRSS/\', \'FreshRSS releases @ GitHub\', 86400 + WHERE NOT EXISTS (SELECT id FROM "%1$sfeed" WHERE url = \'https://github.com/FreshRSS/FreshRSS/releases.atom\');', ); -define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS "%1$sentrytmp", "%1$sentry", "%1$sfeed", "%1$scategory"'); +define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS "%1$sentrytag", "%1$stag", "%1$sentrytmp", "%1$sentry", "%1$sfeed", "%1$scategory"'); diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php index cbfb719e5..1dd5f2647 100644 --- a/app/SQL/install.sql.sqlite.php +++ b/app/SQL/install.sql.sqlite.php @@ -3,27 +3,27 @@ global $SQL_CREATE_TABLES; $SQL_CREATE_TABLES = array( 'CREATE TABLE IF NOT EXISTS `category` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - `name` varchar(255) NOT NULL, + `name` VARCHAR(255) NOT NULL, UNIQUE (`name`) );', 'CREATE TABLE IF NOT EXISTS `feed` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - `url` varchar(511) NOT NULL, + `url` VARCHAR(511) NOT NULL, `category` SMALLINT DEFAULT 0, - `name` varchar(255) NOT NULL, - `website` varchar(255), - `description` text, - `lastUpdate` int(11) DEFAULT 0, -- Until year 2038 - `priority` tinyint(2) NOT NULL DEFAULT 10, - `pathEntries` varchar(511) DEFAULT NULL, - `httpAuth` varchar(511) DEFAULT NULL, - `error` boolean DEFAULT 0, + `name` VARCHAR(255) NOT NULL, + `website` VARCHAR(255), + `description` TEXT, + `lastUpdate` INT(11) DEFAULT 0, -- Until year 2038 + `priority` TINYINT(2) NOT NULL DEFAULT 10, + `pathEntries` VARCHAR(511) DEFAULT NULL, + `httpAuth` VARCHAR(511) DEFAULT NULL, + `error` BOOLEAN DEFAULT 0, `keep_history` MEDIUMINT NOT NULL DEFAULT -2, `ttl` INT NOT NULL DEFAULT 0, `attributes` TEXT, -- v1.11.0 - `cache_nbEntries` int DEFAULT 0, - `cache_nbUnreads` int DEFAULT 0, + `cache_nbEntries` INT DEFAULT 0, + `cache_nbUnreads` INT DEFAULT 0, FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE, UNIQUE (`url`) );', @@ -32,19 +32,19 @@ $SQL_CREATE_TABLES = array( 'CREATE INDEX IF NOT EXISTS feed_keep_history_index ON `feed`(`keep_history`);', 'CREATE TABLE IF NOT EXISTS `entry` ( - `id` bigint NOT NULL, - `guid` varchar(760) NOT NULL, - `title` varchar(255) NOT NULL, - `author` varchar(255), - `content` text, - `link` varchar(1023) NOT NULL, - `date` int(11), -- Until year 2038 + `id` BIGINT NOT NULL, + `guid` VARCHAR(760) NOT NULL, + `title` VARCHAR(255) NOT NULL, + `author` VARCHAR(255), + `content` TEXT, + `link` VARCHAR(1023) NOT NULL, + `date` INT(11), -- Until year 2038 `lastSeen` INT(11) DEFAULT 0, -- v1.1.1, Until year 2038 `hash` BINARY(16), -- v1.1.1 - `is_read` boolean NOT NULL DEFAULT 0, - `is_favorite` boolean NOT NULL DEFAULT 0, + `is_read` BOOLEAN NOT NULL DEFAULT 0, + `is_favorite` BOOLEAN NOT NULL DEFAULT 0, `id_feed` SMALLINT, - `tags` varchar(1023), + `tags` VARCHAR(1023), PRIMARY KEY (`id`), FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE (`id_feed`,`guid`) @@ -59,19 +59,19 @@ $SQL_CREATE_TABLES = array( global $SQL_CREATE_TABLE_ENTRYTMP; $SQL_CREATE_TABLE_ENTRYTMP = array( 'CREATE TABLE IF NOT EXISTS `entrytmp` ( -- v1.7 - `id` bigint NOT NULL, - `guid` varchar(760) NOT NULL, - `title` varchar(255) NOT NULL, - `author` varchar(255), - `content` text, - `link` varchar(1023) NOT NULL, - `date` int(11), + `id` BIGINT NOT NULL, + `guid` VARCHAR(760) NOT NULL, + `title` VARCHAR(255) NOT NULL, + `author` VARCHAR(255), + `content` TEXT, + `link` VARCHAR(1023) NOT NULL, + `date` INT(11), `lastSeen` INT(11) DEFAULT 0, `hash` BINARY(16), - `is_read` boolean NOT NULL DEFAULT 0, - `is_favorite` boolean NOT NULL DEFAULT 0, + `is_read` BOOLEAN NOT NULL DEFAULT 0, + `is_favorite` BOOLEAN NOT NULL DEFAULT 0, `id_feed` SMALLINT, - `tags` varchar(1023), + `tags` VARCHAR(1023), PRIMARY KEY (`id`), FOREIGN KEY (`id_feed`) REFERENCES `feed`(`id`) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE (`id_feed`,`guid`) @@ -81,44 +81,30 @@ $SQL_CREATE_TABLE_ENTRYTMP = array( 'CREATE INDEX IF NOT EXISTS `entry_feed_read_index` ON `entry`(`id_feed`,`is_read`);', //v1.7 ); +global $SQL_CREATE_TABLE_TAGS; +$SQL_CREATE_TABLE_TAGS = array( +'CREATE TABLE IF NOT EXISTS `tag` ( -- v1.12 + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` VARCHAR(63) NOT NULL, + `attributes` TEXT, + UNIQUE (`name`) +);', +'CREATE TABLE IF NOT EXISTS `entrytag` ( + `id_tag` SMALLINT, + `id_entry` SMALLINT, + PRIMARY KEY (`id_tag`,`id_entry`), + FOREIGN KEY (`id_tag`) REFERENCES `tag` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (`id_entry`) REFERENCES `entry` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +);', +'CREATE INDEX entrytag_id_entry_index ON `entrytag` (`id_entry`);', +); + global $SQL_INSERT_FEEDS; $SQL_INSERT_FEEDS = array( -'INSERT OR IGNORE INTO `feed` - ( - url, - category, - name, - website, - description, - ttl - ) - VALUES - ( - "http://freshrss.org/feeds/all.atom.xml", - 1, - "FreshRSS.org", - "http://freshrss.org/", - "FreshRSS, a free, self-hostable aggregator…", - 86400 - );', -'INSERT OR IGNORE INTO `feed` - ( - url, - category, - name, - website, - description, - ttl - ) - VALUES - ( - "https://github.com/FreshRSS/FreshRSS/releases.atom", - 1, - "FreshRSS releases", - "https://github.com/FreshRSS/FreshRSS/", - "FreshRSS releases @ GitHub", - 86400 - );', +'INSERT OR IGNORE INTO `feed` (url, category, name, website, description, ttl) + VALUES ("https://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "https://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);', +'INSERT OR IGNORE INTO `feed` (url, category, name, website, description, ttl) + VALUES ("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS releases", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);', ); -define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `entrytmp`, `entry`, `feed`, `category`'); +define('SQL_DROP_TABLES', 'DROP TABLE IF EXISTS `entrytag`, `tag`, `entrytmp`, `entry`, `feed`, `category`'); diff --git a/app/i18n/cz/conf.php b/app/i18n/cz/conf.php index e73ab168f..84ee78c73 100644 --- a/app/i18n/cz/conf.php +++ b/app/i18n/cz/conf.php @@ -19,7 +19,7 @@ return array( 'bottom_line' => 'Spodní řádek', 'entry' => 'Ikony článků', 'publication_date' => 'Datum vydání', - 'related_tags' => 'Související tagy', + 'related_tags' => 'Související tagy', //TODO 'sharing' => 'Sdílení', 'top_line' => 'Horní řádek', ), diff --git a/app/i18n/cz/gen.php b/app/i18n/cz/gen.php index 66c011da3..b9a65f210 100644 --- a/app/i18n/cz/gen.php +++ b/app/i18n/cz/gen.php @@ -181,7 +181,7 @@ return array( 'short' => array( 'attention' => 'Upozornění!', 'blank_to_disable' => 'Zakázat - ponechte prázdné', - 'by_author' => 'Od <em>%s</em>', + 'by_author' => 'Od:', 'by_default' => 'Výchozí', 'damn' => 'Sakra!', 'default_category' => 'Nezařazeno', diff --git a/app/i18n/cz/index.php b/app/i18n/cz/index.php index 48a28d4da..7e60ca379 100644 --- a/app/i18n/cz/index.php +++ b/app/i18n/cz/index.php @@ -7,7 +7,7 @@ return array( 'bugs_reports' => 'Hlášení chyb', 'credits' => 'Poděkování', 'credits_content' => 'Některé designové prvky pocházejí z <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>, FreshRSS ale tuto platformu nevyužívá. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Ikony</a> pocházejí z <a href="https://www.gnome.org/">GNOME projektu</a>. Font <em>Open Sans</em> vytvořil <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS je založen na PHP framework <a href="https://github.com/marienfressinaud/MINZ">Minz</a>.', - 'freshrss_description' => 'FreshRSS je čtečka RSS kanálů určená k provozu na vlastním serveru, podobná <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> nebo <a href="http://projet.idleman.fr/leed/">Leed</a>. Je to nenáročný a jednoduchý, zároveň ale mocný a konfigurovatelný nástroj.', + 'freshrss_description' => 'FreshRSS je čtečka RSS kanálů určená k provozu na vlastním serveru, podobná <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> nebo <a href="http://leed.idleman.fr/">Leed</a>. Je to nenáročný a jednoduchý, zároveň ale mocný a konfigurovatelný nástroj.', 'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">na Github</a>', 'license' => 'Licence', 'project_website' => 'Stránka projektu', @@ -53,10 +53,11 @@ return array( 'starred' => 'Zobrazit oblíbené', 'stats' => 'Statistika', 'subscription' => 'Správa subskripcí', + 'tags' => 'My labels', //TODO 'unread' => 'Zobrazovat nepřečtené', ), 'share' => 'Sdílet', 'tag' => array( - 'related' => 'Související tagy', + 'related' => 'Související tagy', //TODO ), ); diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php index 5caf9acbe..55441aaf8 100644 --- a/app/i18n/cz/sub.php +++ b/app/i18n/cz/sub.php @@ -27,6 +27,7 @@ return array( 'password' => 'Heslo', 'username' => 'Přihlašovací jméno', ), + 'clear_cache' => 'Always clear cache', //TODO 'css_help' => 'Stáhne zkrácenou verzi RSS kanálů (pozor, náročnější na čas!)', 'css_path' => 'Původní CSS soubor článku z webových stránek', 'description' => 'Popis', diff --git a/app/i18n/de/admin.php b/app/i18n/de/admin.php index fbeb80296..2eb4a69f6 100644 --- a/app/i18n/de/admin.php +++ b/app/i18n/de/admin.php @@ -67,8 +67,8 @@ return array( 'ok' => 'Sie haben die JSON-Erweiterung.', ), 'mbstring' => array( - 'nok' => 'Cannot find the recommended library mbstring for Unicode.', //TODO - 'ok' => 'You have the recommended library mbstring for Unicode.', //TODO + 'nok' => 'Ihnen fehlt die mbstring-Bibliothek für Unicode.', //TODO + 'ok' => 'Sie haben die empfohlene mbstring-Bliothek für Unicode.', //TODO ), 'minz' => array( 'nok' => 'Ihnen fehlt das Minz-Framework.', diff --git a/app/i18n/de/conf.php b/app/i18n/de/conf.php index 78f3b4510..579363cb5 100644 --- a/app/i18n/de/conf.php +++ b/app/i18n/de/conf.php @@ -19,7 +19,7 @@ return array( 'bottom_line' => 'Fußzeile', 'entry' => 'Artikel-Symbole', 'publication_date' => 'Datum der Veröffentlichung', - 'related_tags' => 'Verwandte Tags', + 'related_tags' => 'Verwandte Tags', //TODO 'sharing' => 'Teilen', 'top_line' => 'Kopfzeile', ), @@ -102,7 +102,7 @@ return array( 'read' => array( 'article_open_on_website' => 'wenn der Artikel auf der Original-Webseite geöffnet wird', 'article_viewed' => 'wenn der Artikel angesehen wird', - 'scroll' => 'beim Blättern', + 'scroll' => 'beim Scrollen bzw. Überspringen', 'upon_reception' => 'beim Empfang des Artikels', 'when' => 'Artikel als gelesen markieren…', ), diff --git a/app/i18n/de/feedback.php b/app/i18n/de/feedback.php index c20f58487..dc4f679f9 100644 --- a/app/i18n/de/feedback.php +++ b/app/i18n/de/feedback.php @@ -53,8 +53,8 @@ return array( 'sub' => array( 'actualize' => 'Aktualisieren', 'articles' => array( - 'marked_read' => 'The selected articles have been marked as read.', //TODO - 'marked_unread' => 'The articles have been marked as unread.', //TODO + 'marked_read' => 'Die ausgewählten Artikel wurden als gelesen markiert.', + 'marked_unread' => 'Die ausgewählten Artikel wurden als ungelesen markiert.', ), 'category' => array( 'created' => 'Die Kategorie %s ist erstellt worden.', diff --git a/app/i18n/de/gen.php b/app/i18n/de/gen.php index eb1e74ed6..617b2a494 100644 --- a/app/i18n/de/gen.php +++ b/app/i18n/de/gen.php @@ -167,6 +167,7 @@ return array( 'g+' => 'Google+', 'gnusocial' => 'GNU social', 'jdh' => 'Journal du hacker', + 'Known' => 'Known-Seite (https://withknown.com)', 'linkedin' => 'LinkedIn', 'mastodon' => 'Mastodon', 'movim' => 'Movim', @@ -180,7 +181,7 @@ return array( 'short' => array( 'attention' => 'Achtung!', 'blank_to_disable' => 'Zum Deaktivieren frei lassen', - 'by_author' => 'Von <em>%s</em>', + 'by_author' => 'Von:', 'by_default' => 'standardmäßig', 'damn' => 'Verdammt!', 'default_category' => 'Unkategorisiert', diff --git a/app/i18n/de/index.php b/app/i18n/de/index.php index 1fa3e3933..2d0dcc2dd 100644 --- a/app/i18n/de/index.php +++ b/app/i18n/de/index.php @@ -7,7 +7,7 @@ return array( 'bugs_reports' => 'Fehlerberichte', 'credits' => 'Credits', 'credits_content' => 'Einige Designelemente stammen von <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>, obwohl FreshRSS dieses Framework nicht nutzt. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> stammen vom <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> Font wurde von <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> erstellt. FreshRSS basiert auf <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, einem PHP-Framework.', - 'freshrss_description' => 'FreshRSS ist ein RSS-Feedsaggregator zum selbst hosten wie zum Beispiel <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> oder <a href="http://projet.idleman.fr/leed/">Leed</a>. Er ist leicht und einfach zu handhaben und gleichzeitig ein leistungsstarkes und konfigurierbares Werkzeug.', + 'freshrss_description' => 'FreshRSS ist ein RSS-Feedsaggregator zum selbst hosten wie zum Beispiel <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> oder <a href="http://leed.idleman.fr/">Leed</a>. Er ist leicht und einfach zu handhaben und gleichzeitig ein leistungsstarkes und konfigurierbares Werkzeug.', 'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>', 'license' => 'Lizenz', 'project_website' => 'Projekt-Webseite', @@ -40,7 +40,7 @@ return array( 'mark_all_read' => 'Alle als gelesen markieren', 'mark_cat_read' => 'Kategorie als gelesen markieren', 'mark_feed_read' => 'Feed als gelesen markieren', - 'mark_selection_unread' => 'Mark selection as unread', //TODO + 'mark_selection_unread' => 'Auswahl als ungelesen markieren', 'newer_first' => 'Neuere zuerst', 'non-starred' => 'Alle außer Favoriten zeigen', 'normal_view' => 'Normale Ansicht', @@ -53,10 +53,11 @@ return array( 'starred' => 'Nur Favoriten zeigen', 'stats' => 'Statistiken', 'subscription' => 'Abonnementverwaltung', + 'tags' => 'My labels', //TODO 'unread' => 'Nur ungelesene zeigen', ), 'share' => 'Teilen', 'tag' => array( - 'related' => 'Verwandte Tags', + 'related' => 'Verwandte Tags', //TODO ), ); diff --git a/app/i18n/de/install.php b/app/i18n/de/install.php index d28b22840..d5a28f440 100644 --- a/app/i18n/de/install.php +++ b/app/i18n/de/install.php @@ -69,8 +69,8 @@ return array( 'ok' => 'Sie haben eine empfohlene Bibliothek um JSON zu parsen.', ), 'mbstring' => array( - 'nok' => 'Cannot find the recommended library mbstring for Unicode.', //TODO - 'ok' => 'You have the recommended library mbstring for Unicode.', //TODO + 'nok' => 'Es fehlt die empfohlene mbstring-Bibliothek für Unicode.', + 'ok' => 'Sie haben die empfohlene mbstring-Bibliothek für Unicode.', ), 'minz' => array( 'nok' => 'Ihnen fehlt das Minz-Framework.', diff --git a/app/i18n/de/sub.php b/app/i18n/de/sub.php index 0ba818c69..6a1100dba 100644 --- a/app/i18n/de/sub.php +++ b/app/i18n/de/sub.php @@ -27,6 +27,7 @@ return array( 'password' => 'HTTP-Passwort', 'username' => 'HTTP-Nutzername', ), + 'clear_cache' => 'Nicht cachen (für defekte Feeds)', 'css_help' => 'Ruft gekürzte RSS-Feeds ab (Achtung, benötigt mehr Zeit!)', 'css_path' => 'Pfad zur CSS-Datei des Artikels auf der Original-Webseite', 'description' => 'Beschreibung', @@ -44,10 +45,10 @@ return array( 'main_stream' => 'In Haupt-Feeds zeigen', 'normal' => 'Zeige in eigener Kategorie', ), - 'ssl_verify' => 'Verify SSL security', //TODO + 'ssl_verify' => 'Überprüfe SSL Sicherheit', 'stats' => 'Statistiken', 'think_to_add' => 'Sie können Feeds hinzufügen.', - 'timeout' => 'Timeout in seconds', //TODO + 'timeout' => 'Zeitlimit in Sekunden', 'title' => 'Titel', 'title_add' => 'Einen RSS-Feed hinzufügen', 'ttl' => 'Aktualisiere automatisch nicht öfter als', diff --git a/app/i18n/en/conf.php b/app/i18n/en/conf.php index fd91ed8f6..5c128f8e7 100644 --- a/app/i18n/en/conf.php +++ b/app/i18n/en/conf.php @@ -19,7 +19,7 @@ return array( 'bottom_line' => 'Bottom line', 'entry' => 'Article icons', 'publication_date' => 'Date of publication', - 'related_tags' => 'Related tags', + 'related_tags' => 'Article tags', 'sharing' => 'Sharing', 'top_line' => 'Top line', ), diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index 34e81af2e..9f7da55a5 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -181,7 +181,7 @@ return array( 'short' => array( 'attention' => 'Warning!', 'blank_to_disable' => 'Leave blank to disable', - 'by_author' => 'By <em>%s</em>', + 'by_author' => 'By:', 'by_default' => 'By default', 'damn' => 'Blast!', 'default_category' => 'Uncategorized', diff --git a/app/i18n/en/index.php b/app/i18n/en/index.php index 1c13abdb7..427a769a0 100644 --- a/app/i18n/en/index.php +++ b/app/i18n/en/index.php @@ -7,7 +7,7 @@ return array( 'bugs_reports' => 'Bugs reports', 'credits' => 'Credits', 'credits_content' => 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesn’t use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police has been created by <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.', - 'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> or <a href="http://projet.idleman.fr/leed/">Leed</a>. It is light and easy to take in hand while being powerful and configurable tool.', + 'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> or <a href="http://leed.idleman.fr/">Leed</a>. It is light and easy to take in hand while being powerful and configurable tool.', 'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>', 'license' => 'License', 'project_website' => 'Project website', @@ -53,10 +53,11 @@ return array( 'starred' => 'Show favourites', 'stats' => 'Statistics', 'subscription' => 'Subscriptions management', + 'tags' => 'My labels', 'unread' => 'Show unread', ), 'share' => 'Share', 'tag' => array( - 'related' => 'Related tags', + 'related' => 'Article tags', //TODO ), ); diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php index 5ff41a4b3..22c7edc30 100644 --- a/app/i18n/en/sub.php +++ b/app/i18n/en/sub.php @@ -27,6 +27,7 @@ return array( 'password' => 'HTTP password', 'username' => 'HTTP username', ), + 'clear_cache' => 'Always clear cache', 'css_help' => 'Retrieves truncated RSS feeds (caution, requires more time!)', 'css_path' => 'Articles CSS path on original website', 'description' => 'Description', diff --git a/app/i18n/es/conf.php b/app/i18n/es/conf.php index 0e198caf8..095015d47 100755 --- a/app/i18n/es/conf.php +++ b/app/i18n/es/conf.php @@ -19,7 +19,7 @@ return array( 'bottom_line' => 'Línea inferior', 'entry' => 'Iconos de artículos', 'publication_date' => 'Fecha de publicación', - 'related_tags' => 'Etiquetas relacionadas', + 'related_tags' => 'Etiquetas relacionadas', //TODO 'sharing' => 'Compartir', 'top_line' => 'Línea superior', ), diff --git a/app/i18n/es/gen.php b/app/i18n/es/gen.php index 4dc1145b2..fe3d62e2d 100755 --- a/app/i18n/es/gen.php +++ b/app/i18n/es/gen.php @@ -181,7 +181,7 @@ return array( 'short' => array( 'attention' => '¡Aviso!', 'blank_to_disable' => 'Deja en blanco para desactivar', - 'by_author' => 'Por <em>%s</em>', + 'by_author' => 'Por:', 'by_default' => 'Por defecto', 'damn' => '¡Córcholis!', 'default_category' => 'Sin categorizar', diff --git a/app/i18n/es/index.php b/app/i18n/es/index.php index c88152459..1ed6066fb 100755 --- a/app/i18n/es/index.php +++ b/app/i18n/es/index.php @@ -7,7 +7,7 @@ return array( 'bugs_reports' => 'Informe de fallos', 'credits' => 'Créditos', 'credits_content' => 'Aunque FreshRSS no usa ese entorno, algunos elementos del diseño están obtenidos de <a href="http://twitter.github.io/bootstrap/">Bootstrap</a>. Los <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Iconos</a> han sido obtenidos del <a href="https://www.gnome.org/">proyecto GNOME</a>. La fuente <em>Open Sans</em> es una creación de <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS usa el entorno PHP <a href="https://github.com/marienfressinaud/MINZ">Minz</a>.', - 'freshrss_description' => 'FreshRSS es un agregador de fuentes RSS de alojamiento privado al estilo de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> o <a href="http://projet.idleman.fr/leed/">Leed</a>. Es una herramienta potente, pero ligera y fácil de usar y configurar.', + 'freshrss_description' => 'FreshRSS es un agregador de fuentes RSS de alojamiento privado al estilo de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> o <a href="http://leed.idleman.fr/">Leed</a>. Es una herramienta potente, pero ligera y fácil de usar y configurar.', 'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">en Github</a>', 'license' => 'Licencia', 'project_website' => 'Web del proyecto', @@ -53,10 +53,11 @@ return array( 'starred' => 'Mostrar solo los favoritos', 'stats' => 'Estadísticas', 'subscription' => 'Administración de suscripciones', + 'tags' => 'My labels', //TODO 'unread' => 'Mostar solo no leídos', ), 'share' => 'Compartir', 'tag' => array( - 'related' => 'Etiquetas relacionadas', + 'related' => 'Etiquetas relacionadas', //TODO ), ); diff --git a/app/i18n/es/sub.php b/app/i18n/es/sub.php index 3abc85578..8a4fb98de 100755 --- a/app/i18n/es/sub.php +++ b/app/i18n/es/sub.php @@ -22,6 +22,7 @@ return array( 'password' => 'Contraseña HTTP', 'username' => 'Nombre de usuario HTTP', ), + 'clear_cache' => 'Always clear cache', //TODO 'css_help' => 'Recibir fuentes RSS truncadas (aviso, ¡necesita más tiempo!)', 'css_path' => 'Ruta a la CSS de los artículos en la web original', 'description' => 'Descripción', diff --git a/app/i18n/fr/conf.php b/app/i18n/fr/conf.php index 52b2f7e0d..01239770b 100644 --- a/app/i18n/fr/conf.php +++ b/app/i18n/fr/conf.php @@ -19,7 +19,7 @@ return array( 'bottom_line' => 'Ligne du bas', 'entry' => 'Icônes d’article', 'publication_date' => 'Date de publication', - 'related_tags' => 'Tags associés', + 'related_tags' => 'Tags de l’article', 'sharing' => 'Partage', 'top_line' => 'Ligne du haut', ), diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index 13e19283f..1e1cef590 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -181,7 +181,7 @@ return array( 'short' => array( 'attention' => 'Attention !', 'blank_to_disable' => 'Laissez vide pour désactiver', - 'by_author' => 'Par <em>%s</em>', + 'by_author' => 'Par :', 'by_default' => 'Par défaut', 'damn' => 'Arf !', 'default_category' => 'Sans catégorie', diff --git a/app/i18n/fr/index.php b/app/i18n/fr/index.php index bb0d14faf..c9595e449 100644 --- a/app/i18n/fr/index.php +++ b/app/i18n/fr/index.php @@ -7,7 +7,7 @@ return array( 'bugs_reports' => 'Rapports de bugs', 'credits' => 'Crédits', 'credits_content' => 'Des éléments de design sont issus du <a href="http://twitter.github.io/bootstrap/">projet Bootstrap</a> bien que FreshRSS n’utilise pas ce framework. Les <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icônes</a> sont issues du <a href="https://www.gnome.org/">projet GNOME</a>. La police <em>Open Sans</em> utilisée a été créée par <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS repose sur <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.', - 'freshrss_description' => 'FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> ou <a href="http://projet.idleman.fr/leed/">Leed</a>. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.', + 'freshrss_description' => 'FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> ou <a href="http://leed.idleman.fr/">Leed</a>. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.', 'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">sur Github</a>', 'license' => 'Licence', 'project_website' => 'Site du projet', @@ -53,10 +53,11 @@ return array( 'starred' => 'Afficher les favoris', 'stats' => 'Statistiques', 'subscription' => 'Gestion des abonnements', + 'tags' => 'Mes étiquettes', 'unread' => 'Afficher les non-lus', ), 'share' => 'Partager', 'tag' => array( - 'related' => 'Tags associés', + 'related' => 'Tags de l’article', ), ); diff --git a/app/i18n/fr/sub.php b/app/i18n/fr/sub.php index c6af2fb90..d3921f1d9 100644 --- a/app/i18n/fr/sub.php +++ b/app/i18n/fr/sub.php @@ -27,6 +27,7 @@ return array( 'password' => 'Mot de passe HTTP', 'username' => 'Identifiant HTTP', ), + 'clear_cache' => 'Toujours vider le cache', 'css_help' => 'Permet de récupérer les flux tronqués (attention, demande plus de temps !)', 'css_path' => 'Sélecteur CSS des articles sur le site d’origine', 'description' => 'Description', diff --git a/app/i18n/he/conf.php b/app/i18n/he/conf.php index a682461a6..2ab8aefa9 100644 --- a/app/i18n/he/conf.php +++ b/app/i18n/he/conf.php @@ -19,7 +19,7 @@ return array( 'bottom_line' => 'שורה תחתונה', 'entry' => 'סמלילי מאמרים', 'publication_date' => 'תאריך הפרסום', - 'related_tags' => 'תגיות קשורות', + 'related_tags' => 'תגיות קשורות', //TODO 'sharing' => 'שיתוף', 'top_line' => 'שורה עליונה', ), diff --git a/app/i18n/he/gen.php b/app/i18n/he/gen.php index a59f6b178..26b8f99e6 100644 --- a/app/i18n/he/gen.php +++ b/app/i18n/he/gen.php @@ -181,7 +181,7 @@ return array( 'short' => array( 'attention' => 'זהירות!', 'blank_to_disable' => 'יש להשאיר ריק על מנת לנטרל', - 'by_author' => 'מאת <em>%s</em>', + 'by_author' => 'מאת :', 'by_default' => 'ברירת מחדל', 'damn' => 'הו לא!', 'default_category' => 'ללא קטגוריה', diff --git a/app/i18n/he/index.php b/app/i18n/he/index.php index 8ca6e76f7..d33c09b08 100644 --- a/app/i18n/he/index.php +++ b/app/i18n/he/index.php @@ -7,7 +7,7 @@ return array( 'bugs_reports' => 'דיווח באגים', 'credits' => 'קרדיטים', 'credits_content' => 'מאפייני עיצוב מסויימים הגיעו מ <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> אף על פי ש FreshRSS אינו משתמש בתשתית הזו. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">סמלילים</a> הגיעו מ <a href="https://www.gnome.org/"> פרוייקט GNOME </a>. <em>Open Sans</em> הגופן police נוצר על ידי <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Favicons נאספים בעזרת <a href="https://getfavicon.appspot.com/">getFavicon API</a>. FreshRSS מבוסס על <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, תשתית PHP.', - 'freshrss_description' => 'FreshRSS הוא קורא RSS לאחסון עצמי בדומה ל <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> או <a href="http://projet.idleman.fr/leed/">Leed</a>. אינו צורך משאבים רבים, וקל לתפעול אך בו בזמן חזק וניתן להתאמה.', + 'freshrss_description' => 'FreshRSS הוא קורא RSS לאחסון עצמי בדומה ל <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> או <a href="http://leed.idleman.fr/">Leed</a>. אינו צורך משאבים רבים, וקל לתפעול אך בו בזמן חזק וניתן להתאמה.', 'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">בגיטהאב</a>', 'license' => 'רישיון', 'project_website' => 'אתר', @@ -53,10 +53,11 @@ return array( 'starred' => 'הצגת מועדפים בלבד', 'stats' => 'סטטיסטיקות', 'subscription' => 'ניהול הרשמות', + 'tags' => 'My labels', //TODO 'unread' => 'הצגת מאמרים שלא נקראו בלבד', ), 'share' => 'שיתוף', 'tag' => array( - 'related' => 'תגיות קשורות', + 'related' => 'תגיות קשורות', //TODO ), ); diff --git a/app/i18n/he/sub.php b/app/i18n/he/sub.php index a263cd728..711004662 100644 --- a/app/i18n/he/sub.php +++ b/app/i18n/he/sub.php @@ -27,6 +27,7 @@ return array( 'password' => 'HTTP סיסמה', 'username' => 'HTTP שם משתמש', ), + 'clear_cache' => 'Always clear cache', //TODO 'css_help' => 'קבלת הזנות RSS קטומות (זהירות, לוקח זמן רב יותר!)', 'css_path' => 'נתיב הCSS של המאמר באתר המקורי', 'description' => 'תיאור', diff --git a/app/i18n/it/conf.php b/app/i18n/it/conf.php index 65b979c51..83beb2df5 100644 --- a/app/i18n/it/conf.php +++ b/app/i18n/it/conf.php @@ -19,7 +19,7 @@ return array( 'bottom_line' => 'Barra in fondo', 'entry' => 'Icone degli articoli', 'publication_date' => 'Data di pubblicazione', - 'related_tags' => 'Tags correlati', + 'related_tags' => 'Tags correlati', //TODO 'sharing' => 'Condivisione', 'top_line' => 'Barra in alto', ), diff --git a/app/i18n/it/gen.php b/app/i18n/it/gen.php index 2a90693f9..ab17441e7 100644 --- a/app/i18n/it/gen.php +++ b/app/i18n/it/gen.php @@ -181,7 +181,7 @@ return array( 'short' => array( 'attention' => 'Attenzione!', 'blank_to_disable' => 'Lascia vuoto per disabilitare', - 'by_author' => 'di <em>%s</em>', + 'by_author' => 'di:', 'by_default' => 'predefinito', 'damn' => 'Ops!', 'default_category' => 'Senza categoria', diff --git a/app/i18n/it/index.php b/app/i18n/it/index.php index 718093327..909db1440 100644 --- a/app/i18n/it/index.php +++ b/app/i18n/it/index.php @@ -7,7 +7,7 @@ return array( 'bugs_reports' => 'Bugs', 'credits' => 'Crediti', 'credits_content' => 'Alcuni elementi di design provengono da <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> sebbene FreshRSS non usi questo framework. Le <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icone</a> provengono dal progetto <a href="https://www.gnome.org/">GNOME</a>. Il carattere <em>Open Sans</em> è stato creato da <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS è basato su <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.', - 'freshrss_description' => 'FreshRSS è un aggregatore di feeds RSS da installare sul proprio host come <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> o <a href="http://projet.idleman.fr/leed/">Leed</a>. Leggero e facile da mantenere pur essendo molto configurabile e potente.', + 'freshrss_description' => 'FreshRSS è un aggregatore di feeds RSS da installare sul proprio host come <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> o <a href="http://leed.idleman.fr/">Leed</a>. Leggero e facile da mantenere pur essendo molto configurabile e potente.', 'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">su Github</a>', 'license' => 'Licenza', 'project_website' => 'Sito del progetto', @@ -53,10 +53,11 @@ return array( 'starred' => 'Mostra solo preferiti', 'stats' => 'Statistiche', 'subscription' => 'Gestione sottoscrizioni', + 'tags' => 'My labels', //TODO 'unread' => 'Mostra solo non letti', ), 'share' => 'Condividi', 'tag' => array( - 'related' => 'Tags correlati', + 'related' => 'Tags correlati', //TODO ), ); diff --git a/app/i18n/it/sub.php b/app/i18n/it/sub.php index 22d58a27f..b22340c9b 100644 --- a/app/i18n/it/sub.php +++ b/app/i18n/it/sub.php @@ -27,6 +27,7 @@ return array( 'password' => 'HTTP password', 'username' => 'HTTP username', ), + 'clear_cache' => 'Always clear cache', //TODO 'css_help' => 'In caso di RSS feeds troncati (attenzione, richiede molto tempo!)', 'css_path' => 'Percorso del foglio di stile CSS del sito di origine', 'description' => 'Descrizione', diff --git a/app/i18n/kr/conf.php b/app/i18n/kr/conf.php index f618d6c96..f26e2cf09 100644 --- a/app/i18n/kr/conf.php +++ b/app/i18n/kr/conf.php @@ -19,7 +19,7 @@ return array( 'bottom_line' => '하단', 'entry' => '문서 아이콘', 'publication_date' => '발행일', - 'related_tags' => '관련 태그', + 'related_tags' => '관련 태그', //TODO 'sharing' => '공유', 'top_line' => '상단', ), diff --git a/app/i18n/kr/gen.php b/app/i18n/kr/gen.php index e664eaa42..6a461bdac 100644 --- a/app/i18n/kr/gen.php +++ b/app/i18n/kr/gen.php @@ -181,7 +181,7 @@ return array( 'short' => array( 'attention' => '경고!', 'blank_to_disable' => '빈 칸으로 두면 비활성화', - 'by_author' => 'By <em>%s</em>', + 'by_author' => 'By:', 'by_default' => '기본값', 'damn' => '이런!', 'default_category' => '분류 없음', diff --git a/app/i18n/kr/index.php b/app/i18n/kr/index.php index cb9684dff..87cc12eca 100644 --- a/app/i18n/kr/index.php +++ b/app/i18n/kr/index.php @@ -7,7 +7,7 @@ return array( 'bugs_reports' => '버그 제보하기', 'credits' => '크레딧', 'credits_content' => 'FreshRSS는 <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> 프레임워크를 사용하진 않지만, 일부 디자인 요소를 가져왔습니다. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">아이콘들</a>은 <a href="https://www.gnome.org/">GNOME 프로젝트</a>에서 가져왔습니다. <em>Open Sans</em> 글꼴은 <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>가 제작하였습니다. FreshRSS는 PHP 프레임워크인 <a href="https://github.com/marienfressinaud/MINZ">Minz</a>에 기반하고 있습니다.', - 'freshrss_description' => 'FreshRSS는 <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> 또는 <a href="http://projet.idleman.fr/leed/">Leed</a>와 같은 셀프 호스팅 기반의 RSS 피드 수집기입니다. FreshRSS는 강력하고 다양한 설정을 할 수 있으면서 도 가볍고 사용하기 쉽습니다.', + 'freshrss_description' => 'FreshRSS는 <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> 또는 <a href="http://leed.idleman.fr/">Leed</a>와 같은 셀프 호스팅 기반의 RSS 피드 수집기입니다. FreshRSS는 강력하고 다양한 설정을 할 수 있으면서 도 가볍고 사용하기 쉽습니다.', 'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">Github 저장소에 제보</a>', 'license' => '라이센스', 'project_website' => '프로젝트 웹사이트', @@ -53,10 +53,11 @@ return array( 'starred' => '즐겨찾기만 표시', 'stats' => '통계', 'subscription' => '구독 관리', + 'tags' => 'My labels', //TODO 'unread' => '읽지 않은 글만 표시', ), 'share' => '공유', 'tag' => array( - 'related' => '관련 태그', + 'related' => '관련 태그', //TODO ), ); diff --git a/app/i18n/kr/sub.php b/app/i18n/kr/sub.php index de200c330..ee6b25e3f 100644 --- a/app/i18n/kr/sub.php +++ b/app/i18n/kr/sub.php @@ -27,6 +27,7 @@ return array( 'password' => 'HTTP 암호', 'username' => 'HTTP 사용자 이름', ), + 'clear_cache' => 'Always clear cache', //TODO 'css_help' => '글의 일부가 포함된 RSS 피드를 가져옵니다 (주의, 시간이 좀 더 걸립니다!)', 'css_path' => '웹사이트 상의 글 본문에 해당하는 CSS 경로', 'description' => '설명', diff --git a/app/i18n/nl/conf.php b/app/i18n/nl/conf.php index 041b482b9..883d932ab 100644 --- a/app/i18n/nl/conf.php +++ b/app/i18n/nl/conf.php @@ -19,7 +19,7 @@ return array( 'bottom_line' => 'Onderaan', 'entry' => 'Artikel pictogrammen', 'publication_date' => 'Publicatie datum', - 'related_tags' => 'Gerelateerde labels', + 'related_tags' => 'Gerelateerde labels', //TODO 'sharing' => 'Delen', 'top_line' => 'Bovenaan', ), diff --git a/app/i18n/nl/gen.php b/app/i18n/nl/gen.php index ccbd86579..fdc4338c3 100644 --- a/app/i18n/nl/gen.php +++ b/app/i18n/nl/gen.php @@ -181,7 +181,7 @@ return array( 'short' => array( 'attention' => 'Attentie!', 'blank_to_disable' => 'Laat leeg om uit te zetten', - 'by_author' => 'Door <em>%s</em>', + 'by_author' => 'Door:', 'by_default' => 'Door standaard', 'damn' => 'Potverdorie!', 'default_category' => 'Niet ingedeeld', diff --git a/app/i18n/nl/index.php b/app/i18n/nl/index.php index 67b3886ea..33fec43c0 100644 --- a/app/i18n/nl/index.php +++ b/app/i18n/nl/index.php @@ -7,7 +7,7 @@ return array( 'bugs_reports' => 'Rapporteer fouten', 'credits' => 'Waarderingen', 'credits_content' => 'Sommige ontwerp elementen komen van <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> alhoewel FreshRSS dit raamwerk niet gebruikt. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Pictogrammen</a> komen van het <a href="https://www.gnome.org/">GNOME project</a>. <em>De Open Sans</em> font police is gemaakt door <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS is gebaseerd op <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, een PHP raamwerk. Nederlandse vertaling door Wanabo, <a href="http://www.nieuwskop.be" title="NieuwsKop">NieuwsKop.be</a>. Link naar de Nederlandse vertaling, <a href="https://github.com/Wanabo/FreshRSS-Dutch-translation/tree/master">FreshRSS-Dutch-translation</a>.', - 'freshrss_description' => 'FreshRSS is een RSS feed aggregator om zelf te hosten zoals <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> of <a href="http://projet.idleman.fr/leed/">Leed</a>. Het gebruikt weinig systeembronnen en is makkelijk te administreren terwijl het een krachtig en makkelijk te configureren programma is.', + 'freshrss_description' => 'FreshRSS is een RSS feed aggregator om zelf te hosten zoals <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> of <a href="http://leed.idleman.fr/">Leed</a>. Het gebruikt weinig systeembronnen en is makkelijk te administreren terwijl het een krachtig en makkelijk te configureren programma is.', 'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">op Github</a>', 'license' => 'License', 'project_website' => 'Project website', @@ -57,6 +57,6 @@ return array( ), 'share' => 'Delen', 'tag' => array( - 'related' => 'Verwante labels', + 'related' => 'Verwante labels', //TODO ), ); diff --git a/app/i18n/nl/sub.php b/app/i18n/nl/sub.php index 4ce254ef5..fec7fb4e7 100644 --- a/app/i18n/nl/sub.php +++ b/app/i18n/nl/sub.php @@ -27,6 +27,7 @@ return array( 'password' => 'HTTP wachtwoord', 'username' => 'HTTP gebruikers naam', ), + 'clear_cache' => 'Always clear cache', //TODO 'css_help' => 'Haalt verstoorde RSS feeds op (attentie, heeft meer tijd nodig!)', 'css_path' => 'Artikelen CSS pad op originele website', 'description' => 'Omschrijving', diff --git a/app/i18n/pt-br/conf.php b/app/i18n/pt-br/conf.php index 61a12160c..2547a8624 100644 --- a/app/i18n/pt-br/conf.php +++ b/app/i18n/pt-br/conf.php @@ -19,7 +19,7 @@ return array( 'bottom_line' => 'Linha inferior', 'entry' => 'Ícones de artigos', 'publication_date' => 'Data da publicação', - 'related_tags' => 'Tags relacionadas', + 'related_tags' => 'Tags relacionadas', //TODO 'sharing' => 'Compartilhar', 'top_line' => 'Linha superior', ), diff --git a/app/i18n/pt-br/gen.php b/app/i18n/pt-br/gen.php index 558482f07..59218597b 100644 --- a/app/i18n/pt-br/gen.php +++ b/app/i18n/pt-br/gen.php @@ -180,7 +180,7 @@ return array( 'short' => array( 'attention' => 'Atencão!', 'blank_to_disable' => 'Deixe em branco para desativar', - 'by_author' => 'Por <em>%s</em>', + 'by_author' => 'Por:', 'by_default' => 'Por padrão', 'damn' => 'Buumm!', 'default_category' => 'Sem categoria', diff --git a/app/i18n/pt-br/index.php b/app/i18n/pt-br/index.php index 2eff8d948..9f98902ed 100644 --- a/app/i18n/pt-br/index.php +++ b/app/i18n/pt-br/index.php @@ -7,7 +7,7 @@ return array( 'bugs_reports' => 'Reportar Bugs', 'credits' => 'Créditos', 'credits_content' => 'Alguns elementos de design vieram do <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> Embora FreshRRS não utiliza este framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Ícones</a> vieram do <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police foi criada por <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS é baseado no <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, um framework PHP.', - 'freshrss_description' => 'FreshRSS é um RSS feeds aggregator para um host próprio como o <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> ou <a href="http://projet.idleman.fr/leed/">Leed</a>. É leve e fácil de utilizar enquanto é uma ferramenta poderosa e configurável. ', + 'freshrss_description' => 'FreshRSS é um RSS feeds aggregator para um host próprio como o <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> ou <a href="http://leed.idleman.fr/">Leed</a>. É leve e fácil de utilizar enquanto é uma ferramenta poderosa e configurável. ', 'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">no Github</a>', 'license' => 'licença', 'project_website' => 'Site do projeto', @@ -57,6 +57,6 @@ return array( ), 'share' => 'Compartilhar', 'tag' => array( - 'related' => 'Tags relacionadas', + 'related' => 'Tags relacionadas', //TODO ), ); diff --git a/app/i18n/pt-br/sub.php b/app/i18n/pt-br/sub.php index 1b084f08f..daa24e8f3 100644 --- a/app/i18n/pt-br/sub.php +++ b/app/i18n/pt-br/sub.php @@ -27,6 +27,7 @@ return array( 'password' => 'Senha HTTP', 'username' => 'Usuário HTTP', ), + 'clear_cache' => 'Always clear cache', //TODO 'css_help' => 'Retorna RSS feeds truncados (atenção, requer mais tempo!)', 'css_path' => 'Caminho do CSS do artigo no site original', 'description' => 'Descrição', diff --git a/app/i18n/ru/conf.php b/app/i18n/ru/conf.php index 90a1a6797..b9d45fb20 100644 --- a/app/i18n/ru/conf.php +++ b/app/i18n/ru/conf.php @@ -19,7 +19,7 @@ return array( 'bottom_line' => 'Bottom line', 'entry' => 'Article icons', 'publication_date' => 'Date of publication', - 'related_tags' => 'Related tags', + 'related_tags' => 'Related tags', //TODO 'sharing' => 'Sharing', 'top_line' => 'Top line', ), diff --git a/app/i18n/ru/gen.php b/app/i18n/ru/gen.php index 911410a1c..6c8dd2adf 100644 --- a/app/i18n/ru/gen.php +++ b/app/i18n/ru/gen.php @@ -181,7 +181,7 @@ return array( 'short' => array( 'attention' => 'Warning!', 'blank_to_disable' => 'Leave blank to disable', - 'by_author' => 'By <em>%s</em>', + 'by_author' => 'By:', 'by_default' => 'By default', 'damn' => 'Damn!', 'default_category' => 'Uncategorized', diff --git a/app/i18n/ru/index.php b/app/i18n/ru/index.php index 9bb327786..aaf25a3ab 100644 --- a/app/i18n/ru/index.php +++ b/app/i18n/ru/index.php @@ -7,7 +7,7 @@ return array( 'bugs_reports' => 'Bugs reports', 'credits' => 'Credits', 'credits_content' => 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesn’t use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police has been created by <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.', - 'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> or <a href="http://projet.idleman.fr/leed/">Leed</a>. It is light and easy to take in hand while being powerful and configurable tool.', + 'freshrss_description' => 'FreshRSS is a RSS feeds aggregator to self-host like <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> or <a href="http://leed.idleman.fr/">Leed</a>. It is light and easy to take in hand while being powerful and configurable tool.', 'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">on Github</a>', 'license' => 'License', 'project_website' => 'Project website', @@ -57,6 +57,6 @@ return array( ), 'share' => 'Share', 'tag' => array( - 'related' => 'Related tags', + 'related' => 'Article tags', //TODO ), ); diff --git a/app/i18n/ru/sub.php b/app/i18n/ru/sub.php index bef49623f..12901998d 100644 --- a/app/i18n/ru/sub.php +++ b/app/i18n/ru/sub.php @@ -27,6 +27,7 @@ return array( 'password' => 'HTTP password',// TODO 'username' => 'HTTP username',// TODO ), + 'clear_cache' => 'Always clear cache', //TODO 'css_help' => 'Retrieves truncated RSS feeds (caution, requires more time!)',// TODO 'css_path' => 'Articles CSS path on original website',// TODO 'description' => 'Description',// TODO diff --git a/app/i18n/tr/conf.php b/app/i18n/tr/conf.php index cae1e4cac..49533bb6a 100644 --- a/app/i18n/tr/conf.php +++ b/app/i18n/tr/conf.php @@ -19,7 +19,7 @@ return array( 'bottom_line' => 'Alt çizgi', 'entry' => 'Makale ikonları', 'publication_date' => 'Yayınlama Tarihi', - 'related_tags' => 'İlgili etiketler', + 'related_tags' => 'İlgili etiketler', //TODO 'sharing' => 'Paylaşım', 'top_line' => 'Üst çizgi', ), diff --git a/app/i18n/tr/gen.php b/app/i18n/tr/gen.php index 2e1761517..b8dc18c01 100644 --- a/app/i18n/tr/gen.php +++ b/app/i18n/tr/gen.php @@ -181,7 +181,7 @@ return array( 'short' => array( 'attention' => 'Tehlike!', 'blank_to_disable' => 'Devredışı bırakmak için boş bırakın', - 'by_author' => '<em>%s</em> tarafından', + 'by_author' => 'Tarafından:', 'by_default' => 'Öntanımlı', 'damn' => 'Hay aksi!', 'default_category' => 'Kategorisiz', diff --git a/app/i18n/tr/index.php b/app/i18n/tr/index.php index 1357c05e7..e7db73b96 100644 --- a/app/i18n/tr/index.php +++ b/app/i18n/tr/index.php @@ -7,7 +7,7 @@ return array( 'bugs_reports' => 'Hata raporu', 'credits' => 'Tanıtım', 'credits_content' => 'Bu frameworkü kullanmamasına rağmen FreshRSS bazı tasarım ögelerini <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> dan almıştır. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">İkonlar</a> <a href="https://www.gnome.org/">GNOME projesinden</a> alınmıştır. <em>Open Sans</em> yazı tipi <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> tarafından oluşturulmuştur. FreshRSS bir PHP framework olan <a href="https://github.com/marienfressinaud/MINZ">Minz</a> i temel alır.', - 'freshrss_description' => 'FreshRSS <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> veya <a href="http://projet.idleman.fr/leed/">Leed</a> gibi kendi hostunuzda çalışan bir RSS akış toplayıcısıdır. Güçlü ve yapılandırılabilir araçlarıyla basit ve kullanımı kolay bir uygulamadır.', + 'freshrss_description' => 'FreshRSS <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> veya <a href="http://leed.idleman.fr/">Leed</a> gibi kendi hostunuzda çalışan bir RSS akış toplayıcısıdır. Güçlü ve yapılandırılabilir araçlarıyla basit ve kullanımı kolay bir uygulamadır.', 'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">Github sayfası</a>', 'license' => 'Lisans', 'project_website' => 'Proje sayfası', @@ -53,10 +53,11 @@ return array( 'starred' => 'Favorileri göster', 'stats' => 'İstatistikler', 'subscription' => 'Abonelik yönetimi', + 'tags' => 'My labels', //TODO 'unread' => 'Okunmamışları göster', ), 'share' => 'Share', 'tag' => array( - 'related' => 'İlgili etiketler', + 'related' => 'İlgili etiketler', //TODO ), ); diff --git a/app/i18n/tr/sub.php b/app/i18n/tr/sub.php index e8cd15d0d..ef0c8ffbd 100644 --- a/app/i18n/tr/sub.php +++ b/app/i18n/tr/sub.php @@ -27,6 +27,7 @@ return array( 'password' => 'HTTP şifre', 'username' => 'HTTP kullanıcı adı', ), + 'clear_cache' => 'Always clear cache', //TODO 'css_help' => 'Dikkat, daha çok zaman gerekir!', 'css_path' => 'Makaleleri kendi CSS görünümü ile göster', 'description' => 'Tanım', diff --git a/app/i18n/zh-cn/conf.php b/app/i18n/zh-cn/conf.php index 00bea4d79..6c62349c2 100644 --- a/app/i18n/zh-cn/conf.php +++ b/app/i18n/zh-cn/conf.php @@ -19,7 +19,7 @@ return array( 'bottom_line' => '底栏', 'entry' => '文章图标', 'publication_date' => '更新日期', - 'related_tags' => '相关标签', + 'related_tags' => '相关标签', //TODO 'sharing' => '分享', 'top_line' => '顶栏', ), diff --git a/app/i18n/zh-cn/gen.php b/app/i18n/zh-cn/gen.php index 4ea2d73ab..078e1d378 100644 --- a/app/i18n/zh-cn/gen.php +++ b/app/i18n/zh-cn/gen.php @@ -181,7 +181,7 @@ return array( 'short' => array( 'attention' => '警告!', 'blank_to_disable' => '留空以禁用', - 'by_author' => '作者 <em>%s</em>', + 'by_author' => '作者', 'by_default' => '默认', 'damn' => '错误!', 'default_category' => '未分类', diff --git a/app/i18n/zh-cn/index.php b/app/i18n/zh-cn/index.php index 2b76961ef..dd8eafda7 100644 --- a/app/i18n/zh-cn/index.php +++ b/app/i18n/zh-cn/index.php @@ -7,7 +7,7 @@ return array( 'bugs_reports' => 'Bug 报告', 'credits' => '致谢', 'credits_content' => '某些设计元素来自于 <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> ,尽管 FreshRSS 并没有使用此框架。<a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">图标</a> 来自于 <a href="https://www.gnome.org/">GNOME 项目</a>。<em>Open Sans</em> 字体出自 <a href="https://fonts.google.com/specimen/Open+Sans">Steve Matteson</a> 之手。FreshRSS 基于 PHP 框架 <a href="https://github.com/marienfressinaud/MINZ">Minz</a>。', - 'freshrss_description' => 'FreshRSS 是一个自托管的 RSS 聚合服务,类似于 <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> 或 <a href="http://projet.idleman.fr/leed/">Leed</a>。 它不仅轻快又易用,而且强大又易于配置。', + 'freshrss_description' => 'FreshRSS 是一个自托管的 RSS 聚合服务,类似于 <a href="http://tontof.net/kriss/feed/">Kriss Feed</a> 或 <a href="http://leed.idleman.fr/">Leed</a>。 它不仅轻快又易用,而且强大又易于配置。', 'github' => '<a href="https://github.com/FreshRSS/FreshRSS/issues">Github Issues</a>', 'license' => '授权', 'project_website' => '项目网站', @@ -53,10 +53,11 @@ return array( 'starred' => '显示收藏', 'stats' => '统计', 'subscription' => '订阅管理', + 'tags' => 'My labels', //TODO 'unread' => '显示未读', ), 'share' => '分享', 'tag' => array( - 'related' => '相关标签', + 'related' => '相关标签', //TODO ), ); diff --git a/app/i18n/zh-cn/sub.php b/app/i18n/zh-cn/sub.php index 034f8a9d9..4980b803a 100644 --- a/app/i18n/zh-cn/sub.php +++ b/app/i18n/zh-cn/sub.php @@ -27,6 +27,7 @@ return array( 'password' => 'HTTP 密码', 'username' => 'HTTP 用户名', ), + 'clear_cache' => 'Always clear cache', //TODO 'css_help' => '用于获取全文(注意,这将耗费更多时间!)', 'css_path' => '原文的 CSS 选择器', 'description' => '描述', diff --git a/app/install.php b/app/install.php index eec65be9c..dc79c2388 100644 --- a/app/install.php +++ b/app/install.php @@ -343,13 +343,13 @@ function checkDbUser(&$dbOptions) { try { $c = new PDO($str, $dbOptions['user'], $dbOptions['password'], $driver_options); if (defined('SQL_CREATE_TABLES')) { - $sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP . SQL_INSERT_FEEDS, + $sql = sprintf(SQL_CREATE_TABLES . SQL_CREATE_TABLE_ENTRYTMP . SQL_CREATE_TABLE_TAGS . SQL_INSERT_FEEDS, $dbOptions['prefix_user'], _t('gen.short.default_category')); $stm = $c->prepare($sql); $ok = $stm && $stm->execute(); } else { - global $SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_INSERT_FEEDS; - $instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_INSERT_FEEDS); + global $SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_CREATE_TABLE_TAGS, $SQL_INSERT_FEEDS; + $instructions = array_merge($SQL_CREATE_TABLES, $SQL_CREATE_TABLE_ENTRYTMP, $SQL_CREATE_TABLE_TAGS, $SQL_INSERT_FEEDS); $ok = !empty($instructions); foreach ($instructions as $instruction) { $sql = sprintf($instruction, $dbOptions['prefix_user'], _t('gen.short.default_category')); diff --git a/app/layout/aside_feed.phtml b/app/layout/aside_feed.phtml index 97c0fb0d9..ce029cfa0 100644 --- a/app/layout/aside_feed.phtml +++ b/app/layout/aside_feed.phtml @@ -35,6 +35,30 @@ </li> <?php + $t_active = FreshRSS_Context::isCurrentGet('T'); + ?> + <li class="tree-folder category tags<?php echo $t_active ? ' active' : ''; ?>"> + <div class="tree-folder-title"> + <a class="dropdown-toggle" href="#"><?php echo _i($t_active ? 'up' : 'down'); ?></a> + <a class="title" data-unread="<?php echo format_number($this->nbUnreadTags); ?>" href="<?php echo _url('index', 'index', 'get', 'T'); ?>"><?php echo _t('index.menu.tags'); ?></a> + </div> + <ul class="tree-folder-items<?php echo $t_active ? ' active' : ''; ?>"> + <?php + foreach ($this->tags as $tag): + ?> + <li id="t_<?php echo $tag->id(); ?>" class="item feed<?php echo FreshRSS_Context::isCurrentGet('t_' . $tag->id()) ? ' active' : ''; ?>" data-unread="<?php echo $tag->nbUnread(); ?>"> + <div class="dropdown no-mobile"> + <div class="dropdown-target"></div> + <a class="dropdown-toggle"><?php echo _i('configure'); ?></a> + <?php /* tag_config_template */ ?> + </div> + <?php echo FreshRSS_Themes::alt('label'); ?> <a class="item-title" data-unread="<?php echo format_number($tag->nbUnread()); ?>" href="<?php echo _url('index', 'index', 'get', 't_' . $tag->id()); ?>"><?php echo $tag->name(); ?></a> + </li> + <?php endforeach; ?> + </ul> + </li> + + <?php foreach ($this->categories as $cat) { $feeds = $cat->feeds(); if (!empty($feeds)) { @@ -72,6 +96,17 @@ </form> </div> +<script id="tag_config_template" type="text/html"> + <ul class="dropdown-menu"> + <li class="dropdown-close"><a href="#close">❌</a></li> + <li class="item"> + <button class="as-link confirm" disabled="disabled" + form="mark-read-aside" formaction="<?php echo _url('tag', 'delete', 'id_tag', '------'); ?>" + type="submit"><?php echo _t('gen.action.remove'); ?></button> + </li> + </ul> +</script> + <script id="feed_config_template" type="text/html"> <ul class="dropdown-menu"> <li class="dropdown-close"><a href="#close">❌</a></li> diff --git a/app/layout/layout.phtml b/app/layout/layout.phtml index 1f11e0af1..2e16672e6 100644 --- a/app/layout/layout.phtml +++ b/app/layout/layout.phtml @@ -11,10 +11,6 @@ <?php echo self::headScript(); ?> <link rel="shortcut icon" id="favicon" type="image/x-icon" sizes="16x16 64x64" href="<?php echo Minz_Url::display('/favicon.ico'); ?>" /> <link rel="icon msapplication-TileImage apple-touch-icon" type="image/png" sizes="256x256" href="<?php echo Minz_Url::display('/themes/icons/favicon-256.png'); ?>" /> - <link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('starred', true); ?>" /> - <link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('non-starred', true); ?>" /> - <link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('read', true); ?>" /> - <link rel="prefetch" href="<?php echo FreshRSS_Themes::icon('unread', true); ?>" /> <link rel="apple-touch-icon" href="<?php echo Minz_Url::display('/themes/icons/apple-touch-icon.png'); ?>" /> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-status-bar-style" content="black" /> @@ -22,24 +18,11 @@ <meta name="msapplication-TileColor" content="#FFF" /> <?php if (!FreshRSS_Context::$system_conf->allow_referrer) { ?> <meta name="referrer" content="never" /> -<?php - } - flush(); - if (isset($this->callbackBeforeContent)) { - call_user_func($this->callbackBeforeContent, $this); - } -?> +<?php } ?> <?php echo self::headTitle(); ?> <?php $url_base = Minz_Request::currentRequest(); - if (FreshRSS_Context::$next_id !== '') { - $url_next = $url_base; - $url_next['params']['next'] = FreshRSS_Context::$next_id; - $url_next['params']['ajax'] = 1; -?> - <link id="prefetch" rel="next prefetch" href="<?php echo Minz_Url::display($url_next); ?>" /> -<?php - } if (isset($this->rss_title)) { + if (isset($this->rss_title)) { $url_rss = $url_base; $url_rss['a'] = 'rss'; if (FreshRSS_Context::$user_conf->since_hours_posts_per_rss) { @@ -54,10 +37,19 @@ <?php } ?> </head> <body class="<?php echo Minz_Request::actionName(); ?>"> -<?php $this->partial('header'); ?> +<?php + flush(); + $this->partial('header'); +?> <div id="global"> - <?php $this->render(); ?> + <?php + flush(); + if (isset($this->callbackBeforeFeeds)) { + call_user_func($this->callbackBeforeFeeds, $this); + } + $this->render(); + ?> </div> <?php diff --git a/app/views/configure/display.phtml b/app/views/configure/display.phtml index 414fd2cd6..c6c08e3bc 100644 --- a/app/views/configure/display.phtml +++ b/app/views/configure/display.phtml @@ -39,7 +39,7 @@ <?php } ?> </div> <div class="properties"> - <div><?php echo sprintf('%s — %s', $theme['name'], _t('gen.short.by_author', $theme['author'])); ?></div> + <div><?php echo sprintf('%s — %s %s', $theme['name'], _t('gen.short.by_author'), $theme['author']); ?></div> <div><?php echo $theme['description'] ?></div> <div class="page-number"><?php echo sprintf('%d/%d', $i, $slides) ?></div> </div> @@ -79,8 +79,8 @@ <th> </th> <th title="<?php echo _t('gen.action.mark_read'); ?>"><?php echo _i('read'); ?></th> <th title="<?php echo _t('gen.action.mark_favorite'); ?>"><?php echo _i('bookmark'); ?></th> - <th><?php echo _t('conf.display.icon.sharing'); ?></th> <th><?php echo _t('conf.display.icon.related_tags'); ?></th> + <th><?php echo _t('conf.display.icon.sharing'); ?></th> <th><?php echo _t('conf.display.icon.publication_date'); ?></th> <th><?php echo _i('link'); ?></th> </tr> @@ -98,8 +98,8 @@ <th><?php echo _t('conf.display.icon.bottom_line'); ?></th> <td><input type="checkbox" name="bottomline_read" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_read ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_read; ?>"/></td> <td><input type="checkbox" name="bottomline_favorite" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_favorite ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_favorite; ?>"/></td> - <td><input type="checkbox" name="bottomline_sharing" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_sharing ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_sharing; ?>"/></td> <td><input type="checkbox" name="bottomline_tags" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_tags ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_tags; ?>"/></td> + <td><input type="checkbox" name="bottomline_sharing" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_sharing ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_sharing; ?>"/></td> <td><input type="checkbox" name="bottomline_date" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_date ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_date; ?>"/></td> <td><input type="checkbox" name="bottomline_link" value="1"<?php echo FreshRSS_Context::$user_conf->bottomline_link ? ' checked="checked"' : ''; ?> data-leave-validation="<?php echo FreshRSS_Context::$user_conf->bottomline_link; ?>"/></td> </tr> diff --git a/app/views/entry/read.phtml b/app/views/entry/read.phtml index 73977d94b..fb9e129f2 100755 --- a/app/views/entry/read.phtml +++ b/app/views/entry/read.phtml @@ -12,5 +12,6 @@ $url['params']['is_read'] = Minz_Request::param('is_read', true) ? '0' : '1'; FreshRSS::loadStylesAndScripts(); echo json_encode(array( 'url' => str_ireplace('&', '&', Minz_Url::display($url)), - 'icon' => _i($url['params']['is_read'] === '1' ? 'unread' : 'read') + 'icon' => _i($url['params']['is_read'] === '1' ? 'unread' : 'read'), + 'tags' => $this->tags, )); diff --git a/app/views/helpers/export/articles.phtml b/app/views/helpers/export/articles.phtml index 75651483a..b8958f527 100644 --- a/app/views/helpers/export/articles.phtml +++ b/app/views/helpers/export/articles.phtml @@ -34,7 +34,7 @@ foreach ($this->entriesRaw as $entryRaw) { 'id' => $entry->guid(), 'categories' => array_values($entry->tags()), 'title' => $entry->title(), - 'author' => $entry->author(), + 'author' => $entry->authors(true), //TODO: Make an array like tags? 'published' => $entry->date(true), 'updated' => $entry->date(true), 'alternate' => array(array( diff --git a/app/views/helpers/extension/configure.phtml b/app/views/helpers/extension/configure.phtml index 95d968aba..cde872aa0 100644 --- a/app/views/helpers/extension/configure.phtml +++ b/app/views/helpers/extension/configure.phtml @@ -5,7 +5,7 @@ : _t('admin.extensions.disabled'); ?> </h1> - <p class="alert alert-warn"><?php echo $this->extension->getDescription(); ?> — <?php echo _t('gen.short.by_author', $this->extension->getAuthor()); ?></p> + <p class="alert alert-warn"><?php echo $this->extension->getDescription(); ?> — <?php echo _t('gen.short.by_author'), ' ', $this->extension->getAuthor(); ?></p> <h2><?php echo _t('gen.action.manage'); ?></h2> <?php diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml index 7144aab46..4dbaacd04 100644 --- a/app/views/helpers/feed/update.phtml +++ b/app/views/helpers/feed/update.phtml @@ -205,6 +205,13 @@ </div> </div> + <div class="form-group"> + <label class="group-name" for="clear_cache"><?php echo _t('sub.feed.clear_cache'); ?></label> + <div class="group-controls"> + <input type="checkbox" name="clear_cache" id="clear_cache" value="1"<?php echo $this->feed->attributes('clear_cache') ? ' checked="checked"' : ''; ?> /> + </div> + </div> + <?php if (FreshRSS_Auth::hasAccess('admin')) { ?> <div class="form-group"> <label class="group-name" for="timeout"><?php echo _t('sub.feed.timeout'); ?></label> diff --git a/app/views/helpers/index/normal/entry_bottom.phtml b/app/views/helpers/index/normal/entry_bottom.phtml index 6417da4cb..784a41e1f 100644 --- a/app/views/helpers/index/normal/entry_bottom.phtml +++ b/app/views/helpers/index/normal/entry_bottom.phtml @@ -7,6 +7,7 @@ $bottomline_read = FreshRSS_Context::$user_conf->bottomline_read; $bottomline_favorite = FreshRSS_Context::$user_conf->bottomline_favorite; $bottomline_sharing = FreshRSS_Context::$user_conf->bottomline_sharing && (count($sharing) > 0); + $bottomline_labels = true; //TODO $bottomline_tags = FreshRSS_Context::$user_conf->bottomline_tags; $bottomline_date = FreshRSS_Context::$user_conf->bottomline_date; $bottomline_link = FreshRSS_Context::$user_conf->bottomline_link; @@ -32,8 +33,41 @@ echo _i($this->entry->isFavorite() ? 'starred' : 'non-starred'); ?></a><?php ?></li><?php } - } ?> - <li class="item"><?php + } + if ($bottomline_labels) { + ?><li class="item"> + <div class="dropdown dynamictags"> + <div id="dropdown-labels-<?php echo $this->entry->id();?>" class="dropdown-target"></div> + <?php echo FreshRSS_Themes::alt('label'); ?> + <a class="dropdown-toggle" href="#dropdown-labels-<?php echo $this->entry->id();?>"><?php + echo _t('index.menu.tags'); + ?></a> + <ul class="dropdown-menu"> + <li class="dropdown-close"><a href="#close">❌</a></li> + <!-- Ajax --> + </ul> + </div> + </li><?php + } + $tags = $bottomline_tags ? $this->entry->tags() : null; + if (!empty($tags)) { + ?><li class="item"> + <div class="dropdown"> + <div id="dropdown-tags-<?php echo $this->entry->id();?>" class="dropdown-target"></div> + <?php echo _i('tag'); ?> + <a class="dropdown-toggle" href="#dropdown-tags-<?php echo $this->entry->id();?>"><?php + echo _t('index.tag.related'); + ?></a> + <ul class="dropdown-menu"> + <li class="dropdown-close"><a href="#close">❌</a></li><?php + foreach ($tags as $tag) { + ?><li class="item"><a href="<?php echo _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))); ?>"><?php echo $tag; ?></a></li><?php + } ?> + </ul> + </div> + </li><?php + } + ?><li class="item"><?php if ($bottomline_sharing) { ?><div class="dropdown"> <div id="dropdown-share-<?php echo $this->entry->id();?>" class="dropdown-target"></div> @@ -69,24 +103,6 @@ </div> <?php } ?> </li><?php - $tags = $bottomline_tags ? $this->entry->tags() : null; - if (!empty($tags)) { - ?><li class="item"> - <div class="dropdown"> - <div id="dropdown-tags-<?php echo $this->entry->id();?>" class="dropdown-target"></div> - <?php echo _i('tag'); ?> - <a class="dropdown-toggle" href="#dropdown-tags-<?php echo $this->entry->id();?>"><?php - echo _t('index.tag.related'); - ?></a> - <ul class="dropdown-menu"> - <li class="dropdown-close"><a href="#close">❌</a></li><?php - foreach($tags as $tag) { - ?><li class="item"><a href="<?php echo _url('index', 'index', 'search', '#' . htmlspecialchars_decode($tag, ENT_QUOTES)); ?>"><?php echo $tag; ?></a></li><?php - } ?> - </ul> - </div> - </li><?php - } if ($bottomline_date) { ?><li class="item date"><?php echo $this->entry->date(); ?></li><?php } diff --git a/app/views/index/global.phtml b/app/views/index/global.phtml index 2f25b6dc2..3566abe7e 100644 --- a/app/views/index/global.phtml +++ b/app/views/index/global.phtml @@ -1,6 +1,11 @@ <?php $this->partial('nav_menu'); + flush(); + if (isset($this->callbackBeforeEntries)) { + call_user_func($this->callbackBeforeEntries, $this); + } + $class = ''; if (FreshRSS_Context::$user_conf->hide_read_feeds && FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_READ) && diff --git a/app/views/index/normal.phtml b/app/views/index/normal.phtml index c7cab2d3f..d5ae8e2f9 100644 --- a/app/views/index/normal.phtml +++ b/app/views/index/normal.phtml @@ -3,6 +3,11 @@ $this->partial('aside_feed'); $this->partial('nav_menu'); +flush(); +if (isset($this->callbackBeforeEntries)) { + call_user_func($this->callbackBeforeEntries, $this); +} + if (!empty($this->entries)) { $display_today = true; $display_yesterday = true; @@ -20,7 +25,7 @@ if (!empty($this->entries)) { </div><?php foreach ($this->entries as $item) { $this->entry = Minz_ExtensionManager::callHook('entry_before_display', $item); - if (is_null($this->entry)) { + if ($this->entry == null) { continue; } @@ -67,10 +72,19 @@ if (!empty($this->entries)) { ?><div class="flux_content"> <div class="content <?php echo $content_width; ?>"> <h1 class="title"><a target="_blank" rel="noreferrer" class="go_website" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></h1> - <?php - $author = $this->entry->author(); - echo $author != '' ? '<div class="author">' . _t('gen.short.by_author', $author) . '</div>' : '', - $lazyload && $hidePosts ? lazyimg($this->entry->content()) : $this->entry->content(); + <div class="author"><?php + $authors = $this->entry->authors(); + if (is_array($authors)): + $first = true; + foreach ($authors as $author): + echo $first ? _t('gen.short.by_author') . ' ' : '· '; + $first = false; + ?> +<em><a href="<?php echo _url('index', 'index', 'search', 'author:' . str_replace(' ', '+', htmlspecialchars_decode($author, ENT_QUOTES))); ?>"><?php echo $author; ?></a></em> + <?php endforeach; ?> + </div><?php + endif; + echo $lazyload && $hidePosts ? lazyimg($this->entry->content()) : $this->entry->content(); ?> </div><?php diff --git a/app/views/index/reader.phtml b/app/views/index/reader.phtml index eb6613b28..c15b936ee 100644 --- a/app/views/index/reader.phtml +++ b/app/views/index/reader.phtml @@ -1,6 +1,11 @@ <?php $this->partial('nav_menu'); +flush(); +if (isset($this->callbackBeforeEntries)) { + call_user_func($this->callbackBeforeEntries, $this); +} + if (!empty($this->entries)) { $lazyload = FreshRSS_Context::$user_conf->lazyload; $content_width = FreshRSS_Context::$user_conf->content_width; @@ -39,9 +44,19 @@ if (!empty($this->entries)) { <h1 class="title"><a target="_blank" rel="noreferrer" class="go_website" href="<?php echo $item->link(); ?>"><?php echo $item->title(); ?></a></h1> <div class="author"><?php - $author = $item->author(); - echo $author != '' ? _t('gen.short.by_author', $author) . ' — ' : '', - $item->date(); + $authors = $item->authors(); + if (is_array($authors)): + $first = true; + foreach ($authors as $author): + echo $first ? _t('gen.short.by_author') . ' ' : '· '; + $first = false; + ?> +<em><a href="<?php echo _url('index', 'index', 'search', 'author:' . str_replace(' ', '+', htmlspecialchars_decode($author, ENT_QUOTES))); ?>"><?php echo $author; ?></a></em> + <?php + endforeach; + echo ' — '; + endif; + echo $item->date(); ?></div> <?php echo $item->content(); ?> diff --git a/app/views/index/rss.phtml b/app/views/index/rss.phtml index 86074517c..104e03d15 100755 --- a/app/views/index/rss.phtml +++ b/app/views/index/rss.phtml @@ -13,10 +13,20 @@ foreach ($this->entries as $item) { <item> <title><?php echo $item->title(); ?></title> <link><?php echo $item->link(); ?></link> - <?php $author = $item->author(); ?> - <?php if ($author != '') { ?> - <dc:creator><?php echo $author; ?></dc:creator> - <?php } ?> + <?php + $authors = $item->authors(); + if (is_array($authors)) { + foreach ($authors as $author) { + echo "\t\t\t" , '<author>', $author, '</author>', "\n"; + } + } + $categories = $item->tags(); + if (is_array($categories)) { + foreach ($categories as $category) { + echo "\t\t\t" , '<category>', $category, '</category>', "\n"; + } + } + ?> <description><![CDATA[<?php echo $item->content(); ?>]]></description> diff --git a/app/views/javascript/nbUnreadsPerFeed.phtml b/app/views/javascript/nbUnreadsPerFeed.phtml index 68f98ce9e..ce4db37b7 100644 --- a/app/views/javascript/nbUnreadsPerFeed.phtml +++ b/app/views/javascript/nbUnreadsPerFeed.phtml @@ -1,8 +1,14 @@ <?php -$result = array(); +$result = array( + 'feeds' => array(), + 'tags' => array(), +); foreach ($this->categories as $cat) { foreach ($cat->feeds() as $feed) { - $result[$feed->id()] = $feed->nbNotRead(); + $result['feeds'][$feed->id()] = $feed->nbNotRead(); } } +foreach ($this->tags as $tag) { + $result['tags'][$tag->id()] = $tag->nbUnread(); +} echo json_encode($result); diff --git a/app/views/tag/getTagsForEntry.phtml b/app/views/tag/getTagsForEntry.phtml new file mode 100644 index 000000000..76b2ada4e --- /dev/null +++ b/app/views/tag/getTagsForEntry.phtml @@ -0,0 +1,2 @@ +<?php +echo json_encode($this->tags); |
