Kaynağa Gözat

Refactor feedController

See https://github.com/marienfressinaud/FreshRSS/issues/655
Marien Fressinaud 11 yıl önce
ebeveyn
işleme
e2da6e6e6b
2 değiştirilmiş dosya ile 330 ekleme ve 288 silme
  1. 323 288
      app/Controllers/feedController.php
  2. 7 0
      lib/Minz/ModelPdo.php

+ 323 - 288
app/Controllers/feedController.php

@@ -1,6 +1,14 @@
 <?php
 
+/**
+ * Controller to handle every feed actions.
+ */
 class FreshRSS_feed_Controller extends Minz_ActionController {
+	/**
+	 * This action is called before every other action in that class. It is
+	 * the common boiler plate for every action. It is triggered by the
+	 * underlying framework.
+	 */
 	public function firstAction() {
 		if (!$this->view->loginOk) {
 			// Token is useful in the case that anonymous refresh is forbidden
@@ -10,9 +18,8 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			$token_param = Minz_Request::param('token', '');
 			$token_is_ok = ($token != '' && $token == $token_param);
 			$action = Minz_Request::actionName();
-			if (!(($token_is_ok || Minz_Configuration::allowAnonymousRefresh()) &&
-				$action === 'actualize')
-			) {
+			if ($action !== 'actualize' ||
+					!(Minz_Configuration::allowAnonymousRefresh() || $token_is_ok)) {
 				Minz_Error::error(
 					403,
 					array('error' => array(_t('access_denied')))
@@ -21,10 +28,32 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 	}
 
+	/**
+	 * This action subscribes to a feed.
+	 *
+	 * It can be reached by both GET and POST requests.
+	 *
+	 * GET request displays a form to add and configure a feed.
+	 * Request parameter is:
+	 *   - url_rss (default: false)
+	 *
+	 * POST request adds a feed in database.
+	 * Parameters are:
+	 *   - url_rss (default: false)
+	 *   - category (default: false)
+	 *   - new_category (required if category == 'nc')
+	 *   - http_user (default: false)
+	 *   - http_pass (default: false)
+	 * It tries to get website information from RSS feed.
+	 * If no category is given, feed is added to the default one.
+	 *
+	 * If url_rss is false, nothing happened.
+	 */
 	public function addAction() {
-		$url = Minz_Request::param('url_rss', false);
+		$url = Minz_Request::param('url_rss');
 
 		if ($url === false) {
+			// No url, do nothing
 			Minz_Request::forward(array(
 				'c' => 'subscription',
 				'a' => 'index'
@@ -33,14 +62,19 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$this->catDAO = new FreshRSS_CategoryDAO();
-		$this->catDAO->checkDefault();
+		$url_redirect = array(
+			'c' => 'subscription',
+			'a' => 'index',
+			'params' => array(),
+		);
 
 		if (Minz_Request::isPost()) {
 			@set_time_limit(300);
 
-
-			$cat = Minz_Request::param('category', false);
+			$cat = Minz_Request::param('category');
 			if ($cat === 'nc') {
+				// User want to create a new category, new_category parameter
+				// must exist
 				$new_cat = Minz_Request::param('new_category');
 				if (empty($new_cat['name'])) {
 					$cat = false;
@@ -48,139 +82,114 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 					$cat = $this->catDAO->addCategory($new_cat);
 				}
 			}
+
 			if ($cat === false) {
+				// If category was not given or if creating new category failed,
+				// get the default category
+				$this->catDAO->checkDefault();
 				$def_cat = $this->catDAO->getDefault();
 				$cat = $def_cat->id();
 			}
 
+			// HTTP information are useful if feed is protected behind a
+			// HTTP authentication
 			$user = Minz_Request::param('http_user');
 			$pass = Minz_Request::param('http_pass');
-			$params = array();
+			$http_auth = '';
+			if ($user != '' || $pass != '') {
+				$http_auth = $user . ':' . $pass;
+			}
 
-			$transactionStarted = false;
+			$transaction_started = false;
 			try {
 				$feed = new FreshRSS_Feed($url);
-				$feed->_category($cat);
-
-				$httpAuth = '';
-				if ($user != '' || $pass != '') {
-					$httpAuth = $user . ':' . $pass;
-				}
-				$feed->_httpAuth($httpAuth);
-
-				$feed->load(true);
-
-				$values = array(
-					'url' => $feed->url(),
-					'category' => $feed->category(),
-					'name' => $feed->name(),
-					'website' => $feed->website(),
-					'description' => $feed->description(),
-					'lastUpdate' => time(),
-					'httpAuth' => $feed->httpAuth(),
-				);
-
-				if ($feedDAO->searchByUrl($values['url'])) {
-					// on est déjà abonné à ce flux
-					$notif = array(
-						'type' => 'bad',
-						'content' => _t('already_subscribed', $feed->name())
-					);
-					Minz_Session::_param('notification', $notif);
-				} else {
-					$id = $feedDAO->addFeed($values);
-					if (!$id) {
-						// problème au niveau de la base de données
-						$notif = array(
-							'type' => 'bad',
-							'content' => _t('feed_not_added', $feed->name())
-						);
-						Minz_Session::_param('notification', $notif);
-					} else {
-						$feed->_id($id);
-						$feed->faviconPrepare();
-
-						$is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
-
-						$entryDAO = FreshRSS_Factory::createEntryDao();
-						$entries = array_reverse($feed->entries());	//We want chronological order and SimplePie uses reverse order
-
-						// on calcule la date des articles les plus anciens qu'on accepte
-						$nb_month_old = $this->view->conf->old_entries;
-						$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
-
-						//MySQL: http://docs.oracle.com/cd/E17952_01/refman-5.5-en/optimizing-innodb-transaction-management.html
-						//SQLite: http://stackoverflow.com/questions/1711631/how-do-i-improve-the-performance-of-sqlite
-						$preparedStatement = $entryDAO->addEntryPrepare();
-						$transactionStarted = true;
-						$feedDAO->beginTransaction();
-						// on ajoute les articles en masse sans vérification
-						foreach ($entries as $entry) {
-							$values = $entry->toArray();
-							$values['id_feed'] = $feed->id();
-							$values['id'] = min(time(), $entry->date(true)) . uSecString();
-							$values['is_read'] = $is_read;
-							$entryDAO->addEntry($values, $preparedStatement);
-						}
-						$feedDAO->updateLastUpdate($feed->id());
-						if ($transactionStarted) {
-							$feedDAO->commit();
-						}
-						$transactionStarted = false;
-
-						// ok, ajout terminé
-						$notif = array(
-							'type' => 'good',
-							'content' => _t('feed_added', $feed->name())
-						);
-						Minz_Session::_param('notification', $notif);
-
-						// permet de rediriger vers la page de conf du flux
-						$params['id'] = $feed->id();
-					}
-				}
 			} catch (FreshRSS_BadUrl_Exception $e) {
+				// Given url was not a valid url!
 				Minz_Log::warning($e->getMessage());
-				$notif = array(
-					'type' => 'bad',
-					'content' => _t('invalid_url', $url)
-				);
-				Minz_Session::_param('notification', $notif);
+				Minz_Request::bad(_t('invalid_url', $url), $url_redirect);
+			}
+
+			try {
+				$feed->load(true);
 			} catch (FreshRSS_Feed_Exception $e) {
+				// Something went bad (timeout, server not found, etc.)
 				Minz_Log::warning($e->getMessage());
-				$notif = array(
-					'type' => 'bad',
-					'content' => _t('internal_problem_feed',
-					                Minz_Url::display(array('a' => 'logs')))
+				Minz_Request::bad(
+					_t('internal_problem_feed', _url('index', 'logs')),
+					$url_redirect
 				);
-				Minz_Session::_param('notification', $notif);
 			} catch (Minz_FileNotExistException $e) {
-				// Répertoire de cache n'existe pas
+				// Cache directory doesn't exist!
 				Minz_Log::error($e->getMessage());
-				$notif = array(
-					'type' => 'bad',
-					'content' => _t('internal_problem_feed',
-					                Minz_Url::display(array('a' => 'logs')))
+				Minz_Request::bad(
+					_t('internal_problem_feed', _url('index', 'logs')),
+					$url_redirect
 				);
-				Minz_Session::_param('notification', $notif);
 			}
-			if ($transactionStarted) {
-				$feedDAO->rollBack();
+
+			if ($feedDAO->searchByUrl($feed->url())) {
+				Minz_Request::bad(_t('already_subscribed', $feed->name()), $url_redirect);
 			}
 
-			Minz_Request::forward(array(
-				'c' => 'subscription',
-				'a' => 'index',
-				'params' => $params
-			), true);
-		} else {
+			$feed->_category($cat);
+			$feed->_httpAuth($http_auth);
+
+			$values = array(
+				'url' => $feed->url(),
+				'category' => $feed->category(),
+				'name' => $feed->name(),
+				'website' => $feed->website(),
+				'description' => $feed->description(),
+				'lastUpdate' => time(),
+				'httpAuth' => $feed->httpAuth(),
+			);
+
+			$id = $feedDAO->addFeed($values);
+			if (!$id) {
+				// There was an error in database... we cannot say what here.
+				Minz_Request::bad(_t('feed_not_added', $feed->name()), $url_redirect);
+			}
+
+			// Ok, feed has been added in database. Now we have to refresh entries.
+			$feed->_id($id);
+			$feed->faviconPrepare();
+
+			$is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
+
+			$entryDAO = FreshRSS_Factory::createEntryDao();
+			// We want chronological order and SimplePie uses reverse order.
+			$entries = array_reverse($feed->entries());
+
+			// Calculate date of oldest entries we accept in DB.
+			$nb_month_old = $this->view->conf->old_entries;
+			$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
+
+			// Use a shared statement and a transaction to improve a LOT the
+			// performances.
+			$prepared_statement = $entryDAO->addEntryPrepare();
+			$feedDAO->beginTransaction();
+			foreach ($entries as $entry) {
+				// Entries are added without any verification.
+				$values = $entry->toArray();
+				$values['id_feed'] = $feed->id();
+				$values['id'] = min(time(), $entry->date(true)) . uSecString();
+				$values['is_read'] = $is_read;
+				$entryDAO->addEntry($values, $prepared_statement);
+			}
+			$feedDAO->updateLastUpdate($feed->id());
+			$feedDAO->commit();
 
-			// GET request so we must ask confirmation to user
+			// Entries are in DB, we redirect to feed configuration page.
+			$url_redirect['params']['id'] = $feed->id();
+			Minz_Request::good(_t('feed_added', $feed->name()), $url_redirect);
+		} else {
+			// GET request: we must ask confirmation to user before adding feed.
 			Minz_View::prependTitle(_t('add_rss_feed') . ' · ');
+
 			$this->view->categories = $this->catDAO->listCategories(false);
 			$this->view->feed = new FreshRSS_Feed($url);
 			try {
-				// We try to get some more information about the feed
+				// We try to get more information about the feed.
 				$this->view->feed->load(true);
 				$this->view->load_ok = true;
 			} catch (Exception $e) {
@@ -189,43 +198,53 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 
 			$feed = $feedDAO->searchByUrl($this->view->feed->url());
 			if ($feed) {
-				// Already subscribe so we redirect to the feed configuration page
-				$notif = array(
-					'type' => 'bad',
-					'content' => _t('already_subscribed', $feed->name())
-				);
-				Minz_Session::_param('notification', $notif);
-
-				Minz_Request::forward(array(
-					'c' => 'subscription',
-					'a' => 'index',
-					'params' => array(
-						'id' => $feed->id()
-					)
-				), true);
+				// Already subscribe so we redirect to the feed configuration page.
+				$url_redirect['params']['id'] = $feed->id();
+				Minz_Request::good(_t('already_subscribed', $feed->name()), $url_redirect);
 			}
 		}
 	}
 
+	/**
+	 * This action remove entries from a given feed.
+	 *
+	 * It should be reached by a POST action.
+	 *
+	 * Parameter is:
+	 *   - id (default: false)
+	 */
 	public function truncateAction() {
-		if (Minz_Request::isPost()) {
-			$id = Minz_Request::param('id');
-			$feedDAO = FreshRSS_Factory::createFeedDao();
-			$n = $feedDAO->truncate($id);
-			$notif = array(
-				'type' => $n === false ? 'bad' : 'good',
-				'content' => _t('n_entries_deleted', $n)
-			);
-			Minz_Session::_param('notification', $notif);
-			invalidateHttpCache();
-			Minz_Request::forward(array(
-				'c' => 'subscription',
-				'a' => 'index',
-				'params' => array('id' => $id)
-			), true);
+		$id = Minz_Request::param('id');
+		$url_redirect = array(
+			'c' => 'subscription',
+			'a' => 'index',
+			'params' => array('id' => $id)
+		);
+
+		if (!Minz_Request::isPost()) {
+			Minz_Request::forward($url_redirect, true);
+		}
+
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$n = $feedDAO->truncate($id);
+
+		invalidateHttpCache();
+		if ($n === false) {
+			Minz_Request::bad(_t('error_occurred'), $url_redirect);
+		} else {
+			Minz_Request::good(_t('n_entries_deleted', $n), $url_redirect);
 		}
 	}
 
+	/**
+	 * This action actualizes entries from one or several feeds.
+	 *
+	 * Parameters are:
+	 *   - id (default: false)
+	 *   - force (default: false)
+	 * If id is not specified, all the feeds are actualized. But if force is
+	 * false, process stops at 10 feeds to avoid time execution problem.
+	 */
 	public function actualizeAction() {
 		@set_time_limit(300);
 
@@ -234,213 +253,229 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 
 		Minz_Session::_param('actualize_feeds', false);
 		$id = Minz_Request::param('id');
-		$force = Minz_Request::param('force', false);
+		$force = Minz_Request::param('force');
 
-		// on créé la liste des flux à mettre à actualiser
-		// si on veut mettre un flux à jour spécifiquement, on le met
-		// dans la liste, mais seul (permet d'automatiser le traitement)
+		// Create a list of feeds to actualize.
+		// If id is set and valid, corresponding feed is added to the list but
+		// alone in order to automatize further process.
 		$feeds = array();
 		if ($id) {
 			$feed = $feedDAO->searchById($id);
 			if ($feed) {
-				$feeds = array($feed);
+				$feeds[] = $feed;
 			}
 		} else {
 			$feeds = $feedDAO->listFeedsOrderUpdate($this->view->conf->ttl_default);
 		}
 
-		// on calcule la date des articles les plus anciens qu'on accepte
+		// Calculate date of oldest entries we accept in DB.
 		$nb_month_old = max($this->view->conf->old_entries, 1);
 		$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
 
-		$i = 0;
-		$flux_update = 0;
+		$updated_feeds = 0;
 		$is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
 		foreach ($feeds as $feed) {
 			if (!$feed->lock()) {
 				Minz_Log::notice('Feed already being actualized: ' . $feed->url());
 				continue;
 			}
-			try {
-				$url = $feed->url();
-				$feedHistory = $feed->keepHistory();
 
+			try {
+				// Load entries
 				$feed->load(false);
-				$entries = array_reverse($feed->entries());	//We want chronological order and SimplePie uses reverse order
-				$hasTransaction = false;
-
-				if (count($entries) > 0) {
-					//For this feed, check last n entry GUIDs already in database
-					$existingGuids = array_fill_keys(
-						$entryDAO->listLastGuidsByFeed($feed->id(),
-						                               count($entries) + 10), 1);
-					$useDeclaredDate = empty($existingGuids);
-
-					if ($feedHistory == -2) {	//default
-						$feedHistory = $this->view->conf->keep_history_default;
-					}
+			} catch (FreshRSS_Feed_Exception $e) {
+				Minz_Log::notice($e->getMessage());
+				$feedDAO->updateLastUpdate($feed->id(), 1);
+				continue;
+			}
 
-					$preparedStatement = $entryDAO->addEntryPrepare();
-					$hasTransaction = true;
-					$feedDAO->beginTransaction();
+			$url = $feed->url();
+			$feed_history = $feed->keepHistory();
+			if ($feed_history == -2) {
+				// TODO: -2 must be a constant!
+				// -2 means we take the default value from configuration
+				$feed_history = $this->view->conf->keep_history_default;
+			}
 
-					// On ne vérifie pas strictement que l'article n'est pas déjà en BDD
-					// La BDD refusera l'ajout car (id_feed, guid) doit être unique
-					foreach ($entries as $entry) {
-						$eDate = $entry->date(true);
-						if ((!isset($existingGuids[$entry->guid()])) &&
-							(($feedHistory != 0) || ($eDate  >= $date_min))) {
-							$values = $entry->toArray();
-							//Use declared date at first import, otherwise use discovery date
-							$values['id'] = ($useDeclaredDate || $eDate < $date_min) ?
-								min(time(), $eDate) . uSecString() :
-								uTimeString();
-							$values['is_read'] = $is_read;
-							$entryDAO->addEntry($values, $preparedStatement);
-						}
+			// We want chronological order and SimplePie uses reverse order.
+			$entries = array_reverse($feed->entries());
+			if (count($entries) > 0) {
+				// For this feed, check last n entry GUIDs already in database.
+				$existing_guids = array_fill_keys($entryDAO->listLastGuidsByFeed(
+					$feed->id(), count($entries) + 10
+				), 1);
+				$use_declared_date = empty($existing_guids);
+
+				// Add entries in database if possible.
+				$prepared_statement = $entryDAO->addEntryPrepare();
+				$feedDAO->beginTransaction();
+				foreach ($entries as $entry) {
+					$entry_date = $entry->date(true);
+					if (isset($existing_guids[$entry->guid()]) ||
+							($feed_history == 0 && $entry_date < $date_min)) {
+						// This entry already exists in DB or should not be added
+						// considering configuration and date.
+						continue;
 					}
-				}
 
-				if (($feedHistory >= 0) && (rand(0, 30) === 1)) {
-					if (!$hasTransaction) {
-						$feedDAO->beginTransaction();
-					}
-					$nb = $feedDAO->cleanOldEntries($feed->id(), $date_min, max($feedHistory, count($entries) + 10));
-					if ($nb > 0) {
-						Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url() . ']');
+					$id = uTimeString();
+					if ($use_declared_date || $entry_date < $date_min) {
+						// Use declared date at first import.
+						$id = min(time(), $entry_date) . uSecString();
 					}
+
+					$values = $entry->toArray();
+					$values['id'] = $id;
+					$values['is_read'] = $is_read;
+					$entryDAO->addEntry($values, $prepared_statement);
 				}
+			}
 
-				// on indique que le flux vient d'être mis à jour en BDD
-				$feedDAO->updateLastUpdate($feed->id(), 0, $hasTransaction);
-				if ($hasTransaction) {
-					$feedDAO->commit();
+			if ($feed_history >= 0 && rand(0, 30) === 1) {
+				// TODO: move this function in web cron when available
+				// Remove old entries once in 30.
+				if (!$feedDAO->hasTransaction()) {
+					$feedDAO->beginTransaction();
 				}
-				$flux_update++;
-				if (($feed->url() !== $url)) {	//HTTP 301 Moved Permanently
-					Minz_Log::notice('Feed ' . $url . ' moved permanently to ' . $feed->url());
-					$feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
+
+				$nb = $feedDAO->cleanOldEntries($feed->id(),
+				                                $date_min,
+				                                max($feed_history, count($entries) + 10));
+				if ($nb > 0) {
+					Minz_Log::debug($nb . ' old entries cleaned in feed [' .
+					                $feed->url() . ']');
 				}
-			} catch (FreshRSS_Feed_Exception $e) {
-				Minz_Log::notice($e->getMessage());
-				$feedDAO->updateLastUpdate($feed->id(), 1);
+			}
+
+			$feedDAO->updateLastUpdate($feed->id(), 0, $feedDAO->hasTransaction());
+			if ($feedDAO->hasTransaction()) {
+				$feedDAO->commit();
+			}
+
+			if ($feed->url() !== $url) {
+				// HTTP 301 Moved Permanently
+				Minz_Log::notice('Feed ' . $url . ' moved permanently to ' . $feed->url());
+				$feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
 			}
 
 			$feed->faviconPrepare();
 			$feed->unlock();
+			$updated_feeds++;
 			unset($feed);
 
-			// On arrête à 10 flux pour ne pas surcharger le serveur
-			// sauf si le paramètre $force est à vrai
-			$i++;
-			if ($i >= 10 && !$force) {
+			// No more than 10 feeds unless $force is true to avoid overloading
+			// the server.
+			if ($updated_feeds >= 10 && !$force) {
 				break;
 			}
 		}
 
-		$url = array();
-		if ($flux_update === 1) {
-			// on a mis un seul flux à jour
-			$feed = reset($feeds);
+		if (Minz_Request::param('ajax')) {
+			// Most of the time, ajax request is for only one feed. But since
+			// there are several parallel requests, we should return that there
+			// are several updated feeds.
 			$notif = array(
 				'type' => 'good',
-				'content' => _t('feed_actualized', $feed->name())
-			);
-		} elseif ($flux_update > 1) {
-			// plusieurs flux on été mis à jour
-			$notif = array(
-				'type' => 'good',
-				'content' => _t('n_feeds_actualized', $flux_update)
-			);
-		} else {
-			// aucun flux n'a été mis à jour, oups
-			$notif = array(
-				'type' => 'good',
-				'content' => _t('no_feed_to_refresh')
+				'content' => _t('feeds_actualized')
 			);
+			Minz_Session::_param('notification', $notif);
+			// No layout in ajax request.
+			$this->view->_useLayout(false);
+			return;
 		}
 
-		if ($i === 1) {
-			// Si on a voulu mettre à jour qu'un flux
-			// on filtre l'affichage par ce flux
+		// Redirect to the main page with correct notification.
+		if ($updated_feeds === 1) {
 			$feed = reset($feeds);
-			$url['params'] = array('get' => 'f_' . $feed->id());
-		}
-
-		if (Minz_Request::param('ajax', 0) === 0) {
-			Minz_Session::_param('notification', $notif);
-			Minz_Request::forward($url, true);
+			Minz_Request::good(_t('feed_actualized', $feed->name()),
+			                   array('get' => 'f_' . $feed->id()));
+		} elseif ($updated_feeds > 1) {
+			Minz_Request::good(_t('n_feeds_actualized', $updated_feeds), array());
 		} else {
-			// Une requête Ajax met un seul flux à jour.
-			// Comme en principe plusieurs requêtes ont lieu,
-			// on indique que "plusieurs flux ont été mis à jour".
-			// Cela permet d'avoir une notification plus proche du
-			// ressenti utilisateur
-			$notif = array(
-				'type' => 'good',
-				'content' => _t('feeds_actualized')
-			);
-			Minz_Session::_param('notification', $notif);
-			// et on désactive le layout car ne sert à rien
-			$this->view->_useLayout(false);
+			Minz_Request::good(_t('no_feed_to_refresh'), array());
 		}
 	}
 
+	/**
+	 * This action changes the category of a feed.
+	 *
+	 * This page must be reached by a POST request.
+	 *
+	 * Parameters are:
+	 *   - f_id (default: false)
+	 *   - c_id (default: false)
+	 * If c_id is false, default category is used.
+	 *
+	 * @todo should handle order of the feed inside the category.
+	 */
 	public function moveAction() {
-		if (Minz_Request::isPost()) {
-			$feed_id = Minz_Request::param('f_id');
-			$cat_id = Minz_Request::param('c_id');
+		if (!Minz_Request::isPost()) {
+			Minz_Request::forward(array('c' => 'subscription'), true);
+		}
 
-			$feedDAO = FreshRSS_Factory::createFeedDao();
-			$values = array(
-				'category' => $cat_id,
-			);
+		$feed_id = Minz_Request::param('f_id');
+		$cat_id = Minz_Request::param('c_id');
 
-			$feed = $feedDAO->searchById($feed_id);
+		if ($cat_id === false) {
+			// If category was not given get the default one.
+			$catDAO = new FreshRSS_CategoryDAO();
+			$catDAO->checkDefault();
+			$def_cat = $catDAO->getDefault();
+			$cat_id = $def_cat->id();
+		}
 
-			if ($feed && ($feed->category() == $cat_id ||
-			              $feedDAO->updateFeed($feed_id, $values))) {
-				// TODO: return something useful
-			} else {
-				Minz_Error::error(
-					404,
-					array('error' => array(_t('error_occurred')))
-				);
-			}
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$values = array('category' => $cat_id);
+
+		$feed = $feedDAO->searchById($feed_id);
+		if ($feed && ($feed->category() == $cat_id ||
+		              $feedDAO->updateFeed($feed_id, $values))) {
+			// TODO: return something useful
+		} else {
+			Minz_Log::warning('Cannot move feed `' . $feed_id . '` ' .
+			                  'in the category `' . $cat_id . '`');
+			Minz_Error::error(
+				404,
+				array('error' => array(_t('error_occurred')))
+			);
 		}
 	}
 
+	/**
+	 * This action deletes a feed.
+	 *
+	 * This page must be reached by a POST request.
+	 * If there are related queries, they are deleted too.
+	 *
+	 * Parameters are:
+	 *   - id (default: false)
+	 *   - r (default: false)
+	 * r permits to redirect to a given page at the end of this action.
+	 *
+	 * @todo handle "r" redirection in Minz_Request::forward()?
+	 */
 	public function deleteAction() {
-		if (Minz_Request::isPost()) {
-			$id = Minz_Request::param('id');
-			$feedDAO = FreshRSS_Factory::createFeedDao();
-
-			if ($feedDAO->deleteFeed($id)) {
-				// TODO: Delete old favicon
+		$redirect_url = Minz_Request::param('r', false, true);
+		if (!$redirect_url) {
+			$redirect_url = array('c' => 'subscription', 'a' => 'index');
+		}
 
-				// Remove related queries
-				$this->view->conf->remove_query_by_get('f_' . $id);
-				$this->view->conf->save();
+		if (!Minz_Request::isPost()) {
+			Minz_Request::forward($redirect_url, true);
+		}
 
-				$notif = array(
-					'type' => 'good',
-					'content' => _t('feed_deleted')
-				);
-			} else {
-				$notif = array(
-					'type' => 'bad',
-					'content' => _t('error_occured')
-				);
-			}
+		$id = Minz_Request::param('id');
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		if ($feedDAO->deleteFeed($id)) {
+			// TODO: Delete old favicon
 
-			Minz_Session::_param('notification', $notif);
+			// Remove related queries
+			$this->view->conf->remove_query_by_get('f_' . $id);
+			$this->view->conf->save();
 
-			$redirect_url = Minz_Request::param('r', false, true);
-			if ($redirect_url) {
-				Minz_Request::forward($redirect_url);
-			} else {
-				Minz_Request::forward(array('c' => 'subscription', 'a' => 'index'), true);
-			}
+			Minz_Request::good(_t('feed_deleted'), $redirect_url);
+		} else {
+			Minz_Request::bad(_t('error_occurred'), $redirect_url);
 		}
 	}
 }

+ 7 - 0
lib/Minz/ModelPdo.php

@@ -16,6 +16,7 @@ class Minz_ModelPdo {
 	public static $useSharedBd = true;
 	private static $sharedBd = null;
 	private static $sharedPrefix;
+	private static $has_transaction = false;
 	protected static $sharedDbType;
 
 	/**
@@ -91,12 +92,18 @@ class Minz_ModelPdo {
 
 	public function beginTransaction() {
 		$this->bd->beginTransaction();
+		$this->has_transaction = true;
+	}
+	public function hasTransaction() {
+		return $this->has_transaction;
 	}
 	public function commit() {
 		$this->bd->commit();
+		$this->has_transaction = false;
 	}
 	public function rollBack() {
 		$this->bd->rollBack();
+		$this->has_transaction = false;
 	}
 
 	public static function clean() {