diff --git a/app/Entities/Tools/Cloner.php b/app/Entities/Tools/Cloner.php index d74f2f195..59d8077a4 100644 --- a/app/Entities/Tools/Cloner.php +++ b/app/Entities/Tools/Cloner.php @@ -7,8 +7,12 @@ use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; +use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\ChapterRepo; use BookStack\Entities\Repos\PageRepo; +use BookStack\Uploads\Image; +use BookStack\Uploads\ImageService; +use Illuminate\Http\UploadedFile; class Cloner { @@ -23,10 +27,22 @@ class Cloner */ protected $chapterRepo; - public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo) + /** + * @var BookRepo + */ + protected $bookRepo; + + /** + * @var ImageService + */ + protected $imageService; + + public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo, BookRepo $bookRepo, ImageService $imageService) { $this->pageRepo = $pageRepo; $this->chapterRepo = $chapterRepo; + $this->bookRepo = $bookRepo; + $this->imageService = $imageService; } /** @@ -66,6 +82,55 @@ class Cloner return $copyChapter; } + /** + * Clone the given book. + * Clones all child chapters & pages. + */ + public function cloneBook(Book $original, string $newName): Book + { + $bookDetails = $original->getAttributes(); + $bookDetails['name'] = $newName; + $bookDetails['tags'] = $this->entityTagsToInputArray($original); + + $copyBook = $this->bookRepo->create($bookDetails); + + $directChildren = $original->getDirectChildren(); + foreach ($directChildren as $child) { + + if ($child instanceof Chapter && userCan('chapter-create', $copyBook)) { + $this->cloneChapter($child, $copyBook, $child->name); + } + + if ($child instanceof Page && !$child->draft && userCan('page-create', $copyBook)) { + $this->clonePage($child, $copyBook, $child->name); + } + } + + if ($original->cover) { + try { + $tmpImgFile = tmpfile(); + $uploadedFile = $this->imageToUploadedFile($original->cover, $tmpImgFile); + $this->bookRepo->updateCoverImage($copyBook, $uploadedFile, false); + } catch (\Exception $exception) { + } + } + + return $copyBook; + } + + /** + * Convert an image instance to an UploadedFile instance to mimic + * a file being uploaded. + */ + protected function imageToUploadedFile(Image $image, &$tmpFile): ?UploadedFile + { + $imgData = $this->imageService->getImageData($image); + $tmpImgFilePath = stream_get_meta_data($tmpFile)['uri']; + file_put_contents($tmpImgFilePath, $imgData); + + return new UploadedFile($tmpImgFilePath, basename($image->path)); + } + /** * Convert the tags on the given entity to the raw format * that's used for incoming request data. diff --git a/app/Facades/Activity.php b/app/Facades/Activity.php index 76493efd7..6c279a057 100644 --- a/app/Facades/Activity.php +++ b/app/Facades/Activity.php @@ -4,6 +4,9 @@ namespace BookStack\Facades; use Illuminate\Support\Facades\Facade; +/** + * @see \BookStack\Actions\ActivityLogger + */ class Activity extends Facade { /** diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index 5434afaf8..bc403c6d0 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -2,16 +2,18 @@ namespace BookStack\Http\Controllers; -use Activity; use BookStack\Actions\ActivityQueries; use BookStack\Actions\ActivityType; use BookStack\Actions\View; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Tools\BookContents; +use BookStack\Entities\Tools\Cloner; use BookStack\Entities\Tools\PermissionsUpdater; use BookStack\Entities\Tools\ShelfContext; use BookStack\Exceptions\ImageUploadException; +use BookStack\Exceptions\NotFoundException; +use BookStack\Facades\Activity; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Throwable; @@ -225,4 +227,39 @@ class BookController extends Controller return redirect($book->getUrl()); } + + /** + * Show the view to copy a book. + * + * @throws NotFoundException + */ + public function showCopy(string $bookSlug) + { + $book = $this->bookRepo->getBySlug($bookSlug); + $this->checkOwnablePermission('book-view', $book); + + session()->flashInput(['name' => $book->name]); + + return view('books.copy', [ + 'book' => $book, + ]); + } + + /** + * Create a copy of a book within the requested target destination. + * + * @throws NotFoundException + */ + public function copy(Request $request, Cloner $cloner, string $bookSlug) + { + $book = $this->bookRepo->getBySlug($bookSlug); + $this->checkOwnablePermission('book-view', $book); + $this->checkPermission('book-create-all'); + + $newName = $request->get('name') ?: $book->name; + $bookCopy = $cloner->cloneBook($book, $newName); + $this->showSuccessNotification(trans('entities.books_copy_success')); + + return redirect($bookCopy->getUrl()); + } } diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index 085285fc6..16f0779ca 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -210,7 +210,7 @@ class ChapterController extends Controller } /** - * Create a copy of a page within the requested target destination. + * Create a copy of a chapter within the requested target destination. * * @throws NotFoundException * @throws Throwable diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 665e833f4..7a6930546 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -143,6 +143,8 @@ return [ 'books_sort_chapters_last' => 'Chapters Last', 'books_sort_show_other' => 'Show Other Books', 'books_sort_save' => 'Save New Order', + 'books_copy' => 'Copy Book', + 'books_copy_success' => 'Book successfully copied', // Chapters 'chapter' => 'Chapter', diff --git a/resources/views/books/copy.blade.php b/resources/views/books/copy.blade.php new file mode 100644 index 000000000..4f01f55e2 --- /dev/null +++ b/resources/views/books/copy.blade.php @@ -0,0 +1,38 @@ +@extends('layouts.simple') + +@section('body') + +