1.表題
今回は、文字列をユニコードエンコードする際の処理について記載します。
2.背景
WebページにおけるJavaScriptの出力文字にて、
scriptタグを閉めるようなものが入った場合には、不正な動作をすることがあり、
対応としてJavaScriptで出力される文字を、ユニコードへエンコードしたいということになりました。
※例
\"<\/script><img src onerror=alert(1);><script>a=\"
上記のような、scriptタグを閉めるようなものを、JavaScriptに渡してしまうと
、javascriptの終了箇所と認識され、不正なアラートが表示される。
また、ユニコードエンコードは、以下のサイトのような形で出力されることが理想
https://www.oh-benri-tools.com/tools/programming/unicode-escape-unescape
そのため、今回は、Javaで文字列をユニコードエンコードし、返すメソッドを追加しました。
呼び出し側のJavaScriptと処理側のJavaで分けて記載します。
3.JavaScript側
※ソースに含まれるJavaScriptをF12などの開発者ツールで見たときに正しく表示されない場合があった
この後に作る関数(Convert.unicodeEncode())を以下のように呼び出します。
funciton foo()
{
var input = <% Convert.unicodeEncode()%>
console.log(input);
}
4.Java側
※利用者のことも考え、JavaDocには厚めに利用の例を羅列しています。
今回は一覧のような文字列が設定された時の想定を全て入れております。
(レビュー指摘で網羅)
・null
・空文字
・空白文字
・ASCII文字(数値、アルファベット、記号)
・マルチバイト文字(ひらがな、カタカナ、漢字、shift-jisに無い漢字や外国語)
・絵文字
※また、JavaDocにおいて長文だと崩れるため、preタグで挟み、崩れを防いでおります。
/**
* 文字列をUnicodeエンコードする
*
* <pre>
* Convert.unicodeEncode(null) = null
* Convert.unicodeEncode("") = ""
* Convert.unicodeEncode(" \t\n\r\f") = "\\u0020\\u0009\\u000a\\u000d\\u000c"
* Convert.unicodeEncode("0aA!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") = "\\u0030\\u0061\\u0041\\u0021\\u0022\\u0023\\u0024\\u0025\\u0026\\u0027\\u0028\\u0029\\u002a\\u002b\\u002c\\u002d\\u002e\\u002f\\u003a\\u003b\\u003c\\u003d\\u003e\\u003f\\u0040\\u005b\\u005c\\u005d\\u005e\\u005f\\u0060\\u007b\\u007c\\u007d\\u007e"
* Convert.unicodeEncode("あいアイ照愛髙彅") = "\\u3042\\u3044\\u30a2\\u30a4\\u7167\\u611b\\u9ad9\\u5f45"
* Convert.unicodeEncode("😉😊😋😋😃") = "\\ud83d\\ude09\\ud83d\\ude0a\\ud83d\\ude0b\\ud83d\\ude0b\\ud83d\\ude03"
* Convert.unicodeEncode("\u0061") = "\\u0061"
* </pre>
*
* @param input 処理の対象の文字列
* @return 置換処理後の文字列
*/
static public String unicodeEncode(String input)
{
if (input == null || input.isEmpty())
return (input);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < input.length(); i++)
{
char currentChar = input.charAt(i);
// 絵文字の場合の変換対応
if (Character.isHighSurrogate(currentChar))
{
char nextChar = input.charAt(i + 1);
sb.append(String.format("\\u%04x", (int) currentChar));
sb.append(String.format("\\u%04x", (int) nextChar));
i++;
}
else
{
// 通常の文字
sb.append(String.format("\\u%04x", (int) currentChar));
}
}
return sb.toString();
}
ここでのポイントは絵文字の場合の対応も可能とする場合に、他のユニコードエンコードが正しく出力されないことがあり、絵文字とそれ以外で変換方法を分けております。
5.Java側(Junitファイル)
上記の関数をテストする際にJunit4でのテストも以下のように行いました。
パターンは関数のJavaDocのもの一覧
・null
・空文字
・空白文字
・ASCII文字(数値、アルファベット、記号)
・マルチバイト文字(ひらがな、カタカナ、漢字、shift-jisに無い漢字や外国語)
・絵文字
+α
import static org.junit.jupiter.api.Assertions.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import mockit.integration.junit4.JMockit;
@RunWith(JMockit.class)
public class ConvertTest
{
/**
* nullのチェック
*/
@Test
public final void testEncodeUnicode1()
{
String target = Convert.unicodeEncode(null);
assertEquals(null, target);
}
/**
* 空文字のチェック
*/
@Test
public final void testEncodeUnicode2()
{
String target = Convert.unicodeEncode("");
assertEquals("", target);
}
/**
* 空白文字のチェック
*/
@Test
public final void testEncodeUnicode3()
{
String target = Convert.unicodeEncode(" \t\n\r\f");
assertEquals("\\u0020\\u0009\\u000a\\u000d\\u000c", target);
}
/**
* ASCII文字(数値、アルファベット、記号)においての全文字のチェック
*/
@Test
public final void testEncodeUnicode4()
{
String target = Convert.unicodeEncode(
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
);
assertEquals(
"\\u0030\\u0031\\u0032\\u0033\\u0034\\u0035\\u0036\\u0037\\u0038\\u0039\\u0061\\u0062\\u0063\\u0064\\u0065\\u0066\\u0067\\u0068\\u0069\\u006a\\u006b\\u006c\\u006d\\u006e\\u006f\\u0070\\u0071\\u0072\\u0073\\u0074\\u0075\\u0076\\u0077\\u0078\\u0079\\u007a\\u0041\\u0042\\u0043\\u0044\\u0045\\u0046\\u0047\\u0048\\u0049\\u004a\\u004b\\u004c\\u004d\\u004e\\u004f\\u0050\\u0051\\u0052\\u0053\\u0054\\u0055\\u0056\\u0057\\u0058\\u0059\\u005a\\u0021\\u0022\\u0023\\u0024\\u0025\\u0026\\u0027\\u0028\\u0029\\u002a\\u002b\\u002c\\u002d\\u002e\\u002f\\u003a\\u003b\\u003c\\u003d\\u003e\\u003f\\u0040\\u005b\\u005c\\u005d\\u005e\\u005f\\u0060\\u007b\\u007c\\u007d\\u007e",
target
);
}
/**
* マルチバイト文字(ひらがな、カタカナ、漢字、shift-jisに無い漢字)文字のチェック
*/
@Test
public final void testEncodeUnicode5()
{
String target = Convert.unicodeEncode("あいアイ照愛髙彅");
assertEquals("\\u3042\\u3044\\u30a2\\u30a4\\u7167\\u611b\\u9ad9\\u5f45", target);
}
/**
* 絵文字のチェック
*/
@Test
public final void testEncodeUnicode6()
{
String target = Convert.unicodeEncode("😉😊😋😋😃");
assertEquals("\\ud83d\\ude09\\ud83d\\ude0a\\ud83d\\ude0b\\ud83d\\ude0b\\ud83d\\ude03", target);
}
/**
* Scriptタグが含まれる文字のチェック
*/
@Test
public final void testEncodeUnicode7()
{
String target = Convert.unicodeEncode("\"</script><img src onerror=alert(1);><script>a=\"");
assertEquals(
"\\u0022\\u003c\\u002f\\u0073\\u0063\\u0072\\u0069\\u0070\\u0074\\u003e\\u003c\\u0069\\u006d\\u0067\\u0020\\u0073\\u0072\\u0063\\u0020\\u006f\\u006e\\u0065\\u0072\\u0072\\u006f\\u0072\\u003d\\u0061\\u006c\\u0065\\u0072\\u0074\\u0028\\u0031\\u0029\\u003b\\u003e\\u003c\\u0073\\u0063\\u0072\\u0069\\u0070\\u0074\\u003e\\u0061\\u003d\\u0022",
target
);
}
/**
* ユニコードが含まれる文字のチェック
*/
@Test
public final void testEncodeUnicode8()
{
String target = Convert.unicodeEncode("\u0061");
assertEquals("\\u0061", target);
}
/**
* 絵文字とその他の文字が混在している際のチェック
*/
@Test
public final void testEncodeUnicode9()
{
// 絵文字の場合とそうでない場合とで場合分けして処理を分岐しているため、
// 以下のように複数の文字が組み合わさっている際に適切な単位で分割して処理されるかも確認する。
// <ユニコード文字>b<ユニコード文字>0<絵文字>2<合字><絵文字>
String target = Convert.unicodeEncode("\u0061b\u006301️⃣2👨🏻🦱👨");
assertEquals(
"\\u0061\\u0062\\u0063\\u0030\\u0031\\ufe0f\\u20e3\\u0032\\ud83d\\udc68\\ud83c\\udffb\\u200d\\ud83e\\uddb1\\ud83d\\udc68",
target
);
}
}
上記を行うことで、JavaScritpではソース表示時にそのままの値で表示されなくなり、
正しい値でConsole.logへ表示されます。
6.終わりに
今回のテストパターンの網羅やJavaDocの厚さも全てレビュー指摘されたもの
なので、(最初はJavaDocは@paramと@returnのみだったが、指摘でjunitのパターン一覧の例を載せるべきというものがありました、値が長いため、一部違う例もあります。)
変換処理を今後作る際は自分で同じようなくらいは網羅できるように、ここに残します。