型を意識すればまともなコードが書けるし、通常の文字列とHTML文字列は別の型です
Twitter風URL自動リンクの話(関連: Kazuho@Cybozu Labs: (Twitter の XSS 脆弱性に関連して) 構造化テキストの正しいエスケープ手法について)。
あの問題では、関数のシグネチャを expandString(String):String 、「文字列を受け取って文字列を返す関数」としているが、これはあくまで実装上の都合。実際には「プレインテキストを受け取ってHTMLを返す関数」であって、引数と帰り値のドメインはまったく異なっている。ということを踏まえると以下のコードがどれだけひどいのかわかろうというもの。
module Twitter module AutoLink # (snip) def auto_link(text, options = {}) auto_link_usernames_or_lists( auto_link_urls_custom( auto_link_hashtags(text, options), options), options) end # (snip) end end
http://github.com/mzsanford/twitter-text-rb/blob/master/lib/autolink.rb (commit: 47c90403e7ca9667cd863a9af0146f229b8f199f)
これはつまり、こういうことをしているのでは……?
autoLink :: PlainText -> HTML autoLink text = autoLinkUserName $ autoLinkURL $ autoLinkHashTag text autoLinkURL :: PlainText -> HTML autoLinkHashTag :: PlainText -> HTML autoLinkUserName :: PlainText -> HTML
ルールが単純なので、力技(置換を適用する順序や他の処理の影響をよく考える)でもどうにかなってしまうのだが、かなり筋が悪い方法と言えよう。実際、現在のtwitterでは、
http://example.com/#@hoge/
<a href="http://example.com/#@<a class="tweet-url username" href="/hoge" rel="nofollow">hoge</a>/" class="tweet-url web" rel="nofollow" target="_blank">http://example.com/#@<a class="tweet-url username" href="/hoge" rel="nofollow">hoge</a>/</a>
となってしまう(直す気はないらしい…………)*1
じゃあどうするのが正解なのか。型に厳密にやろうとするなら、
autoLink :: PlainText -> HTML autoLink text = foldl (++) $ replaceByRule [ (REGEX_URL, linkURL), (REGEX_USER, linkUser), (REGEX_HASHTAG, linkHashTag) ] text type Rule = (Regex,(PlainText->HTML)) replaceByRule :: [Rule] -> PlainText -> [HTML] replaceByRule rules text= case matched of | Just (matched_str,(_,filter),rest) = (filter matched_str) : replaceByRule rules rest | Nothing = (htmlPlainText $ head text) : replaceByRule rules $ tail text where matched = matchRule rules text matchRule :: [Rule] -> PlainText -> Maybe (PlainText,Rule,PlainText) ;; ルールを先頭から見ていって、マッチするのがあったら(マッチ文字列,ルール,残りの文字列)を返す
みたいなかんじですかねー。
実際には正規表現一発でどうにかすることが多いと思われる(マッチした文字列からどのルールがヒットしたのかわかるので)
def auto_link text return text.gsub(/URL|UserName|HashTag|./) {|matched| return link_url(matched) if matched is URL return link_userName(matched) if matched is UserName return link_hash_tag(matched) if matched is HashTag return html_escape matched if matched is single char } end