From e71721967bcc532bde1c149815e0fa91d4b304f7 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Sun, 8 Feb 2026 15:25:49 +0100 Subject: [PATCH 1/4] test: Test export structure Signed-off-by: Marcel Klehr --- tests/HtmlImportExportTest.php | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/tests/HtmlImportExportTest.php b/tests/HtmlImportExportTest.php index a011307a6..0ed8b86d6 100644 --- a/tests/HtmlImportExportTest.php +++ b/tests/HtmlImportExportTest.php @@ -131,7 +131,7 @@ public function testExport(...$bookmarks): void { // Set up database for ($i = 0; $i < 4; $i++) { $f = new Db\Folder(); - $f->setTitle($i); + $f->setTitle(md5($i)); $f->setUserId($this->userId); $f = $this->folderMapper->insert($f); $this->treeMapper->move(Db\TreeMapper::TYPE_FOLDER, $f->getId(), $rootFolder->getId()); @@ -143,13 +143,32 @@ public function testExport(...$bookmarks): void { $exported = $this->htmlExporter->exportFolder($this->userId, $rootFolder->getId()); - $rootFolders = $this->treeMapper->findChildren(Db\TreeMapper::TYPE_FOLDER, $rootFolder->getId()); - $this->assertCount(4, $rootFolders); - foreach ($rootFolders as $rootFolder) { + $exportedRootFolders = $this->treeMapper->findChildren(Db\TreeMapper::TYPE_FOLDER, $rootFolder->getId()); + $this->assertCount(4, $exportedRootFolders); + foreach ($exportedRootFolders as $rootFolder) { foreach ($this->treeMapper->findChildren(Db\TreeMapper::TYPE_BOOKMARK, $rootFolder->getId()) as $bookmark) { + $this->assertStringContainsString($rootFolder->getTitle(), $exported); $this->assertStringContainsString($bookmark->getUrl(), $exported); } } + + $f = new Db\Folder(); + $f->setTitle('Testimport'); + $f->setUserId($this->userId); + $f = $this->folderMapper->insert($f); + $this->htmlImporter->import($this->userId, $exported, $f->getId()); + $importedRootFolders = $this->treeMapper->findChildren(Db\TreeMapper::TYPE_FOLDER, $f->getId()); + foreach ($importedRootFolders as $i => $importedRootFolder) { + $exportedRootFolder = $exportedRootFolders[$i]; + $this->assertEquals($exportedRootFolder->getTitle(), $importedRootFolder->getTitle()); + $exportedBookmarks = $this->treeMapper->findChildren(Db\TreeMapper::TYPE_BOOKMARK, $exportedRootFolder->getId()); + foreach ($this->treeMapper->findChildren(Db\TreeMapper::TYPE_BOOKMARK, $importedRootFolder->getId()) as $j => $bookmark) { + $this->assertEquals($bookmark->getUrl(), $exportedBookmarks[$j]->getUrl()); + $this->assertEquals($bookmark->getTitle(), $exportedBookmarks[$j]->getTitle()); + $this->assertEquals($bookmark->getDescription(), $exportedBookmarks[$j]->getDescription()); + $this->assertEquals($this->tagMapper->findByBookmark($bookmark->getId()), $this->tagMapper->findByBookmark($exportedBookmarks[$j]->getId())); + } + } } public function importProvider(): array { @@ -166,10 +185,10 @@ public function exportProvider(): array { return Db\Bookmark::fromArray($props); }, [ ['url' => 'https://google.com/', 'title' => 'Google', 'description' => 'Search engine'], - ['url' => 'https://nextcloud.com/', 'title' => 'Nextcloud', 'description' => ''], + ['url' => 'https://nextcloud.com/', 'title' => 'Nextcloud', 'description' => 'cloud cloud cloud'], ['url' => 'https://php.net/', 'title' => '', 'description' => ''], - ['url' => 'https://de.wikipedia.org/wiki/%C3%9C', 'title' => '', 'description' => ''], - ['url' => 'https://github.com/nextcloud/bookmarks/projects/1', 'title' => '', 'description' => ''], + ['url' => 'https://de.wikipedia.org/wiki/%C3%9C', 'title' => '', 'description' => '

Hello

'], + ['url' => 'https://github.com/nextcloud/bookmarks/projects/1', 'title' => '', 'description' => ''], ]), ]; } From 3125c292030750109323b8dca249fd4e64b31ad8 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Sun, 8 Feb 2026 15:11:45 +0100 Subject: [PATCH 2/4] fix(HtmlExporter): Make sure to produce correct NETSCAPE-Bookmark-file-1 markup Signed-off-by: Marcel Klehr --- lib/Service/HtmlExporter.php | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/Service/HtmlExporter.php b/lib/Service/HtmlExporter.php index 4e5cb2a4a..25d5a2bae 100644 --- a/lib/Service/HtmlExporter.php +++ b/lib/Service/HtmlExporter.php @@ -70,7 +70,9 @@ public function exportFolder(string $userId, int $folderId): string { Bookmarks'; + $file .= "

\n"; $file .= $this->serializeFolder($userId, $folderId, true); + $file .= "

\n"; return $file; } @@ -84,14 +86,15 @@ public function exportFolder(string $userId, int $folderId): string { * @throws MultipleObjectsReturnedException * @throws UnauthorizedAccessError */ - protected function serializeFolder(string $userId, int $id, bool $onlyContent = false): string { - if ($onlyContent) { - $output = ''; - } else { + protected function serializeFolder(string $userId, int $id, string $indent = ''): string { + $nextIndent = $indent . ' '; + $childFolders = $this->treeMapper->findChildren(TreeMapper::TYPE_FOLDER, $id); + foreach ($childFolders as $childFolder) { $folder = $this->folderMapper->find($id); - $output = '

' . htmlspecialchars($folder->getTitle()) . '

' . "\n" - . '

'; + $output = $indent . '

' . htmlspecialchars($folder->getTitle()) . '

' . "\n"; + $output .= $indent . '

' . "\n" . $this->serializeFolder($userId, $childFolder->getId(), $nextIndent) . '

' . "\n"; } + $childBookmarks = $this->treeMapper->findChildren(TreeMapper::TYPE_BOOKMARK, $id); foreach ($childBookmarks as $bookmark) { // discards records with no URL. This should not happen but @@ -111,19 +114,12 @@ protected function serializeFolder(string $userId, int $id, bool $onlyContent = $title = Util::sanitizeHTML($title); $description = Util::sanitizeHTML($bookmark->getDescription()); - $output .= '

' . $title . '' . "\n"; + $output .= $indent . '
' . $title . '' . "\n"; if ($description !== '') { $output .= '
' . $description . '
'; } $output .= "\n"; } - - $childFolders = $this->treeMapper->findChildren(TreeMapper::TYPE_FOLDER, $id); - foreach ($childFolders as $childFolder) { - $output .= $this->serializeFolder($userId, $childFolder->getId()); - } - - $output .= '

'; return $output; } } From c9b811c1b08182da91775c2e08be7e82847648a8 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Sun, 8 Feb 2026 17:46:58 +0100 Subject: [PATCH 3/4] fix(HtmlExporter) Signed-off-by: Marcel Klehr --- lib/Service/HtmlExporter.php | 9 +++------ tests/HtmlImportExportTest.php | 8 ++++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/Service/HtmlExporter.php b/lib/Service/HtmlExporter.php index 25d5a2bae..cd3f60036 100644 --- a/lib/Service/HtmlExporter.php +++ b/lib/Service/HtmlExporter.php @@ -71,7 +71,7 @@ public function exportFolder(string $userId, int $folderId): string { Bookmarks'; $file .= "

\n"; - $file .= $this->serializeFolder($userId, $folderId, true); + $file .= $this->serializeFolder($userId, $folderId); $file .= "

\n"; return $file; @@ -87,11 +87,11 @@ public function exportFolder(string $userId, int $folderId): string { * @throws UnauthorizedAccessError */ protected function serializeFolder(string $userId, int $id, string $indent = ''): string { + $output = ''; $nextIndent = $indent . ' '; $childFolders = $this->treeMapper->findChildren(TreeMapper::TYPE_FOLDER, $id); foreach ($childFolders as $childFolder) { - $folder = $this->folderMapper->find($id); - $output = $indent . '

' . htmlspecialchars($folder->getTitle()) . '

' . "\n"; + $output .= $indent . '

' . htmlspecialchars($childFolder->getTitle()) . '

' . "\n"; $output .= $indent . '

' . "\n" . $this->serializeFolder($userId, $childFolder->getId(), $nextIndent) . '

' . "\n"; } @@ -108,9 +108,6 @@ protected function serializeFolder(string $userId, int $id, string $indent = '') $tags = Util::sanitizeHTML(implode(',', $tags)); $title = trim($bookmark->getTitle()); $url = Util::sanitizeHTML($bookmark->getUrl()); - if ($title === '') { - $title = $url; - } $title = Util::sanitizeHTML($title); $description = Util::sanitizeHTML($bookmark->getDescription()); diff --git a/tests/HtmlImportExportTest.php b/tests/HtmlImportExportTest.php index 0ed8b86d6..156f0ed72 100644 --- a/tests/HtmlImportExportTest.php +++ b/tests/HtmlImportExportTest.php @@ -145,9 +145,9 @@ public function testExport(...$bookmarks): void { $exportedRootFolders = $this->treeMapper->findChildren(Db\TreeMapper::TYPE_FOLDER, $rootFolder->getId()); $this->assertCount(4, $exportedRootFolders); - foreach ($exportedRootFolders as $rootFolder) { - foreach ($this->treeMapper->findChildren(Db\TreeMapper::TYPE_BOOKMARK, $rootFolder->getId()) as $bookmark) { - $this->assertStringContainsString($rootFolder->getTitle(), $exported); + foreach ($exportedRootFolders as $exportedRootFolder) { + foreach ($this->treeMapper->findChildren(Db\TreeMapper::TYPE_BOOKMARK, $exportedRootFolder->getId()) as $bookmark) { + $this->assertStringContainsString($exportedRootFolder->getTitle(), $exported); $this->assertStringContainsString($bookmark->getUrl(), $exported); } } @@ -160,7 +160,7 @@ public function testExport(...$bookmarks): void { $importedRootFolders = $this->treeMapper->findChildren(Db\TreeMapper::TYPE_FOLDER, $f->getId()); foreach ($importedRootFolders as $i => $importedRootFolder) { $exportedRootFolder = $exportedRootFolders[$i]; - $this->assertEquals($exportedRootFolder->getTitle(), $importedRootFolder->getTitle()); + $this->assertEquals($importedRootFolder->getTitle(), $exportedRootFolder->getTitle()); $exportedBookmarks = $this->treeMapper->findChildren(Db\TreeMapper::TYPE_BOOKMARK, $exportedRootFolder->getId()); foreach ($this->treeMapper->findChildren(Db\TreeMapper::TYPE_BOOKMARK, $importedRootFolder->getId()) as $j => $bookmark) { $this->assertEquals($bookmark->getUrl(), $exportedBookmarks[$j]->getUrl()); From 7a627190571ac3d5f01b9c5c867f0632870f721c Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 12 Feb 2026 17:13:56 +0100 Subject: [PATCH 4/4] fix(BookmarkMapper): Don't break transactions Signed-off-by: Marcel Klehr --- lib/Db/BookmarkMapper.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Db/BookmarkMapper.php b/lib/Db/BookmarkMapper.php index 741065bc0..d309804ad 100644 --- a/lib/Db/BookmarkMapper.php +++ b/lib/Db/BookmarkMapper.php @@ -895,6 +895,10 @@ public function insert(Entity $entity): Bookmark { return $entity; } catch (Exception $e) { if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + if ($this->db->inTransaction()) { + $this->db->commit(); + $this->db->beginTransaction(); + } throw new AlreadyExistsError('A bookmark with this URL already exists'); } throw $e;