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.
|
# If set to 'false' a limit will not be enforced.
|
||||||
REVISION_LIMIT=50
|
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
|
# Allow <script> tags in page content
|
||||||
# Note, if set to 'true' the page editor may still escape scripts.
|
# Note, if set to 'true' the page editor may still escape scripts.
|
||||||
ALLOW_CONTENT_SCRIPTS=false
|
ALLOW_CONTENT_SCRIPTS=false
|
||||||
|
@ -31,6 +31,13 @@ return [
|
|||||||
// If set to false then a limit will not be enforced.
|
// If set to false then a limit will not be enforced.
|
||||||
'revision_limit' => env('REVISION_LIMIT', 50),
|
'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.
|
// Allow <script> tags to entered within page content.
|
||||||
// <script> tags are escaped by default.
|
// <script> tags are escaped by default.
|
||||||
// Even when overridden the WYSIWYG editor may still escape script content.
|
// 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\AttachmentService;
|
||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
class TrashCan
|
class TrashCan
|
||||||
{
|
{
|
||||||
@ -231,6 +232,30 @@ class TrashCan
|
|||||||
return $restoreCount;
|
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.
|
* Restore an entity so it is essentially un-deleted.
|
||||||
* Deletions on restored child elements will be removed during this restoration.
|
* Deletions on restored child elements will be removed during this restoration.
|
||||||
|
@ -129,5 +129,6 @@ class BookRepo
|
|||||||
{
|
{
|
||||||
$trashCan = new TrashCan();
|
$trashCan = new TrashCan();
|
||||||
$trashCan->softDestroyBook($book);
|
$trashCan->softDestroyBook($book);
|
||||||
|
$trashCan->autoClearOld();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -175,5 +175,6 @@ class BookshelfRepo
|
|||||||
{
|
{
|
||||||
$trashCan = new TrashCan();
|
$trashCan = new TrashCan();
|
||||||
$trashCan->softDestroyShelf($shelf);
|
$trashCan->softDestroyShelf($shelf);
|
||||||
|
$trashCan->autoClearOld();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,7 @@ class ChapterRepo
|
|||||||
{
|
{
|
||||||
$trashCan = new TrashCan();
|
$trashCan = new TrashCan();
|
||||||
$trashCan->softDestroyChapter($chapter);
|
$trashCan->softDestroyChapter($chapter);
|
||||||
|
$trashCan->autoClearOld();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -266,6 +266,7 @@ class PageRepo
|
|||||||
{
|
{
|
||||||
$trashCan = new TrashCan();
|
$trashCan = new TrashCan();
|
||||||
$trashCan->softDestroyPage($page);
|
$trashCan->softDestroyPage($page);
|
||||||
|
$trashCan->autoClearOld();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,7 +80,7 @@ return [
|
|||||||
'maint_send_test_email_mail_subject' => 'Test Email',
|
'maint_send_test_email_mail_subject' => 'Test Email',
|
||||||
'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',
|
'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_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',
|
'maint_recycle_bin_open' => 'Open Recycle Bin',
|
||||||
|
|
||||||
// Recycle Bin
|
// Recycle Bin
|
||||||
|
@ -5,6 +5,24 @@
|
|||||||
|
|
||||||
@include('settings.navbar-with-version', ['selected' => 'maintenance'])
|
@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">
|
<div id="image-cleanup" class="card content-wrap auto-height">
|
||||||
<h2 class="list-heading">{{ trans('settings.maint_image_cleanup') }}</h2>
|
<h2 class="list-heading">{{ trans('settings.maint_image_cleanup') }}</h2>
|
||||||
<div class="grid half gap-xl">
|
<div class="grid half gap-xl">
|
||||||
@ -15,7 +33,7 @@
|
|||||||
<form method="POST" action="{{ url('/settings/maintenance/cleanup-images') }}">
|
<form method="POST" action="{{ url('/settings/maintenance/cleanup-images') }}">
|
||||||
{!! csrf_field() !!}
|
{!! csrf_field() !!}
|
||||||
<input type="hidden" name="_method" value="DELETE">
|
<input type="hidden" name="_method" value="DELETE">
|
||||||
<div>
|
<div class="mb-s">
|
||||||
@if(session()->has('cleanup-images-warning'))
|
@if(session()->has('cleanup-images-warning'))
|
||||||
<p class="text-neg">
|
<p class="text-neg">
|
||||||
{{ session()->get('cleanup-images-warning') }}
|
{{ session()->get('cleanup-images-warning') }}
|
||||||
@ -50,23 +68,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
@stop
|
@stop
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Book;
|
||||||
use BookStack\Entities\Deletion;
|
use BookStack\Entities\Deletion;
|
||||||
use BookStack\Entities\Page;
|
use BookStack\Entities\Page;
|
||||||
|
use DB;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
class RecycleBinTest extends TestCase
|
class RecycleBinTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -39,11 +41,11 @@ class RecycleBinTest extends TestCase
|
|||||||
$this->giveUserPermissions($editor, ['settings-manage']);
|
$this->giveUserPermissions($editor, ['settings-manage']);
|
||||||
|
|
||||||
foreach($routes as $route) {
|
foreach($routes as $route) {
|
||||||
\DB::beginTransaction();
|
DB::beginTransaction();
|
||||||
[$method, $url] = explode(':', $route);
|
[$method, $url] = explode(':', $route);
|
||||||
$resp = $this->call($method, $url);
|
$resp = $this->call($method, $url);
|
||||||
$this->assertNotPermissionError($resp);
|
$this->assertNotPermissionError($resp);
|
||||||
\DB::rollBack();
|
DB::rollBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -93,15 +95,15 @@ class RecycleBinTest extends TestCase
|
|||||||
$this->asEditor()->delete($book->getUrl());
|
$this->asEditor()->delete($book->getUrl());
|
||||||
$deletion = Deletion::query()->firstOrFail();
|
$deletion = Deletion::query()->firstOrFail();
|
||||||
|
|
||||||
$this->assertEquals($book->pages->count(), \DB::table('pages')->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());
|
$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 = $this->asAdmin()->post("/settings/recycle-bin/{$deletion->id}/restore");
|
||||||
$restoreReq->assertRedirect('/settings/recycle-bin');
|
$restoreReq->assertRedirect('/settings/recycle-bin');
|
||||||
$this->assertTrue(Deletion::query()->count() === 0);
|
$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->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->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
|
||||||
|
|
||||||
$itemCount = 1 + $book->pages->count() + $book->chapters->count();
|
$itemCount = 1 + $book->pages->count() + $book->chapters->count();
|
||||||
$redirectReq = $this->get('/settings/recycle-bin');
|
$redirectReq = $this->get('/settings/recycle-bin');
|
||||||
@ -154,4 +156,47 @@ class RecycleBinTest extends TestCase
|
|||||||
'extra' => $page->name,
|
'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