diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 6ed9fc30c..4ed10d61e 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -158,13 +158,16 @@ class PageController extends Controller $this->checkOwnablePermission('page-view', $page); + $pageContent = $this->entityRepo->renderPage($page); $sidebarTree = $this->entityRepo->getBookChildren($page->book); - $pageNav = $this->entityRepo->getPageNav($page); + $pageNav = $this->entityRepo->getPageNav($pageContent); Views::add($page); $this->setPageTitle($page->getShortName()); - return view('pages/show', ['page' => $page, 'book' => $page->book, - 'current' => $page, 'sidebarTree' => $sidebarTree, 'pageNav' => $pageNav]); + return view('pages/show', [ + 'page' => $page,'book' => $page->book, + 'current' => $page, 'sidebarTree' => $sidebarTree, + 'pageNav' => $pageNav, 'pageContent' => $pageContent]); } /** @@ -430,6 +433,7 @@ class PageController extends Controller { $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $pdfContent = $this->exportService->pageToPdf($page); +// return $pdfContent; return response()->make($pdfContent, 200, [ 'Content-Type' => 'application/octet-stream', 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf' diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index 6eaf0169a..f1428735c 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -139,7 +139,7 @@ class EntityRepo */ public function getById($type, $id, $allowDrafts = false) { - return $this->entityQuery($type, $allowDrafts)->findOrFail($id); + return $this->entityQuery($type, $allowDrafts)->find($id); } /** @@ -796,6 +796,52 @@ class EntityRepo return $html; } + + /** + * Render the page for viewing, Parsing and performing features such as page transclusion. + * @param Page $page + * @return mixed|string + */ + public function renderPage(Page $page) + { + $content = $page->html; + $matches = []; + preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches); + if (count($matches[0]) === 0) return $content; + + foreach ($matches[1] as $index => $includeId) { + $splitInclude = explode('#', $includeId, 2); + $pageId = intval($splitInclude[0]); + if (is_nan($pageId)) continue; + + $page = $this->getById('page', $pageId); + if ($page === null) { + $content = str_replace($matches[0][$index], '', $content); + continue; + } + + if (count($splitInclude) === 1) { + $content = str_replace($matches[0][$index], $page->html, $content); + continue; + } + + $doc = new DOMDocument(); + $doc->loadHTML(mb_convert_encoding('
'.$page->html.'', 'HTML-ENTITIES', 'UTF-8')); + $matchingElem = $doc->getElementById($splitInclude[1]); + if ($matchingElem === null) { + $content = str_replace($matches[0][$index], '', $content); + continue; + } + $innerContent = ''; + foreach ($matchingElem->childNodes as $childNode) { + $innerContent .= $doc->saveHTML($childNode); + } + $content = str_replace($matches[0][$index], trim($innerContent), $content); + } + + return $content; + } + /** * Get a new draft page instance. * @param Book $book @@ -835,15 +881,15 @@ class EntityRepo /** * Parse the headers on the page to get a navigation menu - * @param Page $page + * @param String $pageContent * @return array */ - public function getPageNav(Page $page) + public function getPageNav($pageContent) { - if ($page->html == '') return []; + if ($pageContent == '') return []; libxml_use_internal_errors(true); $doc = new DOMDocument(); - $doc->loadHTML(mb_convert_encoding($page->html, 'HTML-ENTITIES', 'UTF-8')); + $doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8')); $xPath = new DOMXPath($doc); $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6"); diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php index 50ba75c17..880bc54ad 100644 --- a/app/Services/ExportService.php +++ b/app/Services/ExportService.php @@ -1,10 +1,22 @@ entityRepo = $entityRepo; + } + /** * Convert a page to a self-contained HTML file. * Includes required CSS & image content. Images are base64 encoded into the HTML. @@ -14,7 +26,7 @@ class ExportService public function pageToContainedHtml(Page $page) { $cssContent = file_get_contents(public_path('/css/export-styles.css')); - $pageHtml = view('pages/export', ['page' => $page, 'css' => $cssContent])->render(); + $pageHtml = view('pages/export', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render(); return $this->containHtml($pageHtml); } @@ -26,7 +38,8 @@ class ExportService public function pageToPdf(Page $page) { $cssContent = file_get_contents(public_path('/css/export-styles.css')); - $pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render(); + $pageHtml = view('pages/pdf', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render(); +// return $pageHtml; $useWKHTML = config('snappy.pdf.binary') !== false; $containedHtml = $this->containHtml($pageHtml); if ($useWKHTML) { @@ -59,9 +72,13 @@ class ExportService $pathString = $srcString; } if ($isLocal && !file_exists($pathString)) continue; - $imageContent = file_get_contents($pathString); - $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent); - $newImageString = str_replace($srcString, $imageEncoded, $oldImgString); + try { + $imageContent = file_get_contents($pathString); + $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent); + $newImageString = str_replace($srcString, $imageEncoded, $oldImgString); + } catch (\ErrorException $e) { + $newImageString = ''; + } $htmlContent = str_replace($oldImgString, $newImageString, $htmlContent); } } @@ -88,14 +105,14 @@ class ExportService /** * Converts the page contents into simple plain text. - * This method filters any bad looking content to - * provide a nice final output. + * This method filters any bad looking content to provide a nice final output. * @param Page $page * @return mixed */ public function pageToPlainText(Page $page) { - $text = $page->text; + $html = $this->entityRepo->renderPage($page); + $text = strip_tags($html); // Replace multiple spaces with single spaces $text = preg_replace('/\ {2,}/', ' ', $text); // Reduce multiple horrid whitespace characters. diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index 6363541ef..65fe0f33e 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -157,7 +157,7 @@ class PermissionService */ public function buildJointPermissionsForEntity(Entity $entity) { - $roles = $this->role->with('jointPermissions')->get(); + $roles = $this->role->get(); $entities = collect([$entity]); if ($entity->isA('book')) { @@ -177,7 +177,7 @@ class PermissionService */ public function buildJointPermissionsForEntities(Collection $entities) { - $roles = $this->role->with('jointPermissions')->get(); + $roles = $this->role->get(); $this->deleteManyJointPermissionsForEntities($entities); $this->createManyJointPermissions($entities, $roles); } @@ -564,6 +564,7 @@ class PermissionService }); }); }); + $this->clean(); return $q; } diff --git a/package.json b/package.json index ec5911b93..b0805c918 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "laravel-elixir": "^6.0.0-11", "laravel-elixir-browserify-official": "^0.1.3", "marked": "^0.3.5", - "moment": "^2.12.0", - "zeroclipboard": "^2.2.0" + "moment": "^2.12.0" + }, + "dependencies": { + "clipboard": "^1.5.16" } } diff --git a/public/ZeroClipboard.swf b/public/ZeroClipboard.swf deleted file mode 100644 index 8bad6a3e3..000000000 Binary files a/public/ZeroClipboard.swf and /dev/null differ diff --git a/resources/assets/js/pages/page-show.js b/resources/assets/js/pages/page-show.js index 4f8f6e0f1..0f45e1987 100644 --- a/resources/assets/js/pages/page-show.js +++ b/resources/assets/js/pages/page-show.js @@ -1,13 +1,16 @@ "use strict"; // Configure ZeroClipboard -import ZeroClipBoard from "zeroclipboard"; +import Clipboard from "clipboard"; export default window.setupPageShow = function (pageId) { // Set up pointer let $pointer = $('#pointer').detach(); + let pointerShowing = false; let $pointerInner = $pointer.children('div.pointer').first(); let isSelection = false; + let pointerModeLink = true; + let pointerSectionId = ''; // Select all contents on input click $pointer.on('click', 'input', function (e) { @@ -15,19 +18,34 @@ export default window.setupPageShow = function (pageId) { e.stopPropagation(); }); - // Set up copy-to-clipboard - ZeroClipBoard.config({ - swfPath: window.baseUrl('/ZeroClipboard.swf') + // Pointer mode toggle + $pointer.on('click', 'span.icon', event => { + let $icon = $(event.currentTarget); + pointerModeLink = !pointerModeLink; + $icon.html(pointerModeLink ? '' : ''); + updatePointerContent(); }); - new ZeroClipBoard($pointer.find('button').first()[0]); + + // Set up clipboard + let clipboard = new Clipboard('#pointer button'); // Hide pointer when clicking away - $(document.body).find('*').on('click focus', function (e) { - if (!isSelection) { - $pointer.detach(); - } + $(document.body).find('*').on('click focus', event => { + if (!pointerShowing || isSelection) return; + let target = $(event.target); + if (target.is('.zmdi') || $(event.target).closest('#pointer').length === 1) return; + + $pointer.detach(); + pointerShowing = false; }); + function updatePointerContent() { + let inputText = pointerModeLink ? window.baseUrl(`/link/${pageId}#${pointerSectionId}`) : `{{@${pageId}#${pointerSectionId}}}`; + if (pointerModeLink && inputText.indexOf('http') !== 0) inputText = window.location.protocol + "//" + window.location.host + inputText; + + $pointer.find('input').val(inputText); + } + // Show pointer when selecting a single block of tagged content $('.page-content [id^="bkmrk"]').on('mouseup keyup', function (e) { e.stopPropagation(); @@ -36,12 +54,12 @@ export default window.setupPageShow = function (pageId) { // Show pointer and set link let $elem = $(this); - let link = window.baseUrl('/link/' + pageId + '#' + $elem.attr('id')); - if (link.indexOf('http') !== 0) link = window.location.protocol + "//" + window.location.host + link; - $pointer.find('input').val(link); - $pointer.find('button').first().attr('data-clipboard-text', link); + pointerSectionId = $elem.attr('id'); + updatePointerContent(); + $elem.before($pointer); $pointer.show(); + pointerShowing = true; // Set pointer to sit near mouse-up position let pointerLeftOffset = (e.pageX - $elem.offset().left - ($pointerInner.width() / 2)); diff --git a/resources/assets/sass/_blocks.scss b/resources/assets/sass/_blocks.scss index 7eb595d36..a2023aa37 100644 --- a/resources/assets/sass/_blocks.scss +++ b/resources/assets/sass/_blocks.scss @@ -136,9 +136,6 @@ background-color: #EEE; padding: $-s; display: block; - > * { - display: inline-block; - } &:before { font-family: 'Material-Design-Iconic-Font'; padding-right: $-s; diff --git a/resources/assets/sass/_buttons.scss b/resources/assets/sass/_buttons.scss index 5de889673..791a5bb72 100644 --- a/resources/assets/sass/_buttons.scss +++ b/resources/assets/sass/_buttons.scss @@ -108,5 +108,4 @@ $button-border-radius: 2px; cursor: default; box-shadow: none; } -} - +} \ No newline at end of file diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss index c8fd8bcfa..5328057d9 100644 --- a/resources/assets/sass/_components.scss +++ b/resources/assets/sass/_components.scss @@ -70,9 +70,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { #entity-selector-wrap .popup-body .form-group { margin: 0; } -//body.ie #entity-selector-wrap .popup-body .form-group { -// min-height: 60vh; -//} .image-manager-body { min-height: 70vh; diff --git a/resources/assets/sass/_pages.scss b/resources/assets/sass/_pages.scss index 0052a3319..e5334c69c 100755 --- a/resources/assets/sass/_pages.scss +++ b/resources/assets/sass/_pages.scss @@ -138,6 +138,10 @@ font-size: 18px; padding-top: 4px; } + span.icon { + cursor: pointer; + user-select: none; + } .button { line-height: 1; margin: 0 0 0 -4px; diff --git a/resources/assets/sass/export-styles.scss b/resources/assets/sass/export-styles.scss index 60450f3e2..7e1ab4e9e 100644 --- a/resources/assets/sass/export-styles.scss +++ b/resources/assets/sass/export-styles.scss @@ -1,4 +1,4 @@ -//@import "reset"; +@import "reset"; @import "variables"; @import "mixins"; @import "html"; diff --git a/resources/views/pages/page-display.blade.php b/resources/views/pages/page-display.blade.php index fb6ca3045..6eb927687 100644 --- a/resources/views/pages/page-display.blade.php +++ b/resources/views/pages/page-display.blade.php @@ -7,6 +7,6 @@ @if (isset($diff) && $diff) {!! $diff !!} @else - {!! $page->html !!} + {!! isset($pageContent) ? $pageContent : $page->html !!} @endif \ No newline at end of file diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index a734b1b95..fd6cebf41 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -53,9 +53,9 @@Hello, This is a test
This is a second block of content
"; + $secondPage->save(); + + $this->asAdmin()->visit($page->getUrl()) + ->dontSee('Hello, This is a test'); + + $originalHtml = $page->html; + $page->html .= "{{@{$secondPage->id}}}"; + $page->save(); + + $this->asAdmin()->visit($page->getUrl()) + ->see('Hello, This is a test') + ->see('This is a second block of content'); + + $page->html = $originalHtml . " Well {{@{$secondPage->id}#section2}}"; + $page->save(); + + $this->asAdmin()->visit($page->getUrl()) + ->dontSee('Hello, This is a test') + ->see('Well This is a second block of content'); + } + +} diff --git a/tests/Entity/TagTests.php b/tests/Entity/TagTest.php similarity index 59% rename from tests/Entity/TagTests.php rename to tests/Entity/TagTest.php index 0520e1a00..2d42ffa4b 100644 --- a/tests/Entity/TagTests.php +++ b/tests/Entity/TagTest.php @@ -4,7 +4,7 @@ use BookStack\Tag; use BookStack\Page; use BookStack\Services\PermissionService; -class TagTests extends \TestCase +class TagTest extends \TestCase { protected $defaultTagCount = 20; @@ -86,61 +86,16 @@ class TagTests extends \TestCase $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'color'])); $page = $this->getPageWithTags($attrs); - $this->asAdmin()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']); - $this->asEditor()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']); + $this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country']); + $this->asEditor()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country']); // Set restricted permission the page $page->restricted = true; $page->save(); $permissionService->buildJointPermissionsForEntity($page); - $this->asAdmin()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']); - $this->asEditor()->get('/ajax/tags/suggest?search=co')->seeJsonEquals([]); - } - - public function test_entity_tag_updating() - { - $page = $this->getPageWithTags(); - - $testJsonData = [ - ['name' => 'color', 'value' => 'red'], - ['name' => 'color', 'value' => ' blue '], - ['name' => 'city', 'value' => 'London '], - ['name' => 'country', 'value' => ' England'], - ]; - $testResponseJsonData = [ - ['name' => 'color', 'value' => 'red'], - ['name' => 'color', 'value' => 'blue'], - ['name' => 'city', 'value' => 'London'], - ['name' => 'country', 'value' => 'England'], - ]; - - // Do update request - $this->asAdmin()->json("POST", "/ajax/tags/update/page/" . $page->id, ['tags' => $testJsonData]); - $updateData = json_decode($this->response->getContent()); - // Check data is correct - $testDataCorrect = true; - foreach ($updateData->tags as $data) { - $testItem = ['name' => $data->name, 'value' => $data->value]; - if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false; - } - $testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s"; - $this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($updateData))); - $this->assertTrue(isset($updateData->message), "No message returned in tag update response"); - - // Do get request - $this->asAdmin()->get("/ajax/tags/get/page/" . $page->id); - $getResponseData = json_decode($this->response->getContent()); - // Check counts - $this->assertTrue(count($getResponseData) === count($testJsonData), "The received tag count is incorrect"); - // Check data is correct - $testDataCorrect = true; - foreach ($getResponseData as $data) { - $testItem = ['name' => $data->name, 'value' => $data->value]; - if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false; - } - $testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s"; - $this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($getResponseData))); + $this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country']); + $this->asEditor()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals([]); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index d3620eae0..4f3df4b90 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -60,7 +60,7 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase */ public function asEditor() { - if($this->editor === null) { + if ($this->editor === null) { $this->editor = $this->getEditor(); } return $this->actingAs($this->editor);