diff --git a/app/Console/Commands/CleanupImages.php b/app/Console/Commands/CleanupImages.php
new file mode 100644
index 000000000..e05508d5e
--- /dev/null
+++ b/app/Console/Commands/CleanupImages.php
@@ -0,0 +1,83 @@
+imageService = $imageService;
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $checkRevisions = $this->option('all') ? false : true;
+ $dryRun = $this->option('force') ? false : true;
+
+ if (!$dryRun) {
+ $proceed = $this->confirm("This operation is destructive and is not guaranteed to be fully accurate.\nEnsure you have a backup of your images.\nAre you sure you want to proceed?");
+ if (!$proceed) {
+ return;
+ }
+ }
+
+ $deleted = $this->imageService->deleteUnusedImages($checkRevisions, $dryRun);
+ $deleteCount = count($deleted);
+
+ if ($dryRun) {
+ $this->comment('Dry run, No images have been deleted');
+ $this->comment($deleteCount . ' images found that would have been deleted');
+ $this->showDeletedImages($deleted);
+ $this->comment('Run with -f or --force to perform deletions');
+ return;
+ }
+
+ $this->showDeletedImages($deleted);
+ $this->comment($deleteCount . ' images deleted');
+ }
+
+ protected function showDeletedImages($paths)
+ {
+ if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) return;
+ if (count($paths) > 0) {
+ $this->line('Images to delete:');
+ }
+ foreach ($paths as $path) {
+ $this->line($path);
+ }
+ }
+}
diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php
index 8437c80d7..eb92ae9a8 100644
--- a/app/Http/Controllers/ImageController.php
+++ b/app/Http/Controllers/ImageController.php
@@ -164,32 +164,6 @@ 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
@@ -245,26 +219,29 @@ class ImageController extends Controller
}
/**
- * Deletes an image and all thumbnail/image files
+ * Show the usage of an image on pages.
* @param EntityRepo $entityRepo
- * @param Request $request
- * @param int $id
+ * @param $id
* @return \Illuminate\Http\JsonResponse
*/
- public function destroy(EntityRepo $entityRepo, Request $request, $id)
+ public function usage(EntityRepo $entityRepo, $id)
+ {
+ $image = $this->imageRepo->getById($id);
+ $pageSearch = $entityRepo->searchForImage($image->url);
+ return response()->json($pageSearch);
+ }
+
+ /**
+ * Deletes an image and all thumbnail/image files
+ * @param int $id
+ * @return \Illuminate\Http\JsonResponse
+ * @throws \Exception
+ */
+ public function destroy($id)
{
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
- // Check if this image is used on any pages
- $isForced = in_array($request->get('force', ''), [true, 'true']);
- if (!$isForced) {
- $pageSearch = $entityRepo->searchForImage($image->url);
- if ($pageSearch !== false) {
- return response()->json($pageSearch, 400);
- }
- }
-
$this->imageRepo->destroyImage($image);
return response()->json(trans('components.images_deleted'));
}
diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php
index e0e351458..d9d66042e 100644
--- a/app/Http/Controllers/SettingController.php
+++ b/app/Http/Controllers/SettingController.php
@@ -1,5 +1,6 @@
checkPermission('settings-manage');
- $this->setPageTitle('Settings');
+ $this->setPageTitle(trans('settings.settings'));
// Get application version
$version = trim(file_get_contents(base_path('version')));
@@ -43,4 +44,48 @@ class SettingController extends Controller
session()->flash('success', trans('settings.settings_save_success'));
return redirect('/settings');
}
+
+ /**
+ * Show the page for application maintenance.
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function showMaintenance()
+ {
+ $this->checkPermission('settings-manage');
+ $this->setPageTitle(trans('settings.maint'));
+
+ // Get application version
+ $version = trim(file_get_contents(base_path('version')));
+
+ return view('settings/maintenance', ['version' => $version]);
+ }
+
+ /**
+ * Action to clean-up images in the system.
+ * @param Request $request
+ * @param ImageService $imageService
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ */
+ public function cleanupImages(Request $request, ImageService $imageService)
+ {
+ $this->checkPermission('settings-manage');
+
+ $checkRevisions = !($request->get('ignore_revisions', 'false') === 'true');
+ $dryRun = !($request->has('confirm'));
+
+ $imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
+ $deleteCount = count($imagesToDelete);
+ if ($deleteCount === 0) {
+ session()->flash('warning', trans('settings.maint_image_cleanup_nothing_found'));
+ return redirect('/settings/maintenance')->withInput();
+ }
+
+ if ($dryRun) {
+ session()->flash('cleanup-images-warning', trans('settings.maint_image_cleanup_warning', ['count' => $deleteCount]));
+ } else {
+ session()->flash('success', trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
+ }
+
+ return redirect('/settings/maintenance#image-cleanup')->withInput();
+ }
}
diff --git a/app/Image.php b/app/Image.php
index ad23a077a..412beea90 100644
--- a/app/Image.php
+++ b/app/Image.php
@@ -9,13 +9,15 @@ class Image extends Ownable
/**
* Get a thumbnail for this image.
- * @param int $width
- * @param int $height
+ * @param int $width
+ * @param int $height
* @param bool|false $keepRatio
* @return string
+ * @throws \Exception
*/
public function getThumb($width, $height, $keepRatio = false)
{
return Images::getThumbnail($this, $width, $height, $keepRatio);
}
+
}
diff --git a/app/Providers/CustomFacadeProvider.php b/app/Providers/CustomFacadeProvider.php
index a97512e8c..c81a5529d 100644
--- a/app/Providers/CustomFacadeProvider.php
+++ b/app/Providers/CustomFacadeProvider.php
@@ -3,6 +3,7 @@
namespace BookStack\Providers;
use BookStack\Activity;
+use BookStack\Image;
use BookStack\Services\ImageService;
use BookStack\Services\PermissionService;
use BookStack\Services\ViewService;
@@ -57,6 +58,7 @@ class CustomFacadeProvider extends ServiceProvider
$this->app->bind('images', function () {
return new ImageService(
+ $this->app->make(Image::class),
$this->app->make(ImageManager::class),
$this->app->make(Factory::class),
$this->app->make(Repository::class)
diff --git a/app/Repos/ImageRepo.php b/app/Repos/ImageRepo.php
index 245c0f27b..4ccd719ad 100644
--- a/app/Repos/ImageRepo.php
+++ b/app/Repos/ImageRepo.php
@@ -153,17 +153,6 @@ 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.
@@ -183,13 +172,14 @@ class ImageRepo
/**
- * Destroys an Image object along with its files and thumbnails.
+ * Destroys an Image object along with its revisions, files and thumbnails.
* @param Image $image
* @return bool
+ * @throws \Exception
*/
public function destroyImage(Image $image)
{
- $this->imageService->destroyImage($image);
+ $this->imageService->destroy($image);
return true;
}
@@ -200,7 +190,7 @@ class ImageRepo
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
- private function loadThumbs(Image $image)
+ protected function loadThumbs(Image $image)
{
$image->thumbs = [
'gallery' => $this->getThumbnail($image, 150, 150),
@@ -250,7 +240,7 @@ class ImageRepo
*/
public function isValidType($type)
{
- $validTypes = ['drawing', 'gallery', 'cover', 'system', 'user'];
+ $validTypes = ['gallery', 'cover', 'system', 'user'];
return in_array($type, $validTypes);
}
}
diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php
index 3cfd61d27..d113b676a 100644
--- a/app/Repos/UserRepo.php
+++ b/app/Repos/UserRepo.php
@@ -166,7 +166,7 @@ class UserRepo
// Delete user profile images
$profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get();
foreach ($profileImages as $image) {
- Images::destroyImage($image);
+ Images::destroy($image);
}
}
diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php
index 06ef3a0f0..73a677ac2 100644
--- a/app/Services/ImageService.php
+++ b/app/Services/ImageService.php
@@ -3,11 +3,11 @@
use BookStack\Exceptions\ImageUploadException;
use BookStack\Image;
use BookStack\User;
+use DB;
use Exception;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
-use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
use Illuminate\Contracts\Cache\Repository as Cache;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@@ -17,15 +17,18 @@ class ImageService extends UploadService
protected $imageTool;
protected $cache;
protected $storageUrl;
+ protected $image;
/**
* ImageService constructor.
- * @param $imageTool
- * @param $fileSystem
- * @param $cache
+ * @param Image $image
+ * @param ImageManager $imageTool
+ * @param FileSystem $fileSystem
+ * @param Cache $cache
*/
- public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
+ public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
{
+ $this->image = $image;
$this->imageTool = $imageTool;
$this->cache = $cache;
parent::__construct($fileSystem);
@@ -82,31 +85,6 @@ 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
@@ -140,16 +118,16 @@ class ImageService extends UploadService
$secureUploads = setting('app-secure-images');
$imageName = str_replace(' ', '-', $imageName);
- if ($secureUploads) {
- $imageName = str_random(16) . '-' . $imageName;
- }
-
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
while ($storage->exists($imagePath . $imageName)) {
$imageName = str_random(3) . $imageName;
}
+
$fullPath = $imagePath . $imageName;
+ if ($secureUploads) {
+ $fullPath = $imagePath . str_random(16) . '-' . $imageName;
+ }
try {
$storage->put($fullPath, $imageData);
@@ -172,20 +150,11 @@ class ImageService extends UploadService
$imageDetails['updated_by'] = $userId;
}
- $image = (new Image());
+ $image = $this->image->newInstance();
$image->forceFill($imageDetails)->save();
return $image;
}
- /**
- * Get the storage path, Dependant of storage type.
- * @param Image $image
- * @return mixed|string
- */
- protected function getPath(Image $image)
- {
- return $image->path;
- }
/**
* Checks if the image is a gif. Returns true if it is, else false.
@@ -194,7 +163,7 @@ class ImageService extends UploadService
*/
protected function isGif(Image $image)
{
- return strtolower(pathinfo($this->getPath($image), PATHINFO_EXTENSION)) === 'gif';
+ return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
}
/**
@@ -212,11 +181,11 @@ class ImageService extends UploadService
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
if ($keepRatio && $this->isGif($image)) {
- return $this->getPublicUrl($this->getPath($image));
+ return $this->getPublicUrl($image->path);
}
$thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
- $imagePath = $this->getPath($image);
+ $imagePath = $image->path;
$thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
@@ -262,43 +231,51 @@ class ImageService extends UploadService
*/
public function getImageData(Image $image)
{
- $imagePath = $this->getPath($image);
+ $imagePath = $image->path;
$storage = $this->getStorage();
return $storage->get($imagePath);
}
/**
- * Destroys an Image object along with its files and thumbnails.
+ * Destroy an image along with its revisions, thumbnails and remaining folders.
* @param Image $image
- * @return bool
* @throws Exception
*/
- public function destroyImage(Image $image)
+ public function destroy(Image $image)
+ {
+ $this->destroyImagesFromPath($image->path);
+ $image->delete();
+ }
+
+ /**
+ * Destroys an image at the given path.
+ * Searches for image thumbnails in addition to main provided path..
+ * @param string $path
+ * @return bool
+ */
+ protected function destroyImagesFromPath(string $path)
{
$storage = $this->getStorage();
- $imageFolder = dirname($this->getPath($image));
- $imageFileName = basename($this->getPath($image));
+ $imageFolder = dirname($path);
+ $imageFileName = basename($path);
$allImages = collect($storage->allFiles($imageFolder));
+ // Delete image files
$imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
$expectedIndex = strlen($imagePath) - strlen($imageFileName);
return strpos($imagePath, $imageFileName) === $expectedIndex;
});
-
$storage->delete($imagesToDelete->all());
// Cleanup of empty folders
- foreach ($storage->directories($imageFolder) as $directory) {
+ $foldersInvolved = array_merge([$imageFolder], $storage->directories($imageFolder));
+ foreach ($foldersInvolved as $directory) {
if ($this->isFolderEmpty($directory)) {
$storage->deleteDirectory($directory);
}
}
- if ($this->isFolderEmpty($imageFolder)) {
- $storage->deleteDirectory($imageFolder);
- }
- $image->delete();
return true;
}
@@ -321,6 +298,46 @@ class ImageService extends UploadService
return $image;
}
+
+ /**
+ * Delete gallery and drawings that are not within HTML content of pages or page revisions.
+ * Checks based off of only the image name.
+ * Could be much improved to be more specific but kept it generic for now to be safe.
+ *
+ * Returns the path of the images that would be/have been deleted.
+ * @param bool $checkRevisions
+ * @param bool $dryRun
+ * @param array $types
+ * @return array
+ */
+ public function deleteUnusedImages($checkRevisions = true, $dryRun = true, $types = ['gallery', 'drawio'])
+ {
+ $types = array_intersect($types, ['gallery', 'drawio']);
+ $deletedPaths = [];
+
+ $this->image->newQuery()->whereIn('type', $types)
+ ->chunk(1000, function($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) {
+ foreach ($images as $image) {
+ $searchQuery = '%' . basename($image->path) . '%';
+ $inPage = DB::table('pages')
+ ->where('html', 'like', $searchQuery)->count() > 0;
+ $inRevision = false;
+ if ($checkRevisions) {
+ $inRevision = DB::table('page_revisions')
+ ->where('html', 'like', $searchQuery)->count() > 0;
+ }
+
+ if (!$inPage && !$inRevision) {
+ $deletedPaths[] = $image->path;
+ if (!$dryRun) {
+ $this->destroy($image);
+ }
+ }
+ }
+ });
+ return $deletedPaths;
+ }
+
/**
* Convert a image URI to a Base64 encoded string.
* Attempts to find locally via set storage method first.
diff --git a/resources/assets/icons/spanner.svg b/resources/assets/icons/spanner.svg
new file mode 100644
index 000000000..8ab25a247
--- /dev/null
+++ b/resources/assets/icons/spanner.svg
@@ -0,0 +1,4 @@
+
diff --git a/resources/assets/js/components/markdown-editor.js b/resources/assets/js/components/markdown-editor.js
index 46c54408b..06426bf34 100644
--- a/resources/assets/js/components/markdown-editor.js
+++ b/resources/assets/js/components/markdown-editor.js
@@ -52,6 +52,10 @@ class MarkdownEditor {
let action = button.getAttribute('data-action');
if (action === 'insertImage') this.actionInsertImage();
if (action === 'insertLink') this.actionShowLinkSelector();
+ if (action === 'insertDrawing' && event.ctrlKey) {
+ this.actionShowImageManager();
+ return;
+ }
if (action === 'insertDrawing') this.actionStartDrawing();
});
@@ -293,7 +297,14 @@ class MarkdownEditor {
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
- });
+ }, 'gallery');
+ }
+
+ actionShowImageManager() {
+ let cursorPos = this.cm.getCursor('from');
+ window.ImageManager.show(image => {
+ this.insertDrawing(image, cursorPos);
+ }, 'drawio');
}
// Show the popup link selector and insert a link when finished
@@ -324,10 +335,7 @@ class MarkdownEditor {
};
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);
+ this.insertDrawing(resp.data, cursorPos);
DrawIO.close();
}).catch(err => {
window.$events.emit('error', trans('errors.image_upload_error'));
@@ -336,6 +344,13 @@ class MarkdownEditor {
});
}
+ insertDrawing(image, originalCursor) {
+ let newText = `
`;
+ this.cm.focus();
+ this.cm.replaceSelection(newText);
+ this.cm.setCursor(originalCursor.line, originalCursor.ch + newText.length);
+ }
+
// Show draw.io if enabled and handle save.
actionEditDrawing(imgContainer) {
if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true') return;
@@ -353,8 +368,8 @@ class MarkdownEditor {
uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id'))
};
- window.$http.put(window.baseUrl(`/images/drawing/upload/${drawingId}`), data).then(resp => {
- let newText = `}`})
`;
+ window.$http.post(window.baseUrl(`/images/drawing/upload`), data).then(resp => {
+ let newText = `
`;
let newContent = this.cm.getValue().split('\n').map(line => {
if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
return newText;
diff --git a/resources/assets/js/components/wysiwyg-editor.js b/resources/assets/js/components/wysiwyg-editor.js
index 701a1fec6..f7e9bfeed 100644
--- a/resources/assets/js/components/wysiwyg-editor.js
+++ b/resources/assets/js/components/wysiwyg-editor.js
@@ -221,8 +221,6 @@ function 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;
@@ -230,6 +228,22 @@ function drawIoPlugin() {
return node.hasAttribute('drawio-diagram');
}
+ function showDrawingManager(mceEditor, selectedNode = null) {
+ pageEditor = mceEditor;
+ currentNode = selectedNode;
+ // Show image manager
+ window.ImageManager.show(function (image) {
+ if (selectedNode) {
+ let imgElem = selectedNode.querySelector('img');
+ pageEditor.dom.setAttrib(imgElem, 'src', image.url);
+ pageEditor.dom.setAttrib(selectedNode, 'drawio-diagram', image.id);
+ } else {
+ let imgHTML = `
`;
+ pageEditor.insertContent(imgHTML);
+ }
+ }, 'drawio');
+ }
+
function showDrawingEditor(mceEditor, selectedNode = null) {
pageEditor = mceEditor;
currentNode = selectedNode;
@@ -248,9 +262,9 @@ function drawIoPlugin() {
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()}`);
+ window.$http.post(window.baseUrl(`/images/drawing/upload`), data).then(resp => {
+ pageEditor.dom.setAttrib(imgElem, 'src', resp.data.url);
+ pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', resp.data.id);
}).catch(err => {
window.$events.emit('error', trans('errors.image_upload_error'));
console.log(err);
@@ -287,13 +301,24 @@ function drawIoPlugin() {
window.tinymce.PluginManager.add('drawio', function(editor, url) {
editor.addCommand('drawio', () => {
- showDrawingEditor(editor);
+ let selectedNode = editor.selection.getNode();
+ showDrawingEditor(editor, isDrawing(selectedNode) ? selectedNode : null);
});
editor.addButton('drawio', {
+ type: 'splitbutton',
tooltip: 'Drawing',
image: `data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMwMDAwMDAiICB4bWxucz0iaHR0cDovL3d3 dy53My5vcmcvMjAwMC9zdmciPgogICAgPHBhdGggZD0iTTIzIDdWMWgtNnYySDdWMUgxdjZoMnYx MEgxdjZoNnYtMmgxMHYyaDZ2LTZoLTJWN2gyek0zIDNoMnYySDNWM3ptMiAxOEgzdi0yaDJ2Mnpt MTItMkg3di0ySDVWN2gyVjVoMTB2MmgydjEwaC0ydjJ6bTQgMmgtMnYtMmgydjJ6TTE5IDVWM2gy djJoLTJ6bS01LjI3IDloLTMuNDlsLS43MyAySDcuODlsMy40LTloMS40bDMuNDEgOWgtMS42M2wt Ljc0LTJ6bS0zLjA0LTEuMjZoMi42MUwxMiA4LjkxbC0xLjMxIDMuODN6Ii8+CiAgICA8cGF0aCBk PSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+Cjwvc3ZnPg==`,
- cmd: 'drawio'
+ cmd: 'drawio',
+ menu: [
+ {
+ text: 'Drawing Manager',
+ onclick() {
+ let selectedNode = editor.selection.getNode();
+ showDrawingManager(editor, isDrawing(selectedNode) ? selectedNode : null);
+ }
+ }
+ ]
});
editor.on('dblclick', event => {
@@ -443,7 +468,7 @@ class WysiwygEditor {
html += `
`;
html += '';
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
- });
+ }, 'gallery');
}
},
@@ -522,7 +547,7 @@ class WysiwygEditor {
html += `
`;
html += '';
editor.execCommand('mceInsertContent', false, html);
- });
+ }, 'gallery');
}
});
diff --git a/resources/assets/js/vues/image-manager.js b/resources/assets/js/vues/image-manager.js
index 89fe6769e..16c8ef9cf 100644
--- a/resources/assets/js/vues/image-manager.js
+++ b/resources/assets/js/vues/image-manager.js
@@ -26,25 +26,32 @@ const data = {
imageUpdateSuccess: false,
imageDeleteSuccess: false,
+ deleteConfirm: false,
};
const methods = {
- show(providedCallback) {
+ show(providedCallback, imageType = null) {
callback = providedCallback;
this.showing = true;
this.$el.children[0].components.overlay.show();
// Get initial images if they have not yet been loaded in.
- if (dataLoaded) return;
+ if (dataLoaded && imageType === this.imageType) return;
+ if (imageType) {
+ this.imageType = imageType;
+ this.resetState();
+ }
this.fetchData();
dataLoaded = true;
},
hide() {
+ if (this.$refs.dropzone) {
+ this.$refs.dropzone.onClose();
+ }
this.showing = false;
this.selectedImage = false;
- this.$refs.dropzone.onClose();
this.$el.children[0].components.overlay.hide();
},
@@ -62,13 +69,18 @@ const methods = {
},
setView(viewName) {
+ this.view = viewName;
+ this.resetState();
+ this.fetchData();
+ },
+
+ resetState() {
this.cancelSearch();
this.images = [];
this.hasMore = false;
+ this.deleteConfirm = false;
page = 0;
- this.view = viewName;
- baseUrl = window.baseUrl(`/images/${this.imageType}/${viewName}/`);
- this.fetchData();
+ baseUrl = window.baseUrl(`/images/${this.imageType}/${this.view}/`);
},
searchImages() {
@@ -89,6 +101,7 @@ const methods = {
},
cancelSearch() {
+ if (!this.searching) return;
this.searching = false;
this.searchTerm = '';
this.images = preSearchImages;
@@ -105,6 +118,7 @@ const methods = {
this.callbackAndHide(image);
} else {
this.selectedImage = image;
+ this.deleteConfirm = false;
this.dependantPages = false;
}
@@ -134,17 +148,22 @@ const methods = {
},
deleteImage() {
- let force = this.dependantPages !== false;
- let url = window.baseUrl('/images/' + this.selectedImage.id);
- if (force) url += '?force=true';
- this.$http.delete(url).then(response => {
+
+ if (!this.deleteConfirm) {
+ let url = window.baseUrl(`/images/usage/${this.selectedImage.id}`);
+ this.$http.get(url).then(resp => {
+ this.dependantPages = resp.data;
+ }).catch(console.error).then(() => {
+ this.deleteConfirm = true;
+ });
+ return;
+ }
+
+ this.$http.delete(`/images/${this.selectedImage.id}`).then(resp => {
this.images.splice(this.images.indexOf(this.selectedImage), 1);
this.selectedImage = false;
this.$events.emit('success', trans('components.image_delete_success'));
- }).catch(error=> {
- if (error.response.status === 400) {
- this.dependantPages = error.response.data;
- }
+ this.deleteConfirm = false;
});
},
diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss
index 31e006e27..76150fe44 100644
--- a/resources/assets/sass/_components.scss
+++ b/resources/assets/sass/_components.scss
@@ -146,7 +146,8 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
.dropzone-container {
position: relative;
- border: 3px dashed #DDD;
+ background-color: #EEE;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%23a9a9a9' fill-opacity='0.52' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E");
}
.image-manager-list .image {
@@ -163,8 +164,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
transition: all cubic-bezier(.4, 0, 1, 1) 160ms;
overflow: hidden;
&.selected {
- transform: scale3d(0.92, 0.92, 0.92);
- border: 1px solid #444;
+ //transform: scale3d(0.92, 0.92, 0.92);
+ border: 4px solid #FFF;
+ overflow: hidden;
+ border-radius: 8px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
}
img {
@@ -210,12 +213,30 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
.image-manager-sidebar {
width: 300px;
margin-left: 1px;
- padding: $-m $-l;
overflow-y: auto;
overflow-x: hidden;
border-left: 1px solid #DDD;
+ .inner {
+ padding: $-m;
+ }
+ img {
+ max-width: 100%;
+ max-height: 180px;
+ display: block;
+ margin: 0 auto $-m auto;
+ box-shadow: 0 1px 21px 1px rgba(76, 76, 76, 0.3);
+ }
+ .image-manager-viewer {
+ height: 196px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ a {
+ display: inline-block;
+ }
+ }
.dropzone-container {
- margin-top: $-m;
+ border-bottom: 1px solid #DDD;
}
}
@@ -242,10 +263,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
* Copyright (c) 2012 Matias Meno
*/
.dz-message {
- font-size: 1.2em;
- line-height: 1.1;
+ font-size: 1em;
+ line-height: 2.35;
font-style: italic;
- color: #aaa;
+ color: #888;
text-align: center;
cursor: pointer;
padding: $-l $-m;
diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss
index c7d288ad3..0b2dfbf75 100644
--- a/resources/assets/sass/styles.scss
+++ b/resources/assets/sass/styles.scss
@@ -154,6 +154,7 @@ $btt-size: 40px;
}
input {
flex: 5;
+ padding: $-xs $-s;
&:focus, &:active {
outline: 0;
}
diff --git a/resources/lang/de/components.php b/resources/lang/de/components.php
index 510af4dd3..af07f2698 100644
--- a/resources/lang/de/components.php
+++ b/resources/lang/de/components.php
@@ -12,7 +12,8 @@ return [
'image_uploaded' => 'Hochgeladen am :uploadedDate',
'image_load_more' => 'Mehr',
'image_image_name' => 'Bildname',
- 'image_delete_confirm' => 'Dieses Bild wird auf den folgenden Seiten benutzt. Bitte klicken Sie erneut auf löschen, wenn Sie dieses Bild wirklich entfernen möchten.',
+ 'image_delete_used' => 'Dieses Bild wird auf den folgenden Seiten benutzt. ',
+ 'image_delete_confirm' => 'Bitte klicken Sie erneut auf löschen, wenn Sie dieses Bild wirklich entfernen möchten.',
'image_select_image' => 'Bild auswählen',
'image_dropzone' => 'Ziehen Sie Bilder hierher oder klicken Sie, um ein Bild auszuwählen',
'images_deleted' => 'Bilder gelöscht',
diff --git a/resources/lang/en/components.php b/resources/lang/en/components.php
index 2266fe2b2..c093f7316 100644
--- a/resources/lang/en/components.php
+++ b/resources/lang/en/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => 'Uploaded :uploadedDate',
'image_load_more' => 'Load More',
'image_image_name' => 'Image Name',
- 'image_delete_confirm' => 'This image is used in the pages below, Click delete again to confirm you want to delete this image.',
+ 'image_delete_used' => 'This image is used in the pages below.',
+ 'image_delete_confirm' => 'Click delete again to confirm you want to delete this image.',
'image_select_image' => 'Select Image',
'image_dropzone' => 'Drop images or click here to upload',
'images_deleted' => 'Images Deleted',
diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php
index b699b5c4b..30abbc1b9 100755
--- a/resources/lang/en/settings.php
+++ b/resources/lang/en/settings.php
@@ -51,6 +51,19 @@ return [
'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application.
Note that users will be able to change their email addresses after successful registration.',
'reg_confirm_restrict_domain_placeholder' => 'No restriction set',
+ /**
+ * Maintenance settings
+ */
+
+ 'maint' => 'Maintenance',
+ 'maint_image_cleanup' => 'Cleanup Images',
+ 'maint_image_cleanup_desc' => "Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.",
+ 'maint_image_cleanup_ignore_revisions' => 'Ignore images in revisions',
+ 'maint_image_cleanup_run' => 'Run Cleanup',
+ 'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',
+ 'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',
+ 'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',
+
/**
* Role settings
*/
diff --git a/resources/lang/es/components.php b/resources/lang/es/components.php
index a13d5d4fb..cdcba487d 100644
--- a/resources/lang/es/components.php
+++ b/resources/lang/es/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => 'Subido el :uploadedDate',
'image_load_more' => 'Cargar más',
'image_image_name' => 'Nombre de imagen',
- 'image_delete_confirm' => 'Esta imagen está siendo utilizada en las páginas mostradas a continuación, haga click de nuevo para confirmar que quiere borrar esta imagen.',
+ 'image_delete_used' => 'Esta imagen está siendo utilizada en las páginas mostradas a continuación.',
+ 'image_delete_confirm' => 'Haga click de nuevo para confirmar que quiere borrar esta imagen.',
'image_select_image' => 'Seleccionar Imagen',
'image_dropzone' => 'Arrastre las imágenes o hacer click aquí para Subir',
'images_deleted' => 'Imágenes borradas',
diff --git a/resources/lang/es_AR/components.php b/resources/lang/es_AR/components.php
index ef4f33111..ea61f5f4c 100644
--- a/resources/lang/es_AR/components.php
+++ b/resources/lang/es_AR/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => 'Subido el :uploadedDate',
'image_load_more' => 'Cargar más',
'image_image_name' => 'Nombre de imagen',
- 'image_delete_confirm' => 'Esta imagen esta siendo utilizada en las páginas a continuación, haga click de nuevo para confirmar que quiere borrar esta imagen.',
+ 'image_delete_used' => 'Esta imagen esta siendo utilizada en las páginas a continuación.',
+ 'image_delete_confirm' => 'Haga click de nuevo para confirmar que quiere borrar esta imagen.',
'image_select_image' => 'Seleccionar Imagen',
'image_dropzone' => 'Arrastre las imágenes o hacer click aquí para Subir',
'images_deleted' => 'Imágenes borradas',
diff --git a/resources/lang/fr/components.php b/resources/lang/fr/components.php
index ddfe665d9..438137b5e 100644
--- a/resources/lang/fr/components.php
+++ b/resources/lang/fr/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => 'Ajoutée le :uploadedDate',
'image_load_more' => 'Charger plus',
'image_image_name' => 'Nom de l\'image',
- 'image_delete_confirm' => 'Cette image est utilisée dans les pages ci-dessous. Confirmez que vous souhaitez bien supprimer cette image.',
+ 'image_delete_used' => 'Cette image est utilisée dans les pages ci-dessous.',
+ 'image_delete_confirm' => 'Confirmez que vous souhaitez bien supprimer cette image.',
'image_select_image' => 'Selectionner l\'image',
'image_dropzone' => 'Glissez les images ici ou cliquez pour les ajouter',
'images_deleted' => 'Images supprimées',
diff --git a/resources/lang/it/components.php b/resources/lang/it/components.php
index 081a4c0e6..c9ab18a3e 100755
--- a/resources/lang/it/components.php
+++ b/resources/lang/it/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => 'Uploaded :uploadedDate',
'image_load_more' => 'Carica Altre',
'image_image_name' => 'Nome Immagine',
- 'image_delete_confirm' => 'Questa immagine è usata nelle pagine elencate, clicca elimina nuovamente per confermare.',
+ 'image_delete_used' => 'Questa immagine è usata nelle pagine elencate.',
+ 'image_delete_confirm' => 'Clicca elimina nuovamente per confermare.',
'image_select_image' => 'Seleziona Immagine',
'image_dropzone' => 'Rilascia immagini o clicca qui per caricarle',
'images_deleted' => 'Immagini Eliminate',
diff --git a/resources/lang/ja/components.php b/resources/lang/ja/components.php
index 8e9eb2e47..53a9cda1b 100644
--- a/resources/lang/ja/components.php
+++ b/resources/lang/ja/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => 'アップロード日時: :uploadedDate',
'image_load_more' => 'さらに読み込む',
'image_image_name' => '画像名',
- 'image_delete_confirm' => 'この画像は以下のページで利用されています。削除してもよろしければ、再度ボタンを押して下さい。',
+ 'image_delete_used' => 'この画像は以下のページで利用されています。',
+ 'image_delete_confirm' => '削除してもよろしければ、再度ボタンを押して下さい。',
'image_select_image' => '選択',
'image_dropzone' => '画像をドロップするか、クリックしてアップロード',
'images_deleted' => '画像を削除しました',
diff --git a/resources/lang/nl/components.php b/resources/lang/nl/components.php
index fb306820e..576298ef2 100644
--- a/resources/lang/nl/components.php
+++ b/resources/lang/nl/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => 'Uploaded :uploadedDate',
'image_load_more' => 'Meer Laden',
'image_image_name' => 'Afbeeldingsnaam',
- 'image_delete_confirm' => 'Deze afbeeldingen is op onderstaande pagina\'s in gebruik, Klik opnieuw op verwijderen om de afbeelding echt te verwijderen.',
+ 'image_delete_used' => 'Deze afbeeldingen is op onderstaande pagina\'s in gebruik.',
+ 'image_delete_confirm' => 'Klik opnieuw op verwijderen om de afbeelding echt te verwijderen.',
'image_select_image' => 'Kies Afbeelding',
'image_dropzone' => 'Sleep afbeeldingen hier of klik hier om te uploaden',
'images_deleted' => 'Verwijderde Afbeeldingen',
diff --git a/resources/lang/pl/components.php b/resources/lang/pl/components.php
index c1dbcd44b..177fdba5f 100644
--- a/resources/lang/pl/components.php
+++ b/resources/lang/pl/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => 'Udostępniono :uploadedDate',
'image_load_more' => 'Wczytaj więcej',
'image_image_name' => 'Nazwa obrazka',
- 'image_delete_confirm' => 'Ten obrazek jest używany na stronach poniżej, kliknij ponownie Usuń by potwierdzić usunięcie obrazka.',
+ 'image_delete_used' => 'Ten obrazek jest używany na stronach poniżej.',
+ 'image_delete_confirm' => 'Kliknij ponownie Usuń by potwierdzić usunięcie obrazka.',
'image_select_image' => 'Wybierz obrazek',
'image_dropzone' => 'Upuść obrazki tutaj lub kliknij by wybrać obrazki do udostępnienia',
'images_deleted' => 'Usunięte obrazki',
diff --git a/resources/lang/pt_BR/components.php b/resources/lang/pt_BR/components.php
index 03fd4ac0c..872c00c9f 100644
--- a/resources/lang/pt_BR/components.php
+++ b/resources/lang/pt_BR/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => 'Carregado :uploadedDate',
'image_load_more' => 'Carregar Mais',
'image_image_name' => 'Nome da Imagem',
- 'image_delete_confirm' => 'Essa imagem é usada nas páginas abaixo. Clique em Excluir novamente para confirmar que você deseja mesmo eliminar a imagem.',
+ 'image_delete_used' => 'Essa imagem é usada nas páginas abaixo.',
+ 'image_delete_confirm' => 'Clique em Excluir novamente para confirmar que você deseja mesmo eliminar a imagem.',
'image_select_image' => 'Selecionar Imagem',
'image_dropzone' => 'Arraste imagens ou clique aqui para fazer upload',
'images_deleted' => 'Imagens excluídas',
diff --git a/resources/lang/ru/components.php b/resources/lang/ru/components.php
index 49aed1e87..6ee44d6be 100644
--- a/resources/lang/ru/components.php
+++ b/resources/lang/ru/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => 'Загруженно :uploadedDate',
'image_load_more' => 'Загрузить ещё',
'image_image_name' => 'Имя изображения',
- 'image_delete_confirm' => 'Это изображение используется на странице ниже. Снова кликните удалить для подтверждения того что вы хотите удалить.',
+ 'image_delete_used' => 'Это изображение используется на странице ниже.',
+ 'image_delete_confirm' => 'Снова кликните удалить для подтверждения того что вы хотите удалить.',
'image_select_image' => 'Выбрать изображение',
'image_dropzone' => 'Перетащите изображение или кликните для загрузки',
'images_deleted' => 'Изображения удалены',
diff --git a/resources/lang/sk/components.php b/resources/lang/sk/components.php
index f4fa92043..c20b62c7a 100644
--- a/resources/lang/sk/components.php
+++ b/resources/lang/sk/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => 'Nahrané :uploadedDate',
'image_load_more' => 'Načítať viac',
'image_image_name' => 'Názov obrázka',
- 'image_delete_confirm' => 'Tento obrázok je použitý na stránkach uvedených nižšie, kliknite znova na zmazať pre potvrdenie zmazania tohto obrázka.',
+ 'image_delete_used' => 'Tento obrázok je použitý na stránkach uvedených nižšie.',
+ 'image_delete_confirm' => 'Kliknite znova na zmazať pre potvrdenie zmazania tohto obrázka.',
'image_select_image' => 'Vybrať obrázok',
'image_dropzone' => 'Presuňte obrázky sem alebo kliknite sem pre nahranie',
'images_deleted' => 'Obrázky zmazané',
diff --git a/resources/lang/sv/components.php b/resources/lang/sv/components.php
index ec1dad1aa..8b1e95ec6 100644
--- a/resources/lang/sv/components.php
+++ b/resources/lang/sv/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => 'Laddades upp :uploadedDate',
'image_load_more' => 'Ladda fler',
'image_image_name' => 'Bildnamn',
- 'image_delete_confirm' => 'Den här bilden används på nedanstående sidor, klicka på "ta bort" en gång till för att bekräfta att du vill ta bort bilden.',
+ 'image_delete_used' => 'Den här bilden används på nedanstående sidor.',
+ 'image_delete_confirm' => 'Klicka på "ta bort" en gång till för att bekräfta att du vill ta bort bilden.',
'image_select_image' => 'Välj bild',
'image_dropzone' => 'Släpp bilder här eller klicka för att ladda upp',
'images_deleted' => 'Bilder borttagna',
diff --git a/resources/lang/zh_CN/components.php b/resources/lang/zh_CN/components.php
index 9e6f28649..42857b9e7 100644
--- a/resources/lang/zh_CN/components.php
+++ b/resources/lang/zh_CN/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => '上传于 :uploadedDate',
'image_load_more' => '显示更多',
'image_image_name' => '图片名称',
- 'image_delete_confirm' => '该图像用于以下页面,如果你想删除它,请再次按下按钮。',
+ 'image_delete_used' => '该图像用于以下页面。',
+ 'image_delete_confirm' => '如果你想删除它,请再次按下按钮。',
'image_select_image' => '选择图片',
'image_dropzone' => '拖放图片或点击此处上传',
'images_deleted' => '图片已删除',
diff --git a/resources/lang/zh_TW/components.php b/resources/lang/zh_TW/components.php
index ae0a083ad..e8826eebb 100644
--- a/resources/lang/zh_TW/components.php
+++ b/resources/lang/zh_TW/components.php
@@ -13,7 +13,8 @@ return [
'image_uploaded' => '上傳於 :uploadedDate',
'image_load_more' => '載入更多',
'image_image_name' => '圖片名稱',
- 'image_delete_confirm' => '所使用圖片目前用於以下頁面,如果你想刪除它,請再次按下按鈕。',
+ 'image_delete_used' => '所使用圖片目前用於以下頁面。',
+ 'image_delete_confirm' => '如果你想刪除它,請再次按下按鈕。',
'image_select_image' => '選擇圖片',
'image_dropzone' => '拖曳圖片或點選這裡上傳',
'images_deleted' => '圖片已刪除',
diff --git a/resources/views/components/image-manager.blade.php b/resources/views/components/image-manager.blade.php
index 78c6435d6..eca35b8aa 100644
--- a/resources/views/components/image-manager.blade.php
+++ b/resources/views/components/image-manager.blade.php
@@ -1,5 +1,5 @@