mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-07-28 17:02:04 +03:00
Added per-item recycle-bin delete and restore
This commit is contained in:
@ -287,6 +287,22 @@ class Entity extends Ownable
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent entity if existing.
|
||||
* This is the "static" parent and does not include dynamic
|
||||
* relations such as shelves to books.
|
||||
*/
|
||||
public function getParent(): ?Entity
|
||||
{
|
||||
if ($this->isA('page')) {
|
||||
return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book->withTrashed()->first();
|
||||
}
|
||||
if ($this->isA('chapter')) {
|
||||
return $this->book->withTrashed()->first();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the permissions for this entity.
|
||||
*/
|
||||
|
@ -180,24 +180,91 @@ class TrashCan
|
||||
|
||||
/**
|
||||
* Destroy all items that have pending deletions.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroyFromAllDeletions(): int
|
||||
{
|
||||
$deletions = Deletion::all();
|
||||
$deleteCount = 0;
|
||||
foreach ($deletions as $deletion) {
|
||||
// For each one we load in the relation since it may have already
|
||||
// been deleted as part of another deletion in this loop.
|
||||
$entity = $deletion->deletable()->first();
|
||||
if ($entity) {
|
||||
$count = $this->destroyEntity($deletion->deletable);
|
||||
$deleteCount += $count;
|
||||
}
|
||||
$deletion->delete();
|
||||
$deleteCount += $this->destroyFromDeletion($deletion);
|
||||
}
|
||||
return $deleteCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an element from the given deletion model.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroyFromDeletion(Deletion $deletion): int
|
||||
{
|
||||
// We directly load the deletable element here just to ensure it still
|
||||
// exists in the event it has already been destroyed during this request.
|
||||
$entity = $deletion->deletable()->first();
|
||||
$count = 0;
|
||||
if ($entity) {
|
||||
$count = $this->destroyEntity($deletion->deletable);
|
||||
}
|
||||
$deletion->delete();
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the content within the given deletion.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function restoreFromDeletion(Deletion $deletion): int
|
||||
{
|
||||
$shouldRestore = true;
|
||||
$restoreCount = 0;
|
||||
$parent = $deletion->deletable->getParent();
|
||||
|
||||
if ($parent && $parent->trashed()) {
|
||||
$shouldRestore = false;
|
||||
}
|
||||
|
||||
if ($shouldRestore) {
|
||||
$restoreCount = $this->restoreEntity($deletion->deletable);
|
||||
}
|
||||
|
||||
$deletion->delete();
|
||||
return $restoreCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore an entity so it is essentially un-deleted.
|
||||
* Deletions on restored child elements will be removed during this restoration.
|
||||
*/
|
||||
protected function restoreEntity(Entity $entity): int
|
||||
{
|
||||
$count = 1;
|
||||
$entity->restore();
|
||||
|
||||
if ($entity->isA('chapter') || $entity->isA('book')) {
|
||||
foreach ($entity->pages()->withTrashed()->withCount('deletions')->get() as $page) {
|
||||
if ($page->deletions_count > 0) {
|
||||
$page->deletions()->delete();
|
||||
}
|
||||
|
||||
$page->restore();
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($entity->isA('book')) {
|
||||
foreach ($entity->chapters()->withTrashed()->withCount('deletions')->get() as $chapter) {
|
||||
if ($chapter->deletions_count === 0) {
|
||||
$chapter->deletions()->delete();
|
||||
}
|
||||
|
||||
$chapter->restore();
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the given entity.
|
||||
*/
|
||||
|
@ -49,14 +49,6 @@ class Page extends BookChild
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent item
|
||||
*/
|
||||
public function parent(): Entity
|
||||
{
|
||||
return $this->chapter_id ? $this->chapter : $this->book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the chapter that this page is in, If applicable.
|
||||
* @return BelongsTo
|
||||
|
@ -321,7 +321,7 @@ class PageRepo
|
||||
*/
|
||||
public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
|
||||
{
|
||||
$parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->parent();
|
||||
$parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->getParent();
|
||||
if ($parent === null) {
|
||||
throw new MoveOperationException('Book or chapter to move page into not found');
|
||||
}
|
||||
@ -440,8 +440,9 @@ class PageRepo
|
||||
*/
|
||||
protected function getNewPriority(Page $page): int
|
||||
{
|
||||
if ($page->parent() instanceof Chapter) {
|
||||
$lastPage = $page->parent()->pages('desc')->first();
|
||||
$parent = $page->getParent();
|
||||
if ($parent instanceof Chapter) {
|
||||
$lastPage = $parent->pages('desc')->first();
|
||||
return $lastPage ? $lastPage->priority + 1 : 0;
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ class PageController extends Controller
|
||||
public function editDraft(string $bookSlug, int $pageId)
|
||||
{
|
||||
$draft = $this->pageRepo->getById($pageId);
|
||||
$this->checkOwnablePermission('page-create', $draft->parent());
|
||||
$this->checkOwnablePermission('page-create', $draft->getParent());
|
||||
$this->setPageTitle(trans('entities.pages_edit_draft'));
|
||||
|
||||
$draftsEnabled = $this->isSignedIn();
|
||||
@ -104,7 +104,7 @@ class PageController extends Controller
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
$draftPage = $this->pageRepo->getById($pageId);
|
||||
$this->checkOwnablePermission('page-create', $draftPage->parent());
|
||||
$this->checkOwnablePermission('page-create', $draftPage->getParent());
|
||||
|
||||
$page = $this->pageRepo->publishDraft($draftPage, $request->all());
|
||||
Activity::add($page, 'page_create', $draftPage->book->id);
|
||||
|
@ -2,36 +2,103 @@
|
||||
|
||||
use BookStack\Entities\Deletion;
|
||||
use BookStack\Entities\Managers\TrashCan;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class RecycleBinController extends Controller
|
||||
{
|
||||
|
||||
protected $recycleBinBaseUrl = '/settings/recycle-bin';
|
||||
|
||||
/**
|
||||
* On each request to a method of this controller check permissions
|
||||
* using a middleware closure.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// TODO - Check this is enforced.
|
||||
$this->middleware(function ($request, $next) {
|
||||
$this->checkPermission('settings-manage');
|
||||
$this->checkPermission('restrictions-manage-all');
|
||||
return $next($request);
|
||||
});
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the top-level listing for the recycle bin.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->checkPermission('settings-manage');
|
||||
$this->checkPermission('restrictions-manage-all');
|
||||
|
||||
$deletions = Deletion::query()->with(['deletable', 'deleter'])->paginate(10);
|
||||
|
||||
return view('settings.recycle-bin', [
|
||||
return view('settings.recycle-bin.index', [
|
||||
'deletions' => $deletions,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the page to confirm a restore of the deletion of the given id.
|
||||
*/
|
||||
public function showRestore(string $id)
|
||||
{
|
||||
/** @var Deletion $deletion */
|
||||
$deletion = Deletion::query()->findOrFail($id);
|
||||
|
||||
return view('settings.recycle-bin.restore', [
|
||||
'deletion' => $deletion,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the element attached to the given deletion.
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function restore(string $id)
|
||||
{
|
||||
/** @var Deletion $deletion */
|
||||
$deletion = Deletion::query()->findOrFail($id);
|
||||
$restoreCount = (new TrashCan())->restoreFromDeletion($deletion);
|
||||
|
||||
$this->showSuccessNotification(trans('settings.recycle_bin_restore_notification', ['count' => $restoreCount]));
|
||||
return redirect($this->recycleBinBaseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the page to confirm a Permanent deletion of the element attached to the deletion of the given id.
|
||||
*/
|
||||
public function showDestroy(string $id)
|
||||
{
|
||||
/** @var Deletion $deletion */
|
||||
$deletion = Deletion::query()->findOrFail($id);
|
||||
|
||||
return view('settings.recycle-bin.destroy', [
|
||||
'deletion' => $deletion,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete the content associated with the given deletion.
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function destroy(string $id)
|
||||
{
|
||||
/** @var Deletion $deletion */
|
||||
$deletion = Deletion::query()->findOrFail($id);
|
||||
$deleteCount = (new TrashCan())->destroyFromDeletion($deletion);
|
||||
|
||||
$this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
|
||||
return redirect($this->recycleBinBaseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty out the recycle bin.
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function empty()
|
||||
{
|
||||
$this->checkPermission('settings-manage');
|
||||
$this->checkPermission('restrictions-manage-all');
|
||||
|
||||
$deleteCount = (new TrashCan())->destroyFromAllDeletions();
|
||||
|
||||
$this->showSuccessNotification(trans('settings.recycle_bin_empty_notification', ['count' => $deleteCount]));
|
||||
return redirect('/settings/recycle-bin');
|
||||
$this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
|
||||
return redirect($this->recycleBinBaseUrl);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user