-

сайт небольшого сообщества специалистов в сфере IT;
здесь мы делимся экспертизой и наработками

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;
    }
}