]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'master' into draw.io to fetch auth image changes
authorDan Brown <redacted>
Sat, 20 Jan 2018 14:06:44 +0000 (14:06 +0000)
committerDan Brown <redacted>
Sat, 20 Jan 2018 14:06:44 +0000 (14:06 +0000)
app/Http/Controllers/ImageController.php
app/Repos/ImageRepo.php
app/Services/ImageService.php
resources/assets/js/pages/page-form.js
resources/assets/sass/styles.scss
routes/web.php

index d783507545d421ba63a536eaf5cbfeaaaf51dccd..c44b6e480429ab5f24292c0bc8bbab6c631fb789 100644 (file)
@@ -112,6 +112,7 @@ class ImageController extends Controller
      * @param string $type
      * @param Request $request
      * @return \Illuminate\Http\JsonResponse
+     * @throws \Exception
      */
     public function uploadByType($type, Request $request)
     {
@@ -119,11 +120,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);
@@ -132,6 +134,73 @@ 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);
+    }
+
+    /**
+     * 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
+     * @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
@@ -139,6 +208,8 @@ class ImageController extends Controller
      * @param $height
      * @param $crop
      * @return \Illuminate\Http\JsonResponse
+     * @throws ImageUploadException
+     * @throws \Exception
      */
     public function getThumbnail($id, $width, $height, $crop)
     {
@@ -153,6 +224,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)
     {
index 5f04a74b19ab9dd938a3655ec94506aca56b099a..3918d5f67c30b99ff45d14e89915fc4b9adf2ef5 100644 (file)
@@ -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,39 @@ 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;
+    }
+
+    /**
+     * 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
      * @param array $updateDetails
      * @return Image
+     * @throws \BookStack\Exceptions\ImageUploadException
+     * @throws \Exception
      */
     public function updateImageDetails(Image $image, $updateDetails)
     {
@@ -170,6 +200,8 @@ class ImageRepo
     /**
      * Load thumbnails onto an image object.
      * @param Image $image
+     * @throws \BookStack\Exceptions\ImageUploadException
+     * @throws \Exception
      */
     private function loadThumbs(Image $image)
     {
@@ -188,6 +220,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)
     {
@@ -199,5 +233,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
index 43375ee094355dd214e7cb9e77af1d328438de8b..5eea285e5fda62ab74e6afb683277cf4a5d798e6 100644 (file)
@@ -46,6 +46,50 @@ 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);
+    }
+
+    /**
+     * 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
@@ -175,6 +219,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
index 904403fc1a5afbb2d9ceb2a0924d0eff5589aca4..8e70d2db589cb2a340bfbe72539ea4232d03c977 100644 (file)
@@ -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) {
@@ -220,6 +220,146 @@ 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;
+    let currentNode = null;
+
+    function isDrawing(node) {
+        return node.hasAttribute('drawio-diagram');
+    }
+
+    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);
+        }
+    }
+
+    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'))
+        };
+
+        // 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(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`);
+            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) {
+        updateContent(message.data);
+    }
+
+    function drawEventSave(message) {
+        drawPostMessage({action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing'});
+    }
+
+    function drawEventInit() {
+        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() {
+        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, selectedNode);
+        });
+
+        editor.on('SetContent', function () {
+            let drawings = $('body > div[drawio-diagram]');
+            if (!drawings.length) return;
+
+            editor.undoManager.transact(function () {
+                drawings.each((index, elem) => {
+                    elem.setAttribute('contenteditable', 'false');
+                });
+            });
+        });
+
+    });
+}
+drawIoPlugin();
+
 window.tinymce.PluginManager.add('customhr', function (editor) {
     editor.addCommand('InsertHorizontalRule', function () {
         let hrElem = document.createElement('hr');
@@ -259,12 +399,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[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",
+    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"},
index 6a80237c5b6a674b9cd4763408e6b82a95002f3c..2cb72bd75a0313280197c5fad42fbfe075f1db05 100644 (file)
@@ -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
index 06805714d7b644bf3beff65699c8b0473d6df315..a69e672e47ee7a1999b248397c64850045b0aacc 100644 (file)
@@ -89,13 +89,16 @@ 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::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');
         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