Browse Source

Merge pull request #1261 from Alkarex/api-edit

API edit feeds and categories
Alexandre Alapetite 9 years ago
parent
commit
0180fb315c

+ 4 - 0
CHANGELOG.md

@@ -2,12 +2,16 @@
 
 
 ## 2016-XX-XX FreshRSS 1.6.0-dev
 ## 2016-XX-XX FreshRSS 1.6.0-dev
 
 
+* API
+	* Support for editing feeds and categories from client applications [#1261](https://github.com/FreshRSS/FreshRSS/pull/1261)
 * Features
 * Features
 	* Better control of number of entries per page or RSS feed [#1249](https://github.com/FreshRSS/FreshRSS/issues/1249)
 	* Better control of number of entries per page or RSS feed [#1249](https://github.com/FreshRSS/FreshRSS/issues/1249)
 		* Since X hours: `https://freshrss.example/i/?a=rss&hours=3`
 		* Since X hours: `https://freshrss.example/i/?a=rss&hours=3`
 		* Explicit number: `https://freshrss.example/i/?a=rss&nb=10`
 		* Explicit number: `https://freshrss.example/i/?a=rss&nb=10`
 		* Limited by `min_posts_per_rss` and `max_posts_per_rss` in user config
 		* Limited by `min_posts_per_rss` and `max_posts_per_rss` in user config
 	* Support custom ports `localhost:3306` for database servers [#1241](https://github.com/FreshRSS/FreshRSS/issues/1241)
 	* Support custom ports `localhost:3306` for database servers [#1241](https://github.com/FreshRSS/FreshRSS/issues/1241)
+* Bug fixing
+	* Correction of bugs related CSRF tokens introduced in version 1.5.0 [#1253](https://github.com/FreshRSS/FreshRSS/issues/1253), [44f22ab](https://github.com/FreshRSS/FreshRSS/pull/1261/commits/d9bf9b2c6f0b2cc9dec3b638841b7e3040dcf46f)
 * Security
 * Security
 	* Prevent `<a target="_blank">` attacks with `window.opener` [#1245](https://github.com/FreshRSS/FreshRSS/issues/1245)
 	* Prevent `<a target="_blank">` attacks with `window.opener` [#1245](https://github.com/FreshRSS/FreshRSS/issues/1245)
 * UI
 * UI

+ 139 - 149
app/Controllers/feedController.php

@@ -26,6 +26,63 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 		}
 	}
 	}
 
 
+	public static function addFeed($url, $title = '', $cat_id = 0, $new_cat_name = '', $http_auth = '') {
+		@set_time_limit(300);
+
+		$catDAO = new FreshRSS_CategoryDAO();
+
+		$cat = null;
+		if ($cat_id > 0) {
+			$cat = $catDAO->searchById($cat_id);
+		}
+		if ($cat == null && $new_cat_name != '') {
+			$cat = $catDAO->addCategory(array('name' => $new_cat_name));
+		}
+		if ($cat == null) {
+			$catDAO->checkDefault();
+			$cat = $catDAO->getDefault();
+		}
+		$cat_id = $cat->id();
+
+		$feed = new FreshRSS_Feed($url);	//Throws FreshRSS_BadUrl_Exception
+		$feed->_httpAuth($http_auth);
+		$feed->load(true);	//Throws FreshRSS_Feed_Exception, Minz_FileNotExistException
+		$feed->_category($cat_id);
+
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		if ($feedDAO->searchByUrl($feed->url())) {
+			throw new FreshRSS_AlreadySubscribed_Exception($url, $feed->name());
+		}
+
+		// Call the extension hook
+		$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
+		if ($feed === null) {
+			throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name());
+		}
+
+		$values = array(
+			'url' => $feed->url(),
+			'category' => $feed->category(),
+			'name' => $title != '' ? $title : $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.
+			throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name());
+		}
+		$feed->_id($id);
+
+		// Ok, feed has been added in database. Now we have to refresh entries.
+		self::actualizeFeed($id, $url, false, null, true);
+
+		return $feed;
+	}
+
 	/**
 	/**
 	 * This action subscribes to a feed.
 	 * This action subscribes to a feed.
 	 *
 	 *
@@ -59,7 +116,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 		}
 
 
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
-		$this->catDAO = new FreshRSS_CategoryDAO();
 		$url_redirect = array(
 		$url_redirect = array(
 			'c' => 'subscription',
 			'c' => 'subscription',
 			'a' => 'index',
 			'a' => 'index',
@@ -74,26 +130,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 		}
 
 
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
-			@set_time_limit(300);
-
 			$cat = Minz_Request::param('category');
 			$cat = Minz_Request::param('category');
+			$new_cat_name = '';
 			if ($cat === 'nc') {
 			if ($cat === 'nc') {
 				// User want to create a new category, new_category parameter
 				// User want to create a new category, new_category parameter
 				// must exist
 				// must exist
 				$new_cat = Minz_Request::param('new_category');
 				$new_cat = Minz_Request::param('new_category');
-				if (empty($new_cat['name'])) {
-					$cat = false;
-				} else {
-					$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();
+				$new_cat_name = isset($new_cat['name']) ? $new_cat['name'] : '';
 			}
 			}
 
 
 			// HTTP information are useful if feed is protected behind a
 			// HTTP information are useful if feed is protected behind a
@@ -105,103 +148,24 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				$http_auth = $user . ':' . $pass;
 				$http_auth = $user . ':' . $pass;
 			}
 			}
 
 
-			$transaction_started = false;
 			try {
 			try {
-				$feed = new FreshRSS_Feed($url);
+				$feed = self::addFeed($url, '', $cat, $new_cat_name, $http_auth);
 			} catch (FreshRSS_BadUrl_Exception $e) {
 			} catch (FreshRSS_BadUrl_Exception $e) {
 				// Given url was not a valid url!
 				// Given url was not a valid url!
 				Minz_Log::warning($e->getMessage());
 				Minz_Log::warning($e->getMessage());
 				Minz_Request::bad(_t('feedback.sub.feed.invalid_url', $url), $url_redirect);
 				Minz_Request::bad(_t('feedback.sub.feed.invalid_url', $url), $url_redirect);
-			}
-
-			$feed->_httpAuth($http_auth);
-
-			try {
-				$feed->load(true);
 			} catch (FreshRSS_Feed_Exception $e) {
 			} catch (FreshRSS_Feed_Exception $e) {
 				// Something went bad (timeout, server not found, etc.)
 				// Something went bad (timeout, server not found, etc.)
 				Minz_Log::warning($e->getMessage());
 				Minz_Log::warning($e->getMessage());
-				Minz_Request::bad(
-					_t('feedback.sub.feed.internal_problem', _url('index', 'logs')),
-					$url_redirect
-				);
+				Minz_Request::bad(_t('feedback.sub.feed.internal_problem', _url('index', 'logs')), $url_redirect);
 			} catch (Minz_FileNotExistException $e) {
 			} catch (Minz_FileNotExistException $e) {
 				// Cache directory doesn't exist!
 				// Cache directory doesn't exist!
 				Minz_Log::error($e->getMessage());
 				Minz_Log::error($e->getMessage());
-				Minz_Request::bad(
-					_t('feedback.sub.feed.internal_problem', _url('index', 'logs')),
-					$url_redirect
-				);
-			}
-
-			if ($feedDAO->searchByUrl($feed->url())) {
-				Minz_Request::bad(
-					_t('feedback.sub.feed.already_subscribed', $feed->name()),
-					$url_redirect
-				);
-			}
-
-			$feed->_category($cat);
-
-			// Call the extension hook
-			$name = $feed->name();
-			$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
-			if ($feed === null) {
-				Minz_Request::bad(_t('feedback.sub.feed.not_added', $name), $url_redirect);
-			}
-
-			$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('feedback.sub.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();
-			//$feed->pubSubHubbubPrepare();	//TODO: prepare PubSubHubbub already when adding the feed
-
-			$is_read = FreshRSS_Context::$user_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 = FreshRSS_Context::$user_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.
-			$feedDAO->beginTransaction();
-			foreach ($entries as $entry) {
-				// Entries are added without any verification.
-				$entry->_feed($feed->id());
-				$entry->_id(min(time(), $entry->date(true)) . uSecString());
-				$entry->_isRead($is_read);
-
-				$entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry);
-				if ($entry === null) {
-					// An extension has returned a null value, there is nothing to insert.
-					continue;
-				}
-
-				$values = $entry->toArray();
-				$entryDAO->addEntry($values);
-			}
-			$feedDAO->updateLastUpdate($feed->id());
-			if ($feedDAO->inTransaction()) {
-				$feedDAO->commit();
+				Minz_Request::bad(_t('feedback.sub.feed.internal_problem', _url('index', 'logs')), $url_redirect);
+			} catch (FreshRSS_AlreadySubscribed_Exception $e) {
+				Minz_Request::bad(_t('feedback.sub.feed.already_subscribed', $e->feedName()), $url_redirect);
+			} catch (FreshRSS_FeedNotAdded_Exception $e) {
+				Minz_Request::bad(_t('feedback.sub.feed.not_added', $e->feedName()), $url_redirect);
 			}
 			}
 
 
 			// Entries are in DB, we redirect to feed configuration page.
 			// Entries are in DB, we redirect to feed configuration page.
@@ -211,6 +175,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			// GET request: we must ask confirmation to user before adding feed.
 			// GET request: we must ask confirmation to user before adding feed.
 			Minz_View::prependTitle(_t('sub.feed.title_add') . ' · ');
 			Minz_View::prependTitle(_t('sub.feed.title_add') . ' · ');
 
 
+			$this->catDAO = new FreshRSS_CategoryDAO();
 			$this->view->categories = $this->catDAO->listCategories(false);
 			$this->view->categories = $this->catDAO->listCategories(false);
 			$this->view->feed = new FreshRSS_Feed($url);
 			$this->view->feed = new FreshRSS_Feed($url);
 			try {
 			try {
@@ -261,33 +226,18 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 		}
 	}
 	}
 
 
-	/**
-	 * This action actualizes entries from one or several feeds.
-	 *
-	 * Parameters are:
-	 *   - id (default: false): Feed ID
-	 *   - url (default: false): Feed URL
-	 *   - force (default: false)
-	 * If id and url are 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($simplePiePush = null) {
+	public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false) {
 		@set_time_limit(300);
 		@set_time_limit(300);
 
 
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 		$entryDAO = FreshRSS_Factory::createEntryDao();
 
 
-		Minz_Session::_param('actualize_feeds', false);
-		$id = Minz_Request::param('id');
-		$url = Minz_Request::param('url');
-		$force = Minz_Request::param('force');
-
 		// Create a list of feeds to actualize.
 		// Create a list of feeds to actualize.
-		// If id is set and valid, corresponding feed is added to the list but
+		// If feed_id is set and valid, corresponding feed is added to the list but
 		// alone in order to automatize further process.
 		// alone in order to automatize further process.
 		$feeds = array();
 		$feeds = array();
-		if ($id || $url) {
-			$feed = $id ? $feedDAO->searchById($id) : $feedDAO->searchByUrl($url);
+		if ($feed_id > 0 || $feed_url) {
+			$feed = $feed_id > 0 ? $feedDAO->searchById($feed_id) : $feedDAO->searchByUrl($feed_url);
 			if ($feed) {
 			if ($feed) {
 				$feeds[] = $feed;
 				$feeds[] = $feed;
 			}
 			}
@@ -309,7 +259,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			$url = $feed->url();	//For detection of HTTP 301
 			$url = $feed->url();	//For detection of HTTP 301
 
 
 			$pubSubHubbubEnabled = $pubsubhubbubEnabledGeneral && $feed->pubSubHubbubEnabled();
 			$pubSubHubbubEnabled = $pubsubhubbubEnabledGeneral && $feed->pubSubHubbubEnabled();
-			if ((!$simplePiePush) && (!$id) && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) {
+			if ((!$simplePiePush) && (!$feed_id) && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) {
 				//$text = 'Skip pull of feed using PubSubHubbub: ' . $url;
 				//$text = 'Skip pull of feed using PubSubHubbub: ' . $url;
 				//Minz_Log::debug($text);
 				//Minz_Log::debug($text);
 				//file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
 				//file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
@@ -325,7 +275,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				if ($simplePiePush) {
 				if ($simplePiePush) {
 					$feed->loadEntries($simplePiePush);	//Used by PubSubHubbub
 					$feed->loadEntries($simplePiePush);	//Used by PubSubHubbub
 				} else {
 				} else {
-					$feed->load(false);
+					$feed->load(false, $isNewFeed);
 				}
 				}
 			} catch (FreshRSS_Feed_Exception $e) {
 			} catch (FreshRSS_Feed_Exception $e) {
 				Minz_Log::warning($e->getMessage());
 				Minz_Log::warning($e->getMessage());
@@ -335,7 +285,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			}
 			}
 
 
 			$feed_history = $feed->keepHistory();
 			$feed_history = $feed->keepHistory();
-			if ($feed_history == -2) {
+			if ($isNewFeed) {
+				$feed_history = -1; //∞
+			} elseif ($feed_history == -2) {
 				// TODO: -2 must be a constant!
 				// TODO: -2 must be a constant!
 				// -2 means we take the default value from configuration
 				// -2 means we take the default value from configuration
 				$feed_history = FreshRSS_Context::$user_conf->keep_history_default;
 				$feed_history = FreshRSS_Context::$user_conf->keep_history_default;
@@ -375,7 +327,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 						// This entry should not be added considering configuration and date.
 						// This entry should not be added considering configuration and date.
 						$oldGuids[] = $entry->guid();
 						$oldGuids[] = $entry->guid();
 					} else {
 					} else {
-						if ($entry_date < $date_min) {
+						if ($isNewFeed) {
+							$id = min(time(), $entry_date) . uSecString();
+						} elseif ($entry_date < $date_min) {
 							$id = min(time(), $entry_date) . uSecString();
 							$id = min(time(), $entry_date) . uSecString();
 							$entry->_isRead(true);	//Old article that was not in database. Probably an error, so mark as read
 							$entry->_isRead(true);	//Old article that was not in database. Probably an error, so mark as read
 						} else {
 						} else {
@@ -464,6 +418,26 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 				break;
 				break;
 			}
 			}
 		}
 		}
+		return array($updated_feeds, reset($feeds));
+	}
+
+	/**
+	 * This action actualizes entries from one or several feeds.
+	 *
+	 * Parameters are:
+	 *   - id (default: false): Feed ID
+	 *   - url (default: false): Feed URL
+	 *   - force (default: false)
+	 * If id and url are 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() {
+		Minz_Session::_param('actualize_feeds', false);
+		$id = Minz_Request::param('id');
+		$url = Minz_Request::param('url');
+		$force = Minz_Request::param('force');
+
+		list($updated_feeds, $feed) = self::actualizeFeed($id, $url, $force);
 
 
 		if (Minz_Request::param('ajax')) {
 		if (Minz_Request::param('ajax')) {
 			// Most of the time, ajax request is for only one feed. But since
 			// Most of the time, ajax request is for only one feed. But since
@@ -479,7 +453,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		} else {
 		} else {
 			// Redirect to the main page with correct notification.
 			// Redirect to the main page with correct notification.
 			if ($updated_feeds === 1) {
 			if ($updated_feeds === 1) {
-				$feed = reset($feeds);
 				Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), array(
 				Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), array(
 					'params' => array('get' => 'f_' . $feed->id())
 					'params' => array('get' => 'f_' . $feed->id())
 				));
 				));
@@ -492,6 +465,29 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		return $updated_feeds;
 		return $updated_feeds;
 	}
 	}
 
 
+	public static function renameFeed($feed_id, $feed_name) {
+		if ($feed_id <= 0 || $feed_name == '') {
+			return false;
+		}
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		return $feedDAO->updateFeed($feed_id, array('name' => $feed_name));
+	}
+
+	public static function moveFeed($feed_id, $cat_id) {
+		if ($feed_id <= 0) {
+			return false;
+		}
+		if ($cat_id <= 0) {
+			// If category was not given get the default one.
+			$catDAO = new FreshRSS_CategoryDAO();
+			$catDAO->checkDefault();
+			$def_cat = $catDAO->getDefault();
+			$cat_id = $def_cat->id();
+		}
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		return $feedDAO->updateFeed($feed_id, array('category' => $cat_id));
+	}
+
 	/**
 	/**
 	 * This action changes the category of a feed.
 	 * This action changes the category of a feed.
 	 *
 	 *
@@ -512,20 +508,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		$feed_id = Minz_Request::param('f_id');
 		$feed_id = Minz_Request::param('f_id');
 		$cat_id = Minz_Request::param('c_id');
 		$cat_id = Minz_Request::param('c_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();
-		}
-
-		$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))) {
+		if (self::moveFeed($feed_id, $cat_id)) {
 			// TODO: return something useful
 			// TODO: return something useful
 		} else {
 		} else {
 			Minz_Log::warning('Cannot move feed `' . $feed_id . '` ' .
 			Minz_Log::warning('Cannot move feed `' . $feed_id . '` ' .
@@ -534,6 +517,21 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 		}
 	}
 	}
 
 
+	public static function deleteFeed($feed_id) {
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		if ($feedDAO->deleteFeed($feed_id)) {
+			// TODO: Delete old favicon
+
+			// Remove related queries
+			FreshRSS_Context::$user_conf->queries = remove_query_by_get(
+				'f_' . $feed_id, FreshRSS_Context::$user_conf->queries);
+			FreshRSS_Context::$user_conf->save();
+
+			return true;
+		}
+		return false;
+	}
+
 	/**
 	/**
 	 * This action deletes a feed.
 	 * This action deletes a feed.
 	 *
 	 *
@@ -552,21 +550,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		if (!$redirect_url) {
 		if (!$redirect_url) {
 			$redirect_url = array('c' => 'subscription', 'a' => 'index');
 			$redirect_url = array('c' => 'subscription', 'a' => 'index');
 		}
 		}
-
 		if (!Minz_Request::isPost()) {
 		if (!Minz_Request::isPost()) {
 			Minz_Request::forward($redirect_url, true);
 			Minz_Request::forward($redirect_url, true);
 		}
 		}
 
 
 		$id = Minz_Request::param('id');
 		$id = Minz_Request::param('id');
-		$feedDAO = FreshRSS_Factory::createFeedDao();
-		if ($feedDAO->deleteFeed($id)) {
-			// TODO: Delete old favicon
-
-			// Remove related queries
-			FreshRSS_Context::$user_conf->queries = remove_query_by_get(
-				'f_' . $id, FreshRSS_Context::$user_conf->queries);
-			FreshRSS_Context::$user_conf->save();
 
 
+		if (self::deleteFeed($id)) {
 			Minz_Request::good(_t('feedback.sub.feed.deleted'), $redirect_url);
 			Minz_Request::good(_t('feedback.sub.feed.deleted'), $redirect_url);
 		} else {
 		} else {
 			Minz_Request::bad(_t('feedback.sub.feed.error'), $redirect_url);
 			Minz_Request::bad(_t('feedback.sub.feed.error'), $redirect_url);

+ 14 - 0
app/Exceptions/AlreadySubscribedException.php

@@ -0,0 +1,14 @@
+<?php
+
+class FreshRSS_AlreadySubscribed_Exception extends Exception {
+	private $feedName = '';
+
+	public function __construct($url, $feedName) {
+		parent::__construct('Already subscribed! ' . $url, 2135);
+		$this->$feedName = $feedName;
+	}
+
+	public function feedName() {
+		return $this->feedName();
+	}
+}

+ 14 - 0
app/Exceptions/FeedNotAddedException.php

@@ -0,0 +1,14 @@
+<?php
+
+class FreshRSS_FeedNotAdded_Exception extends Exception {
+	private $feedName = '';
+
+	public function __construct($url, $feedName) {
+		parent::__construct('Feed not added! ' . $url, 2147);
+		$this->$feedName = $feedName;
+	}
+
+	public function feedName() {
+		return $this->feedName();
+	}
+}

+ 3 - 3
app/Models/Feed.php

@@ -216,7 +216,7 @@ class FreshRSS_Feed extends Minz_Model {
 		$this->nbEntries = intval($value);
 		$this->nbEntries = intval($value);
 	}
 	}
 
 
-	public function load($loadDetails = false) {
+	public function load($loadDetails = false, $noCache = false) {
 		if ($this->url !== null) {
 		if ($this->url !== null) {
 			if (CACHE_PATH === false) {
 			if (CACHE_PATH === false) {
 				throw new Minz_FileNotExistException(
 				throw new Minz_FileNotExistException(
@@ -268,7 +268,7 @@ class FreshRSS_Feed extends Minz_Model {
 					$this->_url($clean_url);
 					$this->_url($clean_url);
 				}
 				}
 
 
-				if (($mtime === true) || ($mtime > $this->lastUpdate)) {
+				if (($mtime === true) || ($mtime > $this->lastUpdate) || $noCache) {
 					//Minz_Log::debug('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url);
 					//Minz_Log::debug('FreshRSS no cache ' . $mtime . ' > ' . $this->lastUpdate . ' for ' . $clean_url);
 					$this->loadEntries($feed);	// et on charge les articles du flux
 					$this->loadEntries($feed);	// et on charge les articles du flux
 				} else {
 				} else {
@@ -460,7 +460,7 @@ class FreshRSS_Feed extends Minz_Model {
 				CURLOPT_URL => $this->hubUrl,
 				CURLOPT_URL => $this->hubUrl,
 				CURLOPT_FOLLOWLOCATION => true,
 				CURLOPT_FOLLOWLOCATION => true,
 				CURLOPT_RETURNTRANSFER => true,
 				CURLOPT_RETURNTRANSFER => true,
-				CURLOPT_USERAGENT => _t('gen.freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')',
+				CURLOPT_USERAGENT => 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')',
 				CURLOPT_POSTFIELDS => 'hub.verify=sync'
 				CURLOPT_POSTFIELDS => 'hub.verify=sync'
 					. '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe')
 					. '&hub.mode=' . ($state ? 'subscribe' : 'unsubscribe')
 					. '&hub.topic=' . urlencode($this->selfUrl)
 					. '&hub.topic=' . urlencode($this->selfUrl)

+ 1 - 1
lib/lib_rss.php

@@ -165,7 +165,7 @@ function customSimplePie() {
 	$system_conf = Minz_Configuration::get('system');
 	$system_conf = Minz_Configuration::get('system');
 	$limits = $system_conf->limits;
 	$limits = $system_conf->limits;
 	$simplePie = new SimplePie();
 	$simplePie = new SimplePie();
-	$simplePie->set_useragent(_t('gen.freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ') ' . SIMPLEPIE_NAME . '/' . SIMPLEPIE_VERSION);
+	$simplePie->set_useragent('FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ') ' . SIMPLEPIE_NAME . '/' . SIMPLEPIE_VERSION);
 	$simplePie->set_syslog($system_conf->simplepie_syslog_enabled);
 	$simplePie->set_syslog($system_conf->simplepie_syslog_enabled);
 	$simplePie->set_cache_location(CACHE_PATH);
 	$simplePie->set_cache_location(CACHE_PATH);
 	$simplePie->set_cache_duration($limits['cache_duration']);
 	$simplePie->set_cache_duration($limits['cache_duration']);

+ 161 - 31
p/api/greader.php

@@ -153,13 +153,12 @@ function authorizationToUser() {
 		if (count($headerAuthX) === 2) {
 		if (count($headerAuthX) === 2) {
 			$user = $headerAuthX[0];
 			$user = $headerAuthX[0];
 			if (ctype_alnum($user)) {
 			if (ctype_alnum($user)) {
-				$conf = get_user_configuration($user);
-				if (is_null($conf)) {
+				FreshRSS_Context::$user_conf = get_user_configuration($user);
+				if (FreshRSS_Context::$user_conf == null) {
 					Minz_Log::warning('Invalid API user ' . $user . ': configuration cannot be found.');
 					Minz_Log::warning('Invalid API user ' . $user . ': configuration cannot be found.');
 					unauthorized();
 					unauthorized();
 				}
 				}
-				global $system_conf;
-				if ($headerAuthX[1] === sha1($system_conf->salt . $user . $conf->apiPasswordHash)) {
+				if ($headerAuthX[1] === sha1(FreshRSS_Context::$system_conf->salt . $user . FreshRSS_Context::$user_conf->apiPasswordHash)) {
 					return $user;
 					return $user;
 				} else {
 				} else {
 					logMe('Invalid API authorisation for user ' . $user . ': ' . $headerAuthX[1]);
 					logMe('Invalid API authorisation for user ' . $user . ': ' . $headerAuthX[1]);
@@ -181,16 +180,15 @@ function clientLogin($email, $pass) {	//http://web.archive.org/web/2013060409104
 			include_once(LIB_PATH . '/password_compat.php');
 			include_once(LIB_PATH . '/password_compat.php');
 		}
 		}
 
 
-		$conf = get_user_configuration($email);
-		if (is_null($conf)) {
+		FreshRSS_Context::$user_conf = get_user_configuration($email);
+		if (FreshRSS_Context::$user_conf == null) {
 			Minz_Log::warning('Invalid API user ' . $email . ': configuration cannot be found.');
 			Minz_Log::warning('Invalid API user ' . $email . ': configuration cannot be found.');
 			unauthorized();
 			unauthorized();
 		}
 		}
 
 
-		if ($conf->apiPasswordHash != '' && password_verify($pass, $conf->apiPasswordHash)) {
+		if (FreshRSS_Context::$user_conf->apiPasswordHash != '' && password_verify($pass, FreshRSS_Context::$user_conf->apiPasswordHash)) {
 			header('Content-Type: text/plain; charset=UTF-8');
 			header('Content-Type: text/plain; charset=UTF-8');
-			global $system_conf;
-			$auth = $email . '/' . sha1($system_conf->salt . $email . $conf->apiPasswordHash);
+			$auth = $email . '/' . sha1(FreshRSS_Context::$system_conf->salt . $email . FreshRSS_Context::$user_conf->apiPasswordHash);
 			echo 'SID=', $auth, "\n",
 			echo 'SID=', $auth, "\n",
 				'Auth=', $auth, "\n";
 				'Auth=', $auth, "\n";
 			exit();
 			exit();
@@ -209,8 +207,7 @@ function token($conf) {
 //https://github.com/ericmann/gReader-Library/blob/master/greader.class.php
 //https://github.com/ericmann/gReader-Library/blob/master/greader.class.php
 	$user = Minz_Session::param('currentUser', '_');
 	$user = Minz_Session::param('currentUser', '_');
 	//logMe('token('. $user . ")");	//TODO: Implement real token that expires
 	//logMe('token('. $user . ")");	//TODO: Implement real token that expires
-	global $system_conf;
-	$token = str_pad(sha1($system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z');	//Must have 57 characters
+	$token = str_pad(sha1(FreshRSS_Context::$system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z');	//Must have 57 characters
 	echo $token, "\n";
 	echo $token, "\n";
 	exit();
 	exit();
 }
 }
@@ -219,8 +216,7 @@ function checkToken($conf, $token) {
 //http://code.google.com/p/google-reader-api/wiki/ActionToken
 //http://code.google.com/p/google-reader-api/wiki/ActionToken
 	$user = Minz_Session::param('currentUser', '_');
 	$user = Minz_Session::param('currentUser', '_');
 	//logMe('checkToken(' . $token . ")");
 	//logMe('checkToken(' . $token . ")");
-	global $system_conf;
-	if ($token === str_pad(sha1($system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z')) {
+	if ($token === str_pad(sha1(FreshRSS_Context::$system_conf->salt . $user . $conf->apiPasswordHash), 57, 'Z')) {
 		return true;
 		return true;
 	}
 	}
 	unauthorized();
 	unauthorized();
@@ -261,8 +257,7 @@ function subscriptionList() {
 	$stm->execute();
 	$stm->execute();
 	$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 	$res = $stm->fetchAll(PDO::FETCH_ASSOC);
 
 
-	global $system_conf;
-	$salt = $system_conf->salt;
+	$salt = FreshRSS_Context::$system_conf->salt;
 	$subscriptions = array();
 	$subscriptions = array();
 
 
 	foreach ($res as $line) {
 	foreach ($res as $line) {
@@ -287,6 +282,85 @@ function subscriptionList() {
 	exit();
 	exit();
 }
 }
 
 
+function subscriptionEdit($streamNames, $titles, $action, $add = '', $remove = '') {
+	//logMe("subscriptionEdit()");
+	//https://github.com/mihaip/google-reader-api/blob/master/wiki/ApiSubscriptionEdit.wiki
+	switch ($action) {
+		case 'subscribe':
+		case 'unsubscribe':
+		case 'edit':
+			break;
+		default:
+			badRequest();
+	}
+	$addCatId = 0;
+	$categoryDAO = null;
+	if ($add != '' || $remove != '') {
+		$categoryDAO = new FreshRSS_CategoryDAO();
+	}
+	$c_name = '';
+	if ($add != '' && strpos($add, 'user/-/label/') === 0) {	//user/-/label/Example
+		$c_name = substr($add, 13);
+		$cat = $categoryDAO->searchByName($c_name);
+		$addCatId = $cat == null ? -1 : $cat->id();
+	} else if ($remove != '' && strpos($remove, 'user/-/label/')) {
+		$addCatId = 1;	//Default category
+	}
+	if ($addCatId <= 0 && $c_name = '') {
+		$addCatId = 1;	//Default category
+	}
+	$feedDAO = FreshRSS_Factory::createFeedDao();
+	for ($i = count($streamNames) - 1; $i >= 0; $i--) {
+		$streamName = $streamNames[$i];	//feed/http://example.net/sample.xml	;	feed/338
+		if (strpos($streamName, 'feed/') === 0) {
+			$streamName = substr($streamName, 5);
+			$feedId = 0;
+			if (ctype_digit($streamName)) {
+				if ($action === 'subscribe') {
+					continue;
+				}
+				$feedId = $streamName;
+			} else {
+				$feed = $feedDAO->searchByUrl($streamName);
+				$feedId = $feed == null ? -1 : $feed->id();
+			}
+			$title = isset($titles[$i]) ? $titles[$i] : '';
+			switch ($action) {
+				case 'subscribe':
+					if ($feedId <= 0) {
+						$http_auth = '';	//TODO
+						try {
+							$feed = FreshRSS_feed_Controller::addFeed($streamName, $title, $addCatId, $c_name, $http_auth);
+							continue;
+						} catch (Exception $e) {
+							logMe("subscriptionEdit error subscribe: " . $e->getMessage());
+						}
+					}
+					badRequest();
+					break;
+				case 'unsubscribe':
+					if (!($feedId > 0 && FreshRSS_feed_Controller::deleteFeed($feedId))) {
+						badRequest();
+					}
+					break;
+				case 'edit':
+					if ($feedId > 0) {
+						if ($addCatId > 0) {
+							FreshRSS_feed_Controller::moveFeed($feedId, $addCatId);
+						}
+						if ($title != '') {
+							FreshRSS_feed_Controller::renameFeed($feedId, $title);
+						}
+					} else {
+						badRequest();
+					}
+					break;
+			}
+		}
+	}
+	exit('OK');
+}
+
 function unreadCount() {	//http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#unread-count
 function unreadCount() {	//http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#unread-count
 	//logMe("unreadCount()");
 	//logMe("unreadCount()");
 	header('Content-Type: application/json; charset=UTF-8');
 	header('Content-Type: application/json; charset=UTF-8');
@@ -523,8 +597,38 @@ function editTag($e_ids, $a, $r) {
 			break;
 			break;
 	}
 	}
 
 
-	echo 'OK';
-	exit();
+	exit('OK');
+}
+
+function renameTag($s, $dest) {
+	//logMe("renameTag()");
+	if ($s != '' && strpos($s, 'user/-/label/') === 0 &&
+		$dest != '' &&  strpos($dest, 'user/-/label/') === 0) {
+		$s = substr($s, 13);
+		$categoryDAO = new FreshRSS_CategoryDAO();
+		$cat = $categoryDAO->searchByName($s);
+		if ($cat != null) {
+			$dest = substr($dest, 13);
+			$categoryDAO->updateCategory($cat->id(), array('name' => $dest));
+			exit('OK');
+		}
+	}
+	badRequest();
+}
+
+function disableTag($s, $dest) {
+	//logMe("renameTag()");
+	if ($s != '' && strpos($s, 'user/-/label/') === 0) {
+		$s = substr($s, 13);
+		$categoryDAO = new FreshRSS_CategoryDAO();
+		$cat = $categoryDAO->searchByName($s);
+		if ($cat != null) {
+			$feedDAO = FreshRSS_Factory::createFeedDao();
+			$feedDAO->changeCategory($cat->id(), 0);
+			exit('OK');
+		}
+	}
+	badRequest();
 }
 }
 
 
 function markAllAsRead($streamId, $olderThanId) {
 function markAllAsRead($streamId, $olderThanId) {
@@ -542,8 +646,7 @@ function markAllAsRead($streamId, $olderThanId) {
 		$entryDAO->markReadEntries($olderThanId, false, -1);
 		$entryDAO->markReadEntries($olderThanId, false, -1);
 	}
 	}
 
 
-	echo 'OK';
-	exit();
+	exit('OK');
 }
 }
 
 
 //logMe('----------------------------------------------------------------');
 //logMe('----------------------------------------------------------------');
@@ -555,17 +658,17 @@ $pathInfos = explode('/', $pathInfo);
 Minz_Configuration::register('system',
 Minz_Configuration::register('system',
                              DATA_PATH . '/config.php',
                              DATA_PATH . '/config.php',
                              DATA_PATH . '/config.default.php');
                              DATA_PATH . '/config.default.php');
-$system_conf = Minz_Configuration::get('system');
-if (!$system_conf->api_enabled) {
+FreshRSS_Context::$system_conf = Minz_Configuration::get('system');
+if (!FreshRSS_Context::$system_conf->api_enabled) {
 	serviceUnavailable();
 	serviceUnavailable();
 }
 }
 
 
 Minz_Session::init('FreshRSS');
 Minz_Session::init('FreshRSS');
 
 
 $user = authorizationToUser();
 $user = authorizationToUser();
-$conf = null;
+FreshRSS_Context::$user_conf = null;
 if ($user !== '') {
 if ($user !== '') {
-	$conf = get_user_configuration($user);
+	FreshRSS_Context::$user_conf = get_user_configuration($user);
 }
 }
 
 
 //logMe('User => ' . $user);
 //logMe('User => ' . $user);
@@ -625,14 +728,28 @@ elseif ($pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && isset($pathInfo
 			if (isset($pathInfos[5]) && $pathInfos[5] === 'list') {
 			if (isset($pathInfos[5]) && $pathInfos[5] === 'list') {
 				$output = isset($_GET['output']) ? $_GET['output'] : '';
 				$output = isset($_GET['output']) ? $_GET['output'] : '';
 				if ($output !== 'json') notImplemented();
 				if ($output !== 'json') notImplemented();
-				tagList($_GET['output']);
+				tagList($output);
 			}
 			}
 			break;
 			break;
 		case 'subscription':
 		case 'subscription':
-			if (isset($pathInfos[5]) && $pathInfos[5] === 'list') {
-				$output = isset($_GET['output']) ? $_GET['output'] : '';
-				if ($output !== 'json') notImplemented();
-				subscriptionList($_GET['output']);
+			if (isset($pathInfos[5])) {
+				switch ($pathInfos[5]) {
+					case 'list':
+						$output = isset($_GET['output']) ? $_GET['output'] : '';
+						if ($output !== 'json') notImplemented();
+						subscriptionList($_GET['output']);
+						break;
+					case 'edit':
+						if (isset($_POST['s']) && isset($_POST['ac'])) {
+							$streamNames = multiplePosts('s');	//StreamId to operate on. The parameter may be repeated to edit multiple subscriptions at once
+							$titles = multiplePosts('t');	//Title to use for the subscription. For the `subscribe` action, if not specified then the feed's current title will be used. Can be used with the `edit` action to rename a subscription
+							$action = $_POST['ac'];	//Action to perform on the given StreamId. Possible values are `subscribe`, `unsubscribe` and `edit`
+							$add = isset($_POST['a']) ? $_POST['a'] : '';	//StreamId to add the subscription to (generally a user label)
+							$remove = isset($_POST['r']) ? $_POST['r'] : '';	//StreamId to remove the subscription from (generally a user label)
+							subscriptionEdit($streamNames, $titles, $action, $add, $remove);
+						}
+						break;
+				}
 			}
 			}
 			break;
 			break;
 		case 'unread-count':
 		case 'unread-count':
@@ -643,15 +760,28 @@ elseif ($pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && isset($pathInfo
 			break;
 			break;
 		case 'edit-tag':	//http://blog.martindoms.com/2010/01/20/using-the-google-reader-api-part-3/
 		case 'edit-tag':	//http://blog.martindoms.com/2010/01/20/using-the-google-reader-api-part-3/
 			$token = isset($_POST['T']) ? trim($_POST['T']) : '';
 			$token = isset($_POST['T']) ? trim($_POST['T']) : '';
-			checkToken($conf, $token);
+			checkToken(FreshRSS_Context::$user_conf, $token);
 			$a = isset($_POST['a']) ? $_POST['a'] : '';	//Add:	user/-/state/com.google/read	user/-/state/com.google/starred
 			$a = isset($_POST['a']) ? $_POST['a'] : '';	//Add:	user/-/state/com.google/read	user/-/state/com.google/starred
 			$r = isset($_POST['r']) ? $_POST['r'] : '';	//Remove:	user/-/state/com.google/read	user/-/state/com.google/starred
 			$r = isset($_POST['r']) ? $_POST['r'] : '';	//Remove:	user/-/state/com.google/read	user/-/state/com.google/starred
 			$e_ids = multiplePosts('i');	//item IDs
 			$e_ids = multiplePosts('i');	//item IDs
 			editTag($e_ids, $a, $r);
 			editTag($e_ids, $a, $r);
 			break;
 			break;
+		case 'rename-tag':	//https://github.com/theoldreader/api
+			$token = isset($_POST['T']) ? trim($_POST['T']) : '';
+			checkToken(FreshRSS_Context::$user_conf, $token);
+			$s = isset($_POST['s']) ? $_POST['s'] : '';	//user/-/label/Folder
+			$dest = isset($_POST['dest']) ? $_POST['dest'] : '';	//user/-/label/NewFolder
+			renameTag($s, $dest);
+			break;
+		case 'disable-tag':	//https://github.com/theoldreader/api
+			$token = isset($_POST['T']) ? trim($_POST['T']) : '';
+			checkToken(FreshRSS_Context::$user_conf, $token);
+			$s = isset($_POST['s']) ? $_POST['s'] : '';	//user/-/label/Folder
+			disableTag($s);
+			break;
 		case 'mark-all-as-read':
 		case 'mark-all-as-read':
 			$token = isset($_POST['T']) ? trim($_POST['T']) : '';
 			$token = isset($_POST['T']) ? trim($_POST['T']) : '';
-			checkToken($conf, $token);
+			checkToken(FreshRSS_Context::$user_conf, $token);
 			$streamId = $_POST['s'];	//StreamId
 			$streamId = $_POST['s'];	//StreamId
 			$ts = isset($_POST['ts']) ? $_POST['ts'] : '0';	//Older than timestamp in nanoseconds
 			$ts = isset($_POST['ts']) ? $_POST['ts'] : '0';	//Older than timestamp in nanoseconds
 			if (!ctype_digit($ts)) {
 			if (!ctype_digit($ts)) {
@@ -660,7 +790,7 @@ elseif ($pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && isset($pathInfo
 			markAllAsRead($streamId, $ts);
 			markAllAsRead($streamId, $ts);
 			break;
 			break;
 		case 'token':
 		case 'token':
-			token($conf);
+			token(FreshRSS_Context::$user_conf);
 			break;
 			break;
 	}
 	}
 } elseif ($pathInfos[1] === 'check' && $pathInfos[2] === 'compatibility') {
 } elseif ($pathInfos[1] === 'check' && $pathInfos[2] === 'compatibility') {

+ 2 - 5
p/api/pshb.php

@@ -88,9 +88,6 @@ if ($ORIGINAL_INPUT == '') {
 Minz_Configuration::register('system', DATA_PATH . '/config.php', DATA_PATH . '/config.default.php');
 Minz_Configuration::register('system', DATA_PATH . '/config.php', DATA_PATH . '/config.default.php');
 $system_conf = Minz_Configuration::get('system');
 $system_conf = Minz_Configuration::get('system');
 $system_conf->auth_type = 'none';	// avoid necessity to be logged in (not saved!)
 $system_conf->auth_type = 'none';	// avoid necessity to be logged in (not saved!)
-Minz_Translate::init('en');
-Minz_Request::_param('ajax', true);
-$feedController = new FreshRSS_feed_Controller();
 
 
 $simplePie = customSimplePie();
 $simplePie = customSimplePie();
 $simplePie->set_raw_data($ORIGINAL_INPUT);
 $simplePie->set_raw_data($ORIGINAL_INPUT);
@@ -106,7 +103,6 @@ if ($self !== base64url_decode($canonical64)) {
 	//die('Self URL does not match registered canonical URL!');
 	//die('Self URL does not match registered canonical URL!');
 	$self = base64url_decode($canonical64);
 	$self = base64url_decode($canonical64);
 }
 }
-Minz_Request::_param('url', $self);
 
 
 $nb = 0;
 $nb = 0;
 foreach ($users as $userFilename) {
 foreach ($users as $userFilename) {
@@ -121,7 +117,8 @@ foreach ($users as $userFilename) {
 		                             join_path(USERS_PATH, $username, 'config.php'),
 		                             join_path(USERS_PATH, $username, 'config.php'),
 		                             join_path(USERS_PATH, '_', 'config.default.php'));
 		                             join_path(USERS_PATH, '_', 'config.default.php'));
 		FreshRSS_Context::init();
 		FreshRSS_Context::init();
-		if ($feedController->actualizeAction($simplePie) > 0) {
+		list($updated_feeds, $feed) = FreshRSS_feed_Controller::actualizeFeed(0, $self, false, $simplePie);
+		if ($updated_feeds > 0) {
 			$nb++;
 			$nb++;
 		}
 		}
 	} catch (Exception $e) {
 	} catch (Exception $e) {

+ 1 - 0
p/scripts/category.js

@@ -95,6 +95,7 @@ function init_draggable() {
 			data : {
 			data : {
 				f_id: dragFeedId,
 				f_id: dragFeedId,
 				c_id: e.target.parentNode.getAttribute('data-cat-id'),
 				c_id: e.target.parentNode.getAttribute('data-cat-id'),
+				_csrf: context.csrf,
 			}
 			}
 		}).done(function() {
 		}).done(function() {
 			$(e.target).after(dragHtml);
 			$(e.target).after(dragHtml);