Skip to content

Commit 369dc7b

Browse files
authored
Merge pull request #2460 from nextcloud/fixes
Fixes
2 parents eff3777 + f75b63a commit 369dc7b

7 files changed

Lines changed: 60 additions & 15 deletions

File tree

.github/workflows/lint.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343

4444
strategy:
4545
matrix:
46-
php-versions: ['8.3']
46+
php-versions: ['8.5']
4747

4848
name: cs php${{ matrix.php-versions }}
4949
steps:
@@ -59,7 +59,7 @@ jobs:
5959
coverage: none
6060

6161
- name: Install dependencies
62-
run: composer i
62+
run: composer i --ignore-platform-req=php
6363

6464
- name: Run coding standards check
6565
run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 )

lib/Controller/BookmarkController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ private function _returnBookmarkAsArray(Bookmark $bookmark, bool $deleted = fals
8989
$array['folders'] = array_map(function (Folder $folder) {
9090
return $this->toExternalFolderId($folder->getId());
9191
}, $this->treeMapper->findParentsOf(TreeMapper::TYPE_BOOKMARK, $bookmark->getId()));
92-
}else {
92+
} else {
9393
$array['folders'] = array_map(function (Folder $folder) {
9494
return $this->toExternalFolderId($folder->getId());
9595
}, $this->treeMapper->findParentsOfDeletedBookmarks($bookmark->getId()));

lib/Db/BookmarkMapper.php

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -913,23 +913,29 @@ public function insert(Entity $entity): Bookmark {
913913
$entityToInsert->setLastPreview(0);
914914
$entityToInsert->setClickcount(0);
915915

916+
// Wrap the insert in a (possibly nested) transaction so that a failed
917+
// statement – e.g. a unique constraint violation when the URL already
918+
// exists – can be rolled back cleanly. When this runs inside a larger
919+
// transaction (e.g. the HTML importer) Nextcloud uses a SAVEPOINT, so
920+
// rolling back discards only the failed INSERT instead of poisoning or
921+
// committing the outer transaction. This keeps the connection usable for
922+
// the insertOrUpdate() fallback below on SQLite, MySQL and Postgres alike.
923+
$this->db->beginTransaction();
916924
try {
917925
parent::insert($entityToInsert);
918-
if (isset($this->userBookmarkCount[$entityToInsert->getUserId()])) {
919-
$this->userBookmarkCount[$entityToInsert->getUserId()] += 1;
920-
}
921-
$this->eventDispatcher->dispatchTyped(new InsertEvent('bookmark', $entityToInsert->getId()));
922-
return $entityToInsert;
926+
$this->db->commit();
923927
} catch (Exception $e) {
928+
$this->db->rollBack();
924929
if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
925-
if ($this->db->inTransaction()) {
926-
$this->db->commit();
927-
$this->db->beginTransaction();
928-
}
929930
throw new AlreadyExistsError('A bookmark with this URL already exists');
930931
}
931932
throw $e;
932933
}
934+
if (isset($this->userBookmarkCount[$entityToInsert->getUserId()])) {
935+
$this->userBookmarkCount[$entityToInsert->getUserId()] += 1;
936+
}
937+
$this->eventDispatcher->dispatchTyped(new InsertEvent('bookmark', $entityToInsert->getId()));
938+
return $entityToInsert;
933939
}
934940

935941
/**

lib/Service/HtmlImporter.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,16 @@ public function import(string $userId, string $content, ?int $rootFolderId = nul
125125
}
126126
$imported[] = ['type' => 'bookmark', 'id' => $bm->getId(), 'title' => $bookmark['title'], 'url' => $bookmark['href']];
127127
}
128-
} finally {
129128
$this->connection->commit();
129+
} catch (\Throwable $e) {
130+
// Roll back instead of committing a transaction that may contain a
131+
// failed statement – committing such a transaction throws and masks
132+
// the original error.
133+
if ($this->connection->inTransaction()) {
134+
$this->connection->rollBack();
135+
}
136+
$this->hashManager->setInvalidationEnabled(true);
137+
throw $e;
130138
}
131139

132140
$this->hashManager->setInvalidationEnabled(true);

src/components/LoadingModal.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default {
2424
copySelection: this.t('bookmkarks', 'Adding selection to folders'),
2525
emptyTrashbin: this.t('bookmkarks', 'Emptying trashbin'),
2626
deleted_folders: this.t('bookmkarks', 'Loading trashbin'),
27+
undeleteFolder: this.t('bookmarks', 'Restoring folder'),
2728
},
2829
showNcModal: false,
2930
showTimeout: null,

src/store/actions.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ export default {
461461
if (oldFolder) {
462462
const response2 = await axios.delete(
463463
url(state, `/folder/${oldFolder}/bookmarks/${bookmark}?hardDelete=true`),
464-
);
464+
)
465465
if (response2.data.status !== 'success') {
466466
throw new Error(response2.data)
467467
}
@@ -943,6 +943,7 @@ export default {
943943
try {
944944
const parentFolderId = this.getters.getFolder(id)[0].parent_folder
945945
const parentFolderItem = this.getters.getFolder(parentFolderId)[0]
946+
commit(mutations.FETCH_START, { type: 'undeleteFolder' })
946947
const response = await axios.post(url(state, `/folder/${id}/undelete`))
947948
const {
948949
data: { status },
@@ -958,10 +959,12 @@ export default {
958959
actions.LOAD_FOLDER_CHILDREN_ORDER,
959960
parentFolderItem ? parentFolderId : '-1',
960961
)
961-
await dispatch(actions.LOAD_FOLDERS)
962+
await dispatch(actions.LOAD_FOLDERS, /* force: */ true)
962963
await dispatch(actions.LOAD_DELETED_FOLDERS)
963964
}
965+
commit(mutations.FETCH_END, 'undeleteFolder')
964966
} catch (err) {
967+
commit(mutations.FETCH_END, 'undeleteFolder')
965968
console.error(err)
966969
commit(
967970
mutations.SET_ERROR,

tests/HtmlImportExportTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,33 @@ public function testImportFile(string $file): void {
112112
$this->assertEquals(1231231234, $firstBookmark->getAdded());
113113
}
114114

115+
/**
116+
* Re-importing the same file must not fail: every bookmark already exists,
117+
* so the importer takes the unique-constraint -> update path for each one.
118+
* This used to break the import transaction and raise a 500
119+
* ("cannot commit transaction - SQL statements in progress").
120+
*
121+
* @dataProvider importProvider
122+
* @param string $file
123+
* @throws DoesNotExistException
124+
* @throws MultipleObjectsReturnedException
125+
* @throws UnauthorizedAccessError
126+
* @throws AlreadyExistsError
127+
* @throws UserLimitExceededError
128+
* @throws HtmlParseError
129+
*/
130+
public function testReimportFile(string $file): void {
131+
$this->htmlImporter->importFile($this->userId, $file);
132+
// Second import hits the duplicate-URL update path for every bookmark
133+
$result = $this->htmlImporter->importFile($this->userId, $file);
134+
135+
$this->assertEmpty($result['errors']);
136+
137+
$firstBookmark = $this->bookmarkMapper->find($result['imported'][0]['children'][0]['id']);
138+
$this->assertSame('Title 0', $firstBookmark->getTitle());
139+
$this->assertSame('http://url0.net/', $firstBookmark->getUrl());
140+
}
141+
115142
/**
116143
* @dataProvider exportProvider
117144
* @param array $bookmarks

0 commit comments

Comments
 (0)