Просмотр исходного кода

API implement OPML import/export (#3424)

#fix https://github.com/FreshRSS/FreshRSS/issues/3421
Alexandre Alapetite 5 лет назад
Родитель
Сommit
4a87f34bcf
5 измененных файлов с 260 добавлено и 199 удалено
  1. 6 197
      app/Controllers/importExportController.php
  2. 1 1
      app/Services/ExportService.php
  3. 221 0
      app/Services/ImportService.php
  4. 1 1
      lib/lib_opml.php
  5. 31 0
      p/api/greader.php

+ 6 - 197
app/Controllers/importExportController.php

@@ -16,7 +16,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 
 
 		require_once(LIB_PATH . '/lib_opml.php');
 		require_once(LIB_PATH . '/lib_opml.php');
 
 
-		$this->catDAO = new FreshRSS_CategoryDAO();
+		$this->catDAO = FreshRSS_Factory::createCategoryDao();
 		$this->entryDAO = FreshRSS_Factory::createEntryDao();
 		$this->entryDAO = FreshRSS_Factory::createEntryDao();
 		$this->feedDAO = FreshRSS_Factory::createFeedDao();
 		$this->feedDAO = FreshRSS_Factory::createFeedDao();
 	}
 	}
@@ -48,9 +48,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 
 
 	public function importFile($name, $path, $username = null) {
 	public function importFile($name, $path, $username = null) {
 		self::minimumMemory(256);
 		self::minimumMemory(256);
-		require_once(LIB_PATH . '/lib_opml.php');
 
 
-		$this->catDAO = new FreshRSS_CategoryDAO($username);
+		$this->catDAO = FreshRSS_Factory::createCategoryDao($username);
 		$this->entryDAO = FreshRSS_Factory::createEntryDao($username);
 		$this->entryDAO = FreshRSS_Factory::createEntryDao($username);
 		$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
 		$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
 
 
@@ -98,8 +97,11 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		// Starred articles then so the "favourite" status is already set
 		// Starred articles then so the "favourite" status is already set
 		// And finally all other files.
 		// And finally all other files.
 		$ok = true;
 		$ok = true;
+
+		$importService = new FreshRSS_Import_Service($username);
+
 		foreach ($list_files['opml'] as $opml_file) {
 		foreach ($list_files['opml'] as $opml_file) {
-			if (!$this->importOpml($opml_file)) {
+			if (!$importService->importOpml($opml_file)) {
 				$ok = false;
 				$ok = false;
 				if (FreshRSS_Context::$isCli) {
 				if (FreshRSS_Context::$isCli) {
 					fwrite(STDERR, 'FreshRSS error during OPML import' . "\n");
 					fwrite(STDERR, 'FreshRSS error during OPML import' . "\n");
@@ -213,199 +215,6 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		return 'unknown';
 		return 'unknown';
 	}
 	}
 
 
-	/**
-	 * This method parses and imports an OPML file.
-	 *
-	 * @param string $opml_file the OPML file content.
-	 * @return boolean false if an error occured, true otherwise.
-	 */
-	private function importOpml($opml_file) {
-		$opml_array = array();
-		try {
-			$opml_array = libopml_parse_string($opml_file, false);
-		} catch (LibOPML_Exception $e) {
-			if (FreshRSS_Context::$isCli) {
-				fwrite(STDERR, 'FreshRSS error during OPML parsing: ' . $e->getMessage() . "\n");
-			} else {
-				Minz_Log::warning($e->getMessage());
-			}
-			return false;
-		}
-
-		$this->catDAO->checkDefault();
-
-		return $this->addOpmlElements($opml_array['body']);
-	}
-
-	/**
-	 * This method imports an OPML file based on its body.
-	 *
-	 * @param array $opml_elements an OPML element (body or outline).
-	 * @param string $parent_cat the name of the parent category.
-	 * @return boolean false if an error occured, true otherwise.
-	 */
-	private function addOpmlElements($opml_elements, $parent_cat = null) {
-		$ok = true;
-
-		$nb_feeds = count($this->feedDAO->listFeeds());
-		$nb_cats = count($this->catDAO->listCategories(false));
-		$limits = FreshRSS_Context::$system_conf->limits;
-
-		//Sort with categories first
-		usort($opml_elements, function ($a, $b) {
-			return strcmp(
-				(isset($a['xmlUrl']) ? 'Z' : 'A') . $a['text'],
-				(isset($b['xmlUrl']) ? 'Z' : 'A') . $b['text']);
-		});
-
-		foreach ($opml_elements as $elt) {
-			if (isset($elt['xmlUrl'])) {
-				// If xmlUrl exists, it means it is a feed
-				if (FreshRSS_Context::$isCli && $nb_feeds >= $limits['max_feeds']) {
-					Minz_Log::warning(_t('feedback.sub.feed.over_max',
-									  $limits['max_feeds']));
-					$ok = false;
-					continue;
-				}
-
-				if ($this->addFeedOpml($elt, $parent_cat)) {
-					$nb_feeds++;
-				} else {
-					$ok = false;
-				}
-			} else {
-				// No xmlUrl? It should be a category!
-				$limit_reached = ($nb_cats >= $limits['max_categories']);
-				if (!FreshRSS_Context::$isCli && $limit_reached) {
-					Minz_Log::warning(_t('feedback.sub.category.over_max',
-									  $limits['max_categories']));
-					$ok = false;
-					continue;
-				}
-
-				if ($this->addCategoryOpml($elt, $parent_cat, $limit_reached)) {
-					$nb_cats++;
-				} else {
-					$ok = false;
-				}
-			}
-		}
-
-		return $ok;
-	}
-
-	/**
-	 * This method imports an OPML feed element.
-	 *
-	 * @param array $feed_elt an OPML element (must be a feed element).
-	 * @param string $parent_cat the name of the parent category.
-	 * @return boolean false if an error occured, true otherwise.
-	 */
-	private function addFeedOpml($feed_elt, $parent_cat) {
-		if ($parent_cat == null) {
-			// This feed has no parent category so we get the default one
-			$this->catDAO->checkDefault();
-			$default_cat = $this->catDAO->getDefault();
-			$parent_cat = $default_cat->name();
-		}
-
-		$cat = $this->catDAO->searchByName($parent_cat);
-		if ($cat == null) {
-			// If there is not $cat, it means parent category does not exist in
-			// database.
-			// If it happens, take the default category.
-			$this->catDAO->checkDefault();
-			$cat = $this->catDAO->getDefault();
-		}
-
-		// We get different useful information
-		$url = Minz_Helper::htmlspecialchars_utf8($feed_elt['xmlUrl']);
-		$name = Minz_Helper::htmlspecialchars_utf8($feed_elt['text']);
-		$website = '';
-		if (isset($feed_elt['htmlUrl'])) {
-			$website = Minz_Helper::htmlspecialchars_utf8($feed_elt['htmlUrl']);
-		}
-		$description = '';
-		if (isset($feed_elt['description'])) {
-			$description = Minz_Helper::htmlspecialchars_utf8($feed_elt['description']);
-		}
-
-		$error = false;
-		try {
-			// Create a Feed object and add it in DB
-			$feed = new FreshRSS_Feed($url);
-			$feed->_category($cat->id());
-			$feed->_name($name);
-			$feed->_website($website);
-			$feed->_description($description);
-
-			// Call the extension hook
-			$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
-			if ($feed != null) {
-				// addFeedObject checks if feed is already in DB so nothing else to
-				// check here
-				$id = $this->feedDAO->addFeedObject($feed);
-				$error = ($id === false);
-			} else {
-				$error = true;
-			}
-		} catch (FreshRSS_Feed_Exception $e) {
-			if (FreshRSS_Context::$isCli) {
-				fwrite(STDERR, 'FreshRSS error during OPML feed import: ' . $e->getMessage() . "\n");
-			} else {
-				Minz_Log::warning($e->getMessage());
-			}
-			$error = true;
-		}
-
-		if ($error) {
-			if (FreshRSS_Context::$isCli) {
-				fwrite(STDERR, 'FreshRSS error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id() . "\n");
-			} else {
-				Minz_Log::warning('Error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id());
-			}
-		}
-
-		return !$error;
-	}
-
-	/**
-	 * This method imports an OPML category element.
-	 *
-	 * @param array $cat_elt an OPML element (must be a category element).
-	 * @param string $parent_cat the name of the parent category.
-	 * @param boolean $cat_limit_reached indicates if category limit has been reached.
-	 *                if yes, category is not added (but we try for feeds!)
-	 * @return boolean false if an error occured, true otherwise.
-	 */
-	private function addCategoryOpml($cat_elt, $parent_cat, $cat_limit_reached) {
-		// Create a new Category object
-		$catName = Minz_Helper::htmlspecialchars_utf8($cat_elt['text']);
-		$cat = new FreshRSS_Category($catName);
-
-		$error = true;
-		if (FreshRSS_Context::$isCli || !$cat_limit_reached) {
-			$id = $this->catDAO->addCategoryObject($cat);
-			$error = ($id === false);
-		}
-		if ($error) {
-			if (FreshRSS_Context::$isCli) {
-				fwrite(STDERR, 'FreshRSS error during OPML category import from URL: ' . $catName . "\n");
-			} else {
-				Minz_Log::warning('Error during OPML category import from URL: ' . $catName);
-			}
-		}
-
-		if (isset($cat_elt['@outlines'])) {
-			// Our cat_elt contains more categories or more feeds, so we
-			// add them recursively.
-			// Note: FreshRSS does not support yet category arborescence
-			$error &= !$this->addOpmlElements($cat_elt['@outlines'], $catName);
-		}
-
-		return !$error;
-	}
-
 	private function ttrssXmlToJson($xml) {
 	private function ttrssXmlToJson($xml) {
 		$table = (array)simplexml_load_string($xml, null, LIBXML_NOCDATA);
 		$table = (array)simplexml_load_string($xml, null, LIBXML_NOCDATA);
 		$table['items'] = isset($table['article']) ? $table['article'] : array();
 		$table['items'] = isset($table['article']) ? $table['article'] : array();

+ 1 - 1
app/Services/ExportService.php

@@ -27,7 +27,7 @@ class FreshRSS_Export_Service {
 	public function __construct($username) {
 	public function __construct($username) {
 		$this->username = $username;
 		$this->username = $username;
 
 
-		$this->category_dao = new FreshRSS_CategoryDAO($username);
+		$this->category_dao = FreshRSS_Factory::createCategoryDao($username);
 		$this->feed_dao = FreshRSS_Factory::createFeedDao($username);
 		$this->feed_dao = FreshRSS_Factory::createFeedDao($username);
 		$this->entry_dao = FreshRSS_Factory::createEntryDao($username);
 		$this->entry_dao = FreshRSS_Factory::createEntryDao($username);
 		$this->tag_dao = FreshRSS_Factory::createTagDao();
 		$this->tag_dao = FreshRSS_Factory::createTagDao();

+ 221 - 0
app/Services/ImportService.php

@@ -0,0 +1,221 @@
+<?php
+
+/**
+ * Provide methods to import files.
+ */
+class FreshRSS_Import_Service {
+	/** @var string */
+	private $username;
+
+	/** @var FreshRSS_CategoryDAO */
+	private $catDAO;
+
+	/** @var FreshRSS_FeedDAO */
+	private $feedDAO;
+
+	/**
+	 * Initialize the service for the given user.
+	 *
+	 * @param string $username
+	 */
+	public function __construct($username) {
+		require_once(LIB_PATH . '/lib_opml.php');
+
+		$this->username = $username;
+		$this->catDAO = FreshRSS_Factory::createCategoryDao($username);
+		$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
+	}
+
+	/**
+	 * This method parses and imports an OPML file.
+	 *
+	 * @param string $opml_file the OPML file content.
+	 * @return boolean false if an error occured, true otherwise.
+	 */
+	public function importOpml($opml_file) {
+		$opml_array = array();
+		try {
+			$opml_array = libopml_parse_string($opml_file, false);
+		} catch (LibOPML_Exception $e) {
+			if (FreshRSS_Context::$isCli) {
+				fwrite(STDERR, 'FreshRSS error during OPML parsing: ' . $e->getMessage() . "\n");
+			} else {
+				Minz_Log::warning($e->getMessage());
+			}
+			return false;
+		}
+
+		$this->catDAO->checkDefault();
+
+		return $this->addOpmlElements($opml_array['body']);
+	}
+
+	/**
+	 * This method imports an OPML file based on its body.
+	 *
+	 * @param array $opml_elements an OPML element (body or outline).
+	 * @param string $parent_cat the name of the parent category.
+	 * @return boolean false if an error occured, true otherwise.
+	 */
+	private function addOpmlElements($opml_elements, $parent_cat = null) {
+		$ok = true;
+
+		$nb_feeds = count($this->feedDAO->listFeeds());
+		$nb_cats = count($this->catDAO->listCategories(false));
+		$limits = FreshRSS_Context::$system_conf->limits;
+
+		//Sort with categories first
+		usort($opml_elements, function ($a, $b) {
+			return strcmp(
+				(isset($a['xmlUrl']) ? 'Z' : 'A') . $a['text'],
+				(isset($b['xmlUrl']) ? 'Z' : 'A') . $b['text']);
+		});
+
+		foreach ($opml_elements as $elt) {
+			if (isset($elt['xmlUrl'])) {
+				// If xmlUrl exists, it means it is a feed
+				if (FreshRSS_Context::$isCli && $nb_feeds >= $limits['max_feeds']) {
+					Minz_Log::warning(_t('feedback.sub.feed.over_max',
+									  $limits['max_feeds']));
+					$ok = false;
+					continue;
+				}
+
+				if ($this->addFeedOpml($elt, $parent_cat)) {
+					$nb_feeds++;
+				} else {
+					$ok = false;
+				}
+			} else {
+				// No xmlUrl? It should be a category!
+				$limit_reached = ($nb_cats >= $limits['max_categories']);
+				if (!FreshRSS_Context::$isCli && $limit_reached) {
+					Minz_Log::warning(_t('feedback.sub.category.over_max',
+									  $limits['max_categories']));
+					$ok = false;
+					continue;
+				}
+
+				if ($this->addCategoryOpml($elt, $parent_cat, $limit_reached)) {
+					$nb_cats++;
+				} else {
+					$ok = false;
+				}
+			}
+		}
+
+		return $ok;
+	}
+
+	/**
+	 * This method imports an OPML feed element.
+	 *
+	 * @param array $feed_elt an OPML element (must be a feed element).
+	 * @param string $parent_cat the name of the parent category.
+	 * @return boolean false if an error occured, true otherwise.
+	 */
+	private function addFeedOpml($feed_elt, $parent_cat) {
+		if ($parent_cat == null) {
+			// This feed has no parent category so we get the default one
+			$this->catDAO->checkDefault();
+			$default_cat = $this->catDAO->getDefault();
+			$parent_cat = $default_cat->name();
+		}
+
+		$cat = $this->catDAO->searchByName($parent_cat);
+		if ($cat == null) {
+			// If there is not $cat, it means parent category does not exist in
+			// database.
+			// If it happens, take the default category.
+			$this->catDAO->checkDefault();
+			$cat = $this->catDAO->getDefault();
+		}
+
+		// We get different useful information
+		$url = Minz_Helper::htmlspecialchars_utf8($feed_elt['xmlUrl']);
+		$name = Minz_Helper::htmlspecialchars_utf8($feed_elt['text']);
+		$website = '';
+		if (isset($feed_elt['htmlUrl'])) {
+			$website = Minz_Helper::htmlspecialchars_utf8($feed_elt['htmlUrl']);
+		}
+		$description = '';
+		if (isset($feed_elt['description'])) {
+			$description = Minz_Helper::htmlspecialchars_utf8($feed_elt['description']);
+		}
+
+		$error = false;
+		try {
+			// Create a Feed object and add it in DB
+			$feed = new FreshRSS_Feed($url);
+			$feed->_category($cat->id());
+			$feed->_name($name);
+			$feed->_website($website);
+			$feed->_description($description);
+
+			// Call the extension hook
+			$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
+			if ($feed != null) {
+				// addFeedObject checks if feed is already in DB so nothing else to
+				// check here
+				$id = $this->feedDAO->addFeedObject($feed);
+				$error = ($id === false);
+			} else {
+				$error = true;
+			}
+		} catch (FreshRSS_Feed_Exception $e) {
+			if (FreshRSS_Context::$isCli) {
+				fwrite(STDERR, 'FreshRSS error during OPML feed import: ' . $e->getMessage() . "\n");
+			} else {
+				Minz_Log::warning($e->getMessage());
+			}
+			$error = true;
+		}
+
+		if ($error) {
+			if (FreshRSS_Context::$isCli) {
+				fwrite(STDERR, 'FreshRSS error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id() . "\n");
+			} else {
+				Minz_Log::warning('Error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id());
+			}
+		}
+
+		return !$error;
+	}
+
+	/**
+	 * This method imports an OPML category element.
+	 *
+	 * @param array $cat_elt an OPML element (must be a category element).
+	 * @param string $parent_cat the name of the parent category.
+	 * @param boolean $cat_limit_reached indicates if category limit has been reached.
+	 *                if yes, category is not added (but we try for feeds!)
+	 * @return boolean false if an error occured, true otherwise.
+	 */
+	private function addCategoryOpml($cat_elt, $parent_cat, $cat_limit_reached) {
+		// Create a new Category object
+		$catName = Minz_Helper::htmlspecialchars_utf8($cat_elt['text']);
+		$cat = new FreshRSS_Category($catName);
+
+		$error = true;
+		if (FreshRSS_Context::$isCli || !$cat_limit_reached) {
+			$id = $this->catDAO->addCategoryObject($cat);
+			$error = ($id === false);
+		}
+		if ($error) {
+			if (FreshRSS_Context::$isCli) {
+				fwrite(STDERR, 'FreshRSS error during OPML category import from URL: ' . $catName . "\n");
+			} else {
+				Minz_Log::warning('Error during OPML category import from URL: ' . $catName);
+			}
+		}
+
+		if (isset($cat_elt['@outlines'])) {
+			// Our cat_elt contains more categories or more feeds, so we
+			// add them recursively.
+			// Note: FreshRSS does not support yet category arborescence
+			$error &= !$this->addOpmlElements($cat_elt['@outlines'], $catName);
+		}
+
+		return !$error;
+	}
+}

+ 1 - 1
lib/lib_opml.php

@@ -201,7 +201,7 @@ function libopml_parse_string($xml, $strict = true) {
 
 
 	if (!$at_least_one_outline) {
 	if (!$at_least_one_outline) {
 		throw new LibOPML_Exception(
 		throw new LibOPML_Exception(
-			'Body must contain at least one outline element'
+			'OPML body must contain at least one outline element'
 		);
 		);
 	}
 	}
 
 

+ 31 - 0
p/api/greader.php

@@ -270,6 +270,29 @@ function tagList() {
 	exit();
 	exit();
 }
 }
 
 
+function subscriptionExport() {
+	$user = Minz_Session::param('currentUser', '_');
+	$export_service = new FreshRSS_Export_Service($user);
+	list($filename, $content) = $export_service->generateOpml();
+	header('Content-Type: application/xml; charset=UTF-8');
+	header('Content-disposition: attachment; filename="' . $filename . '"');
+	echo $content;
+	exit();
+}
+
+function subscriptionImport($opml) {
+	$user = Minz_Session::param('currentUser', '_');
+	$importService = new FreshRSS_Import_Service($user);
+	$ok = $importService->importOpml($opml);
+	if ($ok) {
+		list($nbUpdatedFeeds, $feed, $nbNewArticles) = FreshRSS_feed_Controller::actualizeFeed(0, '', true);
+		invalidateHttpCache($user);
+		exit('OK');
+	} else {
+		badRequest();
+	}
+}
+
 function subscriptionList() {
 function subscriptionList() {
 	header('Content-Type: application/json; charset=UTF-8');
 	header('Content-Type: application/json; charset=UTF-8');
 
 
@@ -1042,6 +1065,14 @@ if ($pathInfos[1] === 'accounts') {
 		case 'subscription':
 		case 'subscription':
 			if (isset($pathInfos[5])) {
 			if (isset($pathInfos[5])) {
 				switch ($pathInfos[5]) {
 				switch ($pathInfos[5]) {
+					case 'export':
+						subscriptionExport();
+						break;
+					case 'import':
+						if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && $ORIGINAL_INPUT != '') {
+							subscriptionImport($ORIGINAL_INPUT);
+						}
+						break;
 					case 'list':
 					case 'list':
 						$output = isset($_GET['output']) ? $_GET['output'] : '';
 						$output = isset($_GET['output']) ? $_GET['output'] : '';
 						if ($output !== 'json') notImplemented();
 						if ($output !== 'json') notImplemented();