mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-02-21 09:47:55 +08:00
commit
1d1cc19596
@ -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)
|
||||
{
|
||||
@ -120,10 +121,14 @@ class ImageController extends Controller
|
||||
'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);
|
||||
@ -132,6 +137,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 +211,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 +227,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)
|
||||
{
|
||||
|
@ -1,12 +1,9 @@
|
||||
<?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
|
||||
@ -132,6 +129,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 +139,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, '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)
|
||||
{
|
||||
@ -170,6 +197,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 +217,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 +230,29 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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' => [
|
||||
|
107
public/system_images/drawing.svg
Normal file
107
public/system_images/drawing.svg
Normal file
@ -0,0 +1,107 @@
|
||||
<?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>
|
After Width: | Height: | Size: 3.7 KiB |
@ -1,6 +1,8 @@
|
||||
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 {
|
||||
|
||||
@ -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 = `<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 ;
|
69
resources/assets/js/libs/drawio.js
Normal file
69
resources/assets/js/libs/drawio.js
Normal file
@ -0,0 +1,69 @@
|
||||
|
||||
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};
|
@ -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.
|
||||
@ -47,7 +48,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) {
|
||||
@ -218,7 +219,103 @@ 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;
|
||||
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 () {
|
||||
@ -242,7 +339,13 @@ window.tinymce.PluginManager.add('customhr', function (editor) {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 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',
|
||||
@ -259,12 +362,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: 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"},
|
||||
|
@ -1,5 +1,5 @@
|
||||
const Clipboard = require("clipboard");
|
||||
const Code = require('../code');
|
||||
const Code = require('../libs/code');
|
||||
|
||||
let setupPageShow = window.setupPageShow = function (pageId) {
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
const codeLib = require('../code');
|
||||
const codeLib = require('../libs/code');
|
||||
|
||||
const methods = {
|
||||
show() {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
@ -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"',
|
||||
|
@ -36,6 +36,7 @@ return [
|
||||
'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',
|
||||
|
@ -1,5 +1,11 @@
|
||||
|
||||
<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() }}
|
||||
|
||||
@ -80,6 +86,10 @@
|
||||
<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>
|
||||
|
@ -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
|
||||
|
@ -13,17 +13,16 @@ 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;
|
||||
|
||||
/**
|
||||
* The base URL to use while testing the application.
|
||||
* @var string
|
||||
*/
|
||||
protected $baseUrl = 'http://localhost';
|
||||
|
||||
public function tearDown()
|
||||
{
|
||||
\DB::disconnect();
|
||||
|
@ -1,7 +1,18 @@
|
||||
<?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
|
||||
@ -10,7 +21,7 @@ class ImageTest extends BrowserKitTest
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -28,13 +39,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], []);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,25 +53,31 @@ class ImageTest extends BrowserKitTest
|
||||
*/
|
||||
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,
|
||||
@ -75,17 +91,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 +110,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");
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 5.1 KiB |
BIN
tests/test-data/test-image.png
Normal file
BIN
tests/test-data/test-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 158 B |
Loading…
x
Reference in New Issue
Block a user