Blog

Craft CMSのCKEditorで、Office製品の貼り付けをプレーンテキストにする話

みなさんこんにちは。バックエンドエンジニアの山口です。

最近はCraft CMSを使ったWebサイトに携わることが多いです。今回は、Craft CMS向けCKEditorプラグイン(craftcms/ckeditor)で、貼り付け時の書式や画像を取り除く設定をした話です。

今回の対象バージョンは craftcms/ckeditor:4.11.4 です。このプラグインが内部で使っているのはCKEditor 5です。

何が起きたか

メール本文用のリッチテキストエディタに、WordやOutlookから原稿をコピーして貼り付けました。

文字色や太字などの書式が残るだけなら、大きな問題にはなりません。
問題は画像ファイルです。クリップボード上の画像がBase64のdata URI(<img src="data:image/...;base64,...">)として本文に入ります。迷惑ですね。

その結果、次のようなことが起きました。

  • 原稿のHTMLが巨大になる。
  • その本文をメールとして送ると、受信側のスパムフィルタに弾かれる。
  • 管理画面の描画が重くなる。

私の環境では、高負荷によりブラウザの応答がなくなりました。

「貼り付けそのものを禁止する」のではなく、Ctrl / Cmd + vでも常にプレーンテキスト形式でペーストされるようにするのが今回の目標です。
ツールバーから手動で設定できる太字やリンク、文字色などは引き続き使えるようにします。

なぜ標準設定では足りないか

craftcms/ckeditor は、CKEditor本体をそのまま使っているわけではありません。Craft CMSの管理画面向けにビルドされており、以下のようなプラグインが常に有効です。

  • PasteFromOffice: Wordなどの書式をできるだけ残す。
  • GeneralHtmlSupport(GHS): 設定で許可したタグやスタイルは、ツールバーに無くても残す。

貼り付け時、CKEditor 5はクリップボードの text/html を優先します。WordやOutlookのHTMLには、色、フォント、画像のdata URIが含まれます。プレーンテキストになるのはCtrl / Cmd + Shift + vで貼り付けたときだけです。運用で「Shiftを押しながら貼り付けてください」と案内するのは現実的ではありません。

CKEditor 4には forcePasteAsPlainText オプションがありました。CKEditor 5には同等の設定がありません。JSONのConfig optionsに forcePasteAsPlainText: true を定義しても効果はありません。

htmlSupport.disallow で img を拒否する案も考えられますが、有効なプラグインの機能までは止められません。画像対策としては不十分です。removePlugins だけで PasteFromOffice や AutoImage を外しても、HTMLテキストの色や太字は残ることがあります。

方針

Craft CMSのCKEditor設定は、管理画面の「設定」→「CKEditor」で管理できます。各設定にはConfig optionsがあり、次の2種類の形式を選べます。

  • JSON: 静的なオプションだけ設定できます。
  • JavaScript: 即時実行関数の中身として実行されます。引数は渡されません。

今回は関数を追加する必要があるので、JavaScriptモードが必須です。

以下の2点の設定を追加します。

  1. extraPlugins でカスタムプラグインを追加し、clipboardInput で text/plain だけを挿入する。
  2. removePlugins で PasteFromOffice と AutoImage を外す。

貼り付けの制限はブラウザ側の対策です。ソース編集や既存文面の再利用では、画像を含むHTMLが残る可能性があります。貼り付けだけでなく手動でHTMLを入力する運用の場合は、サーバ側でも <img> や data: を取り除いたりエラーを返すのが安全です。

設定方法

対象のCKEditor設定を開き、Config optionsをJavaScriptに切り替えます。既存のJSONがある場合は、その内容を return { ... } の中に移してから追加します。

関数定義と pluginName は、return より前に置く必要があります。Craft CMSはConfig optionsを、次のような関数の中に埋め込んで実行します。

(function () {
  // ここに書いた JavaScript が入る
})()

return の後ろに pluginName を書くと、その行は実行されません。CKEditor 5は pluginName が無いと plugincollection-missing-plugin-name エラーにより初期化に失敗し、エディタが表示されなくなります。

function ForcePastePlainText(editor) {
  const viewDocument = editor.editing.view.document;

  viewDocument.on('clipboardInput', (evt, data) => {
    if (editor.isReadOnly) {
      return;
    }

    const files = data.dataTransfer.files || [];
    const plainText = data.dataTransfer.getData('text/plain') || '';

    // 画像ファイルのみの貼り付け/ドロップは破棄する
    if (files.length && !plainText) {
      evt.stop();
      return;
    }

    const html = plainText
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/\r?\n/g, '<br>');

    data.content = editor.data.htmlProcessor.toView(
      html ? '<p>' + html + '</p>' : ''
    );
  }, { priority: 'high' });
}

ForcePastePlainText.pluginName = 'ForcePastePlainText';

return {
  extraPlugins: [ForcePastePlainText],
  removePlugins: [
    'PasteFromOffice',
    'AutoImage'
  ],
  // すでに別の設定が定義されている場合は、ここに記載してください。
};

clipboardInput は、CKEditor 5で貼り付けやドロップを処理するときに最初に呼ばれるイベントです。ここで data.content を差し替えると、以降の処理には、プレーンテキストから作ったHTMLだけが渡されます。priority: 'high' を付けているのは、標準の text/html の処理より先に動かすためです。

既存の link や wordCount、htmlSupport がある場合は、同じ return オブジェクトの中に一緒に記述して問題ありません。貼り付け時には htmlSupport.allow は使われなくなります。残しておく意味は「ツールバーやソース編集、既存の文面で色・サイズを使えるようにする」ことです。貼り付け以外も含めて装飾を禁止したい場合は、htmlSupport ごと削除します。

管理画面のLinterがカスタム関数をエラー扱いすることがありますが、動作を確認して問題なければ無視して構いません。

確認方法

CKEditor設定の保存後、対象のフィールドを表示して以下の項目を確認します。

  1. WordやOutlookで書式を設定した文章をコピーし、CKEditorへCtrl / Cmd + vで貼り付けると、書式なしで貼り付けられる。
  2. 画像付きの原稿をコピーし、Ctrl / Cmd + vで貼り付けると、テキストだけが貼り付けられ、画像は入らない。
  3. ツールバーの太字・リンクは利用できる。
  4. ブラウザのコンソールにCKEditorの初期化エラーが無い。

試したけどうまくいかなかった方法

以下は、試したものの効果が無かった方法です。

方法 結果
JSONの forcePasteAsPlainText: true CKEditor 5では無効
htmlSupport.disallow のみ定義する 有効なプラグインの機能は残る。画像対策としては不十分

補足事項: craftcms/ckeditorのソースについて

CKEditorのプラグイン設定は、以下のファイルで定義されています。
https://github.com/craftcms/ckeditor/blob/4.11.4/src/web/assets/ckeditor/src/ckeditor5-craftcms.js

管理画面のエディタは、Field.php から CKEditor5.craftcms.create() を呼び、このファイルの create() に到達します。

デフォルトで有効なプラグインは、allPlugins に定義されている一覧です。ここに PasteFromOffice、GeneralHtmlSupport、AutoImage、Image などが含まれています。
https://github.com/craftcms/ckeditor/blob/4.11.4/src/web/assets/ckeditor/src/ckeditor5-craftcms.js#L66

create() は allPlugins の一覧を元に、ツールバーに無いボタンに対応するプラグインだけを外します。
https://github.com/craftcms/ckeditor/blob/4.11.4/src/web/assets/ckeditor/src/ckeditor5-craftcms.js#L554

ボタンとプラグインの対応表が pluginButtonMap です。画像関連のプラグインは、ツールバーの insertImage に紐づいています。
https://github.com/craftcms/ckeditor/blob/4.11.4/src/web/assets/ckeditor/src/ckeditor5-craftcms.js#L178-L193

さいごに

Craft CMSのCKEditorは管理画面との連携が作り込まれている一方、CKEditor 5本体や旧CKEditor 4のノウハウやドキュメントがそのまま使えないことがあります。今回も「プレーンテキストで貼り付けを強制したい」という単純な要件に対して、公式の設定項目は無く、Config optionsのJavaScriptとクリップボードのイベントを組み合わせる必要がありました。

貼り付け時の制限はブラウザ側の対策です。メール送信のようにHTMLをそのまま使う場合は、サーバ側のバリデーションで画像ファイルや data: URIを防ぐことを検討してください。

同じような事故を防ぐヒントになれば幸いです。

 
↑