mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Added recycle bin auto-clear lifetime functionality
This commit is contained in:
parent
68b1d87ebe
commit
ec3aeb3315
@ -255,6 +255,14 @@ APP_VIEWS_BOOKSHELVES=grid
|
||||
# If set to 'false' a limit will not be enforced.
|
||||
REVISION_LIMIT=50
|
||||
|
||||
# Recycle Bin Lifetime
|
||||
# The number of days that content will remain in the recycle bin before
|
||||
# being considered for auto-removal. It is not a guarantee that content will
|
||||
# be removed after this time.
|
||||
# Set to 0 for no recycle bin functionality.
|
||||
# Set to -1 for unlimited recycle bin lifetime.
|
||||
RECYCLE_BIN_LIFETIME=30
|
||||
|
||||
# Allow <script> tags in page content
|
||||
# Note, if set to 'true' the page editor may still escape scripts.
|
||||
ALLOW_CONTENT_SCRIPTS=false
|
||||
|
@ -31,6 +31,13 @@ return [
|
||||
// If set to false then a limit will not be enforced.
|
||||
'revision_limit' => env('REVISION_LIMIT', 50),
|
||||
|
||||
// The number of days that content will remain in the recycle bin before
|
||||
// being considered for auto-removal. It is not a guarantee that content will
|
||||
// be removed after this time.
|
||||
// Set to 0 for no recycle bin functionality.
|
||||
// Set to -1 for unlimited recycle bin lifetime.
|
||||
'recycle_bin_lifetime' => env('RECYCLE_BIN_LIFETIME', 30),
|
||||
|
||||
// Allow <script> tags to entered within page content.
|
||||
// <script> tags are escaped by default.
|
||||
// Even when overridden the WYSIWYG editor may still escape script content.
|
||||
|
@ -13,6 +13,7 @@ use BookStack\Facades\Activity;
|
||||
use BookStack\Uploads\AttachmentService;
|
||||
use BookStack\Uploads\ImageService;
|
||||
use Exception;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class TrashCan
|
||||
{
|
||||
@ -231,6 +232,30 @@ class TrashCan
|
||||
return $restoreCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically clear old content from the recycle bin
|
||||
* depending on the configured lifetime.
|
||||
* Returns the total number of deleted elements.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function autoClearOld(): int
|
||||
{
|
||||
$lifetime = intval(config('app.recycle_bin_lifetime'));
|
||||
if ($lifetime < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$clearBeforeDate = Carbon::now()->addSeconds(10)->subDays($lifetime);
|
||||
$deleteCount = 0;
|
||||
|
||||
$deletionsToRemove = Deletion::query()->where('created_at', '<', $clearBeforeDate)->get();
|
||||
foreach ($deletionsToRemove as $deletion) {
|
||||
$deleteCount += $this->destroyFromDeletion($deletion);
|
||||
}
|
||||
|
||||
return $deleteCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore an entity so it is essentially un-deleted.
|
||||
* Deletions on restored child elements will be removed during this restoration.
|
||||
|
@ -129,5 +129,6 @@ class BookRepo
|
||||
{
|
||||
$trashCan = new TrashCan();
|
||||
$trashCan->softDestroyBook($book);
|
||||
$trashCan->autoClearOld();
|
||||
}
|
||||
}
|
||||
|
@ -175,5 +175,6 @@ class BookshelfRepo
|
||||
{
|
||||
$trashCan = new TrashCan();
|
||||
$trashCan->softDestroyShelf($shelf);
|
||||
$trashCan->autoClearOld();
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,7 @@ class ChapterRepo
|
||||
{
|
||||
$trashCan = new TrashCan();
|
||||
$trashCan->softDestroyChapter($chapter);
|
||||
$trashCan->autoClearOld();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -266,6 +266,7 @@ class PageRepo
|
||||
{
|
||||
$trashCan = new TrashCan();
|
||||
$trashCan->softDestroyPage($page);
|
||||
$trashCan->autoClearOld();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -80,7 +80,7 @@ return [
|
||||
'maint_send_test_email_mail_subject' => 'Test Email',
|
||||
'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',
|
||||
'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',
|
||||
'maint_recycle_bin_desc' => 'Items deleted remain in the recycle bin until it is emptied. Open the recycle bin to restore or permanently remove items.',
|
||||
'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',
|
||||
'maint_recycle_bin_open' => 'Open Recycle Bin',
|
||||
|
||||
// Recycle Bin
|
||||
|
@ -5,6 +5,24 @@
|
||||
|
||||
@include('settings.navbar-with-version', ['selected' => 'maintenance'])
|
||||
|
||||
<div class="card content-wrap auto-height pb-xl">
|
||||
<h2 class="list-heading">{{ trans('settings.recycle_bin') }}</h2>
|
||||
<div class="grid half gap-xl">
|
||||
<div>
|
||||
<p class="small text-muted">{{ trans('settings.maint_recycle_bin_desc') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid half no-gap mb-m">
|
||||
<p class="mb-xs text-bookshelf">@icon('bookshelf'){{ trans('entities.shelves') }}: {{ $recycleStats['bookshelf'] }}</p>
|
||||
<p class="mb-xs text-book">@icon('book'){{ trans('entities.books') }}: {{ $recycleStats['book'] }}</p>
|
||||
<p class="mb-xs text-chapter">@icon('chapter'){{ trans('entities.chapters') }}: {{ $recycleStats['chapter'] }}</p>
|
||||
<p class="mb-xs text-page">@icon('page'){{ trans('entities.pages') }}: {{ $recycleStats['page'] }}</p>
|
||||
</div>
|
||||
<a href="{{ url('/settings/recycle-bin') }}" class="button outline">{{ trans('settings.maint_recycle_bin_open') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="image-cleanup" class="card content-wrap auto-height">
|
||||
<h2 class="list-heading">{{ trans('settings.maint_image_cleanup') }}</h2>
|
||||
<div class="grid half gap-xl">
|
||||
@ -15,7 +33,7 @@
|
||||
<form method="POST" action="{{ url('/settings/maintenance/cleanup-images') }}">
|
||||
{!! csrf_field() !!}
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<div>
|
||||
<div class="mb-s">
|
||||
@if(session()->has('cleanup-images-warning'))
|
||||
<p class="text-neg">
|
||||
{{ session()->get('cleanup-images-warning') }}
|
||||
@ -50,23 +68,5 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card content-wrap auto-height pb-xl">
|
||||
<h2 class="list-heading">{{ trans('settings.recycle_bin') }}</h2>
|
||||
<div class="grid half gap-xl">
|
||||
<div>
|
||||
<p class="small text-muted">{{ trans('settings.maint_recycle_bin_desc') }}</p>
|
||||
<div class="grid half no-gap">
|
||||
<p class="mb-xs text-bookshelf">@icon('bookshelf'){{ trans('entities.shelves') }}: {{ $recycleStats['bookshelf'] }}</p>
|
||||
<p class="mb-xs text-book">@icon('book'){{ trans('entities.books') }}: {{ $recycleStats['book'] }}</p>
|
||||
<p class="mb-xs text-chapter">@icon('chapter'){{ trans('entities.chapters') }}: {{ $recycleStats['chapter'] }}</p>
|
||||
<p class="mb-xs text-page">@icon('page'){{ trans('entities.pages') }}: {{ $recycleStats['page'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ url('/settings/recycle-bin') }}" class="button outline">{{ trans('settings.maint_recycle_bin_open') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@stop
|
||||
|
@ -3,6 +3,8 @@
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Deletion;
|
||||
use BookStack\Entities\Page;
|
||||
use DB;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class RecycleBinTest extends TestCase
|
||||
{
|
||||
@ -39,11 +41,11 @@ class RecycleBinTest extends TestCase
|
||||
$this->giveUserPermissions($editor, ['settings-manage']);
|
||||
|
||||
foreach($routes as $route) {
|
||||
\DB::beginTransaction();
|
||||
DB::beginTransaction();
|
||||
[$method, $url] = explode(':', $route);
|
||||
$resp = $this->call($method, $url);
|
||||
$this->assertNotPermissionError($resp);
|
||||
\DB::rollBack();
|
||||
DB::rollBack();
|
||||
}
|
||||
|
||||
}
|
||||
@ -93,15 +95,15 @@ class RecycleBinTest extends TestCase
|
||||
$this->asEditor()->delete($book->getUrl());
|
||||
$deletion = Deletion::query()->firstOrFail();
|
||||
|
||||
$this->assertEquals($book->pages->count(), \DB::table('pages')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
|
||||
$this->assertEquals($book->chapters->count(), \DB::table('chapters')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
|
||||
$this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
|
||||
$this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
|
||||
|
||||
$restoreReq = $this->asAdmin()->post("/settings/recycle-bin/{$deletion->id}/restore");
|
||||
$restoreReq->assertRedirect('/settings/recycle-bin');
|
||||
$this->assertTrue(Deletion::query()->count() === 0);
|
||||
|
||||
$this->assertEquals($book->pages->count(), \DB::table('pages')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
|
||||
$this->assertEquals($book->chapters->count(), \DB::table('chapters')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
|
||||
$this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
|
||||
$this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
|
||||
|
||||
$itemCount = 1 + $book->pages->count() + $book->chapters->count();
|
||||
$redirectReq = $this->get('/settings/recycle-bin');
|
||||
@ -154,4 +156,47 @@ class RecycleBinTest extends TestCase
|
||||
'extra' => $page->name,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_auto_clear_functionality_works()
|
||||
{
|
||||
config()->set('app.recycle_bin_lifetime', 5);
|
||||
$page = Page::query()->firstOrFail();
|
||||
$otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail();
|
||||
|
||||
$this->asEditor()->delete($page->getUrl());
|
||||
$this->assertDatabaseHas('pages', ['id' => $page->id]);
|
||||
$this->assertEquals(1, Deletion::query()->count());
|
||||
|
||||
Carbon::setTestNow(Carbon::now()->addDays(6));
|
||||
$this->asEditor()->delete($otherPage->getUrl());
|
||||
$this->assertEquals(1, Deletion::query()->count());
|
||||
|
||||
$this->assertDatabaseMissing('pages', ['id' => $page->id]);
|
||||
}
|
||||
|
||||
public function test_auto_clear_functionality_with_negative_time_keeps_forever()
|
||||
{
|
||||
config()->set('app.recycle_bin_lifetime', -1);
|
||||
$page = Page::query()->firstOrFail();
|
||||
$otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail();
|
||||
|
||||
$this->asEditor()->delete($page->getUrl());
|
||||
$this->assertEquals(1, Deletion::query()->count());
|
||||
|
||||
Carbon::setTestNow(Carbon::now()->addDays(6000));
|
||||
$this->asEditor()->delete($otherPage->getUrl());
|
||||
$this->assertEquals(2, Deletion::query()->count());
|
||||
|
||||
$this->assertDatabaseHas('pages', ['id' => $page->id]);
|
||||
}
|
||||
|
||||
public function test_auto_clear_functionality_with_zero_time_deletes_instantly()
|
||||
{
|
||||
config()->set('app.recycle_bin_lifetime', 0);
|
||||
$page = Page::query()->firstOrFail();
|
||||
|
||||
$this->asEditor()->delete($page->getUrl());
|
||||
$this->assertDatabaseMissing('pages', ['id' => $page->id]);
|
||||
$this->assertEquals(0, Deletion::query()->count());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user