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;