diff options
| author | 2014-05-13 21:03:32 +0200 | |
|---|---|---|
| committer | 2014-05-13 21:03:32 +0200 | |
| commit | a86a51cca69b7256d3a8dcf1c68447b6d5abe917 (patch) | |
| tree | b461e144d54656fb3c2c95aee96d6303cf6fa12c /app | |
| parent | 88fa624a02d0346c2eb9173b1e9a02fc38b56d50 (diff) | |
| parent | e763cd407a57e4829c2b6ffbddb164e5676670d7 (diff) | |
Merge branch 'dev' into 320-template
Diffstat (limited to 'app')
39 files changed, 1583 insertions, 700 deletions
diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index ad8bc546a..df5212041 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -44,7 +44,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { 'name' => $cat->name (), ); - if ($catDAO->searchByName ($newCat) == false) { + if ($catDAO->searchByName ($newCat) == null) { $catDAO->addCategory ($values); } } @@ -62,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') . ' · '); } @@ -141,20 +140,6 @@ 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; @@ -187,22 +172,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 ( @@ -211,75 +204,29 @@ 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 () { 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 c718fcd5c..fce008399 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -22,14 +22,32 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } public function addAction () { - @set_time_limit(300); + $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 (); + + if (Minz_Request::isPost()) { + @set_time_limit(300); - if (Minz_Request::isPost ()) { - $this->catDAO = new FreshRSS_CategoryDAO (); - $this->catDAO->checkDefault (); - $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 (); @@ -52,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 (), @@ -128,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) { @@ -136,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); } @@ -146,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 () { @@ -319,80 +368,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } } - public function massiveImportAction () { - @set_time_limit(300); - - $this->catDAO = new FreshRSS_CategoryDAO (); - $this->catDAO->checkDefault (); - - $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'); @@ -436,16 +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 (), - ); - $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 38f4c0e7c..3445c0bd4 100755 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -25,16 +25,14 @@ class FreshRSS_index_Controller extends Minz_ActionController { } } - // construction of RSS url of this feed $params = Minz_Request::params (); - $params['output'] = 'rss'; if (isset ($params['search'])) { $params['search'] = urlencode ($params['search']); } if (!Minz_Configuration::allowAnonymous()) { $params['token'] = $token; } - $this->view->rss_url = array ( + $this->view->url = array ( 'c' => 'index', 'a' => 'index', 'params' => $params @@ -84,20 +82,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': @@ -111,8 +111,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; } } @@ -125,14 +125,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) { @@ -351,6 +351,32 @@ 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, diff --git a/app/Controllers/usersController.php b/app/Controllers/usersController.php index bb4f34c5e..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)); } @@ -57,13 +69,19 @@ class FreshRSS_users_Controller extends Minz_ActionController { $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() || - $anon_refresh != Minz_Configuration::allowAnonymousRefresh()) { + $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/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index 5355228a5..6a9b839b9 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -18,6 +18,19 @@ 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=? WHERE id=?'; $stm = $this->bd->prepare ($sql); @@ -64,7 +77,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if (isset ($cat[0])) { return $cat[0]; } else { - return false; + return null; } } public function searchByName ($name) { @@ -80,7 +93,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if (isset ($cat[0])) { return $cat[0]; } else { - return false; + return null; } } @@ -120,7 +133,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { public function checkDefault () { $def_cat = $this->searchById (1); - if ($def_cat === false) { + if ($def_cat == null) { $cat = new FreshRSS_Category (Minz_Translate::t ('default_category')); $cat->_id (1); diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php index 2b719c370..8d3e69a1c 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( @@ -37,6 +39,7 @@ class FreshRSS_Configuration { 'collapse_entry' => 'c', 'load_more' => 'm', 'auto_share' => 's', + 'focus_search' => 'a', ), 'topline_read' => true, 'topline_favorite' => true, @@ -48,16 +51,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( @@ -65,8 +59,10 @@ class FreshRSS_Configuration { 'fr' => 'Français', ); - public function __construct ($user) { - $this->filename = DATA_PATH . '/' . $user . '_user.php'; + private $shares; + + public function __construct($user) { + $this->filename = DATA_PATH . DIRECTORY_SEPARATOR . $user . '_user.php'; $data = @include($this->filename); if (!is_array($data)) { @@ -80,10 +76,20 @@ class FreshRSS_Configuration { } } $this->data['user'] = $user; + + $this->shares = DATA_PATH . DIRECTORY_SEPARATOR . 'shares.php'; + + $shares = @include($this->shares); + if (!is_array($shares)) { + throw new Minz_PermissionDeniedException($this->shares); + } + + $this->data['shares'] = $shares; } public function save() { @rename($this->filename, $this->filename . '.bak.php'); + unset($this->data['shares']); // Remove shares because it is not intended to be stored in user configuration if (file_put_contents($this->filename, "<?php\n return " . var_export($this->data, true) . ';', LOCK_EX) === false) { throw new Minz_PermissionDeniedException($this->filename); } @@ -104,16 +110,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; } @@ -136,7 +132,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'; @@ -147,6 +143,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'; } @@ -168,6 +167,9 @@ class FreshRSS_Configuration { public function _passwordHash ($value) { $this->data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; } + public function _apiPasswordHash ($value) { + $this->data['apiPasswordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : ''; + } public function _mail_login ($value) { $value = filter_var($value, FILTER_VALIDATE_EMAIL); if ($value) { @@ -187,24 +189,33 @@ 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; + } + + // Add a default name + if (empty($value['name'])) { + $value['name'] = $value['type']; } - $this->data['sharing'][$key] = $value; + $this->data['sharing'][] = $value; } } public function _theme($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..73893a88d 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,11 +465,15 @@ 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 = ''; @@ -366,14 +525,22 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { } } + 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 +550,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 73f9c32fb..13d3dfe88 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -210,8 +210,8 @@ 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())); @@ -254,11 +254,12 @@ 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="" />'; + } } } diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 7ebe68d2b..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) { @@ -170,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) { @@ -186,7 +216,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { if (isset ($feed)) { return $feed; } else { - return false; + return null; } } @@ -198,6 +228,22 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return self::daoToFeed ($stm->fetchAll (PDO::FETCH_ASSOC)); } + 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` ' @@ -229,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); @@ -238,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 (' @@ -250,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..17b95bb9e 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' => '🔍', diff --git a/app/actualize_script.php b/app/actualize_script.php index 8d81e0189..4c306b8da 100755 --- a/app/actualize_script.php +++ b/app/actualize_script.php @@ -28,7 +28,6 @@ foreach ($users as $myUser) { $_SERVER['HTTP_HOST'] = ''; $freshRSS = new FreshRSS(); - $freshRSS->_useOb(false); Minz_Configuration::_authType('none'); diff --git a/app/i18n/en.php b/app/i18n/en.php index 790e853f5..21d37f475 100644 --- a/app/i18n/en.php +++ b/app/i18n/en.php @@ -21,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', @@ -50,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', @@ -78,9 +80,8 @@ 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 "Ctrl" modifier, navigation shortcuts apply on categories.', + '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', @@ -89,7 +90,7 @@ return array ( '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', @@ -136,10 +137,15 @@ return array ( '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', @@ -166,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.', @@ -174,9 +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', @@ -202,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', @@ -209,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', @@ -230,6 +243,7 @@ return array ( 'more_information' => 'More information', 'activate_sharing' => 'Activate sharing', 'shaarli' => 'Shaarli', + 'blogotext' => 'Blogotext', 'wallabag' => 'wallabag', 'diaspora' => 'Diaspora*', 'twitter' => 'Twitter', diff --git a/app/i18n/fr.php b/app/i18n/fr.php index 572bc1628..f42115fcd 100644 --- a/app/i18n/fr.php +++ b/app/i18n/fr.php @@ -21,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', @@ -51,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', @@ -78,9 +80,8 @@ 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 "Ctrl", les raccourcis de navigation s’appliquent aux catégories.', + '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', @@ -89,7 +90,7 @@ return array ( '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', @@ -136,10 +137,15 @@ return array ( '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', @@ -166,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é.', @@ -173,10 +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', @@ -202,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', @@ -209,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', @@ -230,6 +243,7 @@ return array ( 'more_information' => 'Plus d’informations', 'activate_sharing' => 'Activer le partage', 'shaarli' => 'Shaarli', + 'blogotext' => 'Blogotext', 'wallabag' => 'wallabag', 'diaspora' => 'Diaspora*', 'twitter' => 'Twitter', 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 1501df3c3..c22288adb 100644 --- a/app/layout/layout.phtml +++ b/app/layout/layout.phtml @@ -3,42 +3,50 @@ <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 $msg = ''; $status = 'closed'; - if (isset ($this->notification)) { + if (isset($this->notification)) { $msg = $this->notification['content']; $status = $this->notification['type']; diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml index 98064a6f7..9990448ba 100644 --- a/app/layout/nav_menu.phtml +++ b/app/layout/nav_menu.phtml @@ -6,11 +6,81 @@ <a class="btn toggle_aside" href="#aside_flux"><?php echo FreshRSS_Themes::icon('category'); ?></a> <?php } ?> - <?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 } ?> - <?php if ($this->loginOk) { ?> + <?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'); @@ -61,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', ''); @@ -82,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; @@ -95,17 +169,6 @@ </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> @@ -113,7 +176,7 @@ <li class="dropdown-close"><a href="#close">❌</a></li> <?php - $url_output = $url; + $url_output = $this->url; if ($actual_view !== 'normal') { ?> <li class="item"> <?php $url_output['params']['output'] = 'normal'; ?> @@ -136,71 +199,6 @@ </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> </div> @@ -225,4 +223,30 @@ <?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 $url_output['params']['output'] = 'rss'; ?> + <a class="btn view_rss" target="_blank" href="<?php echo Minz_Url::display ($url_output); ?>" title="<?php echo Minz_Translate::t ('rss_view'); ?>"> + <?php echo FreshRSS_Themes::icon('rss'); ?> + </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/views/configure/categorize.phtml b/app/views/configure/categorize.phtml index a564e8cdd..c0171d2dc 100644 --- a/app/views/configure/categorize.phtml +++ b/app/views/configure/categorize.phtml @@ -17,11 +17,11 @@ <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> + <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 } ?> (<?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..5a94376ad 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,8 @@ </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> - - <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="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/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..ddb404ef5 100644 --- a/app/views/configure/sharing.phtml +++ b/app/views/configure/sharing.phtml @@ -3,54 +3,41 @@ <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"><?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"><a href="#" class="share remove"><?php echo FreshRSS_Themes::icon('close'); ?></a> + <input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" /> + <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" /> + <?php echo FreshRSS_Themes::icon('help'); ?> <a target="_blank" href="##help##"><?php echo Minz_Translate::t ('more_information'); ?></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"> + <a href='#' class='share remove'><?php echo FreshRSS_Themes::icon('close'); ?></a> + <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'):?> + <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" /> + <?php echo FreshRSS_Themes::icon('help'); ?> <a target="_blank" href="<?php echo $share['help']?>"><?php echo Minz_Translate::t ('more_information'); ?></a> + <?php endif;?> + </div> </div> - </div> + <?php endforeach;?> - <div class="form-group"> - <label class="group-name"><?php echo Minz_Translate::t ('activate_sharing'); ?></label> + <div class="form-group form-actions"> <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 } ?> + <a href='#' class='share add'><?php echo FreshRSS_Themes::icon('add'); ?></a> + <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> </div> </div> diff --git a/app/views/configure/shortcut.phtml b/app/views/configure/shortcut.phtml index 748a65d17..bfb13f003 100644 --- a/app/views/configure/shortcut.phtml +++ b/app/views/configure/shortcut.phtml @@ -12,7 +12,7 @@ <?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> @@ -96,6 +96,13 @@ </div> </div> + <div class="form-group"> + <label class="group-name" for="focus_search_shortcut"><?php echo Minz_Translate::t ('focus_search'); ?></label> + <div class="group-controls"> + <input type="text" id="focus_search_shortcut" name="shortcuts[focus_search]" list="keys" value="<?php echo $s['focus_search']; ?>" /> + </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> diff --git a/app/views/configure/users.phtml b/app/views/configure/users.phtml index 1305feac9..fdc94cd18 100644 --- a/app/views/configure/users.phtml +++ b/app/views/configure/users.phtml @@ -20,11 +20,22 @@ <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" autocomplete="off" pattern=".{7,}" /> + <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> <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"> + <input type="password" id="apiPasswordPlain" name="apiPasswordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/> + <noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript> + </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; ?> @@ -52,7 +63,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> @@ -80,18 +91,39 @@ </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> @@ -147,6 +179,7 @@ <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" autocomplete="off" pattern=".{7,}" /> + <a class="btn toggle-password"/><?php echo FreshRSS_Themes::icon('key'); ?></a> <noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript> </div> </div> diff --git a/app/views/feed/add.phtml b/app/views/feed/add.phtml new file mode 100644 index 000000000..56ac5f8dd --- /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"> + <label> + <?php echo $this->feed->website(); ?> + <a target="_blank" href="<?php echo $this->feed->website(); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a> + </label> + </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"> + <input type="text" name="url_rss" id="url" class="extend" value="<?php echo $this->feed->url(); ?>" /> + <a target="_blank" href="<?php echo $this->feed->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->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 3d7c8a98f..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={', @@ -23,7 +25,8 @@ echo ',shortcuts={', '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') { diff --git a/app/views/helpers/view/normal_view.phtml b/app/views/helpers/view/normal_view.phtml index ae93b627c..b5cdd3039 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,9 +20,7 @@ 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; @@ -39,7 +28,7 @@ if (!empty($this->entries)) { <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)) { @@ -99,7 +88,7 @@ if (!empty($this->entries)) { <div class="flux_content"> <div class="content"> - <h1 class="title"><?php echo $item->title (); ?></h1> + <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 +135,13 @@ if (!empty($this->entries)) { <ul class="dropdown-menu"> <li class="dropdown-close"><a href="#close">❌</a></li> - <?php if ($shaarli) { ?> - <li class="item share"> - <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 share"> - <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 share"> - <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 share"> - <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 share"> - <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 share"> - <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 share"> - <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 share"> - <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/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/index.phtml b/app/views/index/index.phtml index 78271291e..1ff36ca8e 100644 --- a/app/views/index/index.phtml +++ b/app/views/index/index.phtml @@ -3,12 +3,14 @@ $output = Minz_Request::param ('output', 'normal'); if ($this->loginOk || Minz_Configuration::allowAnonymous()) { - if ($output === 'rss') { - $this->renderHelper ('view/rss_view'); + if ($output === 'normal') { + $this->renderHelper ('view/normal_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'; |
