* @param string $type
* @param Request $request
* @return \Illuminate\Http\JsonResponse
+ * @throws \Exception
*/
public function uploadByType($type, Request $request)
{
'file' => 'is_image'
]);
+ if (!$this->imageRepo->isValidType($type)) {
+ return $this->jsonError(trans('errors.image_upload_type_error'));
+ }
+
$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);
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
* @param $height
* @param $crop
* @return \Illuminate\Http\JsonResponse
+ * @throws ImageUploadException
+ * @throws \Exception
*/
public function getThumbnail($id, $width, $height, $crop)
{
* @param integer $imageId
* @param Request $request
* @return \Illuminate\Http\JsonResponse
+ * @throws ImageUploadException
+ * @throws \Exception
*/
public function update($imageId, Request $request)
{
<?php namespace BookStack\Repos;
-
use BookStack\Image;
use BookStack\Page;
use BookStack\Services\ImageService;
use BookStack\Services\PermissionService;
-use Illuminate\Contracts\Filesystem\FileNotFoundException;
-use Setting;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageRepo
* @param string $type
* @param int $uploadedTo
* @return Image
+ * @throws \BookStack\Exceptions\ImageUploadException
+ * @throws \Exception
*/
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
{
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, 'drawio', $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)
{
/**
* Load thumbnails onto an image object.
* @param Image $image
+ * @throws \BookStack\Exceptions\ImageUploadException
+ * @throws \Exception
*/
private function loadThumbs(Image $image)
{
* @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)
{
}
}
+ /**
+ * 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;
+ }
+ }
+
+ /**
+ * Check if the provided image type is valid.
+ * @param $type
+ * @return bool
+ */
+ public function isValidType($type)
+ {
+ $validTypes = ['drawing', 'gallery', 'cover', 'system', 'user'];
+ return in_array($type, $validTypes);
+ }
}
\ No newline at end of file
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
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
| 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' => [
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="410.44821"
+ height="419.86591"
+ viewBox="0 0 108.59775 111.08952"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
+ sodipodi:docname="drawing.svg">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.98994949"
+ inkscape:cx="314.26392"
+ inkscape:cy="340.27949"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:window-width="2560"
+ inkscape:window-height="1386"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-14.87623,-172.69189)">
+ <path
+ style="opacity:1;fill:#000016;fill-opacity:1;stroke:#000000;stroke-width:2.76340532;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 86.878856,250.68964 -11.880088,9.48754 11.880088,9.48722 z"
+ id="rect872"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <circle
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path815"
+ cx="36.348648"
+ cy="196.87526"
+ r="18.972418" />
+ <rect
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:4;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect817"
+ width="44.366741"
+ height="44.366741"
+ x="77.107246"
+ y="174.69189" />
+ <rect
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:3.96875;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect836"
+ width="44.366741"
+ height="44.366741"
+ x="17.773417"
+ y="237.4303" />
+ <rect
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:3.96875;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect838"
+ width="19.777945"
+ height="0.13363476"
+ x="56.260235"
+ y="196.77391" />
+ <rect
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:4.0334897;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect838-6"
+ width="39.624786"
+ height="0.068895064"
+ x="220.46501"
+ y="-99.424637"
+ transform="rotate(90)" />
+ <rect
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:4.02711964;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect838-6-7"
+ width="36.156651"
+ height="0.075265162"
+ x="-99.381981"
+ y="-260.21466"
+ transform="scale(-1)" />
+ </g>
+</svg>
const MarkdownIt = require("markdown-it");
const mdTasksLists = require('markdown-it-task-lists');
-const code = require('../code');
+const code = require('../libs/code');
+
+const DrawIO = require('../libs/drawio');
class MarkdownEditor {
init() {
+ let lastClick = 0;
+
// Prevent markdown display link click redirect
this.display.addEventListener('click', event => {
+ let isDblClick = Date.now() - lastClick < 300;
+
let link = event.target.closest('a');
- if (link === null) return;
+ if (link !== null) {
+ event.preventDefault();
+ window.open(link.getAttribute('href'));
+ return;
+ }
- event.preventDefault();
- window.open(link.getAttribute('href'));
+ let drawing = event.target.closest('[drawio-diagram]');
+ if (drawing !== null && isDblClick) {
+ this.actionEditDrawing(drawing);
+ return;
+ }
+
+ lastClick = Date.now();
});
// Button actions
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 => {
});
}
+ // 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 = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url}"></div>`;
+ 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 = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url + `?updated=${Date.now()}`}"></div>`;
+ 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
--- /dev/null
+
+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.
+ */
+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';
+ document.body.appendChild(iFrame);
+}
+
+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: xml});
+ });
+}
+
+function drawEventClose() {
+ window.removeEventListener('message', drawReceive);
+ if (iFrame) document.body.removeChild(iFrame);
+}
+
+function drawPostMessage(data) {
+ iFrame.contentWindow.postMessage(JSON.stringify(data), '*');
+}
+
+module.exports = {show, close};
\ No newline at end of file
"use strict";
-const Code = require('../code');
+const Code = require('../libs/code');
+const DrawIO = require('../libs/drawio');
/**
* Handle pasting images from clipboard.
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) {
});
}
-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;
+ DrawIO.show(drawingInit, updateContent);
+ }
+
+ 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) {
+ 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 => {
+ 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>`);
+ 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);
+ }).catch(err => {
+ pageEditor.dom.remove(id);
+ window.$events.emit('error', trans('errors.image_upload_error'));
+ console.log(err);
+ });
+ }, 5);
+ }
+
+
+ function drawingInit() {
+ if (!currentNode) {
+ return Promise.resolve('');
+ }
+
+ let drawingId = currentNode.getAttribute('drawio-diagram');
+ return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => {
+ return `data:image/png;base64,${resp.data.content}`;
+ });
+ }
+
+ window.tinymce.PluginManager.add('drawio', function(editor, url) {
+
+ editor.addCommand('drawio', () => {
+ showDrawingEditor(editor);
+ });
+
+ editor.addButton('drawio', {
+ tooltip: 'Drawing',
+ image: window.baseUrl('/system_images/drawing.svg'),
+ cmd: 'drawio'
+ });
+
+ editor.on('dblclick', event => {
+ let selectedNode = editor.selection.getNode();
+ if (!isDrawing(selectedNode)) return;
+ showDrawingEditor(editor, selectedNode);
+ });
+
+ editor.on('SetContent', function () {
+ let drawings = editor.$('body > div[drawio-diagram]');
+ if (!drawings.length) return;
+
+ editor.undoManager.transact(function () {
+ drawings.each((index, elem) => {
+ elem.setAttribute('contenteditable', 'false');
+ });
+ });
+ });
+
+ });
+}
window.tinymce.PluginManager.add('customhr', function (editor) {
editor.addCommand('InsertHorizontalRule', function () {
});
});
-
+// Load plugins
+let plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor";
+codePlugin();
+if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') === 'true') {
+ drawIoPlugin();
+ plugins += ' drawio';
+}
module.exports = {
selector: '#html-editor',
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: 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",
+ 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"},
const Clipboard = require("clipboard");
-const Code = require('../code');
+const Code = require('../libs/code');
let setupPageShow = window.setupPageShow = function (pageId) {
-const codeLib = require('../code');
+const codeLib = require('../libs/code');
const methods = {
show() {
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;
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
'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"',
'cannot_create_thumbs' => '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',
-<div class="page-editor flex-fill flex" id="page-editor" drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}" editor-type="{{ setting('app-editor') }}" page-id="{{ $model->id or 0 }}" page-new-draft="{{ $model->draft or 0 }}" page-update-draft="{{ $model->isDraft or 0 }}">
+<div class="page-editor flex-fill flex" id="page-editor"
+ drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}"
+ drawio-enabled="{{ config('services.drawio') ? 'true' : 'false' }}"
+ editor-type="{{ setting('app-editor') }}"
+ page-id="{{ $model->id or 0 }}"
+ page-new-draft="{{ $model->draft or 0 }}"
+ page-update-draft="{{ $model->isDraft or 0 }}">
{{ csrf_field() }}
<div class="editor-toolbar">
<span class="float left">{{ trans('entities.pages_md_editor') }}</span>
<div class="float right buttons">
+ @if(config('services.drawio'))
+ <button class="text-button" type="button" data-action="insertDrawing"><i class="zmdi zmdi-widgets"></i>{{ trans('entities.pages_md_insert_drawing') }}</button>
+ | 
+ @endif
<button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>{{ trans('entities.pages_md_insert_image') }}</button>
|
<button class="text-button" type="button" data-action="insertLink"><i class="zmdi zmdi-link"></i>{{ trans('entities.pages_md_insert_link') }}</button>
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
use DatabaseTransactions;
+ // Local user instances
+ private $admin;
+ private $editor;
+
/**
* The base URL to use while testing the application.
- *
* @var string
*/
protected $baseUrl = 'http://localhost';
- // Local user instances
- private $admin;
- private $editor;
-
public function tearDown()
{
\DB::disconnect();
<?php namespace Tests;
-class ImageTest extends BrowserKitTest
+use BookStack\Image;
+use BookStack\Page;
+
+class ImageTest extends TestCase
{
+ /**
+ * Get the path to our basic test image.
+ * @return string
+ */
+ protected function getTestImageFilePath()
+ {
+ return base_path('tests/test-data/test-image.png');
+ }
/**
* Get a test image that can be uploaded
*/
protected function getTestImage($fileName)
{
- return new \Illuminate\Http\UploadedFile(base_path('tests/test-data/test-image.jpg'), $fileName, 'image/jpeg', 5238);
+ return new \Illuminate\Http\UploadedFile($this->getTestImageFilePath(), $fileName, 'image/jpeg', 5238);
}
/**
* 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], []);
}
/**
*/
protected function deleteImage($relPath)
{
- unlink(public_path($relPath));
+ $path = public_path($relPath);
+ if (file_exists($path)) {
+ unlink($path);
+ }
}
public function test_image_upload()
{
- $page = \BookStack\Page::first();
- $this->asAdmin();
+ $page = Page::first();
$admin = $this->getAdmin();
- $imageName = 'first-image.jpg';
+ $this->actingAs($admin);
- $relPath = $this->uploadImage($imageName, $page->id);
- $this->assertResponseOk();
+ $imageName = 'first-image.png';
+ $relPath = $this->getTestImagePath('gallery', $imageName);
+ $this->deleteImage($relPath);
+
+ $upload = $this->uploadImage($imageName, $page->id);
+ $upload->assertStatus(200);
$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,
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'
]);
$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
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