2008年4月10日木曜日

意外と面倒くさい autolink

どうも。こんにちは。今回は Wiki などに使われるテキスト中の URL を自動的にリンクする方法について書いてみたいと思います。

Java で正規表現を使って実装するとこんな感じでしょうか。
import java.util.regex.*;
private static Pattern linkPattern = Pattern.compile(
"(https?)(://[\\p{Alnum}\\+\\$\\;\\?\\.%,!#~*/:@&=_-]+)");
public static String link(String s, int length, String suffix) {
StringBuffer buff = new StringBuffer((int) (s.length() * 1.2f));
Matcher m = linkPattern.matcher(s);
while (m.find()) {
String g = m.group();
if (length > 0 && g.length() > length) {
g = g.substring(0, length);
if (suffix != null) {
g += suffix;
}
}
g = m.quoteReplacement(g);
m.appendReplacement(buff, "<a href=\"$1$2\">" + g + "</a>");
}
m.appendTail(buff);
return new String(buff);
}

ところが、ユーザの投稿した文書をこれでフィルタして HTML として出力するときには CSS(クロスサイトスクリプティング)対策としてサニタイジングを行う必要があり、少し注意しなければなりません。例えば、次のような文書をユーザが投稿したとします。
URL サンプルでは <http://example.com/> を使いましょう。

まず、CSS 対策としてサニタイジングします。
URL サンプルでは &lt;http://example.com/&gt; を使いましょう。

これを自動リンクフィルタすると次のようになります。
URL サンプルでは&lt;<a href="http://example.com/&gt;">http://example.com/&gt;</a>を使いましょう。

本来は http://example.com/ をリンクとして出力したいのですが、http://example.com/&gt; がリンクになってしまいます。

これを回避するためにサニタイジングと自動リンクを同時に行います。
private static Pattern linkHtmlPattern = Pattern.compile(
"((https?)(://[\\p{Alnum}\\+\\$\\;\\?\\.%,!#~*/:@&=_-]+))|([&<>\"\'])");
public static String linkHtml(String s, int length, String suffix) {
StringBuffer buff = new StringBuffer((int) (s.length() * 1.2f));
Matcher m = linkHtmlPattern.matcher(s);
while (m.find()) {
String g = m.group();
if (g.length() == 1) {
if (g.equals("&")) {
g = "&amp;";
} else if (g.equals("<")) {
g = "&lt;";
} else if (g.equals(">")) {
g = "&gt;";
} else if (g.equals("\"")) {
g = "&quot;";
} else if (g.equals("\'")) {
g = "&#x27;";
}
m.appendReplacement(buff, g);
continue;
}
if (length > 0 && g.length() > length) {
g = g.substring(0, length);
if (suffix != null) {
g += suffix;
}
}
g = m.quoteReplacement(g);
m.appendReplacement(buff, "<a href=\"$1\">" + g + "</a>");
}
m.appendTail(buff);
return new String(buff);
}
結構面倒くさいです。。色々考えたのですが、今のところこれが一番かなと思います。

Perl の場合は URI::Find という便利なクラスがあります。使い方はこんな感じです。
use URI::Find;
my $finder = URI::Find->new(
sub {
my($uri, $orig_uri) = @_;
return qq {<a href="$uri">$orig_uri</a>};
});
$finder->find(\$text);

では、サニタイジングと併せて使う場合はどうやったらいいのでしょう?すごく悩んでこんな実装にしたことがあります。
$text =~ s/&gt;/ __GT__ /g;
$text =~ s/&lt;/ __LT__ /g;
$text =~ s/&amp;/ __AMP__ /g;
$text =~ s/&quot;/ __QUOT__ /g;
$text =~ s/&#x27;/ __APOS__ /g;
my $finder = URI::Find->new(
sub {
my($uri, $orig_uri) = @_;
return qq {<a href="$uri">$orig_uri</a>};
});
$finder->find(\$text);
$text =~ s/ __GT__ /&gt;/g;
$text =~ s/ __LT__ /&lt;/g;
$text =~ s/ __AMP__ /&amp;/g;
$text =~ s/ __QUOT__ /&quot;/g;
$text =~ s/ __APOS__ /&#x27;/g;

うむむ。もっといい方法ないですかね。。

0 件のコメント: