Jelajahi Sumber

Merge branch 'dev'

Marien Fressinaud 12 tahun lalu
induk
melakukan
eb5f05304c
71 mengubah file dengan 3669 tambahan dan 626 penghapusan
  1. 0 1
      .gitignore
  2. 0 92
      README
  3. 101 0
      README.md
  4. 1 0
      actualize_script.php
  5. 2 0
      app/App_FrontController.php
  6. 0 46
      app/controllers/apiController.php
  7. 38 10
      app/controllers/configureController.php
  8. 17 69
      app/controllers/entryController.php
  9. 3 4
      app/controllers/feedController.php
  10. 34 1
      app/controllers/indexController.php
  11. 15 5
      app/i18n/en.php
  12. 15 5
      app/i18n/fr.php
  13. 1 1
      app/layout/aside_feed.phtml
  14. 1 1
      app/layout/aside_flux.phtml
  15. 20 14
      app/layout/header.phtml
  16. 5 0
      app/layout/nav_entries.phtml
  17. 48 19
      app/layout/nav_menu.phtml
  18. 3 3
      app/models/Category.php
  19. 51 1
      app/models/Entry.php
  20. 6 0
      app/models/Exception/FeedException.php
  21. 34 13
      app/models/Feed.php
  22. 47 0
      app/models/Log.php
  23. 48 6
      app/models/RSSConfiguration.php
  24. 0 3
      app/views/api/getNbNotRead.phtml
  25. 0 3
      app/views/api/getPublicFeed.phtml
  26. 33 3
      app/views/configure/display.phtml
  27. 18 0
      app/views/configure/feed.phtml
  28. 0 65
      app/views/entry/note.phtml
  29. 37 0
      app/views/helpers/global_view.phtml
  30. 47 0
      app/views/helpers/logs_pagination.phtml
  31. 131 0
      app/views/helpers/normal_view.phtml
  32. 1 1
      app/views/helpers/pagination.phtml
  33. 45 0
      app/views/helpers/reader_view.phtml
  34. 0 0
      app/views/helpers/rss_view.phtml
  35. 8 115
      app/views/index/index.phtml
  36. 21 0
      app/views/index/logs.phtml
  37. 104 53
      app/views/javascript/main.phtml
  38. 0 50
      build.sh
  39. 6 3
      lib/lib_rss.php
  40. 8 0
      lib/lib_text.php
  41. 42 0
      lib/minz/ActionController.php
  42. 116 0
      lib/minz/Cache.php
  43. 240 0
      lib/minz/Configuration.php
  44. 152 0
      lib/minz/Dispatcher.php
  45. 94 0
      lib/minz/Error.php
  46. 123 0
      lib/minz/FrontController.php
  47. 22 0
      lib/minz/Helper.php
  48. 92 0
      lib/minz/Log.php
  49. 12 0
      lib/minz/Model.php
  50. 196 0
      lib/minz/Paginator.php
  51. 196 0
      lib/minz/Request.php
  52. 60 0
      lib/minz/Response.php
  53. 209 0
      lib/minz/Router.php
  54. 78 0
      lib/minz/Session.php
  55. 71 0
      lib/minz/Translate.php
  56. 130 0
      lib/minz/Url.php
  57. 232 0
      lib/minz/View.php
  58. 122 0
      lib/minz/dao/Model_array.php
  59. 39 0
      lib/minz/dao/Model_pdo.php
  60. 77 0
      lib/minz/dao/Model_txt.php
  61. 94 0
      lib/minz/exceptions/MinzException.php
  62. 10 4
      public/install.php
  63. 31 0
      public/scripts/endless_mode.js
  64. 14 0
      public/scripts/jquery.lazyload.min.js
  65. 175 29
      public/theme/freshrss.css
  66. 31 6
      public/theme/global.css
  67. TEMPAT SAMPAH
      public/theme/icons/next.png
  68. 31 0
      public/theme/icons/next.svg
  69. TEMPAT SAMPAH
      public/theme/icons/previous.png
  70. 31 0
      public/theme/icons/previous.svg
  71. TEMPAT SAMPAH
      public/theme/loader.gif

+ 0 - 1
.gitignore

@@ -1 +0,0 @@
-lib/minz

+ 0 - 92
README

@@ -1,92 +0,0 @@
-Un simple agrégateur de flux rss relativement léger et rapide par rapport aux
-mastodontes que sont RSSLounge et TinyTinyRSS.
-
-@name FreshRSS
-@url http://marienfressinaud.github.io/FreshRSS/
-@demo http://marienfressinaud.fr/projets/freshrss/
-@author Marien Fressinaud <dev@marienfressinaud.fr>
-@version 0.3.0
-@date 2013-05-05
-@license AGPL3
-
-DISCLAIMER
-==========
-Cette application a été développée pour s'adapter à mes besoins personnels.
-Je ne garantis en aucun cas la sécurité de celle-ci, ni son bon fonctionnement
-sur un autre serveur que le mien. Je m'engage néanmoins à répondre dans la
-mesure du possible aux demandes d'évolution si celles-ci me semblent justifiées.
-Privilégiez pour cela des demandes sur GitHub
-(https://github.com/marienfressinaud/FreshRSS/issues) ou par mail
-
-PRE-REQUIS
-==========
-- Serveur Apache (non testé sur aucun autre)
-- PHP 5.3 (il me faudrait des retours sur d'autres versions antérieures)
-- libxml pour PHP
-- cURL
-- PDO et MySQL
-
-INSTALLATION
-============
-	1. Récupérez l'application FreshRSS via la commande git ou en
-	   téléchargeant l'archive
-	2. Exécutez le script ./build.sh ou en récupérant à la main la librairie
-	   Minz (https://github.com/marienfressinaud/MINZ) et en copiant la lib
-	   dans ./lib/minz
-	3. Déplacez l'application où vous voulez sur votre serveur (attention,
-	   la partie accessible se trouve dans le répertoire `./public`)
-	4. Accédez à FreshRSS à travers votre navigateur web et suivez les
-	   instructions
-	5. Tout devrait fonctionner :) En cas de problème, n'hésitez pas à me
-	   contacter.
-
-SÉCURITÉ ET CONSEILS
-====================
-	1. Si possible, faites pointer un sous-domaine sur le répertoire
-	   `./public`
-	2. Le fichier de log peut être utile à lire si vous avez des soucis
-	3. Le fichier `./public/index.php` défini les chemins d'accès aux
-	   répertoires clés de l'application. Si vous les bougez, tout se passe
-	   ici.
-	4. Vous pouvez ajouter une tâche CRON sur le script d'actualisation des
-	   flux. Il s'agit d'un script PHP à exécuter avec la commande `php`.
-	   Par exemple, pour exécuter le script toutes les heures :
-	   0 * * * * php /chemin/vers/freshrss/actualize_script.php >/dev/null 2>&1
-	   Veuillez cependant vérifier que le fichier ./public/data/Configuration.array.php
-	   soit accessible en lecture / écriture par l'exécuteur du script
-
-CHANGELOG
-=========
-2013-05-05 changes with FreshRSS 0.3.0
-	*) Fallback pour les icônes SVG (utilisation de PNG à la place)
-	*) Fallback pour les propriétés CSS3 (utilisation de préfixes)
-	*) Affichage des tags associés aux articles
-	*) Internationalisation de l'application (gestion des langues anglaise
-	   et française)
-	*) Gestion des flux protégés par authentification HTTP
-	*) Mise en cache des favicons
-	*) Création d'un logo *temporaire*
-	*) Affichage des vidéos dans les articles
-	*) Gestion de la recherche et filtre par tags pleinement fonctionnels
-	*) Création d'un vrai script CRON permettant de mettre tous les flux à
-	   jour
-	*) Correction bugs divers
-
-2013-04-17 changes with FreshRSS 0.2.0
-	*) Création d'un installateur
-	*) Actualisation des flux en Ajax
-	*) Partage par mail et Shaarli ajouté
-	*) Export par flux RSS
-	*) Possibilité de vider une catégorie
-	*) Possibilité de sélectionner les catégories en vue mobile
-	*) Les flux peuvent être sortis du flux principal (système de priorité)
-	*) Amélioration ajout / import / export des flux
-	*) Amélioration actualisation (meilleure gestion des erreurs)
-	*) Améliorations CSS
-	*) Changements dans la base de données
-	*) Màj de la librairie SimplePie
-	*) Flux sans auteurs gérés normalement
-	*) Correction bugs divers
-
-2013-04-08 changes with FreshRSS 0.1.0
-	*) "Première" version

+ 101 - 0
README.md

@@ -0,0 +1,101 @@
+# FreshRSS
+FreshRSS est un agrégateur de flux RSS à auto-héberger à l'image de [RSSLounge](http://rsslounge.aditu.de/), [TinyTinyRSS](http://tt-rss.org/redmine/projects/tt-rss/wiki) ou [Leed](http://projet.idleman.fr/leed/). Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable. L'objectif étant d'offrir une alternative sérieuse à Google Reader.
+
+* Site officiel : http://marienfressinaud.github.io/FreshRSS/
+* Démo : http://marienfressinaud.fr/projets/freshrss/
+* Développeur : Marien Fressinaud <dev@marienfressinaud.fr>
+* Version actuelle : 0.4.0
+* Date de publication 2013-07-02
+* License AGPL3
+
+![Capture d'écran de FreshRSS](http://marienfressinaud.fr/data/files/wiki_freshrss/freshrss_normal_view.png)
+
+# Disclaimer
+Cette application a été développée pour s'adapter à mes besoins personnels.
+Je ne garantis en aucun cas la sécurité de celle-ci, ni son bon fonctionnement
+sur un autre serveur que le mien. Je m'engage néanmoins à répondre dans la
+mesure du possible aux demandes d'évolution si celles-ci me semblent justifiées.
+Privilégiez pour cela des demandes sur GitHub
+(https://github.com/marienfressinaud/FreshRSS/issues) ou par mail (dev@marienfressinaud.fr)
+
+# Pré-requis
+* Serveur Apache ou Nginx (non testé sur les autres)
+* PHP 5.3 (il me faudrait des retours sur d'autres versions antérieures)
+* libxml pour PHP
+* cURL
+* PDO et MySQL
+
+# Installation
+1. Récupérez l'application FreshRSS via la commande git ou [en téléchargeant l'archive](https://github.com/marienfressinaud/FreshRSS/archive/master.zip)
+2. Déplacez l'application où vous voulez sur votre serveur (attention, la partie accessible se trouve dans le répertoire `./public`)
+3. Accédez à FreshRSS à travers votre navigateur web et suivez les instructions d'installation
+4. Tout devrait fonctionner :) En cas de problème, n'hésitez pas à me contacter.
+
+# Sécurité et conseils
+1. Si possible, faites pointer un sous-domaine sur le répertoire `./public`
+2. Le fichier de log peut être utile à lire si vous avez des soucis
+3. Le fichier `./public/index.php` défini les chemins d'accès aux répertoires clés de l'application. Si vous les bougez, tout se passe ici.
+4. Vous pouvez ajouter une tâche CRON sur le script d'actualisation des flux. Il s'agit d'un script PHP à exécuter avec la commande `php`. Par exemple, pour exécuter le script toutes les heures :
+```
+0 * * * * php /chemin/vers/freshrss/actualize_script.php >/dev/null 2>&1
+```
+
+# Changelog
+## 2013-07-02 changes with FreshRSS 0.4.0
+
+* Correction bug et ajout notification lors de la phase d'installation
+* Affichage d'erreur si fichier OPML invalide
+* Les tags sont maintenant cliquables pour filtrer dessus
+* Amélioration vue mobile (boutons plus gros et ajout d'une barre de navigation)
+* Possibilité d'ajouter directement un flux dans une catégorie dès son ajout
+* Affichage des flux en erreur (injoignable par exemple) en rouge pour les différencier
+* Possiblité de changer les noms des flux
+* Ajout d'une option (désactivable donc) pour charger les images en lazyload permettant de ne pas charger toutes les images d'un coup
+* Le framework Minz est maintenant directement inclus dans l'archive (plus besoin de passer par ./build.sh)
+* Amélioration des performances pour la récupération des flux tronqués
+* Possibilité d'importer des flux sans catégorie lors de l'import OPML
+* Suppression de "l'API" (qui était de toutes façons très basique) et de la fonctionnalité de "notes"
+* Amélioration de la recherche (garde en mémoire si l'on a sélectionné une catégorie) par exemple
+* Modification apparence des balises hr et pre
+* Meilleure vérification des champs de formulaire
+* Remise en place du mode "endless" (permettant de simplement charger les articles qui suivent plutôt que de charger une nouvelle page)
+* Ajout d'une page de visualisation des logs
+* Ajout d'une option pour optimiser la BDD (diminue sa taille)
+* Ajout des vues lecture et globale (assez basique)
+* Les vidéos Youtube ne débordent plus du cadre sur les petits écrans
+* Ajout d'une option pour marquer les articles comme lus lors du défilement (et suppression de celle au chargement de la page)
+
+## 2013-05-05 changes with FreshRSS 0.3.0
+
+* Fallback pour les icônes SVG (utilisation de PNG à la place)
+* Fallback pour les propriétés CSS3 (utilisation de préfixes)
+* Affichage des tags associés aux articles
+* Internationalisation de l'application (gestion des langues anglaise et française)
+* Gestion des flux protégés par authentification HTTP
+* Mise en cache des favicons
+* Création d'un logo *temporaire*
+* Affichage des vidéos dans les articles
+* Gestion de la recherche et filtre par tags pleinement fonctionnels
+* Création d'un vrai script CRON permettant de mettre tous les flux à jour
+* Correction bugs divers
+
+## 2013-04-17 changes with FreshRSS 0.2.0
+
+* Création d'un installateur
+* Actualisation des flux en Ajax
+* Partage par mail et Shaarli ajouté
+* Export par flux RSS
+* Possibilité de vider une catégorie
+* Possibilité de sélectionner les catégories en vue mobile
+* Les flux peuvent être sortis du flux principal (système de priorité)
+* Amélioration ajout / import / export des flux
+* Amélioration actualisation (meilleure gestion des erreurs)
+* Améliorations CSS
+* Changements dans la base de données
+* Màj de la librairie SimplePie
+* Flux sans auteurs gérés normalement
+* Correction bugs divers
+
+## 2013-04-08 changes with FreshRSS 0.1.0
+
+* "Première" version

+ 1 - 0
actualize_script.php

@@ -24,4 +24,5 @@ require (APP_PATH . '/App_FrontController.php');
 
 $front_controller = new App_FrontController ();
 $front_controller->init ();
+Session::_param('mail', true); // permet de se passer de la phase de connexion
 $front_controller->run ();

+ 2 - 0
app/App_FrontController.php

@@ -36,6 +36,7 @@ class App_FrontController extends FrontController {
 		include (APP_PATH . '/models/Entry.php');
 		include (APP_PATH . '/models/EntriesGetter.php');
 		include (APP_PATH . '/models/RSSPaginator.php');
+		include (APP_PATH . '/models/Log.php');
 	}
 
 	private function loadParamsView () {
@@ -56,6 +57,7 @@ class App_FrontController extends FrontController {
 			View::appendScript ('https://login.persona.org/include.js');
 		}
 		View::appendScript (Url::display ('/scripts/jquery.js'));
+		View::appendScript (Url::display ('/scripts/jquery.lazyload.min.js'));
 		View::appendScript (Url::display ('/scripts/notification.js'));
 	}
 

+ 0 - 46
app/controllers/apiController.php

@@ -1,46 +0,0 @@
-<?php
-  
-class apiController extends ActionController {
-	public function firstAction() {
-		header('Content-type: application/json');
-
-		$this->view->_useLayout (false);
-	}
-
-	public function getPublicFeedAction () {
-		$entryDAO = new EntryDAO ();
-		$entryDAO->_nbItemsPerPage (-1);
-
-		$entries_tmp = $entryDAO->listPublic ('low_to_high');
-
-		$entries = array ();
-		foreach ($entries_tmp as $e) {
-			$author = $e->author ();
-
-			$notes = $e->notes ();
-			if ($notes == '') {
-				$feed = $e->feed (true);
-				if($author != '') {
-					$notes = Translate::t ('article_published_on_author', $feed->website (), $feed->name (), $author);
-				} else {
-					$notes = Translate::t ('article_published_on', $feed->website (), $feed->name ());
-				}
-			}
-
-			$id = $e->id ();
-			$entries[$id] = array ();
-			$entries[$id]['title'] = $e->title ();
-			$entries[$id]['content'] = $notes;
-			$entries[$id]['date'] = $e->date (true);
-			$entries[$id]['lastUpdate'] = $e->lastUpdate (true);
-			$entries[$id]['tags'] = $e->tags ();
-			$entries[$id]['url'] = $e->link ();
-			$entries[$id]['type'] = 'url';
-		}
-
-		$this->view->entries = $entries;
-	}
-
-	public function getNbNotReadAction() {
-	}
-}

+ 38 - 10
app/controllers/configureController.php

@@ -91,6 +91,7 @@ class configureController extends ActionController {
 				$this->view->categories = $catDAO->listCategories ();
 
 				if (Request::isPost () && $this->view->flux) {
+					$name = Request::param ('name', '');
 					$cat = Request::param ('category', 0);
 					$path = Request::param ('path_entries', '');
 					$priority = Request::param ('priority', 0);
@@ -103,6 +104,7 @@ class configureController extends ActionController {
 					}
 
 					$values = array (
+						'name' => $name,
 						'category' => $cat,
 						'pathEntries' => $path,
 						'priority' => $priority,
@@ -138,35 +140,41 @@ class configureController extends ActionController {
 		if (Request::isPost ()) {
 			$language = Request::param ('language', 'en');
 			$nb = Request::param ('posts_per_page', 10);
+			$mode = Request::param ('view_mode', 'normal');
 			$view = Request::param ('default_view', 'all');
 			$display = Request::param ('display_posts', 'no');
+			$lazyload = Request::param ('lazyload', 'yes');
 			$sort = Request::param ('sort_order', 'low_to_high');
 			$old = Request::param ('old_entries', 3);
 			$mail = Request::param ('mail_login', false);
 			$openArticle = Request::param ('mark_open_article', 'no');
 			$openSite = Request::param ('mark_open_site', 'no');
-			$openPage = Request::param ('mark_open_page', 'no');
+			$scroll = Request::param ('mark_scroll', 'no');
 			$urlShaarli = Request::param ('shaarli', '');
 
 			$this->view->conf->_language ($language);
 			$this->view->conf->_postsPerPage (intval ($nb));
+			$this->view->conf->_viewMode ($mode);
 			$this->view->conf->_defaultView ($view);
 			$this->view->conf->_displayPosts ($display);
+			$this->view->conf->_lazyload ($lazyload);
 			$this->view->conf->_sortOrder ($sort);
 			$this->view->conf->_oldEntries ($old);
 			$this->view->conf->_mailLogin ($mail);
 			$this->view->conf->_markWhen (array (
 				'article' => $openArticle,
 				'site' => $openSite,
-				'page' => $openPage,
+				'scroll' => $scroll,
 			));
 			$this->view->conf->_urlShaarli ($urlShaarli);
 
 			$values = array (
 				'language' => $this->view->conf->language (),
 				'posts_per_page' => $this->view->conf->postsPerPage (),
+				'view_mode' => $this->view->conf->viewMode (),
 				'default_view' => $this->view->conf->defaultView (),
 				'display_posts' => $this->view->conf->displayPosts (),
+				'lazyload' => $this->view->conf->lazyload (),
 				'sort_order' => $this->view->conf->sortOrder (),
 				'old_entries' => $this->view->conf->oldEntries (),
 				'mail_login' => $this->view->conf->mailLogin (),
@@ -196,6 +204,9 @@ class configureController extends ActionController {
 	}
 
 	public function importExportAction () {
+		$catDAO = new CategoryDAO ();
+		$this->view->categories = $catDAO->listCategories ();
+
 		$this->view->req = Request::param ('q');
 
 		if ($this->view->req == 'export') {
@@ -218,14 +229,31 @@ class configureController extends ActionController {
 		} elseif ($this->view->req == 'import' && Request::isPost ()) {
 			if ($_FILES['file']['error'] == 0) {
 				// on parse le fichier OPML pour récupérer les catégories et les flux associés
-				list ($categories, $feeds) = opml_import (file_get_contents ($_FILES['file']['tmp_name']));
-
-				// 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
-				Request::_param ('q', 'null');
-				Request::_param ('categories', $categories);
-				Request::_param ('feeds', $feeds);
-				Request::forward (array ('c' => 'feed', 'a' => 'massiveImport'));
+				try {
+					list ($categories, $feeds) = opml_import (
+						file_get_contents ($_FILES['file']['tmp_name'])
+					);
+
+					// 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
+					Request::_param ('q', 'null');
+					Request::_param ('categories', $categories);
+					Request::_param ('feeds', $feeds);
+					Request::forward (array ('c' => 'feed', 'a' => 'massiveImport'));
+				} catch (OpmlException $e) {
+					Log::record ($e->getMessage (), Log::ERROR);
+
+					$notif = array (
+						'type' => 'bad',
+						'content' => Translate::t ('bad_opml_file')
+					);
+					Session::_param ('notification', $notif);
+
+					Request::forward (array (
+						'c' => 'configure',
+						'a' => 'importExport'
+					), true);
+				}
 			}
 		}
 

+ 17 - 69
app/controllers/entryController.php

@@ -98,74 +98,22 @@ class entryController extends ActionController {
 		}
 	}
 
-	public function noteAction () {
-		View::appendScript (Url::display (array ('c' => 'javascript', 'a' => 'main')));
-
-		$not_found = false;
-		$entryDAO = new EntryDAO ();
-		$catDAO = new CategoryDAO ();
-
-		$id = Request::param ('id');
-		if ($id) {
-			$entry = $entryDAO->searchById ($id);
-
-			if ($entry) {
-				$feed = $entry->feed (true);
-
-				if (Request::isPost ()) {
-					$note = htmlspecialchars (Request::param ('note', ''));
-					$public = Request::param ('public', 'no');
-					if ($public == 'yes') {
-						$public = true;
-					} else {
-						$public = false;
-					}
-
-					$values = array (
-						'annotation' => $note,
-						'is_public' => $public,
-						'lastUpdate' => time ()
-					);
-
-					if ($entryDAO->updateEntry ($id, $values)) {
-						$notif = array (
-							'type' => 'good',
-							'content' => Translate::t ('updated')
-						);
-					} else {
-						$notif = array (
-							'type' => 'bad',
-							'content' => Translate::t ('error_occured')
-						);
-					}
-					Session::_param ('notification', $notif);
-					Request::forward (array (
-						'c' => 'entry',
-						'a' => 'note',
-						'params' => array (
-							'id' => $id
-						)
-					), true);
-				}
-			} else {
-				$not_found = true;
-			}
-		} else {
-			$not_found = true;
-		}
-
-		if ($not_found) {
-			Error::error (
-				404,
-				array ('error' => array (Translate::t ('page_not_found')))
-			);
-		} else {
-			$this->view->entry = $entry;
-			$this->view->cat_aside = $catDAO->listCategories ();
-			$this->view->nb_favorites = $entryDAO->countFavorites ();
-			$this->view->nb_total = $entryDAO->count ();
-			$this->view->get_c = $feed->category ();
-			$this->view->get_f = $feed->id ();
-		}
+	public function optimizeAction() {
+		// La table des entrées a tendance à grossir énormément
+		// Cette action permet d'optimiser cette table permettant de grapiller un peu de place
+		// Cette fonctionnalité n'est à appeler qu'occasionnellement
+		$entryDAO = new EntryDAO();
+		$entryDAO->optimizeTable();
+
+		$notif = array (
+			'type' => 'good',
+			'content' => Translate::t ('optimization_complete')
+		);
+		Session::_param ('notification', $notif);
+
+		Request::forward(array(
+			'c' => 'configure',
+			'a' => 'display'
+		), true);
 	}
 }

+ 3 - 4
app/controllers/feedController.php

@@ -159,8 +159,7 @@ class feedController extends ActionController {
 				$feedDAO->updateLastUpdate ($feed->id ());
 			} catch (FeedException $e) {
 				Log::record ($e->getMessage (), Log::ERROR);
-				// TODO si on a une erreur ici, il faut mettre
-				// le flux à jour en BDD (error = 1) (issue #70)
+				$feedDAO->isInError ($feed->id ());
 			}
 
 			// On arrête à 10 flux pour ne pas surcharger le serveur
@@ -220,8 +219,8 @@ class feedController extends ActionController {
 		$entryDAO = new EntryDAO ();
 		$feedDAO = new FeedDAO ();
 
-		$categories = Request::param ('categories', array ());
-		$feeds = Request::param ('feeds', array ());
+		$categories = Request::param ('categories', array (), true);
+		$feeds = Request::param ('feeds', array (), true);
 
 		// on ajoute les catégories en masse dans une fonction à part
 		$this->addCategories ($categories);

+ 34 - 1
app/controllers/indexController.php

@@ -6,12 +6,19 @@ class indexController extends ActionController {
 	private $mode = 'all';
 
 	public function indexAction () {
-		if (Request::param ('output') == 'rss') {
+		$output = Request::param ('output');
+
+		if ($output == 'rss') {
 			$this->view->_useLayout (false);
 		} else {
 			View::appendScript (Url::display ('/scripts/shortcut.js'));
 			View::appendScript (Url::display (array ('c' => 'javascript', 'a' => 'main')));
 			View::appendScript (Url::display (array ('c' => 'javascript', 'a' => 'actualize')));
+			View::appendScript (Url::display ('/scripts/endless_mode.js'));
+
+			if(!$output) {
+				Request::_param ('output', $this->view->conf->viewMode());
+			}
 		}
 
 		$entryDAO = new EntryDAO ();
@@ -138,6 +145,32 @@ class indexController extends ActionController {
 		View::prependTitle (Translate::t ('about') . ' - ');
 	}
 
+	public function logsAction () {
+		if (login_is_conf ($this->view->conf) && !is_logged ()) {
+			Error::error (
+				403,
+				array ('error' => array (Translate::t ('access_denied')))
+			);
+		}
+
+		View::prependTitle (Translate::t ('logs') . ' - ');
+
+		$logs = array();
+		try {
+			$logDAO = new LogDAO ();
+			$logs = $logDAO->lister ();
+			$logs = array_reverse ($logs);
+		} catch(FileNotExistException $e) {
+
+		}
+
+		//gestion pagination
+		$page = Request::param ('page', 1);
+		$this->view->logsPaginator = new Paginator ($logs);
+		$this->view->logsPaginator->_nbItemsPerPage (50);
+		$this->view->logsPaginator->_currentPage ($page);
+	}
+
 	public function loginAction () {
 		$this->view->_useLayout (false);
 

+ 15 - 5
app/i18n/en.php

@@ -37,6 +37,9 @@ return array (
 	'before_one_day'		=> 'Before one day',
 	'before_one_week'		=> 'Before one week',
 	'display'			=> 'Display',
+	'normal_view'			=> 'Normal view',
+	'reader_view'			=> 'Reading view',
+	'global_view'			=> 'Global view',
 	'show_all_articles'		=> 'Show all articles',
 	'show_not_reads'		=> 'Show only unread',
 	'older_first'			=> 'Oldest first',
@@ -58,6 +61,7 @@ return array (
 	'rss_feed_management'		=> 'RSS feeds management',
 	'configuration_updated'		=> 'Configuration has been updated',
 	'general_and_reading_management'=> 'General and reading management',
+	'bad_opml_file'			=> 'Your OPML file is invalid',
 	'shortcuts_updated'		=> 'Shortcuts have been updated',
 	'shortcuts_management'		=> 'Shortcuts management',
 	'feeds_marked_read'		=> 'Feeds have been marked as read',
@@ -77,6 +81,8 @@ return array (
 	'category_emptied'		=> 'Category has been emptied',
 	'feed_deleted'			=> 'Feed has been deleted',
 
+	'optimization_complete'		=> 'Optimization complete',
+
 	'your_rss_feeds'		=> 'Your RSS feeds',
 	'your_favorites'		=> 'Your favorites',
 	'public'			=> 'Public',
@@ -113,6 +119,7 @@ return array (
 	'or'				=> 'or',
 
 	'informations'			=> 'Informations',
+	'feed_in_error'			=> 'This feed has encountered a problem. Please verify that it is always reachable then actualize it.',
 	'website_url'			=> 'Website URL',
 	'feed_url'			=> 'Feed URL',
 	'number_articles'		=> 'Number of articles',
@@ -142,20 +149,19 @@ return array (
 	'default_view'			=> 'Default view',
 	'sort_order'			=> 'Sort order',
 	'display_articles_unfolded'	=> 'Show articles unfolded by default',
+	'img_with_lazyload'		=> 'Use "lazy load" mode to load pictures',
 	'auto_read_when'		=> 'Mark automatically as read when',
 	'article_selected'		=> 'Article is selected',
 	'article_open_on_website'	=> 'Article is opened on its original website',
-	'page_loaded'			=> 'Page is loaded',
+	'scroll'			=> 'Page scrolls',
 	'your_shaarli'			=> 'Your Shaarli',
 	'sharing'			=> 'Sharing',
 	'share'				=> 'Share',
 	'by_email'			=> 'By mail',
 	'on_shaarli'			=> 'On your Shaarli',
+	'optimize_bdd'			=> 'Optimize database',
+	'optimize_todo_sometimes'	=> 'To do occasionally to reduce size of database',
 
-	'note'				=> 'Note',
-	'add_note'			=> 'Add a note',
-	'update_note'			=> 'Update your note',
-	'ask_public_article'		=> 'Public article?',
 	'article'			=> 'Article',
 	'title'				=> 'Title',
 	'author'			=> 'Author',
@@ -187,6 +193,9 @@ return array (
 	'credits'			=> 'Credits',
 	'credits_content'		=> 'Some design elements come from <a href="http://twitter.github.io/bootstrap/">Bootstrap</a> although FreshRSS doesn\'t use this framework. <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">Icons</a> come from <a href="https://www.gnome.org/">GNOME project</a>. <em>Open Sans</em> font police used has been created by <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Favicons are collected with <a href="https://getfavicon.appspot.com/">getFavicon API</a>. FreshRSS is based on <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, a PHP framework.',
 
+	'logs'				=> 'Logs',
+	'logs_empty'			=> 'Log file is empty',
+
 	// DATE
 	'january'			=> 'january',
 	'february'			=> 'february',
@@ -259,6 +268,7 @@ return array (
 	'do_not_change_if_doubt'	=> 'Don\'t change if you doubt about it',
 
 	'bdd_conf_is_ok'		=> 'Database configuration has been saved.',
+	'bdd_conf_is_ko'		=> 'Verify your database information.',
 	'host'				=> 'Host',
 	'username'			=> 'Username',
 	'password'			=> 'Password',

+ 15 - 5
app/i18n/fr.php

@@ -37,6 +37,9 @@ return array (
 	'before_one_day'		=> 'Antérieurs à 1 jour',
 	'before_one_week'		=> 'Antérieurs à 1 semaine',
 	'display'			=> 'Affichage',
+	'normal_view'			=> 'Vue normale',
+	'reader_view'			=> 'Vue lecture',
+	'global_view'			=> 'Vue globale',
 	'show_all_articles'		=> 'Afficher tous les articles',
 	'show_not_reads'		=> 'Afficher les non lus',
 	'older_first'			=> 'Plus anciens en premier',
@@ -58,6 +61,7 @@ return array (
 	'rss_feed_management'		=> 'Gestion des flux RSS',
 	'configuration_updated'		=> 'La configuration a été mise à jour',
 	'general_and_reading_management'=> 'Gestion générale et affichage',
+	'bad_opml_file'			=> 'Votre fichier OPML n\'est pas valide',
 	'shortcuts_updated'		=> 'Les raccourcis ont été mis à jour',
 	'shortcuts_management'		=> 'Gestion des raccourcis',
 	'feeds_marked_read'		=> 'Les flux ont été marqués comme lu',
@@ -77,6 +81,8 @@ return array (
 	'category_emptied'		=> 'La catégorie a été vidée',
 	'feed_deleted'			=> 'Le flux a été supprimé',
 
+	'optimization_complete'		=> 'Optimisation terminée',
+
 	'your_rss_feeds'		=> 'Vos flux RSS',
 	'your_favorites'		=> 'Vos favoris',
 	'public'			=> 'Public',
@@ -113,6 +119,7 @@ return array (
 	'or'				=> 'ou',
 
 	'informations'			=> 'Informations',
+	'feed_in_error'			=> 'Ce flux a rencontré un problème. Veuillez vérifier qu\'il est toujours accessible puis actualisez-le.',
 	'website_url'			=> 'URL du site',
 	'feed_url'			=> 'URL du flux',
 	'number_articles'		=> 'Nombre d\'articles',
@@ -142,20 +149,19 @@ return array (
 	'default_view'			=> 'Vue par défaut',
 	'sort_order'			=> 'Ordre de tri',
 	'display_articles_unfolded'	=> 'Afficher les articles dépliés par défaut',
+	'img_with_lazyload'		=> 'Utiliser le mode "lazy load" pour charger les images',
 	'auto_read_when'		=> 'Marquer automatiquement comme lu lorsque',
 	'article_selected'		=> 'L\'article est sélectionné',
 	'article_open_on_website'	=> 'L\'article est ouvert sur le site d\'origine',
-	'page_loaded'			=> 'La page est chargée',
+	'scroll'			=> 'Au défilement de la page',
 	'your_shaarli'			=> 'Votre Shaarli',
 	'sharing'			=> 'Partage',
 	'share'				=> 'Partager',
 	'by_email'			=> 'Par mail',
 	'on_shaarli'			=> 'Sur votre Shaarli',
+	'optimize_bdd'			=> 'Optimiser la base de données',
+	'optimize_todo_sometimes'	=> 'À faire de temps en temps pour réduire la taille de la BDD',
 
-	'note'				=> 'Note',
-	'add_note'			=> 'Ajouter une note',
-	'update_note'			=> 'Modifier votre note',
-	'ask_public_article'		=> 'Article public ?',
 	'article'			=> 'Article',
 	'title'				=> 'Titre',
 	'author'			=> 'Auteur',
@@ -187,6 +193,9 @@ return array (
 	'credits'			=> 'Crédits',
 	'credits_content'		=> 'Des éléments de design sont issus du <a href="http://twitter.github.io/bootstrap/">projet Bootstrap</a> bien que FreshRSS n\'utilise pas ce framework. Les <a href="https://git.gnome.org/browse/gnome-icon-theme-symbolic">icônes</a> sont issues du <a href="https://www.gnome.org/">projet GNOME</a>. La police <em>Open Sans</em> utilisée a été créée par <a href="https://www.google.com/webfonts/specimen/Open+Sans">Steve Matteson</a>. Les favicons sont récupérés grâce au site <a href="https://getfavicon.appspot.com/">getFavicon</a>. FreshRSS repose sur <a href="https://github.com/marienfressinaud/MINZ">Minz</a>, un framework PHP.',
 
+	'logs'				=> 'Logs',
+	'logs_empty'			=> 'Les logs sont vides',
+
 	// DATE
 	'january'			=> 'janvier',
 	'february'			=> 'février',
@@ -259,6 +268,7 @@ return array (
 	'do_not_change_if_doubt'	=> 'Laissez tel quel dans le doute',
 
 	'bdd_conf_is_ok'		=> 'La configuration de la base de données a été enregistrée.',
+	'bdd_conf_is_ko'		=> 'Vérifiez les informations d\'accès à la base de données.',
 	'host'				=> 'Hôte',
 	'username'			=> 'Nom utilisateur',
 	'password'			=> 'Mot de passe',

+ 1 - 1
app/layout/aside_feed.phtml

@@ -43,7 +43,7 @@
 
 	<?php if (!empty ($this->feeds)) { ?>
 	<?php foreach ($this->feeds as $feed) { ?>
-	<li class="item<?php echo ($this->flux && $this->flux->id () == $feed->id ()) ? ' active' : ''; ?>">
+	<li class="item<?php echo ($this->flux && $this->flux->id () == $feed->id ()) ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?>">
 		<a href="<?php echo _url ('configure', 'feed', 'id', $feed->id ()); ?>">
 			<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" />
 			<?php echo $feed->name (); ?>

+ 1 - 1
app/layout/aside_flux.phtml

@@ -62,7 +62,7 @@
 			<ul class="feeds<?php echo $c_active ? ' active' : ''; ?>">
 				<?php foreach ($feeds as $feed) { ?>
 				<?php $f_active = false; if ($this->get_f == $feed->id ()) { $f_active = true; } ?>
-				<li class="item<?php echo $f_active ? ' active' : ''; ?>">
+				<li class="item<?php echo $f_active ? ' active' : ''; ?><?php echo $feed->inError () ? ' error' : ''; ?>">
 					<div class="dropdown">
 						<div id="dropdown-<?php echo $feed->id(); ?>" class="dropdown-target"></div>
 						<a class="dropdown-toggle" href="#dropdown-<?php echo $feed->id(); ?>"><i class="icon i_configure"></i></a>

+ 20 - 14
app/layout/header.phtml

@@ -15,21 +15,26 @@
 	</div>
 
 	<div class="item search">
-		<?php
-			$params = Request::params ();
-			if (isset ($params['search'])) {
-				unset ($params['search']);
-			}
-			$url = array (
-				'c' => 'index',
-				'a' => 'index',
-				'params' => $params
-			);
-		?>
-		<form action="<?php echo Url::display ($url); ?>" method="get">
+		<form action="<?php echo _url ('index', 'index'); ?>" method="get">
 			<div class="stick">
-				<?php $s = Request::param ('search', ''); ?>
-				<input type="text" name="search" id="search" value="<?php echo $s; ?>" placeholder="<?php echo Translate::t ('search'); ?>" />
+				<?php $search = Request::param ('search', ''); ?>
+				<input type="text" name="search" id="search" value="<?php echo $search; ?>" placeholder="<?php echo Translate::t ('search'); ?>" />
+
+				<?php $get = Request::param ('get', ''); ?>
+				<?php if($get != '') { ?>
+				<input type="hidden" name="get" value="<?php echo $get; ?>" />
+				<?php } ?>
+
+				<?php $order = Request::param ('order', ''); ?>
+				<?php if($order != '') { ?>
+				<input type="hidden" name="order" value="<?php echo $order; ?>" />
+				<?php } ?>
+
+				<?php $state = Request::param ('state', ''); ?>
+				<?php if($state != '') { ?>
+				<input type="hidden" name="state" value="<?php echo $state; ?>" />
+				<?php } ?>
+
 				<button class="btn" type="submit"><i class="icon i_search"></i></button>
 			</div>
 		</form>
@@ -49,6 +54,7 @@
 				<li class="item"><a href="<?php echo _url ('configure', 'shortcut'); ?>"><?php echo Translate::t ('shortcuts'); ?></a></li>
 				<li class="separator"></li>
 				<li class="item"><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Translate::t ('about'); ?></a></li>
+				<li class="item"><a href="<?php echo _url ('index', 'logs'); ?>"><?php echo Translate::t ('logs'); ?></a></li>
 			</ul>
 		</div>
 	</div>

+ 5 - 0
app/layout/nav_entries.phtml

@@ -0,0 +1,5 @@
+<ul class="nav_entries">
+	<li class="item"><a class="previous_entry" href="#"><i class="icon i_prev"></i></a></li>
+	<li class="item"><a href="#"><i class="icon i_up"></i></a></li>
+	<li class="item"><a class="next_entry" href="#"><i class="icon i_next"></i></a></li>
+</ul>

+ 48 - 19
app/layout/nav_menu.phtml

@@ -59,43 +59,72 @@
 		<ul class="dropdown-menu">
 			<li class="dropdown-close"><a href="#close"><i class="icon i_close"></i></a></li>
 
+			<?php
+				$url_output = $url;
+				$actual_view = Request::param('output', 'normal');
+			?>
+			<?php if($actual_view != 'normal') { ?>
+			<li class="item">
+				<?php $url_output['params']['output'] = 'normal'; ?>
+				<a class="view_normal" href="<?php echo Url::display ($url_output); ?>">
+					<?php echo Translate::t ('normal_view'); ?>
+				</a>
+			</li>
+			<?php } if($actual_view != 'reader') { ?>
+			<li class="item">
+				<?php $url_output['params']['output'] = 'reader'; ?>
+				<a class="view_normal" href="<?php echo Url::display ($url_output); ?>">
+					<?php echo Translate::t ('reader_view'); ?>
+				</a>
+			</li>
+			<?php } if($actual_view != 'global') { ?>
+			<li class="item">
+				<?php $url_output['params']['output'] = 'global'; ?>
+				<a class="view_normal" href="<?php echo Url::display ($url_output); ?>">
+					<?php echo Translate::t ('global_view'); ?>
+				</a>
+			</li>
+			<?php } ?>
+
+			<li class="separator"></li>
+
 			<li class="item">
 				<?php
+					$url_state = $url;
 					if ($this->state == 'not_read') {
-						$url['params']['state'] = 'all';
+						$url_state['params']['state'] = 'all';
 				?>
-				<a class="print_all" href="<?php echo Url::display ($url); ?>"><?php echo Translate::t ('show_all_articles'); ?></a>
+				<a class="print_all" href="<?php echo Url::display ($url_state); ?>">
+					<?php echo Translate::t ('show_all_articles'); ?>
+				</a>
 				<?php
 					} else {
-						$url['params']['state'] = 'not_read';
+						$url_state['params']['state'] = 'not_read';
 				?>
-				<a class="print_non_read" href="<?php echo Url::display ($url); ?>"><?php echo Translate::t ('show_not_reads'); ?></a>
+				<a class="print_non_read" href="<?php echo Url::display ($url_state); ?>">
+					<?php echo Translate::t ('show_not_reads'); ?>
+				</a>
 				<?php } ?>
 			</li>
+
 			<li class="separator"></li>
 
-			<?php
-				$params = Request::params ();
-				if (isset ($params['search'])) {
-					$params['search'] = urlencode ($params['search']);
-				}
-				$url = array (
-					'c' => 'index',
-					'a' => 'index',
-					'params' => $params
-				);
-			?>
 			<li class="item">
 				<?php
+					$url_order = $url;
 					if ($this->order == 'low_to_high') {
-						$url['params']['order'] = 'high_to_low';
+						$url_order['params']['order'] = 'high_to_low';
 				?>
-				<a href="<?php echo Url::display ($url); ?>"><?php echo Translate::t ('older_first'); ?></a>
+				<a href="<?php echo Url::display ($url_order); ?>">
+					<?php echo Translate::t ('older_first'); ?>
+				</a>
 				<?php
 					} else {
-						$url['params']['order'] = 'low_to_high';
+						$url_order['params']['order'] = 'low_to_high';
 				?>
-				<a href="<?php echo Url::display ($url); ?>"><?php echo Translate::t ('newer_first'); ?></a>
+				<a href="<?php echo Url::display ($url_order); ?>">
+					<?php echo Translate::t ('newer_first'); ?>
+				</a>
 				<?php } ?>
 			</li>
 		</ul>

+ 3 - 3
app/models/Category.php

@@ -35,10 +35,10 @@ class Category extends Model {
 	public function feeds () {
 		if (is_null ($this->feeds)) {
 			$feedDAO = new FeedDAO ();
-			return $feedDAO->listByCategory ($this->id ());
-		} else {
-			return $this->feeds;
+			$this->feeds = $feedDAO->listByCategory ($this->id ());
 		}
+
+		return $this->feeds;
 	}
 
 	public function _id ($value) {

+ 51 - 1
app/models/Entry.php

@@ -194,6 +194,29 @@ class Entry extends Model {
 		}
 	}
 
+	public function loadCompleteContent($pathEntries) {
+		// Gestion du contenu
+		// On cherche à récupérer les articles en entier... même si le flux ne le propose pas
+		if ($pathEntries) {
+			$entryDAO = new EntryDAO();
+			$entry = $entryDAO->searchByGuid($this->feed, $this->guid);
+
+			if($entry) {
+				// l'article existe déjà en BDD, en se contente de recharger ce contenu
+				$this->content = $entry->content();
+			} else {
+				try {
+					// l'article n'est pas en BDD, on va le chercher sur le site
+					$this->content = get_content_by_parsing(
+						$this->link(), $pathEntries
+					);
+				} catch (Exception $e) {
+					// rien à faire, on garde l'ancien contenu (requête a échoué)
+				}
+			}
+		}
+	}
+
 	public function toArray () {
 		return array (
 			'id' => $this->id (),
@@ -239,7 +262,7 @@ class EntryDAO extends Model_pdo {
 			return true;
 		} else {
 			$info = $stm->errorInfo();
-			Log::record ('SQL error : ' . $info[2], Log::ERROR);
+			Log::record ('SQL error : ' . $info[2], Log::NOTICE);
 			return false;
 		}
 	}
@@ -360,6 +383,27 @@ class EntryDAO extends Model_pdo {
 		}
 	}
 
+	public function searchByGuid ($feed_id, $id) {
+		// un guid est unique pour un flux donné
+		$sql = 'SELECT * FROM entry WHERE id_feed=? AND guid=?';
+		$stm = $this->bd->prepare ($sql);
+
+		$values = array (
+			$feed_id,
+			$id
+		);
+
+		$stm->execute ($values);
+		$res = $stm->fetchAll (PDO::FETCH_ASSOC);
+		list ($entry, $next) = HelperEntry::daoToEntry ($res);
+
+		if (isset ($entry[0])) {
+			return $entry[0];
+		} else {
+			return false;
+		}
+	}
+
 	public function searchById ($id) {
 		$sql = 'SELECT * FROM entry WHERE id=?';
 		$stm = $this->bd->prepare ($sql);
@@ -465,6 +509,12 @@ class EntryDAO extends Model_pdo {
 
 		return $res[0]['count'];
 	}
+
+	public function optimizeTable() {
+		$sql = 'OPTIMIZE TABLE entry';
+		$stm = $this->bd->prepare ($sql);
+		$stm->execute ();
+	}
 }
 
 class HelperEntry {

+ 6 - 0
app/models/Exception/FeedException.php

@@ -11,3 +11,9 @@ class BadUrlException extends FeedException {
 		parent::__construct ('`' . $url . '` is not a valid URL');
 	}
 }
+
+class OpmlException extends FeedException {
+	public function __construct ($name_file) {
+		parent::__construct ('OPML file is invalid');
+	}
+}

+ 34 - 13
app/models/Feed.php

@@ -12,6 +12,7 @@ class Feed extends Model {
 	private $priority = 10;
 	private $pathEntries = '';
 	private $httpAuth = '';
+	private $error = false;
 
 	public function __construct ($url) {
 		$this->_url ($url);
@@ -69,6 +70,9 @@ class Feed extends Model {
 			);
 		}
 	}
+	public function inError () {
+		return $this->error;
+	}
 	public function nbEntries () {
 		$feedDAO = new FeedDAO ();
 		return $feedDAO->countEntries ($this->id ());
@@ -138,6 +142,14 @@ class Feed extends Model {
 	public function _httpAuth ($value) {
 		$this->httpAuth = $value;
 	}
+	public function _error ($value) {
+		if ($value) {
+			$value = true;
+		} else {
+			$value = false;
+		}
+		$this->error = $value;
+	}
 
 	public function load () {
 		if (!is_null ($this->url)) {
@@ -204,18 +216,7 @@ class Feed extends Model {
 				}
 			}
 
-			// Gestion du contenu
-			// On cherche à récupérer les articles en entier... même si le flux ne le propose pas
-			$path = $this->pathEntries ();
-			if ($path) {
-				try {
-					$content = get_content_by_parsing ($item->get_permalink (), $path);
-				} catch (Exception $e) {
-					$content = $item->get_content ();
-				}
-			} else {
-				$content = $item->get_content ();
-			}
+			$content = $item->get_content ();
 
 			$entry = new Entry (
 				$this->id (),
@@ -227,6 +228,8 @@ class Feed extends Model {
 				$date ? $date : time ()
 			);
 			$entry->_tags ($tags);
+			// permet de récupérer le contenu des flux tronqués
+			$entry->loadCompleteContent($this->pathEntries());
 
 			$entries[$entry->id ()] = $entry;
 		}
@@ -289,7 +292,7 @@ class FeedDAO extends Model_pdo {
 	}
 
 	public function updateLastUpdate ($id) {
-		$sql = 'UPDATE feed SET lastUpdate=? WHERE id=?';
+		$sql = 'UPDATE feed SET lastUpdate=?, error=0 WHERE id=?';
 		$stm = $this->bd->prepare ($sql);
 
 		$values = array (
@@ -306,6 +309,23 @@ class FeedDAO extends Model_pdo {
 		}
 	}
 
+	public function isInError ($id) {
+		$sql = 'UPDATE feed SET error=1 WHERE id=?';
+		$stm = $this->bd->prepare ($sql);
+
+		$values = array (
+			$id
+		);
+
+		if ($stm && $stm->execute ($values)) {
+			return true;
+		} else {
+			$info = $stm->errorInfo();
+			Log::record ('SQL error : ' . $info[2], Log::ERROR);
+			return false;
+		}
+	}
+
 	public function changeCategory ($idOldCat, $idNewCat) {
 		$catDAO = new CategoryDAO ();
 		$newCat = $catDAO->searchById ($idNewCat);
@@ -470,6 +490,7 @@ class HelperFeed {
 			$list[$key]->_priority ($dao['priority']);
 			$list[$key]->_pathEntries ($dao['pathEntries']);
 			$list[$key]->_httpAuth (base64_decode ($dao['httpAuth']));
+			$list[$key]->_error ($dao['error']);
 
 			if (isset ($dao['id'])) {
 				$list[$key]->_id ($dao['id']);

+ 47 - 0
app/models/Log.php

@@ -0,0 +1,47 @@
+<?php
+
+class Log_Model extends Model {
+	private $date;
+	private $level;
+	private $information;
+
+	public function date () {
+		return $this->date;
+	}
+	public function level () {
+		return $this->level;
+	}
+	public function info () {
+		return $this->information;
+	}
+	public function _date ($date) {
+		$this->date = $date;
+	}
+	public function _level ($level) {
+		$this->level = $level;
+	}
+	public function _info ($information) {
+		$this->information = $information;
+	}
+}
+
+class LogDAO extends Model_txt {
+	public function __construct () {
+		parent::__construct (LOG_PATH . '/application.log', 'r+');
+	}
+	
+	public function lister () {
+		$logs = array ();
+
+		$i = 0;
+		while (($line = $this->readLine ()) !== false) {
+			$logs[$i] = new Log_Model ();
+			$logs[$i]->_date (preg_replace ("'\[(.*?)\] \[(.*?)\] --- (.*?)'U", "\\1", $line));
+			$logs[$i]->_level (preg_replace ("'\[(.*?)\] \[(.*?)\] --- (.*?)'U", "\\2", $line));
+			$logs[$i]->_info (preg_replace ("'\[(.*?)\] \[(.*?)\] --- (.*?)'U", "\\3", $line));
+			$i++;
+		}
+
+		return $logs;
+	}
+}

+ 48 - 6
app/models/RSSConfiguration.php

@@ -7,8 +7,10 @@ class RSSConfiguration extends Model {
 	);
 	private $language;
 	private $posts_per_page;
+	private $view_mode;
 	private $default_view;
 	private $display_posts;
+	private $lazyload;
 	private $sort_order;
 	private $old_entries;
 	private $shortcuts = array ();
@@ -20,8 +22,10 @@ class RSSConfiguration extends Model {
 		$confDAO = new RSSConfigurationDAO ();
 		$this->_language ($confDAO->language);
 		$this->_postsPerPage ($confDAO->posts_per_page);
+		$this->_viewMode ($confDAO->view_mode);
 		$this->_defaultView ($confDAO->default_view);
 		$this->_displayPosts ($confDAO->display_posts);
+		$this->_lazyload ($confDAO->lazyload);
 		$this->_sortOrder ($confDAO->sort_order);
 		$this->_oldEntries ($confDAO->old_entries);
 		$this->_shortcuts ($confDAO->shortcuts);
@@ -39,12 +43,18 @@ class RSSConfiguration extends Model {
 	public function postsPerPage () {
 		return $this->posts_per_page;
 	}
+	public function viewMode () {
+		return $this->view_mode;
+	}
 	public function defaultView () {
 		return $this->default_view;
 	}
 	public function displayPosts () {
 		return $this->display_posts;
 	}
+	public function lazyload () {
+		return $this->lazyload;
+	}
 	public function sortOrder () {
 		return $this->sort_order;
 	}
@@ -66,8 +76,8 @@ class RSSConfiguration extends Model {
 	public function markWhenSite () {
 		return $this->mark_when['site'];
 	}
-	public function markWhenPage () {
-		return $this->mark_when['page'];
+	public function markWhenScroll () {
+		return $this->mark_when['scroll'];
 	}
 	public function urlShaarli () {
 		return $this->url_shaarli;
@@ -80,12 +90,19 @@ class RSSConfiguration extends Model {
 		$this->language = $value;
 	}
 	public function _postsPerPage ($value) {
-		if (is_int (intval ($value))) {
+		if (is_int (intval ($value)) && $value > 0) {
 			$this->posts_per_page = $value;
 		} else {
 			$this->posts_per_page = 10;
 		}
 	}
+	public function _viewMode ($value) {
+		if ($value == 'global' || $value == 'reader') {
+			$this->view_mode = $value;
+		} else {
+			$this->view_mode = 'normal';
+		}
+	}
 	public function _defaultView ($value) {
 		if ($value == 'not_read') {
 			$this->default_view = 'not_read';
@@ -100,6 +117,13 @@ class RSSConfiguration extends Model {
 			$this->display_posts = 'no';
 		}
 	}
+	public function _lazyload ($value) {
+		if ($value == 'no') {
+			$this->lazyload = 'no';
+		} else {
+			$this->lazyload = 'yes';
+		}
+	}
 	public function _sortOrder ($value) {
 		if ($value == 'high_to_low') {
 			$this->sort_order = 'high_to_low';
@@ -108,7 +132,7 @@ class RSSConfiguration extends Model {
 		}
 	}
 	public function _oldEntries ($value) {
-		if (is_int (intval ($value))) {
+		if (is_int (intval ($value)) && $value > 0) {
 			$this->old_entries = $value;
 		} else {
 			$this->old_entries = 3;
@@ -127,9 +151,19 @@ class RSSConfiguration extends Model {
 		}
 	}
 	public function _markWhen ($values) {
+		if(!isset($values['article'])) {
+			$values['article'] = 'yes';
+		}
+		if(!isset($values['site'])) {
+			$values['site'] = 'yes';
+		}
+		if(!isset($values['scroll'])) {
+			$values['scroll'] = 'yes';
+		}
+
 		$this->mark_when['article'] = $values['article'];
 		$this->mark_when['site'] = $values['site'];
-		$this->mark_when['page'] = $values['page'];
+		$this->mark_when['scroll'] = $values['scroll'];
 	}
 	public function _urlShaarli ($value) {
 		$this->url_shaarli = '';
@@ -142,8 +176,10 @@ class RSSConfiguration extends Model {
 class RSSConfigurationDAO extends Model_array {
 	public $language = 'en';
 	public $posts_per_page = 20;
+	public $view_mode = 'normal';
 	public $default_view = 'not_read';
 	public $display_posts = 'no';
+	public $lazyload = 'yes';
 	public $sort_order = 'low_to_high';
 	public $old_entries = 3;
 	public $shortcuts = array (
@@ -159,7 +195,7 @@ class RSSConfigurationDAO extends Model_array {
 	public $mark_when = array (
 		'article' => 'yes',
 		'site' => 'yes',
-		'page' => 'no'
+		'scroll' => 'no'
 	);
 	public $url_shaarli = '';
 
@@ -172,12 +208,18 @@ class RSSConfigurationDAO extends Model_array {
 		if (isset ($this->array['posts_per_page'])) {
 			$this->posts_per_page = $this->array['posts_per_page'];
 		}
+		if (isset ($this->array['view_mode'])) {
+			$this->view_mode = $this->array['view_mode'];
+		}
 		if (isset ($this->array['default_view'])) {
 			$this->default_view = $this->array['default_view'];
 		}
 		if (isset ($this->array['display_posts'])) {
 			$this->display_posts = $this->array['display_posts'];
 		}
+		if (isset ($this->array['lazyload'])) {
+			$this->lazyload = $this->array['lazyload'];
+		}
 		if (isset ($this->array['sort_order'])) {
 			$this->sort_order = $this->array['sort_order'];
 		}

+ 0 - 3
app/views/api/getNbNotRead.phtml

@@ -1,3 +0,0 @@
-<?php
-echo json_encode($this->nb_not_read);
-

+ 0 - 3
app/views/api/getPublicFeed.phtml

@@ -1,3 +0,0 @@
-<?php
-echo json_encode ($this->entries);
-?>

+ 33 - 3
app/views/configure/display.phtml

@@ -46,6 +46,11 @@
 		<div class="form-group">
 			<label class="group-name"><?php echo Translate::t ('default_view'); ?></label>
 			<div class="group-controls">
+				<select name="view_mode" id="view_mode">
+					<option value="normal"<?php echo $this->conf->viewMode () == 'normal' ? ' selected="selected"' : ''; ?>><?php echo Translate::t ('normal_view'); ?></option>
+					<option value="reader"<?php echo $this->conf->viewMode () == 'reader' ? ' selected="selected"' : ''; ?>><?php echo Translate::t ('reader_view'); ?></option>
+					<option value="global"<?php echo $this->conf->viewMode () == 'global' ? ' selected="selected"' : ''; ?>><?php echo 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->defaultView () == 'all' ? ' checked="checked"' : ''; ?> />
 					<?php echo Translate::t ('show_all_articles'); ?>
@@ -67,6 +72,20 @@
 			</div>
 		</div>
 
+		<div class="form-group">
+			<label class="group-name"><?php echo Translate::t ('img_with_lazyload'); ?></label>
+			<div class="group-controls">
+				<label class="radio" for="lazyload_yes">
+					<input type="radio" name="lazyload" id="lazyload_yes" value="yes"<?php echo $this->conf->lazyload () == 'yes' ? ' checked="checked"' : ''; ?> />
+					<?php echo Translate::t ('yes'); ?><noscript> - <b><?php echo Translate::t ('javascript_should_be_activated'); ?></b></noscript>
+				</label>
+				<label class="radio" for="lazyload_no">
+					<input type="radio" name="lazyload" id="lazyload_no" value="no"<?php echo $this->conf->lazyload () == 'no' ? ' checked="checked"' : ''; ?> />
+					<?php echo Translate::t ('no'); ?>
+				</label>
+			</div>
+		</div>
+
 		<div class="form-group">
 			<label class="group-name"><?php echo Translate::t ('display_articles_unfolded'); ?></label>
 			<div class="group-controls">
@@ -92,9 +111,9 @@
 					<input type="checkbox" name="mark_open_site" id="check_open_site" value="yes"<?php echo $this->conf->markWhenSite () == 'yes' ? ' checked="checked"' : ''; ?> />
 					<?php echo Translate::t ('article_open_on_website'); ?>
 				</label>
-				<label class="checkbox" for="check_open_page">
-					<input type="checkbox" name="mark_open_page" id="check_open_page" value="yes"<?php echo $this->conf->markWhenPage () == 'yes' ? ' checked="checked"' : ''; ?> />
-					<?php echo Translate::t ('page_loaded'); ?>
+				<label class="checkbox" for="check_scroll">
+					<input type="checkbox" name="mark_scroll" id="check_scroll" value="yes"<?php echo $this->conf->markWhenScroll () == 'yes' ? ' checked="checked"' : ''; ?> />
+					<?php echo Translate::t ('scroll'); ?>
 				</label>
 			</div>
 		</div>
@@ -107,6 +126,17 @@
 			</div>
 		</div>
 
+		<legend><?php echo Translate::t ('advanced'); ?></legend>
+		<div class="form-group">
+			<label class="group-name"></label>
+			<div class="group-controls">
+				<a class="btn" href="<?php echo _url('entry', 'optimize'); ?>">
+					<?php echo Translate::t('optimize_bdd'); ?>
+				</a>
+				<i class="icon i_help"></i> <?php echo Translate::t('optimize_todo_sometimes'); ?>
+			</div>
+		</div>
+
 		<div class="form-group form-actions">
 			<div class="group-controls">
 				<button type="submit" class="btn btn-important"><?php echo Translate::t ('save'); ?></button>

+ 18 - 0
app/views/configure/feed.phtml

@@ -7,8 +7,18 @@
 	<h1><?php echo $this->flux->name (); ?></h1>
 	<?php echo $this->flux->description (); ?>
 
+	<?php if ($this->flux->inError ()) { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo Translate::t ('damn'); ?></span> <?php echo Translate::t ('feed_in_error'); ?></p>
+	<?php } ?>
+
 	<form method="post" action="<?php echo _url ('configure', 'feed', 'id', $this->flux->id ()); ?>">
 		<legend><?php echo Translate::t ('informations'); ?></legend>
+		<div class="form-group">
+			<label class="group-name" for="name"><?php echo Translate::t ('title'); ?></label>
+			<div class="group-controls">
+				<input type="text" name="name" id="name" value="<?php echo $this->flux->name () ; ?>" />
+			</div>
+		</div>
 		<div class="form-group">
 			<label class="group-name"><?php echo Translate::t ('website_url'); ?></label>
 			<div class="group-controls">
@@ -21,6 +31,14 @@
 				<span class="control"><a target="_blank" href="<?php echo $this->flux->url (); ?>"><?php echo $this->flux->url (); ?></a></span>
 			</div>
 		</div>
+		<div class="form-group">
+			<label class="group-name"></label>
+			<div class="group-controls">
+				<a class="btn" href="<?php echo _url ('feed', 'actualize', 'id', $this->flux->id ()); ?>">
+					<i class="icon i_refresh"></i> <?php echo Translate::t('actualize'); ?>
+				</a>
+			</div>
+		</div>
 		<div class="form-group">
 			<label class="group-name"><?php echo Translate::t ('number_articles'); ?></label>
 			<div class="group-controls">

+ 0 - 65
app/views/entry/note.phtml

@@ -1,65 +0,0 @@
-<?php $this->partial ('aside_flux'); ?>
-
-<div class="post">
-	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Translate::t ('back_to_rss_feeds'); ?></a>
-
-	<form method="post" action="<?php echo _url ('entry', 'note', 'id', $this->entry->id ()); ?>">
-		<legend><?php echo Translate::t ('note'); ?></legend>
-
-		<div class="form-group">
-			<label class="group-name" for="note"><?php echo Translate::t ('add_note'); ?></label>
-			<div class="group-controls">
-				<textarea rows="5" cols="80" name="note" id="note"><?php echo $this->entry->notes (); ?></textarea>
-			</div>
-		</div>
-		<div class="form-group">
-			<label class="group-name" for="public_note"><?php echo Translate::t ('ask_public_article'); ?></label>
-			<div class="group-controls">
-				<label class="checkbox" for="public">
-					<input type="checkbox" name="public" id="public" value="yes"<?php echo $this->entry->isPublic () ? ' checked="checked"' : ''; ?> /> <?php echo Translate::t ('yes'); ?>
-				</label>
-			</div>
-		</div>
-
-		<div class="form-group form-actions">
-			<div class="group-controls">
-				<button type="submit" class="btn btn-important"><?php echo Translate::t ('save'); ?></button>
-				<button type="reset" class="btn"><?php echo Translate::t ('cancel'); ?></button>
-			</div>
-		</div>
-
-		<legend><?php echo Translate::t ('article'); ?></legend>
-
-		<div class="form-group">
-			<label class="group-name"><?php echo Translate::t ('title'); ?></label>
-			<div class="group-controls">
-				<span class="control"><a href="<?php echo $this->entry->link (); ?>"><?php echo $this->entry->title (); ?></a></span>
-			</div>
-		</div>
-
-		<?php
-		$author = $this->entry->author ();
-		if ($author) { ?>
-		<div class="form-group">
-			<label class="group-name"><?php echo Translate::t ('author'); ?></label>
-			<div class="group-controls">
-				<span class="control"><?php echo $author; ?></span>
-			</div>
-		</div>
-		<?php } ?>
-
-		<div class="form-group">
-			<label class="group-name"><?php echo Translate::t ('publication_date'); ?></label>
-			<div class="group-controls">
-				<span class="control"><?php echo $this->entry->date (); ?></span>
-			</div>
-		</div>
-
-		<div class="form-group">
-			<label class="group-name"><?php echo Translate::t ('article'); ?></label>
-			<div class="group-controls">
-				<span class="control"><?php echo $this->entry->content (); ?></span>
-			</div>
-		</div>
-	</form>
-</div>

+ 37 - 0
app/views/helpers/global_view.phtml

@@ -0,0 +1,37 @@
+<?php $this->partial ('nav_menu'); ?>
+
+<div id="stream" class="global">
+<?php
+	foreach ($this->cat_aside as $cat) {
+		$feeds = $cat->feeds ();
+		$catNotRead = $cat->nbNotRead ();
+		if (!empty ($feeds)) {
+?>
+	<div class="category">
+		<div class="cat_header">
+			<a href="<?php echo _url ('index', 'index', 'get', 'c_' . $cat->id ()); ?>">
+			<?php echo $cat->name(); ?><?php echo $catNotRead > 0 ? ' (' . $catNotRead . ')' : ''; ?>
+			</a>
+		</div>
+
+		<ul class="feeds">
+			<?php foreach ($feeds as $feed) { ?>
+			<?php $not_read = $feed->nbNotRead (); ?>
+			<li class="item">
+				<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" />
+
+				<a href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id ()); ?>">
+				<?php echo $not_read > 0 ? '<b>' : ''; ?>
+				<?php echo $feed->name(); ?>
+				<?php echo $not_read > 0 ? ' (' . $not_read . ')' : ''; ?>
+				<?php echo $not_read > 0 ? '</b>' : ''; ?>
+				</a>
+			</li>
+			<?php } ?>
+		</ul>
+	</div>
+<?php
+		}
+	}
+?>
+</div>

+ 47 - 0
app/views/helpers/logs_pagination.phtml

@@ -0,0 +1,47 @@
+<?php
+	$c = Request::controllerName ();
+	$a = Request::actionName ();
+	$params = Request::params ();
+?>
+
+<?php if ($this->nbPage > 1) { ?>
+<ul class="pagination">
+	<?php $params[$getteur] = 1; ?>
+	<li class="item pager-first">
+		<?php if ($this->currentPage > 1) { ?>
+		<a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>">« Début</a>
+		<?php } ?>
+	</li>
+
+	<?php $params[$getteur] = $this->currentPage - 1; ?>
+	<li class="item pager-previous">
+		<?php if ($this->currentPage > 1) { ?>
+		<a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>">‹ Précédent</a>
+		<?php } ?>
+	</li>
+
+	<?php for ($i = $this->currentPage - 2; $i <= $this->currentPage + 2; $i++) { ?>
+		<?php if($i > 0 && $i <= $this->nbPage) { ?>
+			<?php if ($i != $this->currentPage) { ?>
+			<?php $params[$getteur] = $i; ?>
+			<li class="item pager-item"><a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo $i; ?></a></li>
+			<?php } else { ?>
+			<li class="item pager-current"><?php echo $i; ?></li>
+			<?php } ?>
+		<?php } ?>
+	<?php } ?>
+
+	<?php $params[$getteur] = $this->currentPage + 1; ?>
+	<li class="item pager-next">
+		<?php if ($this->currentPage < $this->nbPage) { ?>
+		<a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>">Suivant ›</a>
+		<?php } ?>
+	</li>
+	<?php $params[$getteur] = $this->nbPage; ?>
+	<li class="item pager-last">
+		<?php if ($this->currentPage < $this->nbPage) { ?>
+		<a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>">Fin »</a>
+		<?php } ?>
+	</li>
+</ul>
+<?php } ?>

+ 131 - 0
app/views/helpers/normal_view.phtml

@@ -0,0 +1,131 @@
+<?php
+
+$this->partial ('aside_flux');
+$this->partial ('nav_menu');
+
+if (isset ($this->entryPaginator) && !$this->entryPaginator->isEmpty ()) {
+	$items = $this->entryPaginator->items ();
+?>
+
+<div id="stream" class="normal">
+	<?php
+		$display_today = true;
+		$display_yesterday = true;
+		$display_others = true;
+	?>
+	<?php foreach ($items as $item) { ?>
+
+	<?php if ($display_today && $item->isDay (Days::TODAY)) { ?>
+	<div class="day"><?php echo Translate::t ('today'); ?> - <?php echo timestamptodate (time (), false); ?></div>
+	<?php $display_today = false; } ?>
+	<?php if ($display_yesterday && $item->isDay (Days::YESTERDAY)) { ?>
+	<div class="day"><?php echo Translate::t ('yesterday'); ?> - <?php echo timestamptodate (time () - 86400, false); ?></div>
+	<?php $display_yesterday = false; } ?>
+	<?php if ($display_others && $item->isDay (Days::BEFORE_YESTERDAY)) { ?>
+	<div class="day"><?php echo Translate::t ('before_yesterday'); ?></div>
+	<?php $display_others = false; } ?>
+
+	<div class="flux<?php echo !$item->isRead () ? ' not_read' : ''; ?><?php echo $item->isFavorite () ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id (); ?>">
+		<ul class="horizontal-list flux_header">
+			<?php if (!login_is_conf ($this->conf) || is_logged ()) { ?>
+			<li class="item manage">
+				<?php if (!$item->isRead ()) { ?>
+				<a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 1); ?>">&nbsp;</a>
+				<?php } else { ?>
+				<a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 0); ?>">&nbsp;</a>
+				<?php } ?>
+
+				<?php if (!$item->isFavorite ()) { ?>
+				<a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 1); ?>">&nbsp;</a>
+				<?php } else { ?>
+				<a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 0); ?>">&nbsp;</a>
+				<?php } ?>
+			</li>
+			<?php } ?>
+			<?php $feed = $item->feed (true); ?>
+			<li class="item website"><a href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id ()); ?>"><img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" /> <span><?php echo $feed->name (); ?></span></a></li>
+			<li class="item title"><?php echo $item->title (); ?></li>
+			<li class="item date"><?php echo $item->date (); ?></li>
+			<li class="item link"><a target="_blank" href="<?php echo $item->link (); ?>">&nbsp;</a></li>
+		</ul>
+
+		<div class="flux_content">
+			<div class="content">
+				<h1 class="title"><?php echo $item->title (); ?></h1>
+				<?php $author = $item->author (); ?>
+				<?php echo $author != '' ? '<div class="author">' . Translate::t ('by_author', $author) . '</div>' : ''; ?>
+				<?php
+					if($this->conf->lazyload() == 'yes') {
+						echo lazyimg($item->content ());
+					} else {
+						echo $item->content();
+					}
+				?>
+			</div>
+
+			<ul class="horizontal-list bottom">
+				<?php if (!login_is_conf ($this->conf) || is_logged ()) { ?>
+				<li class="item manage">
+					<?php if (!$item->isRead ()) { ?>
+					<a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 1); ?>">&nbsp;</a>
+					<?php } else { ?>
+					<a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 0); ?>">&nbsp;</a>
+					<?php } ?>
+
+					<?php if (!$item->isFavorite ()) { ?>
+					<a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 1); ?>">&nbsp;</a>
+					<?php } else { ?>
+					<a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 0); ?>">&nbsp;</a>
+					<?php } ?>
+				</li>
+				<?php } ?>
+				<li class="item">
+					<div class="dropdown">
+						<div id="dropdown-share-<?php echo $item->id ();?>" class="dropdown-target"></div>
+						<i class="icon i_share"></i> <a class="dropdown-toggle" href="#dropdown-share-<?php echo $item->id ();?>"><?php echo Translate::t ('share'); ?></a>
+
+						<ul class="dropdown-menu">
+							<li class="dropdown-close"><a href="#close"><i class="icon i_close"></i></a></li>
+
+							<li class="item"><a href="mailto:?subject=<?php echo $item->title (); ?>&amp;body=J'ai trouvé cet article intéressant, tu peux le lire à cette adresse : <?php echo urlencode($item->link ()); ?>"><?php echo Translate::t ('by_email'); ?></a></li>
+							<?php
+							$shaarli = $this->conf->urlShaarli ();
+							if ($shaarli) {
+							?>
+							<li class="item"><a target="_blank" href="<?php echo $shaarli . '?post=' . urlencode($item->link ()) . '&amp;title=' . urlencode ($item->title ()) . '&amp;source=bookmarklet'; ?>"><?php echo Translate::t ('on_shaarli'); ?></a></li>
+							<?php } ?>
+						</ul>
+					</div>
+				</li>
+				<?php $tags = $item->tags(); ?>
+				<?php if(!empty($tags)) { ?>
+				<li class="item">
+					<div class="dropdown">
+						<div id="dropdown-tags-<?php echo $item->id ();?>" class="dropdown-target"></div>
+						<i class="icon i_tag"></i> <a class="dropdown-toggle" href="#dropdown-tags-<?php echo $item->id ();?>"><?php echo Translate::t ('related_tags'); ?></a>
+
+						<ul class="dropdown-menu">
+							<li class="dropdown-close"><a href="#close"><i class="icon i_close"></i></a></li>
+
+							<?php foreach($tags as $tag) { ?>
+							<li class="item"><a href="<?php echo _url ('index', 'index', 'search', urlencode ('#' . $tag)); ?>"><?php echo $tag; ?></a></li>
+							<?php } ?>
+						</ul>
+					</div>
+				</li>
+				<?php } ?>
+			</ul>
+		</div>
+	</div>
+	<?php } ?>
+	
+	<?php $this->entryPaginator->render ('pagination.phtml', 'next'); ?>
+</div>
+
+<?php $this->partial ('nav_entries'); ?>
+
+<?php } else { ?>
+<div class="alert alert-warn">
+	<span class="alert-head"><?php echo Translate::t ('no_feed_to_display'); ?></span>
+</div>
+<?php } ?>

+ 1 - 1
app/views/helpers/pagination.phtml

@@ -8,7 +8,7 @@
 	<li class="item pager-next">
 	<?php if ($this->next != '') { ?>
 	<?php $params[$getteur] = $this->next; ?>
-	<a href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Translate::t ('load_more'); ?></a>
+	<a id="load_more" href="<?php echo Url::display (array ('c' => $c, 'a' => $a, 'params' => $params)); ?>"><?php echo Translate::t ('load_more'); ?></a>
 	<?php } else { ?>
 	<?php echo Translate::t ('nothing_to_load'); ?>
 	<?php } ?>

+ 45 - 0
app/views/helpers/reader_view.phtml

@@ -0,0 +1,45 @@
+<?php
+$this->partial ('nav_menu');
+
+if (isset ($this->entryPaginator) && !$this->entryPaginator->isEmpty ()) {
+	$items = $this->entryPaginator->items ();
+?>
+
+<div id="stream" class="reader">
+	<?php foreach ($items as $item) { ?>
+
+	<div class="flux<?php echo !$item->isRead () ? ' not_read' : ''; ?><?php echo $item->isFavorite () ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id (); ?>">
+		<div class="flux_content">
+			<div class="content">
+				<?php $feed = $item->feed (true); ?>
+				<a href="<?php echo $item->link (); ?>">
+					<img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" /> <span><?php echo $feed->name (); ?></span>
+				</a>
+				<h1 class="title"><?php echo $item->title (); ?></h1>
+
+				<div class="author">
+					<?php $author = $item->author (); ?>
+					<?php echo $author != '' ? Translate::t ('by_author', $author) . ' - ' : ''; ?>
+					<?php echo $item->date (); ?>
+				</div>
+
+				<?php
+					if($this->conf->lazyload() == 'yes') {
+						echo lazyimg($item->content ());
+					} else {
+						echo $item->content();
+					}
+				?>
+			</div>
+		</div>
+	</div>
+	<?php } ?>
+	
+	<?php $this->entryPaginator->render ('pagination.phtml', 'next'); ?>
+</div>
+
+<?php } else { ?>
+<div class="alert alert-warn">
+	<span class="alert-head"><?php echo Translate::t ('no_feed_to_display'); ?></span>
+</div>
+<?php } ?>

+ 0 - 0
app/views/helpers/rss.phtml → app/views/helpers/rss_view.phtml


+ 8 - 115
app/views/index/index.phtml

@@ -1,120 +1,13 @@
 <?php
+
 $output = Request::param ('output', 'normal');
 
 if ($output == 'rss') {
-	$this->renderHelper ('rss');
+	$this->renderHelper ('rss_view');
+} elseif($output == 'reader') {
+	$this->renderHelper ('reader_view');
+} elseif($output == 'global') {
+	$this->renderHelper ('global_view');
 } else {
-	$this->partial ('aside_flux');
-	$this->partial ('nav_menu');
-
-	if (isset ($this->entryPaginator) && !$this->entryPaginator->isEmpty ()) {
-		$items = $this->entryPaginator->items ();
-?>
-
-<div id="stream">
-	<?php
-		$display_today = true;
-		$display_yesterday = true;
-		$display_others = true;
-	?>
-	<?php foreach ($items as $item) { ?>
-
-	<?php if ($display_today && $item->isDay (Days::TODAY)) { ?>
-	<div class="day"><?php echo Translate::t ('today'); ?> - <?php echo timestamptodate (time (), false); ?></div>
-	<?php $display_today = false; } ?>
-	<?php if ($display_yesterday && $item->isDay (Days::YESTERDAY)) { ?>
-	<div class="day"><?php echo Translate::t ('yesterday'); ?> - <?php echo timestamptodate (time () - 86400, false); ?></div>
-	<?php $display_yesterday = false; } ?>
-	<?php if ($display_others && $item->isDay (Days::BEFORE_YESTERDAY)) { ?>
-	<div class="day"><?php echo Translate::t ('before_yesterday'); ?></div>
-	<?php $display_others = false; } ?>
-
-	<div class="flux<?php echo !$item->isRead () ? ' not_read' : ''; ?><?php echo $item->isFavorite () ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id (); ?>">
-		<ul class="horizontal-list flux_header">
-			<?php if (!login_is_conf ($this->conf) || is_logged ()) { ?>
-			<li class="item manage">
-				<?php if (!$item->isRead ()) { ?>
-				<a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 1); ?>">&nbsp;</a>
-				<?php } else { ?>
-				<a class="read" href="<?php echo _url ('entry', 'read', 'id', $item->id (), 'is_read', 0); ?>">&nbsp;</a>
-				<?php } ?>
-
-				<?php if (!$item->isFavorite ()) { ?>
-				<a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 1); ?>">&nbsp;</a>
-				<?php } else { ?>
-				<a class="bookmark" href="<?php echo _url ('entry', 'bookmark', 'id', $item->id (), 'is_favorite', 0); ?>">&nbsp;</a>
-				<?php } ?>
-			</li>
-			<?php } ?>
-			<?php $feed = $item->feed (true); ?>
-			<li class="item website"><a href="<?php echo _url ('index', 'index', 'get', 'f_' . $feed->id ()); ?>"><img class="favicon" src="<?php echo $feed->favicon (); ?>" alt="" /> <span><?php echo $feed->name (); ?></span></a></li>
-			<li class="item title"><?php echo $item->title (); ?></li>
-			<li class="item date"><?php echo $item->date (); ?></li>
-			<li class="item link"><a target="_blank" href="<?php echo $item->link (); ?>">&nbsp;</a></li>
-		</ul>
-
-		<div class="flux_content">
-			<div class="content">
-				<h1 class="title"><?php echo $item->title (); ?></h1>
-				<?php $author = $item->author (); ?>
-				<?php echo $author != '' ? '<div class="author">' . Translate::t ('by_author', $author) . '</div>' : ''; ?>
-				<?php echo $item->content (); ?>
-			</div>
-
-			<ul class="horizontal-list bottom">
-				<li class="item">
-					<?php if ($item->notes () != '') { ?>
-					<i class="icon i_note"></i> <a class="note" href="<?php echo _url ('entry', 'note', 'id', $item->id ()); ?>"><?php echo Translate::t ('update_note'); ?></a>
-					<?php } else { ?>
-					<i class="icon i_note_empty"></i> <a class="note" href="<?php echo _url ('entry', 'note', 'id', $item->id ()); ?>"><?php echo Translate::t ('add_note'); ?></a>
-					<?php } ?>
-				</li>
-				<li class="item">
-					<div class="dropdown">
-						<div id="dropdown-share-<?php echo $item->id ();?>" class="dropdown-target"></div>
-						<i class="icon i_share"></i> <a class="dropdown-toggle" href="#dropdown-share-<?php echo $item->id ();?>"><?php echo Translate::t ('share'); ?></a>
-
-						<ul class="dropdown-menu">
-							<li class="dropdown-close"><a href="#close"><i class="icon i_close"></i></a></li>
-
-							<li class="item"><a href="mailto:?subject=<?php echo $item->title (); ?>&amp;body=J'ai trouvé cet article intéressant, tu peux le lire à cette adresse : <?php echo urlencode($item->link ()); ?>"><?php echo Translate::t ('by_email'); ?></a></li>
-							<?php
-							$shaarli = $this->conf->urlShaarli ();
-							if ($shaarli) {
-							?>
-							<li class="item"><a target="_blank" href="<?php echo $shaarli . '?post=' . urlencode($item->link ()) . '&amp;title=' . urlencode ($item->title ()) . '&amp;source=bookmarklet'; ?>"><?php echo Translate::t ('on_shaarli'); ?></a></li>
-							<?php } ?>
-						</ul>
-					</div>
-				</li>
-				<?php $tags = $item->tags(); ?>
-				<?php if(!empty($tags)) { ?>
-				<li class="item">
-					<div class="dropdown">
-						<div id="dropdown-tags-<?php echo $item->id ();?>" class="dropdown-target"></div>
-						<i class="icon i_tag"></i> <a class="dropdown-toggle" href="#dropdown-tags-<?php echo $item->id ();?>"><?php echo Translate::t ('related_tags'); ?></a>
-
-						<ul class="dropdown-menu">
-							<li class="dropdown-close"><a href="#close"><i class="icon i_close"></i></a></li>
-
-							<?php foreach($tags as $tag) { ?>
-							<li class="item"><span><?php echo $tag; ?></span></li>
-							<?php } ?>
-						</ul>
-					</div>
-				</li>
-				<?php } ?>
-			</ul>
-		</div>
-	</div>
-	<?php } ?>
-	
-	<?php $this->entryPaginator->render ('pagination.phtml', 'next'); ?>
-</div>
-
-	<?php } else { ?>
-<div class="alert alert-warn">
-	<span class="alert-head"><?php echo Translate::t ('no_feed_to_display'); ?></span>
-</div>
-	<?php } ?>
-<?php } ?>
+	$this->renderHelper ('normal_view');
+}

+ 21 - 0
app/views/index/logs.phtml

@@ -0,0 +1,21 @@
+<div class="post content">
+	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Translate::t ('back_to_rss_feeds'); ?></a>
+
+	<h1><?php echo Translate::t ('logs'); ?></h1>
+
+	<?php $items = $this->logsPaginator->items (); ?>
+
+	<?php if (!empty ($items)) { ?>
+	<div class="logs">
+		<?php $this->logsPaginator->render ('logs_pagination.phtml', 'page'); ?>
+		
+		<?php foreach ($items as $log) { ?>
+		<div class="log <?php echo $log->level (); ?>"><span class="date"><?php echo date ('d/m/Y - H:i:s', strtotime ($log->date ())); ?></span><?php echo $log->info (); ?></div>
+		<?php } ?>
+		
+		<?php $this->logsPaginator->render ('logs_pagination.phtml','page'); ?>
+	</div>
+	<?php } else { ?>
+	<p class="alert alert-warn"><?php echo Translate::t ('logs_empty'); ?></p>
+	<?php } ?>
+</div>

+ 104 - 53
app/views/javascript/main.phtml

@@ -9,6 +9,16 @@ var hide_posts = false;
 	$mark = $this->conf->markWhen ();
 ?>
 
+function is_reader_mode() {
+	var stream = $("#stream.reader");
+	return stream.html() != null;
+}
+
+function is_normal_mode() {
+	var stream = $("#stream.normal");
+	return stream.html() != null;
+}
+
 function redirect (url, new_tab) {
 	if (url) {
 		if (new_tab) {
@@ -42,19 +52,20 @@ function toggleContent (new_active, old_active) {
 	<?php } ?>
 }
 
-var load = false;
 function mark_read (active, only_not_read) {
 	if (active[0] === undefined || (
-	    only_not_read === true && !active.hasClass("not_read")) ||
-	    load === true) {
+	    only_not_read === true && !active.hasClass("not_read"))) {
 		return false;
 	}
 
-	load = true;
+	if (active.hasClass ("not_read")) {
+		active.removeClass ("not_read");
+	} else {
+		active.addClass ("not_read");
+	}
 
 	url =  active.find ("a.read").attr ("href");
 	if (url === undefined) {
-		load = false;
 		return false;
 	}
 
@@ -66,27 +77,16 @@ function mark_read (active, only_not_read) {
 		res = jQuery.parseJSON(data);
 
 		active.find ("a.read").attr ("href", res.url);
-		if (active.hasClass ("not_read")) {
-			active.removeClass ("not_read");
-		} else {
-			active.addClass ("not_read");
-		}
-
-		load = false;
 	});
 }
 
 function mark_favorite (active) {
-	if (active[0] === undefined ||
-	    load === true) {
+	if (active[0] === undefined) {
 		return false;
 	}
 
-	load = true;
-
 	url =  active.find ("a.bookmark").attr ("href");
 	if (url === undefined) {
-		load = false;
 		return false;
 	}
 
@@ -103,34 +103,69 @@ function mark_favorite (active) {
 		} else {
 			active.addClass ("favorite");
 		}
-
-		load = false;
 	});
 }
 
+function prev_entry() {
+	old_active = $(".flux.active");
+	last_active = $(".flux:last");
+	new_active = old_active.prevAll (".flux:first");
+
+	if (new_active.hasClass("flux")) {
+		toggleContent (new_active, old_active);
+	} else if (old_active[0] === undefined &&
+	           new_active[0] === undefined) {
+		toggleContent (last_active, old_active);
+	}
+}
+
+function next_entry() {
+	old_active = $(".flux.active");
+	first_active = $(".flux:first");
+	new_active = old_active.nextAll (".flux:first");
+
+	if (new_active.hasClass("flux")) {
+		toggleContent (new_active, old_active);
+	} else if (old_active[0] === undefined &&
+	           new_active[0] === undefined) {
+		toggleContent (first_active, old_active);
+	}
+}
+
 function init_img () {
-	$(".flux .content img").each (function () {
-		if ($(this).width () > ($("#stream .content").width()) / 2) {
+	$(".flux_content .content img").each (function () {
+		if ($(this).width () > ($(".flux_content .content").width()) / 2) {
 			$(this).addClass("big");
 		}
 	});
 }
 
-function init_posts () {
-	<?php if ($mark['page'] == 'yes') { ?>
-	if ($(".flux.not_read")[0] != undefined) {
-		url = $(".nav_menu a.read_all").attr ("href");
-		redirect (url, false);
-	}
-	<?php } ?>
+function inMarkViewport(flux) {
+	var top = flux.position().top;
+	var height = flux.height();
+	var begin = top + 3 * height / 4;
+	var bot = top + height;
 
+	var windowTop = $(window).scrollTop();
+	var windowBot = windowTop + $(window).height();
+
+	return (windowBot >= begin && windowBot <= bot);
+}
+
+var lastScroll = 0;
+function init_posts () {
 	init_img ();
+	<?php if($this->conf->lazyload() == 'yes') { ?>
+	$(".flux .content img").lazyload();
+	<?php } ?>
 
 	if (hide_posts) {
 		$(".flux:not(.active) .flux_content").hide ();
 	}
 
-	$(".flux_header .item.title, .flux_header .item.date").click (function () {
+	var flux_header_toggle = $(".flux_header .item.title, .flux_header .item.date");
+	flux_header_toggle.unbind('click'); // évite d'associer 2 fois le toggle
+	flux_header_toggle.click (function () {
 		old_active = $(".flux.active");
 		new_active = $(this).parent ().parent ();
 
@@ -160,9 +195,32 @@ function init_posts () {
 		mark_read($(this).parent().parent().parent(), true);
 	});
 	<?php } ?>
+
+	<?php if ($mark['scroll'] == 'yes') { ?>
+	var flux = $('.flux');
+	$(window).scroll(function() {
+		var windowTop = $(this).scrollTop();
+		if(Math.abs(windowTop - lastScroll) <= 50) {
+			return;
+		}
+		lastScroll = windowTop;
+
+		flux.each(function() {
+			if($(this).hasClass('not_read') &&
+			   $(this).children(".flux_content").is(':visible') &&
+			   inMarkViewport($(this))) {
+				mark_read($(this), true);
+			}
+		});
+	});
+	<?php } ?>
 }
 
 function init_column_categories () {
+	if(!is_normal_mode()) {
+		return;
+	}
+
 	$(".category").addClass ("stick");
 	$(".categories .category .btn:first-child").width ("160px");
 	$(".category").append ("<a class=\"btn dropdown-toggle\" href=\"#\"><i class=\"icon i_down\"></i></a>");
@@ -202,18 +260,7 @@ function init_shortcuts () {
 	});
 
 	// Touches de navigation
-	shortcut.add("<?php echo $s['prev_entry']; ?>", function () {
-		old_active = $(".flux.active");
-		last_active = $(".flux:last");
-		new_active = old_active.prevAll (".flux:first");
-
-		if (new_active.hasClass("flux")) {
-			toggleContent (new_active, old_active);
-		} else if (old_active[0] === undefined &&
-		           new_active[0] === undefined) {
-			toggleContent (last_active, old_active);
-		}
-	}, {
+	shortcut.add("<?php echo $s['prev_entry']; ?>", prev_entry, {
 		'disable_in_input':true
 	});
 	shortcut.add("shift+<?php echo $s['prev_entry']; ?>", function () {
@@ -226,18 +273,7 @@ function init_shortcuts () {
 	}, {
 		'disable_in_input':true
 	});
-	shortcut.add("<?php echo $s['next_entry']; ?>", function () {
-		old_active = $(".flux.active");
-		first_active = $(".flux:first");
-		new_active = old_active.nextAll (".flux:first");
-
-		if (new_active.hasClass("flux")) {
-			toggleContent (new_active, old_active);
-		} else if (old_active[0] === undefined &&
-		           new_active[0] === undefined) {
-			toggleContent (first_active, old_active);
-		}
-	}, {
+	shortcut.add("<?php echo $s['next_entry']; ?>", next_entry, {
 		'disable_in_input':true
 	});
 	shortcut.add("shift+<?php echo $s['next_entry']; ?>", function () {
@@ -277,8 +313,23 @@ function init_shortcuts () {
 	});
 }
 
+function init_nav_entries() {
+	$('.nav_entries a.previous_entry').click(function() {
+		prev_entry();
+		return false;
+	});
+	$('.nav_entries a.next_entry').click(function() {
+		next_entry();
+		return false;
+	});
+}
+
 $(document).ready (function () {
+	if(is_reader_mode()) {
+		hide_posts = false;
+	}
 	init_posts ();
 	init_column_categories ();
 	init_shortcuts ();
+	init_nav_entries();
 });

+ 0 - 50
build.sh

@@ -1,50 +0,0 @@
-#!/bin/bash
-
-MINZ_REPO_URL="https://github.com/marienfressinaud/MINZ.git"
-MINZ_CLONE_PATH="./minz_tmp"
-LIB_MINZ_PATH="./minz_tmp/lib/*"
-LIB_PATH="./lib/minz"
-LOG_PATH="./log"
-CACHE_PATH="./cache"
-
-git_check() {
-	printf "Vérification de la présence de git... "
-
-	EXE_PATH=$(which "git" 2>/dev/null)
-	if [ $? -ne 0 ]; then
-		printf "git n'est pas présent sur votre système. Veuillez l'installer avant de continuer\n";
-		exit 1
-	else
-		printf "git a été trouvé\n"
-	fi
-}
-
-dir_check() {
-	test -d $LOG_PATH
-	if [ $? -ne 0 ]; then
-		mkdir $LOG_PATH
-	fi
-
-	test -d $CACHE_PATH
-	if [ $? -ne 0 ]; then
-		mkdir $CACHE_PATH
-	fi
-}
-
-clone_minz() {
-	printf "Récupération de Minz...\n"
-
-	git clone $MINZ_REPO_URL $MINZ_CLONE_PATH
-	test -d $LIB_PATH
-	if [ $? -ne 0 ]; then
-		mkdir -p $LIB_PATH
-	fi
-	mv $LIB_MINZ_PATH $LIB_PATH
-	rm -rf $MINZ_CLONE_PATH
-
-	printf "Récupération de Minz terminée...\n"
-}
-
-git_check
-dir_check
-clone_minz

+ 6 - 3
lib/lib_rss.php

@@ -65,9 +65,12 @@ function opml_import ($xml) {
 	$opml = @simplexml_load_string ($xml);
 
 	if (!$opml) {
-		return array (array (), array ());
+		throw new OpmlException ();
 	}
 
+	$catDAO = new CategoryDAO();
+	$defCat = $catDAO->getDefault();
+
 	$categories = array ();
 	$feeds = array ();
 
@@ -99,8 +102,8 @@ function opml_import ($xml) {
 				$feeds = array_merge ($feeds, getFeedsOutline ($outline, $cat->id ()));
 			}
 		} else {
-			// Flux rss
-			$feeds[] = getFeed ($outline, '');
+			// Flux rss sans catégorie, on récupère l'ajoute dans la catégorie par défaut
+			$feeds[] = getFeed ($outline, $defCat->id());
 		}
 	}
 

+ 8 - 0
lib/lib_text.php

@@ -86,3 +86,11 @@ function parse_tags ($desc) {
 
 	return $desc_parse;
 }
+
+function lazyimg($content) {
+	return preg_replace(
+		'/<img([^<]+)src=([\'"])([^"\']*)([\'"])([^<]*)>/i',
+		'<img$1src="' . Url::display('/data/grey.gif') . '" data-original="$3"$5>',
+		$content
+	);
+}

+ 42 - 0
lib/minz/ActionController.php

@@ -0,0 +1,42 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * La classe ActionController représente le contrôleur de l'application
+ */
+class ActionController {
+	protected $router;
+	protected $view;
+
+	/**
+	 * Constructeur
+	 * @param $controller nom du controller
+	 * @param $action nom de l'action à lancer
+	 */
+	public function __construct ($router) {
+		$this->router = $router;
+		$this->view = new View ();
+		$this->view->attributeParams ();
+	}
+
+	/**
+	 * Getteur
+	 */
+	public function view () {
+		return $this->view;
+	}
+	
+	/**
+	 * Méthodes à redéfinir (ou non) par héritage
+	 * firstAction est la première méthode exécutée par le Dispatcher
+	 * lastAction est la dernière
+	 */
+	public function init () { }
+	public function firstAction () { }
+	public function lastAction () { }
+}
+
+

+ 116 - 0
lib/minz/Cache.php

@@ -0,0 +1,116 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * La classe Cache permet de gérer facilement les pages en cache
+ */
+class Cache {
+	/**
+	 * $expire timestamp auquel expire le cache de $url
+	 */
+	private $expire = 0;
+
+	/**
+	 * $file est le nom du fichier de cache
+	 */
+	private $file = '';
+
+	/**
+	 * $enabled permet de déterminer si le cache est activé
+	 */
+	private static $enabled = true;
+
+	/**
+	 * Constructeur
+	 */
+	public function __construct () {
+		$this->_fileName ();
+		$this->_expire ();
+	}
+
+	/**
+	 * Setteurs
+	 */
+	public function _fileName () {
+		$file = md5 (Request::getURI ());
+
+		$this->file = CACHE_PATH . '/'.$file;
+	}
+
+	public function _expire () {
+		if ($this->exist ()) {
+			$this->expire = filemtime ($this->file)
+			              + Configuration::delayCache ();
+		}
+	}
+
+	/**
+	 * Permet de savoir si le cache est activé
+	 * @return true si activé, false sinon
+	 */
+	public static function isEnabled () {
+		return Configuration::cacheEnabled () && self::$enabled;
+	}
+
+	/**
+	 * Active / désactive le cache
+	 */
+	public static function switchOn () {
+		self::$enabled = true;
+	}
+	public static function switchOff () {
+		self::$enabled = false;
+	}
+
+	/**
+	 * Détermine si le cache de $url a expiré ou non
+	 * @return true si il a expiré, false sinon
+	 */
+	public function expired () {
+		return time () > $this->expire;
+	}
+
+	/**
+	 * Affiche le contenu du cache
+	 * @print le code html du cache
+	 */
+	public function render () {
+		if ($this->exist ()) {
+			include ($this->file);
+		}
+	}
+
+	/**
+	 * Enregistre $html en cache
+	 * @param $html le html à mettre en cache
+	 */
+	public function cache ($html) {
+		file_put_contents ($this->file, $html);
+	}
+
+	/**
+	 * Permet de savoir si le cache existe
+	 * @return true si il existe, false sinon
+	 */
+	public function exist () {
+		return file_exists ($this->file);
+	}
+
+	/**
+	 * Nettoie le cache en supprimant tous les fichiers
+	 */
+	public static function clean () {
+		$files = opendir (CACHE_PATH);
+
+		while ($fic = readdir ($files)) {
+			if ($fic != '.' && $fic != '..') {
+				unlink (CACHE_PATH.'/'.$fic);
+			}
+		}
+
+		closedir ($files);
+	}
+}

+ 240 - 0
lib/minz/Configuration.php

@@ -0,0 +1,240 @@
+<?php
+/**
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * La classe Configuration permet de gérer la configuration de l'application
+ */
+class Configuration {
+	const CONF_PATH_NAME = '/configuration/application.ini';
+
+	/**
+	 * VERSION est la version actuelle de MINZ
+	 */
+	const VERSION = '1.3.1';
+
+	/**
+	 * valeurs possibles pour l'"environment"
+	 * SILENT rend l'application muette (pas de log)
+	 * PRODUCTION est recommandée pour une appli en production
+	 *			(log les erreurs critiques)
+	 * DEVELOPMENT log toutes les erreurs
+	 */
+	const SILENT = 0;
+	const PRODUCTION = 1;
+	const DEVELOPMENT = 2;
+
+	/**
+	 * définition des variables de configuration
+	 * $sel_application une chaîne de caractères aléatoires (obligatoire)
+	 * $environment gère le niveau d'affichage pour log et erreurs
+	 * $use_url_rewriting indique si on utilise l'url_rewriting
+	 * $base_url le chemin de base pour accéder à l'application
+	 * $title le nom de l'application
+	 * $language la langue par défaut de l'application
+	 * $cacheEnabled permet de savoir si le cache doit être activé
+	 * $delayCache la limite de cache
+	 * $db paramètres pour la base de données (tableau)
+	 *     - host le serveur de la base
+	 *     - user nom d'utilisateur
+	 *     - password mot de passe de l'utilisateur
+	 *     - base le nom de la base de données
+	 */
+	private static $sel_application = '';
+	private static $environment = Configuration::PRODUCTION;
+	private static $base_url = '';
+	private static $use_url_rewriting = false;
+	private static $title = '';
+	private static $language = 'en';
+	private static $cache_enabled = false;
+	private static $delay_cache = 3600;
+
+	private static $db = array (
+		'host' => false,
+		'user' => false,
+		'password' => false,
+		'base' => false
+	);
+
+	/*
+	 * Getteurs
+	 */
+	public static function selApplication () {
+		return self::$sel_application;
+	}
+	public static function environment () {
+		return self::$environment;
+	}
+	public static function baseUrl () {
+		return self::$base_url;
+	}
+	public static function useUrlRewriting () {
+		return self::$use_url_rewriting;
+	}
+	public static function title () {
+		return self::$title;
+	}
+	public static function language () {
+		return self::$language;
+	}
+	public static function cacheEnabled () {
+		return self::$cache_enabled;
+	}
+	public static function delayCache () {
+		return self::$delay_cache;
+	}
+	public static function dataBase () {
+		return self::$db;
+	}
+
+	/**
+	 * Initialise les variables de configuration
+	 * @exception FileNotExistException si le CONF_PATH_NAME n'existe pas
+	 * @exception BadConfigurationException si CONF_PATH_NAME mal formaté
+	 */
+	public static function init () {
+		try {
+			self::parseFile ();
+			self::setReporting ();
+		} catch (BadConfigurationException $e) {
+			throw $e;
+		} catch (FileNotExistException $e) {
+			throw $e;
+		}
+	}
+
+	/**
+	 * Parse un fichier de configuration de type ".ini"
+	 * @exception FileNotExistException si le CONF_PATH_NAME n'existe pas
+	 * @exception BadConfigurationException si CONF_PATH_NAME mal formaté
+	 */
+	private static function parseFile () {
+		if (!file_exists (APP_PATH . self::CONF_PATH_NAME)) {
+			throw new FileNotExistException (
+				APP_PATH . self::CONF_PATH_NAME,
+				MinzException::ERROR
+			);
+		}
+		$ini_array = parse_ini_file (
+			APP_PATH . self::CONF_PATH_NAME,
+			true
+		);
+
+		// [general] est obligatoire
+		if (!isset ($ini_array['general'])) {
+			throw new BadConfigurationException (
+				'[general]',
+				MinzException::ERROR
+			);
+		}
+		$general = $ini_array['general'];
+
+
+		// sel_application est obligatoire
+		if (!isset ($general['sel_application'])) {
+			throw new BadConfigurationException (
+				'sel_application',
+				MinzException::ERROR
+			);
+		}
+		self::$sel_application = $general['sel_application'];
+
+		if (isset ($general['environment'])) {
+			switch ($general['environment']) {
+			case 'silent':
+				self::$environment = Configuration::SILENT;
+				break;
+			case 'development':
+				self::$environment = Configuration::DEVELOPMENT;
+				break;
+			case 'production':
+				self::$environment = Configuration::PRODUCTION;
+				break;
+			default:
+				throw new BadConfigurationException (
+					'environment',
+					MinzException::ERROR
+				);
+			}
+
+		}
+		if (isset ($general['base_url'])) {
+			self::$base_url = $general['base_url'];
+		}
+		if (isset ($general['use_url_rewriting'])) {
+			self::$use_url_rewriting = $general['use_url_rewriting'];
+		}
+
+		if (isset ($general['title'])) {
+			self::$title = $general['title'];
+		}
+		if (isset ($general['language'])) {
+			self::$language = $general['language'];
+		}
+		if (isset ($general['cache_enabled'])) {
+			self::$cache_enabled = $general['cache_enabled'];
+			if (CACHE_PATH === false && self::$cache_enabled) {
+				throw new FileNotExistException (
+					'CACHE_PATH',
+					MinzException::ERROR
+				);
+			}
+		}
+		if (isset ($general['delay_cache'])) {
+			self::$delay_cache = $general['delay_cache'];
+		}
+
+		// Base de données
+		$db = false;
+		if (isset ($ini_array['db'])) {
+			$db = $ini_array['db'];
+		}
+		if ($db) {
+			if (!isset ($db['host'])) {
+				throw new BadConfigurationException (
+					'host',
+					MinzException::ERROR
+				);
+			}
+			if (!isset ($db['user'])) {
+				throw new BadConfigurationException (
+					'user',
+					MinzException::ERROR
+				);
+			}
+			if (!isset ($db['password'])) {
+				throw new BadConfigurationException (
+					'password',
+					MinzException::ERROR
+				);
+			}
+			if (!isset ($db['base'])) {
+				throw new BadConfigurationException (
+					'base',
+					MinzException::ERROR
+				);
+			}
+
+			self::$db['host'] = $db['host'];
+			self::$db['user'] = $db['user'];
+			self::$db['password'] = $db['password'];
+			self::$db['base'] = $db['base'];
+		}
+	}
+
+	private static function setReporting () {
+		if (self::environment () == self::DEVELOPMENT) {
+			error_reporting (E_ALL);
+			ini_set ('display_errors','On');
+			ini_set('log_errors', 'On');
+		} elseif (self::environment () == self::PRODUCTION) {
+			error_reporting(E_ALL);
+			ini_set('display_errors','Off');
+			ini_set('log_errors', 'On');
+		} else {
+			error_reporting(0);
+		}
+	}
+}

+ 152 - 0
lib/minz/Dispatcher.php

@@ -0,0 +1,152 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * Le Dispatcher s'occupe d'initialiser le Controller et d'executer l'action
+ * déterminée dans la Request
+ * C'est un singleton
+ */
+class Dispatcher {
+	const CONTROLLERS_PATH_NAME = '/controllers';
+
+	/* singleton */
+	private static $instance = null;
+
+	private $router;
+	private $controller;
+
+	/**
+	 * Récupère l'instance du Dispatcher
+	 */
+	public static function getInstance ($router) {
+		if (is_null (self::$instance)) {
+			self::$instance = new Dispatcher ($router);
+		}
+		return self::$instance;
+	}
+
+	/**
+	 * Constructeur
+	 */
+	private function __construct ($router) {
+		$this->router = $router;
+	}
+
+	/**
+	 * Lance le controller indiqué dans Request
+	 * Remplit le body de Response à partir de la Vue
+	 * @exception MinzException
+	 */
+	public function run () {
+		$cache = new Cache();
+		// Le ob_start est dupliqué : sans ça il y a un bug sous Firefox
+		// ici on l'appelle avec 'ob_gzhandler', après sans.
+		// Vraisemblablement la compression fonctionne mais c'est sale
+		// J'ignore les effets de bord :(
+		ob_start ('ob_gzhandler');
+
+		if (Cache::isEnabled () && !$cache->expired ()) {
+			ob_start ();
+			$cache->render ();
+			$text = ob_get_clean();
+		} else {
+			while (Request::$reseted) {
+				Request::$reseted = false;
+
+				try {
+					$this->createController (
+						Request::controllerName ()
+						. 'Controller'
+					);
+
+					$this->controller->init ();
+					$this->controller->firstAction ();
+					$this->launchAction (
+						Request::actionName ()
+						. 'Action'
+					);
+					$this->controller->lastAction ();
+
+					if (!Request::$reseted) {
+						ob_start ();
+						$this->controller->view ()->build ();
+						$text = ob_get_clean();
+					}
+				} catch (MinzException $e) {
+					throw $e;
+				}
+			}
+
+			if (Cache::isEnabled ()) {
+				$cache->cache ($text);
+			}
+		}
+
+		Response::setBody ($text);
+	}
+
+	/**
+	 * Instancie le Controller
+	 * @param $controller_name le nom du controller à instancier
+	 * @exception FileNotExistException le fichier correspondant au
+	 *          > controller n'existe pas
+	 * @exception ControllerNotExistException le controller n'existe pas
+	 * @exception ControllerNotActionControllerException controller n'est
+	 *          > pas une instance de ActionController
+	 */
+	private function createController ($controller_name) {
+		$filename = APP_PATH . self::CONTROLLERS_PATH_NAME . '/'
+		          . $controller_name . '.php';
+
+		if (!file_exists ($filename)) {
+			throw new FileNotExistException (
+				$filename,
+				MinzException::ERROR
+			);
+		}
+		require_once ($filename);
+
+		if (!class_exists ($controller_name)) {
+			throw new ControllerNotExistException (
+				$controller_name,
+				MinzException::ERROR
+			);
+		}
+		$this->controller = new $controller_name ($this->router);
+
+		if (! ($this->controller instanceof ActionController)) {
+			throw new ControllerNotActionControllerException (
+				$controller_name,
+				MinzException::ERROR
+			);
+		}
+	}
+
+	/**
+	 * Lance l'action sur le controller du dispatcher
+	 * @param $action_name le nom de l'action
+	 * @exception ActionException si on ne peut pas exécuter l'action sur
+	 *          > le controller
+	 */
+	private function launchAction ($action_name) {
+		if (!Request::$reseted) {
+			if (!is_callable (array (
+				$this->controller,
+				$action_name
+			))) {
+				throw new ActionException (
+					get_class ($this->controller),
+					$action_name,
+					MinzException::ERROR
+				);
+			}
+			call_user_func (array (
+				$this->controller,
+				$action_name
+			));
+		}
+	}
+}

+ 94 - 0
lib/minz/Error.php

@@ -0,0 +1,94 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * La classe Error permet de lancer des erreurs HTTP
+ */
+class Error {
+	public function __construct () { }
+
+	/**
+	* Permet de lancer une erreur
+	* @param $code le type de l'erreur, par défaut 404 (page not found)
+	* @param $logs logs d'erreurs découpés de la forme
+	*      > $logs['error']
+	*      > $logs['warning']
+	*      > $logs['notice']
+	* @param $redirect indique s'il faut forcer la redirection (les logs ne seront pas transmis)
+	*/
+	public static function error ($code = 404, $logs = array (), $redirect = false) {
+		$logs = self::processLogs ($logs);
+		$error_filename = APP_PATH . '/controllers/errorController.php';
+		
+		if (file_exists ($error_filename)) {
+			$params = array (
+				'code' => $code,
+				'logs' => $logs
+			);
+			
+			Response::setHeader ($code);
+			if ($redirect) {
+				Request::forward (array (
+					'c' => 'error'
+				), true);
+			} else {
+				Request::forward (array (
+					'c' => 'error',
+					'params' => $params
+				), false);
+			}
+		} else {
+			$text = '<h1>An error occured</h1>'."\n";
+			
+			if (!empty ($logs)) {
+				$text .= '<ul>'."\n";
+				foreach ($logs as $log) {
+					$text .= '<li>' . $log . '</li>'."\n";
+				}
+				$text .= '</ul>'."\n";
+			}
+			
+			Response::setHeader ($code);
+			Response::setBody ($text);
+			Response::send ();
+			exit ();
+		}
+	}
+	
+	/**
+	 * Permet de retourner les logs de façon à n'avoir que
+	 * ceux que l'on veut réellement
+	 * @param $logs les logs rangés par catégories (error, warning, notice)
+	 * @return la liste des logs, sans catégorie,
+	 *       > en fonction de l'environment
+	 */
+	private static function processLogs ($logs) {
+		$env = Configuration::environment ();
+		$logs_ok = array ();
+		$error = array ();
+		$warning = array ();
+		$notice = array ();
+		
+		if (isset ($logs['error'])) {
+			$error = $logs['error'];
+		}
+		if (isset ($logs['warning'])) {
+			$warning = $logs['warning'];
+		}
+		if (isset ($logs['notice'])) {
+			$notice = $logs['notice'];
+		}
+		
+		if ($env == Configuration::PRODUCTION) {
+			$logs_ok = $error;
+		}
+		if ($env == Configuration::DEVELOPMENT) {
+			$logs_ok = array_merge ($error, $warning, $notice);
+		}
+		
+		return $logs_ok;
+	}
+}

+ 123 - 0
lib/minz/FrontController.php

@@ -0,0 +1,123 @@
+<?php
+# ***** BEGIN LICENSE BLOCK *****
+# MINZ - a free PHP Framework like Zend Framework
+# Copyright (C) 2011 Marien Fressinaud
+# 
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# ***** END LICENSE BLOCK *****
+
+/**
+ * La classe FrontController est le noyau du framework, elle lance l'application
+ * Elle est appelée en général dans le fichier index.php à la racine du serveur
+ */
+class FrontController {
+	protected $dispatcher;
+	protected $router;
+	
+	/**
+	 * Constructeur
+	 * Initialise le router et le dispatcher
+	 */
+	public function __construct () {
+		$this->loadLib ();
+
+		if (LOG_PATH === false) {
+			$this->killApp ('Path doesn\'t exist : LOG_PATH');
+		}
+		
+		try {
+			Configuration::init ();
+
+			Request::init ();
+			
+			$this->router = new Router ();
+			$this->router->init ();
+		} catch (RouteNotFoundException $e) {
+			Log::record ($e->getMessage (), Log::ERROR);
+			Error::error (
+				404,
+				array ('error' => array ($e->getMessage ()))
+			);
+		} catch (MinzException $e) {
+			Log::record ($e->getMessage (), Log::ERROR);
+			$this->killApp ();
+		}
+		
+		$this->dispatcher = Dispatcher::getInstance ($this->router);
+	}
+	
+	/**
+	 * Inclue les fichiers de la librairie
+	 */
+	private function loadLib () {
+		require ('ActionController.php');
+		require ('Cache.php');
+		require ('Configuration.php');
+		require ('Dispatcher.php');
+		require ('Error.php');
+		require ('Helper.php');
+		require ('Log.php');
+		require ('Model.php');
+		require ('Paginator.php');
+		require ('Request.php');
+		require ('Response.php');
+		require ('Router.php');
+		require ('Session.php');
+		require ('Translate.php');
+		require ('Url.php');
+		require ('View.php');
+		
+		require ('dao/Model_pdo.php');
+		require ('dao/Model_txt.php');
+		require ('dao/Model_array.php');
+		
+		require ('exceptions/MinzException.php');
+	}
+	
+	/**
+	 * Démarre l'application (lance le dispatcher et renvoie la réponse
+	 */
+	public function run () {
+		try {
+			$this->dispatcher->run ();
+			Response::send ();
+		} catch (MinzException $e) {
+			Log::record ($e->getMessage (), Log::ERROR);
+
+			if ($e instanceof FileNotExistException ||
+			    $e instanceof ControllerNotExistException ||
+			    $e instanceof ControllerNotActionControllerException ||
+			    $e instanceof ActionException) {
+				Error::error (
+					404,
+					array ('error' => array ($e->getMessage ())),
+					true
+				);
+			} else {
+				$this->killApp ();
+			}
+		}
+	}
+	
+	/**
+	* Permet d'arrêter le programme en urgence
+	*/
+	private function killApp ($txt = '') {
+		if ($txt == '') {
+			$txt = 'See logs files';
+		}
+		exit ('### Application problem ###'."\n".$txt);
+	}
+}

+ 22 - 0
lib/minz/Helper.php

@@ -0,0 +1,22 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * La classe Helper représente une aide pour des tâches récurrentes
+ */
+class Helper {
+	/**
+	 * Annule les effets des magic_quotes pour une variable donnée
+	 * @param $var variable à traiter (tableau ou simple variable)
+	 */
+	public static function stripslashes_r ($var) {
+		if (is_array ($var)){
+			return array_map (array ('Helper', 'stripslashes_r'), $var);
+		} else {
+			return stripslashes($var);
+		}
+	}
+}

+ 92 - 0
lib/minz/Log.php

@@ -0,0 +1,92 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * La classe Log permet de logger des erreurs
+ */
+class Log {
+	/**
+	 * Les différents niveau de log
+	 * ERROR erreurs bloquantes de l'application
+	 * WARNING erreurs pouvant géner le bon fonctionnement, mais non bloquantes
+	 * NOTICE messages d'informations, affichés pour le déboggage
+	 */
+	const ERROR = 0;
+	const WARNING = 10;
+	const NOTICE = 20;
+	
+	/**
+	 * Enregistre un message dans un fichier de log spécifique
+	 * Message non loggué si
+	 * 	- environment = SILENT
+	 * 	- level = WARNING et environment = PRODUCTION
+	 * 	- level = NOTICE et environment = PRODUCTION
+	 * @param $information message d'erreur / information à enregistrer
+	 * @param $level niveau d'erreur
+	 * @param $file_name fichier de log, par défaut LOG_PATH/application.log
+	 */
+	public static function record ($information, $level, $file_name = null) {
+		$env = Configuration::environment ();
+		
+		if (! ($env == Configuration::SILENT
+		       || ($env == Configuration::PRODUCTION
+		       && ($level == Log::WARNING || $level == Log::NOTICE)))) {
+			if (is_null ($file_name)) {
+				$file_name = LOG_PATH . '/application.log';
+			}
+			
+			switch ($level) {
+			case Log::ERROR :
+				$level_label = 'error';
+				break;
+			case Log::WARNING :
+				$level_label = 'warning';
+				break;
+			case Log::NOTICE :
+				$level_label = 'notice';
+				break;
+			default :
+				$level_label = 'unknown';
+			}
+			
+			if ($env == Configuration::PRODUCTION) {
+				$file = @fopen ($file_name, 'a');
+			} else {
+				$file = fopen ($file_name, 'a');
+			}
+			
+			if ($file !== false) {
+				$log = '[' . date('r') . ']';
+				$log .= ' [' . $level_label . ']';
+				$log .= ' --- ' . $information . "\n";
+				fwrite ($file, $log); 
+				fclose ($file);
+			} else {
+				Error::error (
+					500,
+					array ('error' => array (
+						'Permission is denied for `'
+						. $file_name . '`')
+					)
+				);
+			}
+		}
+	}
+
+	/**
+	 * Automatise le log des variables globales $_GET et $_POST
+	 * Fait appel à la fonction record(...)
+	 * Ne fonctionne qu'en environnement "development"
+	 * @param $file_name fichier de log, par défaut LOG_PATH/application.log
+	 */
+	public static function recordRequest($file_name = null) {
+		$msg_get = str_replace("\n", '', '$_GET content : ' . print_r($_GET, true));
+		$msg_post = str_replace("\n", '', '$_POST content : ' . print_r($_POST, true));
+
+		self::record($msg_get, Log::NOTICE, $file_name);
+		self::record($msg_post, Log::NOTICE, $file_name);
+	}
+}

+ 12 - 0
lib/minz/Model.php

@@ -0,0 +1,12 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * La classe Model représente un modèle de l'application (représentation MVC)
+ */
+class Model {
+
+}

+ 196 - 0
lib/minz/Paginator.php

@@ -0,0 +1,196 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * La classe Paginator permet de gérer la pagination de l'application facilement
+ */
+class Paginator {
+	/**
+	 * $items tableau des éléments à afficher/gérer
+	 */
+	private $items = array ();
+
+	/**
+	 * $nbItemsPerPage le nombre d'éléments par page
+	 */
+	private $nbItemsPerPage = 10;
+
+	/**
+	 * $currentPage page actuelle à gérer
+	 */
+	private $currentPage = 1;
+
+	/**
+	 * $nbPage le nombre de pages de pagination
+	 */
+	private $nbPage = 1;
+
+	/**
+	 * $nbItems le nombre d'éléments
+	 */
+	private $nbItems = 0;
+
+	/**
+	 * Constructeur
+	 * @param $items les éléments à gérer
+	 */
+	public function __construct ($items) {
+		$this->_items ($items);
+		$this->_nbItems (count ($this->items (true)));
+		$this->_nbItemsPerPage ($this->nbItemsPerPage);
+		$this->_currentPage ($this->currentPage);
+	}
+
+	/**
+	 * Permet d'afficher la pagination
+	 * @param $view nom du fichier de vue situé dans /app/views/helpers/
+	 * @param $getteur variable de type $_GET[] permettant de retrouver la page
+	 */
+	public function render ($view, $getteur) {
+		$view = APP_PATH . '/views/helpers/'.$view;
+		
+		if (file_exists ($view)) {
+			include ($view);
+		}
+	}
+
+	/**
+	 * Permet de retrouver la page d'un élément donné
+	 * @param $item l'élément à retrouver
+	 * @return la page à laquelle se trouve l'élément (false si non trouvé)
+	 */
+	public function pageByItem ($item) {
+		$page = false;
+		$i = 0;
+
+		do {
+			if ($item == $this->items[$i]) {
+				$page = ceil (($i + 1) / $this->nbItemsPerPage);
+			}
+
+			$i++;
+		} while (!$page && $i < $this->nbItems ());
+
+		return $page;
+	}
+
+	/**
+	 * Permet de retrouver la position d'un élément donné (à partir de 0)
+	 * @param $item l'élément à retrouver
+	 * @return la position à laquelle se trouve l'élément (false si non trouvé)
+	 */
+	public function positionByItem ($item) {
+		$find = false;
+		$i = 0;
+
+		do {
+			if ($item == $this->items[$i]) {
+				$find = true;
+			} else {
+				$i++;
+			}
+		} while (!$find && $i < $this->nbItems ());
+
+		return $i;
+	}
+
+	/**
+	 * Permet de récupérer un item par sa position
+	 * @param $pos la position de l'élément
+	 * @return l'item situé à $pos (dernier item si $pos<0, 1er si $pos>=count($items))
+	 */
+	public function itemByPosition ($pos) {
+		if ($pos < 0) {
+			$pos = $this->nbItems () - 1;
+		}
+		if ($pos >= count($this->items)) {
+			$pos = 0;
+		}
+
+		return $this->items[$pos];
+	}
+
+	/**
+	 * GETTEURS
+	 */
+	/**
+	 * @param $all si à true, retourne tous les éléments sans prendre en compte la pagination
+	 */
+	public function items ($all = false) {
+		$array = array ();
+		$nbItems = $this->nbItems ();
+
+		if ($nbItems <= $this->nbItemsPerPage || $all) {
+			$array = $this->items;
+		} else {
+			$begin = ($this->currentPage - 1) * $this->nbItemsPerPage;
+			$counter = 0;
+			$i = 0;
+			
+			foreach ($this->items as $key => $item) {
+				if ($i >= $begin) {
+					$array[$key] = $item;
+					$counter++;
+				}
+				if ($counter >= $this->nbItemsPerPage) {
+					break;
+				}
+				$i++;
+			}
+		}
+
+		return $array;
+	}
+	public function nbItemsPerPage  () {
+		return $this->nbItemsPerPage;
+	}
+	public function currentPage () {
+		return $this->currentPage;
+	}
+	public function nbPage () {
+		return $this->nbPage;
+	}
+	public function nbItems () {
+		return $this->nbItems;
+	}
+
+	/**
+	 * SETTEURS
+	 */
+	public function _items ($items) {
+		if (is_array ($items)) {
+			$this->items = $items;
+		}
+		
+		$this->_nbPage ();
+	}
+	public function _nbItemsPerPage ($nbItemsPerPage) {
+		if ($nbItemsPerPage > $this->nbItems ()) {
+			$nbItemsPerPage = $this->nbItems ();
+		}
+		if ($nbItemsPerPage < 0) {
+			$nbItemsPerPage = 0;
+		}
+
+		$this->nbItemsPerPage = $nbItemsPerPage;
+		$this->_nbPage ();
+	}
+	public function _currentPage ($page) {
+		if($page < 1 || ($page > $this->nbPage && $this->nbPage > 0)) {
+			throw new CurrentPagePaginationException ($page);
+		}
+
+		$this->currentPage = $page;
+	}
+	private function _nbPage () {
+		if ($this->nbItemsPerPage > 0) {
+			$this->nbPage = ceil ($this->nbItems () / $this->nbItemsPerPage);
+		}
+	}
+	public function _nbItems ($value) {
+		$this->nbItems = $value;
+	}
+}

+ 196 - 0
lib/minz/Request.php

@@ -0,0 +1,196 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * Request représente la requête http
+ */
+class Request {
+	private static $controller_name = '';
+	private static $action_name = '';
+	private static $params = array ();
+	
+	private static $default_controller_name = 'index';
+	private static $default_action_name = 'index';
+	
+	public static $reseted = true;
+	
+	/**
+	 * Getteurs
+	 */
+	public static function controllerName () {
+		return self::$controller_name;
+	}
+	public static function actionName () {
+		return self::$action_name;
+	}
+	public static function params () {
+		return self::$params;
+	}
+	public static function param ($key, $default = false, $specialchars = false) {
+		if (isset (self::$params[$key])) {
+			$p = self::$params[$key];
+			if(is_object($p) || $specialchars) {
+				return $p;
+			} elseif(is_array($p)) {
+				return array_map('htmlspecialchars', $p);
+			} else {
+				return htmlspecialchars($p);
+			}
+		} else {
+			return $default;
+		}
+	}
+	public static function defaultControllerName () {
+		return self::$default_controller_name;
+	}
+	public static function defaultActionName () {
+		return self::$default_action_name;
+	}
+	
+	/**
+	 * Setteurs
+	 */
+	public static function _controllerName ($controller_name) {
+		self::$controller_name = $controller_name;
+	}
+	public static function _actionName ($action_name) {
+		self::$action_name = $action_name;
+	}
+	public static function _params ($params) {
+		if (!is_array($params)) {
+			$params = array ($params);
+		}
+		
+		self::$params = $params;
+	}
+	public static function _param ($key, $value = false) {
+		if ($value === false) {
+			unset (self::$params[$key]);
+		} else {
+			self::$params[$key] = $value;
+		}
+	}
+	
+	/**
+	 * Initialise la Request
+	 */
+	public static function init () {
+		self::magicQuotesOff ();
+	}
+	
+	/**
+	 * Retourn le nom de domaine du site
+	 */
+	public static function getDomainName () {
+		return $_SERVER['HTTP_HOST'];
+	}
+	
+	/**
+	 * Détermine la base de l'url
+	 * @return la base de l'url
+	 */
+	public static function getBaseUrl () {
+		return Configuration::baseUrl ();
+	}
+	
+	/**
+	 * Récupère l'URI de la requête
+	 * @return l'URI
+	 */
+	public static function getURI () {
+		if (isset ($_SERVER['REQUEST_URI'])) {
+			$base_url = self::getBaseUrl ();
+			$uri = $_SERVER['REQUEST_URI'];
+			
+			$len_base_url = strlen ($base_url);
+			$real_uri = substr ($uri, $len_base_url); 
+		} else {
+			$real_uri = '';
+		}
+		
+		return $real_uri;
+	}
+	
+	/**
+	 * Relance une requête
+	 * @param $url l'url vers laquelle est relancée la requête
+	 * @param $redirect si vrai, force la redirection http
+	 *                > sinon, le dispatcher recharge en interne
+	 */
+	public static function forward ($url = array (), $redirect = false) {
+		$url = Url::checkUrl ($url);
+		
+		if ($redirect) {
+			header ('Location: ' . Url::display ($url, 'php'));
+			exit ();
+		} else {
+			self::$reseted = true;
+		
+			self::_controllerName ($url['c']);
+			self::_actionName ($url['a']);
+			self::_params (array_merge (
+				self::$params,
+				$url['params']
+			));
+		}
+	}
+	
+	/**
+	 * Permet de récupérer une variable de type $_GET
+	 * @param $param nom de la variable
+	 * @param $default valeur par défaut à attribuer à la variable
+	 * @return $_GET[$param]
+	 *         $_GET si $param = false
+	 *         $default si $_GET[$param] n'existe pas
+	 */
+	public static function fetchGET ($param = false, $default = false) {
+		if ($param === false) {
+			return $_GET;
+		} elseif (isset ($_GET[$param])) {
+			return $_GET[$param];
+		} else {
+			return $default;
+		}
+	}
+	
+	/**
+	 * Permet de récupérer une variable de type $_POST
+	 * @param $param nom de la variable
+	 * @param $default valeur par défaut à attribuer à la variable
+	 * @return $_POST[$param]
+	 *         $_POST si $param = false
+	 *         $default si $_POST[$param] n'existe pas
+	 */
+	public static function fetchPOST ($param = false, $default = false) {
+		if ($param === false) {
+			return $_POST;
+		} elseif (isset ($_POST[$param])) {
+			return $_POST[$param];
+		} else {
+			return $default;
+		}
+	}
+	
+	/**
+	 * Méthode désactivant les magic_quotes pour les variables
+	 *   $_GET
+	 *   $_POST
+	 *   $_COOKIE
+	 */
+	private static function magicQuotesOff () {
+		if (get_magic_quotes_gpc ()) {
+			$_GET = Helper::stripslashes_r ($_GET);
+			$_POST = Helper::stripslashes_r ($_POST);
+			$_COOKIE = Helper::stripslashes_r ($_COOKIE);
+		}
+	}
+	
+	public static function isPost () {
+		return !empty ($_POST) || !empty ($_FILES);
+	}
+}
+
+

+ 60 - 0
lib/minz/Response.php

@@ -0,0 +1,60 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * Response représente la requête http renvoyée à l'utilisateur
+ */
+class Response {
+	private static $header = 'HTTP/1.0 200 OK';
+	private static $body = '';
+	
+	/**
+	 * Mets à jour le body de la Response
+	 * @param $text le texte à incorporer dans le body
+	 */
+	public static function setBody ($text) {
+		self::$body = $text;
+	}
+	
+	/**
+	 * Mets à jour le header de la Response
+	 * @param $code le code HTTP, valeurs possibles
+	 *	- 200 (OK)
+	 *	- 403 (Forbidden)
+	 *	- 404 (Forbidden)
+	 *	- 500 (Forbidden) -> par défaut si $code erroné
+	 *	- 503 (Forbidden)
+	 */
+	public static function setHeader ($code) {
+		switch ($code) {
+		case 200 :
+			self::$header = 'HTTP/1.0 200 OK';
+			break;
+		case 403 :
+			self::$header = 'HTTP/1.0 403 Forbidden';
+			break;
+		case 404 :
+			self::$header = 'HTTP/1.0 404 Not Found';
+			break;
+		case 500 :
+			self::$header = 'HTTP/1.0 500 Internal Server Error';
+			break;
+		case 503 :
+			self::$header = 'HTTP/1.0 503 Service Unavailable';
+			break;
+		default :
+			self::$header = 'HTTP/1.0 500 Internal Server Error';
+		}
+	}
+
+	/**
+	 * Envoie la Response à l'utilisateur
+	 */
+	public static function send () {
+		header (self::$header);
+		echo self::$body;
+	}
+}

+ 209 - 0
lib/minz/Router.php

@@ -0,0 +1,209 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * La classe Router gère le routage de l'application
+ * Les routes sont définies dans APP_PATH.'/configuration/routes.php'
+ */
+class Router {
+	const ROUTES_PATH_NAME = '/configuration/routes.php';
+
+	private $routes = array ();
+	
+	/**
+	 * Constructeur
+	 * @exception FileNotExistException si ROUTES_PATH_NAME n'existe pas
+	 *            et que l'on utilise l'url rewriting
+	 */
+	public function __construct () {
+		if (Configuration::useUrlRewriting ()) {
+			if (file_exists (APP_PATH . self::ROUTES_PATH_NAME)) {
+				$routes = include (
+					APP_PATH . self::ROUTES_PATH_NAME
+				);
+		
+				if (!is_array ($routes)) {
+					$routes = array ();
+				}
+				
+				$this->routes = array_map (
+					array ('Url', 'checkUrl'),
+					$routes
+				);
+			} else {
+				throw new FileNotExistException (
+					self::ROUTES_PATH_NAME,
+					MinzException::ERROR
+				);
+			}
+		}
+	}
+	
+	/**
+	 * Initialise le Router en déterminant le couple Controller / Action
+	 * Mets à jour la Request
+	 * @exception RouteNotFoundException si l'uri n'est pas présente dans
+	 *          > la table de routage
+	 */
+	public function init () {
+		$url = array ();
+		
+		if (Configuration::useUrlRewriting ()) {
+			try {
+				$url = $this->buildWithRewriting ();
+			} catch (RouteNotFoundException $e) {
+				throw $e;
+			}
+		} else {
+			$url = $this->buildWithoutRewriting ();
+		}
+		
+		$url['params'] = array_merge (
+			$url['params'],
+			Request::fetchPOST ()
+		);
+		
+		Request::forward ($url);
+	}
+	
+	/**
+	 * Retourne un tableau représentant l'url passée par la barre d'adresses
+	 * Ne se base PAS sur la table de routage
+	 * @return tableau représentant l'url
+	 */
+	public function buildWithoutRewriting () {
+		$url = array ();
+		
+		$url['c'] = Request::fetchGET (
+			'c',
+			Request::defaultControllerName ()
+		);
+		$url['a'] = Request::fetchGET (
+			'a',
+			Request::defaultActionName ()
+		);
+		$url['params'] = Request::fetchGET ();
+		
+		// post-traitement
+		unset ($url['params']['c']);
+		unset ($url['params']['a']);
+		
+		return $url;
+	}
+	
+	/**
+	 * Retourne un tableau représentant l'url passée par la barre d'adresses
+	 * Se base sur la table de routage
+	 * @return tableau représentant l'url
+	 * @exception RouteNotFoundException si l'uri n'est pas présente dans
+	 *          > la table de routage
+	 */
+	public function buildWithRewriting () {
+		$url = array ();
+		$uri = Request::getURI ();
+		$find = false;
+		
+		foreach ($this->routes as $route) {
+			$regex = '*^' . $route['route'] . '$*';
+			if (preg_match ($regex, $uri, $matches)) {
+				$url['c'] = $route['controller'];
+				$url['a'] = $route['action'];
+				$url['params'] = $this->getParams (
+					$route['params'],
+					$matches
+				);
+				$find = true;
+				break;
+			}
+		}
+		
+		if (!$find && $uri != '/') {
+			throw new RouteNotFoundException (
+				$uri,
+				MinzException::ERROR
+			);
+		}
+		
+		// post-traitement
+		$url = Url::checkUrl ($url);
+		
+		return $url;
+	}
+	
+	/**
+	 * Retourne l'uri d'une url en se basant sur la table de routage
+	 * @param l'url sous forme de tableau
+	 * @return l'uri formatée (string) selon une route trouvée
+	 */
+	public function printUriRewrited ($url) {
+		$route = $this->searchRoute ($url);
+		
+		if ($route !== false) {
+			return $this->replaceParams ($route, $url['params']);
+		}
+		
+		return '';
+	}
+	
+	/**
+	 * Recherche la route correspondante à une url
+	 * @param l'url sous forme de tableau
+	 * @return la route telle que spécifiée dans la table de routage,
+	 *         false si pas trouvée
+	 */
+	public function searchRoute ($url) {
+		foreach ($this->routes as $route) {
+			if ($route['controller'] == $url['c']
+			 && $route['action'] == $url['a']) {
+				// calcule la différence des tableaux de params
+				$params = array_flip ($route['params']);
+				$difference_params = array_diff_key (
+					$params,
+					$url['params']
+				);
+				
+				// vérifie que pas de différence
+				// et le cas où $params est vide et pas $url['params']
+				if (empty ($difference_params)
+				&& (!empty ($params) || empty ($url['params']))) {
+					return $route;
+				}
+			}
+		}
+		
+		return false;
+	}
+	
+	/**
+	 * Récupère un tableau dont
+	 * 	- les clés sont définies dans $params_route
+	 *	- les valeurs sont situées dans $matches
+	 * Le tableau $matches est décalé de +1 par rapport à $params_route
+	 */
+	private function getParams($params_route, $matches) {
+		$params = array ();
+		
+		for ($i = 0; $i < count ($params_route); $i++) {
+			$param = $params_route[$i];
+			$params[$param] = $matches[$i + 1];
+		}
+	
+		return $params;
+	}
+	
+	/**
+	 * Remplace les éléments de la route par les valeurs contenues dans $params
+	 */
+	private function replaceParams ($route, $params_replace) {
+		$uri = $route['route'];
+		$params = array();
+		foreach($route['params'] as $param) {
+			$uri = preg_replace('#\((.+)\)#U', $params_replace[$param], $uri, 1);
+		}
+
+		return stripslashes($uri);
+	 }
+}

+ 78 - 0
lib/minz/Session.php

@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * La classe Session gère la session utilisateur
+ * C'est un singleton
+ */
+class Session {
+	/**
+	 * $session stocke les variables de session
+	 */
+	private static $session = array ();
+	
+	/**
+	 * Initialise la session
+	 */
+	public static function init () {
+		// démarre la session
+		session_name (md5 (Configuration::selApplication ()));
+		session_start ();
+		
+		if (isset ($_SESSION)) {
+			self::$session = $_SESSION;
+		}
+	}
+	
+	
+	/**
+	 * Permet de récupérer une variable de session
+	 * @param $p le paramètre à récupérer
+	 * @return la valeur de la variable de session, false si n'existe pas
+	 */
+	public static function param ($p, $default = false) {
+		if (isset (self::$session[$p])) {
+			$return = self::$session[$p];
+		} else {
+			$return = $default;
+		}
+		
+		return $return;
+	}
+	
+	
+	/**
+	 * Permet de créer ou mettre à jour une variable de session
+	 * @param $p le paramètre à créer ou modifier
+	 * @param $v la valeur à attribuer, false pour supprimer
+	 */
+	public static function _param ($p, $v = false) {
+		if ($v === false) {
+			unset ($_SESSION[$p]);
+			unset (self::$session[$p]);
+		} else {
+			$_SESSION[$p] = $v;
+			self::$session[$p] = $v;
+
+			if($p == 'language') {
+				// reset pour remettre à jour le fichier de langue à utiliser
+				Translate::reset ();
+			}
+		}
+	}
+	
+	
+	/**
+	 * Permet d'effacer une session
+	 * @param $force si à false, n'efface pas le paramètre de langue
+	 */
+	public static function unset_session ($force = false) {
+		$language = self::param ('language');
+		
+		session_unset ();
+		self::$session = array ();
+		
+		if (!$force) {
+			self::_param ('language', $language);
+		}
+	}
+}

+ 71 - 0
lib/minz/Translate.php

@@ -0,0 +1,71 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * La classe Translate se charge de la traduction
+ * Utilise les fichiers du répertoire /app/i18n/
+ */
+class Translate {
+	/**
+	 * $language est la langue à afficher
+	 */
+	private static $language;
+	
+	/**
+	 * $translates est le tableau de correspondance
+	 * 	$key => $traduction
+	 */
+	private static $translates = array ();
+	
+	/**
+	 * Inclus le fichier de langue qui va bien
+	 * l'enregistre dans $translates
+	 */
+	public static function init () {
+		$l = Configuration::language ();
+		self::$language = Session::param ('language', $l);
+		
+		$l_path = APP_PATH . '/i18n/' . self::$language . '.php';
+		
+		if (file_exists ($l_path)) {
+			self::$translates = include ($l_path);
+		}
+	}
+	
+	/**
+	 * Alias de init
+	 */
+	public static function reset () {
+		self::init ();
+	}
+	
+	/**
+	 * Traduit une clé en sa valeur du tableau $translates
+	 * @param $key la clé à traduire
+	 * @return la valeur correspondante à la clé
+	 *       > si non présente dans le tableau, on retourne la clé elle-même
+	 */ 
+	public static function t ($key) {
+		$translate = $key;
+		
+		if (isset (self::$translates[$key])) {
+			$translate = self::$translates[$key];
+		}
+
+		$args = func_get_args ();
+		unset($args[0]);
+		
+		return vsprintf ($translate, $args);
+	}
+	
+	/**
+	 * Retourne la langue utilisée actuellement
+	 * @return la langue
+	 */
+	public static function language () {
+		return self::$language;
+	}
+}

+ 130 - 0
lib/minz/Url.php

@@ -0,0 +1,130 @@
+<?php
+
+/**
+ * La classe Url permet de gérer les URL à travers MINZ
+ */
+class Url {
+	/**
+	 * Affiche une Url formatée selon que l'on utilise l'url_rewriting ou non
+	 * si oui, on cherche dans la table de routage la correspondance pour formater
+	 * @param $url l'url à formater définie comme un tableau :
+	 *                    $url['c'] = controller
+	 *                    $url['a'] = action
+	 *                    $url['params'] = tableau des paramètres supplémentaires
+	 *                    $url['protocol'] = protocole à utiliser (http par défaut)
+	 *             ou comme une chaîne de caractère
+	 * @param $encodage pour indiquer comment encoder les & (& ou &amp; pour html)
+	 * @return l'url formatée
+	 */
+	public static function display ($url = array (), $encodage = 'html') {
+		$url = self::checkUrl ($url);
+		
+		$url_string = '';
+		
+		if (is_array ($url) && isset ($url['protocol'])) {
+			$protocol = $url['protocol'];
+		} else {
+			if(isset($_SERVER['HTTPS']) && $_SERVER["HTTPS"] == 'on') {
+				$protocol = 'https';
+			} else {
+				$protocol = 'http';
+			}
+		}
+		$url_string .= $protocol . '://';
+		
+		$url_string .= Request::getDomainName ();
+		
+		$url_string .= Request::getBaseUrl ();
+		
+		if (is_array ($url)) {
+			$router = new Router ();
+			
+			if (Configuration::useUrlRewriting ()) {
+				$url_string .= $router->printUriRewrited ($url);
+			} else {
+				$url_string .= self::printUri ($url, $encodage);
+			}
+		} else {
+			$url_string .= $url;
+		}
+		
+		return $url_string;
+	}
+	
+	/**
+	 * Construit l'URI d'une URL sans url rewriting
+	 * @param l'url sous forme de tableau
+	 * @param $encodage pour indiquer comment encoder les & (& ou &amp; pour html)
+	 * @return l'uri sous la forme ?key=value&key2=value2
+	 */
+	private static function printUri ($url, $encodage) {
+		$uri = '';
+		$separator = '/?';
+		
+		if($encodage == 'html') {
+			$and = '&amp;';
+		} else {
+			$and = '&';
+		}
+		
+		if (isset ($url['c'])
+		 && $url['c'] != Request::defaultControllerName ()) {
+			$uri .= $separator . 'c=' . $url['c'];
+			$separator = $and;
+		}
+		
+		if (isset ($url['a'])
+		 && $url['a'] != Request::defaultActionName ()) {
+			$uri .= $separator . 'a=' . $url['a'];
+			$separator = $and;
+		}
+		
+		if (isset ($url['params'])) {
+			foreach ($url['params'] as $key => $param) {
+				$uri .= $separator . $key . '=' . $param;
+				$separator = $and;
+			}
+		}
+		
+		return $uri;
+	}
+	
+	/**
+	 * Vérifie que les éléments du tableau représentant une url soit ok
+	 * @param l'url sous forme de tableau (sinon renverra directement $url)
+	 * @return l'url vérifié
+	 */
+	public static function checkUrl ($url) {
+		$url_checked = $url;
+		
+		if (is_array ($url)) {
+			if (!isset ($url['c'])) {
+				$url_checked['c'] = Request::defaultControllerName ();
+			}
+			if (!isset ($url['a'])) {
+				$url_checked['a'] = Request::defaultActionName ();
+			}
+			if (!isset ($url['params'])) {
+				$url_checked['params'] = array ();
+			}
+		}
+		
+		return $url_checked;
+	}
+}
+
+function _url ($controller, $action) {
+	$nb_args = func_num_args ();
+
+	if($nb_args < 2 || $nb_args % 2 != 0) {
+		return false;
+	}
+
+	$args = func_get_args ();
+	$params = array ();
+	for($i = 2; $i < $nb_args; $i = $i + 2) {
+		$params[$args[$i]] = $args[$i + 1];
+	}
+
+	return Url::display (array ('c' => $controller, 'a' => $action, 'params' => $params));
+}

+ 232 - 0
lib/minz/View.php

@@ -0,0 +1,232 @@
+<?php
+/**
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * La classe View représente la vue de l'application
+ */
+class View {
+	const VIEWS_PATH_NAME = '/views';
+	const LAYOUT_PATH_NAME = '/layout';
+	const LAYOUT_FILENAME = '/layout.phtml';
+
+	private $view_filename = '';
+	private $use_layout = false;
+
+	private static $title = '';
+	private static $styles = array ();
+	private static $scripts = array ();
+
+	private static $params = array ();
+
+	/**
+	 * Constructeur
+	 * Détermine si on utilise un layout ou non
+	 */
+	public function __construct () {
+		$this->view_filename = APP_PATH
+		                     . self::VIEWS_PATH_NAME . '/'
+		                     . Request::controllerName () . '/'
+		                     . Request::actionName () . '.phtml';
+
+		if (file_exists (APP_PATH
+		               . self::LAYOUT_PATH_NAME
+		               . self::LAYOUT_FILENAME)) {
+			$this->use_layout = true;
+		}
+
+		self::$title = Configuration::title ();
+	}
+
+	/**
+	 * Construit la vue
+	 */
+	public function build () {
+		if ($this->use_layout) {
+			$this->buildLayout ();
+		} else {
+			$this->render ();
+		}
+	}
+
+	/**
+	 * Construit le layout
+	 */
+	public function buildLayout () {
+		include (
+			APP_PATH
+			. self::LAYOUT_PATH_NAME
+			. self::LAYOUT_FILENAME
+		);
+	}
+
+	/**
+	 * Affiche la Vue en elle-même
+	 */
+	public function render () {
+		if (file_exists ($this->view_filename)) {
+			include ($this->view_filename);
+		} else {
+			Log::record ('File doesn\'t exist : `'
+			            . $this->view_filename . '`',
+			            Log::NOTICE);
+		}
+	}
+
+	/**
+	 * Ajoute un élément du layout
+	 * @param $part l'élément partial à ajouter
+	 */
+	public function partial ($part) {
+		$fic_partial = APP_PATH
+		             . self::LAYOUT_PATH_NAME . '/'
+		             . $part . '.phtml';
+
+		if (file_exists ($fic_partial)) {
+			include ($fic_partial);
+		} else {
+			Log::record ('File doesn\'t exist : `'
+			            . $fic_partial . '`',
+			            Log::WARNING);
+		}
+	}
+
+	/**
+	 * Affiche un élément graphique situé dans APP./views/helpers/
+	 * @param $helper l'élément à afficher
+	 */
+	public function renderHelper ($helper) {
+		$fic_helper = APP_PATH
+		            . '/views/helpers/'
+		            . $helper . '.phtml';
+
+		if (file_exists ($fic_helper)) {
+			include ($fic_helper);
+		} else {
+			Log::record ('File doesn\'t exist : `'
+			            . $fic_helper . '`',
+			            Log::WARNING);
+		}
+	}
+
+	/**
+	 * Permet de choisir si on souhaite utiliser le layout
+	 * @param $use true si on souhaite utiliser le layout, false sinon
+	 */
+	public function _useLayout ($use) {
+		$this->use_layout = $use;
+	}
+
+	/**
+	 * Gestion du titre
+	 */
+	public static function title () {
+		return self::$title;
+	}
+	public static function headTitle () {
+		return '<title>' . self::$title . '</title>' . "\n";
+	}
+	public static function _title ($title) {
+		self::$title = $title;
+	}
+	public static function prependTitle ($title) {
+		self::$title = $title . self::$title;
+	}
+	public static function appendTitle ($title) {
+		self::$title = self::$title . $title;
+	}
+
+	/**
+	 * Gestion des feuilles de style
+	 */
+	public static function headStyle () {
+		$styles = '';
+
+		foreach(self::$styles as $style) {
+			$cond = $style['cond'];
+			if ($cond) {
+				$styles .= '<!--[if ' . $cond . ']>';
+			}
+
+			$styles .= '<link rel="stylesheet" type="text/css"';
+			$styles .= ' media="' . $style['media'] . '"';
+			$styles .= ' href="' . $style['url'] . '" />';
+
+			if ($cond) {
+				$styles .= '<![endif]-->';
+			}
+
+			$styles .= "\n";
+		}
+
+		return $styles;
+	}
+	public static function prependStyle ($url, $media = 'all', $cond = false) {
+		array_unshift (self::$styles, array (
+			'url' => $url,
+			'media' => $media,
+			'cond' => $cond
+		));
+	}
+	public static function appendStyle ($url, $media = 'all', $cond = false) {
+		self::$styles[] = array (
+			'url' => $url,
+			'media' => $media,
+			'cond' => $cond
+		);
+	}
+
+	/**
+	 * Gestion des scripts JS
+	 */
+	public static function headScript () {
+		$scripts = '';
+
+		foreach (self::$scripts as $script) {
+			$cond = $script['cond'];
+			if ($cond) {
+				$scripts .= '<!--[if ' . $cond . ']>';
+			}
+
+			$scripts .= '<script type="text/javascript"';
+			$scripts .= ' src="' . $script['url'] . '">';
+			$scripts .= '</script>';
+
+			if ($cond) {
+				$scripts .= '<![endif]-->';
+			}
+
+			$scripts .= "\n";
+		}
+
+		return $scripts;
+	}
+	public static function prependScript ($url, $cond = false) {
+		array_unshift(self::$scripts, array (
+			'url' => $url,
+			'cond' => $cond
+		));
+	}
+	public static function appendScript ($url, $cond = false) {
+		self::$scripts[] = array (
+			'url' => $url,
+			'cond' => $cond
+		);
+	}
+
+	/**
+	 * Gestion des paramètres ajoutés à la vue
+	 */
+	public static function _param ($key, $value) {
+		self::$params[$key] = $value;
+	}
+	public function attributeParams () {
+		foreach (View::$params as $key => $value) {
+			$this->$key = $value;
+		}
+	}
+}
+
+

+ 122 - 0
lib/minz/dao/Model_array.php

@@ -0,0 +1,122 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * La classe Model_array représente le modèle interragissant avec les fichiers de type texte gérant des tableaux php
+ */
+class Model_array extends Model_txt {
+	/**
+	 * $array Le tableau php contenu dans le fichier $nameFile
+	 */
+	protected $array = array ();
+	
+	/**
+	 * Ouvre le fichier indiqué, charge le tableau dans $array et le $nameFile
+	 * @param $nameFile le nom du fichier à ouvrir contenant un tableau
+	 * Remarque : $array sera obligatoirement un tableau
+	 */
+	public function __construct ($nameFile) {
+		parent::__construct ($nameFile);
+		
+		if (!$this->getLock ('read')) {
+			throw new PermissionDeniedException ($this->filename);
+		} else {
+			$this->array = include ($this->filename);
+			$this->releaseLock ();
+		
+			if (!is_array ($this->array)) {
+				$this->array = array ();
+			}
+		
+			$this->array = $this->decodeArray ($this->array);
+		}
+	}	
+	
+	/**
+	 * Écrit un tableau dans le fichier $nameFile
+	 * @param $array le tableau php à enregistrer
+	 **/
+	public function writeFile ($array) {
+		if (!$this->getLock ('write')) {
+			throw new PermissionDeniedException ($this->namefile);
+		} else {
+			$this->erase ();
+		
+			$this->writeLine ('<?php');
+			$this->writeLine ('return ', false);
+			$this->writeArray ($array);
+			$this->writeLine (';');
+		
+			$this->releaseLock ();
+		}
+	}
+	
+	private function writeArray ($array, $profondeur = 0) {
+		$tab = '';
+		for ($i = 0; $i < $profondeur; $i++) {
+			$tab .= "\t";
+		}
+		$this->writeLine ('array (');
+		
+		foreach ($array as $key => $value) {
+			if (is_int ($key)) {
+				$this->writeLine ($tab . "\t" . $key . ' => ', false);
+			} else {
+				$this->writeLine ($tab . "\t" . '\'' . $key . '\'' . ' => ', false);
+			}
+			
+			if (is_array ($value)) {
+				$this->writeArray ($value, $profondeur + 1);
+				$this->writeLine (',');
+			} else {
+				if (is_numeric ($value)) {
+					$this->writeLine ($value . ',');
+				} else {
+					$this->writeLine ('\'' . addslashes ($value) . '\',');
+				}
+			}
+		}
+		
+		$this->writeLine ($tab . ')', false);
+	}
+	
+	private function decodeArray ($array) {
+		$new_array = array ();
+		
+		foreach ($array as $key => $value) {
+			if (is_array ($value)) {
+				$new_array[$key] = $this->decodeArray ($value);
+			} else {
+				$new_array[$key] = stripslashes ($value);
+			}
+		}
+		
+		return $new_array;
+	}
+	
+	private function getLock ($type) {
+		if ($type == 'write') {
+			$lock = LOCK_EX;
+		} else {
+			$lock = LOCK_SH;
+		}
+	
+		$count = 1;
+		while (!flock ($this->file, $lock) && $count <= 50) {
+			$count++;
+		}
+		
+		if ($count >= 50) {
+			return false;
+		} else {
+			return true;
+		}
+	}
+	
+	private function releaseLock () {
+		flock ($this->file, LOCK_UN);
+	}
+}

+ 39 - 0
lib/minz/dao/Model_pdo.php

@@ -0,0 +1,39 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * La classe Model_sql représente le modèle interragissant avec les bases de données
+ * Seul la connexion MySQL est prise en charge pour le moment
+ */
+class Model_pdo {
+	/**
+	 * $bd variable représentant la base de données
+	 */
+	protected $bd;
+	
+	/**
+	 * Créé la connexion à la base de données à l'aide des variables
+	 * HOST, BASE, USER et PASS définies dans le fichier de configuration
+	 */
+	public function __construct ($type = 'mysql') {
+		$db = Configuration::dataBase ();
+		try {
+			$string = $type
+			        . ':host=' . $db['host']
+			        . ';dbname=' . $db['base'];
+			$this->bd = new PDO (
+				$string,
+				$db['user'],
+				$db['password']
+			);
+		} catch (Exception $e) {
+			throw new PDOConnectionException (
+				$string,
+				$db['user'], MinzException::WARNING
+			);
+		}
+	}
+}

+ 77 - 0
lib/minz/dao/Model_txt.php

@@ -0,0 +1,77 @@
+<?php
+/** 
+ * MINZ - Copyright 2011 Marien Fressinaud
+ * Sous licence AGPL3 <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * La classe Model_txt représente le modèle interragissant avec les fichiers de type texte
+ */
+class Model_txt {
+	/**
+	 * $file représente le fichier à ouvrir
+	 */
+	protected $file;
+	
+	/**
+	 * $filename est le nom du fichier
+	 */
+	protected $filename;
+	
+	/**
+	 * Ouvre un fichier dans $file
+	 * @param $nameFile nom du fichier à ouvrir
+	 * @param $mode mode d'ouverture du fichier ('a+' par défaut)
+	 * @exception FileNotExistException si le fichier n'existe pas
+	 *          > ou ne peux pas être ouvert
+	 */
+	public function __construct ($nameFile, $mode = 'a+') {
+		$this->filename = $nameFile;
+		$this->file = @fopen ($this->filename, $mode);
+		
+		if (!$this->file) {
+			throw new FileNotExistException (
+				$this->filename,
+				MinzException::WARNING
+			);
+		}
+	}
+	
+	/**
+	 * Lit une ligne de $file
+	 * @return une ligne du fichier
+	 */
+	public function readLine () {
+		return fgets ($this->file);
+	}
+	
+	/**
+	 * Écrit une ligne dans $file
+	 * @param $line la ligne à écrire
+	 */
+	public function writeLine ($line, $newLine = true) {
+		$char = '';
+		if ($newLine) {
+			$char = "\n";
+		}
+		
+		fwrite ($this->file, $line . $char);
+	}
+	
+	/**
+	 * Efface le fichier $file
+	 * @return true en cas de succès, false sinon
+	 */
+	public function erase () {
+		return ftruncate ($this->file, 0);
+	}
+	
+	/**
+	 * Ferme $file
+	 */
+	public function __destruct () {
+		if (isset ($this->file)) {
+			fclose ($this->file);
+		}
+	}
+}

+ 94 - 0
lib/minz/exceptions/MinzException.php

@@ -0,0 +1,94 @@
+<?php
+
+class MinzException extends Exception {
+	const ERROR = 0;
+	const WARNING = 10;
+	const NOTICE = 20;
+
+	public function __construct ($message, $code = self::ERROR) {
+		if ($code != MinzException::ERROR
+		 && $code != MinzException::WARNING
+		 && $code != MinzException::NOTICE) {
+			$code = MinzException::ERROR;
+		}
+		
+		parent::__construct ($message, $code);
+	}
+}
+
+class PermissionDeniedException extends MinzException {
+	public function __construct ($file_name, $code = self::ERROR) {
+		$message = 'Permission is denied for `' . $file_name.'`';
+		
+		parent::__construct ($message, $code);
+	}
+}
+class FileNotExistException extends MinzException {
+	public function __construct ($file_name, $code = self::ERROR) {
+		$message = 'File doesn\'t exist : `' . $file_name.'`';
+		
+		parent::__construct ($message, $code);
+	}
+}
+class BadConfigurationException extends MinzException {
+	public function __construct ($part_missing, $code = self::ERROR) {
+		$message = '`' . $part_missing
+		         . '` in the configuration file is missing';
+		
+		parent::__construct ($message, $code);
+	}
+}
+class ControllerNotExistException extends MinzException {
+	public function __construct ($controller_name, $code = self::ERROR) {
+		$message = 'Controller `' . $controller_name
+		         . '` doesn\'t exist';
+		
+		parent::__construct ($message, $code);
+	}
+}
+class ControllerNotActionControllerException extends MinzException {
+	public function __construct ($controller_name, $code = self::ERROR) {
+		$message = 'Controller `' . $controller_name
+		         . '` isn\'t instance of ActionController';
+		
+		parent::__construct ($message, $code);
+	}
+}
+class ActionException extends MinzException {
+	public function __construct ($controller_name, $action_name, $code = self::ERROR) {
+		$message = '`' . $action_name . '` cannot be invoked on `'
+		         . $controller_name . '`';
+		
+		parent::__construct ($message, $code);
+	}
+}
+class RouteNotFoundException extends MinzException {
+	private $route;
+	
+	public function __construct ($route, $code = self::ERROR) {
+		$this->route = $route;
+		
+		$message = 'Route `' . $route . '` not found';
+		
+		parent::__construct ($message, $code);
+	}
+	
+	public function route () {
+		return $this->route;
+	}
+}
+class PDOConnectionException extends MinzException {
+	public function __construct ($string_connection, $user, $code = self::ERROR) {
+		$message = 'Access to database is denied for `' . $user . '`'
+		         . ' (`' . $string_connection . '`)';
+		
+		parent::__construct ($message, $code);
+	}
+}
+class CurrentPagePaginationException extends MinzException {
+	public function __construct ($page) {
+		$message = 'Page number `' . $page . '` doesn\'t exist';
+		
+		parent::__construct ($message, self::ERROR);
+	}
+}

+ 10 - 4
public/install.php

@@ -168,9 +168,8 @@ function saveStep3 () {
 	if (!empty ($_POST)) {
 		if (empty ($_POST['host']) ||
 		    empty ($_POST['user']) ||
-		    empty ($_POST['pass']) ||
 		    empty ($_POST['base'])) {
-			return false;
+			$_SESSION['bd_error'] = true;
 		}
 
 		$_SESSION['bd_host'] = $_POST['host'];
@@ -196,7 +195,10 @@ function saveStep3 () {
 		$res = checkBD ();
 
 		if ($res) {
+			$_SESSION['bd_error'] = false;
 			header ('Location: index.php?step=4');
+		} else {
+			$_SESSION['bd_error'] = true;
 		}
 	}
 }
@@ -275,11 +277,13 @@ function checkStep3 () {
 	      isset ($_SESSION['bd_user']) &&
 	      isset ($_SESSION['bd_pass']) &&
 	      isset ($_SESSION['bd_name']);
+	$conn = !isset ($_SESSION['bd_error']) || !$_SESSION['bd_error'];
 
 	return array (
 		'bd' => $bd ? 'ok' : 'ko',
+		'conn' => $conn ? 'ok' : 'ko',
 		'conf' => $conf ? 'ok' : 'ko',
-		'all' => $bd && $conf ? 'ok' : 'ko'
+		'all' => $bd && $conn && $conf ? 'ok' : 'ko'
 	);
 }
 function checkBD () {
@@ -358,8 +362,8 @@ function printStep1 () {
 	<p class="alert alert-error"><span class="alert-head"><?php echo _t ('damn'); ?></span> <?php echo _t ('minz_is_nok', LIB_PATH . '/minz'); ?></p>
 	<?php } ?>
 
-	<?php $version = curl_version(); ?>
 	<?php if ($res['curl'] == 'ok') { ?>
+	<?php $version = curl_version(); ?>
 	<p class="alert alert-success"><span class="alert-head"><?php echo _t ('ok'); ?></span> <?php echo _t ('curl_is_ok', $version['version']); ?></p>
 	<?php } else { ?>
 	<p class="alert alert-error"><span class="alert-head"><?php echo _t ('damn'); ?></span> <?php echo _t ('curl_is_nok'); ?></p>
@@ -467,6 +471,8 @@ function printStep3 () {
 ?>
 	<?php $s3 = checkStep3 (); if ($s3['all'] == 'ok') { ?>
 	<p class="alert alert-success"><span class="alert-head"><?php echo _t ('ok'); ?></span> <?php echo _t ('bdd_conf_is_ok'); ?></p>
+	<?php } elseif ($s3['conn'] == 'ko') { ?>
+	<p class="alert alert-error"><span class="alert-head"><?php echo _t ('damn'); ?></span> <?php echo _t ('bdd_conf_is_ko'); ?></p>
 	<?php } ?>
 
 	<form action="index.php?step=3" method="post">

+ 31 - 0
public/scripts/endless_mode.js

@@ -0,0 +1,31 @@
+var url_load_more = "";
+var load = false;
+
+function init_load_more() {
+	url_load_more = $("a#load_more").attr("href");
+	
+	$("#load_more").click (function () {
+		load_more_posts ();
+		
+		return false;
+	});
+}
+
+function load_more_posts () {
+	load = true;
+	$("#load_more").addClass("loading");
+	$.get (url_load_more, function (data) {
+		$("#stream .flux:last").after($("#stream .flux", data));
+		$(".pagination").html($(".pagination", data).html());
+		
+		init_load_more();
+		init_posts();
+		
+		$("#load_more").removeClass("loading");
+		load = false;
+	});
+}
+
+$(document).ready (function () {
+	init_load_more();
+});

File diff ditekan karena terlalu besar
+ 14 - 0
public/scripts/jquery.lazyload.min.js


+ 175 - 29
public/theme/freshrss.css

@@ -77,6 +77,7 @@
 	}
 
 .favicon {
+	height: 16px;
 	width: 16px;
 }
 
@@ -114,6 +115,9 @@
 			line-height: 35px;
 			float: right;
 		}
+		.categories .feeds .item.error .feed {
+			color: #BD362F;
+		}
 		.categories .feeds .item .feed {
 			display: inline-block;
 			margin: 0;
@@ -197,39 +201,39 @@
 		line-height: 25px;
 		border-top: 1px solid #ddd;
 	}
-		.flux_header .item.manage {
-			width: 60px;
+		.item.manage {
+			width: 80px;
 			white-space: nowrap;
 			font-size: 0px;
 			text-align: center;
 		}
-			.flux_header .item.manage .read {
+			.item.manage .read {
 				display: inline-block;
-				width: 30px;
+				width: 40px;
 				height: 40px;
 				background: url("icons/read.png") center center no-repeat;
 				background: url("icons/read.svg") center center no-repeat;
 				vertical-align: middle;
 			}
-				.flux_header .item.manage .read:hover {
+				.item.manage .read:hover {
 					text-decoration: none;
 				}
-				.flux.not_read .flux_header .item.manage .read {
+				.flux.not_read .item.manage .read {
 					background: url("icons/unread.png") center center no-repeat;
 					background: url("icons/unread.svg") center center no-repeat;
 				}
-			.flux_header .item.manage .bookmark {
+			.item.manage .bookmark {
 				display: inline-block;
-				width: 30px;
+				width: 40px;
 				height: 40px;
 				background: url("icons/non-starred.png") center center no-repeat;
 				background: url("icons/non-starred.svg") center center no-repeat;
 				vertical-align: middle;
 			}
-				.flux_header .item.manage .bookmark:hover {
+				.item.manage .bookmark:hover {
 					text-decoration: none;
 				}
-				.flux.favorite .flux_header .item.manage .bookmark {
+				.flux.favorite .item.manage .bookmark {
 					background: url("icons/starred.png") center center no-repeat;
 					background: url("icons/starred.svg") center center no-repeat;
 				}
@@ -240,9 +244,11 @@
 			text-overflow: ellipsis;
 			line-height: 40px;
 		}
+			.flux_header .item.website .favicon {
+				padding: 5px;
+			}
 			.flux_header .item.website a {
 				display: block;
-				padding: 0 5px;
 				height: 40px;
 			}
 		.flux_header .item.title {
@@ -265,12 +271,12 @@
 			cursor: pointer;
 		}
 		.flux_header .item.link {
-			width: 35px;
+			width: 40px;
 			text-align: center;
 		}
 			.flux_header .item.link a {
 				display: inline-block;
-				width: 35px;
+				width: 40px;
 				height: 40px;
 				background: url("icons/link.png") center center no-repeat;
 				background: url("icons/link.svg") center center no-repeat;
@@ -280,12 +286,64 @@
 					text-decoration: none;
 				}
 
+#stream.reader .flux {
+	padding: 0 0 30px;
+	border: none;
+	background: #f0f0f0;
+	color: #333;
+}
+	#stream.reader .flux .author {
+		margin: 0 0 10px;
+		font-size: 90%;
+		color: #666;
+	}
+
+#stream.global {
+	text-align: center;
+}
+	#stream.global .category {
+		display: inline-block;
+		width: 280px;
+		margin: 20px 10px;
+		vertical-align: top;
+		background: #fff;
+		border: 1px solid #aaa;
+		border-radius: 5px;
+		text-align: left;
+		box-shadow: 0 0 5px #bbb;
+	}
+		#stream.global .cat_header {
+			height: 35px;
+			padding: 0 10px;
+			background: #eee;
+			border-bottom: 1px solid #aaa;
+			border-radius: 5px 5px 0 0;
+			line-height: 35px;
+			font-size: 120%;
+		}
+			#stream.global .cat_header a {
+				color: #333;
+				text-shadow: 0 -1px 0px #aaa;
+			}
+		#stream.global .category .feeds {
+			max-height: 250px;
+			margin: 0;
+			list-style: none;
+			overflow: auto;
+		}
+			#stream.global .category .feeds .item {
+				padding: 2px 10px;
+				font-size: 90%;
+			}
+
 .content {
+	min-height: 300px;
 	max-width: 550px;
 	margin: 0 auto;
 	padding: 20px 10px;
 	line-height: 170%;
 	font-family: 'OpenSans';
+	word-wrap: break-word;
 }
 	.content .title {
 		margin: 0 0 5px;
@@ -301,15 +359,19 @@
 		display: block;
 		margin: 10px auto;
 	}
+	.content hr {
+		margin: 30px 0;
+		height: 1px;
+		background: #ddd;
+		border: 0;
+	}
 	.content pre {
-		width: 90%;
 		margin: 10px auto;
 		padding: 10px;
 		overflow: auto;
-		background: #666;
-		border: 1px solid #000;
-		color: #fafafa;
-		border-radius: 5px;
+		background: #000;
+		color: #fff;
+		font-size: 110%;
 	}
 	.content q, .content blockquote {
 		display: block;
@@ -342,20 +404,39 @@
 }
 	.pagination .item {
 		display: table-cell;
-		padding: 5px 10px;
-		border-top: 1px solid #aaa;
+		line-height: 40px;
 	}
+		.pagination .item.pager-current {
+			font-weight: bold;
+			font-size: 140%;
+		}
+		.pagination .item.pager-first,
+		.pagination .item.pager-previous,
+		.pagination .item.pager-next,
+		.pagination .item.pager-last {
+			width: 100px;
+		}
 		.pagination .item a {
+			display: block;
 			color: #333;
 			font-style: italic;
 		}
-	.pagination .pager-previous, .pagination .pager-next {
-		width: 200px;
+	.pagination:first-child .item {
+		border-bottom: 1px solid #aaa;
 	}
-	.pagination .item.pager-current {
-		font-weight: bold;
+	.pagination:last-child .item {
+		border-top: 1px solid #aaa;
 	}
 
+.nav_entries {
+	display: none;
+}
+
+.loading {
+	background: url("loader.gif") center center no-repeat;
+	font-size: 0;
+}
+
 /*** NOTIFICATION ***/
 .notification {
 	position: fixed;
@@ -415,6 +496,33 @@
 		vertical-align: middle;
 	}
 
+.logs {
+	border: 1px solid #aaa;
+}
+	.logs .log {
+		padding: 5px 2%;
+		overflow: auto;
+		background: #fafafa;
+		border-bottom: 1px solid #999;
+		color: #333;
+		font-size: 90%;
+	}
+		.logs .log .date {
+			display: block;
+		}
+	.logs .log.error {
+		background: #fdd;
+		color: #844;
+	}
+	.logs .log.warning {
+		background: #ffe;
+		color: #c95;
+	}
+	.logs .log.notice {
+		background: #f4f4f4;
+		color: #aaa;
+	}
+
 @media(max-width: 840px) {
 	.header,
 	.aside .btn-important,
@@ -425,8 +533,19 @@
 		display: none;
 	}
 	.flux_header .item.website {
-		width: 30px;
+		width: 40px;
 		text-align: center;
+	}
+		.flux_header .item.website .favicon {
+			padding: 12px;
+		}
+
+	.content {
+		font-size: 120%;
+	}
+
+	.pagination {
+		margin: 0 0 40px;
 	}
 	.pagination .pager-previous, .pagination .pager-next {
 		width: 100px;
@@ -440,26 +559,53 @@
 		top: 0; left: 0;
 		width: 0;
 		overflow: hidden;
+		border-right: none;
 		z-index: 10;
 		transition: width 200ms linear;
 	}
 		.aside:target {
 			width: 80%;
+			border-right: 1px solid #aaa;
 			overflow: auto;
 		}
 		.aside .toggle_aside {
 			position: absolute;
 			right: 0;
 			display: inline-block;
-			width: 20px;
-			height: 20px;
+			width: 26px;
+			height: 26px;
 			margin: 0 10px 0 0;
 			border: 1px solid #ccc;
-			border-radius: 10px;
+			border-radius: 20px;
 			text-align: center;
-			line-height: 20px;
+			line-height: 26px;
 		}
 		.aside .categories {
 			margin: 30px 0;
 		}
+
+	.nav_entries {
+		display: table;
+		width: 100%;
+		height: 40px;
+		position: fixed;
+		bottom: 0;
+		margin: 0;
+		background: #fff;
+		border-top: 1px solid #ddd;
+		text-align: center;
+		line-height: 40px;
+		table-layout: fixed;
+	}
+		.nav_entries .item {
+			display: table-cell;
+			width: 30%;
+		}
+			.nav_entries .item a {
+				display: block;
+			}
+			.nav_entries .item .icon.i_up {
+				margin: 5px 0 0;
+				vertical-align: top;
+			}
 }

+ 31 - 6
public/theme/global.css

@@ -50,6 +50,11 @@ img {
 		border: none;
 	}
 
+/* VIDEOS */
+iframe, embed, object {
+	max-width: 100%;
+}
+
 /* FORMULAIRES */
 legend {
 	display: block;
@@ -83,8 +88,8 @@ input, select, textarea {
 }
 	input[type="radio"],
 	input[type="checkbox"] {
-		width: 15px;
-		min-height: 15px;
+		width: 15px !important;
+		min-height: 15px !important;
 	}
 	input:focus, select:focus, textarea:focus {
 		color: #0062BE;
@@ -183,6 +188,7 @@ input, select, textarea {
 	line-height: 20px;
 	vertical-align: middle;
 	cursor: pointer;
+	overflow: hidden;
 }
 	a.btn {
 		min-height: 25px;
@@ -261,6 +267,13 @@ input, select, textarea {
 		.nav.nav-list a:hover {
 			text-decoration: none;
 		}
+	.nav.nav-list .item.error a {
+		color: #BD362F;
+	}
+		.nav.nav-list .item.active.error a {
+			color: #fff;
+			background: #BD362F;
+		}
 
 	.nav.nav-list .nav-header {
 		padding: 0 10px;
@@ -384,15 +397,19 @@ input, select, textarea {
 		display: inline-block;
 		position: absolute;
 		top: -16px; right: -16px;
-		width: 16px;
-		height: 16px;
-		padding: 5px;
+		width: 26px;
+		height: 26px;
 		background: #fff;
 		border-radius: 50px;
 		border: 1px solid #ddd;
-		line-height: 16px;
+		line-height: 26px;
 		text-align: center;
 	}
+		.dropdown .dropdown-close a {
+			display: block;
+			width: 100%;
+			height: 100%;
+		}
 		.dropdown .dropdown-close:hover {
 			background: #f4f4f4;
 		}
@@ -501,6 +518,14 @@ input, select, textarea {
 		background-image: url("icons/up.png");
 		background-image: url("icons/up.svg");
 	}
+	.icon.i_next {
+		background-image: url("icons/next.png");
+		background-image: url("icons/next.svg");
+	}
+	.icon.i_prev {
+		background-image: url("icons/previous.png");
+		background-image: url("icons/previous.svg");
+	}
 	.icon.i_help {
 		background-image: url("icons/help.png");
 		background-image: url("icons/help.svg");

TEMPAT SAMPAH
public/theme/icons/next.png


+ 31 - 0
public/theme/icons/next.svg

@@ -0,0 +1,31 @@
+<?xml version='1.0' encoding='UTF-8' standalone='no'?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg xmlns:cc='http://creativecommons.org/ns#' xmlns:dc='http://purl.org/dc/elements/1.1/' sodipodi:docname='go-next-symbolic.svg' height='16' id='svg7384' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:svg='http://www.w3.org/2000/svg' inkscape:version='0.48.4 r9939' version='1.1' width='16' xmlns='http://www.w3.org/2000/svg'>
+  <metadata id='metadata90'>
+    <rdf:RDF>
+      <cc:Work rdf:about=''>
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage'/>
+        <dc:title>Gnome Symbolic Icon Theme</dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview inkscape:bbox-nodes='false' inkscape:bbox-paths='true' bordercolor='#666666' borderopacity='1' inkscape:current-layer='layer12' inkscape:cx='78.648774' inkscape:cy='9.99302' gridtolerance='10' inkscape:guide-bbox='true' guidetolerance='10' id='namedview88' inkscape:object-nodes='false' inkscape:object-paths='false' objecttolerance='10' pagecolor='#3a3b39' inkscape:pageopacity='1' inkscape:pageshadow='2' showborder='false' showgrid='false' showguides='true' inkscape:snap-bbox='true' inkscape:snap-bbox-midpoints='false' inkscape:snap-global='true' inkscape:snap-grids='true' inkscape:snap-nodes='false' inkscape:snap-others='false' inkscape:snap-to-guides='true' inkscape:window-height='1408' inkscape:window-maximized='1' inkscape:window-width='2560' inkscape:window-x='0' inkscape:window-y='0' inkscape:zoom='1'>
+    <inkscape:grid empspacing='2' enabled='true' id='grid4866' originx='120px' originy='530px' snapvisiblegridlinesonly='true' spacingx='1px' spacingy='1px' type='xygrid' visible='true'/>
+  </sodipodi:namedview>
+  <title id='title9167'>Gnome Symbolic Icon Theme</title>
+  <defs id='defs7386'/>
+  <g inkscape:groupmode='layer' id='layer9' inkscape:label='status' style='display:inline' transform='translate(-121.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='layer10' inkscape:label='devices' transform='translate(-121.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='layer11' inkscape:label='apps' transform='translate(-121.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='layer13' inkscape:label='places' transform='translate(-121.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='layer14' inkscape:label='mimetypes' transform='translate(-121.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='layer15' inkscape:label='emblems' style='display:inline' transform='translate(-121.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='g71291' inkscape:label='emotes' style='display:inline' transform='translate(-121.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='g4953' inkscape:label='categories' style='display:inline' transform='translate(-121.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='layer12' inkscape:label='actions' style='display:inline' transform='translate(-121.0002,-747)'>
+    
+    <path inkscape:connector-curvature='0' d='m 125.0004,749 1,0 c 0.0104,-1.2e-4 0.0208,-4.6e-4 0.0313,0 0.25495,0.0112 0.50987,0.12858 0.6875,0.3125 l 6.29767,5.71875 -6.29772,5.71875 c -0.18816,0.18819 -0.45346,0.28125 -0.71875,0.28125 l -1,0 0,-1 c 0,-0.26529 0.0931,-0.53058 0.28125,-0.71875 l 4.82897,-4.28125 -4.82897,-4.28125 c -0.21074,-0.19463 -0.30316,-0.46925 -0.28125,-0.75 z' id='path10839-9-9-5-9' sodipodi:nodetypes='ccsccccccccccc' style='font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;color:#bebebe;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.78124988;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new;font-family:Andale Mono;-inkscape-font-specification:Andale Mono'/>
+  </g>
+</svg>

TEMPAT SAMPAH
public/theme/icons/previous.png


+ 31 - 0
public/theme/icons/previous.svg

@@ -0,0 +1,31 @@
+<?xml version='1.0' encoding='UTF-8' standalone='no'?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg xmlns:cc='http://creativecommons.org/ns#' xmlns:dc='http://purl.org/dc/elements/1.1/' sodipodi:docname='go-next-rtl-symbolic.svg' height='16' id='svg7384' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:svg='http://www.w3.org/2000/svg' inkscape:version='0.48.4 r9939' version='1.1' width='16' xmlns='http://www.w3.org/2000/svg'>
+  <metadata id='metadata90'>
+    <rdf:RDF>
+      <cc:Work rdf:about=''>
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage'/>
+        <dc:title>Gnome Symbolic Icon Theme</dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview inkscape:bbox-nodes='false' inkscape:bbox-paths='true' bordercolor='#666666' borderopacity='1' inkscape:current-layer='layer12' inkscape:cx='-101.35123' inkscape:cy='9.99302' gridtolerance='10' inkscape:guide-bbox='true' guidetolerance='10' id='namedview88' inkscape:object-nodes='false' inkscape:object-paths='false' objecttolerance='10' pagecolor='#3a3b39' inkscape:pageopacity='1' inkscape:pageshadow='2' showborder='false' showgrid='false' showguides='true' inkscape:snap-bbox='true' inkscape:snap-bbox-midpoints='false' inkscape:snap-global='true' inkscape:snap-grids='true' inkscape:snap-nodes='false' inkscape:snap-others='false' inkscape:snap-to-guides='true' inkscape:window-height='1408' inkscape:window-maximized='1' inkscape:window-width='2560' inkscape:window-x='0' inkscape:window-y='0' inkscape:zoom='1'>
+    <inkscape:grid empspacing='2' enabled='true' id='grid4866' originx='-60px' originy='530px' snapvisiblegridlinesonly='true' spacingx='1px' spacingy='1px' type='xygrid' visible='true'/>
+  </sodipodi:namedview>
+  <title id='title9167'>Gnome Symbolic Icon Theme</title>
+  <defs id='defs7386'/>
+  <g inkscape:groupmode='layer' id='layer9' inkscape:label='status' style='display:inline' transform='translate(-301.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='layer10' inkscape:label='devices' transform='translate(-301.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='layer11' inkscape:label='apps' transform='translate(-301.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='layer13' inkscape:label='places' transform='translate(-301.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='layer14' inkscape:label='mimetypes' transform='translate(-301.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='layer15' inkscape:label='emblems' style='display:inline' transform='translate(-301.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='g71291' inkscape:label='emotes' style='display:inline' transform='translate(-301.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='g4953' inkscape:label='categories' style='display:inline' transform='translate(-301.0002,-747)'/>
+  <g inkscape:groupmode='layer' id='layer12' inkscape:label='actions' style='display:inline' transform='translate(-301.0002,-747)'>
+    
+    <path inkscape:connector-curvature='0' d='m 313.01372,749 -1,0 c -0.0104,-1.2e-4 -0.0208,-4.6e-4 -0.0313,0 -0.25495,0.0112 -0.50987,0.12858 -0.6875,0.3125 l -6.29767,5.71875 6.29772,5.71875 c 0.18816,0.18819 0.45346,0.28125 0.71875,0.28125 l 1,0 0,-1 c 0,-0.26529 -0.0931,-0.53058 -0.28125,-0.71875 l -4.82897,-4.28125 4.82897,-4.28125 c 0.21074,-0.19463 0.30316,-0.46925 0.28125,-0.75 z' id='path5441' sodipodi:nodetypes='ccsccccccccccc' style='font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;color:#bebebe;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.78124988;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new;font-family:Andale Mono;-inkscape-font-specification:Andale Mono'/>
+  </g>
+</svg>

TEMPAT SAMPAH
public/theme/loader.gif


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini