diff options
| author | 2021-02-06 12:43:30 +0100 | |
|---|---|---|
| committer | 2021-02-06 12:43:30 +0100 | |
| commit | 4a87f34bcff6afe28e33692e40dcbfb1f663f75a (patch) | |
| tree | f17fc44db9db6ad3397a4f46dbd4f4ceccb374ac | |
| parent | 8edce0e2095a4e78599bfdd1b39fc778ec29c720 (diff) | |
API implement OPML import/export (#3424)
#fix https://github.com/FreshRSS/FreshRSS/issues/3421
| -rw-r--r-- | app/Controllers/importExportController.php | 203 | ||||
| -rw-r--r-- | app/Services/ExportService.php | 2 | ||||
| -rw-r--r-- | app/Services/ImportService.php | 221 | ||||
| -rw-r--r-- | lib/lib_opml.php | 2 | ||||
| -rw-r--r-- | p/api/greader.php | 31 |
5 files changed, 260 insertions, 199 deletions
diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 9e89189fd..aa9ffaba7 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -16,7 +16,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { require_once(LIB_PATH . '/lib_opml.php'); - $this->catDAO = new FreshRSS_CategoryDAO(); + $this->catDAO = FreshRSS_Factory::createCategoryDao(); $this->entryDAO = FreshRSS_Factory::createEntryDao(); $this->feedDAO = FreshRSS_Factory::createFeedDao(); } @@ -48,9 +48,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { public function importFile($name, $path, $username = null) { 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->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 // And finally all other files. $ok = true; + + $importService = new FreshRSS_Import_Service($username); + foreach ($list_files['opml'] as $opml_file) { - if (!$this->importOpml($opml_file)) { + if (!$importService->importOpml($opml_file)) { $ok = false; if (FreshRSS_Context::$isCli) { fwrite(STDERR, 'FreshRSS error during OPML import' . "\n"); @@ -213,199 +215,6 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { 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) { $table = (array)simplexml_load_string($xml, null, LIBXML_NOCDATA); $table['items'] = isset($table['article']) ? $table['article'] : array(); diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php index 38f896324..78ef45c4e 100644 --- a/app/Services/ExportService.php +++ b/app/Services/ExportService.php @@ -27,7 +27,7 @@ class FreshRSS_Export_Service { public function __construct($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->entry_dao = FreshRSS_Factory::createEntryDao($username); $this->tag_dao = FreshRSS_Factory::createTagDao(); diff --git a/app/Services/ImportService.php b/app/Services/ImportService.php new file mode 100644 index 000000000..973cd7825 --- /dev/null +++ b/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; + } +} diff --git a/lib/lib_opml.php b/lib/lib_opml.php index 1bd2ee77b..3dd415d05 100644 --- a/lib/lib_opml.php +++ b/lib/lib_opml.php @@ -201,7 +201,7 @@ function libopml_parse_string($xml, $strict = true) { if (!$at_least_one_outline) { throw new LibOPML_Exception( - 'Body must contain at least one outline element' + 'OPML body must contain at least one outline element' ); } diff --git a/p/api/greader.php b/p/api/greader.php index 5c0e754bd..a1665b952 100644 --- a/p/api/greader.php +++ b/p/api/greader.php @@ -270,6 +270,29 @@ function tagList() { 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() { header('Content-Type: application/json; charset=UTF-8'); @@ -1042,6 +1065,14 @@ if ($pathInfos[1] === 'accounts') { case 'subscription': if (isset($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': $output = isset($_GET['output']) ? $_GET['output'] : ''; if ($output !== 'json') notImplemented(); |
