X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/0e0a17cc30fc4be84b09ab1e30d7689839ff1bae..refs/pull/1688/head:/app/Entities/Repos/PageRepo.php diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index 3558b29b3..0e0585a85 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -7,6 +7,7 @@ use BookStack\Entities\Page; use BookStack\Entities\PageRevision; use Carbon\Carbon; use DOMDocument; +use DOMElement; use DOMXPath; class PageRepo extends EntityRepo @@ -19,9 +20,9 @@ class PageRepo extends EntityRepo * @return Page * @throws \BookStack\Exceptions\NotFoundException */ - public function getPageBySlug(string $pageSlug, string $bookSlug) + public function getBySlug(string $pageSlug, string $bookSlug) { - return $this->getBySlug('page', $pageSlug, $bookSlug); + return $this->getEntityBySlug('page', $pageSlug, $bookSlug); } /** @@ -58,34 +59,40 @@ class PageRepo extends EntityRepo $oldHtml = $page->html; $oldName = $page->name; - // Prevent slug being updated if no name change - if ($page->name !== $input['name']) { - $page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id); - } - // Save page tags if present if (isset($input['tags'])) { $this->tagRepo->saveTagsToEntity($page, $input['tags']); } + if (isset($input['template']) && userCan('templates-manage')) { + $page->template = ($input['template'] === 'true'); + } + // Update with new details $userId = user()->id; $page->fill($input); $page->html = $this->formatHtml($input['html']); $page->text = $this->pageToPlainText($page); + $page->updated_by = $userId; + $page->revision_count++; + if (setting('app-editor') !== 'markdown') { $page->markdown = ''; } - $page->updated_by = $userId; - $page->revision_count++; + + if ($page->isDirty('name')) { + $page->refreshSlug(); + } + $page->save(); // Remove all update drafts for this user & page. $this->userUpdatePageDraftsQuery($page, $userId)->delete(); // Save a revision after updating - if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) { - $this->savePageRevision($page, $input['summary']); + $summary = $input['summary'] ?? null; + if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $summary !== null) { + $this->savePageRevision($page, $summary); } $this->searchService->indexEntity($page); @@ -129,8 +136,7 @@ class PageRepo extends EntityRepo } /** - * Formats a page's html to be tagged correctly - * within the system. + * Formats a page's html to be tagged correctly within the system. * @param string $htmlText * @return string */ @@ -139,6 +145,7 @@ class PageRepo extends EntityRepo if ($htmlText == '') { return $htmlText; } + libxml_use_internal_errors(true); $doc = new DOMDocument(); $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8')); @@ -147,37 +154,17 @@ class PageRepo extends EntityRepo $body = $container->childNodes->item(0); $childNodes = $body->childNodes; - // Ensure no duplicate ids are used - $idArray = []; - + // Set ids on top-level nodes + $idMap = []; foreach ($childNodes as $index => $childNode) { - /** @var \DOMElement $childNode */ - if (get_class($childNode) !== 'DOMElement') { - continue; - } - - // Overwrite id if not a BookStack custom id - if ($childNode->hasAttribute('id')) { - $id = $childNode->getAttribute('id'); - if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) { - $idArray[] = $id; - continue; - }; - } - - // Create an unique id for the element - // Uses the content as a basis to ensure output is the same every time - // the same content is passed through. - $contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20); - $newId = urlencode($contentId); - $loopIndex = 0; - while (in_array($newId, $idArray)) { - $newId = urlencode($contentId . '-' . $loopIndex); - $loopIndex++; - } + $this->setUniqueId($childNode, $idMap); + } - $childNode->setAttribute('id', $newId); - $idArray[] = $newId; + // Ensure no duplicate ids within child items + $xPath = new DOMXPath($doc); + $idElems = $xPath->query('//body//*//*[@id]'); + foreach ($idElems as $domElem) { + $this->setUniqueId($domElem, $idMap); } // Generate inner html as a string @@ -189,6 +176,41 @@ class PageRepo extends EntityRepo return $html; } + /** + * Set a unique id on the given DOMElement. + * A map for existing ID's should be passed in to check for current existence. + * @param DOMElement $element + * @param array $idMap + */ + protected function setUniqueId($element, array &$idMap) + { + if (get_class($element) !== 'DOMElement') { + return; + } + + // Overwrite id if not a BookStack custom id + $existingId = $element->getAttribute('id'); + if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) { + $idMap[$existingId] = true; + return; + } + + // Create an unique id for the element + // Uses the content as a basis to ensure output is the same every time + // the same content is passed through. + $contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20); + $newId = urlencode($contentId); + $loopIndex = 0; + + while (isset($idMap[$newId])) { + $newId = urlencode($contentId . '-' . $loopIndex); + $loopIndex++; + } + + $element->setAttribute('id', $newId); + $idMap[$newId] = true; + } + /** * Get the plain text version of a page's content. * @param \BookStack\Entities\Page $page @@ -220,8 +242,7 @@ class PageRepo extends EntityRepo } $book->pages()->save($page); - $page = $this->entityProvider->page->find($page->id); - $this->permissionService->buildJointPermissionsForEntity($page); + $page->refresh()->rebuildPermissions(); return $page; } @@ -284,12 +305,15 @@ class PageRepo extends EntityRepo $this->tagRepo->saveTagsToEntity($draftPage, $input['tags']); } - $draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id); + if (isset($input['template']) && userCan('templates-manage')) { + $draftPage->template = ($input['template'] === 'true'); + } + $draftPage->html = $this->formatHtml($input['html']); $draftPage->text = $this->pageToPlainText($draftPage); $draftPage->draft = false; $draftPage->revision_count = 1; - + $draftPage->refreshSlug(); $draftPage->save(); $this->savePageRevision($draftPage, trans('entities.pages_initial_revision')); $this->searchService->indexEntity($draftPage); @@ -406,25 +430,27 @@ class PageRepo extends EntityRepo return []; } - $tree = collect([]); - foreach ($headers as $header) { - $text = $header->nodeValue; - $tree->push([ + $tree = collect($headers)->map(function ($header) { + $text = trim(str_replace("\xc2\xa0", '', $header->nodeValue)); + $text = mb_substr($text, 0, 100); + + return [ 'nodeName' => strtolower($header->nodeName), 'level' => intval(str_replace('h', '', $header->nodeName)), 'link' => '#' . $header->getAttribute('id'), - 'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text - ]); - } + 'text' => $text, + ]; + })->filter(function ($header) { + return mb_strlen($header['text']) > 0; + }); + + // Shift headers if only smaller headers have been used + $levelChange = ($tree->pluck('level')->min() - 1); + $tree = $tree->map(function ($header) use ($levelChange) { + $header['level'] -= ($levelChange); + return $header; + }); - // Normalise headers if only smaller headers have been used - if (count($tree) > 0) { - $minLevel = $tree->pluck('level')->min(); - $tree = $tree->map(function ($header) use ($minLevel) { - $header['level'] -= ($minLevel - 2); - return $header; - }); - } return $tree->toArray(); } @@ -440,12 +466,14 @@ class PageRepo extends EntityRepo { $page->revision_count++; $this->savePageRevision($page); + $revision = $page->revisions()->where('id', '=', $revisionId)->first(); $page->fill($revision->toArray()); - $page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id); $page->text = $this->pageToPlainText($page); $page->updated_by = user()->id; + $page->refreshSlug(); $page->save(); + $this->searchService->indexEntity($page); return $page; } @@ -454,18 +482,19 @@ class PageRepo extends EntityRepo * Change the page's parent to the given entity. * @param Page $page * @param Entity $parent - * @throws \Throwable */ public function changePageParent(Page $page, Entity $parent) { $book = $parent->isA('book') ? $parent : $parent->book; $page->chapter_id = $parent->isA('chapter') ? $parent->id : 0; $page->save(); + if ($page->book->id !== $book->id) { - $page = $this->changeBook('page', $book->id, $page); + $page = $this->changeBook($page, $book->id); } + $page->load('book'); - $this->permissionService->buildJointPermissionsForEntity($book); + $book->rebuildPermissions(); } /** @@ -505,4 +534,29 @@ class PageRepo extends EntityRepo return $this->publishPageDraft($copyPage, $pageData); } -} \ No newline at end of file + + /** + * Get pages that have been marked as templates. + * @param int $count + * @param int $page + * @param string $search + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function getPageTemplates(int $count = 10, int $page = 1, string $search = '') + { + $query = $this->entityQuery('page') + ->where('template', '=', true) + ->orderBy('name', 'asc') + ->skip(($page - 1) * $count) + ->take($count); + + if ($search) { + $query->where('name', 'like', '%' . $search . '%'); + } + + $paginator = $query->paginate($count, ['*'], 'page', $page); + $paginator->withPath('/templates'); + + return $paginator; + } +}