aboutsummaryrefslogtreecommitdiff
path: root/app/Controllers/importExportController.php
diff options
context:
space:
mode:
authorGravatar Marien Fressinaud <dev@marienfressinaud.fr> 2020-06-13 19:36:24 +0200
committerGravatar GitHub <noreply@github.com> 2020-06-13 19:36:24 +0200
commit15505a03779326f9497644e9827477cdcc26c2d2 (patch)
tree863250aaf3af42491bacd679d928a733fc132c16 /app/Controllers/importExportController.php
parent7a748e25ab7187bba53decd2f41bd7b6383440f3 (diff)
tec: Refactor the export feature (#3045)
Even if the issue #3035 seemed pretty simple at a first glance, it was more complicated than I expected. Because we send CSP headers AFTER running the controller actions, it means we can't "echo" any content from the controller. It's in fact a good practice, but it was easier at the time we developed the feature. To fix that, the only thing I had to do was to move the `print()` and `readfile()` function into the view. The problem was that we needed to output the content from the CLI too. Then, things became more complicated. I decided to extract the export-related methods in a `FreshRSS_Export_Service` class, in order to use it from both the controller and the CLI. It was an opportunity to refactor the whole feature in order to make it a bit more linear and easy to read. Reference: https://github.com/FreshRSS/FreshRSS/issues/3035
Diffstat (limited to 'app/Controllers/importExportController.php')
-rw-r--r--app/Controllers/importExportController.php258
1 files changed, 83 insertions, 175 deletions
diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php
index d4e1ae81c..3187e1325 100644
--- a/app/Controllers/importExportController.php
+++ b/app/Controllers/importExportController.php
@@ -721,76 +721,6 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
}
/**
- * Export the files of a user and send them to the user by HTTP
- *
- * @param string $username
- * @param boolean $export_opml
- * @param boolean $export_starred
- * @param boolean $export_labelled
- * @param array|boolean $export_feeds The list of feeds ids to export, true to export all the feeds
- * @param integer $maxFeedEntries Limit the number of entries to export (default is 50)
- *
- * @throws FreshRSS_ZipMissing_Exception if we try to export more than two files and the zip extension doesn't exist
- *
- * @return integer The number of exported files
- */
- public function exportFile($username, $export_opml = true, $export_starred = false, $export_labelled = false, $export_feeds = array(), $maxFeedEntries = 50) {
- require_once(LIB_PATH . '/lib_opml.php');
-
- $this->catDAO = new FreshRSS_CategoryDAO($username);
- $this->entryDAO = FreshRSS_Factory::createEntryDao($username);
- $this->feedDAO = FreshRSS_Factory::createFeedDao($username);
-
- if ($export_feeds === true) {
- //All feeds
- $export_feeds = $this->feedDAO->listFeedsIds();
- }
- if (!is_array($export_feeds)) {
- $export_feeds = array();
- }
-
- $day = date('Y-m-d');
-
- $export_files = array();
- if ($export_opml) {
- $export_files["feeds_${day}.opml.xml"] = $this->generateOpml();
- }
-
- if ($export_starred || $export_labelled) {
- $export_files["starred_${day}.json"] = $this->generateEntries(
- ($export_starred ? 'S' : '') .
- ($export_labelled ? 'T' : '')
- );
- }
-
- foreach ($export_feeds as $feed_id) {
- $feed = $this->feedDAO->searchById($feed_id);
- if ($feed) {
- $filename = "feed_${day}_" . $feed->category() . '_'
- . $feed->id() . '.json';
- $export_files[$filename] = $this->generateEntries('f', $feed, $maxFeedEntries);
- }
- }
-
- $nb_files = count($export_files);
- if ($nb_files > 1) {
- // If there are more than 1 file to export, we need a ZIP archive.
- $filename = 'freshrss_' . $username . '_' . $day . '_export.zip';
- try {
- $this->sendZip($filename, $export_files);
- } catch (Exception $e) {
- throw new FreshRSS_ZipMissing_Exception($e);
- }
- } elseif ($nb_files === 1) {
- // Only one file? Guess its type and export it.
- $filename = key($export_files);
- $type = self::guessFileType($filename);
- $this->sendFile('freshrss_' . $username . '_' . $filename, $export_files[$filename], $type);
- }
- return $nb_files;
- }
-
- /**
* This action handles export action.
*
* This action must be reached by a POST request.
@@ -798,135 +728,113 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
* Parameters are:
* - export_opml (default: false)
* - export_starred (default: false)
+ * - export_labelled (default: false)
* - export_feeds (default: array()) a list of feed ids
*/
public function exportAction() {
if (!Minz_Request::isPost()) {
- Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
- }
- $this->view->_layout(false);
-
- $nb_files = 0;
- try {
- $nb_files = $this->exportFile(
- Minz_Session::param('currentUser'),
- Minz_Request::param('export_opml', false),
- Minz_Request::param('export_starred', false),
- Minz_Request::param('export_labelled', false),
- Minz_Request::param('export_feeds', array())
+ return Minz_Request::forward(
+ array('c' => 'importExport', 'a' => 'index'),
+ true
);
- } catch (FreshRSS_ZipMissing_Exception $zme) {
- # Oops, there is no ZIP extension!
- Minz_Request::bad(_t('feedback.import_export.export_no_zip_extension'),
- array('c' => 'importExport', 'a' => 'index'));
}
- if ($nb_files < 1) {
- // Nothing to do...
- Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
- }
- }
+ $username = Minz_Session::param('currentUser');
+ $export_service = new FreshRSS_Export_Service($username);
- /**
- * This method returns the OPML file based on user subscriptions.
- *
- * @return string the OPML file content.
- */
- private function generateOpml() {
- $list = array();
- foreach ($this->catDAO->listCategories() as $key => $cat) {
- $list[$key]['name'] = $cat->name();
- $list[$key]['feeds'] = $this->feedDAO->listByCategory($cat->id());
+ $export_opml = Minz_Request::param('export_opml', false);
+ $export_starred = Minz_Request::param('export_starred', false);
+ $export_labelled = Minz_Request::param('export_labelled', false);
+ $export_feeds = Minz_Request::param('export_feeds', array());
+ $max_number_entries = 50;
+
+ $exported_files = [];
+
+ if ($export_opml) {
+ list($filename, $content) = $export_service->generateOpml();
+ $exported_files[$filename] = $content;
}
- $this->view->categories = $list;
- return $this->view->helperToString('export/opml');
- }
+ // Starred and labelled entries are merged in the same `starred` file
+ // to avoid duplication of content.
+ if ($export_starred && $export_labelled) {
+ list($filename, $content) = $export_service->generateStarredEntries('ST');
+ $exported_files[$filename] = $content;
+ } elseif ($export_starred) {
+ list($filename, $content) = $export_service->generateStarredEntries('S');
+ $exported_files[$filename] = $content;
+ } elseif ($export_labelled) {
+ list($filename, $content) = $export_service->generateStarredEntries('T');
+ $exported_files[$filename] = $content;
+ }
- /**
- * This method returns a JSON file content.
- *
- * @param string $type must be one of:
- * 'S' (starred/favourite), 'f' (feed), 'T' (taggued/labelled), 'ST' (starred or labelled)
- * @param FreshRSS_Feed $feed feed of which we want to get entries.
- * @return string the JSON file content.
- */
- private function generateEntries($type, $feed = null, $maxFeedEntries = 50) {
- $this->view->categories = $this->catDAO->listCategories();
- $tagDAO = FreshRSS_Factory::createTagDao();
+ foreach ($export_feeds as $feed_id) {
+ $result = $export_service->generateFeedEntries($feed_id, $max_number_entries);
+ if (!$result) {
+ // It means the actual feed_id doesn't correspond to any existing feed
+ continue;
+ }
- if ($type === 's' || $type === 'S' || $type === 'T' || $type === 'ST') {
- $this->view->list_title = _t('sub.import_export.starred_list');
- $this->view->type = 'starred';
- $this->view->entriesId = $this->entryDAO->listIdsWhere($type, '', FreshRSS_Entry::STATE_ALL, 'ASC', -1);
- $this->view->entryIdsTagNames = $tagDAO->getEntryIdsTagNames($this->view->entriesId);
- //The following is a streamable query, i.e. must be last
- $this->view->entriesRaw = $this->entryDAO->listWhereRaw($type, '', FreshRSS_Entry::STATE_ALL, 'ASC', -1);
- } elseif ($type === 'f' && $feed != null) {
- $this->view->list_title = _t('sub.import_export.feed_list', $feed->name());
- $this->view->type = 'feed/' . $feed->id();
- $this->view->entriesId = $this->entryDAO->listIdsWhere($type, $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', $maxFeedEntries);
- $this->view->entryIdsTagNames = $tagDAO->getEntryIdsTagNames($this->view->entriesId);
- //The following is a streamable query, i.e. must be last
- $this->view->entriesRaw = $this->entryDAO->listWhereRaw($type, $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', $maxFeedEntries);
- $this->view->feed = $feed;
- }
-
- return $this->view->helperToString('export/articles');
- }
+ list($filename, $content) = $result;
+ $exported_files[$filename] = $content;
+ }
- /**
- * This method zips a list of files and returns it by HTTP.
- *
- * @param string $export_filename The name of the file to export
- * @param array $files list of files where key is filename and value the content.
- * @throws Exception if Zip extension is not loaded.
- */
- private function sendZip($export_filename, $files) {
- if (!extension_loaded('zip')) {
- throw new Exception();
+ $nb_files = count($exported_files);
+ if ($nb_files <= 0) {
+ // There's nothing to do, there're no files to export
+ return Minz_Request::forward(
+ array('c' => 'importExport', 'a' => 'index'),
+ true
+ );
}
- // From https://stackoverflow.com/questions/1061710/php-zip-files-on-the-fly
- $zip_file = @tempnam('/tmp', 'zip');
- $zip = new ZipArchive();
- $zip->open($zip_file, ZipArchive::OVERWRITE);
+ if ($nb_files === 1) {
+ // If we only have one file, we just export it as it is
+ $filename = key($exported_files);
+ $content = $exported_files[$filename];
+ } else {
+ // More files? Let's compress them in a Zip archive
+ if (!extension_loaded('zip')) {
+ // Oops, there is no ZIP extension!
+ return Minz_Request::bad(
+ _t('feedback.import_export.export_no_zip_extension'),
+ array('c' => 'importExport', 'a' => 'index')
+ );
+ }
- foreach ($files as $filename => $content) {
- $zip->addFromString($filename, $content);
+ list($filename, $content) = $export_service->zip($exported_files);
}
- // Close and send to user
- $zip->close();
- header('Content-Type: application/zip');
- header('Content-Length: ' . filesize($zip_file));
- header('Content-Disposition: attachment; filename="' . $export_filename . '"');
- readfile($zip_file);
- unlink($zip_file);
+ $content_type = self::filenameToContentType($filename);
+ header('Content-Type: ' . $content_type);
+ header('Content-disposition: attachment; filename="' . $filename . '"');
+
+ $this->view->_layout(false);
+ $this->view->content = $content;
}
/**
- * This method returns a single file (OPML or JSON) by HTTP.
+ * Return the Content-Type corresponding to a filename.
+ *
+ * If the type of the filename is not supported, it returns
+ * `application/octet-stream` by default.
*
* @param string $filename
- * @param string $content
- * @param string $type the file type (opml, json_feed or json_starred).
- * If equals to unknown, nothing happens.
+ *
+ * @return string
*/
- private function sendFile($filename, $content, $type) {
- if ($type === 'unknown') {
- return;
- }
-
- $content_type = '';
- if ($type === 'opml') {
- $content_type = 'application/xml';
- } elseif ($type === 'json_feed' || $type === 'json_starred') {
- $content_type = 'application/json';
+ private static function filenameToContentType($filename) {
+ $filetype = self::guessFileType($filename);
+ switch ($filetype) {
+ case 'zip':
+ return 'application/zip';
+ case 'opml':
+ return 'application/xml; charset=utf-8';
+ case 'json_starred':
+ case 'json_feed':
+ return 'application/json; charset=utf-8';
+ default:
+ return 'application/octet-stream';
}
-
- header('Content-Type: ' . $content_type . '; charset=utf-8');
- header('Content-disposition: attachment; filename=' . $filename);
- print($content);
}
}