diff options
Diffstat (limited to 'app')
48 files changed, 2038 insertions, 958 deletions
diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index 645f9eabf..a117e0f9c 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -29,7 +29,6 @@ class FreshRSS_configure_Controller extends Minz_ActionController { $cat = new FreshRSS_Category ($name); $values = array ( 'name' => $cat->name (), - 'color' => $cat->color () ); $catDAO->updateCategory ($ids[$key], $values); } elseif ($ids[$key] != $defaultId) { @@ -43,10 +42,9 @@ class FreshRSS_configure_Controller extends Minz_ActionController { $values = array ( 'id' => $cat->id (), 'name' => $cat->name (), - 'color' => $cat->color () ); - if ($catDAO->searchByName ($newCat) == false) { + if ($catDAO->searchByName ($newCat) == null) { $catDAO->addCategory ($values); } } @@ -64,7 +62,6 @@ class FreshRSS_configure_Controller extends Minz_ActionController { $this->view->categories = $catDAO->listCategories (false); $this->view->defaultCategory = $catDAO->getDefault (); $this->view->feeds = $feedDAO->listFeeds (); - $this->view->flux = false; Minz_View::prependTitle (Minz_Translate::t ('categories_management') . ' · '); } @@ -116,7 +113,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { if ($feedDAO->updateFeed ($id, $values)) { $this->view->flux->_category ($cat); - + $this->view->flux->faviconPrepare(); $notif = array ( 'type' => 'good', 'content' => Minz_Translate::t ('feed_updated') @@ -143,25 +140,12 @@ class FreshRSS_configure_Controller extends Minz_ActionController { public function displayAction () { if (Minz_Request::isPost()) { $this->view->conf->_language(Minz_Request::param('language', 'en')); - $this->view->conf->_posts_per_page(Minz_Request::param('posts_per_page', 10)); - $this->view->conf->_view_mode(Minz_Request::param('view_mode', 'normal')); - $this->view->conf->_default_view (Minz_Request::param('default_view', 'a')); - $this->view->conf->_auto_load_more(Minz_Request::param('auto_load_more', false)); - $this->view->conf->_display_posts(Minz_Request::param('display_posts', false)); - $this->view->conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false)); - $this->view->conf->_lazyload (Minz_Request::param('lazyload', false)); - $this->view->conf->_sort_order(Minz_Request::param('sort_order', 'DESC')); - $this->view->conf->_mark_when (array( - 'article' => Minz_Request::param('mark_open_article', false), - 'site' => Minz_Request::param('mark_open_site', false), - 'scroll' => Minz_Request::param('mark_scroll', false), - 'reception' => Minz_Request::param('mark_upon_reception', false), - )); $themeId = Minz_Request::param('theme', ''); if ($themeId == '') { $themeId = FreshRSS_Themes::defaultTheme; } $this->view->conf->_theme($themeId); + $this->view->conf->_content_width(Minz_Request::param('content_width', 'thin')); $this->view->conf->_topline_read(Minz_Request::param('topline_read', false)); $this->view->conf->_topline_favorite(Minz_Request::param('topline_favorite', false)); $this->view->conf->_topline_date(Minz_Request::param('topline_date', false)); @@ -189,22 +173,30 @@ class FreshRSS_configure_Controller extends Minz_ActionController { $this->view->themes = FreshRSS_Themes::get(); - Minz_View::prependTitle (Minz_Translate::t ('reading_configuration') . ' · '); + Minz_View::prependTitle (Minz_Translate::t ('display_configuration') . ' · '); } - public function sharingAction () { - if (Minz_Request::isPost ()) { - $this->view->conf->_sharing (array( - 'shaarli' => Minz_Request::param ('shaarli', false), - 'wallabag' => Minz_Request::param ('wallabag', false), - 'diaspora' => Minz_Request::param ('diaspora', false), - 'twitter' => Minz_Request::param ('twitter', false), - 'g+' => Minz_Request::param ('g+', false), - 'facebook' => Minz_Request::param ('facebook', false), - 'email' => Minz_Request::param ('email', false), - 'print' => Minz_Request::param ('print', false), + public function readingAction () { + if (Minz_Request::isPost()) { + $this->view->conf->_posts_per_page(Minz_Request::param('posts_per_page', 10)); + $this->view->conf->_view_mode(Minz_Request::param('view_mode', 'normal')); + $this->view->conf->_default_view (Minz_Request::param('default_view', 'a')); + $this->view->conf->_auto_load_more(Minz_Request::param('auto_load_more', false)); + $this->view->conf->_display_posts(Minz_Request::param('display_posts', false)); + $this->view->conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false)); + $this->view->conf->_lazyload (Minz_Request::param('lazyload', false)); + $this->view->conf->_sticky_post (Minz_Request::param('sticky_post', false)); + $this->view->conf->_sort_order(Minz_Request::param('sort_order', 'DESC')); + $this->view->conf->_mark_when (array( + 'article' => Minz_Request::param('mark_open_article', false), + 'site' => Minz_Request::param('mark_open_site', false), + 'scroll' => Minz_Request::param('mark_scroll', false), + 'reception' => Minz_Request::param('mark_upon_reception', false), )); $this->view->conf->save(); + + Minz_Session::_param ('language', $this->view->conf->language); + Minz_Translate::reset (); invalidateHttpCache(); $notif = array ( @@ -213,80 +205,34 @@ class FreshRSS_configure_Controller extends Minz_ActionController { ); Minz_Session::_param ('notification', $notif); - Minz_Request::forward (array ('c' => 'configure', 'a' => 'sharing'), true); + Minz_Request::forward (array ('c' => 'configure', 'a' => 'reading'), true); } - Minz_View::prependTitle (Minz_Translate::t ('sharing') . ' · '); + Minz_View::prependTitle (Minz_Translate::t ('reading_configuration') . ' · '); } - public function importExportAction () { - require_once(LIB_PATH . '/lib_opml.php'); - $catDAO = new FreshRSS_CategoryDAO (); - $this->view->categories = $catDAO->listCategories (); - - $this->view->req = Minz_Request::param ('q'); - - if ($this->view->req == 'export') { - Minz_View::_title ('freshrss_feeds.opml'); - - $this->view->_useLayout (false); - header('Content-Type: application/xml; charset=utf-8'); - header('Content-disposition: attachment; filename=freshrss_feeds.opml'); - - $feedDAO = new FreshRSS_FeedDAO (); - $catDAO = new FreshRSS_CategoryDAO (); - - $list = array (); - foreach ($catDAO->listCategories () as $key => $cat) { - $list[$key]['name'] = $cat->name (); - $list[$key]['feeds'] = $feedDAO->listByCategory ($cat->id ()); - } - - $this->view->categories = $list; - } elseif ($this->view->req == 'import' && Minz_Request::isPost ()) { - if ($_FILES['file']['error'] == 0) { - invalidateHttpCache(); - // on parse le fichier OPML pour récupérer les catégories et les flux associés - try { - list ($categories, $feeds) = opml_import ( - file_get_contents ($_FILES['file']['tmp_name']) - ); + public function sharingAction () { + if (Minz_Request::isPost ()) { + $params = Minz_Request::params(); + $this->view->conf->_sharing ($params['share']); + $this->view->conf->save(); + invalidateHttpCache(); - // On redirige vers le controller feed qui va se charger d'insérer les flux en BDD - // les flux sont mis au préalable dans des variables de Request - Minz_Request::_param ('q', 'null'); - Minz_Request::_param ('categories', $categories); - Minz_Request::_param ('feeds', $feeds); - Minz_Request::forward (array ('c' => 'feed', 'a' => 'massiveImport')); - } catch (FreshRSS_Opml_Exception $e) { - Minz_Log::record ($e->getMessage (), Minz_Log::WARNING); - - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('bad_opml_file') - ); - Minz_Session::_param ('notification', $notif); + $notif = array ( + 'type' => 'good', + 'content' => Minz_Translate::t ('configuration_updated') + ); + Minz_Session::_param ('notification', $notif); - Minz_Request::forward (array ( - 'c' => 'configure', - 'a' => 'importExport' - ), true); - } - } + Minz_Request::forward (array ('c' => 'configure', 'a' => 'sharing'), true); } - $feedDAO = new FreshRSS_FeedDAO (); - $this->view->feeds = $feedDAO->listFeeds (); - - // au niveau de la vue, permet de ne pas voir un flux sélectionné dans la liste - $this->view->flux = false; - - Minz_View::prependTitle (Minz_Translate::t ('import_export_opml') . ' · '); + Minz_View::prependTitle (Minz_Translate::t ('sharing') . ' · '); } public function shortcutAction () { $list_keys = array ('a', 'b', 'backspace', 'c', 'd', 'delete', 'down', 'e', 'end', 'enter', - 'escape', 'f', 'g', 'h', 'i', 'insert', 'j', 'k', 'l', 'left', + 'escape', 'f', 'g', 'h', 'home', 'i', 'insert', 'j', 'k', 'l', 'left', 'm', 'n', 'o', 'p', 'page_down', 'page_up', 'q', 'r', 'return', 'right', 's', 'space', 't', 'tab', 'u', 'up', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php index 1756c91e5..bbcb990f5 100755 --- a/app/Controllers/entryController.php +++ b/app/Controllers/entryController.php @@ -100,6 +100,9 @@ class FreshRSS_entry_Controller extends Minz_ActionController { $entryDAO = new FreshRSS_EntryDAO(); $entryDAO->optimizeTable(); + $feedDAO = new FreshRSS_FeedDAO(); + $feedDAO->updateCachedValues(); + invalidateHttpCache(); $notif = array ( @@ -137,11 +140,13 @@ class FreshRSS_entry_Controller extends Minz_ActionController { if ($nb > 0) { $nbTotal += $nb; Minz_Log::record($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG); - $feedDAO->updateLastUpdate($feed->id()); + //$feedDAO->updateLastUpdate($feed->id()); } } } + $feedDAO->updateCachedValues(); + invalidateHttpCache(); $notif = array( diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index c40b3c400..fce008399 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -3,28 +3,51 @@ class FreshRSS_feed_Controller extends Minz_ActionController { public function firstAction () { if (!$this->view->loginOk) { - $token = $this->view->conf->token; //TODO: check the token logic again, and if it is still needed + // Token is useful in the case that anonymous refresh is forbidden + // and CRON task cannot be used with php command so the user can + // set a CRON task to refresh his feeds by using token inside url + $token = $this->view->conf->token; $token_param = Minz_Request::param ('token', ''); $token_is_ok = ($token != '' && $token == $token_param); $action = Minz_Request::actionName (); - if (!($token_is_ok && $action === 'actualize')) { + if (!(($token_is_ok || Minz_Configuration::allowAnonymousRefresh()) && + $action === 'actualize') + ) { Minz_Error::error ( 403, array ('error' => array (Minz_Translate::t ('access_denied'))) ); } } + } + + public function addAction () { + $url = Minz_Request::param('url_rss', false); + if ($url === false) { + Minz_Request::forward(array( + 'c' => 'configure', + 'a' => 'feed' + ), true); + } + + $feedDAO = new FreshRSS_FeedDAO (); $this->catDAO = new FreshRSS_CategoryDAO (); $this->catDAO->checkDefault (); - } - public function addAction () { - @set_time_limit(300); + if (Minz_Request::isPost()) { + @set_time_limit(300); + - if (Minz_Request::isPost ()) { - $url = Minz_Request::param ('url_rss'); $cat = Minz_Request::param ('category', false); + if ($cat === 'nc') { + $new_cat = Minz_Request::param ('new_category'); + if (empty($new_cat['name'])) { + $cat = false; + } else { + $cat = $this->catDAO->addCategory($new_cat); + } + } if ($cat === false) { $def_cat = $this->catDAO->getDefault (); $cat = $def_cat->id (); @@ -47,7 +70,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feed->load(true); - $feedDAO = new FreshRSS_FeedDAO (); $values = array ( 'url' => $feed->url (), 'category' => $feed->category (), @@ -123,7 +145,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { Minz_Log::record ($e->getMessage (), Minz_Log::WARNING); $notif = array ( 'type' => 'bad', - 'content' => Minz_Translate::t ('internal_problem_feed') + 'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs'))) ); Minz_Session::_param ('notification', $notif); } catch (Minz_FileNotExistException $e) { @@ -131,7 +153,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { Minz_Log::record ($e->getMessage (), Minz_Log::ERROR); $notif = array ( 'type' => 'bad', - 'content' => Minz_Translate::t ('internal_problem_feed') + 'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs'))) ); Minz_Session::_param ('notification', $notif); } @@ -141,6 +163,38 @@ class FreshRSS_feed_Controller extends Minz_ActionController { Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => $params), true); } + + // GET request so we must ask confirmation to user + Minz_View::prependTitle(Minz_Translate::t('add_rss_feed') . ' · '); + $this->view->categories = $this->catDAO->listCategories(); + $this->view->feed = new FreshRSS_Feed($url); + try { + // We try to get some more information about the feed + $this->view->feed->load(true); + $this->view->load_ok = true; + } catch (Exception $e) { + $this->view->load_ok = false; + } + + $feed = $feedDAO->searchByUrl($this->view->feed->url()); + if ($feed) { + // Already subscribe so we redirect to the feed configuration page + $notif = array( + 'type' => 'bad', + 'content' => Minz_Translate::t( + 'already_subscribed', $feed->name() + ) + ); + Minz_Session::_param('notification', $notif); + + Minz_Request::forward(array( + 'c' => 'configure', + 'a' => 'feed', + 'params' => array( + 'id' => $feed->id() + ) + ), true); + } } public function truncateAction () { @@ -189,38 +243,51 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $flux_update = 0; $is_read = $this->view->conf->mark_when['reception'] ? 1 : 0; foreach ($feeds as $feed) { + if (!$feed->lock()) { + Minz_Log::record('Feed already being actualized: ' . $feed->url(), Minz_Log::NOTICE); + continue; + } try { $url = $feed->url(); + $feedHistory = $feed->keepHistory(); + $feed->load(false); $entries = array_reverse($feed->entries()); //We want chronological order and SimplePie uses reverse order + $hasTransaction = false; - //For this feed, check last n entry GUIDs already in database - $existingGuids = array_fill_keys ($entryDAO->listLastGuidsByFeed ($feed->id (), count($entries) + 10), 1); - $useDeclaredDate = empty($existingGuids); + if (count($entries) > 0) { + //For this feed, check last n entry GUIDs already in database + $existingGuids = array_fill_keys ($entryDAO->listLastGuidsByFeed ($feed->id (), count($entries) + 10), 1); + $useDeclaredDate = empty($existingGuids); - $feedHistory = $feed->keepHistory(); - if ($feedHistory == -2) { //default - $feedHistory = $this->view->conf->keep_history_default; - } + if ($feedHistory == -2) { //default + $feedHistory = $this->view->conf->keep_history_default; + } + + $hasTransaction = true; + $feedDAO->beginTransaction(); - // On ne vérifie pas strictement que l'article n'est pas déjà en BDD - // La BDD refusera l'ajout car (id_feed, guid) doit être unique - $feedDAO->beginTransaction (); - foreach ($entries as $entry) { - $eDate = $entry->date (true); - if ((!isset ($existingGuids[$entry->guid ()])) && - (($feedHistory != 0) || ($eDate >= $date_min))) { - $values = $entry->toArray (); - //Use declared date at first import, otherwise use discovery date - $values['id'] = ($useDeclaredDate || $eDate < $date_min) ? - min(time(), $eDate) . uSecString() : - uTimeString(); - $values['is_read'] = $is_read; - $entryDAO->addEntry ($values); + // On ne vérifie pas strictement que l'article n'est pas déjà en BDD + // La BDD refusera l'ajout car (id_feed, guid) doit être unique + foreach ($entries as $entry) { + $eDate = $entry->date (true); + if ((!isset ($existingGuids[$entry->guid ()])) && + (($feedHistory != 0) || ($eDate >= $date_min))) { + $values = $entry->toArray (); + //Use declared date at first import, otherwise use discovery date + $values['id'] = ($useDeclaredDate || $eDate < $date_min) ? + min(time(), $eDate) . uSecString() : + uTimeString(); + $values['is_read'] = $is_read; + $entryDAO->addEntry ($values); + } } } if (($feedHistory >= 0) && (rand(0, 30) === 1)) { + if (!$hasTransaction) { + $feedDAO->beginTransaction(); + } $nb = $feedDAO->cleanOldEntries ($feed->id (), $date_min, max($feedHistory, count($entries) + 10)); if ($nb > 0) { Minz_Log::record ($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG); @@ -228,18 +295,23 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } // on indique que le flux vient d'être mis à jour en BDD - $feedDAO->updateLastUpdate ($feed->id ()); - $feedDAO->commit (); + $feedDAO->updateLastUpdate ($feed->id (), 0, $hasTransaction); + if ($hasTransaction) { + $feedDAO->commit(); + } $flux_update++; if ($feed->url() !== $url) { //URL has changed (auto-discovery) $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); } - $feed->faviconPrepare(); } catch (FreshRSS_Feed_Exception $e) { Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE); $feedDAO->updateLastUpdate ($feed->id (), 1); } + $feed->faviconPrepare(); + $feed->unlock(); + unset($feed); + // On arrête à 10 flux pour ne pas surcharger le serveur // sauf si le paramètre $force est à vrai $i++; @@ -251,6 +323,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $url = array (); if ($flux_update === 1) { // on a mis un seul flux à jour + $feed = reset ($feeds); $notif = array ( 'type' => 'good', 'content' => Minz_Translate::t ('feed_actualized', $feed->name ()) @@ -264,8 +337,8 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } else { // aucun flux n'a été mis à jour, oups $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('no_feed_actualized') + 'type' => 'good', + 'content' => Minz_Translate::t ('no_feed_to_refresh') ); } @@ -295,77 +368,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } } - public function massiveImportAction () { - @set_time_limit(300); - - $entryDAO = new FreshRSS_EntryDAO (); - $feedDAO = new FreshRSS_FeedDAO (); - - $categories = Minz_Request::param ('categories', array (), true); - $feeds = Minz_Request::param ('feeds', array (), true); - - // on ajoute les catégories en masse dans une fonction à part - $this->addCategories ($categories); - - // on calcule la date des articles les plus anciens qu'on accepte - $nb_month_old = $this->view->conf->old_entries; - $date_min = time () - (3600 * 24 * 30 * $nb_month_old); - - // la variable $error permet de savoir si une erreur est survenue - // Le but est de ne pas arrêter l'import même en cas d'erreur - // L'utilisateur sera mis au courant s'il y a eu des erreurs, mais - // ne connaîtra pas les détails. Ceux-ci seront toutefois logguées - $error = false; - $i = 0; - foreach ($feeds as $feed) { - try { - $values = array ( - 'id' => $feed->id (), - 'url' => $feed->url (), - 'category' => $feed->category (), - 'name' => $feed->name (), - 'website' => $feed->website (), - 'description' => $feed->description (), - 'lastUpdate' => 0, - 'httpAuth' => $feed->httpAuth () - ); - - // ajout du flux que s'il n'est pas déjà en BDD - if (!$feedDAO->searchByUrl ($values['url'])) { - $id = $feedDAO->addFeed ($values); - if ($id) { - $feed->_id ($id); - $feed->faviconPrepare(); - } else { - $error = true; - } - } - } catch (FreshRSS_Feed_Exception $e) { - $error = true; - Minz_Log::record ($e->getMessage (), Minz_Log::WARNING); - } - } - - if ($error) { - $res = Minz_Translate::t ('feeds_imported_with_errors'); - } else { - $res = Minz_Translate::t ('feeds_imported'); - } - - $notif = array ( - 'type' => 'good', - 'content' => $res - ); - Minz_Session::_param ('notification', $notif); - Minz_Session::_param ('actualize_feeds', true); - - // et on redirige vers la page d'accueil - Minz_Request::forward (array ( - 'c' => 'index', - 'a' => 'index' - ), true); - } - public function deleteAction () { if (Minz_Request::isPost ()) { $type = Minz_Request::param ('type', 'feed'); @@ -409,17 +411,4 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } } } - - private function addCategories ($categories) { - foreach ($categories as $cat) { - if (!$this->catDAO->searchByName ($cat->name ())) { - $values = array ( - 'id' => $cat->id (), - 'name' => $cat->name (), - 'color' => $cat->color () - ); - $catDAO->addCategory ($values); - } - } - } } diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php new file mode 100644 index 000000000..3cd791781 --- /dev/null +++ b/app/Controllers/importExportController.php @@ -0,0 +1,390 @@ +<?php + +class FreshRSS_importExport_Controller extends Minz_ActionController { + public function firstAction() { + if (!$this->view->loginOk) { + Minz_Error::error( + 403, + array('error' => array(Minz_Translate::t('access_denied'))) + ); + } + + require_once(LIB_PATH . '/lib_opml.php'); + + $this->catDAO = new FreshRSS_CategoryDAO(); + $this->entryDAO = new FreshRSS_EntryDAO(); + $this->feedDAO = new FreshRSS_FeedDAO(); + } + + public function indexAction() { + $this->view->categories = $this->catDAO->listCategories(); + $this->view->feeds = $this->feedDAO->listFeeds(); + + Minz_View::prependTitle(Minz_Translate::t('import_export') . ' · '); + } + + public function importAction() { + if (Minz_Request::isPost() && $_FILES['file']['error'] == 0) { + @set_time_limit(300); + + $file = $_FILES['file']; + $type_file = $this->guessFileType($file['name']); + + $list_files = array( + 'opml' => array(), + 'json_starred' => array(), + 'json_feed' => array() + ); + + // We try to list all files according to their type + // A zip file is first opened and then its files are listed + $list = array(); + if ($type_file === 'zip') { + $zip = zip_open($file['tmp_name']); + + while (($zipfile = zip_read($zip)) !== false) { + $type_zipfile = $this->guessFileType( + zip_entry_name($zipfile) + ); + + if ($type_file !== 'unknown') { + $list_files[$type_zipfile][] = zip_entry_read( + $zipfile, + zip_entry_filesize($zipfile) + ); + } + } + + zip_close($zip); + } elseif ($type_file !== 'unknown') { + $list_files[$type_file][] = file_get_contents( + $file['tmp_name'] + ); + } + + // Import different files. + // OPML first(so categories and feeds are imported) + // Starred articles then so the "favourite" status is already set + // And finally all other files. + $error = false; + foreach ($list_files['opml'] as $opml_file) { + $error = $this->importOpml($opml_file); + } + foreach ($list_files['json_starred'] as $article_file) { + $error = $this->importArticles($article_file, true); + } + foreach ($list_files['json_feed'] as $article_file) { + $error = $this->importArticles($article_file); + } + + // And finally, we get import status and redirect to the home page + $notif = null; + if ($error === true) { + $content_notif = Minz_Translate::t( + 'feeds_imported_with_errors' + ); + } else { + $content_notif = Minz_Translate::t( + 'feeds_imported' + ); + } + + Minz_Session::_param('notification', array( + 'type' => 'good', + 'content' => $content_notif + )); + Minz_Session::_param('actualize_feeds', true); + + Minz_Request::forward(array( + 'c' => 'index', + 'a' => 'index' + ), true); + } + + // What are you doing? you have to call this controller + // with a POST request! + Minz_Request::forward(array( + 'c' => 'importExport', + 'a' => 'index' + )); + } + + private function guessFileType($filename) { + // A *very* basic guess file type function. Only based on filename + // That's could be improved but should be enough, at least for a first + // implementation. + // TODO: improve this function? + + if (substr_compare($filename, '.zip', -4) === 0) { + return 'zip'; + } elseif (substr_compare($filename, '.opml', -5) === 0 || + substr_compare($filename, '.xml', -4) === 0) { + return 'opml'; + } elseif (strcmp($filename, 'starred.json') === 0) { + return 'json_starred'; + } elseif (substr_compare($filename, '.json', -5) === 0 && + strpos($filename, 'feed_') === 0) { + return 'json_feed'; + } else { + return 'unknown'; + } + } + + private function importOpml($opml_file) { + $opml_array = array(); + try { + $opml_array = libopml_parse_string($opml_file); + } catch (LibOPML_Exception $e) { + Minz_Log::warning($e->getMessage()); + return true; + } + + $this->catDAO->checkDefault(); + + return $this->addOpmlElements($opml_array['body']); + } + + private function addOpmlElements($opml_elements, $parent_cat = null) { + $error = false; + foreach ($opml_elements as $elt) { + $res = false; + if (isset($elt['xmlUrl'])) { + $res = $this->addFeedOpml($elt, $parent_cat); + } else { + $res = $this->addCategoryOpml($elt, $parent_cat); + } + + if (!$error && $res) { + // oops: there is at least one error! + $error = $res; + } + } + + return $error; + } + + private function addFeedOpml($feed_elt, $parent_cat) { + if (is_null($parent_cat)) { + // This feed has no parent category so we get the default one + $parent_cat = $this->catDAO->getDefault()->name(); + } + + $cat = $this->catDAO->searchByName($parent_cat); + + if (!$cat) { + return true; + } + + // We get different useful information + $url = html_chars_utf8($feed_elt['xmlUrl']); + $name = html_chars_utf8($feed_elt['text']); + $website = ''; + if (isset($feed_elt['htmlUrl'])) { + $website = html_chars_utf8($feed_elt['htmlUrl']); + } + $description = ''; + if (isset($feed_elt['description'])) { + $description = html_chars_utf8($feed_elt['description']); + } + + $error = false; + try { + // Create a Feed object and add it in DB + $feed = new FreshRSS_Feed($url); + $feed->_category($cat->id()); + $feed->_name($name); + $feed->_website($website); + $feed->_description($description); + + // addFeedObject checks if feed is already in DB so nothing else to + // check here + $id = $this->feedDAO->addFeedObject($feed); + $error = ($id === false); + } catch (FreshRSS_Feed_Exception $e) { + Minz_Log::warning($e->getMessage()); + $error = true; + } + + return $error; + } + + private function addCategoryOpml($cat_elt, $parent_cat) { + // Create a new Category object + $cat = new FreshRSS_Category(html_chars_utf8($cat_elt['text'])); + + $id = $this->catDAO->addCategoryObject($cat); + $error = ($id === false); + + if (isset($cat_elt['@outlines'])) { + // Our cat_elt contains more categories or more feeds, so we + // add them recursively. + // Note: FreshRSS does not support yet category arborescence + $res = $this->addOpmlElements($cat_elt['@outlines'], $cat->name()); + if (!$error && $res) { + $error = true; + } + } + + return $error; + } + + private function importArticles($article_file, $starred = false) { + $article_object = json_decode($article_file, true); + if (is_null($article_object)) { + Minz_Log::warning('Try to import a non-JSON file'); + return true; + } + + $is_read = $this->view->conf->mark_when['reception'] ? 1 : 0; + + $google_compliant = ( + strpos($article_object['id'], 'com.google') !== false + ); + + $error = false; + foreach ($article_object['items'] as $item) { + $feed = $this->addFeedArticles($item['origin'], $google_compliant); + if (is_null($feed)) { + $error = true; + continue; + } + + $author = isset($item['author']) ? $item['author'] : ''; + $key_content = ($google_compliant && !isset($item['content'])) ? + 'summary' : 'content'; + $tags = $item['categories']; + if ($google_compliant) { + $tags = array_filter($tags, function($var) { + return strpos($var, '/state/com.google') === false; + }); + } + + $entry = new FreshRSS_Entry( + $feed->id(), $item['id'], $item['title'], $author, + $item[$key_content]['content'], $item['alternate'][0]['href'], + $item['published'], $is_read, $starred + ); + $entry->_tags($tags); + + $id = $this->entryDAO->addEntryObject( + $entry, $this->view->conf, $feed->keepHistory() + ); + + if (!$error && ($id === false)) { + $error = true; + } + } + + return $error; + } + + private function addFeedArticles($origin, $google_compliant) { + $default_cat = $this->catDAO->getDefault(); + + $return = null; + $key = $google_compliant ? 'htmlUrl' : 'feedUrl'; + $url = $origin[$key]; + $name = $origin['title']; + $website = $origin['htmlUrl']; + $error = false; + try { + // Create a Feed object and add it in DB + $feed = new FreshRSS_Feed($url); + $feed->_category($default_cat->id()); + $feed->_name($name); + $feed->_website($website); + + // addFeedObject checks if feed is already in DB so nothing else to + // check here + $id = $this->feedDAO->addFeedObject($feed); + + if ($id !== false) { + $feed->_id($id); + $return = $feed; + } + } catch (FreshRSS_Feed_Exception $e) { + Minz_Log::warning($e->getMessage()); + } + + return $return; + } + + public function exportAction() { + if (Minz_Request::isPost()) { + $this->view->_useLayout(false); + + $export_opml = Minz_Request::param('export_opml', false); + $export_starred = Minz_Request::param('export_starred', false); + $export_feeds = Minz_Request::param('export_feeds', false); + + // From https://stackoverflow.com/questions/1061710/php-zip-files-on-the-fly + $file = tempnam('tmp', 'zip'); + $zip = new ZipArchive(); + $zip->open($file, ZipArchive::OVERWRITE); + + // Stuff with content + if ($export_opml) { + $zip->addFromString( + 'feeds.opml', $this->generateOpml() + ); + } + if ($export_starred) { + $zip->addFromString( + 'starred.json', $this->generateArticles('starred') + ); + } + foreach ($export_feeds as $feed_id) { + $feed = $this->feedDAO->searchById($feed_id); + $zip->addFromString( + 'feed_' . $feed->category() . '_' . $feed->id() . '.json', + $this->generateArticles('feed', $feed) + ); + } + + // Close and send to user + $zip->close(); + header('Content-Type: application/zip'); + header('Content-Length: ' . filesize($file)); + header('Content-Disposition: attachment; filename="freshrss_export.zip"'); + readfile($file); + unlink($file); + } + } + + private function generateOpml() { + $list = array(); + foreach ($this->catDAO->listCategories() as $key => $cat) { + $list[$key]['name'] = $cat->name(); + $list[$key]['feeds'] = $this->feedDAO->listByCategory($cat->id()); + } + + $this->view->categories = $list; + return $this->view->helperToString('export/opml'); + } + + private function generateArticles($type, $feed = NULL) { + $this->view->categories = $this->catDAO->listCategories(); + + if ($type == 'starred') { + $this->view->list_title = Minz_Translate::t('starred_list'); + $this->view->type = 'starred'; + $unread_fav = $this->entryDAO->countUnreadReadFavorites(); + $this->view->entries = $this->entryDAO->listWhere( + 's', '', FreshRSS_Entry::STATE_ALL, 'ASC', + $unread_fav['all'] + ); + } elseif ($type == 'feed' && !is_null($feed)) { + $this->view->list_title = Minz_Translate::t( + 'feed_list', $feed->name() + ); + $this->view->type = 'feed/' . $feed->id(); + $this->view->entries = $this->entryDAO->listWhere( + 'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', + $this->view->conf->posts_per_page + ); + $this->view->feed = $feed; + } + + return $this->view->helperToString('export/articles'); + } +} diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index 986a322a1..c843748c3 100755 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -5,27 +5,32 @@ class FreshRSS_index_Controller extends Minz_ActionController { public function indexAction () { $output = Minz_Request::param ('output'); - $token = ''; + $token = $this->view->conf->token; // check if user is logged in - if (!$this->view->loginOk && !Minz_Configuration::allowAnonymous()) - { - $token = $this->view->conf->token; + if (!$this->view->loginOk && !Minz_Configuration::allowAnonymous()) { $token_param = Minz_Request::param ('token', ''); $token_is_ok = ($token != '' && $token === $token_param); - if (!($output === 'rss' && $token_is_ok)) { + if ($output === 'rss' && !$token_is_ok) { + Minz_Error::error ( + 403, + array ('error' => array (Minz_Translate::t ('access_denied'))) + ); + return; + } elseif ($output !== 'rss') { + // "hard" redirection is not required, just ask dispatcher to + // forward to the login form without 302 redirection + Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin')); return; } - $params['token'] = $token; } - // construction of RSS url of this feed $params = Minz_Request::params (); - $params['output'] = 'rss'; if (isset ($params['search'])) { $params['search'] = urlencode ($params['search']); } - $this->view->rss_url = array ( + + $this->view->url = array ( 'c' => 'index', 'a' => 'index', 'params' => $params @@ -75,20 +80,22 @@ class FreshRSS_index_Controller extends Minz_ActionController { // On récupère les différents éléments de filtrage $this->view->state = $state = Minz_Request::param ('state', $this->view->conf->default_view); + $state_param = Minz_Request::param ('state', null); $filter = Minz_Request::param ('search', ''); if (!empty($filter)) { - $state = 'all'; //Search always in read and unread articles + $state = FreshRSS_Entry::STATE_ALL; //Search always in read and unread articles } $this->view->order = $order = Minz_Request::param ('order', $this->view->conf->sort_order); $nb = Minz_Request::param ('nb', $this->view->conf->posts_per_page); $first = Minz_Request::param ('next', ''); - if ($state === 'not_read') { //Any unread article in this category at all? + if ($state === FreshRSS_Entry::STATE_NOT_READ) { //Any unread article in this category at all? switch ($getType) { case 'a': $hasUnread = $this->view->nb_not_read > 0; break; case 's': + // This is deprecated. The favorite button does not exist anymore $hasUnread = $this->view->nb_favorites['unread'] > 0; break; case 'c': @@ -102,8 +109,8 @@ class FreshRSS_index_Controller extends Minz_ActionController { $hasUnread = true; break; } - if (!$hasUnread) { - $this->view->state = $state = 'all'; + if (!$hasUnread && ($state_param === null)) { + $this->view->state = $state = FreshRSS_Entry::STATE_ALL; } } @@ -116,14 +123,14 @@ class FreshRSS_index_Controller extends Minz_ActionController { $keepHistoryDefault = $this->view->conf->keep_history_default; try { - $entries = $entryDAO->listWhere($getType, $getId, $state, $order, $nb + 1, $first, $filter, $date_min, $keepHistoryDefault); + $entries = $entryDAO->listWhere($getType, $getId, $state, $order, $nb + 1, $first, $filter, $date_min, true, $keepHistoryDefault); // Si on a récupéré aucun article "non lus" // on essaye de récupérer tous les articles - if ($state === 'not_read' && empty($entries)) { + if ($state === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null)) { Minz_Log::record ('Conflicting information about nbNotRead!', Minz_Log::DEBUG); - $this->view->state = 'all'; - $entries = $entryDAO->listWhere($getType, $getId, 'all', $order, $nb, $first, $filter, $date_min, $keepHistoryDefault); + $this->view->state = FreshRSS_Entry::STATE_ALL; + $entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb, $first, $filter, $date_min, true, $keepHistoryDefault); } if (count($entries) <= $nb) { @@ -342,6 +349,37 @@ class FreshRSS_index_Controller extends Minz_ActionController { } $this->view->_useLayout(false); Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); + } elseif (Minz_Configuration::unsafeAutologinEnabled() && isset($_GET['u']) && isset($_GET['p'])) { + Minz_Session::_param('currentUser'); + Minz_Session::_param('mail'); + Minz_Session::_param('passwordHash'); + $username = ctype_alnum($_GET['u']) ? $_GET['u'] : ''; + $passwordPlain = $_GET['p']; + Minz_Request::_param('p'); //Discard plain-text password ASAP + $_GET['p'] = ''; + if (!function_exists('password_verify')) { + include_once(LIB_PATH . '/password_compat.php'); + } + try { + $conf = new FreshRSS_Configuration($username); + $s = $conf->passwordHash; + $ok = password_verify($passwordPlain, $s); + unset($passwordPlain); + if ($ok) { + Minz_Session::_param('currentUser', $username); + Minz_Session::_param('passwordHash', $s); + } else { + Minz_Log::record('Unsafe password mismatch for user ' . $username, Minz_Log::WARNING); + } + } catch (Minz_Exception $me) { + Minz_Log::record('Unsafe login failure: ' . $me->getMessage(), Minz_Log::WARNING); + } + Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); + } elseif (!Minz_Configuration::canLogIn()) { + Minz_Error::error ( + 403, + array ('error' => array (Minz_Translate::t ('access_denied'))) + ); } invalidateHttpCache(); } diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php index b879dcd6d..3d741e298 100755 --- a/app/Controllers/javascriptController.php +++ b/app/Controllers/javascriptController.php @@ -8,7 +8,7 @@ class FreshRSS_javascript_Controller extends Minz_ActionController { public function actualizeAction () { header('Content-Type: text/javascript; charset=UTF-8'); $feedDAO = new FreshRSS_FeedDAO (); - $this->view->feeds = $feedDAO->listFeeds (); + $this->view->feeds = $feedDAO->listFeedsOrderUpdate(); } public function nbUnreadsPerFeedAction() { diff --git a/app/Controllers/usersController.php b/app/Controllers/usersController.php index 8314b75fc..fa967cedc 100644 --- a/app/Controllers/usersController.php +++ b/app/Controllers/usersController.php @@ -32,6 +32,18 @@ class FreshRSS_users_Controller extends Minz_ActionController { } Minz_Session::_param('passwordHash', $this->view->conf->passwordHash); + $passwordPlain = Minz_Request::param('apiPasswordPlain', false); + if ($passwordPlain != '') { + if (!function_exists('password_hash')) { + include_once(LIB_PATH . '/password_compat.php'); + } + $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST)); + $passwordPlain = ''; + $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js + $ok &= ($passwordHash != ''); + $this->view->conf->_apiPasswordHash($passwordHash); + } + if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { $this->view->conf->_mail_login(Minz_Request::param('mail_login', false)); } @@ -54,11 +66,22 @@ class FreshRSS_users_Controller extends Minz_ActionController { $anon = Minz_Request::param('anon_access', false); $anon = ((bool)$anon) && ($anon !== 'no'); + $anon_refresh = Minz_Request::param('anon_refresh', false); + $anon_refresh = ((bool)$anon_refresh) && ($anon_refresh !== 'no'); $auth_type = Minz_Request::param('auth_type', 'none'); + $unsafe_autologin = Minz_Request::param('unsafe_autologin', false); + $api_enabled = Minz_Request::param('api_enabled', false); if ($anon != Minz_Configuration::allowAnonymous() || - $auth_type != Minz_Configuration::authType()) { + $auth_type != Minz_Configuration::authType() || + $anon_refresh != Minz_Configuration::allowAnonymousRefresh() || + $unsafe_autologin != Minz_Configuration::unsafeAutologinEnabled() || + $api_enabled != Minz_Configuration::apiEnabled()) { + Minz_Configuration::_authType($auth_type); Minz_Configuration::_allowAnonymous($anon); + Minz_Configuration::_allowAnonymousRefresh($anon_refresh); + Minz_Configuration::_enableAutologin($unsafe_autologin); + Minz_Configuration::_enableApi($api_enabled); $ok &= Minz_Configuration::writeFile(); } } diff --git a/app/Exceptions/OpmlException.php b/app/Exceptions/OpmlException.php deleted file mode 100644 index e0ea3e493..000000000 --- a/app/Exceptions/OpmlException.php +++ /dev/null @@ -1,6 +0,0 @@ -<?php -class FreshRSS_Opml_Exception extends FreshRSS_Feed_Exception { - public function __construct ($name_file) { - parent::__construct ('OPML file is invalid'); - } -} diff --git a/app/FreshRSS.php b/app/FreshRSS.php index c51f91dec..84cf3429b 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -94,10 +94,6 @@ class FreshRSS extends Minz_FrontController { $loginOk = false; break; } - if ((!$loginOk) && (PHP_SAPI === 'cli') && (Minz_Request::actionName() === 'actualize')) { //Command line - Minz_Configuration::_authType('none'); - $loginOk = true; - } } Minz_View::_param ('loginOk', $loginOk); return $loginOk; diff --git a/app/Models/Category.php b/app/Models/Category.php index 8e1e44ef8..328bae799 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -3,14 +3,12 @@ class FreshRSS_Category extends Minz_Model { private $id = 0; private $name; - private $color; private $nbFeed = -1; private $nbNotRead = -1; private $feeds = null; - public function __construct ($name = '', $color = '#0062BE', $feeds = null) { + public function __construct ($name = '', $feeds = null) { $this->_name ($name); - $this->_color ($color); if (isset ($feeds)) { $this->_feeds ($feeds); $this->nbFeed = 0; @@ -28,9 +26,6 @@ class FreshRSS_Category extends Minz_Model { public function name () { return $this->name; } - public function color () { - return $this->color; - } public function nbFeed () { if ($this->nbFeed < 0) { $catDAO = new FreshRSS_CategoryDAO (); @@ -68,13 +63,6 @@ class FreshRSS_Category extends Minz_Model { public function _name ($value) { $this->name = $value; } - public function _color ($value) { - if (preg_match ('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) { - $this->color = $value; - } else { - $this->color = '#0062BE'; - } - } public function _feeds ($values) { if (!is_array ($values)) { $values = array ($values); diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index 1cc616ac0..6a9b839b9 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -2,12 +2,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { public function addCategory ($valuesTmp) { - $sql = 'INSERT INTO `' . $this->prefix . 'category` (name, color) VALUES(?, ?)'; + $sql = 'INSERT INTO `' . $this->prefix . 'category` (name) VALUES(?)'; $stm = $this->bd->prepare ($sql); $values = array ( substr($valuesTmp['name'], 0, 255), - substr($valuesTmp['color'], 0, 7), ); if ($stm && $stm->execute ($values)) { @@ -19,13 +18,25 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { } } + public function addCategoryObject($category) { + $cat = $this->searchByName($category->name()); + if (!$cat) { + // Category does not exist yet in DB so we add it before continue + $values = array( + 'name' => $category->name(), + ); + return $this->addCategory($values); + } + + return $cat->id(); + } + public function updateCategory ($id, $valuesTmp) { - $sql = 'UPDATE `' . $this->prefix . 'category` SET name=?, color=? WHERE id=?'; + $sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=?'; $stm = $this->bd->prepare ($sql); $values = array ( $valuesTmp['name'], - $valuesTmp['color'], $id ); @@ -66,7 +77,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if (isset ($cat[0])) { return $cat[0]; } else { - return false; + return null; } } public function searchByName ($name) { @@ -82,14 +93,13 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if (isset ($cat[0])) { return $cat[0]; } else { - return false; + return null; } } public function listCategories ($prePopulateFeeds = true, $details = false) { if ($prePopulateFeeds) { $sql = 'SELECT c.id AS c_id, c.name AS c_name, ' - . ($details ? 'c.color AS c_color, ' : '') . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.cache_nbEntries, f.cache_nbUnreads ') . 'FROM `' . $this->prefix . 'category` c ' . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category = c.id ' @@ -123,14 +133,13 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { public function checkDefault () { $def_cat = $this->searchById (1); - if ($def_cat === false) { + if ($def_cat == null) { $cat = new FreshRSS_Category (Minz_Translate::t ('default_category')); $cat->_id (1); $values = array ( 'id' => $cat->id (), 'name' => $cat->name (), - 'color' => $cat->color () ); $this->addCategory ($values); @@ -203,7 +212,6 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { // End of the current category, we add it to the $list $cat = new FreshRSS_Category ( $previousLine['c_name'], - isset($previousLine['c_color']) ? $previousLine['c_color'] : '', FreshRSS_FeedDAO::daoToFeed ($feedsDao, $previousLine['c_id']) ); $cat->_id ($previousLine['c_id']); @@ -220,7 +228,6 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if ($previousLine != null) { $cat = new FreshRSS_Category ( $previousLine['c_name'], - isset($previousLine['c_color']) ? $previousLine['c_color'] : '', FreshRSS_FeedDAO::daoToFeed ($feedsDao, $previousLine['c_id']) ); $cat->_id ($previousLine['c_id']); @@ -239,8 +246,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { foreach ($listDAO as $key => $dao) { $cat = new FreshRSS_Category ( - $dao['name'], - $dao['color'] + $dao['name'] ); $cat->_id ($dao['id']); $list[$key] = $cat; diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php index 2a7fe95aa..0d6666297 100644 --- a/app/Models/Configuration.php +++ b/app/Models/Configuration.php @@ -10,13 +10,15 @@ class FreshRSS_Configuration { 'mail_login' => '', 'token' => '', 'passwordHash' => '', //CRYPT_BLOWFISH + 'apiPasswordHash' => '', //CRYPT_BLOWFISH 'posts_per_page' => 20, 'view_mode' => 'normal', - 'default_view' => 'not_read', + 'default_view' => FreshRSS_Entry::STATE_NOT_READ, 'auto_load_more' => true, 'display_posts' => false, 'onread_jump_next' => true, 'lazyload' => true, + 'sticky_post' => true, 'sort_order' => 'DESC', 'anon_access' => false, 'mark_when' => array( @@ -26,15 +28,19 @@ class FreshRSS_Configuration { 'reception' => false, ), 'theme' => 'Origine', + 'content_width' => 'thin', 'shortcuts' => array( 'mark_read' => 'r', 'mark_favorite' => 'f', 'go_website' => 'space', 'next_entry' => 'j', 'prev_entry' => 'k', + 'first_entry' => 'home', + 'last_entry' => 'end', 'collapse_entry' => 'c', 'load_more' => 'm', 'auto_share' => 's', + 'focus_search' => 'a', ), 'topline_read' => true, 'topline_favorite' => true, @@ -46,16 +52,7 @@ class FreshRSS_Configuration { 'bottomline_tags' => true, 'bottomline_date' => true, 'bottomline_link' => true, - 'sharing' => array( - 'shaarli' => '', - 'wallabag' => '', - 'diaspora' => '', - 'twitter' => true, - 'g+' => true, - 'facebook' => true, - 'email' => true, - 'print' => true, - ), + 'sharing' => array(), ); private $available_languages = array( @@ -63,8 +60,10 @@ class FreshRSS_Configuration { 'fr' => 'Français', ); - public function __construct ($user) { - $this->filename = DATA_PATH . '/' . $user . '_user.php'; + private $shares; + + public function __construct($user) { + $this->filename = DATA_PATH . DIRECTORY_SEPARATOR . $user . '_user.php'; $data = @include($this->filename); if (!is_array($data)) { @@ -78,10 +77,20 @@ class FreshRSS_Configuration { } } $this->data['user'] = $user; + + $this->shares = DATA_PATH . DIRECTORY_SEPARATOR . 'shares.php'; + + $shares = @include($this->shares); + if (!is_array($shares)) { + throw new Minz_PermissionDeniedException($this->shares); + } + + $this->data['shares'] = $shares; } public function save() { @rename($this->filename, $this->filename . '.bak.php'); + unset($this->data['shares']); // Remove shares because it is not intended to be stored in user configuration if (file_put_contents($this->filename, "<?php\n return " . var_export($this->data, true) . ';', LOCK_EX) === false) { throw new Minz_PermissionDeniedException($this->filename); } @@ -102,16 +111,6 @@ class FreshRSS_Configuration { } } - public function sharing($key = false) { - if ($key === false) { - return $this->data['sharing']; - } - if (isset($this->data['sharing'][$key])) { - return $this->data['sharing'][$key]; - } - return false; - } - public function availableLanguages() { return $this->available_languages; } @@ -134,7 +133,7 @@ class FreshRSS_Configuration { } } public function _default_view ($value) { - $this->data['default_view'] = $value === 'all' ? 'all' : 'not_read'; + $this->data['default_view'] = $value === FreshRSS_Entry::STATE_ALL ? FreshRSS_Entry::STATE_ALL : FreshRSS_Entry::STATE_NOT_READ; } public function _display_posts ($value) { $this->data['display_posts'] = ((bool)$value) && $value !== 'no'; @@ -145,6 +144,9 @@ class FreshRSS_Configuration { public function _lazyload ($value) { $this->data['lazyload'] = ((bool)$value) && $value !== 'no'; } + public function _sticky_post($value) { + $this->data['sticky_post'] = ((bool)$value) && $value !== 'no'; + } public function _sort_order ($value) { $this->data['sort_order'] = $value === 'ASC' ? 'ASC' : 'DESC'; } @@ -166,6 +168,9 @@ class FreshRSS_Configuration { public function _passwordHash ($value) { $this->data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; } + public function _apiPasswordHash ($value) { + $this->data['apiPasswordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; + } public function _mail_login ($value) { $value = filter_var($value, FILTER_VALIDATE_EMAIL); if ($value) { @@ -185,29 +190,47 @@ class FreshRSS_Configuration { } } public function _sharing ($values) { - $are_url = array ('shaarli', 'wallabag', 'diaspora'); - foreach ($values as $key => $value) { - if (in_array($key, $are_url)) { + $this->data['sharing'] = array(); + foreach ($values as $value) { + if (!is_array($value)) { + continue; + } + + // Verify URL and add default value when needed + if (isset($value['url'])) { $is_url = ( - filter_var ($value, FILTER_VALIDATE_URL) || + filter_var ($value['url'], FILTER_VALIDATE_URL) || (version_compare(PHP_VERSION, '5.3.3', '<') && (strpos($value, '-') > 0) && ($value === filter_var($value, FILTER_SANITIZE_URL))) ); //PHP bug #51192 - if (!$is_url) { - $value = ''; + continue; } - } elseif (!is_bool($value)) { - $value = true; + } else { + $value['url'] = null; } - $this->data['sharing'][$key] = $value; + // Add a default name + if (empty($value['name'])) { + $value['name'] = $value['type']; + } + + $this->data['sharing'][] = $value; } } public function _theme($value) { $this->data['theme'] = $value; } + public function _content_width($value) { + if ($value === 'medium' || + $value === 'large' || + $value === 'no_limit') { + $this->data['content_width'] = $value; + } else { + $this->data['content_width'] = 'thin'; + } + } public function _token($value) { $this->data['token'] = $value; } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index a6c67221b..fa9066d5b 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -1,6 +1,11 @@ <?php class FreshRSS_Entry extends Minz_Model { + const STATE_ALL = 0; + const STATE_READ = 1; + const STATE_NOT_READ = 2; + const STATE_FAVORITE = 4; + const STATE_NOT_FAVORITE = 8; private $id = 0; private $guid; diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index aaf4dcf6a..4e24541dc 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -35,11 +35,45 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { } } - public function markFavorite ($id, $is_favorite = true) { + public function addEntryObject($entry, $conf, $feedHistory) { + $existingGuids = array_fill_keys( + $this->listLastGuidsByFeed($entry->feed(), 20), 1 + ); + + $nb_month_old = max($conf->old_entries, 1); + $date_min = time() - (3600 * 24 * 30 * $nb_month_old); + + $eDate = $entry->date(true); + + if ($feedHistory == -2) { + $feedHistory = $conf->keep_history_default; + } + + if (!isset($existingGuids[$entry->guid()]) && + ($feedHistory != 0 || $eDate >= $date_min)) { + $values = $entry->toArray(); + + $useDeclaredDate = empty($existingGuids); + $values['id'] = ($useDeclaredDate || $eDate < $date_min) ? + min(time(), $eDate) . uSecString() : + uTimeString(); + + return $this->addEntry($values); + } + + // We don't return Entry object to avoid a research in DB + return -1; + } + + public function markFavorite($ids, $is_favorite = true) { + if (!is_array($ids)) { + $ids = array($ids); + } $sql = 'UPDATE `' . $this->prefix . 'entry` e ' . 'SET e.is_favorite = ? ' - . 'WHERE e.id=?'; - $values = array ($is_favorite ? 1 : 0, $id); + . 'WHERE e.id IN (' . str_repeat('?,', count($ids) - 1). '?)'; + $values = array ($is_favorite ? 1 : 0); + $values = array_merge($values, $ids); $stm = $this->bd->prepare ($sql); if ($stm && $stm->execute ($values)) { return $stm->rowCount(); @@ -49,30 +83,79 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return false; } } - public function markRead ($id, $is_read = true) { - $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' - . 'SET e.is_read = ?,' - . 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 ' - . 'WHERE e.id=?'; - $values = array ($is_read ? 1 : 0, $id); - $stm = $this->bd->prepare ($sql); - if ($stm && $stm->execute ($values)) { - return $stm->rowCount(); + + public function markRead($ids, $is_read = true) { + if (is_array($ids)) { + if (count($ids) < 6) { //Speed heuristics + $affected = 0; + foreach ($ids as $id) { + $affected += $this->markRead($id, $is_read); + } + return $affected; + } + + $this->bd->beginTransaction(); + $sql = 'UPDATE `' . $this->prefix . 'entry` e ' + . 'SET e.is_read = ? ' + . 'WHERE e.id IN (' . str_repeat('?,', count($ids) - 1). '?)'; + $values = array($is_read ? 1 : 0); + $values = array_merge($values, $ids); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm->errorInfo(); + Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); + return false; + } + $affected = $stm->rowCount(); + + if ($affected > 0) { + $sql = 'UPDATE `' . $this->prefix . 'feed` f ' + . 'INNER JOIN (' + . 'SELECT e.id_feed, ' + . 'COUNT(CASE WHEN e.is_read = 0 THEN 1 END) AS nbUnreads, ' + . 'COUNT(e.id) AS nbEntries ' + . 'FROM `' . $this->prefix . 'entry` e ' + . 'GROUP BY e.id_feed' + . ') x ON x.id_feed=f.id ' + . 'SET f.cache_nbEntries=x.nbEntries, f.cache_nbUnreads=x.nbUnreads'; + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute())) { + $info = $stm->errorInfo(); + Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); + return false; + } + } + + $this->bd->commit(); + return $affected; } else { - $info = $stm->errorInfo(); - Minz_Log::record ('SQL error : ' . $info[2], Minz_Log::ERROR); - return false; + $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' + . 'SET e.is_read = ?,' + . 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 ' + . 'WHERE e.id=?'; + $values = array($is_read ? 1 : 0, $ids); + $stm = $this->bd->prepare($sql); + if ($stm && $stm->execute($values)) { + return $stm->rowCount(); + } else { + $info = $stm->errorInfo(); + Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR); + return false; + } } } - public function markReadEntries ($idMax = 0, $favorites = false) { - if ($idMax === 0) { + + public function markReadEntries ($idMax = 0, $onlyFavorites = false, $priorityMin = 0) { + if ($idMax == 0) { $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' . 'SET e.is_read = 1, f.cache_nbUnreads=0 ' - . 'WHERE e.is_read = 0 AND '; - if ($favorites) { - $sql .= 'e.is_favorite = 1'; - } else { - $sql .= 'f.priority > 0'; + . 'WHERE e.is_read = 0'; + if ($onlyFavorites) { + $sql .= ' AND e.is_favorite = 1'; + } elseif ($priorityMin >= 0) { + $sql .= ' AND f.priority > ' . intval($priorityMin); } $stm = $this->bd->prepare ($sql); if ($stm && $stm->execute ()) { @@ -87,11 +170,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' . 'SET e.is_read = 1 ' - . 'WHERE e.is_read = 0 AND e.id <= ? AND '; - if ($favorites) { - $sql .= 'e.is_favorite = 1'; - } else { - $sql .= 'f.priority > 0'; + . 'WHERE e.is_read = 0 AND e.id <= ?'; + if ($onlyFavorites) { + $sql .= ' AND e.is_favorite = 1'; + } elseif ($priorityMin >= 0) { + $sql .= ' AND f.priority > ' . intval($priorityMin); } $values = array ($idMax); $stm = $this->bd->prepare ($sql); @@ -126,8 +209,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return $affected; } } + public function markReadCat ($id, $idMax = 0) { - if ($idMax === 0) { + if ($idMax == 0) { $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' . 'SET e.is_read = 1, f.cache_nbUnreads=0 ' . 'WHERE f.category = ? AND e.is_read = 0'; @@ -181,8 +265,70 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return $affected; } } + + public function markReadCatName($name, $idMax = 0) { + if ($idMax == 0) { + $sql = 'UPDATE `' . $this->prefix . 'entry` e ' + . 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' + . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category ' + . 'SET e.is_read = 1, f.cache_nbUnreads=0 ' + . 'WHERE c.name = ?'; + $values = array($name); + $stm = $this->bd->prepare($sql); + if ($stm && $stm->execute($values)) { + return $stm->rowCount(); + } else { + $info = $stm->errorInfo(); + Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR); + return false; + } + } else { + $this->bd->beginTransaction(); + + $sql = 'UPDATE `' . $this->prefix . 'entry` e ' + . 'INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' + . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category ' + . 'SET e.is_read = 1 ' + . 'WHERE c.name = ? AND e.id <= ?'; + $values = array($name, $idMax); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm->errorInfo(); + Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); + return false; + } + $affected = $stm->rowCount(); + + if ($affected > 0) { + $sql = 'UPDATE `' . $this->prefix . 'feed` f ' + . 'LEFT OUTER JOIN (' + . 'SELECT e.id_feed, ' + . 'COUNT(*) AS nbUnreads ' + . 'FROM `' . $this->prefix . 'entry` e ' + . 'WHERE e.is_read = 0 ' + . 'GROUP BY e.id_feed' + . ') x ON x.id_feed=f.id ' + . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category ' + . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) ' + . 'WHERE c.name = ?'; + $values = array($name); + $stm = $this->bd->prepare($sql); + if (!($stm && $stm->execute($values))) { + $info = $stm->errorInfo(); + Minz_Log::record('SQL error : ' . $info[2], Minz_Log::ERROR); + $this->bd->rollBack(); + return false; + } + } + + $this->bd->commit(); + return $affected; + } + } + public function markReadFeed ($id, $idMax = 0) { - if ($idMax === 0) { + if ($idMax == 0) { $sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed = f.id ' . 'SET e.is_read = 1, f.cache_nbUnreads=0 ' . 'WHERE f.id=? AND e.is_read = 0'; @@ -244,7 +390,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $stm->execute ($values); $res = $stm->fetchAll (PDO::FETCH_ASSOC); $entries = self::daoToEntry ($res); - return isset ($entries[0]) ? $entries[0] : false; + return isset ($entries[0]) ? $entries[0] : null; } public function searchById ($id) { @@ -257,10 +403,13 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $stm->execute ($values); $res = $stm->fetchAll (PDO::FETCH_ASSOC); $entries = self::daoToEntry ($res); - return isset ($entries[0]) ? $entries[0] : false; + return isset ($entries[0]) ? $entries[0] : null; } - public function listWhere($type = 'a', $id = '', $state = 'all', $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $keepHistoryDefault = 0) { + private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) { + if (!$state) { + $state = FreshRSS_Entry::STATE_ALL; + } $where = ''; $joinFeed = false; $values = array(); @@ -269,7 +418,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $where .= 'f.priority > 0 '; $joinFeed = true; break; - case 's': + case 's': //Deprecated: use $state instead $where .= 'e1.is_favorite = 1 '; break; case 'c': @@ -281,24 +430,30 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $where .= 'e1.id_feed = ? '; $values[] = intval($id); break; + case 'A': + $where .= '1 '; + break; default: throw new FreshRSS_EntriesGetter_Exception ('Bad type in Entry->listByType: [' . $type . ']!'); } - switch ($state) { - case 'all': - break; - case 'not_read': + + if ($state & FreshRSS_Entry::STATE_NOT_READ) { + if (!($state & FreshRSS_Entry::STATE_READ)) { $where .= 'AND e1.is_read = 0 '; - break; - case 'read': - $where .= 'AND e1.is_read = 1 '; - break; - case 'favorite': + } + } + elseif ($state & FreshRSS_Entry::STATE_READ) { + $where .= 'AND e1.is_read = 1 '; + } + if ($state & FreshRSS_Entry::STATE_FAVORITE) { + if (!($state & FreshRSS_Entry::STATE_NOT_FAVORITE)) { $where .= 'AND e1.is_favorite = 1 '; - break; - default: - throw new FreshRSS_EntriesGetter_Exception ('Bad state in Entry->listByType: [' . $state . ']!'); + } + } + elseif ($state & FreshRSS_Entry::STATE_NOT_FAVORITE) { + $where .= 'AND e1.is_favorite = 0 '; } + switch ($order) { case 'DESC': case 'ASC': @@ -310,70 +465,84 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { $where .= 'AND e1.id ' . ($order === 'DESC' ? '<=' : '>=') . $firstId . ' '; } if (($date_min > 0) && ($type !== 's')) { - $where .= 'AND (e1.id >= ' . $date_min . '000000 OR e1.is_read = 0 OR e1.is_favorite = 1 OR (f.keep_history <> 0'; - if (intval($keepHistoryDefault) === 0) { - $where .= ' AND f.keep_history <> -2'; //default + $where .= 'AND (e1.id >= ' . $date_min . '000000'; + if ($showOlderUnreadsorFavorites) { //Lax date constraint + $where .= ' OR e1.is_read = 0 OR e1.is_favorite = 1 OR (f.keep_history <> 0'; + if (intval($keepHistoryDefault) === 0) { + $where .= ' AND f.keep_history <> -2'; //default + } + $where .= ')'; } - $where .= ')) '; + $where .= ') '; $joinFeed = true; } $search = ''; if ($filter !== '') { + require_once(LIB_PATH . '/lib_date.php'); $filter = trim($filter); $filter = addcslashes($filter, '\\%_'); - if (stripos($filter, 'intitle:') === 0) { - $filter = substr($filter, strlen('intitle:')); - $intitle = true; - } else { - $intitle = false; - } - if (stripos($filter, 'inurl:') === 0) { - $filter = substr($filter, strlen('inurl:')); - $inurl = true; - } else { - $inurl = false; - } - if (stripos($filter, 'author:') === 0) { - $filter = substr($filter, strlen('author:')); - $author = true; - } else { - $author = false; - } $terms = array_unique(explode(' ', $filter)); - sort($terms); //Put #tags first + //sort($terms); //Put #tags first //TODO: Put the cheapest filters first foreach ($terms as $word) { $word = trim($word); - if (strlen($word) > 0) { - if ($intitle) { - $search .= 'AND e1.title LIKE ? '; - $values[] = '%' . $word .'%'; - } elseif ($inurl) { - $search .= 'AND CONCAT(e1.link, e1.guid) LIKE ? '; - $values[] = '%' . $word .'%'; - } elseif ($author) { - $search .= 'AND e1.author LIKE ? '; + if (stripos($word, 'intitle:') === 0) { + $word = substr($word, strlen('intitle:')); + $search .= 'AND e1.title LIKE ? '; + $values[] = '%' . $word .'%'; + } elseif (stripos($word, 'inurl:') === 0) { + $word = substr($word, strlen('inurl:')); + $search .= 'AND CONCAT(e1.link, e1.guid) LIKE ? '; + $values[] = '%' . $word .'%'; + } elseif (stripos($word, 'author:') === 0) { + $word = substr($word, strlen('author:')); + $search .= 'AND e1.author LIKE ? '; + $values[] = '%' . $word .'%'; + } elseif (stripos($word, 'date:') === 0) { + $word = substr($word, strlen('date:')); + list($minDate, $maxDate) = parseDateInterval($word); + if ($minDate) { + $search .= 'AND e1.id >= ' . $minDate . '000000 '; + } + if ($maxDate) { + $search .= 'AND e1.id <= ' . $maxDate . '000000 '; + } + } elseif (stripos($word, 'pubdate:') === 0) { + $word = substr($word, strlen('pubdate:')); + list($minDate, $maxDate) = parseDateInterval($word); + if ($minDate) { + $search .= 'AND e1.date >= ' . $minDate . ' '; + } + if ($maxDate) { + $search .= 'AND e1.date <= ' . $maxDate . ' '; + } + } else { + if ($word[0] === '#' && isset($word[1])) { + $search .= 'AND e1.tags LIKE ? '; $values[] = '%' . $word .'%'; } else { - if ($word[0] === '#' && isset($word[1])) { - $search .= 'AND e1.tags LIKE ? '; - $values[] = '%' . $word .'%'; - } else { - $search .= 'AND CONCAT(e1.title, UNCOMPRESS(e1.content_bin)) LIKE ? '; - $values[] = '%' . $word .'%'; - } + $search .= 'AND CONCAT(e1.title, UNCOMPRESS(e1.content_bin)) LIKE ? '; + $values[] = '%' . $word .'%'; } } } } + return array($values, + 'SELECT e1.id FROM `' . $this->prefix . 'entry` e1 ' + . ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e1.id_feed = f.id ' : '') + . 'WHERE ' . $where + . $search + . 'ORDER BY e1.id ' . $order + . ($limit > 0 ? ' LIMIT ' . $limit : '')); //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ + } + + public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) { + list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min, $showOlderUnreadsorFavorites, $keepHistoryDefault); + $sql = 'SELECT e.id, e.guid, e.title, e.author, UNCOMPRESS(e.content_bin) AS content, e.link, e.date, e.is_read, e.is_favorite, e.id_feed, e.tags ' . 'FROM `' . $this->prefix . 'entry` e ' - . 'INNER JOIN (SELECT e1.id FROM `' . $this->prefix . 'entry` e1 ' - . ($joinFeed ? 'INNER JOIN `' . $this->prefix . 'feed` f ON e1.id_feed = f.id ' : '') - . 'WHERE ' . $where - . $search - . 'ORDER BY e1.id ' . $order - . ($limit > 0 ? ' LIMIT ' . $limit : '') //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ + . 'INNER JOIN (' + . $sql . ') e2 ON e2.id = e.id ' . 'ORDER BY e.id ' . $order; @@ -383,6 +552,15 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return self::daoToEntry ($stm->fetchAll (PDO::FETCH_ASSOC)); } + public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0, $showOlderUnreadsorFavorites = false, $keepHistoryDefault = 0) { //For API + list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min, $showOlderUnreadsorFavorites, $keepHistoryDefault); + + $stm = $this->bd->prepare($sql); + $stm->execute($values); + + return $stm->fetchAll(PDO::FETCH_COLUMN, 0); + } + public function listLastGuidsByFeed($id, $n) { $sql = 'SELECT guid FROM `' . $this->prefix . 'entry` WHERE id_feed=? ORDER BY id DESC LIMIT ' . intval($n); $stm = $this->bd->prepare ($sql); diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 22c019080..757eacd59 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -193,10 +193,10 @@ class FreshRSS_Feed extends Minz_Model { } $feed = customSimplePie(); $feed->set_feed_url ($url); - $feed->init (); + $mtime = $feed->init(); - if ($feed->error ()) { - throw new FreshRSS_Feed_Exception ($feed->error . ' [' . $url . ']'); + if ((!$mtime) || $feed->error()) { + throw new FreshRSS_Feed_Exception ($feed->error() . ' [' . $url . ']'); } // si on a utilisé l'auto-discover, notre url va avoir changé @@ -210,18 +210,27 @@ class FreshRSS_Feed extends Minz_Model { } if ($loadDetails) { - $title = htmlspecialchars(html_only_entity_decode($feed->get_title()), ENT_COMPAT, 'UTF-8'); - $this->_name ($title === null ? $this->url : $title); + $title = strtr(html_only_entity_decode($feed->get_title()), array('<' => '<', '>' => '>', '"' => '"')); //HTML to HTML-PRE //ENT_COMPAT except & + $this->_name ($title == '' ? $this->url : $title); $this->_website(html_only_entity_decode($feed->get_link())); $this->_description(html_only_entity_decode($feed->get_description())); } - // et on charge les articles du flux - $this->loadEntries ($feed); + if (($mtime === true) || ($mtime > $this->lastUpdate)) { + syslog(LOG_DEBUG, 'FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $subscribe_url); + $this->loadEntries($feed); // et on charge les articles du flux + } else { + syslog(LOG_DEBUG, 'FreshRSS use cache for ' . $subscribe_url); + $this->entries = array(); + } + + $feed->__destruct(); //http://simplepie.org/wiki/faq/i_m_getting_memory_leaks + unset($feed); } } } + private function loadEntries ($feed) { $entries = array (); @@ -245,11 +254,16 @@ class FreshRSS_Feed extends Minz_Model { $elinks = array(); foreach ($item->get_enclosures() as $enclosure) { $elink = $enclosure->get_link(); - if (array_key_exists($elink, $elinks)) continue; - $elinks[$elink] = '1'; - $mime = strtolower($enclosure->get_type()); - if (strpos($mime, 'image/') === 0) { - $content .= '<br /><img src="' . $elink . '" alt="" />'; + if (empty($elinks[$elink])) { + $elinks[$elink] = '1'; + $mime = strtolower($enclosure->get_type()); + if (strpos($mime, 'image/') === 0) { + $content .= '<br /><img src="' . $elink . '" alt="" />'; + } elseif (strpos($mime, 'audio/') === 0) { + $content .= '<br /><audio src="' . $elink . '" controls="controls" />'; + } elseif (strpos($mime, 'video/') === 0) { + $content .= '<br /><video src="' . $elink . '" controls="controls" />'; + } } } @@ -267,8 +281,27 @@ class FreshRSS_Feed extends Minz_Model { $entry->loadCompleteContent($this->pathEntries()); $entries[] = $entry; + unset($item); } $this->entries = $entries; } + + function lock() { + $lock = TMP_PATH . '/' . md5(Minz_Configuration::salt() . $this->url) . '.freshrss.lock'; + if (file_exists($lock) && ((time() - @filemtime($lock)) > 3600)) { + @unlink($lock); + } + if (($handle = @fopen($lock, 'x')) === false) { + return false; + } + //register_shutdown_function('unlink', $lock); + @fclose($handle); + return true; + } + + function unlock() { + $lock = TMP_PATH . '/' . md5(Minz_Configuration::salt() . $this->url) . '.freshrss.lock'; + @unlink($lock); + } } diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index e102da4ec..b65ff4af0 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -24,6 +24,36 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { } } + public function addFeedObject($feed) { + // TODO: not sure if we should write this method in DAO since DAO + // should not be aware about feed class + + // Add feed only if we don't find it in DB + $feed_search = $this->searchByUrl($feed->url()); + if (!$feed_search) { + $values = array( + 'id' => $feed->id(), + 'url' => $feed->url(), + 'category' => $feed->category(), + 'name' => $feed->name(), + 'website' => $feed->website(), + 'description' => $feed->description(), + 'lastUpdate' => 0, + 'httpAuth' => $feed->httpAuth() + ); + + $id = $this->addFeed($values); + if ($id) { + $feed->_id($id); + $feed->faviconPrepare(); + } + + return $id; + } + + return $feed_search->id(); + } + public function updateFeed ($id, $valuesTmp) { $set = ''; foreach ($valuesTmp as $key => $v) { @@ -52,21 +82,27 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { } } - public function updateLastUpdate ($id, $inError = 0) { - $sql = 'UPDATE `' . $this->prefix . 'feed` f ' //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE - . 'SET f.cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=f.id),' - . 'f.cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=f.id AND e2.is_read=0),' - . 'lastUpdate=?, error=? ' - . 'WHERE f.id=?'; - - $stm = $this->bd->prepare ($sql); + public function updateLastUpdate ($id, $inError = 0, $updateCache = true) { + if ($updateCache) { + $sql = 'UPDATE `' . $this->prefix . 'feed` f ' //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE + . 'SET f.cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=f.id),' + . 'f.cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=f.id AND e2.is_read=0),' + . 'lastUpdate=?, error=? ' + . 'WHERE f.id=?'; + } else { + $sql = 'UPDATE `' . $this->prefix . 'feed` f ' + . 'SET lastUpdate=?, error=? ' + . 'WHERE f.id=?'; + } $values = array ( - time (), + time(), $inError, $id, ); + $stm = $this->bd->prepare ($sql); + if ($stm && $stm->execute ($values)) { return $stm->rowCount(); } else { @@ -164,7 +200,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { if (isset ($feed[$id])) { return $feed[$id]; } else { - return false; + return null; } } public function searchByUrl ($url) { @@ -180,7 +216,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { if (isset ($feed)) { return $feed; } else { - return false; + return null; } } @@ -192,8 +228,27 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return self::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC)); } - public function listFeedsOrderUpdate () { - $sql = 'SELECT id, name, url, pathEntries, httpAuth, keep_history FROM `' . $this->prefix . 'feed` ORDER BY lastUpdate'; + public function arrayFeedCategoryNames() { //For API + $sql = 'SELECT f.id, f.name, c.name as c_name FROM `' . $this->prefix . 'feed` f ' + . 'INNER JOIN `' . $this->prefix . 'category` c ON c.id = f.category'; + $stm = $this->bd->prepare ($sql); + $stm->execute (); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + $feedCategoryNames = array(); + foreach ($res as $line) { + $feedCategoryNames[$line['id']] = array( + 'name' => $line['name'], + 'c_name' => $line['c_name'], + ); + } + return $feedCategoryNames; + } + + public function listFeedsOrderUpdate ($cacheDuration = 1500) { + $sql = 'SELECT id, name, url, lastUpdate, pathEntries, httpAuth, keep_history ' + . 'FROM `' . $this->prefix . 'feed` ' + . 'WHERE lastUpdate < ' . (time() - intval($cacheDuration)) + . ' ORDER BY lastUpdate'; $stm = $this->bd->prepare ($sql); $stm->execute (); @@ -220,6 +275,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return $res[0]['count']; } + public function countNotRead ($id) { $sql = 'SELECT COUNT(*) AS count FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND is_read=0'; $stm = $this->bd->prepare ($sql); @@ -229,6 +285,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return $res[0]['count']; } + public function updateCachedValues () { //For one single feed, call updateLastUpdate($id) $sql = 'UPDATE `' . $this->prefix . 'feed` f ' . 'INNER JOIN (' @@ -241,9 +298,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { . 'SET f.cache_nbEntries=x.nbEntries, f.cache_nbUnreads=x.nbUnreads'; $stm = $this->bd->prepare ($sql); - $values = array ($feed_id); - - if ($stm && $stm->execute ($values)) { + if ($stm && $stm->execute()) { return $stm->rowCount(); } else { $info = $stm->errorInfo(); diff --git a/app/Models/Share.php b/app/Models/Share.php new file mode 100644 index 000000000..b146db722 --- /dev/null +++ b/app/Models/Share.php @@ -0,0 +1,44 @@ +<?php + +class FreshRSS_Share { + + static public function generateUrl($options, $selected, $link, $title) { + $share = $options[$selected['type']]; + $matches = array( + '~URL~', + '~TITLE~', + '~LINK~', + ); + $replaces = array( + $selected['url'], + self::transformData($title, self::getTransform($share, 'title')), + self::transformData($link, self::getTransform($share, 'link')), + ); + $url = str_replace($matches, $replaces, $share['url']); + return $url; + } + + static private function transformData($data, $transform) { + if (!is_array($transform)) { + return $data; + } + if (count($transform) === 0) { + return $data; + } + foreach ($transform as $action) { + $data = call_user_func($action, $data); + } + return $data; + } + + static private function getTransform($options, $type) { + $transform = $options['transform']; + + if (array_key_exists($type, $transform)) { + return $transform[$type]; + } + + return $transform; + } + +} diff --git a/app/Models/Themes.php b/app/Models/Themes.php index c7099a1df..620149934 100644 --- a/app/Models/Themes.php +++ b/app/Models/Themes.php @@ -77,6 +77,7 @@ class FreshRSS_Themes extends Minz_Model { 'down' => '▽', 'favorite' => '★', 'help' => 'ⓘ', + 'key' => '⚿', 'link' => '↗', 'login' => '🔒', 'logout' => '🔓', @@ -84,6 +85,7 @@ class FreshRSS_Themes extends Minz_Model { 'non-starred' => '☆', 'prev' => '⏪', 'read' => '☑', + 'rss' => '☄', 'unread' => '☐', 'refresh' => '🔃', //↻ 'search' => '🔍', @@ -91,6 +93,9 @@ class FreshRSS_Themes extends Minz_Model { 'starred' => '★', 'tag' => '⚐', 'up' => '△', + 'view-normal' => '☰', + 'view-global' => '☷', + 'view-reader' => '☕', ); if (!isset($alts[$name])) { return ''; diff --git a/app/actualize_script.php b/app/actualize_script.php index 9ac80a852..4c306b8da 100755 --- a/app/actualize_script.php +++ b/app/actualize_script.php @@ -1,21 +1,5 @@ <?php require(dirname(__FILE__) . '/../constants.php'); - -//<Mutex> -$lock = DATA_PATH . '/actualize.lock.txt'; -if (file_exists($lock) && ((time() - @filemtime($lock)) > 3600)) { - @unlink($lock); -} -if (($handle = @fopen($lock, 'x')) === false) { - syslog(LOG_NOTICE, 'FreshRSS actualize already running?'); - fwrite(STDERR, 'FreshRSS actualize already running?' . "\n"); - return; -} -register_shutdown_function('unlink', $lock); -//Could use http://php.net/function.pcntl-signal.php to catch interruptions -@fclose($handle); -//</Mutex> - require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader session_cache_limiter(''); @@ -32,7 +16,9 @@ $users = array_unique($users); foreach ($users as $myUser) { syslog(LOG_INFO, 'FreshRSS actualize ' . $myUser); - fwrite(STDOUT, 'Actualize ' . $myUser . "...\n"); //Unbuffered + if (defined('STDOUT')) { + fwrite(STDOUT, 'Actualize ' . $myUser . "...\n"); //Unbuffered + } echo $myUser, ' '; //Buffered $_GET['c'] = 'feed'; @@ -42,7 +28,8 @@ foreach ($users as $myUser) { $_SERVER['HTTP_HOST'] = ''; $freshRSS = new FreshRSS(); - $freshRSS->_useOb(false); + + Minz_Configuration::_authType('none'); Minz_Session::init('FreshRSS'); Minz_Session::_param('currentUser', $myUser); @@ -50,10 +37,18 @@ foreach ($users as $myUser) { $freshRSS->init(); $freshRSS->run(); - invalidateHttpCache(); + if (!invalidateHttpCache()) { + syslog(LOG_NOTICE, 'FreshRSS write access problem in ' . LOG_PATH . '/*.log!'); + if (defined('STDERR')) { + fwrite(STDERR, 'Write access problem in ' . LOG_PATH . '/*.log!' . "\n"); + } + } Minz_Session::unset_session(true); Minz_ModelPdo::clean(); } syslog(LOG_INFO, 'FreshRSS actualize done.'); +if (defined('STDOUT')) { + fwrite(STDOUT, 'Done.' . "\n"); +} +echo 'End.', "\n"; ob_end_flush(); -fwrite(STDOUT, 'Done.' . "\n"); diff --git a/app/i18n/en.php b/app/i18n/en.php index a2cc461c5..a3c1dfeb7 100644 --- a/app/i18n/en.php +++ b/app/i18n/en.php @@ -3,6 +3,7 @@ return array ( // LAYOUT 'login' => 'Login', + 'login_with_persona' => 'Login with Persona', 'logout' => 'Logout', 'search' => 'Search words or #tags', 'search_short' => 'Search', @@ -20,12 +21,13 @@ return array ( 'your_rss_feeds' => 'Your RSS feeds', 'add_rss_feed' => 'Add a RSS feed', 'no_rss_feed' => 'No RSS feed', - 'import_export_opml' => 'Import / export (OPML)', + 'import_export' => 'Import / export', + 'bookmark' => 'Subscribe (FreshRSS bookmark)', 'subscription_management' => 'Subscriptions management', 'main_stream' => 'Main stream', 'all_feeds' => 'All feeds', - 'favorite_feeds' => 'Favourites (%d)', + 'favorite_feeds' => 'Favourites (%s)', 'not_read' => '%d unread', 'not_reads' => '%d unread', @@ -49,7 +51,8 @@ return array ( 'show_all_articles' => 'Show all articles', 'show_not_reads' => 'Show only unread', 'show_read' => 'Show only read', - 'show_favorite' => 'Show favorites', + 'show_favorite' => 'Show only favorites', + 'show_not_favorite' => 'Show all but favorites', 'older_first' => 'Oldest first', 'newer_first' => 'Newer first', @@ -77,14 +80,17 @@ return array ( 'sharing_management' => 'Sharing options management', 'bad_opml_file' => 'Your OPML file is invalid', 'shortcuts_updated' => 'Shortcuts have been updated', - 'shortcuts_management' => 'Shortcuts management', + 'shortcuts_navigation' => 'Navigation', + 'shortcuts_navigation_help' => 'With the "Shift" modifier, navigation shortcuts apply on feeds.<br/>With the "Alt" modifier, navigation shortcuts apply on categories.', + 'shortcuts_article_action' => 'Article actions', + 'shortcuts_other_action' => 'Other actions', 'feeds_marked_read' => 'Feeds have been marked as read', 'updated' => 'Modifications have been updated', 'already_subscribed' => 'You have already subscribed to <em>%s</em>', 'feed_added' => 'RSS feed <em>%s</em> has been added', 'feed_not_added' => '<em>%s</em> could not be added', - 'internal_problem_feed' => 'The RSS feed could not be added. Check FressRSS logs for details.', + 'internal_problem_feed' => 'The RSS feed could not be added. <a href="%s">Check FressRSS logs</a> for details.', 'invalid_url' => 'URL <em>%s</em> is invalid', 'feed_actualized' => '<em>%s</em> has been updated', 'n_feeds_actualized' => '%d feeds have been updated', @@ -121,23 +127,31 @@ return array ( 'javascript_for_shortcuts' => 'JavaScript must be enabled in order to use shortcuts', 'javascript_should_be_activated'=> 'JavaScript must be enabled', 'shift_for_all_read' => '+ <code>shift</code> to mark all articles as read', - 'see_on_website' => 'See article on its original website', + 'see_on_website' => 'See on original website', 'next_article' => 'Skip to the next article', - 'shift_for_last' => '+ <code>shift</code> to skip to the last article of page', + 'last_article' => 'Skip to the last article', 'previous_article' => 'Skip to the previous article', - 'shift_for_first' => '+ <code>shift</code> to skip to the first article of page', + 'first_article' => 'Skip to the first article', 'next_page' => 'Skip to the next page', 'previous_page' => 'Skip to the previous page', - 'collapse_article' => 'Collapse current article', - 'auto_share' => 'Share current article', + 'collapse_article' => 'Collapse', + 'auto_share' => 'Share', + 'auto_share_help' => 'If there is only one sharing mode, it is used. Else modes are accessible by their number.', + 'focus_search' => 'Access search box', - 'file_to_import' => 'File to import', + 'file_to_import' => 'File to import<br />(OPML, Json or Zip)', 'import' => 'Import', 'export' => 'Export', + 'export_opml' => 'Export list of feeds (OPML)', + 'export_starred' => 'Export your favourites', + 'starred_list' => 'List of favourite articles', + 'feed_list' => 'List of %s articles', 'or' => 'or', 'informations' => 'Information', + 'damn' => 'Damn!', 'feed_in_error' => 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.', + 'feed_empty' => 'This feed is empty. Please verify that it is still maintained.', 'feed_description' => 'Description', 'website_url' => 'Website URL', 'feed_url' => 'Feed URL', @@ -158,6 +172,8 @@ return array ( 'http_username' => 'HTTP username', 'http_password' => 'HTTP password', 'blank_to_disable' => 'Leave blank to disable', + 'share_name' => 'Share name to display', + 'share_url' => 'Share URL to use', 'not_yet_implemented' => 'Not yet implemented', 'access_protected_feeds' => 'Connection allows to access HTTP protected RSS feeds', 'no_selected_feed' => 'No feed selected.', @@ -166,8 +182,12 @@ return array ( 'current_user' => 'Current user', 'default_user' => 'Username of the default user <small>(maximum 16 alphanumeric characters)</small>', 'password_form' => 'Password<br /><small>(for the Web-form login method)</small>', + 'password_api' => 'Password API<br /><small>(e.g., for mobile apps)</small>', 'persona_connection_email' => 'Login mail address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>', 'allow_anonymous' => 'Allow anonymous reading of the articles of the default user (%s)', + 'allow_anonymous_refresh' => 'Allow anonymous refresh of the articles', + 'unsafe_autologin' => 'Allow unsafe automatic login using the format: ', + 'api_enabled' => 'Allow <abbr>API</abbr> access <small>(required for mobile apps)</small>', 'auth_token' => 'Authentication token', 'explain_token' => 'Allows to access RSS output of the default user without authentication.<br /><kbd>%s?output=rss&token=%s</kbd>', 'login_configuration' => 'Login', @@ -193,6 +213,7 @@ return array ( 'purge_completed' => 'Purge completed (%d articles deleted)', 'archiving_configuration_help' => 'More options are available in the individual stream settings', 'reading_configuration' => 'Reading', + 'display_configuration' => 'Display', 'articles_per_page' => 'Number of articles per page', 'default_view' => 'Default view', 'sort_order' => 'Sort order', @@ -200,10 +221,11 @@ return array ( 'display_articles_unfolded' => 'Show articles unfolded by default', 'after_onread' => 'After “mark all as read”,', 'jump_next' => 'jump to next unread sibling (feed or category)', - 'reading_icons' => 'Reading icons', + 'article_icons' => 'Article icons', 'top_line' => 'Top line', 'bottom_line' => 'Bottom line', 'img_with_lazyload' => 'Use "lazy load" mode to load pictures', + 'sticky_post' => 'Stick the article to the top when opened', 'auto_read_when' => 'Mark article as read…', 'article_selected' => 'when article is selected', 'article_open_on_website' => 'when article is opened on its original website', @@ -218,9 +240,15 @@ return array ( 'optimize_bdd' => 'Optimize database', 'optimize_todo_sometimes' => 'To do occasionally to reduce the size of the database', 'theme' => 'Theme', + 'content_width' => 'Content width', + 'width_thin' => 'Thin', + 'width_medium' => 'Medium', + 'width_large' => 'Large', + 'width_no_limit' => 'No limit', 'more_information' => 'More information', 'activate_sharing' => 'Activate sharing', 'shaarli' => 'Shaarli', + 'blogotext' => 'Blogotext', 'wallabag' => 'wallabag', 'diaspora' => 'Diaspora*', 'twitter' => 'Twitter', @@ -241,6 +269,7 @@ return array ( 'rss_feeds_of' => 'RSS feed of %s', 'refresh' => 'Refresh', + 'no_feed_to_refresh' => 'There is no feed to refresh…', 'today' => 'Today', 'yesterday' => 'Yesterday', @@ -267,7 +296,7 @@ return array ( 'logs_empty' => 'Log file is empty', 'clear_logs' => 'Clear the logs', - 'forbidden_access' => 'Access forbidden! (%s)', + 'forbidden_access' => 'Access is forbidden!', 'login_required' => 'Login required:', 'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!', diff --git a/app/i18n/fr.php b/app/i18n/fr.php index 9ab06ba26..4acf5b397 100644 --- a/app/i18n/fr.php +++ b/app/i18n/fr.php @@ -3,6 +3,7 @@ return array ( // LAYOUT 'login' => 'Connexion', + 'login_with_persona' => 'Connexion avec Persona', 'logout' => 'Déconnexion', 'search' => 'Rechercher des mots ou des #tags', 'search_short' => 'Rechercher', @@ -20,12 +21,13 @@ return array ( 'your_rss_feeds' => 'Vos flux RSS', 'add_rss_feed' => 'Ajouter un flux RSS', 'no_rss_feed' => 'Aucun flux RSS', - 'import_export_opml' => 'Importer / exporter (OPML)', + 'import_export' => 'Importer / exporter', + 'bookmark' => 'S’abonner (bookmark FreshRSS)', 'subscription_management' => 'Gestion des abonnements', 'main_stream' => 'Flux principal', 'all_feeds' => 'Tous les flux', - 'favorite_feeds' => 'Favoris (%d)', + 'favorite_feeds' => 'Favoris (%s)', 'not_read' => '%d non lu', 'not_reads' => '%d non lus', @@ -50,6 +52,7 @@ return array ( 'show_not_reads' => 'Afficher les non lus', 'show_read' => 'Afficher les lus', 'show_favorite' => 'Afficher les favoris', + 'show_not_favorite' => 'Afficher tout sauf les favoris', 'older_first' => 'Plus anciens en premier', 'newer_first' => 'Plus récents en premier', @@ -77,14 +80,17 @@ return array ( 'sharing_management' => 'Gestion des options de partage', 'bad_opml_file' => 'Votre fichier OPML n’est pas valide', 'shortcuts_updated' => 'Les raccourcis ont été mis à jour', - 'shortcuts_management' => 'Gestion des raccourcis', + 'shortcuts_navigation' => 'Navigation', + 'shortcuts_navigation_help' => 'Avec le modificateur "Shift", les raccourcis de navigation s’appliquent aux flux.<br/>Avec le modificateur "Alt", les raccourcis de navigation s’appliquent aux catégories.', + 'shortcuts_article_action' => 'Actions associées à l’article courant', + 'shortcuts_other_action' => 'Autres actions', 'feeds_marked_read' => 'Les flux ont été marqués comme lus', 'updated' => 'Modifications enregistrées', 'already_subscribed' => 'Vous êtes déjà abonné à <em>%s</em>', 'feed_added' => 'Le flux <em>%s</em> a bien été ajouté', 'feed_not_added' => '<em>%s</em> n’a pas pu être ajouté', - 'internal_problem_feed' => 'Le flux n’a pas pu être ajouté. Consulter les logs de FreshRSS pour plus de détails.', + 'internal_problem_feed' => 'Le flux ne peut pas être ajouté. <a href="%s">Consulter les logs de FreshRSS</a> pour plus de détails.', 'invalid_url' => 'L’url <em>%s</em> est invalide', 'feed_actualized' => '<em>%s</em> a été mis à jour', 'n_feeds_actualized' => '%d flux ont été mis à jour', @@ -121,23 +127,31 @@ return array ( 'javascript_for_shortcuts' => 'Le JavaScript doit être activé pour pouvoir profiter des raccourcis', 'javascript_should_be_activated'=> 'Le JavaScript doit être activé', 'shift_for_all_read' => '+ <code>shift</code> pour marquer tous les articles comme lus', - 'see_on_website' => 'Voir l’article sur le site d’origine', + 'see_on_website' => 'Voir sur le site d’origine', 'next_article' => 'Passer à l’article suivant', - 'shift_for_last' => '+ <code>shift</code> pour passer au dernier article de la page', + 'last_article' => 'Passer au dernier article', 'previous_article' => 'Passer à l’article précédent', - 'shift_for_first' => '+ <code>shift</code> pour passer au premier article de la page', + 'first_article' => 'Passer au premier article', 'next_page' => 'Passer à la page suivante', 'previous_page' => 'Passer à la page précédente', - 'collapse_article' => 'Refermer l’article courant', - 'auto_share' => 'Partager l’article courant', + 'collapse_article' => 'Refermer', + 'auto_share' => 'Partager', + 'auto_share_help' => 'Si il n’y a qu’un mode de partage, celui ci est utilisé automatiquement. Sinon ils sont accessibles par leur numéro.', + 'focus_search' => 'Accéder à la recherche', - 'file_to_import' => 'Fichier à importer', + 'file_to_import' => 'Fichier à importer<br />(OPML, Json ou Zip)', 'import' => 'Importer', 'export' => 'Exporter', + 'export_opml' => 'Exporter la liste des flux (OPML)', + 'export_starred' => 'Exporter les favoris', + 'starred_list' => 'Liste des articles favoris', + 'feed_list' => 'Liste des articles de %s', 'or' => 'ou', 'informations' => 'Informations', + 'damn' => 'Arf !', 'feed_in_error' => 'Ce flux a rencontré un problème. Veuillez vérifier qu’il est toujours accessible puis actualisez-le.', + 'feed_empty' => 'Ce flux est vide. Veuillez vérifier qu’il est toujours maintenu.', 'feed_description' => 'Description', 'website_url' => 'URL du site', 'feed_url' => 'URL du flux', @@ -158,6 +172,8 @@ return array ( 'http_username' => 'Identifiant HTTP', 'http_password' => 'Mot de passe HTTP', 'blank_to_disable' => 'Laissez vide pour désactiver', + 'share_name' => 'Nom du partage à afficher', + 'share_url' => 'URL du partage à utiliser', 'not_yet_implemented' => 'Pas encore implémenté', 'access_protected_feeds' => 'La connexion permet d’accéder aux flux protégés par une authentification HTTP', 'no_selected_feed' => 'Aucun flux sélectionné.', @@ -165,9 +181,13 @@ return array ( 'current_user' => 'Utilisateur actuel', 'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>', + 'password_api' => 'Mot de passe API<br /><small>(ex. : pour applis mobiles)</small>', 'default_user' => 'Nom de l’utilisateur par défaut <small>(16 caractères alphanumériques maximum)</small>', 'persona_connection_email' => 'Adresse courriel de connexion<br /><small>(pour <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>', 'allow_anonymous' => 'Autoriser la lecture anonyme des articles de l’utilisateur par défaut (%s)', + 'allow_anonymous_refresh' => 'Autoriser le rafraîchissement anonyme des flux', + 'unsafe_autologin' => 'Autoriser les connexion automatiques non-sûres au format : ', + 'api_enabled' => 'Autoriser l’accès par <abbr>API</abbr> <small>(nécessaire pour les applis mobiles)</small>', 'auth_token' => 'Jeton d’identification', 'explain_token' => 'Permet d’accéder à la sortie RSS de l’utilisateur par défaut sans besoin de s’authentifier.<br /><kbd>%s?output=rss&token=%s</kbd>', 'login_configuration' => 'Identification', @@ -193,6 +213,7 @@ return array ( 'purge_completed' => 'Purge effectuée (%d articles supprimés)', 'archiving_configuration_help' => 'D’autres options sont disponibles dans la configuration individuelle des flux', 'reading_configuration' => 'Lecture', + 'display_configuration' => 'Affichage', 'articles_per_page' => 'Nombre d’articles par page', 'default_view' => 'Vue par défaut', 'sort_order' => 'Ordre de tri', @@ -200,10 +221,11 @@ return array ( 'display_articles_unfolded' => 'Afficher les articles dépliés par défaut', 'after_onread' => 'Après “marquer tout comme lu”,', 'jump_next' => 'sauter au prochain voisin non lu (flux ou catégorie)', - 'reading_icons' => 'Icônes de lecture', + 'article_icons' => 'Icônes d’article', 'top_line' => 'Ligne du haut', 'bottom_line' => 'Ligne du bas', 'img_with_lazyload' => 'Utiliser le mode “chargement différé” pour les images', + 'sticky_post' => 'Aligner l’article en haut quand il est ouvert', 'auto_read_when' => 'Marquer un article comme lu…', 'article_selected' => 'lorsque l’article est sélectionné', 'article_open_on_website' => 'lorsque l’article est ouvert sur le site d’origine', @@ -218,9 +240,15 @@ return array ( 'optimize_bdd' => 'Optimiser la base de données', 'optimize_todo_sometimes' => 'À faire de temps en temps pour réduire la taille de la BDD', 'theme' => 'Thème', + 'content_width' => 'Largeur du contenu', + 'width_thin' => 'Fine', + 'width_medium' => 'Moyenne', + 'width_large' => 'Large', + 'width_no_limit' => 'Pas de limite', 'more_information' => 'Plus d’informations', 'activate_sharing' => 'Activer le partage', 'shaarli' => 'Shaarli', + 'blogotext' => 'Blogotext', 'wallabag' => 'wallabag', 'diaspora' => 'Diaspora*', 'twitter' => 'Twitter', @@ -241,6 +269,7 @@ return array ( 'rss_feeds_of' => 'Flux RSS de %s', 'refresh' => 'Actualisation', + 'no_feed_to_refresh' => 'Il n’y a aucun flux à actualiser…', 'today' => 'Aujourd’hui', 'yesterday' => 'Hier', @@ -267,7 +296,7 @@ return array ( 'logs_empty' => 'Les logs sont vides', 'clear_logs' => 'Effacer les logs', - 'forbidden_access' => 'Accès interdit ! (%s)', + 'forbidden_access' => 'L’accès vous est interdit !', 'login_required' => 'Accès protégé par mot de passe :', 'confirm_action' => 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !', diff --git a/app/layout/aside_configure.phtml b/app/layout/aside_configure.phtml index 27f11ab6d..43adeb3c6 100644 --- a/app/layout/aside_configure.phtml +++ b/app/layout/aside_configure.phtml @@ -1,7 +1,10 @@ <ul class="nav nav-list aside"> <li class="nav-header"><?php echo Minz_Translate::t ('configuration'); ?></li> <li class="item<?php echo Minz_Request::actionName () == 'display' ? ' active' : ''; ?>"> - <a href="<?php echo _url ('configure', 'display'); ?>"><?php echo Minz_Translate::t ('reading_configuration'); ?></a> + <a href="<?php echo _url ('configure', 'display'); ?>"><?php echo Minz_Translate::t ('display_configuration'); ?></a> + </li> + <li class="item<?php echo Minz_Request::actionName () == 'reading' ? ' active' : ''; ?>"> + <a href="<?php echo _url ('configure', 'reading'); ?>"><?php echo Minz_Translate::t ('reading_configuration'); ?></a> </li> <li class="item<?php echo Minz_Request::actionName () == 'archiving' ? ' active' : ''; ?>"> <a href="<?php echo _url ('configure', 'archiving'); ?>"><?php echo Minz_Translate::t ('archiving_configuration'); ?></a> diff --git a/app/layout/aside_feed.phtml b/app/layout/aside_feed.phtml index e324b15bd..5ffb1c791 100644 --- a/app/layout/aside_feed.phtml +++ b/app/layout/aside_feed.phtml @@ -20,9 +20,14 @@ <?php echo $cat->name (); ?> </option> <?php } ?> + <option value="nc"><?php echo Minz_Translate::t ('new_category'); ?></option> </select> </li> + <li class="input" style="display:none"> + <input type="text" name="new_category[name]" id="new_category_name" autocomplete="off" placeholder="<?php echo Minz_Translate::t ('new_category'); ?>" /> + </li> + <li class="separator"></li> <li class="dropdown-header"><?php echo Minz_Translate::t ('http_authentication'); ?></li> @@ -38,8 +43,14 @@ </div> </form></li> - <li class="item<?php echo Minz_Request::actionName () == 'importExport' ? ' active' : ''; ?>"> - <a href="<?php echo _url ('configure', 'importExport'); ?>"><?php echo Minz_Translate::t ('import_export_opml'); ?></a> + <li class="item"> + <a onclick="return false;" href="javascript:(function(){var%20url%20=%20location.href;window.open('<?php echo Minz_Url::display(array('c' => 'feed', 'a' => 'add'), 'html', true); ?>&url_rss='+encodeURIComponent(url), '_blank');})();"> + <?php echo Minz_Translate::t('bookmark'); ?> + </a> + </li> + + <li class="item<?php echo Minz_Request::controllerName () == 'importExport' ? ' active' : ''; ?>"> + <a href="<?php echo _url ('importExport', 'index'); ?>"><?php echo Minz_Translate::t ('import_export'); ?></a> </li> <li class="item<?php echo Minz_Request::actionName () == 'categorize' ? ' active' : ''; ?>"> @@ -51,7 +62,7 @@ <?php if (!empty ($this->feeds)) { ?> <?php foreach ($this->feeds as $feed) { ?> <?php $nbEntries = $feed->nbEntries (); ?> - <li class="item<?php echo ($this->flux && $this->flux->id () == $feed->id ()) ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?><?php echo $nbEntries == 0 ? ' empty' : ''; ?>"> + <li class="item<?php echo (isset($this->flux) && $this->flux->id () == $feed->id ()) ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?><?php echo $nbEntries == 0 ? ' empty' : ''; ?>"> <a href="<?php echo _url ('configure', 'feed', 'id', $feed->id ()); ?>"> <img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="✇" /> <?php echo $feed->name (); ?> diff --git a/app/layout/aside_flux.phtml b/app/layout/aside_flux.phtml index 8454b4459..817dae676 100644 --- a/app/layout/aside_flux.phtml +++ b/app/layout/aside_flux.phtml @@ -20,7 +20,7 @@ } ?> <li> - <div class="category all"> + <div class="category all<?php echo $this->get_c == 'a' ? ' active' : ''; ?>"> <a data-unread="<?php echo formatNumber($this->nb_not_read); ?>" class="btn<?php echo $this->get_c == 'a' ? ' active' : ''; ?>" href="<?php echo Minz_Url::display($arUrl); ?>"> <?php echo FreshRSS_Themes::icon('all'); ?> <?php echo Minz_Translate::t ('main_stream'); ?> @@ -29,7 +29,7 @@ </li> <li> - <div class="category favorites"> + <div class="category favorites<?php echo $this->get_c == 's' ? ' active' : ''; ?>"> <a data-unread="<?php echo formatNumber($this->nb_favorites['unread']); ?>" class="btn<?php echo $this->get_c == 's' ? ' active' : ''; ?>" href="<?php $arUrl['params']['get'] = 's'; echo Minz_Url::display($arUrl); ?>"> <?php echo FreshRSS_Themes::icon('bookmark'); ?> <?php echo Minz_Translate::t('favorite_feeds', formatNumber($this->nb_favorites['all'])); ?> diff --git a/app/layout/header.phtml b/app/layout/header.phtml index eef53a3fd..08aa7715d 100644 --- a/app/layout/header.phtml +++ b/app/layout/header.phtml @@ -67,7 +67,8 @@ if (Minz_Configuration::canLogIn()) { <ul class="dropdown-menu"> <li class="dropdown-close"><a href="#close">❌</a></li> <li class="dropdown-header"><?php echo Minz_Translate::t ('configuration'); ?></li> - <li class="item"><a href="<?php echo _url ('configure', 'display'); ?>"><?php echo Minz_Translate::t ('reading_configuration'); ?></a></li> + <li class="item"><a href="<?php echo _url ('configure', 'display'); ?>"><?php echo Minz_Translate::t ('display_configuration'); ?></a></li> + <li class="item"><a href="<?php echo _url ('configure', 'reading'); ?>"><?php echo Minz_Translate::t ('reading_configuration'); ?></a></li> <li class="item"><a href="<?php echo _url ('configure', 'archiving'); ?>"><?php echo Minz_Translate::t ('archiving_configuration'); ?></a></li> <li class="item"><a href="<?php echo _url ('configure', 'sharing'); ?>"><?php echo Minz_Translate::t ('sharing'); ?></a></li> <li class="item"><a href="<?php echo _url ('configure', 'shortcut'); ?>"><?php echo Minz_Translate::t ('shortcuts'); ?></a></li> @@ -75,8 +76,8 @@ if (Minz_Configuration::canLogIn()) { <li class="item"><a href="<?php echo _url ('configure', 'users'); ?>"><?php echo Minz_Translate::t ('users'); ?></a></li> <li class="separator"></li> <li class="item"><a href="<?php echo _url ('index', 'stats'); ?>"><?php echo Minz_Translate::t ('stats'); ?></a></li> - <li class="item"><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Minz_Translate::t ('about'); ?></a></li> <li class="item"><a href="<?php echo _url ('index', 'logs'); ?>"><?php echo Minz_Translate::t ('logs'); ?></a></li> + <li class="item"><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Minz_Translate::t ('about'); ?></a></li> <?php if (Minz_Configuration::canLogIn()) { ?><li class="separator"></li><?php diff --git a/app/layout/layout.phtml b/app/layout/layout.phtml index d6a1737ee..d2e1e4b3b 100644 --- a/app/layout/layout.phtml +++ b/app/layout/layout.phtml @@ -3,46 +3,59 @@ <head> <meta charset="UTF-8" /> <meta name="viewport" content="initial-scale=1.0" /> - <?php echo self::headTitle (); ?> - <?php echo self::headStyle (); ?> - <?php echo self::headScript (); ?> + <?php echo self::headTitle(); ?> + <?php echo self::headStyle(); ?> + <?php echo self::headScript(); ?> <script>//<![CDATA[ -<?php $this->renderHelper ('javascript_vars'); ?> +<?php $this->renderHelper('javascript_vars'); ?> //]]></script> <?php if (!empty($this->nextId)) { - $params = Minz_Request::params (); + $params = Minz_Request::params(); $params['next'] = $this->nextId; ?> - <link id="prefetch" rel="next prefetch" href="<?php echo Minz_Url::display (array ('c' => Minz_Request::controllerName (), 'a' => Minz_Request::actionName (), 'params' => $params)); ?>" /> + <link id="prefetch" rel="next prefetch" href="<?php echo Minz_Url::display(array('c' => Minz_Request::controllerName(), 'a' => Minz_Request::actionName(), 'params' => $params)); ?>" /> <?php } ?> <link rel="shortcut icon" 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'); ?>" /> -<?php if (isset ($this->rss_url)) { ?> - <link rel="alternate" type="application/rss+xml" title="<?php echo $this->rss_title; ?>" href="<?php echo Minz_Url::display ($this->rss_url); ?>" /> +<?php + if (isset($this->url)) { + $rss_url = $this->url; + $rss_url['params']['output'] = 'rss'; +?> + <link rel="alternate" type="application/rss+xml" title="<?php echo $this->rss_title; ?>" href="<?php echo Minz_Url::display($rss_url); ?>" /> <?php } ?> <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" /> + <meta name="apple-mobile-web-app-title" content="<?php echo Minz_Configuration::title(); ?>"> <meta name="msapplication-TileColor" content="#FFF" /> <meta name="robots" content="noindex,nofollow" /> </head> <body class="<?php echo Minz_Request::param('output', 'normal'); ?>"> -<?php $this->partial ('header'); ?> +<?php $this->partial('header'); ?> <div id="global"> - <?php $this->render (); ?> + <?php $this->render(); ?> </div> <?php - if (isset ($this->notification)) { + $msg = ''; + $status = 'closed'; + if (isset($this->notification)) { + $msg = $this->notification['content']; + $status = $this->notification['type']; + invalidateHttpCache(); + } ?> -<div class="notification <?php echo $this->notification['type']; ?>"> - <?php echo $this->notification['content']; ?> +<div id="notification" class="notification <?php echo $status; ?>"> + <span class="msg"><?php echo $msg; ?></span> <a class="close" href=""><?php echo FreshRSS_Themes::icon('close'); ?></a> </div> -<?php } ?> </body> </html> diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml index c807e6dd5..b42f816b4 100644 --- a/app/layout/nav_menu.phtml +++ b/app/layout/nav_menu.phtml @@ -7,8 +7,80 @@ <?php } ?> <?php if ($this->loginOk) { ?> - <a id="actualize" class="btn" href="<?php echo _url ('feed', 'actualize'); ?>"><?php echo FreshRSS_Themes::icon('refresh'); ?></a> - + <?php $url_state = $this->url; + if ($this->state & FreshRSS_Entry::STATE_READ) { + $url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_READ; + $checked = 'true'; + $class = 'active'; + } else { + $url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_READ; + $checked = 'false'; + $class = ''; + } + ?> + <div class="stick"> + <a id="toggle-read" + class="btn <?php echo $class; ?>" + aria-checked="<?php echo $checked; ?>" + href="<?php echo Minz_Url::display ($url_state); ?>" + title="<?php echo Minz_Translate::t ('show_read'); ?>"> + <?php echo FreshRSS_Themes::icon('read'); ?> + </a> + <?php + if ($this->state & FreshRSS_Entry::STATE_NOT_READ) { + $url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_NOT_READ; + $checked = 'true'; + $class = 'active'; + } else { + $url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_NOT_READ; + $checked = 'false'; + $class = ''; + } + ?> + <a id="toggle-unread" + class="btn <?php echo $class; ?>" + aria-checked="<?php echo $checked; ?>" + href="<?php echo Minz_Url::display ($url_state); ?>" + title="<?php echo Minz_Translate::t ('show_not_reads'); ?>"> + <?php echo FreshRSS_Themes::icon('unread'); ?> + </a> + <?php + if ($this->state & FreshRSS_Entry::STATE_FAVORITE) { + $url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_FAVORITE; + $checked = 'true'; + $class = 'active'; + } else { + $url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_FAVORITE; + $checked = 'false'; + $class = ''; + } + ?> + <a id="toggle-favorite" + class="btn <?php echo $class; ?>" + aria-checked="<?php echo $checked; ?>" + href="<?php echo Minz_Url::display ($url_state); ?>" + title="<?php echo Minz_Translate::t ('show_favorite'); ?>"> + <?php echo FreshRSS_Themes::icon('starred'); ?> + </a> + <?php + if ($this->state & FreshRSS_Entry::STATE_NOT_FAVORITE) { + $url_state['params']['state'] = $this->state & ~FreshRSS_Entry::STATE_NOT_FAVORITE; + $checked = 'true'; + $class = 'active'; + } else { + $url_state['params']['state'] = $this->state | FreshRSS_Entry::STATE_NOT_FAVORITE; + $checked = 'false'; + $class = ''; + } + ?> + <a id="toggle-not-favorite" + class="btn <?php echo $class; ?>" + aria-checked="<?php echo $checked; ?>" + href="<?php echo Minz_Url::display ($url_state); ?>" + title="<?php echo Minz_Translate::t ('show_not_favorite'); ?>"> + <?php echo FreshRSS_Themes::icon('non-starred'); ?> + </a> + </div> <?php $get = false; $string_mark = Minz_Translate::t ('mark_all_read'); @@ -59,8 +131,12 @@ break; } } - $p = isset($this->entries[0]) ? $this->entries[0] : null; - $idMax = $p === null ? '0' : $p->id(); + if ($this->order === 'ASC') { + $idMax = 0; + } else { + $p = isset($this->entries[0]) ? $this->entries[0] : null; + $idMax = $p === null ? '0' : $p->id(); + } $arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('get' => $get, 'nextGet' => $nextGet, 'idMax' => $idMax)); $output = Minz_Request::param('output', ''); @@ -80,7 +156,7 @@ <ul class="dropdown-menu"> <li class="dropdown-close"><a href="#close">❌</a></li> - <li class="item"><a href="<?php echo $markReadUrl; ?>"><?php echo $string_mark; ?></a></li> + <li class="item"><a href="<?php echo $markReadUrl; ?>"><?php echo $string_mark; ?></a></li> <li class="separator"></li> <?php $today = $this->today; @@ -93,113 +169,30 @@ </div> <?php } ?> - <?php - $params = Minz_Request::params (); - if (isset ($params['search'])) { - $params['search'] = urlencode ($params['search']); - } - $url = array ( - 'c' => 'index', - 'a' => 'index', - 'params' => $params - ); - ?> - <div class="dropdown" id="nav_menu_views"> - <div id="dropdown-views" class="dropdown-target"></div> - <a class="dropdown-toggle btn" href="#dropdown-views"><?php echo Minz_Translate::t ('display'); ?> <?php echo FreshRSS_Themes::icon('down'); ?></a> - <ul class="dropdown-menu"> - <li class="dropdown-close"><a href="#close">❌</a></li> - - <?php - $url_output = $url; - if ($actual_view !== 'normal') { ?> - <li class="item"> - <?php $url_output['params']['output'] = 'normal'; ?> - <a class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>"> - <?php echo Minz_Translate::t ('normal_view'); ?> - </a> - </li> - <?php } if($actual_view !== 'reader') { ?> - <li class="item"> - <?php $url_output['params']['output'] = 'reader'; ?> - <a class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>"> - <?php echo Minz_Translate::t ('reader_view'); ?> - </a> - </li> - <?php } if($actual_view !== 'global') { ?> - <li class="item"> - <?php $url_output['params']['output'] = 'global'; ?> - <a class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>"> - <?php echo Minz_Translate::t ('global_view'); ?> - </a> - </li> - <?php } ?> - - <li class="separator"></li> - - <?php - $url_state = $url; - $url_state['params']['state'] = 'all'; - ?> - <li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'all') ? 'true' :'false'; ?>"> - <a class="print_all" href="<?php echo Minz_Url::display ($url_state); ?>"> - <?php echo Minz_Translate::t ('show_all_articles'); ?> - </a> - </li> - <?php - $url_state['params']['state'] = 'not_read'; - ?> - <li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'not_read') ? 'true' :'false'; ?>"> - <a class="print_non_read" href="<?php echo Minz_Url::display ($url_state); ?>"> - <?php echo Minz_Translate::t ('show_not_reads'); ?> - </a> - </li> - <?php - $url_state['params']['state'] = 'read'; - ?> - <li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'read') ? 'true' :'false'; ?>"> - <a class="print_read" href="<?php echo Minz_Url::display ($url_state); ?>"> - <?php echo Minz_Translate::t ('show_read'); ?> - </a> - </li> - <?php - $url_state['params']['state'] = 'favorite'; - ?> - <li class="item" role="checkbox" aria-checked="<?php echo ($this->state === 'favorite') ? 'true' :'false'; ?>"> - <a class="print_favorite" href="<?php echo Minz_Url::display ($url_state); ?>"> - <?php echo Minz_Translate::t ('show_favorite'); ?> - </a> - </li> - - <li class="separator"></li> - - <li class="item"> - <?php - $url_order = $url; - if ($this->order === 'DESC') { - $url_order['params']['order'] = 'ASC'; - ?> - <a href="<?php echo Minz_Url::display ($url_order); ?>"> - <?php echo Minz_Translate::t ('older_first'); ?> - </a> - <?php - } else { - $url_order['params']['order'] = 'DESC'; - ?> - <a href="<?php echo Minz_Url::display ($url_order); ?>"> - <?php echo Minz_Translate::t ('newer_first'); ?> - </a> - <?php } ?> - </li> - - <li class="separator"></li> - - <li class="item"> - <a class="view_rss" target="_blank" href="<?php echo Minz_Url::display ($this->rss_url); ?>"> - <?php echo Minz_Translate::t ('rss_view'); ?> - </a> - </li> - </ul> + <?php $url_output = $this->url; ?> + <div class="stick" id="nav_menu_views"> + <?php $url_output['params']['output'] = 'normal'; ?> + <a class="view_normal btn <?php echo $actual_view == 'normal'? 'active' : ''; ?>" title="<?php echo Minz_Translate::t('normal_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>"> + <?php echo FreshRSS_Themes::icon("view-normal"); ?> + </a> + + <?php $url_output['params']['output'] = 'global'; ?> + <a class="view_global btn <?php echo $actual_view == 'global'? 'active' : ''; ?>" title="<?php echo Minz_Translate::t('global_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>"> + <?php echo FreshRSS_Themes::icon("view-global"); ?> + </a> + + <?php $url_output['params']['output'] = 'reader'; ?> + <a class="view_reader btn <?php echo $actual_view == 'reader'? 'active' : ''; ?>" title="<?php echo Minz_Translate::t('reader_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>"> + <?php echo FreshRSS_Themes::icon("view-reader"); ?> + </a> + + <?php + $url_output['params']['output'] = 'rss'; + $url_output['params']['token'] = $this->conf->token; + ?> + <a class="view_rss btn" target="_blank" title="<?php echo Minz_Translate::t ('rss_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>"> + <?php echo FreshRSS_Themes::icon('rss'); ?> + </a> </div> <div class="item search"> @@ -223,4 +216,25 @@ <?php } ?> </form> </div> + + <?php + if ($this->order === 'DESC') { + $order = 'ASC'; + $icon = 'up'; + $title = 'older_first'; + } else { + $order = 'DESC'; + $icon = 'down'; + $title = 'newer_first'; + } + $url_order = $this->url; + $url_order['params']['order'] = $order; + ?> + <a class="btn" href="<?php echo Minz_Url::display ($url_order); ?>" title="<?php echo Minz_Translate::t ($title); ?>"> + <?php echo FreshRSS_Themes::icon($icon); ?> + </a> + + <?php if ($this->loginOk || Minz_Configuration::allowAnonymousRefresh()) { ?> + <a id="actualize" class="btn" href="<?php echo _url ('feed', 'actualize'); ?>"><?php echo FreshRSS_Themes::icon('refresh'); ?></a> + <?php } ?> </div> diff --git a/app/sql.php b/app/sql.php index 1b43da30a..5cd7c52ed 100644 --- a/app/sql.php +++ b/app/sql.php @@ -3,7 +3,6 @@ define('SQL_CREATE_TABLES', ' CREATE TABLE IF NOT EXISTS `%1$scategory` ( `id` SMALLINT NOT NULL AUTO_INCREMENT, -- v0.7 `name` varchar(255) NOT NULL, - `color` char(7), PRIMARY KEY (`id`), UNIQUE KEY (`name`) -- v0.7 ) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci diff --git a/app/views/configure/categorize.phtml b/app/views/configure/categorize.phtml index a564e8cdd..9bae99b39 100644 --- a/app/views/configure/categorize.phtml +++ b/app/views/configure/categorize.phtml @@ -14,14 +14,16 @@ <?php echo Minz_Translate::t ('category_number', $i); ?> </label> <div class="group-controls"> - <input type="text" id="cat_<?php echo $cat->id (); ?>" name="categories[]" value="<?php echo $cat->name (); ?>" /> + <div class="stick"> + <input type="text" id="cat_<?php echo $cat->id (); ?>" name="categories[]" value="<?php echo $cat->name (); ?>" /> - <?php if ($cat->nbFeed () > 0) { ?> - <a class="confirm" href="<?php echo _url ('feed', 'delete', 'id', $cat->id (), 'type', 'category'); ?>"><?php echo Minz_Translate::t ('ask_empty'); ?></a> - <?php } ?> + <?php if ($cat->nbFeed () > 0) { ?> + <button type="submit" class="btn btn-attention confirm" formaction="<?php echo _url ('feed', 'delete', 'id', $cat->id (), 'type', 'category'); ?>"><?php echo Minz_Translate::t ('ask_empty'); ?></button> + <?php } ?> + </div> (<?php echo Minz_Translate::t ('number_feeds', $cat->nbFeed ()); ?>) - <?php if ($cat->id () == $this->defaultCategory->id ()) { ?> + <?php if ($cat->id () === $this->defaultCategory->id ()) { ?> <?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t ('can_not_be_deleted'); ?> <?php } ?> diff --git a/app/views/configure/display.phtml b/app/views/configure/display.phtml index 9104e4ef1..955fc6747 100644 --- a/app/views/configure/display.phtml +++ b/app/views/configure/display.phtml @@ -4,7 +4,7 @@ <a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a> <form method="post" action="<?php echo _url ('configure', 'display'); ?>"> - <legend><?php echo Minz_Translate::t ('theme'); ?></legend> + <legend><?php echo Minz_Translate::t ('display_configuration'); ?></legend> <div class="form-group"> <label class="group-name" for="language"><?php echo Minz_Translate::t ('language'); ?></label> @@ -35,122 +35,29 @@ </div> </div> - <div class="form-group form-actions"> - <div class="group-controls"> - <button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button> - <button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button> - </div> - </div> - - <legend><?php echo Minz_Translate::t ('reading_configuration'); ?></legend> - - <div class="form-group"> - <label class="group-name" for="posts_per_page"><?php echo Minz_Translate::t ('articles_per_page'); ?></label> - <div class="group-controls"> - <input type="number" id="posts_per_page" name="posts_per_page" value="<?php echo $this->conf->posts_per_page; ?>" /> - </div> - </div> - + <?php $width = $this->conf->content_width; ?> <div class="form-group"> - <label class="group-name" for="sort_order"><?php echo Minz_Translate::t ('sort_order'); ?></label> + <label class="group-name" for="content_width"><?php echo Minz_Translate::t('content_width'); ?></label> <div class="group-controls"> - <select name="sort_order" id="sort_order"> - <option value="DESC"<?php echo $this->conf->sort_order === 'DESC' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('newer_first'); ?></option> - <option value="ASC"<?php echo $this->conf->sort_order === 'ASC' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('older_first'); ?></option> + <select name="content_width" id="content_width" required=""> + <option value="thin" <?php echo $width === 'thin'? 'selected="selected"' : ''; ?>> + <?php echo Minz_Translate::t('width_thin'); ?> + </option> + <option value="medium" <?php echo $width === 'medium'? 'selected="selected"' : ''; ?>> + <?php echo Minz_Translate::t('width_medium'); ?> + </option> + <option value="large" <?php echo $width === 'large'? 'selected="selected"' : ''; ?>> + <?php echo Minz_Translate::t('width_large'); ?> + </option> + <option value="no_limit" <?php echo $width === 'no_limit'? 'selected="selected"' : ''; ?>> + <?php echo Minz_Translate::t('width_no_limit'); ?> + </option> </select> </div> </div> <div class="form-group"> - <label class="group-name" for="view_mode"><?php echo Minz_Translate::t ('default_view'); ?></label> - <div class="group-controls"> - <select name="view_mode" id="view_mode"> - <option value="normal"<?php echo $this->conf->view_mode === 'normal' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('normal_view'); ?></option> - <option value="reader"<?php echo $this->conf->view_mode === 'reader' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('reader_view'); ?></option> - <option value="global"<?php echo $this->conf->view_mode === 'global' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('global_view'); ?></option> - </select> - <label class="radio" for="radio_all"> - <input type="radio" name="default_view" id="radio_all" value="all"<?php echo $this->conf->default_view === 'all' ? ' checked="checked"' : ''; ?> /> - <?php echo Minz_Translate::t ('show_all_articles'); ?> - </label> - <label class="radio" for="radio_not_read"> - <input type="radio" name="default_view" id="radio_not_read" value="not_read"<?php echo $this->conf->default_view === 'not_read' ? ' checked="checked"' : ''; ?> /> - <?php echo Minz_Translate::t ('show_not_reads'); ?> - </label> - </div> - </div> - - <div class="form-group"> - <div class="group-controls"> - <label class="checkbox" for="auto_load_more"> - <input type="checkbox" name="auto_load_more" id="auto_load_more" value="1"<?php echo $this->conf->auto_load_more ? ' checked="checked"' : ''; ?> /> - <?php echo Minz_Translate::t ('auto_load_more'); ?> - <noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript> - </label> - </div> - </div> - - <div class="form-group"> - <div class="group-controls"> - <label class="checkbox" for="display_posts"> - <input type="checkbox" name="display_posts" id="display_posts" value="1"<?php echo $this->conf->display_posts ? ' checked="checked"' : ''; ?> /> - <?php echo Minz_Translate::t ('display_articles_unfolded'); ?> - <noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript> - </label> - </div> - </div> - - <div class="form-group"> - <div class="group-controls"> - <label class="checkbox" for="lazyload"> - <input type="checkbox" name="lazyload" id="lazyload" value="1"<?php echo $this->conf->lazyload ? ' checked="checked"' : ''; ?> /> - <?php echo Minz_Translate::t ('img_with_lazyload'); ?> - <noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript> - </label> - </div> - </div> - - <div class="form-group"> - <label class="group-name"><?php echo Minz_Translate::t ('auto_read_when'); ?></label> - <div class="group-controls"> - <label class="checkbox" for="check_open_article"> - <input type="checkbox" name="mark_open_article" id="check_open_article" value="1"<?php echo $this->conf->mark_when['article'] ? ' checked="checked"' : ''; ?> /> - <?php echo Minz_Translate::t ('article_selected'); ?> - </label> - <label class="checkbox" for="check_open_site"> - <input type="checkbox" name="mark_open_site" id="check_open_site" value="1"<?php echo $this->conf->mark_when['site'] ? ' checked="checked"' : ''; ?> /> - <?php echo Minz_Translate::t ('article_open_on_website'); ?> - </label> - <label class="checkbox" for="check_scroll"> - <input type="checkbox" name="mark_scroll" id="check_scroll" value="1"<?php echo $this->conf->mark_when['scroll'] ? ' checked="checked"' : ''; ?> /> - <?php echo Minz_Translate::t ('scroll'); ?> - </label> - <label class="checkbox" for="check_reception"> - <input type="checkbox" name="mark_upon_reception" id="check_reception" value="1"<?php echo $this->conf->mark_when['reception'] ? ' checked="checked"' : ''; ?> /> - <?php echo Minz_Translate::t ('upon_reception'); ?> - </label> - </div> - </div> - - <div class="form-group"> - <label class="group-name"><?php echo Minz_Translate::t ('after_onread'); ?></label> - <div class="group-controls"> - <label class="checkbox" for="onread_jump_next"> - <input type="checkbox" name="onread_jump_next" id="onread_jump_next" value="1"<?php echo $this->conf->onread_jump_next ? ' checked="checked"' : ''; ?> /> - <?php echo Minz_Translate::t ('jump_next'); ?> - </label> - </div> - </div> - - <div class="form-group form-actions"> - <div class="group-controls"> - <button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button> - <button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button> - </div> - </div> - - <legend><?php echo Minz_Translate::t ('reading_icons'); ?></legend> - <div class="form-group"> + <label class="group-name" for="theme"><?php echo Minz_Translate::t ('article_icons'); ?></label> <table> <thead> <tr> diff --git a/app/views/configure/feed.phtml b/app/views/configure/feed.phtml index 138808a9f..27b0990ff 100644 --- a/app/views/configure/feed.phtml +++ b/app/views/configure/feed.phtml @@ -7,8 +7,12 @@ <h1><?php echo $this->flux->name (); ?></h1> <?php echo $this->flux->description (); ?> + <?php $nbEntries = $this->flux->nbEntries (); ?> + <?php if ($this->flux->inError ()) { ?> <p class="alert alert-error"><span class="alert-head"><?php echo Minz_Translate::t ('damn'); ?></span> <?php echo Minz_Translate::t ('feed_in_error'); ?></p> + <?php } elseif ($nbEntries === 0) { ?> + <p class="alert alert-warn"><?php echo Minz_Translate::t ('feed_empty'); ?></p> <?php } ?> <form method="post" action="<?php echo _url ('configure', 'feed', 'id', $this->flux->id ()); ?>" autocomplete="off"> @@ -28,16 +32,21 @@ <div class="form-group"> <label class="group-name" for="website"><?php echo Minz_Translate::t ('website_url'); ?></label> <div class="group-controls"> - <input type="text" name="website" id="website" class="extend" value="<?php echo $this->flux->website (); ?>" /> - <a target="_blank" href="<?php echo $this->flux->website (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a> + <div class="stick"> + <input type="text" name="website" id="website" class="extend" value="<?php echo $this->flux->website (); ?>" /> + <a class="btn" target="_blank" href="<?php echo $this->flux->website (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a> + </div> </div> </div> <div class="form-group"> <label class="group-name" for="url"><?php echo Minz_Translate::t ('feed_url'); ?></label> <div class="group-controls"> - <input type="text" name="url" id="url" class="extend" value="<?php echo $this->flux->url (); ?>" /> - <a target="_blank" href="<?php echo $this->flux->url (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a> - <a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->flux->url (); ?>"><?php echo Minz_Translate::t ('feed_validator'); ?></a> + <div class="stick"> + <input type="text" name="url" id="url" class="extend" value="<?php echo $this->flux->url (); ?>" /> + <a class="btn" target="_blank" href="<?php echo $this->flux->url (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a> + </div> + + <a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->flux->url (); ?>"><?php echo Minz_Translate::t ('feed_validator'); ?></a> </div> </div> <div class="form-group"> @@ -81,7 +90,7 @@ <div class="form-group"> <label class="group-name"><?php echo Minz_Translate::t ('number_articles'); ?></label> <div class="group-controls"> - <span class="control"><?php echo $this->flux->nbEntries (); ?></span> + <span class="control"><?php echo $nbEntries; ?></span> </div> </div> <div class="form-group"> diff --git a/app/views/configure/importExport.phtml b/app/views/configure/importExport.phtml deleted file mode 100644 index e2217d9ed..000000000 --- a/app/views/configure/importExport.phtml +++ /dev/null @@ -1,40 +0,0 @@ -<?php -require_once(LIB_PATH . '/lib_opml.php'); -if ($this->req == 'export') { - echo '<?xml version="1.0" encoding="UTF-8" ?>'; -?> -<!-- Generated by <?php echo Minz_Configuration::title (); ?> --> -<opml version="2.0"> - <head> - <title><?php echo Minz_Configuration::title (); ?> OPML Feed</title> - <dateCreated><?php echo date('D, d M Y H:i:s'); ?></dateCreated> - </head> - <body> -<?php echo opml_export ($this->categories); ?> - </body> -</opml> -<?php } else { ?> -<?php $this->partial ('aside_feed'); ?> - -<div class="post "> - <a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a> - - <form method="post" action="<?php echo Minz_Url::display (array ('c' => 'configure', 'a' => 'importExport', 'params' => array ('q' => 'import'))); ?>" enctype="multipart/form-data"> - <legend><?php echo Minz_Translate::t ('import_export_opml'); ?></legend> - <div class="form-group"> - <label class="group-name" for="file"><?php echo Minz_Translate::t ('file_to_import'); ?></label> - <div class="group-controls"> - <input type="file" name="file" id="file" /> - </div> - </div> - - <div class="form-group form-actions"> - <div class="group-controls"> - <button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('import'); ?></button> - <?php echo Minz_Translate::t ('or'); ?> - <a target="_blank" class="btn btn-important" href="<?php echo _url ('configure', 'importExport', 'q', 'export'); ?>"><?php echo Minz_Translate::t ('export'); ?></a> - </div> - </div> - </form> -</div> -<?php } ?> diff --git a/app/views/configure/reading.phtml b/app/views/configure/reading.phtml new file mode 100644 index 000000000..456ab9522 --- /dev/null +++ b/app/views/configure/reading.phtml @@ -0,0 +1,125 @@ +<?php $this->partial ('aside_configure'); ?> + +<div class="post"> + <a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a> + + <form method="post" action="<?php echo _url ('configure', 'reading'); ?>"> + <legend><?php echo Minz_Translate::t ('reading_configuration'); ?></legend> + + <div class="form-group"> + <label class="group-name" for="posts_per_page"><?php echo Minz_Translate::t ('articles_per_page'); ?></label> + <div class="group-controls"> + <input type="number" id="posts_per_page" name="posts_per_page" value="<?php echo $this->conf->posts_per_page; ?>" /> + </div> + </div> + + <div class="form-group"> + <label class="group-name" for="sort_order"><?php echo Minz_Translate::t ('sort_order'); ?></label> + <div class="group-controls"> + <select name="sort_order" id="sort_order"> + <option value="DESC"<?php echo $this->conf->sort_order === 'DESC' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('newer_first'); ?></option> + <option value="ASC"<?php echo $this->conf->sort_order === 'ASC' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('older_first'); ?></option> + </select> + </div> + </div> + + <div class="form-group"> + <label class="group-name" for="view_mode"><?php echo Minz_Translate::t ('default_view'); ?></label> + <div class="group-controls"> + <select name="view_mode" id="view_mode"> + <option value="normal"<?php echo $this->conf->view_mode === 'normal' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('normal_view'); ?></option> + <option value="reader"<?php echo $this->conf->view_mode === 'reader' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('reader_view'); ?></option> + <option value="global"<?php echo $this->conf->view_mode === 'global' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t ('global_view'); ?></option> + </select> + <label class="radio" for="radio_all"> + <input type="radio" name="default_view" id="radio_all" value="<?php echo FreshRSS_Entry::STATE_ALL; ?>"<?php echo $this->conf->default_view === FreshRSS_Entry::STATE_ALL ? ' checked="checked"' : ''; ?> /> + <?php echo Minz_Translate::t ('show_all_articles'); ?> + </label> + <label class="radio" for="radio_not_read"> + <input type="radio" name="default_view" id="radio_not_read" value="<?php echo FreshRSS_Entry::STATE_NOT_READ; ?>"<?php echo $this->conf->default_view === FreshRSS_Entry::STATE_NOT_READ ? ' checked="checked"' : ''; ?> /> + <?php echo Minz_Translate::t ('show_not_reads'); ?> + </label> + </div> + </div> + + <div class="form-group"> + <div class="group-controls"> + <label class="checkbox" for="auto_load_more"> + <input type="checkbox" name="auto_load_more" id="auto_load_more" value="1"<?php echo $this->conf->auto_load_more ? ' checked="checked"' : ''; ?> /> + <?php echo Minz_Translate::t ('auto_load_more'); ?> + <noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript> + </label> + </div> + </div> + + <div class="form-group"> + <div class="group-controls"> + <label class="checkbox" for="display_posts"> + <input type="checkbox" name="display_posts" id="display_posts" value="1"<?php echo $this->conf->display_posts ? ' checked="checked"' : ''; ?> /> + <?php echo Minz_Translate::t ('display_articles_unfolded'); ?> + <noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript> + </label> + </div> + </div> + + <div class="form-group"> + <div class="group-controls"> + <label class="checkbox" for="lazyload"> + <input type="checkbox" name="lazyload" id="lazyload" value="1"<?php echo $this->conf->lazyload ? ' checked="checked"' : ''; ?> /> + <?php echo Minz_Translate::t ('img_with_lazyload'); ?> + <noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript> + </label> + </div> + </div> + + <div class="form-group"> + <div class="group-controls"> + <label class="checkbox" for="sticky_post"> + <input type="checkbox" name="sticky_post" id="sticky_post" value="1"<?php echo $this->conf->sticky_post ? ' checked="checked"' : ''; ?> /> + <?php echo Minz_Translate::t ('sticky_post'); ?> + <noscript> — <strong><?php echo Minz_Translate::t ('javascript_should_be_activated'); ?></strong></noscript> + </label> + </div> + </div> + + <div class="form-group"> + <label class="group-name"><?php echo Minz_Translate::t ('auto_read_when'); ?></label> + <div class="group-controls"> + <label class="checkbox" for="check_open_article"> + <input type="checkbox" name="mark_open_article" id="check_open_article" value="1"<?php echo $this->conf->mark_when['article'] ? ' checked="checked"' : ''; ?> /> + <?php echo Minz_Translate::t ('article_selected'); ?> + </label> + <label class="checkbox" for="check_open_site"> + <input type="checkbox" name="mark_open_site" id="check_open_site" value="1"<?php echo $this->conf->mark_when['site'] ? ' checked="checked"' : ''; ?> /> + <?php echo Minz_Translate::t ('article_open_on_website'); ?> + </label> + <label class="checkbox" for="check_scroll"> + <input type="checkbox" name="mark_scroll" id="check_scroll" value="1"<?php echo $this->conf->mark_when['scroll'] ? ' checked="checked"' : ''; ?> /> + <?php echo Minz_Translate::t ('scroll'); ?> + </label> + <label class="checkbox" for="check_reception"> + <input type="checkbox" name="mark_upon_reception" id="check_reception" value="1"<?php echo $this->conf->mark_when['reception'] ? ' checked="checked"' : ''; ?> /> + <?php echo Minz_Translate::t ('upon_reception'); ?> + </label> + </div> + </div> + + <div class="form-group"> + <label class="group-name"><?php echo Minz_Translate::t ('after_onread'); ?></label> + <div class="group-controls"> + <label class="checkbox" for="onread_jump_next"> + <input type="checkbox" name="onread_jump_next" id="onread_jump_next" value="1"<?php echo $this->conf->onread_jump_next ? ' checked="checked"' : ''; ?> /> + <?php echo Minz_Translate::t ('jump_next'); ?> + </label> + </div> + </div> + + <div class="form-group form-actions"> + <div class="group-controls"> + <button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('save'); ?></button> + <button type="reset" class="btn"><?php echo Minz_Translate::t ('cancel'); ?></button> + </div> + </div> + + </form> +</div> diff --git a/app/views/configure/sharing.phtml b/app/views/configure/sharing.phtml index e3ea11665..a952bc3b4 100644 --- a/app/views/configure/sharing.phtml +++ b/app/views/configure/sharing.phtml @@ -3,54 +3,49 @@ <div class="post"> <a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a> - <form method="post" action="<?php echo _url ('configure', 'sharing'); ?>"> + <form method="post" action="<?php echo _url ('configure', 'sharing'); ?>" + data-simple='<div class="form-group"><label class="group-name">##label##</label><div class="group-controls"><a href="#" class="share remove btn btn-attention"><?php echo FreshRSS_Themes::icon('close'); ?></a> + <input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" /></div></div>' + data-advanced='<div class="form-group"><label class="group-name">##label##</label><div class="group-controls"> + <input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" /> + <div class="stick"> + <input type="text" id="share_##key##_name" name="share[##key##][name]" class="extend" value="" placeholder="<?php echo Minz_Translate::t ('share_name'); ?>" size="64" /> + <input type="url" id="share_##key##_url" name="share[##key##][url]" class="extend" value="" placeholder="<?php echo Minz_Translate::t ('share_url'); ?>" size="64" /> + <a href="#" class="share remove btn btn-attention"><?php echo FreshRSS_Themes::icon('close'); ?></a></div> + <a target="_blank" class="btn" title="<?php echo Minz_Translate::t('more_information'); ?>" href="##help##"><?php echo FreshRSS_Themes::icon('help'); ?></a> + </div></div>'> <legend><?php echo Minz_Translate::t ('sharing'); ?></legend> - <div class="form-group"> - <label class="group-name" for="shaarli"> - <?php echo Minz_Translate::t ('your_shaarli'); ?> - </label> - <div class="group-controls"> - <input type="url" id="shaarli" name="shaarli" class="extend" value="<?php echo $this->conf->sharing ('shaarli'); ?>" placeholder="<?php echo Minz_Translate::t ('blank_to_disable'); ?>" size="64" /> - - <?php echo FreshRSS_Themes::icon('help'); ?> <a target="_blank" href="http://sebsauvage.net/wiki/doku.php?id=php:shaarli"><?php echo Minz_Translate::t ('more_information'); ?></a> - </div> - </div> - - <div class="form-group"> - <label class="group-name" for="wallabag"> - <?php echo Minz_Translate::t ('your_wallabag'); ?> - </label> - <div class="group-controls"> - <input type="url" id="wallabag" name="wallabag" class="extend" value="<?php echo $this->conf->sharing ('wallabag'); ?>" placeholder="<?php echo Minz_Translate::t ('blank_to_disable'); ?>" size="64" /> - - <?php echo FreshRSS_Themes::icon('help'); ?> <a target="_blank" href="http://www.wallabag.org"><?php echo Minz_Translate::t ('more_information'); ?></a> - </div> - </div> - - <div class="form-group"> - <label class="group-name" for="diaspora"> - <?php echo Minz_Translate::t ('your_diaspora_pod'); ?> - </label> - <div class="group-controls"> - <input type="url" id="diaspora" name="diaspora" class="extend" value="<?php echo $this->conf->sharing ('diaspora'); ?>" placeholder="<?php echo Minz_Translate::t ('blank_to_disable'); ?>" size="64" /> - - <?php echo FreshRSS_Themes::icon('help'); ?> <a target="_blank" href="https://diasporafoundation.org/"><?php echo Minz_Translate::t ('more_information'); ?></a> + <?php foreach ($this->conf->sharing as $key => $sharing): ?> + <?php $share = $this->conf->shares[$sharing['type']]; ?> + <div class="form-group"> + <label class="group-name"> + <?php echo Minz_Translate::t ($sharing['type']); ?> + </label> + <div class="group-controls"> + <input type='hidden' id='share_<?php echo $key;?>_type' name="share[<?php echo $key;?>][type]" value='<?php echo $sharing['type']?>' /> + <?php if ($share['form'] === 'advanced'){ ?> + <div class="stick"> + <input type="text" id="share_<?php echo $key;?>_name" name="share[<?php echo $key;?>][name]" class="extend" value="<?php echo $sharing['name']?>" placeholder="<?php echo Minz_Translate::t ('share_name'); ?>" size="64" /> + <input type="url" id="share_<?php echo $key;?>_url" name="share[<?php echo $key;?>][url]" class="extend" value="<?php echo $sharing['url']?>" placeholder="<?php echo Minz_Translate::t ('share_url'); ?>" size="64" /> + <a href='#' class='share remove btn btn-attention'><?php echo FreshRSS_Themes::icon('close'); ?></a> + </div> + + <a target="_blank" class="btn" title="<?php echo Minz_Translate::t('more_information'); ?>" href="<?php echo $share['help']?>"><?php echo FreshRSS_Themes::icon('help'); ?></a> + <?php } else { ?> + <a href='#' class='share remove btn btn-attention'><?php echo FreshRSS_Themes::icon('close'); ?></a> + <?php } ?> + </div> </div> - </div> + <?php endforeach;?> <div class="form-group"> - <label class="group-name"><?php echo Minz_Translate::t ('activate_sharing'); ?></label> <div class="group-controls"> - <?php - $services = array ('twitter', 'g+', 'facebook', 'email', 'print'); - - foreach ($services as $service) { - ?> - <label class="checkbox" for="<?php echo $service; ?>"> - <input type="checkbox" name="<?php echo $service; ?>" id="<?php echo $service; ?>" value="1"<?php echo $this->conf->sharing($service) ? ' checked="checked"' : ''; ?> /> - <?php echo Minz_Translate::t ($service); ?> - </label> - <?php } ?> + <select> + <?php foreach($this->conf->shares as $key => $params):?> + <option value='<?php echo $key?>' data-form='<?php echo $params['form']?>' data-help='<?php if (!empty($params['help'])) {echo $params['help'];}?>'><?php echo Minz_Translate::t($key) ?></option> + <?php endforeach; ?> + </select> + <a href='#' class='share add btn'><?php echo FreshRSS_Themes::icon('add'); ?></a> </div> </div> diff --git a/app/views/configure/shortcut.phtml b/app/views/configure/shortcut.phtml index b0867f711..bfb13f003 100644 --- a/app/views/configure/shortcut.phtml +++ b/app/views/configure/shortcut.phtml @@ -12,10 +12,44 @@ <?php $s = $this->conf->shortcuts; ?> <form method="post" action="<?php echo _url ('configure', 'shortcut'); ?>"> - <legend><?php echo Minz_Translate::t ('shortcuts_management'); ?></legend> + <legend><?php echo Minz_Translate::t ('shortcuts'); ?></legend> <noscript><p class="alert alert-error"><?php echo Minz_Translate::t ('javascript_for_shortcuts'); ?></p></noscript> + <legend><?php echo Minz_Translate::t ('shortcuts_navigation'); ?></legend> + + <div class="form-group"> + <label class="group-name" for="next_entry"><?php echo Minz_Translate::t ('next_article'); ?></label> + <div class="group-controls"> + <input type="text" id="next_entry" name="shortcuts[next_entry]" list="keys" value="<?php echo $s['next_entry']; ?>" /> + </div> + </div> + + <div class="form-group"> + <label class="group-name" for="prev_entry"><?php echo Minz_Translate::t ('previous_article'); ?></label> + <div class="group-controls"> + <input type="text" id="prev_entry" name="shortcuts[prev_entry]" list="keys" value="<?php echo $s['prev_entry']; ?>" /> + </div> + </div> + + <div class="form-group"> + <label class="group-name" for="first_entry"><?php echo Minz_Translate::t ('first_article'); ?></label> + <div class="group-controls"> + <input type="text" id="first_entry" name="shortcuts[first_entry]" list="keys" value="<?php echo $s['first_entry']; ?>" /> + </div> + </div> + + <div class="form-group"> + <label class="group-name" for="last_entry"><?php echo Minz_Translate::t ('last_article'); ?></label> + <div class="group-controls"> + <input type="text" id="last_entry" name="shortcuts[last_entry]" list="keys" value="<?php echo $s['last_entry']; ?>" /> + </div> + </div> + + <div><?php echo Minz_Translate::t ('shortcuts_navigation_help');?></div> + + <legend><?php echo Minz_Translate::t ('shortcuts_article_action');?></legend> + <div class="form-group"> <label class="group-name" for="mark_read"><?php echo Minz_Translate::t ('mark_read'); ?></label> <div class="group-controls"> @@ -39,18 +73,10 @@ </div> <div class="form-group"> - <label class="group-name" for="next_entry"><?php echo Minz_Translate::t ('next_article'); ?></label> - <div class="group-controls"> - <input type="text" id="next_entry" name="shortcuts[next_entry]" list="keys" value="<?php echo $s['next_entry']; ?>" /> - <?php echo Minz_Translate::t ('shift_for_last'); ?> - </div> - </div> - - <div class="form-group"> - <label class="group-name" for="prev_entry"><?php echo Minz_Translate::t ('previous_article'); ?></label> + <label class="group-name" for="auto_share_shortcut"><?php echo Minz_Translate::t ('auto_share'); ?></label> <div class="group-controls"> - <input type="text" id="prev_entry" name="shortcuts[prev_entry]" list="keys" value="<?php echo $s['prev_entry']; ?>" /> - <?php echo Minz_Translate::t ('shift_for_first'); ?> + <input type="text" id="auto_share_shortcut" name="shortcuts[auto_share]" list="keys" value="<?php echo $s['auto_share']; ?>" /> + <?php echo Minz_Translate::t ('auto_share_help'); ?> </div> </div> @@ -61,6 +87,8 @@ </div> </div> + <legend><?php echo Minz_Translate::t ('shortcuts_other_action');?></legend> + <div class="form-group"> <label class="group-name" for="load_more_shortcut"><?php echo Minz_Translate::t ('load_more'); ?></label> <div class="group-controls"> @@ -69,9 +97,9 @@ </div> <div class="form-group"> - <label class="group-name" for="auto_share_shortcut"><?php echo Minz_Translate::t ('auto_share'); ?></label> + <label class="group-name" for="focus_search_shortcut"><?php echo Minz_Translate::t ('focus_search'); ?></label> <div class="group-controls"> - <input type="text" id="auto_share_shortcut" name="shortcuts[auto_share]" list="keys" value="<?php echo $s['auto_share']; ?>" /> + <input type="text" id="focus_search_shortcut" name="shortcuts[focus_search]" list="keys" value="<?php echo $s['focus_search']; ?>" /> </div> </div> diff --git a/app/views/configure/users.phtml b/app/views/configure/users.phtml index 8ab4c04ba..272896fb2 100644 --- a/app/views/configure/users.phtml +++ b/app/views/configure/users.phtml @@ -20,16 +20,31 @@ <div class="form-group"> <label class="group-name" for="passwordPlain"><?php echo Minz_Translate::t('password_form'); ?></label> <div class="group-controls"> - <input type="password" id="passwordPlain" name="passwordPlain" pattern=".{7,}" /> + <div class="stick"> + <input type="password" id="passwordPlain" name="passwordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/> + <a class="btn toggle-password"><?php echo FreshRSS_Themes::icon('key'); ?></a> + </div> <noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript> </div> </div> + <?php if (Minz_Configuration::apiEnabled()) { ?> + <div class="form-group"> + <label class="group-name" for="apiPasswordPlain"><?php echo Minz_Translate::t('password_api'); ?></label> + <div class="group-controls"> + <div class="stick"> + <input type="password" id="apiPasswordPlain" name="apiPasswordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/> + <a class="btn toggle-password"><?php echo FreshRSS_Themes::icon('key'); ?></a> + </div> + </div> + </div> + <?php } ?> + <div class="form-group"> <label class="group-name" for="mail_login"><?php echo Minz_Translate::t('persona_connection_email'); ?></label> <?php $mail = $this->conf->mail_login; ?> <div class="group-controls"> - <input type="email" id="mail_login" name="mail_login" class="extend" value="<?php echo $mail; ?>" <?php echo Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_')) ? '' : 'disabled="disabled"'; ?> placeholder="alice@example.net" /> + <input type="email" id="mail_login" name="mail_login" class="extend" autocomplete="off" value="<?php echo $mail; ?>" <?php echo Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_')) ? '' : 'disabled="disabled"'; ?> placeholder="alice@example.net" /> <noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript> </div> </div> @@ -52,7 +67,7 @@ <?php if (!in_array(Minz_Configuration::authType(), array('form', 'persona', 'http_auth', 'none'))) { ?> <option selected="selected"></option> <?php } ?> - <option value="form"<?php echo Minz_Configuration::authType() === 'form' ? ' selected="selected"' : '', version_compare(PHP_VERSION, '5.3', '<') ? ' disabled="disabled"' : ''; ?>><?php echo Minz_Translate::t('auth_form'); ?></option> + <option value="form"<?php echo Minz_Configuration::authType() === 'form' ? ' selected="selected"' : '', cryptAvailable() ? '' : ' disabled="disabled"'; ?>><?php echo Minz_Translate::t('auth_form'); ?></option> <option value="persona"<?php echo Minz_Configuration::authType() === 'persona' ? ' selected="selected"' : '', $this->conf->mail_login == '' ? ' disabled="disabled"' : ''; ?>><?php echo Minz_Translate::t('auth_persona'); ?></option> <option value="http_auth"<?php echo Minz_Configuration::authType() === 'http_auth' ? ' selected="selected"' : '', httpAuthUser() == '' ? ' disabled="disabled"' : ''; ?>><?php echo Minz_Translate::t('http_auth'); ?> (REMOTE_USER = '<?php echo httpAuthUser(); ?>')</option> <option value="none"<?php echo Minz_Configuration::authType() === 'none' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t('auth_none'); ?></option> @@ -70,18 +85,49 @@ </div> </div> + <div class="form-group"> + <div class="group-controls"> + <label class="checkbox" for="anon_refresh"> + <input type="checkbox" name="anon_refresh" id="anon_refresh" value="1"<?php echo Minz_Configuration::allowAnonymousRefresh() ? ' checked="checked"' : '', + Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> /> + <?php echo Minz_Translate::t('allow_anonymous_refresh'); ?> + </label> + </div> + </div> + + <div class="form-group"> + <div class="group-controls"> + <label class="checkbox" for="unsafe_autologin"> + <input type="checkbox" name="unsafe_autologin" id="unsafe_autologin" value="1"<?php echo Minz_Configuration::unsafeAutologinEnabled() ? ' checked="checked"' : '', + Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> /> + <?php echo Minz_Translate::t('unsafe_autologin'); ?> + <kbd>p/i/?a=formLogin&u=Alice&p=1234</kbd> + </label> + </div> + </div> + <?php if (Minz_Configuration::canLogIn()) { ?> <div class="form-group"> <label class="group-name" for="token"><?php echo Minz_Translate::t('auth_token'); ?></label> <?php $token = $this->conf->token; ?> <div class="group-controls"> - <input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo Minz_Translate::t('blank_to_disable'); ?>"<?php + <input type="text" id="token" name="token" value="<?php echo $token; ?>" placeholder="<?php echo Minz_Translate::t('blank_to_disable'); ?>"<?php echo Minz_Configuration::canLogIn() ? '' : ' disabled="disabled"'; ?> /> <?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t('explain_token', Minz_Url::display(null, 'html', true), $token); ?> </div> </div> <?php } ?> + <div class="form-group"> + <div class="group-controls"> + <label class="checkbox" for="api_enabled"> + <input type="checkbox" name="api_enabled" id="api_enabled" value="1"<?php echo Minz_Configuration::apiEnabled() ? ' checked="checked"' : '', + Minz_Configuration::needsLogin() ? '' : ' disabled="disabled"'; ?> /> + <?php echo Minz_Translate::t('api_enabled'); ?> + </label> + </div> + </div> + <div class="form-group form-actions"> <div class="group-controls"> <button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('save'); ?></button> @@ -129,14 +175,17 @@ <div class="form-group"> <label class="group-name" for="new_user_name"><?php echo Minz_Translate::t('username'); ?></label> <div class="group-controls"> - <input id="new_user_name" name="new_user_name" type="text" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" placeholder="demo" /> + <input id="new_user_name" name="new_user_name" type="text" size="16" required="required" maxlength="16" autocomplete="off" pattern="[0-9a-zA-Z]{1,16}" placeholder="demo" /> </div> </div> <div class="form-group"> <label class="group-name" for="new_user_passwordPlain"><?php echo Minz_Translate::t('password_form'); ?></label> <div class="group-controls"> - <input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" pattern=".{7,}" /> + <div class="stick"> + <input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="off" pattern=".{7,}" /> + <a class="btn toggle-password"><?php echo FreshRSS_Themes::icon('key'); ?></a> + </div> <noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript> </div> </div> @@ -145,7 +194,7 @@ <label class="group-name" for="new_user_email"><?php echo Minz_Translate::t('persona_connection_email'); ?></label> <?php $mail = $this->conf->mail_login; ?> <div class="group-controls"> - <input type="email" id="new_user_email" name="new_user_email" class="extend" placeholder="alice@example.net" /> + <input type="email" id="new_user_email" name="new_user_email" class="extend" autocomplete="off" placeholder="alice@example.net" /> </div> </div> diff --git a/app/views/error/index.phtml b/app/views/error/index.phtml index 36fcb56f9..6a09c3aa2 100644 --- a/app/views/error/index.phtml +++ b/app/views/error/index.phtml @@ -3,7 +3,15 @@ <h1 class="alert-head"><?php echo $this->code; ?></h1> <p> - <?php echo Minz_Translate::t ('page_not_found'); ?><br /> + <?php + switch(Minz_Request::param ('code')) { + case 403: + echo Minz_Translate::t ('forbidden_access'); + break; + case 404: + default: + echo Minz_Translate::t ('page_not_found'); + } ?><br /> <a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a> </p> </div> diff --git a/app/views/feed/add.phtml b/app/views/feed/add.phtml new file mode 100644 index 000000000..849dacac6 --- /dev/null +++ b/app/views/feed/add.phtml @@ -0,0 +1,91 @@ +<?php if ($this->feed) { ?> +<div class="post"> + <h1><?php echo Minz_Translate::t ('add_rss_feed'); ?></h1> + + <?php if (!$this->load_ok) { ?> + <p class="alert alert-error"><span class="alert-head"><?php echo Minz_Translate::t('damn'); ?></span> <?php echo Minz_Translate::t('internal_problem_feed', _url('index', 'logs')); ?></p> + <?php } ?> + + <form method="post" action="<?php echo _url('feed', 'add'); ?>" autocomplete="off"> + <legend><?php echo Minz_Translate::t('informations'); ?></legend> + <?php if ($this->load_ok) { ?> + <div class="form-group"> + <label class="group-name"><?php echo Minz_Translate::t('title'); ?></label> + <div class="group-controls"> + <label><?php echo $this->feed->name() ; ?></label> + </div> + </div> + + <?php $desc = $this->feed->description(); if ($desc != '') { ?> + <div class="form-group"> + <label class="group-name"><?php echo Minz_Translate::t('feed_description'); ?></label> + <div class="group-controls"> + <label><?php echo htmlspecialchars($desc, ENT_NOQUOTES, 'UTF-8'); ?></label> + </div> + </div> + <?php } ?> + + <div class="form-group"> + <label class="group-name"><?php echo Minz_Translate::t('website_url'); ?></label> + <div class="group-controls"> + <?php echo $this->feed->website(); ?> + <a class="btn" target="_blank" href="<?php echo $this->feed->website(); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a> + </div> + </div> + <?php } ?> + + <div class="form-group"> + <label class="group-name" for="url"><?php echo Minz_Translate::t('feed_url'); ?></label> + <div class="group-controls"> + <div class="stick"> + <input type="text" name="url_rss" id="url" class="extend" value="<?php echo $this->feed->url(); ?>" /> + <a class="btn" target="_blank" href="<?php echo $this->feed->url(); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a> + </div> + <a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->feed->url(); ?>"><?php echo Minz_Translate::t('feed_validator'); ?></a> + </div> + </div> + <div class="form-group"> + <label class="group-name" for="category"><?php echo Minz_Translate::t('category'); ?></label> + <div class="group-controls"> + <select name="category" id="category"> + <?php foreach ($this->categories as $cat) { ?> + <option value="<?php echo $cat->id(); ?>"<?php echo $cat->id() == 1 ? ' selected="selected"' : ''; ?>> + <?php echo $cat->name(); ?> + </option> + <?php } ?> + <option value="nc"><?php echo Minz_Translate::t('new_category'); ?></option> + </select> + + <span style="display: none;"> + <input type="text" name="new_category[name]" id="new_category_name" autocomplete="off" placeholder="<?php echo Minz_Translate::t('new_category'); ?>" /> + </span> + </div> + </div> + + <legend><?php echo Minz_Translate::t('http_authentication'); ?></legend> + <?php $auth = $this->feed->httpAuth(false); ?> + <div class="form-group"> + <label class="group-name" for="http_user"><?php echo Minz_Translate::t('http_username'); ?></label> + <div class="group-controls"> + <input type="text" name="http_user" id="http_user" class="extend" value="<?php echo $auth['username']; ?>" autocomplete="off" /> + </div> + + <label class="group-name" for="http_pass"><?php echo Minz_Translate::t('http_password'); ?></label> + <div class="group-controls"> + <input type="password" name="http_pass" id="http_pass" class="extend" value="<?php echo $auth['password']; ?>" autocomplete="off" /> + </div> + + <div class="group-controls"> + <?php echo FreshRSS_Themes::icon('help'); ?> <?php echo Minz_Translate::t('access_protected_feeds'); ?> + </div> + </div> + + <div class="form-group form-actions"> + <div class="group-controls"> + <button type="submit" class="btn btn-important"><?php echo Minz_Translate::t('save'); ?></button> + <button type="reset" class="btn"><?php echo Minz_Translate::t('cancel'); ?></button> + </div> + </div> + </form> +</div> +<?php } ?> diff --git a/app/views/helpers/export/articles.phtml b/app/views/helpers/export/articles.phtml new file mode 100644 index 000000000..ffdca1daa --- /dev/null +++ b/app/views/helpers/export/articles.phtml @@ -0,0 +1,47 @@ +<?php + $username = Minz_Session::param('currentUser', '_'); + + $articles = array( + 'id' => 'user/' . str_replace('/', '', $username) . '/state/org.freshrss/' . $this->type, + 'title' => $this->list_title, + 'author' => $username, + 'items' => array() + ); + + foreach ($this->entries as $entry) { + if (!isset($this->feed)) { + $feed = FreshRSS_CategoryDAO::findFeed($this->categories, $entry->feed ()); + } else { + $feed = $this->feed; + } + + $articles['items'][] = array( + 'id' => $entry->guid(), + 'categories' => array_values($entry->tags()), + 'title' => $entry->title(), + 'author' => $entry->author(), + 'published' => $entry->date(true), + 'updated' => $entry->date(true), + 'alternate' => array(array( + 'href' => $entry->link(), + 'type' => 'text/html' + )), + 'content' => array( + 'content' => $entry->content() + ), + 'origin' => array( + 'streamId' => $feed->id(), + 'title' => $feed->name(), + 'htmlUrl' => $feed->website(), + 'feedUrl' => $feed->url() + ) + ); + } + + $options = 0; + if (version_compare(PHP_VERSION, '5.4.0') >= 0) { + $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; + } + + echo json_encode($articles, $options); +?> diff --git a/app/views/helpers/export/opml.phtml b/app/views/helpers/export/opml.phtml new file mode 100644 index 000000000..f667d093c --- /dev/null +++ b/app/views/helpers/export/opml.phtml @@ -0,0 +1,28 @@ +<?php + +$opml_array = array( + 'head' => array( + 'title' => Minz_Configuration::title(), + 'dateCreated' => date('D, d M Y H:i:s') + ), + 'body' => array() +); + +foreach ($this->categories as $key => $cat) { + $opml_array['body'][$key] = array( + 'text' => $cat['name'], + '@outlines' => array() + ); + + foreach ($cat['feeds'] as $feed) { + $opml_array['body'][$key]['@outlines'][] = array( + 'text' => htmlspecialchars_decode($feed->name()), + 'type' => 'rss', + 'xmlUrl' => $feed->url(), + 'htmlUrl' => $feed->website(), + 'description' => $feed->description() + ); + } +} + +echo libopml_render($opml_array); diff --git a/app/views/helpers/javascript_vars.phtml b/app/views/helpers/javascript_vars.phtml index 0ecdc1bca..6e0a20de3 100644 --- a/app/views/helpers/javascript_vars.phtml +++ b/app/views/helpers/javascript_vars.phtml @@ -5,12 +5,14 @@ echo '"use strict";', "\n"; $mark = $this->conf->mark_when; echo 'var ', 'hide_posts=', ($this->conf->display_posts || Minz_Request::param('output') === 'reader') ? 'false' : 'true', + ',display_order="', Minz_Request::param('order', $this->conf->sort_order), '"', ',auto_mark_article=', $mark['article'] ? 'true' : 'false', ',auto_mark_site=', $mark['site'] ? 'true' : 'false', ',auto_mark_scroll=', $mark['scroll'] ? 'true' : 'false', ',auto_load_more=', $this->conf->auto_load_more ? 'true' : 'false', ',full_lazyload=', $this->conf->lazyload && ($this->conf->display_posts || Minz_Request::param('output') === 'reader') ? 'true' : 'false', - ',does_lazyload=', $this->conf->lazyload ? 'true' : 'false'; + ',does_lazyload=', $this->conf->lazyload ? 'true' : 'false', + ',sticky_post=', $this->conf->sticky_post ? 'true' : 'false'; $s = $this->conf->shortcuts; echo ',shortcuts={', @@ -19,9 +21,12 @@ echo ',shortcuts={', 'go_website:"', $s['go_website'], '",', 'prev_entry:"', $s['prev_entry'], '",', 'next_entry:"', $s['next_entry'], '",', + 'first_entry:"', $s['first_entry'], '",', + 'last_entry:"', $s['last_entry'], '",', 'collapse_entry:"', $s['collapse_entry'], '",', 'load_more:"', $s['load_more'], '",', - 'auto_share:"', $s['auto_share'], '"', + 'auto_share:"', $s['auto_share'], '",', + 'focus_search:"', $s['focus_search'], '"', "},\n"; if (Minz_Request::param ('output') === 'global') { @@ -30,7 +35,13 @@ if (Minz_Request::param ('output') === 'global') { $authType = Minz_Configuration::authType(); if ($authType === 'persona') { - echo 'current_user_mail="' . Minz_Session::param ('mail', '') . '",'; + // If user is disconnected, current_user_mail MUST be null + $mail = Minz_Session::param ('mail', false); + if ($mail) { + echo 'current_user_mail="' . $mail . '",'; + } else { + echo 'current_user_mail=null,'; + } } echo 'authType="', $authType, '",', diff --git a/app/views/helpers/view/normal_view.phtml b/app/views/helpers/view/normal_view.phtml index 7b7faccee..6f172d579 100644 --- a/app/views/helpers/view/normal_view.phtml +++ b/app/views/helpers/view/normal_view.phtml @@ -8,19 +8,10 @@ if (!empty($this->entries)) { $display_yesterday = true; $display_others = true; if ($this->loginOk) { - $shaarli = $this->conf->sharing ('shaarli'); - $wallabag = $this->conf->sharing ('wallabag'); - $diaspora = $this->conf->sharing ('diaspora'); + $sharing = $this->conf->sharing; } else { - $shaarli = ''; - $wallabag = ''; - $diaspora = ''; + $sharing = array(); } - $twitter = $this->conf->sharing ('twitter'); - $google_plus = $this->conf->sharing ('g+'); - $facebook = $this->conf->sharing ('facebook'); - $email = $this->conf->sharing ('email'); - $print = $this->conf->sharing ('print'); $hidePosts = !$this->conf->display_posts; $lazyload = $this->conf->lazyload; $topline_read = $this->conf->topline_read; @@ -29,17 +20,17 @@ if (!empty($this->entries)) { $topline_link = $this->conf->topline_link; $bottomline_read = $this->conf->bottomline_read; $bottomline_favorite = $this->conf->bottomline_favorite; - $bottomline_sharing = $this->conf->bottomline_sharing && ( - $shaarli || $wallabag || $diaspora || $twitter || - $google_plus || $facebook || $email || $print); + $bottomline_sharing = $this->conf->bottomline_sharing && (count($sharing)); $bottomline_tags = $this->conf->bottomline_tags; $bottomline_date = $this->conf->bottomline_date; $bottomline_link = $this->conf->bottomline_link; + + $content_width = $this->conf->content_width; ?> <div id="stream" class="normal<?php echo $hidePosts ? ' hide_posts' : ''; ?>"><?php ?><div id="new-article"> - <a href="<?php echo _url('index', 'index'); ?>"><?php echo Minz_Translate::t ('new_article'); ?></a> + <a href="<?php echo Minz_Url::display ($this->url); ?>"><?php echo Minz_Translate::t ('new_article'); ?></a> </div><?php foreach ($this->entries as $item) { if ($display_today && $item->isDay (FreshRSS_Days::TODAY, $this->today)) { @@ -98,8 +89,8 @@ if (!empty($this->entries)) { </ul> <div class="flux_content"> - <div class="content"> - <h1 class="title"><?php echo $item->title (); ?></h1> + <div class="content <?php echo $content_width; ?>"> + <h1 class="title"><a target="_blank" href="<?php echo $item->link (); ?>"><?php echo $item->title (); ?></a></h1> <?php $author = $item->author (); echo $author != '' ? '<div class="author">' . Minz_Translate::t ('by_author', $author) . '</div>' : ''; @@ -146,55 +137,13 @@ if (!empty($this->entries)) { <ul class="dropdown-menu"> <li class="dropdown-close"><a href="#close">❌</a></li> - <?php if ($shaarli) { ?> - <li class="item"> - <a target="_blank" href="<?php echo $shaarli . '?post=' . $link . '&title=' . $title . '&source=FreshRSS'; ?>"> - <?php echo Minz_Translate::t ('shaarli'); ?> - </a> - </li> - <?php } if ($wallabag) { ?> - <li class="item"> - <a target="_blank" href="<?php echo $wallabag . '?action=add&url=' . base64_encode (urldecode($link)); ?>"> - <?php echo Minz_Translate::t ('wallabag'); ?> - </a> - </li> - <?php } if ($diaspora) { ?> - <li class="item"> - <a target="_blank" href="<?php echo $diaspora . '/bookmarklet?url=' . $link . '&title=' . $title; ?>"> - <?php echo Minz_Translate::t ('diaspora'); ?> - </a> - </li> - <?php } if ($twitter) { ?> - <li class="item"> - <a target="_blank" href="https://twitter.com/share?url=<?php echo $link; ?>&text=<?php echo $title; ?>"> - <?php echo Minz_Translate::t ('twitter'); ?> - </a> - </li> - <?php } if ($google_plus) { ?> - <li class="item"> - <a target="_blank" href="https://plus.google.com/share?url=<?php echo $link; ?>"> - <?php echo Minz_Translate::t ('g+'); ?> - </a> - </li> - <?php } if ($facebook) { ?> - <li class="item"> - <a target="_blank" href="https://www.facebook.com/sharer.php?u=<?php echo $link; ?>&t=<?php echo $title; ?>"> - <?php echo Minz_Translate::t ('facebook'); ?> - </a> - </li> - <?php } if ($email) { ?> - <li class="item"> - <a href="mailto:?subject=<?php echo urldecode($title); ?>&body=<?php echo $link; ?>"> - <?php echo Minz_Translate::t ('by_email'); ?> - </a> - </li> - <?php } if ($print) { ?> - <li class="item"> - <a href="#" class="print-article"> - <?php echo Minz_Translate::t ('print'); ?> - </a> - </li> - <?php } ?> + <?php foreach ($sharing as $share) :?> + <li class="item share"> + <a target="_blank" href="<?php echo FreshRSS_Share::generateUrl($this->conf->shares, $share, $item->link(), $item->title() . ' . ' . $feed->name())?>"> + <?php echo Minz_Translate::t ($share['name']);?> + </a> + </li> + <?php endforeach;?> </ul> </div> <?php } ?> diff --git a/app/views/helpers/view/reader_view.phtml b/app/views/helpers/view/reader_view.phtml index bda96e86d..e37c78cb4 100644 --- a/app/views/helpers/view/reader_view.phtml +++ b/app/views/helpers/view/reader_view.phtml @@ -3,6 +3,7 @@ $this->partial ('nav_menu'); if (!empty($this->entries)) { $lazyload = $this->conf->lazyload; + $content_width = $this->conf->content_width; ?> <div id="stream" class="reader"> @@ -10,7 +11,7 @@ if (!empty($this->entries)) { <div class="flux<?php echo !$item->isRead () ? ' not_read' : ''; ?><?php echo $item->isFavorite () ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id (); ?>"> <div class="flux_content"> - <div class="content"> + <div class="content <?php echo $content_width; ?>"> <?php $feed = FreshRSS_CategoryDAO::findFeed($this->cat_aside, $item->feed ()); //We most likely already have the feed object in cache if (empty($feed)) $feed = $item->feed (true); diff --git a/app/views/helpers/view/rss_view.phtml b/app/views/helpers/view/rss_view.phtml index 620bf1388..2c6ca610b 100755 --- a/app/views/helpers/view/rss_view.phtml +++ b/app/views/helpers/view/rss_view.phtml @@ -6,7 +6,7 @@ <description><?php echo Minz_Translate::t ('rss_feeds_of', $this->rss_title); ?></description> <pubDate><?php echo date('D, d M Y H:i:s O'); ?></pubDate> <lastBuildDate><?php echo gmdate('D, d M Y H:i:s'); ?> GMT</lastBuildDate> - <atom:link href="<?php echo Minz_Url::display ($this->rss_url, 'html', true); ?>" rel="self" type="application/rss+xml" /> + <atom:link href="<?php echo Minz_Url::display ($this->url, 'html', true); ?>" rel="self" type="application/rss+xml" /> <?php foreach ($this->entries as $item) { ?> diff --git a/app/views/importExport/index.phtml b/app/views/importExport/index.phtml new file mode 100644 index 000000000..309058959 --- /dev/null +++ b/app/views/importExport/index.phtml @@ -0,0 +1,52 @@ +<?php $this->partial ('aside_feed'); ?> + +<div class="post "> + <a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a> + + <form method="post" action="<?php echo _url('importExport', 'import'); ?>" enctype="multipart/form-data"> + <legend><?php echo Minz_Translate::t ('import'); ?></legend> + <div class="form-group"> + <label class="group-name" for="file"><?php echo Minz_Translate::t ('file_to_import'); ?></label> + <div class="group-controls"> + <input type="file" name="file" id="file" /> + </div> + </div> + + <div class="form-group form-actions"> + <div class="group-controls"> + <button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('import'); ?></button> + </div> + </div> + </form> + + <?php if (count($this->feeds) > 0) { ?> + <form method="post" action="<?php echo _url('importExport', 'export'); ?>"> + <legend><?php echo Minz_Translate::t ('export'); ?></legend> + <div class="form-group"> + <div class="group-controls"> + <label class="checkbox" for="export_opml"> + <input type="checkbox" name="export_opml" id="export_opml" value="1" checked="checked" /> + <?php echo Minz_Translate::t ('export_opml'); ?> + </label> + + <label class="checkbox" for="export_starred"> + <input type="checkbox" name="export_starred" id="export_starred" value="1" checked="checked" /> + <?php echo Minz_Translate::t ('export_starred'); ?> + </label> + + <select name="export_feeds[]" size="<?php echo min(10, count($this->feeds)); ?>" multiple="multiple"> + <?php foreach ($this->feeds as $feed) { ?> + <option value="<?php echo $feed->id(); ?>"><?php echo $feed->name(); ?></option> + <?php } ?> + </select> + </div> + </div> + + <div class="form-group form-actions"> + <div class="group-controls"> + <button type="submit" class="btn btn-important"><?php echo Minz_Translate::t ('export'); ?></button> + </div> + </div> + </form> + <?php } ?> +</div> diff --git a/app/views/index/formLogin.phtml b/app/views/index/formLogin.phtml index e4560c1a0..cc925ea59 100644 --- a/app/views/index/formLogin.phtml +++ b/app/views/index/formLogin.phtml @@ -1,34 +1,32 @@ <div class="prompt"> -<?php -if (Minz_Configuration::canLogIn()) { - ?><h1><?php echo Minz_Translate::t('login'); ?></h1><?php - switch (Minz_Configuration::authType()) { + <h1><?php echo Minz_Translate::t('login'); ?></h1><?php - case 'form': - ?><form id="loginForm" method="post" action="<?php echo _url('index', 'formLogin'); ?>"> - <p> - <label for="username"><?php echo Minz_Translate::t('username'); ?></label> - <input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" /> - </p><p> - <label for="passwordPlain"><?php echo Minz_Translate::t('password'); ?></label> - <input type="password" id="passwordPlain" required="required" /> - <input type="hidden" id="challenge" name="challenge" /><br /> - <noscript><strong><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></strong></noscript> - </p><p> - <button id="loginButton" type="submit" class="btn btn-important"><?php echo Minz_Translate::t('login'); ?></button> - </p> - </form><?php - break; + switch (Minz_Configuration::authType()) { + case 'form': + ?><form id="loginForm" method="post" action="<?php echo _url('index', 'formLogin'); ?>"> + <div> + <label for="username"><?php echo Minz_Translate::t('username'); ?></label> + <input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" /> + </div> + <div> + <label for="passwordPlain"><?php echo Minz_Translate::t('password'); ?></label> + <input type="password" id="passwordPlain" required="required" /> + <input type="hidden" id="challenge" name="challenge" /><br /> + <noscript><strong><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></strong></noscript> + </div> + <div> + <button id="loginButton" type="submit" class="btn btn-important"><?php echo Minz_Translate::t('login'); ?></button> + </div> + </form><?php + break; - case 'persona': - ?><p><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="#"><?php echo Minz_Translate::t('login'); ?></a></p><?php - break; - } -} else { - ?><h1>FreshRSS</h1> - <p><?php echo Minz_Translate::t('forbidden_access', Minz_Configuration::authType()); ?></p><?php -} -?> + case 'persona': + ?><p> + <?php echo FreshRSS_Themes::icon('login'); ?> + <a class="signin" href="#"><?php echo Minz_Translate::t('login_with_persona'); ?></a> + </p><?php + break; + } ?> -<p><a href="<?php echo _url('index', 'about'); ?>"><?php echo Minz_Translate::t('about_freshrss'); ?></a></p> + <p><a href="<?php echo _url('index', 'about'); ?>"><?php echo Minz_Translate::t('about_freshrss'); ?></a></p> </div> diff --git a/app/views/index/index.phtml b/app/views/index/index.phtml index 4545a33e4..1ff36ca8e 100644 --- a/app/views/index/index.phtml +++ b/app/views/index/index.phtml @@ -5,26 +5,21 @@ $output = Minz_Request::param ('output', 'normal'); if ($this->loginOk || Minz_Configuration::allowAnonymous()) { if ($output === 'normal') { $this->renderHelper ('view/normal_view'); - } elseif ($output === 'rss') { - $this->renderHelper ('view/rss_view'); } elseif ($output === 'reader') { $this->renderHelper ('view/reader_view'); } elseif ($output === 'global') { $this->renderHelper ('view/global_view'); + } elseif ($output === 'rss') { + $this->renderHelper ('view/rss_view'); } else { Minz_Request::_param ('output', 'normal'); $output = 'normal'; $this->renderHelper ('view/normal_view'); } } elseif ($output === 'rss') { - $token = $this->conf->token; - $token_param = Minz_Request::param ('token', ''); - $token_is_ok = ($token != '' && $token == $token_param); - if ($token_is_ok) { - $this->renderHelper ('view/rss_view'); - } else { - Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'), true); - } + // token has already been checked in the controller so we can show the view + $this->renderHelper ('view/rss_view'); } else { - Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'), true); + // Normally, it should not happen, but log it anyway + Minz_Log::record ('Something is wrong in ' . __FILE__ . ' line ' . __LINE__, Minz_Log::ERROR); } diff --git a/app/views/javascript/actualize.phtml b/app/views/javascript/actualize.phtml index 1f6072c29..6a92e5642 100644 --- a/app/views/javascript/actualize.phtml +++ b/app/views/javascript/actualize.phtml @@ -1,14 +1,17 @@ "use strict"; -var feeds = []; -<?php foreach ($this->feeds as $feed) { ?> -feeds.push("<?php echo Minz_Url::display (array ('c' => 'feed', 'a' => 'actualize', 'params' => array ('id' => $feed->id (), 'ajax' => '1')), 'php'); ?>"); -<?php } ?> +var feeds = [<?php + foreach ($this->feeds as $feed) { + echo "'", Minz_Url::display(array('c' => 'feed', 'a' => 'actualize', 'params' => array('id' => $feed->id(), 'ajax' => '1')), 'php'), "',\n"; + } + ?>], + feed_processed = 0, + feed_count = feeds.length; function initProgressBar(init) { if (init) { - $("body").after("\<div id=\"actualizeProgress\" class=\"actualizeProgress\">\ - <?php echo Minz_Translate::t ('refresh'); ?> <span class=\"progress\">0 / " + feeds.length + "</span><br />\ - <progress id=\"actualizeProgressBar\" value=\"0\" max=\"" + feeds.length + "\"></progress>\ + $("body").after("\<div id=\"actualizeProgress\" class=\"notification good\">\ + <?php echo Minz_Translate::t ('refresh'); ?> <span class=\"progress\">0 / " + feed_count + "</span><br />\ + <progress id=\"actualizeProgressBar\" value=\"0\" max=\"" + feed_count + "\"></progress>\ </div>"); } else { window.location.reload(); @@ -16,27 +19,37 @@ function initProgressBar(init) { } function updateProgressBar(i) { $("#actualizeProgressBar").val(i); - $("#actualizeProgress .progress").html(i + " / " + feeds.length); + $("#actualizeProgress .progress").html(i + " / " + feed_count); } function updateFeeds() { - if (feeds.length === 0) { + if (feed_count === 0) { + openNotification("<?php echo Minz_Translate::t ('no_feed_to_refresh'); ?>", "good"); return; } initProgressBar(true); - var i = 0; - for (var f in feeds) { - $.ajax({ - type: 'POST', - url: feeds[f], - }).done(function (data) { - i++; - updateProgressBar(i); + for (var i = 0; i < 10; i++) { + updateFeed(); + } +} - if (i === feeds.length) { - initProgressBar(false); - } - }); +function updateFeed() { + var feed = feeds.pop(); + if (feed == undefined) { + return; } + $.ajax({ + type: 'POST', + url: feed, + }).complete(function (data) { + feed_processed++; + updateProgressBar(feed_processed); + + if (feed_processed === feed_count) { + initProgressBar(false); + } else { + updateFeed(); + } + }); } |
