Merge pull request #3118 from BookStackApp/copy_stuff
Additional copy/clone abilities
This commit is contained in:
		
						commit
						21f2a7087c
					
				| 
						 | 
				
			
			@ -18,7 +18,7 @@ class Chapter extends BookChild
 | 
			
		|||
 | 
			
		||||
    public $searchFactor = 1.2;
 | 
			
		||||
 | 
			
		||||
    protected $fillable = ['name', 'description', 'priority', 'book_id'];
 | 
			
		||||
    protected $fillable = ['name', 'description', 'priority'];
 | 
			
		||||
    protected $hidden = ['restricted', 'pivot', 'deleted_at'];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ namespace BookStack\Entities\Repos;
 | 
			
		|||
use BookStack\Actions\ActivityType;
 | 
			
		||||
use BookStack\Entities\Models\Book;
 | 
			
		||||
use BookStack\Entities\Models\Chapter;
 | 
			
		||||
use BookStack\Entities\Models\Entity;
 | 
			
		||||
use BookStack\Entities\Tools\BookContents;
 | 
			
		||||
use BookStack\Entities\Tools\TrashCan;
 | 
			
		||||
use BookStack\Exceptions\MoveOperationException;
 | 
			
		||||
| 
						 | 
				
			
			@ -87,17 +88,9 @@ class ChapterRepo
 | 
			
		|||
     */
 | 
			
		||||
    public function move(Chapter $chapter, string $parentIdentifier): Book
 | 
			
		||||
    {
 | 
			
		||||
        $stringExploded = explode(':', $parentIdentifier);
 | 
			
		||||
        $entityType = $stringExploded[0];
 | 
			
		||||
        $entityId = intval($stringExploded[1]);
 | 
			
		||||
 | 
			
		||||
        if ($entityType !== 'book') {
 | 
			
		||||
            throw new MoveOperationException('Chapters can only be moved into books');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /** @var Book $parent */
 | 
			
		||||
        $parent = Book::visible()->where('id', '=', $entityId)->first();
 | 
			
		||||
        if ($parent === null) {
 | 
			
		||||
        $parent = $this->findParentByIdentifier($parentIdentifier);
 | 
			
		||||
        if (is_null($parent)) {
 | 
			
		||||
            throw new MoveOperationException('Book to move chapter into not found');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -107,4 +100,24 @@ class ChapterRepo
 | 
			
		|||
 | 
			
		||||
        return $parent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find a page parent entity via an identifier string in the format:
 | 
			
		||||
     * {type}:{id}
 | 
			
		||||
     * Example: (book:5).
 | 
			
		||||
     *
 | 
			
		||||
     * @throws MoveOperationException
 | 
			
		||||
     */
 | 
			
		||||
    public function findParentByIdentifier(string $identifier): ?Book
 | 
			
		||||
    {
 | 
			
		||||
        $stringExploded = explode(':', $identifier);
 | 
			
		||||
        $entityType = $stringExploded[0];
 | 
			
		||||
        $entityId = intval($stringExploded[1]);
 | 
			
		||||
 | 
			
		||||
        if ($entityType !== 'book') {
 | 
			
		||||
            throw new MoveOperationException('Chapters can only be in books');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Book::visible()->where('id', '=', $entityId)->first();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -347,50 +347,13 @@ class PageRepo
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Copy an existing page in the system.
 | 
			
		||||
     * Optionally providing a new parent via string identifier and a new name.
 | 
			
		||||
     *
 | 
			
		||||
     * @throws MoveOperationException
 | 
			
		||||
     * @throws PermissionsException
 | 
			
		||||
     */
 | 
			
		||||
    public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
 | 
			
		||||
    {
 | 
			
		||||
        $parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->getParent();
 | 
			
		||||
        if ($parent === null) {
 | 
			
		||||
            throw new MoveOperationException('Book or chapter to move page into not found');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!userCan('page-create', $parent)) {
 | 
			
		||||
            throw new PermissionsException('User does not have permission to create a page within the new parent');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $copyPage = $this->getNewDraftPage($parent);
 | 
			
		||||
        $pageData = $page->getAttributes();
 | 
			
		||||
 | 
			
		||||
        // Update name
 | 
			
		||||
        if (!empty($newName)) {
 | 
			
		||||
            $pageData['name'] = $newName;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Copy tags from previous page if set
 | 
			
		||||
        if ($page->tags) {
 | 
			
		||||
            $pageData['tags'] = [];
 | 
			
		||||
            foreach ($page->tags as $tag) {
 | 
			
		||||
                $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->publishDraft($copyPage, $pageData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find a page parent entity via a identifier string in the format:
 | 
			
		||||
     * Find a page parent entity via an identifier string in the format:
 | 
			
		||||
     * {type}:{id}
 | 
			
		||||
     * Example: (book:5).
 | 
			
		||||
     *
 | 
			
		||||
     * @throws MoveOperationException
 | 
			
		||||
     */
 | 
			
		||||
    protected function findParentByIdentifier(string $identifier): ?Entity
 | 
			
		||||
    public function findParentByIdentifier(string $identifier): ?Entity
 | 
			
		||||
    {
 | 
			
		||||
        $stringExploded = explode(':', $identifier);
 | 
			
		||||
        $entityType = $stringExploded[0];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,150 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace BookStack\Entities\Tools;
 | 
			
		||||
 | 
			
		||||
use BookStack\Actions\Tag;
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var PageRepo
 | 
			
		||||
     */
 | 
			
		||||
    protected $pageRepo;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var ChapterRepo
 | 
			
		||||
     */
 | 
			
		||||
    protected $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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Clone the given page into the given parent using the provided name.
 | 
			
		||||
     */
 | 
			
		||||
    public function clonePage(Page $original, Entity $parent, string $newName): Page
 | 
			
		||||
    {
 | 
			
		||||
        $copyPage = $this->pageRepo->getNewDraftPage($parent);
 | 
			
		||||
        $pageData = $original->getAttributes();
 | 
			
		||||
 | 
			
		||||
        // Update name & tags
 | 
			
		||||
        $pageData['name'] = $newName;
 | 
			
		||||
        $pageData['tags'] = $this->entityTagsToInputArray($original);
 | 
			
		||||
 | 
			
		||||
        return $this->pageRepo->publishDraft($copyPage, $pageData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Clone the given page into the given parent using the provided name.
 | 
			
		||||
     * Clones all child pages.
 | 
			
		||||
     */
 | 
			
		||||
    public function cloneChapter(Chapter $original, Book $parent, string $newName): Chapter
 | 
			
		||||
    {
 | 
			
		||||
        $chapterDetails = $original->getAttributes();
 | 
			
		||||
        $chapterDetails['name'] = $newName;
 | 
			
		||||
        $chapterDetails['tags'] = $this->entityTagsToInputArray($original);
 | 
			
		||||
 | 
			
		||||
        $copyChapter = $this->chapterRepo->create($chapterDetails, $parent);
 | 
			
		||||
 | 
			
		||||
        if (userCan('page-create', $copyChapter)) {
 | 
			
		||||
            /** @var Page $page */
 | 
			
		||||
            foreach ($original->getVisiblePages() as $page) {
 | 
			
		||||
                $this->clonePage($page, $copyChapter, $page->name);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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.
 | 
			
		||||
     */
 | 
			
		||||
    protected function entityTagsToInputArray(Entity $entity): array
 | 
			
		||||
    {
 | 
			
		||||
        $tags = [];
 | 
			
		||||
 | 
			
		||||
        /** @var Tag $tag */
 | 
			
		||||
        foreach ($entity->tags as $tag) {
 | 
			
		||||
            $tags[] = ['name' => $tag->name, 'value' => $tag->value];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $tags;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,6 +4,9 @@ namespace BookStack\Facades;
 | 
			
		|||
 | 
			
		||||
use Illuminate\Support\Facades\Facade;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @see \BookStack\Actions\ActivityLogger
 | 
			
		||||
 */
 | 
			
		||||
class Activity extends Facade
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ use BookStack\Actions\View;
 | 
			
		|||
use BookStack\Entities\Models\Book;
 | 
			
		||||
use BookStack\Entities\Repos\ChapterRepo;
 | 
			
		||||
use BookStack\Entities\Tools\BookContents;
 | 
			
		||||
use BookStack\Entities\Tools\Cloner;
 | 
			
		||||
use BookStack\Entities\Tools\NextPreviousContentLocator;
 | 
			
		||||
use BookStack\Entities\Tools\PermissionsUpdater;
 | 
			
		||||
use BookStack\Exceptions\MoveOperationException;
 | 
			
		||||
| 
						 | 
				
			
			@ -190,6 +191,52 @@ class ChapterController extends Controller
 | 
			
		|||
        return redirect($chapter->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the view to copy a chapter.
 | 
			
		||||
     *
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     */
 | 
			
		||||
    public function showCopy(string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-view', $chapter);
 | 
			
		||||
 | 
			
		||||
        session()->flashInput(['name' => $chapter->name]);
 | 
			
		||||
 | 
			
		||||
        return view('chapters.copy', [
 | 
			
		||||
            'book' => $chapter->book,
 | 
			
		||||
            'chapter' => $chapter,
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a copy of a chapter within the requested target destination.
 | 
			
		||||
     *
 | 
			
		||||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function copy(Request $request, Cloner $cloner, string $bookSlug, string $chapterSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
 | 
			
		||||
        $this->checkOwnablePermission('chapter-view', $chapter);
 | 
			
		||||
 | 
			
		||||
        $entitySelection = $request->get('entity_selection') ?: null;
 | 
			
		||||
        $newParentBook = $entitySelection ? $this->chapterRepo->findParentByIdentifier($entitySelection) : $chapter->getParent();
 | 
			
		||||
 | 
			
		||||
        if (is_null($newParentBook)) {
 | 
			
		||||
            $this->showErrorNotification(trans('errors.selected_book_not_found'));
 | 
			
		||||
            return redirect()->back();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->checkOwnablePermission('chapter-create', $newParentBook);
 | 
			
		||||
 | 
			
		||||
        $newName = $request->get('name') ?: $chapter->name;
 | 
			
		||||
        $chapterCopy = $cloner->cloneChapter($chapter, $newParentBook, $newName);
 | 
			
		||||
        $this->showSuccessNotification(trans('entities.chapters_copy_success'));
 | 
			
		||||
 | 
			
		||||
        return redirect($chapterCopy->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Show the Restrictions view.
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ use BookStack\Actions\View;
 | 
			
		|||
use BookStack\Entities\Models\Page;
 | 
			
		||||
use BookStack\Entities\Repos\PageRepo;
 | 
			
		||||
use BookStack\Entities\Tools\BookContents;
 | 
			
		||||
use BookStack\Entities\Tools\Cloner;
 | 
			
		||||
use BookStack\Entities\Tools\NextPreviousContentLocator;
 | 
			
		||||
use BookStack\Entities\Tools\PageContent;
 | 
			
		||||
use BookStack\Entities\Tools\PageEditActivity;
 | 
			
		||||
| 
						 | 
				
			
			@ -447,26 +448,23 @@ class PageController extends Controller
 | 
			
		|||
     * @throws NotFoundException
 | 
			
		||||
     * @throws Throwable
 | 
			
		||||
     */
 | 
			
		||||
    public function copy(Request $request, string $bookSlug, string $pageSlug)
 | 
			
		||||
    public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
 | 
			
		||||
    {
 | 
			
		||||
        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
 | 
			
		||||
        $this->checkOwnablePermission('page-view', $page);
 | 
			
		||||
 | 
			
		||||
        $entitySelection = $request->get('entity_selection', null) ?? null;
 | 
			
		||||
        $newName = $request->get('name', null);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
 | 
			
		||||
        } catch (Exception $exception) {
 | 
			
		||||
            if ($exception instanceof PermissionsException) {
 | 
			
		||||
                $this->showPermissionError();
 | 
			
		||||
            }
 | 
			
		||||
        $entitySelection = $request->get('entity_selection') ?: null;
 | 
			
		||||
        $newParent = $entitySelection ? $this->pageRepo->findParentByIdentifier($entitySelection) : $page->getParent();
 | 
			
		||||
 | 
			
		||||
        if (is_null($newParent)) {
 | 
			
		||||
            $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
 | 
			
		||||
 | 
			
		||||
            return redirect()->back();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->checkOwnablePermission('page-create', $newParent);
 | 
			
		||||
 | 
			
		||||
        $newName = $request->get('name') ?: $page->name;
 | 
			
		||||
        $pageCopy = $cloner->clonePage($page, $newParent, $newName);
 | 
			
		||||
        $this->showSuccessNotification(trans('entities.pages_copy_success'));
 | 
			
		||||
 | 
			
		||||
        return redirect($pageCopy->getUrl());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
namespace BookStack\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use BookStack\Auth\Permissions\PermissionsRepo;
 | 
			
		||||
use BookStack\Auth\Role;
 | 
			
		||||
use BookStack\Exceptions\PermissionsException;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
| 
						 | 
				
			
			@ -34,11 +35,21 @@ class RoleController extends Controller
 | 
			
		|||
    /**
 | 
			
		||||
     * Show the form to create a new role.
 | 
			
		||||
     */
 | 
			
		||||
    public function create()
 | 
			
		||||
    public function create(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $this->checkPermission('user-roles-manage');
 | 
			
		||||
 | 
			
		||||
        return view('settings.roles.create');
 | 
			
		||||
        /** @var ?Role $role */
 | 
			
		||||
        $role = null;
 | 
			
		||||
        if ($request->has('copy_from')) {
 | 
			
		||||
            $role = Role::query()->find($request->get('copy_from'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($role) {
 | 
			
		||||
            $role->display_name .= ' (' . trans('common.copy') . ')';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return view('settings.roles.create', ['role' => $role]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +60,7 @@ class RoleController extends Controller
 | 
			
		|||
        $this->checkPermission('user-roles-manage');
 | 
			
		||||
        $this->validate($request, [
 | 
			
		||||
            'display_name' => ['required', 'min:3', 'max:180'],
 | 
			
		||||
            'description'  => 'max:180',
 | 
			
		||||
            'description'  => ['max:180'],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->permissionsRepo->saveNewRole($request->all());
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +95,7 @@ class RoleController extends Controller
 | 
			
		|||
        $this->checkPermission('user-roles-manage');
 | 
			
		||||
        $this->validate($request, [
 | 
			
		||||
            'display_name' => ['required', 'min:3', 'max:180'],
 | 
			
		||||
            'description'  => 'max:180',
 | 
			
		||||
            'description'  => ['max:180'],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->permissionsRepo->updateRole($id, $request->all());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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',
 | 
			
		||||
| 
						 | 
				
			
			@ -161,6 +163,8 @@ return [
 | 
			
		|||
    'chapters_move' => 'Move Chapter',
 | 
			
		||||
    'chapters_move_named' => 'Move Chapter :chapterName',
 | 
			
		||||
    'chapter_move_success' => 'Chapter moved to :bookName',
 | 
			
		||||
    'chapters_copy' => 'Copy Chapter',
 | 
			
		||||
    'chapters_copy_success' => 'Chapter successfully copied',
 | 
			
		||||
    'chapters_permissions' => 'Chapter Permissions',
 | 
			
		||||
    'chapters_empty' => 'No pages are currently in this chapter.',
 | 
			
		||||
    'chapters_permissions_active' => 'Chapter Permissions Active',
 | 
			
		||||
| 
						 | 
				
			
			@ -332,4 +336,12 @@ return [
 | 
			
		|||
    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',
 | 
			
		||||
    'revision_delete_success' => 'Revision deleted',
 | 
			
		||||
    'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',
 | 
			
		||||
 | 
			
		||||
    // Copy view
 | 
			
		||||
    'copy_consider' => 'Please consider the below when copying content.',
 | 
			
		||||
    'copy_consider_permissions' => 'Custom permission settings will not be copied.',
 | 
			
		||||
    'copy_consider_owner' => 'You will become the owner of all copied content.',
 | 
			
		||||
    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
 | 
			
		||||
    'copy_consider_attachments' => 'Page attachments will not be copied.',
 | 
			
		||||
    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',
 | 
			
		||||
];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
@extends('layouts.simple')
 | 
			
		||||
 | 
			
		||||
@section('body')
 | 
			
		||||
 | 
			
		||||
    <div class="container small">
 | 
			
		||||
 | 
			
		||||
        <div class="my-s">
 | 
			
		||||
            @include('entities.breadcrumbs', ['crumbs' => [
 | 
			
		||||
                $book,
 | 
			
		||||
                $book->getUrl('/copy') => [
 | 
			
		||||
                    'text' => trans('entities.books_copy'),
 | 
			
		||||
                    'icon' => 'copy',
 | 
			
		||||
                ]
 | 
			
		||||
            ]])
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="card content-wrap auto-height">
 | 
			
		||||
 | 
			
		||||
            <h1 class="list-heading">{{ trans('entities.books_copy') }}</h1>
 | 
			
		||||
 | 
			
		||||
            <form action="{{ $book->getUrl('/copy') }}" method="POST">
 | 
			
		||||
                {!! csrf_field() !!}
 | 
			
		||||
 | 
			
		||||
                <div class="form-group title-input">
 | 
			
		||||
                    <label for="name">{{ trans('common.name') }}</label>
 | 
			
		||||
                    @include('form.text', ['name' => 'name'])
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                @include('entities.copy-considerations')
 | 
			
		||||
 | 
			
		||||
                <div class="form-group text-right">
 | 
			
		||||
                    <a href="{{ $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
			
		||||
                    <button type="submit" class="button">{{ trans('entities.books_copy') }}</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@stop
 | 
			
		||||
| 
						 | 
				
			
			@ -110,6 +110,12 @@
 | 
			
		|||
                    <span>{{ trans('common.sort') }}</span>
 | 
			
		||||
                </a>
 | 
			
		||||
            @endif
 | 
			
		||||
            @if(userCan('book-create-all'))
 | 
			
		||||
                <a href="{{ $book->getUrl('/copy') }}" class="icon-list-item">
 | 
			
		||||
                    <span>@icon('copy')</span>
 | 
			
		||||
                    <span>{{ trans('common.copy') }}</span>
 | 
			
		||||
                </a>
 | 
			
		||||
            @endif
 | 
			
		||||
            @if(userCan('restrictions-manage', $book))
 | 
			
		||||
                <a href="{{ $book->getUrl('/permissions') }}" class="icon-list-item">
 | 
			
		||||
                    <span>@icon('lock')</span>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
@extends('layouts.simple')
 | 
			
		||||
 | 
			
		||||
@section('body')
 | 
			
		||||
 | 
			
		||||
    <div class="container small">
 | 
			
		||||
 | 
			
		||||
        <div class="my-s">
 | 
			
		||||
            @include('entities.breadcrumbs', ['crumbs' => [
 | 
			
		||||
                $chapter->book,
 | 
			
		||||
                $chapter,
 | 
			
		||||
                $chapter->getUrl('/copy') => [
 | 
			
		||||
                    'text' => trans('entities.chapters_copy'),
 | 
			
		||||
                    'icon' => 'copy',
 | 
			
		||||
                ]
 | 
			
		||||
            ]])
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="card content-wrap auto-height">
 | 
			
		||||
 | 
			
		||||
            <h1 class="list-heading">{{ trans('entities.chapters_copy') }}</h1>
 | 
			
		||||
 | 
			
		||||
            <form action="{{ $chapter->getUrl('/copy') }}" method="POST">
 | 
			
		||||
                {!! csrf_field() !!}
 | 
			
		||||
 | 
			
		||||
                <div class="form-group title-input">
 | 
			
		||||
                    <label for="name">{{ trans('common.name') }}</label>
 | 
			
		||||
                    @include('form.text', ['name' => 'name'])
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="form-group" collapsible>
 | 
			
		||||
                    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
 | 
			
		||||
                        <label for="entity_selection">{{ trans('entities.pages_copy_desination') }}</label>
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <div class="collapse-content" collapsible-content>
 | 
			
		||||
                        @include('entities.selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book', 'entityPermission' => 'chapter-create'])
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                @include('entities.copy-considerations')
 | 
			
		||||
 | 
			
		||||
                <div class="form-group text-right">
 | 
			
		||||
                    <a href="{{ $chapter->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
			
		||||
                    <button type="submit" class="button">{{ trans('entities.chapters_copy') }}</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@stop
 | 
			
		||||
| 
						 | 
				
			
			@ -108,6 +108,12 @@
 | 
			
		|||
                    <span>{{ trans('common.edit') }}</span>
 | 
			
		||||
                </a>
 | 
			
		||||
            @endif
 | 
			
		||||
            @if(userCanOnAny('chapter-create'))
 | 
			
		||||
                <a href="{{ $chapter->getUrl('/copy') }}" class="icon-list-item">
 | 
			
		||||
                    <span>@icon('copy')</span>
 | 
			
		||||
                    <span>{{ trans('common.copy') }}</span>
 | 
			
		||||
                </a>
 | 
			
		||||
            @endif
 | 
			
		||||
            @if(userCan('chapter-update', $chapter) && userCan('chapter-delete', $chapter))
 | 
			
		||||
                <a href="{{ $chapter->getUrl('/move') }}" class="icon-list-item">
 | 
			
		||||
                    <span>@icon('folder')</span>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
<p class="text-warn mb-none mt-l">
 | 
			
		||||
    @icon('warning') <strong>{{ trans('entities.copy_consider') }}</strong>
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
<div class="grid half no-gap no-row-gap text-warn mb-m">
 | 
			
		||||
    <ul class="pr-s mb-none">
 | 
			
		||||
        <li>{{ trans('entities.copy_consider_permissions') }}</li>
 | 
			
		||||
        <li>{{ trans('entities.copy_consider_owner') }}</li>
 | 
			
		||||
        <li>{{ trans('entities.copy_consider_images') }}</li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <ul class="pr-s mb-none">
 | 
			
		||||
        <li>{{ trans('entities.copy_consider_attachments') }}</li>
 | 
			
		||||
        <li>{{ trans('entities.copy_consider_access') }}</li>
 | 
			
		||||
    </ul>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +37,8 @@
 | 
			
		|||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                @include('entities.copy-considerations')
 | 
			
		||||
 | 
			
		||||
                <div class="form-group text-right">
 | 
			
		||||
                    <a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
			
		||||
                    <button type="submit" class="button">{{ trans('entities.pages_copy') }}</button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,9 +8,21 @@
 | 
			
		|||
            @include('settings.parts.navbar', ['selected' => 'roles'])
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <form action="{{ url("/settings/roles/new") }}" method="POST">
 | 
			
		||||
            @include('settings.roles.parts.form', ['title' => trans('settings.role_create')])
 | 
			
		||||
        </form>
 | 
			
		||||
        <div class="card content-wrap">
 | 
			
		||||
            <h1 class="list-heading">{{ trans('settings.role_create') }}</h1>
 | 
			
		||||
 | 
			
		||||
            <form action="{{ url("/settings/roles/new") }}" method="POST">
 | 
			
		||||
                {{ csrf_field() }}
 | 
			
		||||
 | 
			
		||||
                @include('settings.roles.parts.form', ['role' => $role ?? null])
 | 
			
		||||
 | 
			
		||||
                <div class="form-group text-right">
 | 
			
		||||
                    <a href="{{ url("/settings/roles") }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
			
		||||
                    <button type="submit" class="button">{{ trans('settings.role_save') }}</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@stop
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,10 +7,53 @@
 | 
			
		|||
            @include('settings.parts.navbar', ['selected' => 'roles'])
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <form action="{{ url("/settings/roles/{$role->id}") }}" method="POST">
 | 
			
		||||
            <input type="hidden" name="_method" value="PUT">
 | 
			
		||||
            @include('settings.roles.parts.form', ['model' => $role, 'title' => trans('settings.role_edit'), 'icon' => 'edit'])
 | 
			
		||||
        </form>
 | 
			
		||||
        <div class="card content-wrap">
 | 
			
		||||
            <h1 class="list-heading">{{ trans('settings.role_edit') }}</h1>
 | 
			
		||||
 | 
			
		||||
            <form action="{{ url("/settings/roles/{$role->id}") }}" method="POST">
 | 
			
		||||
                {{ csrf_field() }}
 | 
			
		||||
                {{ method_field('PUT') }}
 | 
			
		||||
 | 
			
		||||
                @include('settings.roles.parts.form', ['role' => $role])
 | 
			
		||||
 | 
			
		||||
                <div class="form-group text-right">
 | 
			
		||||
                    <a href="{{ url("/settings/roles") }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
			
		||||
                    <a href="{{ url("/settings/roles/new?copy_from={$role->id}") }}" class="button outline">{{ trans('common.copy') }}</a>
 | 
			
		||||
                    <a href="{{ url("/settings/roles/delete/{$role->id}") }}" class="button outline">{{ trans('settings.role_delete') }}</a>
 | 
			
		||||
                    <button type="submit" class="button">{{ trans('settings.role_save') }}</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        <div class="card content-wrap auto-height">
 | 
			
		||||
            <h2 class="list-heading">{{ trans('settings.role_users') }}</h2>
 | 
			
		||||
            @if(count($role->users ?? []) > 0)
 | 
			
		||||
                <div class="grid third">
 | 
			
		||||
                    @foreach($role->users as $user)
 | 
			
		||||
                        <div class="user-list-item">
 | 
			
		||||
                            <div>
 | 
			
		||||
                                <img class="avatar small" src="{{ $user->getAvatar(40) }}" alt="{{ $user->name }}">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div>
 | 
			
		||||
                                @if(userCan('users-manage') || user()->id == $user->id)
 | 
			
		||||
                                    <a href="{{ url("/settings/users/{$user->id}") }}">
 | 
			
		||||
                                        @endif
 | 
			
		||||
                                        {{ $user->name }}
 | 
			
		||||
                                        @if(userCan('users-manage') || user()->id == $user->id)
 | 
			
		||||
                                    </a>
 | 
			
		||||
                                @endif
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    @endforeach
 | 
			
		||||
                </div>
 | 
			
		||||
            @else
 | 
			
		||||
                <p class="text-muted">
 | 
			
		||||
                    {{ trans('settings.role_users_none') }}
 | 
			
		||||
                </p>
 | 
			
		||||
            @endif
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@stop
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,267 +1,224 @@
 | 
			
		|||
{!! csrf_field() !!}
 | 
			
		||||
 | 
			
		||||
<div class="card content-wrap">
 | 
			
		||||
    <h1 class="list-heading">{{ $title }}</h1>
 | 
			
		||||
 | 
			
		||||
    <div class="setting-list">
 | 
			
		||||
 | 
			
		||||
        <div class="grid half">
 | 
			
		||||
            <div>
 | 
			
		||||
                <label class="setting-list-label">{{ trans('settings.role_details') }}</label>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div>
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                    <label for="display_name">{{ trans('settings.role_name') }}</label>
 | 
			
		||||
                    @include('form.text', ['name' => 'display_name'])
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                    <label for="description">{{ trans('settings.role_desc') }}</label>
 | 
			
		||||
                    @include('form.text', ['name' => 'description'])
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                    @include('form.checkbox', ['name' => 'mfa_enforced', 'label' => trans('settings.role_mfa_enforced') ])
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                @if(in_array(config('auth.method'), ['ldap', 'saml2', 'oidc']))
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="name">{{ trans('settings.role_external_auth_id') }}</label>
 | 
			
		||||
                        @include('form.text', ['name' => 'external_auth_id'])
 | 
			
		||||
                    </div>
 | 
			
		||||
                @endif
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div permissions-table>
 | 
			
		||||
            <label class="setting-list-label">{{ trans('settings.role_system') }}</label>
 | 
			
		||||
            <a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
 | 
			
		||||
            <div class="toggle-switch-list grid half mt-m">
 | 
			
		||||
                <div>
 | 
			
		||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])</div>
 | 
			
		||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])</div>
 | 
			
		||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>
 | 
			
		||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
 | 
			
		||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])</div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
 | 
			
		||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'users-manage', 'label' => trans('settings.role_manage_users')])</div>
 | 
			
		||||
                    <div>@include('settings.roles.parts.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])</div>
 | 
			
		||||
                    <p class="text-warn text-small mt-s mb-none">{{ trans('settings.roles_system_warning') }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
<div class="setting-list">
 | 
			
		||||
 | 
			
		||||
    <div class="grid half">
 | 
			
		||||
        <div>
 | 
			
		||||
            <label class="setting-list-label">{{ trans('settings.role_asset') }}</label>
 | 
			
		||||
            <p>{{ trans('settings.role_asset_desc') }}</p>
 | 
			
		||||
 | 
			
		||||
            @if (isset($role) && $role->system_name === 'admin')
 | 
			
		||||
                <p class="text-warn">{{ trans('settings.role_asset_admins') }}</p>
 | 
			
		||||
            @endif
 | 
			
		||||
 | 
			
		||||
            <table permissions-table class="table toggle-switch-list compact permissions-table">
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th width="20%">
 | 
			
		||||
                        <a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.create') }}</th>
 | 
			
		||||
                    <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.view') }}</th>
 | 
			
		||||
                    <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.edit') }}</th>
 | 
			
		||||
                    <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.delete') }}</th>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <div>{{ trans('entities.shelves_long') }}</div>
 | 
			
		||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <div>{{ trans('entities.books') }}</div>
 | 
			
		||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-view-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-view-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <div>{{ trans('entities.chapters') }}</div>
 | 
			
		||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <div>{{ trans('entities.pages') }}</div>
 | 
			
		||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-create-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-view-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-view-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <div>{{ trans('entities.images') }}</div>
 | 
			
		||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>@include('settings.roles.parts.checkbox', ['permission' => 'image-create-all', 'label' => ''])</td>
 | 
			
		||||
                    <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'image-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'image-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <div>{{ trans('entities.attachments') }}</div>
 | 
			
		||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>@include('settings.roles.parts.checkbox', ['permission' => 'attachment-create-all', 'label' => ''])</td>
 | 
			
		||||
                    <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <div>{{ trans('entities.comments') }}</div>
 | 
			
		||||
                        <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>@include('settings.roles.parts.checkbox', ['permission' => 'comment-create-all', 'label' => ''])</td>
 | 
			
		||||
                    <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                        <br>
 | 
			
		||||
                        @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </table>
 | 
			
		||||
            <label class="setting-list-label">{{ trans('settings.role_details') }}</label>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
        <div>
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <label for="display_name">{{ trans('settings.role_name') }}</label>
 | 
			
		||||
                @include('form.text', ['name' => 'display_name', 'model' => $role])
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <label for="description">{{ trans('settings.role_desc') }}</label>
 | 
			
		||||
                @include('form.text', ['name' => 'description', 'model' => $role])
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                @include('form.checkbox', ['name' => 'mfa_enforced', 'label' => trans('settings.role_mfa_enforced'), 'model' => $role ])
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
    <div class="form-group text-right">
 | 
			
		||||
        <a href="{{ url("/settings/roles") }}" class="button outline">{{ trans('common.cancel') }}</a>
 | 
			
		||||
        @if (isset($role) && $role->id)
 | 
			
		||||
            <a href="{{ url("/settings/roles/delete/{$role->id}") }}" class="button outline">{{ trans('settings.role_delete') }}</a>
 | 
			
		||||
        @endif
 | 
			
		||||
        <button type="submit" class="button">{{ trans('settings.role_save') }}</button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="card content-wrap auto-height">
 | 
			
		||||
    <h2 class="list-heading">{{ trans('settings.role_users') }}</h2>
 | 
			
		||||
    @if(count($role->users ?? []) > 0)
 | 
			
		||||
        <div class="grid third">
 | 
			
		||||
            @foreach($role->users as $user)
 | 
			
		||||
                <div class="user-list-item">
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <img class="avatar small" src="{{ $user->getAvatar(40) }}" alt="{{ $user->name }}">
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div>
 | 
			
		||||
                        @if(userCan('users-manage') || user()->id == $user->id)
 | 
			
		||||
                            <a href="{{ url("/settings/users/{$user->id}") }}">
 | 
			
		||||
                                @endif
 | 
			
		||||
                                {{ $user->name }}
 | 
			
		||||
                                @if(userCan('users-manage') || user()->id == $user->id)
 | 
			
		||||
                            </a>
 | 
			
		||||
                        @endif
 | 
			
		||||
                    </div>
 | 
			
		||||
            @if(in_array(config('auth.method'), ['ldap', 'saml2', 'oidc']))
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                    <label for="name">{{ trans('settings.role_external_auth_id') }}</label>
 | 
			
		||||
                    @include('form.text', ['name' => 'external_auth_id', 'model' => $role])
 | 
			
		||||
                </div>
 | 
			
		||||
            @endforeach
 | 
			
		||||
            @endif
 | 
			
		||||
        </div>
 | 
			
		||||
    @else
 | 
			
		||||
        <p class="text-muted">
 | 
			
		||||
            {{ trans('settings.role_users_none') }}
 | 
			
		||||
        </p>
 | 
			
		||||
    @endif
 | 
			
		||||
</div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div permissions-table>
 | 
			
		||||
        <label class="setting-list-label">{{ trans('settings.role_system') }}</label>
 | 
			
		||||
        <a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
 | 
			
		||||
        <div class="toggle-switch-list grid half mt-m">
 | 
			
		||||
            <div>
 | 
			
		||||
                <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])</div>
 | 
			
		||||
                <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])</div>
 | 
			
		||||
                <div>@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>
 | 
			
		||||
                <div>@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
 | 
			
		||||
                <div>@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])</div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div>
 | 
			
		||||
                <div>@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
 | 
			
		||||
                <div>@include('settings.roles.parts.checkbox', ['permission' => 'users-manage', 'label' => trans('settings.role_manage_users')])</div>
 | 
			
		||||
                <div>@include('settings.roles.parts.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])</div>
 | 
			
		||||
                <p class="text-warn text-small mt-s mb-none">{{ trans('settings.roles_system_warning') }}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div>
 | 
			
		||||
        <label class="setting-list-label">{{ trans('settings.role_asset') }}</label>
 | 
			
		||||
        <p>{{ trans('settings.role_asset_desc') }}</p>
 | 
			
		||||
 | 
			
		||||
        @if (isset($role) && $role->system_name === 'admin')
 | 
			
		||||
            <p class="text-warn">{{ trans('settings.role_asset_admins') }}</p>
 | 
			
		||||
        @endif
 | 
			
		||||
 | 
			
		||||
        <table permissions-table class="table toggle-switch-list compact permissions-table">
 | 
			
		||||
            <tr>
 | 
			
		||||
                <th width="20%">
 | 
			
		||||
                    <a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                </th>
 | 
			
		||||
                <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.create') }}</th>
 | 
			
		||||
                <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.view') }}</th>
 | 
			
		||||
                <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.edit') }}</th>
 | 
			
		||||
                <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.delete') }}</th>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div>{{ trans('entities.shelves_long') }}</div>
 | 
			
		||||
                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div>{{ trans('entities.books') }}</div>
 | 
			
		||||
                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'book-view-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'book-view-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'book-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'book-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div>{{ trans('entities.chapters') }}</div>
 | 
			
		||||
                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div>{{ trans('entities.pages') }}</div>
 | 
			
		||||
                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'page-create-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'page-view-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'page-view-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'page-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'page-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div>{{ trans('entities.images') }}</div>
 | 
			
		||||
                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>@include('settings.roles.parts.checkbox', ['permission' => 'image-create-all', 'label' => ''])</td>
 | 
			
		||||
                <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'image-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'image-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div>{{ trans('entities.attachments') }}</div>
 | 
			
		||||
                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>@include('settings.roles.parts.checkbox', ['permission' => 'attachment-create-all', 'label' => ''])</td>
 | 
			
		||||
                <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div>{{ trans('entities.comments') }}</div>
 | 
			
		||||
                    <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>@include('settings.roles.parts.checkbox', ['permission' => 'comment-create-all', 'label' => ''])</td>
 | 
			
		||||
                <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-own', 'label' => trans('settings.role_own')])
 | 
			
		||||
                    <br>
 | 
			
		||||
                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-all', 'label' => trans('settings.role_all')])
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
        </table>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -80,6 +80,8 @@ Route::middleware('auth')->group(function () {
 | 
			
		|||
    Route::get('/books/{bookSlug}/permissions', [BookController::class, 'showPermissions']);
 | 
			
		||||
    Route::put('/books/{bookSlug}/permissions', [BookController::class, 'permissions']);
 | 
			
		||||
    Route::get('/books/{slug}/delete', [BookController::class, 'showDelete']);
 | 
			
		||||
    Route::get('/books/{bookSlug}/copy', [BookController::class, 'showCopy']);
 | 
			
		||||
    Route::post('/books/{bookSlug}/copy', [BookController::class, 'copy']);
 | 
			
		||||
    Route::get('/books/{bookSlug}/sort', [BookSortController::class, 'show']);
 | 
			
		||||
    Route::put('/books/{bookSlug}/sort', [BookSortController::class, 'update']);
 | 
			
		||||
    Route::get('/books/{bookSlug}/export/html', [BookExportController::class, 'html']);
 | 
			
		||||
| 
						 | 
				
			
			@ -127,6 +129,8 @@ Route::middleware('auth')->group(function () {
 | 
			
		|||
    Route::put('/books/{bookSlug}/chapter/{chapterSlug}', [ChapterController::class, 'update']);
 | 
			
		||||
    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/move', [ChapterController::class, 'showMove']);
 | 
			
		||||
    Route::put('/books/{bookSlug}/chapter/{chapterSlug}/move', [ChapterController::class, 'move']);
 | 
			
		||||
    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/copy', [ChapterController::class, 'showCopy']);
 | 
			
		||||
    Route::post('/books/{bookSlug}/chapter/{chapterSlug}/copy', [ChapterController::class, 'copy']);
 | 
			
		||||
    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/edit', [ChapterController::class, 'edit']);
 | 
			
		||||
    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [ChapterController::class, 'showPermissions']);
 | 
			
		||||
    Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/pdf', [ChapterExportController::class, 'pdf']);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,10 +3,15 @@
 | 
			
		|||
namespace Tests\Entity;
 | 
			
		||||
 | 
			
		||||
use BookStack\Entities\Models\Book;
 | 
			
		||||
use BookStack\Entities\Models\BookChild;
 | 
			
		||||
use BookStack\Entities\Repos\BookRepo;
 | 
			
		||||
use Tests\TestCase;
 | 
			
		||||
use Tests\Uploads\UsesImages;
 | 
			
		||||
 | 
			
		||||
class BookTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use UsesImages;
 | 
			
		||||
 | 
			
		||||
    public function test_create()
 | 
			
		||||
    {
 | 
			
		||||
        $book = Book::factory()->make([
 | 
			
		||||
| 
						 | 
				
			
			@ -204,4 +209,88 @@ class BookTest extends TestCase
 | 
			
		|||
 | 
			
		||||
        $this->assertEquals('parta-partb-partc', $book->slug);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_show_view_has_copy_button()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Book $book */
 | 
			
		||||
        $book = Book::query()->first();
 | 
			
		||||
        $resp = $this->asEditor()->get($book->getUrl());
 | 
			
		||||
 | 
			
		||||
        $resp->assertElementContains("a[href=\"{$book->getUrl('/copy')}\"]", 'Copy');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_copy_view()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Book $book */
 | 
			
		||||
        $book = Book::query()->first();
 | 
			
		||||
        $resp = $this->asEditor()->get($book->getUrl('/copy'));
 | 
			
		||||
 | 
			
		||||
        $resp->assertOk();
 | 
			
		||||
        $resp->assertSee('Copy Book');
 | 
			
		||||
        $resp->assertElementExists("input[name=\"name\"][value=\"{$book->name}\"]");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_copy()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Book $book */
 | 
			
		||||
        $book = Book::query()->whereHas('chapters')->whereHas('pages')->first();
 | 
			
		||||
        $resp = $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
 | 
			
		||||
 | 
			
		||||
        /** @var Book $copy */
 | 
			
		||||
        $copy = Book::query()->where('name', '=', 'My copy book')->first();
 | 
			
		||||
 | 
			
		||||
        $resp->assertRedirect($copy->getUrl());
 | 
			
		||||
        $this->assertEquals($book->getDirectChildren()->count(), $copy->getDirectChildren()->count());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_copy_does_not_copy_non_visible_content()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Book $book */
 | 
			
		||||
        $book = Book::query()->whereHas('chapters')->whereHas('pages')->first();
 | 
			
		||||
 | 
			
		||||
        // Hide child content
 | 
			
		||||
        /** @var BookChild $page */
 | 
			
		||||
        foreach ($book->getDirectChildren() as $child) {
 | 
			
		||||
            $child->restricted = true;
 | 
			
		||||
            $child->save();
 | 
			
		||||
            $this->regenEntityPermissions($child);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
 | 
			
		||||
        /** @var Book $copy */
 | 
			
		||||
        $copy = Book::query()->where('name', '=', 'My copy book')->first();
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(0, $copy->getDirectChildren()->count());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_copy_does_not_copy_pages_or_chapters_if_user_cant_create()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Book $book */
 | 
			
		||||
        $book = Book::query()->whereHas('chapters')->whereHas('directPages')->whereHas('chapters')->first();
 | 
			
		||||
        $viewer = $this->getViewer();
 | 
			
		||||
        $this->giveUserPermissions($viewer, ['book-create-all']);
 | 
			
		||||
 | 
			
		||||
        $this->actingAs($viewer)->post($book->getUrl('/copy'), ['name' => 'My copy book']);
 | 
			
		||||
        /** @var Book $copy */
 | 
			
		||||
        $copy = Book::query()->where('name', '=', 'My copy book')->first();
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(0, $copy->pages()->count());
 | 
			
		||||
        $this->assertEquals(0, $copy->chapters()->count());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_copy_clones_cover_image_if_existing()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Book $book */
 | 
			
		||||
        $book = Book::query()->first();
 | 
			
		||||
        $bookRepo = $this->app->make(BookRepo::class);
 | 
			
		||||
        $coverImageFile = $this->getTestImage('cover.png');
 | 
			
		||||
        $bookRepo->updateCoverImage($book, $coverImageFile);
 | 
			
		||||
 | 
			
		||||
        $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
 | 
			
		||||
 | 
			
		||||
        /** @var Book $copy */
 | 
			
		||||
        $copy = Book::query()->where('name', '=', 'My copy book')->first();
 | 
			
		||||
        $this->assertNotNull($copy->cover);
 | 
			
		||||
        $this->assertNotEquals($book->cover->id, $copy->cover->id);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ namespace Tests\Entity;
 | 
			
		|||
 | 
			
		||||
use BookStack\Entities\Models\Book;
 | 
			
		||||
use BookStack\Entities\Models\Chapter;
 | 
			
		||||
use BookStack\Entities\Models\Page;
 | 
			
		||||
use Tests\TestCase;
 | 
			
		||||
 | 
			
		||||
class ChapterTest extends TestCase
 | 
			
		||||
| 
						 | 
				
			
			@ -54,4 +55,95 @@ class ChapterTest extends TestCase
 | 
			
		|||
        $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
 | 
			
		||||
        $redirectReq->assertNotificationContains('Chapter Successfully Deleted');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_show_view_has_copy_button()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Chapter $chapter */
 | 
			
		||||
        $chapter = Chapter::query()->first();
 | 
			
		||||
 | 
			
		||||
        $resp = $this->asEditor()->get($chapter->getUrl());
 | 
			
		||||
        $resp->assertElementContains("a[href$=\"{$chapter->getUrl('/copy')}\"]", 'Copy');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_copy_view()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Chapter $chapter */
 | 
			
		||||
        $chapter = Chapter::query()->first();
 | 
			
		||||
 | 
			
		||||
        $resp = $this->asEditor()->get($chapter->getUrl('/copy'));
 | 
			
		||||
        $resp->assertOk();
 | 
			
		||||
        $resp->assertSee('Copy Chapter');
 | 
			
		||||
        $resp->assertElementExists("input[name=\"name\"][value=\"{$chapter->name}\"]");
 | 
			
		||||
        $resp->assertElementExists("input[name=\"entity_selection\"]");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_copy()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Chapter $chapter */
 | 
			
		||||
        $chapter = Chapter::query()->whereHas('pages')->first();
 | 
			
		||||
        /** @var Book $otherBook */
 | 
			
		||||
        $otherBook = Book::query()->where('id', '!=', $chapter->book_id)->first();
 | 
			
		||||
 | 
			
		||||
        $resp = $this->asEditor()->post($chapter->getUrl('/copy'), [
 | 
			
		||||
            'name' => 'My copied chapter',
 | 
			
		||||
            'entity_selection' => 'book:' . $otherBook->id,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        /** @var Chapter $newChapter */
 | 
			
		||||
        $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first();
 | 
			
		||||
 | 
			
		||||
        $resp->assertRedirect($newChapter->getUrl());
 | 
			
		||||
        $this->assertEquals($otherBook->id, $newChapter->book_id);
 | 
			
		||||
        $this->assertEquals($chapter->pages->count(), $newChapter->pages->count());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_copy_does_not_copy_non_visible_pages()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Chapter $chapter */
 | 
			
		||||
        $chapter = Chapter::query()->whereHas('pages')->first();
 | 
			
		||||
 | 
			
		||||
        // Hide pages to all non-admin roles
 | 
			
		||||
        /** @var Page $page */
 | 
			
		||||
        foreach ($chapter->pages as $page) {
 | 
			
		||||
            $page->restricted = true;
 | 
			
		||||
            $page->save();
 | 
			
		||||
            $this->regenEntityPermissions($page);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->asEditor()->post($chapter->getUrl('/copy'), [
 | 
			
		||||
            'name' => 'My copied chapter',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        /** @var Chapter $newChapter */
 | 
			
		||||
        $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first();
 | 
			
		||||
        $this->assertEquals(0, $newChapter->pages()->count());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_copy_does_not_copy_pages_if_user_cant_page_create()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Chapter $chapter */
 | 
			
		||||
        $chapter = Chapter::query()->whereHas('pages')->first();
 | 
			
		||||
        $viewer = $this->getViewer();
 | 
			
		||||
        $this->giveUserPermissions($viewer, ['chapter-create-all']);
 | 
			
		||||
 | 
			
		||||
        // Lacking permission results in no copied pages
 | 
			
		||||
        $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [
 | 
			
		||||
            'name' => 'My copied chapter',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        /** @var Chapter $newChapter */
 | 
			
		||||
        $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first();
 | 
			
		||||
        $this->assertEquals(0, $newChapter->pages()->count());
 | 
			
		||||
 | 
			
		||||
        $this->giveUserPermissions($viewer, ['page-create-all']);
 | 
			
		||||
 | 
			
		||||
        // Having permission rules in copied pages
 | 
			
		||||
        $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [
 | 
			
		||||
            'name' => 'My copied again chapter',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        /** @var Chapter $newChapter2 */
 | 
			
		||||
        $newChapter2 = Chapter::query()->where('name', '=', 'My copied again chapter')->first();
 | 
			
		||||
        $this->assertEquals($chapter->pages()->count(), $newChapter2->pages()->count());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -163,6 +163,23 @@ class RolesTest extends TestCase
 | 
			
		|||
        $this->assertEquals($this->user->id, $roleA->users()->first()->id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_copy_role_button_shown()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Role $role */
 | 
			
		||||
        $role = Role::query()->first();
 | 
			
		||||
        $resp = $this->asAdmin()->get("/settings/roles/{$role->id}");
 | 
			
		||||
        $resp->assertElementContains('a[href$="/roles/new?copy_from=' . $role->id . '"]', 'Copy');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_copy_from_param_on_create_prefills_with_other_role_data()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Role $role */
 | 
			
		||||
        $role = Role::query()->first();
 | 
			
		||||
        $resp = $this->asAdmin()->get("/settings/roles/new?copy_from={$role->id}");
 | 
			
		||||
        $resp->assertOk();
 | 
			
		||||
        $resp->assertElementExists('input[name="display_name"][value="' . ($role->display_name . ' (Copy)')  . '"]');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function test_manage_user_permission()
 | 
			
		||||
    {
 | 
			
		||||
        $this->actingAs($this->user)->get('/settings/users')->assertRedirect('/');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue