summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSean Molenaar <sean@seanmolenaar.eu>2024-11-12 17:16:26 +0100
committerSean Molenaar <sean@seanmolenaar.eu>2024-11-16 15:49:28 +0100
commit1ff33e92ac4de44efae3509dece068f46da61e53 (patch)
tree0e978df4e7c08ee6ecefe52c08e4586b98657e23
parent99fabc323933f9078834dfaa911357860624c8fc (diff)
feat: Add OPML import in CLI
Signed-off-by: Sean Molenaar <sean@seanmolenaar.eu>
-rw-r--r--CHANGELOG.md1
-rw-r--r--appinfo/info.xml1
-rw-r--r--lib/Command/Config/OpmlImport.php63
-rw-r--r--lib/Service/OpmlService.php73
-rw-r--r--lib/Utility/OPMLImporter.php95
-rw-r--r--tests/Unit/Service/OPMLServiceTest.php12
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';
}