さて、
このシリーズを書くのは久しぶりだったりします。
クラシックエディタにクイックタグを追加導入した理由
ところで、
自分は投稿を書くのに、ブロックエディタではなくもっぱらクラシックエディタ(テキストエディタ)を使っています。シリーズのこの回でも書いていますが、クラシックエディタには使い勝手を向上させるためにクイックタグというのがあります。以下のような、HTMLタグを入れる際に便利なボタンのことです。
このサイトでは、
プログラムのソースコードを載せることがあるので、シンタックスハイライトさせるために Prism.js を導入し、ソースコードの入力を楽にするためにこちらで書いたようにクイックタグを追加しています。
ソースコードの場合はエスケープしないといけないのが面倒
ただソースコードの場合は、
こちらのツール等を使って、いちいちHTMLな特殊文字をエスケープしないと行けないのが面倒だったりします。エスケープというのは、HTMLを記述するのに必要な「<」や「>」等といったメタ文字を、メタではない文字として識別させるための変換です。XSS攻撃を防ぐという意味もあります。ブログのコメントとかに入力された悪意あるスクリプトが実行されてしまったりしたら、脆弱性となってしまいます。一方、シンタックスハイライトという意味では、貼り付けたソースコードが単なるテキストとして表示されるようにエスケープするわけです。
ということで、
ソースコードを貼り付ける際にエスケープし忘れてたりして面倒なので、自動でどうにかできないものかと思った次第です。クイックタグに関する処理は wp-includes/js/quicktags.js でやっているので、まずはこれをよく読んでみることにしました。
その結果、
クイックタグなボタンを押すと、callback
なる関数が呼ばれて処理されることが分かりました。この関数を書き換えて、入力されたソースコードをエスケープするように改造すれば良さそうです。
なお、
HTMLな特殊文字をエスケープするやり方はこちらを参考にしました。エスケープすべき文字として、「"」「&」「'」「<」「>」「`」の6文字を対象としていますが、バッククォートはやらなくても良いかもしれません。実際、PHP の htmlspecialchars()
では変換対象となる文字は5つとなっています。
そんなわけで、
functions.php に書いたクイックタグに関するコードを以下のように変更しました。いちいち手動でエスケープしなくて済むようになったので、だいぶ楽チンになりました(^_^)
add_action( 'admin_print_footer_scripts', function() {
// テキストエデイタにクイックタグを追加➔https://wpdocs.osdn.jp/%E3%82%AF%E3%82%A4%E3%83%83%E3%82%AF%E3%82%BF%E3%82%B0API
if ( wp_script_is( 'quicktags' ) ) {
?>
<script>
(function(){
// wp-includes/js/quicktags.jsの700行目あたりで既定のクイックタグが設定されている。
// タグ名と一致させるために既定の表示名を変更。
edButtons[10].display = 'strong';
edButtons[20].display = 'em';
// 順番も考慮しつつ、隙間に登録。
edButtons[9] = new QTags.TagButton('b', 'b', '<b>', '</b>');
edButtons[121] = new QTags.TagButton('h2', 'h2', '<h2>', '</h2>\n');
edButtons[122] = new QTags.TagButton('h3', 'h3', '<h3>', '</h3>\n');
edButtons[123] = new QTags.TagButton('h4', 'h4', '<h4>', '</h4>\n');
edButtons[124] = new QTags.TagButton('h5',' h5', '<h5>', '</h5>\n');
edButtons[125] = new QTags.TagButton('h6',' h6', '<h6>', '</h6>\n');
// for prism.js
edButtons[126] = new QTags.TagButton('html','html', '<pre><code class="language-markup">', '</code></pre>\n');
edButtons[127] = new QTags.TagButton('css','css', '<pre><code class="language-css">', '</code></pre>\n');
edButtons[128] = new QTags.TagButton('js','js', '<pre><code class="language-javascript">', '</code></pre>\n');
edButtons[129] = new QTags.TagButton('php','php', '<pre><code class="language-php">', '</code></pre>\n');
edButtons[130] = new QTags.TagButton('bash','bash', '<pre><code class="language-bash">', '</code></pre>\n');
// callbackを書き換えて、自動でhtml特殊文字をエスケープさせる。
edButtons[126].callback = callback;
edButtons[127].callback = callback;
edButtons[128].callback = callback;
edButtons[129].callback = callback;
edButtons[130].callback = callback;
function escape(t){ // html特殊文字をエスケープ -> https://pisuke-code.com/javascript-imple-htmlspecialchars/
// 参考:https://www.php.net/manual/ja/function.htmlspecialchars.php
return t.replace(/[&'`"<>]/g,function(m) {
return {
'"': '"', // 数字表記の方が " より短いので。
'&': '&',
"'": ''', // 数字表記の方が ' より短いので。
'<': '<',
'>': '>',
'`': '`' // バッククォートはやらなくても良いかも。
}[m];
});
}
function callback(element, canvas, ed) { // wp-includes/js/quicktags.js から引用して改造。
var t = this, startPos, endPos, cursorPos, scrollTop, v = canvas.value, l, r, i, sel, endTag = v ? t.tagEnd : '', event;
if ( document.selection ) { // IE.
canvas.focus();
sel = document.selection.createRange();
if ( sel.text.length > 0 ) {
if ( !t.tagEnd ) {
sel.text = escape(sel.text) + t.tagStart;
} else {
sel.text = t.tagStart + escape(sel.text) + endTag;
}
} else {
if ( !t.tagEnd ) {
sel.text = t.tagStart;
} else if ( t.isOpen(ed) === false ) {
sel.text = t.tagStart;
t.openTag(element, ed);
} else {
sel.text = endTag;
t.closeTag(element, ed);
}
}
canvas.focus();
} else if ( canvas.selectionStart || canvas.selectionStart === 0 ) { // FF, WebKit, Opera.
startPos = canvas.selectionStart;
endPos = canvas.selectionEnd;
if ( startPos < endPos && v.charAt( endPos - 1 ) === '\n' ) {
endPos -= 1;
}
cursorPos = endPos;
scrollTop = canvas.scrollTop;
l = v.substring(0, startPos); // Left of the selection.
r = v.substring(endPos, v.length); // Right of the selection.
i = escape(v.substring(startPos, endPos)); // Inside the selection.
if ( startPos !== endPos ) {
if ( !t.tagEnd ) {
canvas.value = l + i + t.tagStart + r; // Insert self-closing tags after the selection.
cursorPos += t.tagStart.length;
} else {
canvas.value = l + t.tagStart + i + endTag + r;
cursorPos += t.tagStart.length + endTag.length;
}
} else {
if ( !t.tagEnd ) {
canvas.value = l + t.tagStart + r;
cursorPos = startPos + t.tagStart.length;
} else if ( t.isOpen(ed) === false ) {
canvas.value = l + t.tagStart + r;
t.openTag(element, ed);
cursorPos = startPos + t.tagStart.length;
} else {
canvas.value = l + endTag + r;
cursorPos = startPos + endTag.length;
t.closeTag(element, ed);
}
}
canvas.selectionStart = cursorPos;
canvas.selectionEnd = cursorPos;
canvas.scrollTop = scrollTop;
canvas.focus();
} else { // Other browsers?
if ( !endTag ) {
canvas.value += t.tagStart;
} else if ( t.isOpen(ed) !== false ) {
canvas.value += t.tagStart;
t.openTag(element, ed);
} else {
canvas.value += endTag;
t.closeTag(element, ed);
}
canvas.focus();
}
if ( document.createEvent ) {
event = document.createEvent( 'HTMLEvents' );
event.initEvent( 'change', false, true );
canvas.dispatchEvent( event );
} else if ( canvas.fireEvent ) {
canvas.fireEvent( 'onchange' );
}
}
}());
</script>
<?php
}
},999); // 優先度を下げないと quicktags.js より前に読み込まれてしまってマズイ(^_^;)