]> BookStack Code Mirror - bookstack/blob - app/Entities/Tools/Cloner.php
Merge pull request #5917 from BookStackApp/copy_references
[bookstack] / app / Entities / Tools / Cloner.php
1 <?php
2
3 namespace BookStack\Entities\Tools;
4
5 use BookStack\Activity\Models\Tag;
6 use BookStack\Entities\Models\Book;
7 use BookStack\Entities\Models\Bookshelf;
8 use BookStack\Entities\Models\Chapter;
9 use BookStack\Entities\Models\HasCoverInterface;
10 use BookStack\Entities\Models\Entity;
11 use BookStack\Entities\Models\Page;
12 use BookStack\Entities\Repos\BookRepo;
13 use BookStack\Entities\Repos\ChapterRepo;
14 use BookStack\Entities\Repos\PageRepo;
15 use BookStack\Permissions\Permission;
16 use BookStack\References\ReferenceChangeContext;
17 use BookStack\References\ReferenceUpdater;
18 use BookStack\Uploads\Image;
19 use BookStack\Uploads\ImageService;
20 use Illuminate\Http\UploadedFile;
21
22 class Cloner
23 {
24     protected ReferenceChangeContext $referenceChangeContext;
25
26     public function __construct(
27         protected PageRepo $pageRepo,
28         protected ChapterRepo $chapterRepo,
29         protected BookRepo $bookRepo,
30         protected ImageService $imageService,
31         protected ReferenceUpdater $referenceUpdater,
32     ) {
33         $this->referenceChangeContext = new ReferenceChangeContext();
34     }
35
36     /**
37      * Clone the given page into the given parent using the provided name.
38      */
39     public function clonePage(Page $original, Entity $parent, string $newName): Page
40     {
41         $context = $this->newReferenceChangeContext();
42         $page = $this->createPageClone($original, $parent, $newName);
43         $this->referenceUpdater->changeReferencesUsingContext($context);
44         return $page;
45     }
46
47     protected function createPageClone(Page $original, Entity $parent, string $newName): Page
48     {
49         $copyPage = $this->pageRepo->getNewDraftPage($parent);
50         $pageData = $this->entityToInputData($original);
51         $pageData['name'] = $newName;
52
53         $newPage = $this->pageRepo->publishDraft($copyPage, $pageData);
54         $this->referenceChangeContext->add($original, $newPage);
55
56         return $newPage;
57     }
58
59     /**
60      * Clone the given page into the given parent using the provided name.
61      * Clones all child pages.
62      */
63     public function cloneChapter(Chapter $original, Book $parent, string $newName): Chapter
64     {
65         $context = $this->newReferenceChangeContext();
66         $chapter = $this->createChapterClone($original, $parent, $newName);
67         $this->referenceUpdater->changeReferencesUsingContext($context);
68         return $chapter;
69     }
70
71     protected function createChapterClone(Chapter $original, Book $parent, string $newName): Chapter
72     {
73         $chapterDetails = $this->entityToInputData($original);
74         $chapterDetails['name'] = $newName;
75
76         $copyChapter = $this->chapterRepo->create($chapterDetails, $parent);
77
78         if (userCan(Permission::PageCreate, $copyChapter)) {
79             /** @var Page $page */
80             foreach ($original->getVisiblePages() as $page) {
81                 $this->createPageClone($page, $copyChapter, $page->name);
82             }
83         }
84
85         $this->referenceChangeContext->add($original, $copyChapter);
86
87         return $copyChapter;
88     }
89
90     /**
91      * Clone the given book.
92      * Clones all child chapters and pages.
93      */
94     public function cloneBook(Book $original, string $newName): Book
95     {
96         $context = $this->newReferenceChangeContext();
97         $book = $this->createBookClone($original, $newName);
98         $this->referenceUpdater->changeReferencesUsingContext($context);
99         return $book;
100     }
101
102     protected function createBookClone(Book $original, string $newName): Book
103     {
104         $bookDetails = $this->entityToInputData($original);
105         $bookDetails['name'] = $newName;
106
107         // Clone book
108         $copyBook = $this->bookRepo->create($bookDetails);
109
110         // Clone contents
111         $directChildren = $original->getDirectVisibleChildren();
112         foreach ($directChildren as $child) {
113             if ($child instanceof Chapter && userCan(Permission::ChapterCreate, $copyBook)) {
114                 $this->createChapterClone($child, $copyBook, $child->name);
115             }
116
117             if ($child instanceof Page && !$child->draft && userCan(Permission::PageCreate, $copyBook)) {
118                 $this->createPageClone($child, $copyBook, $child->name);
119             }
120         }
121
122         // Clone bookshelf relationships
123         /** @var Bookshelf $shelf */
124         foreach ($original->shelves as $shelf) {
125             if (userCan(Permission::BookshelfUpdate, $shelf)) {
126                 $shelf->appendBook($copyBook);
127             }
128         }
129
130         $this->referenceChangeContext->add($original, $copyBook);
131
132         return $copyBook;
133     }
134
135     /**
136      * Convert an entity to a raw data array of input data.
137      *
138      * @return array<string, mixed>
139      */
140     public function entityToInputData(Entity $entity): array
141     {
142         $inputData = $entity->getAttributes();
143         $inputData['tags'] = $this->entityTagsToInputArray($entity);
144
145         // Add a cover to the data if existing on the original entity
146         if ($entity instanceof HasCoverInterface) {
147             $cover = $entity->coverInfo()->getImage();
148             if ($cover) {
149                 $inputData['image'] = $this->imageToUploadedFile($cover);
150             }
151         }
152
153         return $inputData;
154     }
155
156     /**
157      * Copy the permission settings from the source entity to the target entity.
158      */
159     public function copyEntityPermissions(Entity $sourceEntity, Entity $targetEntity): void
160     {
161         $permissions = $sourceEntity->permissions()->get(['role_id', 'view', 'create', 'update', 'delete'])->toArray();
162         $targetEntity->permissions()->delete();
163         $targetEntity->permissions()->createMany($permissions);
164         $targetEntity->rebuildPermissions();
165     }
166
167     /**
168      * Convert an image instance to an UploadedFile instance to mimic
169      * a file being uploaded.
170      */
171     protected function imageToUploadedFile(Image $image): ?UploadedFile
172     {
173         $imgData = $this->imageService->getImageData($image);
174         $tmpImgFilePath = tempnam(sys_get_temp_dir(), 'bs_cover_clone_');
175         file_put_contents($tmpImgFilePath, $imgData);
176
177         return new UploadedFile($tmpImgFilePath, basename($image->path));
178     }
179
180     /**
181      * Convert the tags on the given entity to the raw format
182      * that's used for incoming request data.
183      */
184     protected function entityTagsToInputArray(Entity $entity): array
185     {
186         $tags = [];
187
188         /** @var Tag $tag */
189         foreach ($entity->tags as $tag) {
190             $tags[] = ['name' => $tag->name, 'value' => $tag->value];
191         }
192
193         return $tags;
194     }
195
196     protected function newReferenceChangeContext(): ReferenceChangeContext
197     {
198         $this->referenceChangeContext = new ReferenceChangeContext();
199         return $this->referenceChangeContext;
200     }
201 }