Grav CMS + TinyMCE: загрузка изображений при вставке (Ctrl + V)
В плагине tinymce при вставке изображения на сервер уходит всё тело картинки в base64, из-за ограничения на размер content для страницы Grav CMS выкидывает ошибку и не сохраняет, поэтому мне пришлось поймать вставку изображения в редактор, перехватить, отправить на сервер сохранить и вставить в текст уже тег с ссылкой на изображение <img src="url">. Пришлось много дебажить как работает плагин admin и сам движок, в итоге удалось найти решение, при котором не пришлось менять исходники движка.
Для начала необходимо сделать копию файла настроек user/plugins/tinymce-editor/tinymce-editor.yaml -> user/config/plugins/tinymce-editor.yaml. Теперь в новом файле можно регулировать поведение плагина, а именно необходимо отключить вставку изображений: в разделе parameters надо поставить value = 0 для name = paste_data_image. И тут же рядом есть настройка evals, он вставляется внутрь объекта инициализации редактора (в js коде):
tinymce.init({
// здесь настройки самого плагина
{{ config.plugins["tinymce-editor"].evals|raw }}
});
Я добавил глобальный объект kakvam до инициализации скрипта, в котором функция отлавливает изображения и отсылает на сервер сохранить, а потом вставляет ссылку в редактор. Код для настройки плагина:
evals: 'init_instance_callback: kakvam.onTinymceInit'
Код самой функции в js:
window.kakvam = {
onTinymceInit: function(editor) {
editor.on('PastePreProcess', function(e) {
if (e.content.indexOf("<img src=\"data:image") === 0) {
$.ajax({
url: '/admin/api/upload',
method: 'post',
data: {
content: e.content,
page: location.pathname.replace('/admin/pages', '').split('?')[0]
},
success(response) {
tinymce.execCommand('mceInsertContent', false, '<img src="' + response.image + '"/>');
}
});
e.content = '';
}
})
}
}
Мой класс темы после изменений выглядит так:
class Kakvam extends Theme
{
private $context;
public static function getSubscribedEvents()
{
return [
'onThemeInitialized' => ['onThemeInitialized', 0],
'onRequestHandlerInit' => [
['onRequestHandlerInit', 100000]
]
];
}
public function onThemeInitialized() {
$this->context = [];
if ($this->isAdmin()) {
$this->enable([
'onAdminSave' => ['onAdminSave', 0],
'onAssetsInitialized' => ['onAdminAssetsInitialized', 0],
'onPageInitialized' => ['onAdminPageInitialized', 0]
]);
} else {
$this->enable([
'onPageInitialized' => ['onAdminPageInitialized', 0]
]);
}
}
public function onRequestHandlerInit(RequestHandlerEvent $event) {
$route = $event->getRoute();
$path = $route->getRoute();
if ($path == '/admin/api/upload') {
$event->addMiddleware('kakvam_middleware', new KakvamMiddleware($this->grav, $this->context));
}
}
private function saveImage($folder, $data) {
if (preg_match('/^data:image\/(\w+);base64,/', $data, $type)) {
$data = substr($data, strpos($data, ',') + 1);
$type = strtolower($type[1]); // jpg, png, gif
if (!in_array($type, [ 'jpg', 'jpeg', 'gif', 'png' ])) {
throw new \Exception('invalid image type');
}
$data = str_replace( ' ', '+', $data );
$data = base64_decode($data);
if ($data === false) {
throw new \Exception('base64_decode failed');
}
} else {
throw new \Exception('did not match data URI with image data');
}
$filename = uniqid() . '.' . $type;
file_put_contents($folder . '/' . $filename, $data);
return $filename;
}
public function onAdminPageInitialized() {
if (!isset($this->context['upload_data'])) {
return;
}
/* @var \Grav\Common\Page\Pages $pages */
$pages = $this->grav['pages'];
$pages->enablePages();
/* @var \Grav\Common\Page\Page */
$page = $pages->find($this->context['upload_data']['page']);
$folderPath = $page->getMediaFolder();
$pages->disablePages();
$content = $this->context['upload_data']['content'];
preg_match("/<img src=\"([^\"]+)\"(\s|>)/m", $content, $matches);
$data = $matches[1];
$filename = $this->saveImage($folderPath, $data);
$this->grav->close(new Response(200, ['Content-Type' => 'application/json'], json_encode(['image' => $filename])));
}
public function onAdminAssetsInitialized() {
/* @var \Grav\Common\Assets $assets */
$assets = $this->grav['assets'];
$assets->addJs('theme://js/admin.js');
}
public function onAdminSave(Event $event) {
// maybe use it in future
}
}
И еще написал middleware:
class KakvamMiddleware extends ProcessorBase {
private $context;
public function __construct(Grav $container, &$context)
{
parent::__construct($container);
$this->context = &$context;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
if ($request->getMethod() == 'POST') {
$body = $request->getParsedBody();
$this->context['upload_data'] = $body;
}
$response = $handler->handle($request);
$this->stopTimer();
return $response;
}
}