From 15505a03779326f9497644e9827477cdcc26c2d2 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 13 Jun 2020 19:36:24 +0200 Subject: 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 --- app/Services/ExportService.php | 189 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 app/Services/ExportService.php (limited to 'app/Services/ExportService.php') diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php new file mode 100644 index 000000000..38f896324 --- /dev/null +++ b/app/Services/ExportService.php @@ -0,0 +1,189 @@ +username = $username; + + $this->category_dao = new FreshRSS_CategoryDAO($username); + $this->feed_dao = FreshRSS_Factory::createFeedDao($username); + $this->entry_dao = FreshRSS_Factory::createEntryDao($username); + $this->tag_dao = FreshRSS_Factory::createTagDao(); + } + + /** + * Generate OPML file content. + * + * @return array First item is the filename, second item is the content + */ + public function generateOpml() { + require_once(LIB_PATH . '/lib_opml.php'); + + $view = new Minz_View(); + $day = date('Y-m-d'); + $categories = []; + + foreach ($this->category_dao->listCategories() as $key => $category) { + $categories[$key]['name'] = $category->name(); + $categories[$key]['feeds'] = $this->feed_dao->listByCategory($category->id()); + } + + $view->categories = $categories; + + return [ + "feeds_{$day}.opml.xml", + $view->helperToString('export/opml') + ]; + } + + /** + * Generate the starred and labelled entries file content. + * + * Both starred and labelled entries are put into a "starred" file, that's + * why there is only one method for both. + * + * @param string $type must be one of: + * 'S' (starred/favourite), + * 'T' (taggued/labelled), + * 'ST' (starred or labelled) + * + * @return array First item is the filename, second item is the content + */ + public function generateStarredEntries($type) { + $view = new Minz_View(); + $view->categories = $this->category_dao->listCategories(); + $day = date('Y-m-d'); + + $view->list_title = _t('sub.import_export.starred_list'); + $view->type = 'starred'; + $view->entriesId = $this->entry_dao->listIdsWhere( + $type, '', FreshRSS_Entry::STATE_ALL, 'ASC', -1 + ); + $view->entryIdsTagNames = $this->tag_dao->getEntryIdsTagNames($view->entriesId); + // The following is a streamable query, i.e. must be last + $view->entriesRaw = $this->entry_dao->listWhereRaw( + $type, '', FreshRSS_Entry::STATE_ALL, 'ASC', -1 + ); + + return [ + "starred_{$day}.json", + $view->helperToString('export/articles') + ]; + } + + /** + * Generate the entries file content for the given feed. + * + * @param integer $feed_id + * @param integer $max_number_entries + * + * @return array|null First item is the filename, second item is the content. + * It also can return null if the feed doesn't exist. + */ + public function generateFeedEntries($feed_id, $max_number_entries) { + $feed = $this->feed_dao->searchById($feed_id); + if (!$feed) { + return null; + } + + $view = new Minz_View(); + $view->categories = $this->category_dao->listCategories(); + $view->feed = $feed; + $day = date('Y-m-d'); + $filename = "feed_{$day}_" . $feed->category() . '_' . $feed->id() . '.json'; + + $view->list_title = _t('sub.import_export.feed_list', $feed->name()); + $view->type = 'feed/' . $feed->id(); + $view->entriesId = $this->entry_dao->listIdsWhere( + 'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', $max_number_entries + ); + $view->entryIdsTagNames = $this->tag_dao->getEntryIdsTagNames($view->entriesId); + // The following is a streamable query, i.e. must be last + $view->entriesRaw = $this->entry_dao->listWhereRaw( + 'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', $max_number_entries + ); + + return [ + $filename, + $view->helperToString('export/articles') + ]; + } + + /** + * Generate the entries file content for all the feeds. + * + * @param integer $max_number_entries + * + * @return array Keys are filenames and values are contents. + */ + public function generateAllFeedEntries($max_number_entries) { + $feed_ids = $this->feed_dao->listFeedsIds(); + + $exported_files = []; + foreach ($feed_ids as $feed_id) { + $result = $this->generateFeedEntries($feed_id, $max_number_entries); + if (!$result) { + continue; + } + + list($filename, $content) = $result; + $exported_files[$filename] = $content; + } + + return $exported_files; + } + + /** + * Compress several files in a Zip file. + * + * @param array $files where first item is the filename, second item is the content + * + * @return array First item is the zip filename, second item is the zip content + */ + public function zip($files) { + $day = date('Y-m-d'); + $zip_filename = 'freshrss_' . $this->username . '_' . $day . '_export.zip'; + + // From https://stackoverflow.com/questions/1061710/php-zip-files-on-the-fly + $zip_file = @tempnam('/tmp', 'zip'); + $zip_archive = new ZipArchive(); + $zip_archive->open($zip_file, ZipArchive::OVERWRITE); + + foreach ($files as $filename => $content) { + $zip_archive->addFromString($filename, $content); + } + + $zip_archive->close(); + + $content = file_get_contents($zip_file); + + unlink($zip_file); + + return [ + $zip_filename, + $content, + ]; + } +} -- cgit v1.2.3