diff options
author | Sean Molenaar <sean@seanmolenaar.eu> | 2024-11-12 17:16:26 +0100 |
---|---|---|
committer | Sean Molenaar <sean@seanmolenaar.eu> | 2024-11-16 15:49:28 +0100 |
commit | 1ff33e92ac4de44efae3509dece068f46da61e53 (patch) | |
tree | 0e978df4e7c08ee6ecefe52c08e4586b98657e23 | |
parent | 99fabc323933f9078834dfaa911357860624c8fc (diff) |
feat: Add OPML import in CLI
Signed-off-by: Sean Molenaar <sean@seanmolenaar.eu>
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | appinfo/info.xml | 1 | ||||
-rw-r--r-- | lib/Command/Config/OpmlImport.php | 63 | ||||
-rw-r--r-- | lib/Service/OpmlService.php | 73 | ||||
-rw-r--r-- | lib/Utility/OPMLImporter.php | 95 | ||||
-rw-r--r-- | tests/Unit/Service/OPMLServiceTest.php | 12 |
6 files changed, 222 insertions, 23 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 456ef245d..75efe1111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ You can also check [on GitHub](https://github.com/nextcloud/news/releases), the ## [25.x.x] ### Changed - If title of feed is empty during creation set hostname of feed as title (#2872) +- Add command to import OPML file ### Fixed - Feed without Title returned by DB causes exception (#2872) diff --git a/appinfo/info.xml b/appinfo/info.xml index c0bb40dff..922bc114d 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -84,6 +84,7 @@ Report a [feed issue](https://github.com/nextcloud/news/discussions/new) <command>OCA\News\Command\Config\FeedDelete</command> <command>OCA\News\Command\Config\FeedDelete</command> <command>OCA\News\Command\Config\OpmlExport</command> + <command>OCA\News\Command\Config\OpmlImport</command> <command>OCA\News\Command\Debug\ItemList</command> <command>OCA\News\Command\Debug\FolderItemList</command> <command>OCA\News\Command\Debug\FeedItemList</command> diff --git a/lib/Command/Config/OpmlImport.php b/lib/Command/Config/OpmlImport.php new file mode 100644 index 000000000..0feae62e5 --- /dev/null +++ b/lib/Command/Config/OpmlImport.php @@ -0,0 +1,63 @@ +<?php + +namespace OCA\News\Command\Config; + +use OCA\News\Service\OpmlService; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class OpmlImport extends Command +{ + /** + * @var OpmlService service for the data. + */ + protected $opmlService; + + public function __construct(OpmlService $opmlService) + { + parent::__construct(null); + + $this->opmlService = $opmlService; + } + + /** + * Configure command + * + * @return void + */ + protected function configure() + { + $this->setName('news:opml:import') + ->setDescription('Import OPML file') + ->addArgument('user-id', InputArgument::REQUIRED, 'User to import data for') + ->addArgument('file', InputArgument::REQUIRED, 'Data to import'); + } + + /** + * Execute command + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $user = $input->getArgument('user-id'); + $data = file_get_contents($input->getArgument('file')); + if ($data === false) { + $output->writeln('Failed to read data file!'); + return 2; + } + + $success = $this->opmlService->import($user, $data); + if ($success === false) { + $output->write("Failed to import data"); + return 1; + } + + return 0; + } +} diff --git a/lib/Service/OpmlService.php b/lib/Service/OpmlService.php index f03e0448b..32bb28c62 100644 --- a/lib/Service/OpmlService.php +++ b/lib/Service/OpmlService.php @@ -15,33 +15,18 @@ namespace OCA\News\Service; use OCA\News\Utility\OPMLExporter; +use OCA\News\Utility\OPMLImporter; +use OCA\News\Db\Folder; class OpmlService { - - /** - * @var FolderServiceV2 - */ - private $folderService; - - /** - * @var FeedServiceV2 - */ - private $feedService; - - /** - * @var OPMLExporter - */ - private $exporter; - public function __construct( - FolderServiceV2 $folderService, - FeedServiceV2 $feedService, - OPMLExporter $exporter + private FolderServiceV2 $folderService, + private FeedServiceV2 $feedService, + private OPMLExporter $exporter, + private OPMLImporter $importer, ) { - $this->folderService = $folderService; - $this->feedService = $feedService; - $this->exporter = $exporter; + //NO-OP } /** @@ -59,4 +44,48 @@ class OpmlService return $this->exporter->build($folders, $feeds) ->saveXML(); } + + /** + * Import all feeds and folders for a user. + * + * @param string $userId User ID + * @param string $data OPML data + * + * @return bool Status of the import + */ + public function import(string $userId, string $data): bool + { + list($folders, $feeds) = $this->importer->import($userId, $data); + + $folderEntities = []; + $dbFolders = $this->folderService->findAllForUser($userId); + foreach ($folders as $folder) { + $existing = array_filter($dbFolders, fn(Folder $dbFolder) => $dbFolder->getName() === $folder['name']); + + if (count($existing) > 0) { + $folderEntities[$folder['name']] = $existing[0]; + continue; + } + + $folderEntities[$folder['name']] = $this->folderService->create( + $userId, + $folder['name'], + $folderEntities[$folder['parentName']] ?? null, + ); + } + + foreach ($feeds as $feed) { + $parent = $folderEntities[$feed['folder']] ?? null; + $this->feedService->create( + $userId, + $feed['url'], + $parent?->getId(), + full_text: false, + title: $feed['title'], + full_discover: false, + ); + } + + return true; + } } diff --git a/lib/Utility/OPMLImporter.php b/lib/Utility/OPMLImporter.php new file mode 100644 index 000000000..0068f3e71 --- /dev/null +++ b/lib/Utility/OPMLImporter.php @@ -0,0 +1,95 @@ +<?php +/** + * Nextcloud - News + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + */ + +namespace OCA\News\Utility; + +use \DOMDocument; +use \DOMElement; +use \DOMText; + +/** + * Imports the OPML + */ +class OPMLImporter +{ + + /** + * The user ID to import to. + */ + private ?string $userId = null; + + /** + * Intermediate data. + */ + private array $feeds = []; + private array $folders = []; + + /** + * Imports the OPML + * + * @return null|array{0: list<array<string,mixed>>, 1: list<array<string,mixed>>} the items to import + */ + public function import(string $userId, string $data): ?array + { + $this->feeds = []; + $this->folders = []; + $this->userId = $userId; + + $document = new DOMDocument('1.0', 'UTF-8'); + $loaded = $document->loadXML($data); + if ($loaded === false) { + return null; + } + + $bodies = $document->getElementsByTagName('body'); + if ($bodies->count() < 1) { + return null; + } + + foreach ($bodies[0]->childNodes as $node) { + if ($node instanceof DOMText) { + continue; + } + + $this->outlineToItem($node); + } + + return [$this->folders, $this->feeds]; + } + + private function outlineToItem(DOMElement $outline, ?string $parent = null): void + { + if ($outline->getAttribute('type') === 'rss') { + $feed = [ + 'link' => $outline->getAttribute('htmlUrl'), + 'url' => $outline->getAttribute('xmlUrl'), + 'title' => $outline->getAttribute('title'), + 'folder' => $parent, + ]; + + $this->feeds[] = $feed; + return; + } + + $folder = ['name' => $outline->getAttribute('text'), 'parentName' => $parent]; + + $this->folders[] = $folder; + + if ($outline->hasChildNodes() === false) { + return; + } + + foreach ($outline->childNodes as $child) { + if ($child instanceof DOMText) { + continue; + } + + $this->outlineToItem($child, $folder['name']); + } + } +} diff --git a/tests/Unit/Service/OPMLServiceTest.php b/tests/Unit/Service/OPMLServiceTest.php index 83335dfe8..1e785d2e3 100644 --- a/tests/Unit/Service/OPMLServiceTest.php +++ b/tests/Unit/Service/OPMLServiceTest.php @@ -19,6 +19,7 @@ use OCA\News\Service\FeedServiceV2; use OCA\News\Service\FolderServiceV2; use OCA\News\Service\OpmlService; use OCA\News\Utility\OPMLExporter; +use OCA\News\Utility\OPMLImporter; use OCA\News\Db\Feed; @@ -41,6 +42,11 @@ class OPMLServiceTest extends TestCase */ private $exporter; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|OPMLImporter + */ + private $importer; + /** @var OpmlService */ private $class; @@ -59,6 +65,9 @@ class OPMLServiceTest extends TestCase $this->exporter = $this->getMockBuilder(OPMLExporter::class) ->disableOriginalConstructor() ->getMock(); + $this->importer = $this->getMockBuilder(OPMLImporter::class) + ->disableOriginalConstructor() + ->getMock(); $this->folderService = $this ->getMockBuilder(FolderServiceV2::class) ->disableOriginalConstructor() @@ -73,7 +82,8 @@ class OPMLServiceTest extends TestCase $this->class = new OpmlService( $this->folderService, $this->feedService, - $this->exporter + $this->exporter, + $this->importer, ); $this->uid = 'jack'; } |