use BookStack\Facades\Activity;
use BookStack\Http\Controller;
use BookStack\References\ReferenceFetcher;
+use BookStack\Util\DatabaseTransaction;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
$this->checkPermission('bookshelf-create-all');
$this->checkPermission('book-create-all');
- $shelf = $transformer->transformBookToShelf($book);
+ $shelf = (new DatabaseTransaction(function () use ($book, $transformer) {
+ return $transformer->transformBookToShelf($book);
+ }))->run();
return redirect($shelf->getUrl());
}
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\Controller;
use BookStack\References\ReferenceFetcher;
+use BookStack\Util\DatabaseTransaction;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->checkPermission('book-create-all');
- $book = $transformer->transformChapterToBook($chapter);
+ $book = (new DatabaseTransaction(function () use ($chapter, $transformer) {
+ return $transformer->transformChapterToBook($chapter);
+ }))->run();
return redirect($book->getUrl());
}
use BookStack\Facades\Activity;
use BookStack\Sorting\SortRule;
use BookStack\Uploads\ImageRepo;
+use BookStack\Util\DatabaseTransaction;
use Exception;
use Illuminate\Http\UploadedFile;
*/
public function create(array $input): Book
{
- $book = new Book();
- $this->baseRepo->create($book, $input);
- $this->baseRepo->updateCoverImage($book, $input['image'] ?? null);
- $this->baseRepo->updateDefaultTemplate($book, intval($input['default_template_id'] ?? null));
- Activity::add(ActivityType::BOOK_CREATE, $book);
+ return (new DatabaseTransaction(function () use ($input) {
+ $book = new Book();
- $defaultBookSortSetting = intval(setting('sorting-book-default', '0'));
- if ($defaultBookSortSetting && SortRule::query()->find($defaultBookSortSetting)) {
- $book->sort_rule_id = $defaultBookSortSetting;
- $book->save();
- }
+ $this->baseRepo->create($book, $input);
+ $this->baseRepo->updateCoverImage($book, $input['image'] ?? null);
+ $this->baseRepo->updateDefaultTemplate($book, intval($input['default_template_id'] ?? null));
+ Activity::add(ActivityType::BOOK_CREATE, $book);
- return $book;
+ $defaultBookSortSetting = intval(setting('sorting-book-default', '0'));
+ if ($defaultBookSortSetting && SortRule::query()->find($defaultBookSortSetting)) {
+ $book->sort_rule_id = $defaultBookSortSetting;
+ $book->save();
+ }
+
+ return $book;
+ }))->run();
}
/**
use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Facades\Activity;
+use BookStack\Util\DatabaseTransaction;
use Exception;
class BookshelfRepo
*/
public function create(array $input, array $bookIds): Bookshelf
{
- $shelf = new Bookshelf();
- $this->baseRepo->create($shelf, $input);
- $this->baseRepo->updateCoverImage($shelf, $input['image'] ?? null);
- $this->updateBooks($shelf, $bookIds);
- Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);
-
- return $shelf;
+ return (new DatabaseTransaction(function () use ($input, $bookIds) {
+ $shelf = new Bookshelf();
+ $this->baseRepo->create($shelf, $input);
+ $this->baseRepo->updateCoverImage($shelf, $input['image'] ?? null);
+ $this->updateBooks($shelf, $bookIds);
+ Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);
+ }))->run();
}
/**
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Facades\Activity;
+use BookStack\Util\DatabaseTransaction;
use Exception;
class ChapterRepo
*/
public function create(array $input, Book $parentBook): Chapter
{
- $chapter = new Chapter();
- $chapter->book_id = $parentBook->id;
- $chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
- $this->baseRepo->create($chapter, $input);
- $this->baseRepo->updateDefaultTemplate($chapter, intval($input['default_template_id'] ?? null));
- Activity::add(ActivityType::CHAPTER_CREATE, $chapter);
-
- $this->baseRepo->sortParent($chapter);
-
- return $chapter;
+ return (new DatabaseTransaction(function () use ($input, $parentBook) {
+ $chapter = new Chapter();
+ $chapter->book_id = $parentBook->id;
+ $chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
+ $this->baseRepo->create($chapter, $input);
+ $this->baseRepo->updateDefaultTemplate($chapter, intval($input['default_template_id'] ?? null));
+ Activity::add(ActivityType::CHAPTER_CREATE, $chapter);
+
+ $this->baseRepo->sortParent($chapter);
+ }))->run();
}
/**
throw new PermissionsException('User does not have permission to create a chapter within the chosen book');
}
- $chapter->changeBook($parent->id);
- $chapter->rebuildPermissions();
- Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
+ return (new DatabaseTransaction(function () use ($chapter, $parent) {
+ $chapter->changeBook($parent->id);
+ $chapter->rebuildPermissions();
+ Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
- $this->baseRepo->sortParent($chapter);
+ $this->baseRepo->sortParent($chapter);
- return $parent;
+ return $parent;
+ }))->run();
}
}
use BookStack\Facades\Activity;
use BookStack\References\ReferenceStore;
use BookStack\References\ReferenceUpdater;
+use BookStack\Util\DatabaseTransaction;
use Exception;
class PageRepo
]);
}
- $page->save();
- $page->refresh()->rebuildPermissions();
+ (new DatabaseTransaction(function () use ($page) {
+ $page->save();
+ $page->refresh()->rebuildPermissions();
+ }))->run();
return $page;
}
*/
public function publishDraft(Page $draft, array $input): Page
{
- $draft->draft = false;
- $draft->revision_count = 1;
- $draft->priority = $this->getNewPriority($draft);
- $this->updateTemplateStatusAndContentFromInput($draft, $input);
- $this->baseRepo->update($draft, $input);
- $draft->rebuildPermissions();
-
- $summary = trim($input['summary'] ?? '') ?: trans('entities.pages_initial_revision');
- $this->revisionRepo->storeNewForPage($draft, $summary);
- $draft->refresh();
-
- Activity::add(ActivityType::PAGE_CREATE, $draft);
- $this->baseRepo->sortParent($draft);
-
- return $draft;
+ return (new DatabaseTransaction(function () use ($draft, $input) {
+ $draft->draft = false;
+ $draft->revision_count = 1;
+ $draft->priority = $this->getNewPriority($draft);
+ $this->updateTemplateStatusAndContentFromInput($draft, $input);
+ $this->baseRepo->update($draft, $input);
+ $draft->rebuildPermissions();
+
+ $summary = trim($input['summary'] ?? '') ?: trans('entities.pages_initial_revision');
+ $this->revisionRepo->storeNewForPage($draft, $summary);
+ $draft->refresh();
+
+ Activity::add(ActivityType::PAGE_CREATE, $draft);
+ $this->baseRepo->sortParent($draft);
+
+ return $draft;
+ }))->run();
}
/**
$page->revision_count++;
$page->save();
- // Remove all update drafts for this user & page.
+ // Remove all update drafts for this user and page.
$this->revisionRepo->deleteDraftsForCurrentUser($page);
// Save a revision after updating
throw new PermissionsException('User does not have permission to create a page within the new parent');
}
- $page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null;
- $newBookId = ($parent instanceof Chapter) ? $parent->book->id : $parent->id;
- $page->changeBook($newBookId);
- $page->rebuildPermissions();
+ return (new DatabaseTransaction(function () use ($page, $parent) {
+ $page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null;
+ $newBookId = ($parent instanceof Chapter) ? $parent->book->id : $parent->id;
+ $page->changeBook($newBookId);
+ $page->rebuildPermissions();
- Activity::add(ActivityType::PAGE_MOVE, $page);
+ Activity::add(ActivityType::PAGE_MOVE, $page);
- $this->baseRepo->sortParent($page);
+ $this->baseRepo->sortParent($page);
- return $parent;
+ return $parent;
+ }))->run();
}
/**
class HierarchyTransformer
{
- protected BookRepo $bookRepo;
- protected BookshelfRepo $shelfRepo;
- protected Cloner $cloner;
- protected TrashCan $trashCan;
-
- public function __construct(BookRepo $bookRepo, BookshelfRepo $shelfRepo, Cloner $cloner, TrashCan $trashCan)
- {
- $this->bookRepo = $bookRepo;
- $this->shelfRepo = $shelfRepo;
- $this->cloner = $cloner;
- $this->trashCan = $trashCan;
+ public function __construct(
+ protected BookRepo $bookRepo,
+ protected BookshelfRepo $shelfRepo,
+ protected Cloner $cloner,
+ protected TrashCan $trashCan
+ ) {
}
/**
use BookStack\Facades\Activity;
use BookStack\Uploads\AttachmentService;
use BookStack\Uploads\ImageService;
+use BookStack\Util\DatabaseTransaction;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
/**
* Destroy the given entity.
+ * Returns the number of total entities destroyed in the operation.
*
* @throws Exception
*/
public function destroyEntity(Entity $entity): int
{
- if ($entity instanceof Page) {
- return $this->destroyPage($entity);
- }
- if ($entity instanceof Chapter) {
- return $this->destroyChapter($entity);
- }
- if ($entity instanceof Book) {
- return $this->destroyBook($entity);
- }
- if ($entity instanceof Bookshelf) {
- return $this->destroyShelf($entity);
- }
+ $result = (new DatabaseTransaction(function () use ($entity) {
+ if ($entity instanceof Page) {
+ return $this->destroyPage($entity);
+ } else if ($entity instanceof Chapter) {
+ return $this->destroyChapter($entity);
+ } else if ($entity instanceof Book) {
+ return $this->destroyBook($entity);
+ } else if ($entity instanceof Bookshelf) {
+ return $this->destroyShelf($entity);
+ }
+ return null;
+ }))->run();
- return 0;
+ return $result ?? 0;
}
/**
/**
* Re-generate all entity permission from scratch.
*/
- public function rebuildForAll()
+ public function rebuildForAll(): void
{
JointPermission::query()->truncate();
/**
* Rebuild the entity jointPermissions for a particular entity.
*/
- public function rebuildForEntity(Entity $entity)
+ public function rebuildForEntity(Entity $entity): void
{
$entities = [$entity];
if ($entity instanceof Book) {
/**
* Build joint permissions for the given book and role combinations.
*/
- protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
+ protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false): void
{
$entities = clone $books;
/**
* Rebuild the entity jointPermissions for a collection of entities.
*/
- protected function buildJointPermissionsForEntities(array $entities)
+ protected function buildJointPermissionsForEntities(array $entities): void
{
$roles = Role::query()->get()->values()->all();
$this->deleteManyJointPermissionsForEntities($entities);
*
* @param Entity[] $entities
*/
- protected function deleteManyJointPermissionsForEntities(array $entities)
+ protected function deleteManyJointPermissionsForEntities(array $entities): void
{
$simpleEntities = $this->entitiesToSimpleEntities($entities);
$idsByType = $this->entitiesToTypeIdMap($simpleEntities);
- DB::transaction(function () use ($idsByType) {
- foreach ($idsByType as $type => $ids) {
- foreach (array_chunk($ids, 1000) as $idChunk) {
- DB::table('joint_permissions')
- ->where('entity_type', '=', $type)
- ->whereIn('entity_id', $idChunk)
- ->delete();
- }
+ foreach ($idsByType as $type => $ids) {
+ foreach (array_chunk($ids, 1000) as $idChunk) {
+ DB::table('joint_permissions')
+ ->where('entity_type', '=', $type)
+ ->whereIn('entity_id', $idChunk)
+ ->delete();
}
- });
+ }
}
/**
* @param Entity[] $originalEntities
* @param Role[] $roles
*/
- protected function createManyJointPermissions(array $originalEntities, array $roles)
+ protected function createManyJointPermissions(array $originalEntities, array $roles): void
{
$entities = $this->entitiesToSimpleEntities($originalEntities);
$jointPermissions = [];
}
}
- DB::transaction(function () use ($jointPermissions) {
- foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
- DB::table('joint_permissions')->insert($jointPermissionChunk);
- }
- });
+ foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
+ DB::table('joint_permissions')->insert($jointPermissionChunk);
+ }
}
/**
use BookStack\Http\Controller;
use BookStack\Permissions\Models\EntityPermission;
use BookStack\Users\Models\Role;
+use BookStack\Util\DatabaseTransaction;
use Illuminate\Http\Request;
class PermissionsController extends Controller
$page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
- $this->permissionsUpdater->updateFromPermissionsForm($page, $request);
+ (new DatabaseTransaction(function () use ($page, $request) {
+ $this->permissionsUpdater->updateFromPermissionsForm($page, $request);
+ }))->run();
$this->showSuccessNotification(trans('entities.pages_permissions_success'));
$chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
- $this->permissionsUpdater->updateFromPermissionsForm($chapter, $request);
+ (new DatabaseTransaction(function () use ($chapter, $request) {
+ $this->permissionsUpdater->updateFromPermissionsForm($chapter, $request);
+ }))->run();
$this->showSuccessNotification(trans('entities.chapters_permissions_success'));
$book = $this->queries->books->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $book);
- $this->permissionsUpdater->updateFromPermissionsForm($book, $request);
+ (new DatabaseTransaction(function () use ($book, $request) {
+ $this->permissionsUpdater->updateFromPermissionsForm($book, $request);
+ }))->run();
$this->showSuccessNotification(trans('entities.books_permissions_updated'));
$shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
- $this->permissionsUpdater->updateFromPermissionsForm($shelf, $request);
+ (new DatabaseTransaction(function () use ($shelf, $request) {
+ $this->permissionsUpdater->updateFromPermissionsForm($shelf, $request);
+ }))->run();
$this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
$shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
- $updateCount = $this->permissionsUpdater->updateBookPermissionsFromShelf($shelf);
+ $updateCount = (new DatabaseTransaction(function () use ($shelf) {
+ return $this->permissionsUpdater->updateBookPermissionsFromShelf($shelf);
+ }))->run();
+
$this->showSuccessNotification(trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
return redirect($shelf->getUrl());
use BookStack\Facades\Activity;
use BookStack\Permissions\Models\RolePermission;
use BookStack\Users\Models\Role;
+use BookStack\Util\DatabaseTransaction;
use Exception;
use Illuminate\Database\Eloquent\Collection;
*/
public function saveNewRole(array $roleData): Role
{
- $role = new Role($roleData);
- $role->mfa_enforced = boolval($roleData['mfa_enforced'] ?? false);
- $role->save();
+ return (new DatabaseTransaction(function () use ($roleData) {
+ $role = new Role($roleData);
+ $role->mfa_enforced = boolval($roleData['mfa_enforced'] ?? false);
+ $role->save();
- $permissions = $roleData['permissions'] ?? [];
- $this->assignRolePermissions($role, $permissions);
- $this->permissionBuilder->rebuildForRole($role);
+ $permissions = $roleData['permissions'] ?? [];
+ $this->assignRolePermissions($role, $permissions);
+ $this->permissionBuilder->rebuildForRole($role);
- Activity::add(ActivityType::ROLE_CREATE, $role);
+ Activity::add(ActivityType::ROLE_CREATE, $role);
- return $role;
+ return $role;
+ }))->run();
}
/**
* Updates an existing role.
- * Ensures Admin system role always have core permissions.
+ * Ensures the Admin system role always has core permissions.
*/
public function updateRole($roleId, array $roleData): Role
{
$role = $this->getRoleById($roleId);
- if (isset($roleData['permissions'])) {
- $this->assignRolePermissions($role, $roleData['permissions']);
- }
+ return (new DatabaseTransaction(function () use ($role, $roleData) {
+ if (isset($roleData['permissions'])) {
+ $this->assignRolePermissions($role, $roleData['permissions']);
+ }
- $role->fill($roleData);
- $role->save();
- $this->permissionBuilder->rebuildForRole($role);
+ $role->fill($roleData);
+ $role->save();
+ $this->permissionBuilder->rebuildForRole($role);
- Activity::add(ActivityType::ROLE_UPDATE, $role);
+ Activity::add(ActivityType::ROLE_UPDATE, $role);
- return $role;
+ return $role;
+ }))->run();
}
/**
/**
* Delete a role from the system.
* Check it's not an admin role or set as default before deleting.
- * If a migration Role ID is specified the users assign to the current role
+ * If a migration Role ID is specified, the users assigned to the current role
* will be added to the role of the specified id.
*
* @throws PermissionsException
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
}
- if ($migrateRoleId !== 0) {
- $newRole = Role::query()->find($migrateRoleId);
- if ($newRole) {
- $users = $role->users()->pluck('id')->toArray();
- $newRole->users()->sync($users);
+ (new DatabaseTransaction(function () use ($migrateRoleId, $role) {
+ if ($migrateRoleId !== 0) {
+ $newRole = Role::query()->find($migrateRoleId);
+ if ($newRole) {
+ $users = $role->users()->pluck('id')->toArray();
+ $newRole->users()->sync($users);
+ }
}
- }
- $role->entityPermissions()->delete();
- $role->jointPermissions()->delete();
- Activity::add(ActivityType::ROLE_DELETE, $role);
- $role->delete();
+ $role->entityPermissions()->delete();
+ $role->jointPermissions()->delete();
+ Activity::add(ActivityType::ROLE_DELETE, $role);
+ $role->delete();
+ }))->run();
}
}
use BookStack\Entities\Tools\BookContents;
use BookStack\Facades\Activity;
use BookStack\Http\Controller;
+use BookStack\Util\DatabaseTransaction;
use Illuminate\Http\Request;
class BookSortController extends Controller
// Sort via map
if ($request->filled('sort-tree')) {
- $sortMap = BookSortMap::fromJson($request->get('sort-tree'));
- $booksInvolved = $sorter->sortUsingMap($sortMap);
+ (new DatabaseTransaction(function () use ($book, $request, $sorter, &$loggedActivityForBook) {
+ $sortMap = BookSortMap::fromJson($request->get('sort-tree'));
+ $booksInvolved = $sorter->sortUsingMap($sortMap);
- // Rebuild permissions and add activity for involved books.
- foreach ($booksInvolved as $bookInvolved) {
- Activity::add(ActivityType::BOOK_SORT, $bookInvolved);
- if ($bookInvolved->id === $book->id) {
- $loggedActivityForBook = true;
+ // Add activity for involved books.
+ foreach ($booksInvolved as $bookInvolved) {
+ Activity::add(ActivityType::BOOK_SORT, $bookInvolved);
+ if ($bookInvolved->id === $book->id) {
+ $loggedActivityForBook = true;
+ }
}
- }
+ }))->run();
}
if ($request->filled('auto-sort')) {
--- /dev/null
+<?php
+
+namespace BookStack\Util;
+
+use Closure;
+use Illuminate\Support\Facades\DB;
+use Throwable;
+
+/**
+ * Run the given code within a database transactions.
+ * Wraps Laravel's own transaction method, but sets a specific runtime isolation method.
+ * This sets a session level since this won't cause issues if already within a transaction,
+ * and this should apply to the next transactions anyway.
+ *
+ * "READ COMMITTED" ensures that changes from other transactions can be read within
+ * a transaction, even if started afterward (and for example, it was blocked by the initial
+ * transaction). This is quite important for things like permission generation, where we would
+ * want to consider the changes made by other committed transactions by the time we come to
+ * regenerate permission access.
+ *
+ * @throws Throwable
+ * @template TReturn of mixed
+ */
+class DatabaseTransaction
+{
+ /**
+ * @param (Closure(static): TReturn) $callback
+ */
+ public function __construct(
+ protected Closure $callback
+ ) {
+ }
+
+ /**
+ * @return TReturn
+ */
+ public function run(): mixed
+ {
+ DB::statement('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED');
+ return DB::transaction($this->callback);
+ }
+}