From 0dc1f0b07fbedb0df78a659b2ce65b3fa5dc2259 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 24 Dec 2017 14:28:35 +0000 Subject: [PATCH 1/9] Started draw.io integration --- resources/assets/js/pages/page-form.js | 126 ++++++++++++++++++++++++- resources/assets/sass/styles.scss | 12 +++ 2 files changed, 134 insertions(+), 4 deletions(-) diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 904403fc1..e47c575f3 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -220,6 +220,124 @@ function codePlugin() { } codePlugin(); +function drawIoPlugin() { + + const drawIoUrl = 'https://www.draw.io/?embed=1&ui=atlas&spin=1&proto=json'; + let iframe = null; + let pageEditor = null; + + function isDrawing(node) { + return node.hasAttribute('drawio-diagram'); + } + + function showDrawingEditor(mceEditor) { + pageEditor = mceEditor; + iframe = document.createElement('iframe'); + iframe.setAttribute('frameborder', '0'); + window.addEventListener('message', drawReceive); + iframe.setAttribute('src', drawIoUrl); + iframe.setAttribute('class', 'fullscreen'); + document.body.appendChild(iframe); + } + + function drawReceive(event) { + if (!event.data || event.data.length < 1) return; + let message = JSON.parse(event.data); + console.log(message); + if (message.event === 'init') { + drawEventInit(); + } else if (message.event === 'exit') { + drawEventClose(); + } else if (message.event === 'save') { + drawEventSave(message); + } else if (message.event === 'export') { + drawEventExport(message); + } + } + + function updateContent(svg) { + let svgWrap = document.createElement('div'); + svgWrap.setAttribute('drawio-diagram', svg); + svgWrap.setAttribute('contenteditable', 'false'); + pageEditor.insertContent(svgWrap.outerHTML); + } + + function b64DecodeUnicode(str) { + str = str.split(';base64,')[1]; + // Going backwards: from bytestream, to percent-encoding, to original string. + return decodeURIComponent(atob(str).split('').map(function(c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + } + + function drawEventExport(message) { + updateContent(message.data); + } + + function drawEventSave(message) { + drawPostMessage({action: 'export', format: 'svg', xml: message.xml, spin: 'Updating drawing'}); + } + + function drawEventInit() { + drawPostMessage({action: 'load', autosave: 1, xml: ''}); + } + + function drawEventClose() { + window.removeEventListener('message', drawReceive); + if (iframe) document.body.removeChild(iframe); + } + + function drawPostMessage(data) { + iframe.contentWindow.postMessage(JSON.stringify(data), '*'); + } + + window.tinymce.PluginManager.add('drawio', function(editor, url) { + + let $ = editor.$; + + editor.addCommand('drawio', () => { + showDrawingEditor(editor); + }); + + editor.addButton('drawio', { + text: 'Drawing', + icon: false, + cmd: 'drawio' + }); + + editor.on('dblclick', event => { + let selectedNode = editor.selection.getNode(); + if (!isDrawing(selectedNode)) return; + showDrawingEditor(editor); + }); + + editor.on('PreProcess', function (e) { + $('div[drawio-diagram]', e.node). + each((index, elem) => { + let $elem = $(elem); + let svgData = b64DecodeUnicode($elem.attr('drawio-diagram')); + $elem.html(svgData); + }); + }); + + editor.on('SetContent', function () { + + let drawings = $('body > div[drawio-diagram]'); + + if (!drawings.length) return; + editor.undoManager.transact(function () { + drawings.each((index, elem) => { + let svgContent = b64DecodeUnicode(elem.getAttribute('drawio-diagram')); + elem.setAttribute('contenteditable', 'false'); + elem.innerHTML = svgContent; + }); + }); + }); + + }); +} +drawIoPlugin(); + window.tinymce.PluginManager.add('customhr', function (editor) { editor.addCommand('InsertHorizontalRule', function () { let hrElem = document.createElement('hr'); @@ -259,12 +377,12 @@ module.exports = { statusbar: false, menubar: false, paste_data_images: false, - extended_valid_elements: 'pre[*]', + extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]', automatic_uploads: false, - valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre]", - plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor", + valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[svg],+svg", + plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor drawio", imagetools_toolbar: 'imageoptions', - toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen", + toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen drawio", content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}", style_formats: [ {title: "Header Large", format: "h2"}, diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index 6a80237c5..2cb72bd75 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -231,4 +231,16 @@ $btt-size: 40px; input { width: 100%; } +} + +.fullscreen { + border:0; + position:fixed; + top:0; + left:0; + right:0; + bottom:0; + width:100%; + height:100%; + z-index: 150; } \ No newline at end of file From 920964a5612494bb1f25223b7c1de1379096c848 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 30 Dec 2017 15:24:03 +0000 Subject: [PATCH 2/9] Enabled system-storage of drawings made via draw.io --- app/Http/Controllers/ImageController.php | 49 +++++++++++++++- app/Repos/ImageRepo.php | 36 ++++++++++++ app/Services/ImageService.php | 31 ++++++++++ resources/assets/js/pages/page-form.js | 73 +++++++++++++----------- routes/web.php | 4 +- 5 files changed, 159 insertions(+), 34 deletions(-) diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php index d40f88255..1b064de01 100644 --- a/app/Http/Controllers/ImageController.php +++ b/app/Http/Controllers/ImageController.php @@ -96,6 +96,7 @@ class ImageController extends Controller * @param string $type * @param Request $request * @return \Illuminate\Http\JsonResponse + * @throws \Exception */ public function uploadByType($type, Request $request) { @@ -103,11 +104,12 @@ class ImageController extends Controller $this->validate($request, [ 'file' => 'is_image' ]); + // TODO - Restrict & validate types $imageUpload = $request->file('file'); try { - $uploadedTo = $request->filled('uploaded_to') ? $request->get('uploaded_to') : 0; + $uploadedTo = $request->get('uploaded_to', 0); $image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo); } catch (ImageUploadException $e) { return response($e->getMessage(), 500); @@ -116,6 +118,47 @@ class ImageController extends Controller return response()->json($image); } + /** + * Upload a drawing to the system. + * @param Request $request + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response + */ + public function uploadDrawing(Request $request) + { + $this->validate($request, [ + 'image' => 'required|string', + 'uploaded_to' => 'required|integer' + ]); + $this->checkPermission('image-create-all'); + $imageBase64Data = $request->get('image'); + + try { + $uploadedTo = $request->get('uploaded_to', 0); + $image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo); + } catch (ImageUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($image); + } + + /** + * Get the content of an image based64 encoded. + * @param $id + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function getBase64Image($id) + { + $image = $this->imageRepo->getById($id); + $imageData = $this->imageRepo->getImageData($image); + if ($imageData === null) { + return $this->jsonError("Image data could not be found"); + } + return response()->json([ + 'content' => base64_encode($imageData) + ]); + } + /** * Generate a sized thumbnail for an image. * @param $id @@ -123,6 +166,8 @@ class ImageController extends Controller * @param $height * @param $crop * @return \Illuminate\Http\JsonResponse + * @throws ImageUploadException + * @throws \Exception */ public function getThumbnail($id, $width, $height, $crop) { @@ -137,6 +182,8 @@ class ImageController extends Controller * @param integer $imageId * @param Request $request * @return \Illuminate\Http\JsonResponse + * @throws ImageUploadException + * @throws \Exception */ public function update($imageId, Request $request) { diff --git a/app/Repos/ImageRepo.php b/app/Repos/ImageRepo.php index 8ddde7b0f..734254b3d 100644 --- a/app/Repos/ImageRepo.php +++ b/app/Repos/ImageRepo.php @@ -132,6 +132,8 @@ class ImageRepo * @param string $type * @param int $uploadedTo * @return Image + * @throws \BookStack\Exceptions\ImageUploadException + * @throws \Exception */ public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0) { @@ -140,11 +142,27 @@ class ImageRepo return $image; } + /** + * Save a drawing the the database; + * @param string $base64Uri + * @param int $uploadedTo + * @return Image + * @throws \BookStack\Exceptions\ImageUploadException + */ + public function saveDrawing(string $base64Uri, int $uploadedTo) + { + $name = 'Drawing-' . user()->getShortName(40) . '-' . strval(time()) . '.png'; + $image = $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawing', $uploadedTo); + return $image; + } + /** * Update the details of an image via an array of properties. * @param Image $image * @param array $updateDetails * @return Image + * @throws \BookStack\Exceptions\ImageUploadException + * @throws \Exception */ public function updateImageDetails(Image $image, $updateDetails) { @@ -170,6 +188,8 @@ class ImageRepo /** * Load thumbnails onto an image object. * @param Image $image + * @throws \BookStack\Exceptions\ImageUploadException + * @throws \Exception */ private function loadThumbs(Image $image) { @@ -189,6 +209,8 @@ class ImageRepo * @param int $height * @param bool $keepRatio * @return string + * @throws \BookStack\Exceptions\ImageUploadException + * @throws \Exception */ public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) { @@ -200,5 +222,19 @@ class ImageRepo } } + /** + * Get the raw image data from an Image. + * @param Image $image + * @return null|string + */ + public function getImageData(Image $image) + { + try { + return $this->imageService->getImageData($image); + } catch (\Exception $exception) { + return null; + } + } + } \ No newline at end of file diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php index e34b3fb2b..82d1acef7 100644 --- a/app/Services/ImageService.php +++ b/app/Services/ImageService.php @@ -46,6 +46,24 @@ class ImageService extends UploadService return $this->saveNew($imageName, $imageData, $type, $uploadedTo); } + /** + * Save a new image from a uri-encoded base64 string of data. + * @param string $base64Uri + * @param string $name + * @param string $type + * @param int $uploadedTo + * @return Image + * @throws ImageUploadException + */ + public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, $uploadedTo = 0) + { + $splitData = explode(';base64,', $base64Uri); + if (count($splitData) < 2) { + throw new ImageUploadException("Invalid base64 image data provided"); + } + $data = base64_decode($splitData[1]); + return $this->saveNew($name, $data, $type, $uploadedTo); + } /** * Gets an image from url and saves it to the database. @@ -183,6 +201,19 @@ class ImageService extends UploadService return $this->getPublicUrl($thumbFilePath); } + /** + * Get the raw data content from an image. + * @param Image $image + * @return string + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function getImageData(Image $image) + { + $imagePath = $this->getPath($image); + $storage = $this->getStorage(); + return $storage->get($imagePath); + } + /** * Destroys an Image object along with its files and thumbnails. * @param Image $image diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index e47c575f3..b1b9680c7 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -47,7 +47,7 @@ function uploadImageFile(file) { let formData = new FormData(); formData.append('file', file, remoteFilename); - return window.$http.post('/images/gallery/upload', formData).then(resp => (resp.data)); + return window.$http.post(window.baseUrl('/images/gallery/upload'), formData).then(resp => (resp.data)); } function registerEditorShortcuts(editor) { @@ -225,25 +225,27 @@ function drawIoPlugin() { const drawIoUrl = 'https://www.draw.io/?embed=1&ui=atlas&spin=1&proto=json'; let iframe = null; let pageEditor = null; + let currentNode = null; function isDrawing(node) { return node.hasAttribute('drawio-diagram'); } - function showDrawingEditor(mceEditor) { + function showDrawingEditor(mceEditor, selectedNode = null) { pageEditor = mceEditor; + currentNode = selectedNode; iframe = document.createElement('iframe'); iframe.setAttribute('frameborder', '0'); window.addEventListener('message', drawReceive); iframe.setAttribute('src', drawIoUrl); iframe.setAttribute('class', 'fullscreen'); + iframe.style.backgroundColor = '#FFFFFF'; document.body.appendChild(iframe); } function drawReceive(event) { if (!event.data || event.data.length < 1) return; let message = JSON.parse(event.data); - console.log(message); if (message.event === 'init') { drawEventInit(); } else if (message.event === 'exit') { @@ -255,19 +257,28 @@ function drawIoPlugin() { } } - function updateContent(svg) { - let svgWrap = document.createElement('div'); - svgWrap.setAttribute('drawio-diagram', svg); - svgWrap.setAttribute('contenteditable', 'false'); - pageEditor.insertContent(svgWrap.outerHTML); - } + function updateContent(pngData) { + let id = "image-" + Math.random().toString(16).slice(2); + let loadingImage = window.baseUrl('/loading.gif'); + let data = { + image: pngData, + uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id')) + }; - function b64DecodeUnicode(str) { - str = str.split(';base64,')[1]; - // Going backwards: from bytestream, to percent-encoding, to original string. - return decodeURIComponent(atob(str).split('').map(function(c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }).join('')); + // TODO - Handle updating an existing image + + setTimeout(() => { + pageEditor.insertContent(`
`); + drawEventClose(); + window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => { + pageEditor.dom.setAttrib(id, 'src', resp.data.url); + pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', resp.data.id); + }).catch(err => { + pageEditor.dom.remove(id); + window.$events.emit('error', trans('errors.image_upload_error')); + console.log(err); + }); + }, 5); } function drawEventExport(message) { @@ -275,11 +286,21 @@ function drawIoPlugin() { } function drawEventSave(message) { - drawPostMessage({action: 'export', format: 'svg', xml: message.xml, spin: 'Updating drawing'}); + drawPostMessage({action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing'}); } function drawEventInit() { - drawPostMessage({action: 'load', autosave: 1, xml: ''}); + if (!currentNode) { + drawPostMessage({action: 'load', autosave: 1, xml: ''}); + return; + } + + let imgElem = currentNode.querySelector('img'); + let drawingId = currentNode.getAttribute('drawio-diagram'); + $http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { + let xml = `data:image/png;base64,${resp.data.content}`; + drawPostMessage({action: 'load', autosave: 1, xml}); + }); } function drawEventClose() { @@ -308,28 +329,16 @@ function drawIoPlugin() { editor.on('dblclick', event => { let selectedNode = editor.selection.getNode(); if (!isDrawing(selectedNode)) return; - showDrawingEditor(editor); - }); - - editor.on('PreProcess', function (e) { - $('div[drawio-diagram]', e.node). - each((index, elem) => { - let $elem = $(elem); - let svgData = b64DecodeUnicode($elem.attr('drawio-diagram')); - $elem.html(svgData); - }); + showDrawingEditor(editor, selectedNode); }); editor.on('SetContent', function () { - let drawings = $('body > div[drawio-diagram]'); - if (!drawings.length) return; + editor.undoManager.transact(function () { drawings.each((index, elem) => { - let svgContent = b64DecodeUnicode(elem.getAttribute('drawio-diagram')); elem.setAttribute('contenteditable', 'false'); - elem.innerHTML = svgContent; }); }); }); @@ -379,7 +388,7 @@ module.exports = { paste_data_images: false, extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]', automatic_uploads: false, - valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[svg],+svg", + valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]", plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor drawio", imagetools_toolbar: 'imageoptions', toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen drawio", diff --git a/routes/web.php b/routes/web.php index f5e59f109..3d4db7f12 100644 --- a/routes/web.php +++ b/routes/web.php @@ -86,13 +86,15 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/user/all/{page}', 'ImageController@getAllForUserType'); // Standard get, update and deletion for all types Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail'); + Route::get('/base64/{id}', 'ImageController@getBase64Image'); Route::put('/update/{imageId}', 'ImageController@update'); + Route::post('/drawing/upload', 'ImageController@uploadDrawing'); Route::post('/{type}/upload', 'ImageController@uploadByType'); Route::get('/{type}/all', 'ImageController@getAllByType'); Route::get('/{type}/all/{page}', 'ImageController@getAllByType'); Route::get('/{type}/search/{page}', 'ImageController@searchByType'); Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered'); - Route::delete('/{imageId}', 'ImageController@destroy'); + Route::delete('/{id}', 'ImageController@destroy'); }); // Attachments routes From 1bfd77e7a15cd52b2ccfa221ea90019438c2fbc1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 20 Jan 2018 14:01:35 +0000 Subject: [PATCH 3/9] Added drawing update ability --- app/Http/Controllers/ImageController.php | 26 ++++++++++++++++++++++++ app/Repos/ImageRepo.php | 12 +++++++++++ app/Services/ImageService.php | 25 +++++++++++++++++++++++ resources/assets/js/pages/page-form.js | 15 +++++++++++++- routes/web.php | 1 + 5 files changed, 78 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php index 1b064de01..81e300a68 100644 --- a/app/Http/Controllers/ImageController.php +++ b/app/Http/Controllers/ImageController.php @@ -142,6 +142,32 @@ class ImageController extends Controller return response()->json($image); } + /** + * Replace the data content of a drawing. + * @param string $id + * @param Request $request + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response + */ + public function replaceDrawing(string $id, Request $request) + { + $this->validate($request, [ + 'image' => 'required|string' + ]); + $this->checkPermission('image-create-all'); + + $imageBase64Data = $request->get('image'); + $image = $this->imageRepo->getById($id); + $this->checkOwnablePermission('image-update', $image); + + try { + $image = $this->imageRepo->replaceDrawingContent($image, $imageBase64Data); + } catch (ImageUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($image); + } + /** * Get the content of an image based64 encoded. * @param $id diff --git a/app/Repos/ImageRepo.php b/app/Repos/ImageRepo.php index 734254b3d..492834446 100644 --- a/app/Repos/ImageRepo.php +++ b/app/Repos/ImageRepo.php @@ -156,6 +156,18 @@ class ImageRepo return $image; } + /** + * Replace the image content of a drawing. + * @param Image $image + * @param string $base64Uri + * @return Image + * @throws \BookStack\Exceptions\ImageUploadException + */ + public function replaceDrawingContent(Image $image, string $base64Uri) + { + return $this->imageService->replaceImageDataFromBase64Uri($image, $base64Uri); + } + /** * Update the details of an image via an array of properties. * @param Image $image diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php index 82d1acef7..1da68ddf1 100644 --- a/app/Services/ImageService.php +++ b/app/Services/ImageService.php @@ -65,6 +65,31 @@ class ImageService extends UploadService return $this->saveNew($name, $data, $type, $uploadedTo); } + /** + * Replace the data for an image via a Base64 encoded string. + * @param Image $image + * @param string $base64Uri + * @return Image + * @throws ImageUploadException + */ + public function replaceImageDataFromBase64Uri(Image $image, string $base64Uri) + { + $splitData = explode(';base64,', $base64Uri); + if (count($splitData) < 2) { + throw new ImageUploadException("Invalid base64 image data provided"); + } + $data = base64_decode($splitData[1]); + $storage = $this->getStorage(); + + try { + $storage->put($image->path, $data); + } catch (Exception $e) { + throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $image->path])); + } + + return $image; + } + /** * Gets an image from url and saves it to the database. * @param $url diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index b1b9680c7..8e70d2db5 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -265,7 +265,20 @@ function drawIoPlugin() { uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id')) }; - // TODO - Handle updating an existing image + // Handle updating an existing image + if (currentNode) { + console.log(currentNode); + drawEventClose(); + let imgElem = currentNode.querySelector('img'); + let drawingId = currentNode.getAttribute('drawio-diagram'); + window.$http.put(window.baseUrl(`/images/drawing/upload/${drawingId}`), data).then(resp => { + pageEditor.dom.setAttrib(imgElem, 'src', `${resp.data.url}?updated=${Date.now()}`); + }).catch(err => { + window.$events.emit('error', trans('errors.image_upload_error')); + console.log(err); + }); + return; + } setTimeout(() => { pageEditor.insertContent(`
`); diff --git a/routes/web.php b/routes/web.php index 3d4db7f12..266e297f3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -89,6 +89,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/base64/{id}', 'ImageController@getBase64Image'); Route::put('/update/{imageId}', 'ImageController@update'); Route::post('/drawing/upload', 'ImageController@uploadDrawing'); + Route::put('/drawing/upload/{id}', 'ImageController@replaceDrawing'); Route::post('/{type}/upload', 'ImageController@uploadByType'); Route::get('/{type}/all', 'ImageController@getAllByType'); Route::get('/{type}/all/{page}', 'ImageController@getAllByType'); From 56264551e7a616c363ce817513d216f967abba43 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 20 Jan 2018 15:00:54 +0000 Subject: [PATCH 4/9] Added drawing icon and made drawio disablable --- app/Repos/ImageRepo.php | 2 +- config/services.php | 5 ++ public/system_images/drawing.svg | 107 +++++++++++++++++++++++++ resources/assets/js/pages/page-form.js | 17 ++-- resources/views/pages/form.blade.php | 8 +- 5 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 public/system_images/drawing.svg diff --git a/app/Repos/ImageRepo.php b/app/Repos/ImageRepo.php index 3918d5f67..97839c27f 100644 --- a/app/Repos/ImageRepo.php +++ b/app/Repos/ImageRepo.php @@ -152,7 +152,7 @@ class ImageRepo public function saveDrawing(string $base64Uri, int $uploadedTo) { $name = 'Drawing-' . user()->getShortName(40) . '-' . strval(time()) . '.png'; - $image = $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawing', $uploadedTo); + $image = $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo); return $image; } diff --git a/config/services.php b/config/services.php index ba9be69de..8695ea91c 100644 --- a/config/services.php +++ b/config/services.php @@ -13,7 +13,12 @@ return [ | to have a conventional place to find your various credentials. | */ + + // Single option to disable non-auth external services such as Gravatar and Draw.io 'disable_services' => env('DISABLE_EXTERNAL_SERVICES', false), + 'drawio' => env('DRAWIO', !env('DISABLE_EXTERNAL_SERVICES', false)), + + 'callback_url' => env('APP_URL', false), 'mailgun' => [ diff --git a/public/system_images/drawing.svg b/public/system_images/drawing.svg new file mode 100644 index 000000000..9a9231a18 --- /dev/null +++ b/public/system_images/drawing.svg @@ -0,0 +1,107 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 8e70d2db5..15e438b17 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -218,7 +218,6 @@ function codePlugin() { }); } -codePlugin(); function drawIoPlugin() { @@ -334,8 +333,8 @@ function drawIoPlugin() { }); editor.addButton('drawio', { - text: 'Drawing', - icon: false, + tooltip: 'Drawing', + image: window.baseUrl('/system_images/drawing.svg'), cmd: 'drawio' }); @@ -358,7 +357,6 @@ function drawIoPlugin() { }); } -drawIoPlugin(); window.tinymce.PluginManager.add('customhr', function (editor) { editor.addCommand('InsertHorizontalRule', function () { @@ -382,7 +380,14 @@ window.tinymce.PluginManager.add('customhr', function (editor) { }); }); +let plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor"; +// Load custom plugins +codePlugin(); +if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') === 'true') { + drawIoPlugin(); + plugins += ' drawio'; +} module.exports = { selector: '#html-editor', @@ -402,9 +407,9 @@ module.exports = { extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]', automatic_uploads: false, valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]", - plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor drawio", + plugins: plugins, imagetools_toolbar: 'imageoptions', - toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen drawio", + toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr drawio | removeformat code fullscreen", content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}", style_formats: [ {title: "Header Large", format: "h2"}, diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php index f450452ce..936f49790 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -1,5 +1,11 @@ -
+
{{ csrf_field() }} From 1411ee86b3f70013b3fd7735e3a504ebe15a154e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 20 Jan 2018 16:32:13 +0000 Subject: [PATCH 5/9] Extracted draw.io functionality to own file --- .../assets/js/components/markdown-editor.js | 2 +- resources/assets/js/{ => libs}/code.js | 0 resources/assets/js/libs/drawio.js | 66 +++++++++++++++++++ resources/assets/js/pages/page-form.js | 64 ++++-------------- resources/assets/js/pages/page-show.js | 2 +- resources/assets/js/vues/code-editor.js | 2 +- 6 files changed, 80 insertions(+), 56 deletions(-) rename resources/assets/js/{ => libs}/code.js (100%) create mode 100644 resources/assets/js/libs/drawio.js diff --git a/resources/assets/js/components/markdown-editor.js b/resources/assets/js/components/markdown-editor.js index 7b051dd12..cd0156e9b 100644 --- a/resources/assets/js/components/markdown-editor.js +++ b/resources/assets/js/components/markdown-editor.js @@ -1,6 +1,6 @@ const MarkdownIt = require("markdown-it"); const mdTasksLists = require('markdown-it-task-lists'); -const code = require('../code'); +const code = require('../libs/code'); class MarkdownEditor { diff --git a/resources/assets/js/code.js b/resources/assets/js/libs/code.js similarity index 100% rename from resources/assets/js/code.js rename to resources/assets/js/libs/code.js diff --git a/resources/assets/js/libs/drawio.js b/resources/assets/js/libs/drawio.js new file mode 100644 index 000000000..a44c12c44 --- /dev/null +++ b/resources/assets/js/libs/drawio.js @@ -0,0 +1,66 @@ + +const drawIoUrl = 'https://www.draw.io/?embed=1&ui=atlas&spin=1&proto=json'; +let iFrame = null; + +let onInit, onSave; + +/** + * Show the draw.io editor. + * @param onInitCallback - Must return a promise with the xml to load for the editor. + * @param onSaveCallback - Is called with the drawing data on save. + */ +export function show(onInitCallback, onSaveCallback) { + onInit = onInitCallback; + onSave = onSaveCallback; + + iFrame = document.createElement('iframe'); + iFrame.setAttribute('frameborder', '0'); + window.addEventListener('message', drawReceive); + iFrame.setAttribute('src', drawIoUrl); + iFrame.setAttribute('class', 'fullscreen'); + iFrame.style.backgroundColor = '#FFFFFF'; +} + +export function close() { + drawEventClose(); +} + +function drawReceive(event) { + if (!event.data || event.data.length < 1) return; + let message = JSON.parse(event.data); + if (message.event === 'init') { + drawEventInit(); + } else if (message.event === 'exit') { + drawEventClose(); + } else if (message.event === 'save') { + drawEventSave(message); + } else if (message.event === 'export') { + drawEventExport(message); + } +} + +function drawEventExport(message) { + if (onSave) { + onSave(message.data); + } +} + +function drawEventSave(message) { + drawPostMessage({action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing'}); +} + +function drawEventInit() { + if (!onInit) return; + onInit().then(xml => { + drawPostMessage({action: 'load', autosave: 1, xml: ''}); + }); +} + +function drawEventClose() { + window.removeEventListener('message', drawReceive); + if (iFrame) document.body.removeChild(iFrame); +} + +function drawPostMessage(data) { + iFrame.contentWindow.postMessage(JSON.stringify(data), '*'); +} \ No newline at end of file diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 15e438b17..f7bfe22cf 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -1,5 +1,6 @@ "use strict"; -const Code = require('../code'); +const Code = require('../libs/code'); +const DrawIO = require('../libs/drawio'); /** * Handle pasting images from clipboard. @@ -233,27 +234,7 @@ function drawIoPlugin() { function showDrawingEditor(mceEditor, selectedNode = null) { pageEditor = mceEditor; currentNode = selectedNode; - iframe = document.createElement('iframe'); - iframe.setAttribute('frameborder', '0'); - window.addEventListener('message', drawReceive); - iframe.setAttribute('src', drawIoUrl); - iframe.setAttribute('class', 'fullscreen'); - iframe.style.backgroundColor = '#FFFFFF'; - document.body.appendChild(iframe); - } - - function drawReceive(event) { - if (!event.data || event.data.length < 1) return; - let message = JSON.parse(event.data); - if (message.event === 'init') { - drawEventInit(); - } else if (message.event === 'exit') { - drawEventClose(); - } else if (message.event === 'save') { - drawEventSave(message); - } else if (message.event === 'export') { - drawEventExport(message); - } + DrawIO.show(drawingInit, updateContent); } function updateContent(pngData) { @@ -266,8 +247,7 @@ function drawIoPlugin() { // Handle updating an existing image if (currentNode) { - console.log(currentNode); - drawEventClose(); + DrawIO.close(); let imgElem = currentNode.querySelector('img'); let drawingId = currentNode.getAttribute('drawio-diagram'); window.$http.put(window.baseUrl(`/images/drawing/upload/${drawingId}`), data).then(resp => { @@ -281,7 +261,7 @@ function drawIoPlugin() { setTimeout(() => { pageEditor.insertContent(`
`); - drawEventClose(); + DrawIO.close(); window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => { pageEditor.dom.setAttrib(id, 'src', resp.data.url); pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', resp.data.id); @@ -293,41 +273,20 @@ function drawIoPlugin() { }, 5); } - function drawEventExport(message) { - updateContent(message.data); - } - function drawEventSave(message) { - drawPostMessage({action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing'}); - } - - function drawEventInit() { + function drawingInit() { if (!currentNode) { - drawPostMessage({action: 'load', autosave: 1, xml: ''}); - return; + return Promise.resolve(''); } - let imgElem = currentNode.querySelector('img'); let drawingId = currentNode.getAttribute('drawio-diagram'); - $http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { - let xml = `data:image/png;base64,${resp.data.content}`; - drawPostMessage({action: 'load', autosave: 1, xml}); + return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { + return `data:image/png;base64,${resp.data.content}`; }); } - function drawEventClose() { - window.removeEventListener('message', drawReceive); - if (iframe) document.body.removeChild(iframe); - } - - function drawPostMessage(data) { - iframe.contentWindow.postMessage(JSON.stringify(data), '*'); - } - window.tinymce.PluginManager.add('drawio', function(editor, url) { - let $ = editor.$; - editor.addCommand('drawio', () => { showDrawingEditor(editor); }); @@ -345,7 +304,7 @@ function drawIoPlugin() { }); editor.on('SetContent', function () { - let drawings = $('body > div[drawio-diagram]'); + let drawings = editor.$('body > div[drawio-diagram]'); if (!drawings.length) return; editor.undoManager.transact(function () { @@ -380,9 +339,8 @@ window.tinymce.PluginManager.add('customhr', function (editor) { }); }); +// Load plugins let plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor"; - -// Load custom plugins codePlugin(); if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') === 'true') { drawIoPlugin(); diff --git a/resources/assets/js/pages/page-show.js b/resources/assets/js/pages/page-show.js index 6af5af57d..2efaf66c6 100644 --- a/resources/assets/js/pages/page-show.js +++ b/resources/assets/js/pages/page-show.js @@ -1,5 +1,5 @@ const Clipboard = require("clipboard"); -const Code = require('../code'); +const Code = require('../libs/code'); let setupPageShow = window.setupPageShow = function (pageId) { diff --git a/resources/assets/js/vues/code-editor.js b/resources/assets/js/vues/code-editor.js index 35a98cc77..c7926cf28 100644 --- a/resources/assets/js/vues/code-editor.js +++ b/resources/assets/js/vues/code-editor.js @@ -1,4 +1,4 @@ -const codeLib = require('../code'); +const codeLib = require('../libs/code'); const methods = { show() { From 9bbef3a3dd75b9cdaf126835fab318f2f216ffc5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 20 Jan 2018 20:40:21 +0000 Subject: [PATCH 6/9] Added drawio abilities to markdown editor --- .../assets/js/components/markdown-editor.js | 88 ++++++++++++++++++- resources/assets/js/libs/drawio.js | 11 ++- resources/assets/sass/_forms.scss | 17 ++-- resources/lang/en/entities.php | 1 + resources/views/pages/form.blade.php | 4 + 5 files changed, 107 insertions(+), 14 deletions(-) diff --git a/resources/assets/js/components/markdown-editor.js b/resources/assets/js/components/markdown-editor.js index cd0156e9b..3393829cc 100644 --- a/resources/assets/js/components/markdown-editor.js +++ b/resources/assets/js/components/markdown-editor.js @@ -2,6 +2,8 @@ const MarkdownIt = require("markdown-it"); const mdTasksLists = require('markdown-it-task-lists'); const code = require('../libs/code'); +const DrawIO = require('../libs/drawio'); + class MarkdownEditor { constructor(elem) { @@ -20,13 +22,26 @@ class MarkdownEditor { init() { + let lastClick = 0; + // Prevent markdown display link click redirect this.display.addEventListener('click', event => { - let link = event.target.closest('a'); - if (link === null) return; + let isDblClick = Date.now() - lastClick < 300; - event.preventDefault(); - window.open(link.getAttribute('href')); + let link = event.target.closest('a'); + if (link !== null) { + event.preventDefault(); + window.open(link.getAttribute('href')); + return; + } + + let drawing = event.target.closest('[drawio-diagram]'); + if (drawing !== null && isDblClick) { + this.actionEditDrawing(drawing); + return; + } + + lastClick = Date.now(); }); // Button actions @@ -37,6 +52,7 @@ class MarkdownEditor { let action = button.getAttribute('data-action'); if (action === 'insertImage') this.actionInsertImage(); if (action === 'insertLink') this.actionShowLinkSelector(); + if (action === 'insertDrawing') this.actionStartDrawing(); }); window.$events.listen('editor-markdown-update', value => { @@ -290,6 +306,70 @@ class MarkdownEditor { }); } + // Show draw.io if enabled and handle save. + actionStartDrawing() { + if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true') return; + let cursorPos = this.cm.getCursor('from'); + + DrawIO.show(() => { + return Promise.resolve(''); + }, (pngData) => { + // let id = "image-" + Math.random().toString(16).slice(2); + // let loadingImage = window.baseUrl('/loading.gif'); + let data = { + image: pngData, + uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id')) + }; + + window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => { + let newText = `
`; + this.cm.focus(); + this.cm.replaceSelection(newText); + this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length); + DrawIO.close(); + }).catch(err => { + window.$events.emit('error', trans('errors.image_upload_error')); + console.log(err); + }); + }); + } + + // Show draw.io if enabled and handle save. + actionEditDrawing(imgContainer) { + if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true') return; + let cursorPos = this.cm.getCursor('from'); + let drawingId = imgContainer.getAttribute('drawio-diagram'); + + DrawIO.show(() => { + return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { + return `data:image/png;base64,${resp.data.content}`; + }); + }, (pngData) => { + + let data = { + image: pngData, + uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id')) + }; + + window.$http.put(window.baseUrl(`/images/drawing/upload/${drawingId}`), data).then(resp => { + let newText = `
`; + let newContent = this.cm.getValue().split('\n').map(line => { + if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) { + return newText; + } + return line; + }).join('\n'); + this.cm.setValue(newContent); + this.cm.setCursor(cursorPos); + this.cm.focus(); + DrawIO.close(); + }).catch(err => { + window.$events.emit('error', trans('errors.image_upload_error')); + console.log(err); + }); + }); + } + } module.exports = MarkdownEditor ; \ No newline at end of file diff --git a/resources/assets/js/libs/drawio.js b/resources/assets/js/libs/drawio.js index a44c12c44..beb6f0d59 100644 --- a/resources/assets/js/libs/drawio.js +++ b/resources/assets/js/libs/drawio.js @@ -9,7 +9,7 @@ let onInit, onSave; * @param onInitCallback - Must return a promise with the xml to load for the editor. * @param onSaveCallback - Is called with the drawing data on save. */ -export function show(onInitCallback, onSaveCallback) { +function show(onInitCallback, onSaveCallback) { onInit = onInitCallback; onSave = onSaveCallback; @@ -19,9 +19,10 @@ export function show(onInitCallback, onSaveCallback) { iFrame.setAttribute('src', drawIoUrl); iFrame.setAttribute('class', 'fullscreen'); iFrame.style.backgroundColor = '#FFFFFF'; + document.body.appendChild(iFrame); } -export function close() { +function close() { drawEventClose(); } @@ -52,7 +53,7 @@ function drawEventSave(message) { function drawEventInit() { if (!onInit) return; onInit().then(xml => { - drawPostMessage({action: 'load', autosave: 1, xml: ''}); + drawPostMessage({action: 'load', autosave: 1, xml: xml}); }); } @@ -63,4 +64,6 @@ function drawEventClose() { function drawPostMessage(data) { iFrame.contentWindow.postMessage(JSON.stringify(data), '*'); -} \ No newline at end of file +} + +module.exports = {show, close}; \ No newline at end of file diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss index 457d30e54..97620ff3f 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -59,16 +59,21 @@ border: 1px solid #DDD; width: 50%; } - .markdown-display { - padding: 0 $-m 0; - margin-left: -1px; - overflow-y: scroll; - } - .markdown-display.page-content { +} + +.markdown-display { + padding: 0 $-m 0; + margin-left: -1px; + overflow-y: scroll; + &.page-content { margin: 0 auto; max-width: 100%; } + [drawio-diagram]:hover { + outline: 2px solid $primary; + } } + .editor-toolbar { width: 100%; padding: $-xs $-m; diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 4dc5ccc38..6c5dd9f77 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -162,6 +162,7 @@ return [ 'pages_md_preview' => 'Preview', 'pages_md_insert_image' => 'Insert Image', 'pages_md_insert_link' => 'Insert Entity Link', + 'pages_md_insert_drawing' => 'Insert Drawing', 'pages_not_in_chapter' => 'Page is not in a chapter', 'pages_move' => 'Move Page', 'pages_move_success' => 'Page moved to ":parentName"', diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php index 936f49790..53861527b 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -86,6 +86,10 @@
{{ trans('entities.pages_md_editor') }}
+ @if(config('services.drawio')) + +  |  + @endif  |  From 88d09a2a3b76a2d914d523dc03fcf695849ba1b7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 28 Jan 2018 13:18:28 +0000 Subject: [PATCH 7/9] Added drawing endpoint tests Also refactored ImageTests away from BrowserKit Also added image upload type validation. --- app/Http/Controllers/ImageController.php | 5 +- app/Repos/ImageRepo.php | 13 ++- resources/lang/en/errors.php | 1 + tests/BrowserKitTest.php | 7 -- tests/ImageTest.php | 121 +++++++++++++++++++---- tests/test-data/test-image.jpg | Bin 5238 -> 0 bytes tests/test-data/test-image.png | Bin 0 -> 158 bytes 7 files changed, 119 insertions(+), 28 deletions(-) delete mode 100644 tests/test-data/test-image.jpg create mode 100644 tests/test-data/test-image.png diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php index c44b6e480..e675bff0c 100644 --- a/app/Http/Controllers/ImageController.php +++ b/app/Http/Controllers/ImageController.php @@ -120,7 +120,10 @@ class ImageController extends Controller $this->validate($request, [ 'file' => 'is_image' ]); - // TODO - Restrict & validate types + + if (!$this->imageRepo->isValidType($type)) { + return $this->jsonError(trans('errors.image_upload_type_error')); + } $imageUpload = $request->file('file'); diff --git a/app/Repos/ImageRepo.php b/app/Repos/ImageRepo.php index 97839c27f..0c15a4310 100644 --- a/app/Repos/ImageRepo.php +++ b/app/Repos/ImageRepo.php @@ -1,12 +1,9 @@ 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.', 'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.', 'image_upload_error' => 'An error occurred uploading the image', + 'image_upload_type_error' => 'The image type being uploaded is invalid', // Attachments 'attachment_page_mismatch' => 'Page mismatch during attachment update', diff --git a/tests/BrowserKitTest.php b/tests/BrowserKitTest.php index d5c9911f8..b5c32cef5 100644 --- a/tests/BrowserKitTest.php +++ b/tests/BrowserKitTest.php @@ -13,13 +13,6 @@ abstract class BrowserKitTest extends TestCase use DatabaseTransactions; - /** - * The base URL to use while testing the application. - * - * @var string - */ - protected $baseUrl = 'http://localhost'; - // Local user instances private $admin; private $editor; diff --git a/tests/ImageTest.php b/tests/ImageTest.php index 3bb41138b..822cc969b 100644 --- a/tests/ImageTest.php +++ b/tests/ImageTest.php @@ -1,8 +1,20 @@ getTestImageFilePath(), $fileName, 'image/jpeg', 5238); } /** @@ -28,13 +40,12 @@ class ImageTest extends BrowserKitTest * Uploads an image with the given name. * @param $name * @param int $uploadedTo - * @return string + * @return \Illuminate\Foundation\Testing\TestResponse */ protected function uploadImage($name, $uploadedTo = 0) { $file = $this->getTestImage($name); - $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []); - return $this->getTestImagePath('gallery', $name); + return $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []); } /** @@ -49,19 +60,20 @@ class ImageTest extends BrowserKitTest public function test_image_upload() { - $page = \BookStack\Page::first(); + $page = Page::first(); $this->asAdmin(); $admin = $this->getAdmin(); - $imageName = 'first-image.jpg'; + $imageName = 'first-image.png'; - $relPath = $this->uploadImage($imageName, $page->id); - $this->assertResponseOk(); + $upload = $this->uploadImage($imageName, $page->id); + $upload->assertStatus(200); + $relPath = $this->getTestImagePath('gallery', $imageName); $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image not found at path: '. public_path($relPath)); $this->deleteImage($relPath); - $this->seeInDatabase('images', [ + $this->assertDatabaseHas('images', [ 'url' => $this->baseUrl . $relPath, 'type' => 'gallery', 'uploaded_to' => $page->id, @@ -75,17 +87,18 @@ class ImageTest extends BrowserKitTest public function test_image_delete() { - $page = \BookStack\Page::first(); + $page = Page::first(); $this->asAdmin(); - $imageName = 'first-image.jpg'; + $imageName = 'first-image.png'; - $relPath = $this->uploadImage($imageName, $page->id); - $image = \BookStack\Image::first(); + $this->uploadImage($imageName, $page->id); + $image = Image::first(); + $relPath = $this->getTestImagePath('gallery', $imageName); - $this->call('DELETE', '/images/' . $image->id); - $this->assertResponseOk(); + $delete = $this->delete( '/images/' . $image->id); + $delete->assertStatus(200); - $this->dontSeeInDatabase('images', [ + $this->assertDatabaseMissing('images', [ 'url' => $this->baseUrl . $relPath, 'type' => 'gallery' ]); @@ -93,4 +106,78 @@ class ImageTest extends BrowserKitTest $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded image has not been deleted as expected'); } + public function testBase64Get() + { + $page = Page::first(); + $this->asAdmin(); + $imageName = 'first-image.png'; + + $this->uploadImage($imageName, $page->id); + $image = Image::first(); + + $imageGet = $this->getJson("/images/base64/{$image->id}"); + $imageGet->assertJson([ + 'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=' + ]); + } + + public function test_drawing_base64_upload() + { + $page = Page::first(); + $editor = $this->getEditor(); + $this->actingAs($editor); + + $upload = $this->postJson('images/drawing/upload', [ + 'uploaded_to' => $page->id, + 'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=' + ]); + + $upload->assertStatus(200); + $upload->assertJson([ + 'type' => 'drawio', + 'uploaded_to' => $page->id, + 'created_by' => $editor->id, + 'updated_by' => $editor->id, + ]); + + $image = Image::where('type', '=', 'drawio')->first(); + $this->assertTrue(file_exists(public_path($image->path)), 'Uploaded image not found at path: '. public_path($image->path)); + + $testImageData = file_get_contents($this->getTestImageFilePath()); + $uploadedImageData = file_get_contents(public_path($image->path)); + $this->assertTrue($testImageData === $uploadedImageData, "Uploaded image file data does not match our test image as expected"); + } + + public function test_drawing_replacing() + { + $page = Page::first(); + $editor = $this->getEditor(); + $this->actingAs($editor); + + $this->postJson('images/drawing/upload', [ + 'uploaded_to' => $page->id, + 'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDQ4S1RUeKwAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12NctNWSAQkwMaACUvkAfCkBmjyhGl4AAAAASUVORK5CYII=' + ]); + + $image = Image::where('type', '=', 'drawio')->first(); + + $replace = $this->putJson("images/drawing/upload/{$image->id}", [ + 'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=' + ]); + + $replace->assertStatus(200); + $replace->assertJson([ + 'type' => 'drawio', + 'uploaded_to' => $page->id, + 'created_by' => $editor->id, + 'updated_by' => $editor->id, + ]); + + $this->assertTrue(file_exists(public_path($image->path)), 'Uploaded image not found at path: '. public_path($image->path)); + + $testImageData = file_get_contents($this->getTestImageFilePath()); + $uploadedImageData = file_get_contents(public_path($image->path)); + $this->assertTrue($testImageData === $uploadedImageData, "Uploaded image file data does not match our test image as expected"); + } + } \ No newline at end of file diff --git a/tests/test-data/test-image.jpg b/tests/test-data/test-image.jpg deleted file mode 100644 index fb8da91011c2d691eb038afe579d60a8451d28cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5238 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 Date: Sun, 28 Jan 2018 13:27:41 +0000 Subject: [PATCH 8/9] Fixed test failing from missing baseURL Also updated image upload test to delete before upload to prevent failed tests breaking subsequent tests. --- tests/BrowserKitTest.php | 6 ++++++ tests/ImageTest.php | 12 ++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/BrowserKitTest.php b/tests/BrowserKitTest.php index b5c32cef5..a8ff03044 100644 --- a/tests/BrowserKitTest.php +++ b/tests/BrowserKitTest.php @@ -17,6 +17,12 @@ abstract class BrowserKitTest extends TestCase private $admin; private $editor; + /** + * The base URL to use while testing the application. + * @var string + */ + protected $baseUrl = 'http://localhost'; + public function tearDown() { \DB::disconnect(); diff --git a/tests/ImageTest.php b/tests/ImageTest.php index 822cc969b..c75617c0e 100644 --- a/tests/ImageTest.php +++ b/tests/ImageTest.php @@ -5,7 +5,6 @@ use BookStack\Page; class ImageTest extends TestCase { - /** * Get the path to our basic test image. * @return string @@ -54,20 +53,25 @@ class ImageTest extends TestCase */ protected function deleteImage($relPath) { - unlink(public_path($relPath)); + $path = public_path($relPath); + if (file_exists($path)) { + unlink($path); + } } public function test_image_upload() { $page = Page::first(); - $this->asAdmin(); $admin = $this->getAdmin(); + $this->actingAs($admin); + $imageName = 'first-image.png'; + $relPath = $this->getTestImagePath('gallery', $imageName); + $this->deleteImage($relPath); $upload = $this->uploadImage($imageName, $page->id); $upload->assertStatus(200); - $relPath = $this->getTestImagePath('gallery', $imageName); $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image not found at path: '. public_path($relPath)); From faf7c55fdd360faae4920a72e6a79d08dbeb6c6e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 28 Jan 2018 13:33:50 +0000 Subject: [PATCH 9/9] =?UTF-8?q?Actually=20fixed=20the=20BaseURL=20this=20t?= =?UTF-8?q?ime=20=F0=9F=A4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/TestCase.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/TestCase.php b/tests/TestCase.php index 81bd93ec4..94751b004 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -16,6 +16,12 @@ abstract class TestCase extends BaseTestCase protected $admin; protected $editor; + /** + * The base URL to use while testing the application. + * @var string + */ + protected $baseUrl = 'http://localhost'; + /** * Set the current user context to be an admin. * @return $this