diff --git a/app/Entity.php b/app/Entity.php index 67edec4e0..5d4449f2b 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -197,8 +197,8 @@ class Entity extends Ownable * @param $path * @return string */ - public function getUrl($path) + public function getUrl($path = '/') { - return '/'; + return $path; } } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 9cc73ae15..f42cf63ec 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -592,12 +592,70 @@ class PageController extends Controller return redirect($page->getUrl()); } + /** + * Show the view to copy a page. + * @param string $bookSlug + * @param string $pageSlug + * @return mixed + * @throws NotFoundException + */ + public function showCopy($bookSlug, $pageSlug) + { + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $this->checkOwnablePermission('page-update', $page); + session()->flashInput(['name' => $page->name]); + return view('pages/copy', [ + 'book' => $page->book, + 'page' => $page + ]); + } + + /** + * Create a copy of a page within the requested target destination. + * @param string $bookSlug + * @param string $pageSlug + * @param Request $request + * @return mixed + * @throws NotFoundException + */ + public function copy($bookSlug, $pageSlug, Request $request) + { + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $this->checkOwnablePermission('page-update', $page); + + $entitySelection = $request->get('entity_selection', null); + if ($entitySelection === null || $entitySelection === '') { + $parent = $page->chapter ? $page->chapter : $page->book; + } else { + $stringExploded = explode(':', $entitySelection); + $entityType = $stringExploded[0]; + $entityId = intval($stringExploded[1]); + + try { + $parent = $this->entityRepo->getById($entityType, $entityId); + } catch (\Exception $e) { + session()->flash(trans('entities.selected_book_chapter_not_found')); + return redirect()->back(); + } + } + + $this->checkOwnablePermission('page-create', $parent); + + $pageCopy = $this->entityRepo->copyPage($page, $parent, $request->get('name', '')); + + Activity::add($pageCopy, 'page_create', $pageCopy->book->id); + session()->flash('success', trans('entities.pages_copy_success')); + + return redirect($pageCopy->getUrl()); + } + /** * Set the permissions for this page. * @param string $bookSlug * @param string $pageSlug * @param Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * @throws NotFoundException */ public function restrict($bookSlug, $pageSlug, Request $request) { diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index ddc92f705..49f9885ad 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -89,16 +89,17 @@ class SearchController extends Controller { $entityTypes = $request->filled('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']); $searchTerm = $request->get('term', false); + $permission = $request->get('permission', 'view'); // Search for entities otherwise show most popular if ($searchTerm !== false) { $searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}'; - $entities = $this->searchService->searchEntities($searchTerm)['results']; + $entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results']; } else { $entityNames = $entityTypes->map(function ($type) { return 'BookStack\\' . ucfirst($type); })->toArray(); - $entities = $this->viewService->getPopular(20, 0, $entityNames); + $entities = $this->viewService->getPopular(20, 0, $entityNames, $permission); } return view('search/entity-ajax-list', ['entities' => $entities]); diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index 14f9d8d0e..bdd1e37b1 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -593,6 +593,30 @@ class EntityRepo return $slug; } + /** + * Get a new draft page instance. + * @param Book $book + * @param Chapter|bool $chapter + * @return Page + */ + public function getDraftPage(Book $book, $chapter = false) + { + $page = $this->page->newInstance(); + $page->name = trans('entities.pages_initial_name'); + $page->created_by = user()->id; + $page->updated_by = user()->id; + $page->draft = true; + + if ($chapter) { + $page->chapter_id = $chapter->id; + } + + $book->pages()->save($page); + $page = $this->page->find($page->id); + $this->permissionService->buildJointPermissionsForEntity($page); + return $page; + } + /** * Publish a draft page to make it a normal page. * Sets the slug and updates the content. @@ -621,6 +645,43 @@ class EntityRepo return $draftPage; } + /** + * Create a copy of a page in a new location with a new name. + * @param Page $page + * @param Entity $newParent + * @param string $newName + * @return Page + */ + public function copyPage(Page $page, Entity $newParent, $newName = '') + { + $newBook = $newParent->isA('book') ? $newParent : $newParent->book; + $newChapter = $newParent->isA('chapter') ? $newParent : null; + $copyPage = $this->getDraftPage($newBook, $newChapter); + $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]; + } + } + + // Set priority + if ($newParent->isA('chapter')) { + $pageData['priority'] = $this->getNewChapterPriority($newParent); + } else { + $pageData['priority'] = $this->getNewBookPriority($newParent); + } + + return $this->publishPageDraft($copyPage, $pageData); + } + /** * Saves a page revision into the system. * @param Page $page @@ -805,30 +866,6 @@ class EntityRepo return strip_tags($html); } - /** - * Get a new draft page instance. - * @param Book $book - * @param Chapter|bool $chapter - * @return Page - */ - public function getDraftPage(Book $book, $chapter = false) - { - $page = $this->page->newInstance(); - $page->name = trans('entities.pages_initial_name'); - $page->created_by = user()->id; - $page->updated_by = user()->id; - $page->draft = true; - - if ($chapter) { - $page->chapter_id = $chapter->id; - } - - $book->pages()->save($page); - $page = $this->page->find($page->id); - $this->permissionService->buildJointPermissionsForEntity($page); - return $page; - } - /** * Search for image usage within page content. * @param $imageString diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index 331ed06c8..e74801dc8 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -630,16 +630,17 @@ class PermissionService * @param string $tableName * @param string $entityIdColumn * @param string $entityTypeColumn + * @param string $action * @return mixed */ - public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn) + public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view') { if ($this->isAdmin()) { $this->clean(); return $query; } - $this->currentAction = 'view'; + $this->currentAction = $action; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; $q = $query->where(function ($query) use ($tableDetails) { diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index 056e1f077..6390b8bc4 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -67,7 +67,7 @@ class SearchService * @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed. * @return array[int, Collection]; */ - public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20) + public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view') { $terms = $this->parseSearchString($searchString); $entityTypes = array_keys($this->entities); @@ -87,8 +87,8 @@ class SearchService if (!in_array($entityType, $entityTypes)) { continue; } - $search = $this->searchEntityTable($terms, $entityType, $page, $count); - $entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, true); + $search = $this->searchEntityTable($terms, $entityType, $page, $count, $action); + $entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, $action, true); if ($entityTotal > $page * $count) { $hasMore = true; } @@ -147,12 +147,13 @@ class SearchService * @param string $entityType * @param int $page * @param int $count + * @param string $action * @param bool $getCount Return the total count of the search * @return \Illuminate\Database\Eloquent\Collection|int|static[] */ - public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false) + public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $action = 'view', $getCount = false) { - $query = $this->buildEntitySearchQuery($terms, $entityType); + $query = $this->buildEntitySearchQuery($terms, $entityType, $action); if ($getCount) { return $query->count(); } @@ -165,9 +166,10 @@ class SearchService * Create a search query for an entity * @param array $terms * @param string $entityType + * @param string $action * @return \Illuminate\Database\Eloquent\Builder */ - protected function buildEntitySearchQuery($terms, $entityType = 'page') + protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view') { $entity = $this->getEntity($entityType); $entitySelect = $entity->newQuery(); @@ -212,7 +214,7 @@ class SearchService } } - return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view'); + return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action); } diff --git a/app/Services/ViewService.php b/app/Services/ViewService.php index ddcf2eb7e..cd869018c 100644 --- a/app/Services/ViewService.php +++ b/app/Services/ViewService.php @@ -51,11 +51,13 @@ class ViewService * @param int $count * @param int $page * @param bool|false|array $filterModel + * @param string $action - used for permission checking + * @return */ - public function getPopular($count = 10, $page = 0, $filterModel = false) + public function getPopular($count = 10, $page = 0, $filterModel = false, $action = 'view') { $skipCount = $count * $page; - $query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type') + $query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action) ->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count')) ->groupBy('viewable_id', 'viewable_type') ->orderBy('view_count', 'desc'); diff --git a/package-lock.json b/package-lock.json index c794a62da..30f58c000 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6825,11 +6825,6 @@ } } }, - "moment": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz", - "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ==" - }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", diff --git a/resources/assets/js/components/entity-selector.js b/resources/assets/js/components/entity-selector.js index 53358378a..5bd0d5497 100644 --- a/resources/assets/js/components/entity-selector.js +++ b/resources/assets/js/components/entity-selector.js @@ -7,7 +7,8 @@ class EntitySelector { this.lastClick = 0; let entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter'; - this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}`); + let entityPermission = elem.hasAttribute('entity-permission') ? elem.getAttribute('entity-permission') : 'view'; + this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}&permission=${encodeURIComponent(entityPermission)}`); this.input = elem.querySelector('[entity-selector-input]'); this.searchInput = elem.querySelector('[entity-selector-search]'); @@ -68,7 +69,6 @@ class EntitySelector { onClick(event) { let t = event.target; - console.log('click', t); if (t.matches('.entity-list-item *')) { event.preventDefault(); diff --git a/resources/lang/en/common.php b/resources/lang/en/common.php index 26f096327..c2744d906 100644 --- a/resources/lang/en/common.php +++ b/resources/lang/en/common.php @@ -31,6 +31,7 @@ return [ 'edit' => 'Edit', 'sort' => 'Sort', 'move' => 'Move', + 'copy' => 'Copy', 'reply' => 'Reply', 'delete' => 'Delete', 'search' => 'Search', diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index a4d3ae6e8..430655a87 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -166,6 +166,9 @@ return [ 'pages_not_in_chapter' => 'Page is not in a chapter', 'pages_move' => 'Move Page', 'pages_move_success' => 'Page moved to ":parentName"', + 'pages_copy' => 'Copy Page', + 'pages_copy_desination' => 'Copy Destination', + 'pages_copy_success' => 'Page successfully copied', 'pages_permissions' => 'Page Permissions', 'pages_permissions_success' => 'Page permissions updated', 'pages_revision' => 'Revision', diff --git a/resources/views/books/form.blade.php b/resources/views/books/form.blade.php index a47c26e26..bf94b5b07 100644 --- a/resources/views/books/form.blade.php +++ b/resources/views/books/form.blade.php @@ -30,9 +30,9 @@ -
+