challenge 一部のHTMLタグを通すフィルタ

ユーザが入力した文字列から、一部のタグだけを許可して他をエスケープするコードを書いてください。要件は次のようになります。
  • 通すタグはAとBRとSTRONGのみ。大文字小文字は区別しない。
  • それ以外のタグとして意味を持ちうる文字列は<を&lt;に変換することで無効化する(削除するのではない。>は変換してもしなくてもよい)
  • Aタグのhrefとname以外の属性は削除する。BRやSTRONGの属性はすべて削除する。

このお題はperezvonさんの提案を元にしています。ありがとうございました。 ただ、いきなりだと難しいかと思ったので、肝の部分以外を先に出題しました。このお題は続編で徐々に難しくなっていきます。

追記:属性に<や>が含まれてしまうケースに漏れのある解答が多いようなのでテストケースを追加します。
これは「この出力なら十分」という意味です。この出力の通りでなければいけないという意味ではありません。

<script foo="<script>alert('bar')</script>">alert('foo')</script>
&lt;script foo="&lt;script&gt;alert('bar')&lt;/script&gt;"&gt;alert('foo')&lt;/script&gt;


<script foo="<a href='link'>link</a>">alert('foo')</script>
&lt;script foo="&lt;a href='link'&gt;link&lt;/a&gt;"&gt;alert('foo')&lt;/script&gt;

<a href='www.g>oogle.com'>link</a>

<a href="./www.g%3Eoogle.com">link</a>

Posted feedbacks - Flatten

Nested Hidden
タグを無効化するために「<」のサニタイズするのなら
「;」も対象にしたほうが良い予感?


TripletaiL フレームワークには一部のタグだけを許可する機能があります.
ただ,題意のサンプルのように,属性値を '' で括った場合は
うまく対応できないので,そこを変更しています.

また,閉じタグの自動修復機能が少しバグっていたので
以下のパッチを当てて実行しています.

--- lib/Tripletail/TagCheck.pm  27 Jun 2007 03:01:50 -0000      1.15
+++ lib/Tripletail/TagCheck.pm  31 Aug 2007 01:03:31 -0000
@@ -253,7 +253,9 @@
                                }

                                # スタックにプッシュ
-                               push @$open_stack, $elem;
+                               if(!$taginfo->mustBeEmpty) {
+                                       push @$open_stack, $elem;
+                               }
                        }
                }
        }

----
実行結果
<<<<

<a href="www.google.com">link</a> <blink>and</blink>
<strong onClick='alert("NG")'>click<br>me!</strong>
>>>>

<a href="www.google.com">link</a> &lt;blink&gt;and&lt;/blink&gt;
<strong>click<br>me!</strong>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/usr/bin/perl

use Tripletail qw(/dev/null);

my $input = q{
<a href="www.google.com">link</a> <blink>and</blink>
<strong onClick='alert("NG")'>click<br>me!</strong>
};

my $tc = $TL->newTagCheck;
$tc->setATarget(undef);
$tc->setAllowTag(':BR;A(HREF,NAME);STRONG()');
my $output = $tc->check($input);

print "<<<<\n";
print $input;
print ">>>>\n";
print $output;

validityチェックなしの手抜きですが。。。
Tag Soup というライブラリを使う
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import Data.Char
import Text.HTML.TagSoup

proc :: Tag Char -> Tag Char
proc tag@(TagOpen t attrs)
 | t' == "a"      = TagOpen t (filter allowA attrs)
 | t' == "br"     = TagOpen t []
 | t' == "strong" = TagOpen t []
 | otherwise      = tag
 where t' = map toLower t
proc tag = tag

allowA (a,_) = a == "href" || a == "name"

pprTags :: [Tag Char] -> String
pprTags [] = ""
pprTags tags@(TagOpen s attrs : TagClose e : ts)
 | map toLower s == "br" = "<br/>" ++ pprTags ts
pprTags (t:ts) = pprTag t ++ pprTags ts

pprTag :: Tag Char -> String
pprTag tag = case tag of
  TagOpen t attrs | ignore t  -> "&lt;"++pprOpen' t attrs++">"
                  | otherwise -> pprOpen t attrs
  TagClose t      | ignore t  -> "&lt;"++pprClose' t++">"
                  | otherwise -> pprClose t
  TagText s                   -> s
  TagComment s                -> "<!--"++s++"-->"
  TagSpecial s c              -> "<!"++s++' ':c++">"
  TagWarning s                -> ""

pprOpen' t []    = t
pprOpen' t attrs = t++' ':unwords (map pprAttr attrs)
pprOpen  t attrs = "<" ++ pprOpen' t attrs ++">"

pprClose' t = t
pprClose  t = "</" ++ t ++ ">"

pprAttr (a,v) = a++"='"++v++"'"

ignore :: String -> Bool
ignore t = notElem (map toLower t) ["a","strong","br"] 

testdata="<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert(\"NG\")'>click<br/>me!</strong>"

-- main = putStrLn . pprTags . map proc . parseTags =<< getContents
main = putStrLn . pprTags . map proc . parseTags $ testdata

{-
*Main> :main
<a href='www.google.com'>link</a> &lt;blink>and&lt;blink> <strong>click<br/>me!</strong>
-}

ライブラリを使ったら面白くないので、自分で書いてみた。思ったよりすっきりかけた気がする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.regex._
class ExtendedString(self:String) {
  def gsub(reg:Pattern, f:(Matcher)=>String):String = {
    val result = new StringBuffer
    val m = reg.matcher(self)
    while(m.find) m.appendReplacement(result, f(m))
    m.appendTail(result)
    result.toString
  }
  def gsub(reg:String, f:(Matcher)=>String):String = gsub(Pattern.compile(reg), f)
}
implicit def string2ext(self:String) = new ExtendedString(self);


object htmlEscape{
  lazy val tagRegex = Pattern.compile(
    """(</?)([^"'< >]*)([^"'<>]*(?:"[^"]*"[^"'<>]*|'[^']*'[^"'<>]*)*)((?:>|(?=<)|$(?!\n)))"""
  )
  lazy val attrRegex = Pattern.compile(
    """[\s'"](\w+)\s*=\s*([^\s'">]+|'[^']+'|\"[^"]+")"""
  , Pattern.DOTALL | Pattern.CASE_INSENSITIVE)
  lazy val tagAllowed = Set("a", "br", "strong")
  lazy val attrAllowed = Map("a" -> Set("href", "name"))

  def apply(html:String) = {
    html.gsub(tagRegex, (m:Matcher) => {
      val tag = m.group(2).toLowerCase.replace("/","")
      (if(tagAllowed.contains(tag)){
        val attrs = m.group(3).gsub(attrRegex, (m2:Matcher) => {
          if(attrAllowed.getOrElse(tag, Set[String]()).contains(m2.group(1).toLowerCase)) {
            m2.group(0)
          }else {
            ""
          }
        })
        List(m.group(1),m.group(2), attrs, m.group(4))
      }else {
        List(m.group(1).replace("<", "&lt;"), m.group(2), m.group(3), m.group(4))
      }).mkString("")
    })
  }
}
println(htmlEscape("""<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>"""))

正規表現置換でごり押し。
可読性最悪ですみません。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var input = "<a href='www.google.com' name='hoge' title=\"fuga\">\
link</a><blink>and</blink> <strong onClick='alert(\"NG\")'>click<br/>me!</strong>";


var reg = /<\s*(a|strong|br)((?:\s+\w+\s*=\s*(["']).*?\3)*)?\s*(?:>(([^<]*|<[^<>]*[^\/]>|<[^<>]*\/>)*)<\/s*\1\s*>|\/>)/gmi;

function deleteAttr(attr) {
  return attr.replace(/\s+(\w+)\s*=\s*(["'])(.*?)\2/g, function(all, name, q, value) {
    if(!name.match(/name|href/i)) return '';
    else return all;
  });
}
function eacapeTags(str) {
  var escaped = [];
  str = str.replace(reg, function(all, tag, attr, q, inner) {
    attr = tag.toUpperCase() == 'A' ? deleteAttr(attr) : '';
    escaped .push(tag+ " " + attr, inner);
    return "<A/>";
  }).replace(/</g, "&lt;");
  str = str.replace(/&lt;A\/>/g, function() {
    var tag = escaped.shift(), inner = escaped.shift();
    if(inner) return "<" + tag + ">" + eacapeTags(inner) + "</" + tag + ">";
    else return "<" + tag + "/>";
  });
  return str;
}
document.body.innerHTML=(eacapeTags(input));

inとoutにWSHを使ってます。

 cscript "javascriptファイル"  フィルタしたい文字列

で実行可能です。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if (WScript.Arguments.length != 1) 
	WScript.Quit();
var target = WScript.Arguments.item(0);

var ret = target.replace(
	/<((\/?)([a-z]+)(.*?)(\/?))>/ig,
    function(all, ins, head, tag, elms, tail){
    	switch (tag.toUpperCase())
    	{
			case "A":
				var filterElms = elms.match(/ ?(href|name) *= *[^ \/>]+/ig);
				var newElement = "";
				if (filterElms)
					for(var i = 0; i < filterElms.length; i++)
						newElement += filterElms[i];
				
				return "<" + head + tag + newElement + tail +">";

			case "BR":
			case "STRONG":
				return "<" + head + tag + tail + ">";

			default:
				return "&lt;" + ins + ">";
				break;
    	}
    });

WScript.Echo(ret);

それはなぜですか?

実体参照の話であれば ; というよりも & を &amp; にしなくていいいのかということですよね。違うかな。。

エンティティの無効化です。
このお題で必要かと問われると怪しいですけど、
きっとタグが有効になると何か影響でちゃうのですよね?
タグが特別なものであるとしてとらえると
タグを無効化する必要のある処理であるならば
エンティティも一緒に無効化しても良いかなと。

個人的な好みが強いですが片手落ちというイメージが。。。

CGI.escapeElementが使えるかなと思ったが微妙に要求が異なる
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def filter(html)
  html.gsub(%r!<((/?)([a-z]+)[^>/]*(/?)>)!i){
    rest, endtagp, tagname, slash = Regexp.last_match.captures
    case tagname
    when "br", "BR", "strong", "STRONG"
      "<#{endtagp}#{tagname}#{slash}>"
    when "a", "A"
      endtagp.empty? ? "<a #{$&.scan(/(?:href|name)=(?:".+?"|'.+?'|[^ >]+)/).join ' '}>" : '</a>'
    else
      "&lt;#{rest}"
    end
  }
end

ちょっと無駄に難しく作りすぎました。
開きタグと閉じタグは個別に扱っても構わなかったのか。
ところで、(要件に含まれるのかわかりませんが)属性に">"が含まれるケースを考慮してない回答が結構ありますね。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
var input = "<a title=\"(>_<;)\" href='www.google.com' name='hoge'>\
link</a><blink>and</blink> <strong onClick='alert(\"NG\")'>click<br/>me!</strong>";

function deleteAttr(attr) {
  return attr.replace(/\s+(\w+)\s*=\s*(["'])(.*?)\2/g, function(all, name, q, value) {
    return name.match(/name|href/i) ? all : '';
  });
}
function filter(html) {
  return html.replace(/<(\/?)(\w+)((?:\s+\w+\s*=\s*(["']).*?\4)*)?(\/?)>/gmi,
      function(all, fslash, tag, attrs, q, rslash) {
         switch(tag.toUpperCase()) {
           case 'STRONG' :  // drop through
           case 'BR' : attrs = ''; break;
           case 'A' : attrs = deleteAttr(attrs); break;
           default : return all.replace('&', '&amp;').replace('<', '&lt;');
         }
         return '<' + fslash + tag + attrs + rslash + '>';
      });
}

document.body.innerHTML=filter(input);

htmlって曖昧だから、あまり自信なし。例えば、
<a href=foo.com> みたいに属性がクオートされてない場合はサポートしていない。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import re

def filter(html):
    def repl(m):
        content = m.group(1)
        m = re.match('(/?)\s*(a|br|strong)(\s.*|(/?))', content, re.I + re.S)
        if m:
            beg_slash, tag, other, end_slash = m.groups()
            def attrs(*names):
                yield ""
                for m in re.finditer(r"""(\S+)\s*=\s*(['"]).+?\2""", other, re.S):
                    if m.group(1).lower() in names:
                        yield m.group(0)
            def combine(*names):
                return "<%s%s%s%s>" % (beg_slash, tag, " ".join(attrs(*names)), end_slash or "")
            if tag.lower() == "a":
                return combine("href", "name")
            else:
                return combine()
        return "&lt;" + content + ">"
    return re.compile('<([^>]*)>', re.S).sub(repl, html)

def main():
    print filter("""<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>""")

if __name__ == '__main__':
    main()

タグに大文字小文字が混在している場合や、属性が大文字の場合が考慮されていないのでは?少なくともtagnameはdowncaseしてから比較したほうがよい気がします。

属性値の中の">"が考慮されていませんね。

<a href='www.g>oogle.com'>link</a>

というようなケースで

<a>oogle.com'>link</a>

となってしまいます。

修正。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def filter(html)
  html.gsub(%r!<((/?)([a-z]+)[^>/]*(/?)>)!i){
    rest, endslash, tagname, slash = Regexp.last_match.captures
    case tagname.downcase
    when "br", "strong"
      "<#{endslash}#{tagname}#{slash}>"
    when "a"
      endslash.empty? ? "<a #{$&.scan(/(?:href|name)=(?:".+?"|'.+?'|[^ >]+)/i).join ' '}>" : '</a>'
    else
      "&lt;#{rest}"
    end
  }
end

なるほど、そんな場合があるのか・・・というわけで修正版です。

属性をパースする正規表現が二度出てくるのが美しくないですが・・・このあたりをPythonでうまく処理する方法ってあるのかな。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import re

def filter(html):
    def repl(m):
        beg_slash, tag_name, attrs, _, end_slash = m.groups()
        def combine(*names):
            s = "<" + beg_slash + tag_name
            for m in re.finditer(r"""(\w+)\s*=\s*(["']).+?\2""", attrs, re.S):
                if m.group(1).lower() in names:
                    s += " " + m.group(0)
            return s + end_slash + ">"
        if tag_name.lower() == 'a':
            return combine("href", "name")
        elif tag_name.lower() in ('br', 'strong'):
            return combine()
        else:
            return "&lt;" + m.group(0)[1:]
    return re.compile(r"""<(/?)(\w+)((?:\s*\w+\s*=\s*(["']).+?\4)*)\s*(/?)>""", re.S).sub(repl, html)

def main():
    print filter("""<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>""")

if __name__ == '__main__':
    main()

br以外の通したタグの閉じ忘れにも対応してみた。
(閉じる順番が間違ってる場合は今回は大目に見るとして。)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php
function safehtml($str)
{
	$r=array();
	$tags=array();
	$offs=0;
	while(preg_match('!<\s*(/|)\s*(([^>\'"]+|\'[^\']*\'|"[^"]*")*)>!',$str,$m1,PREG_OFFSET_CAPTURE,$offs))
	{	$r[]=substr($str,$offs,$m1[0][1]-$offs);
		$offs=$m1[0][1]+strlen($m1[0][0]);
		preg_match_all('!([a-z0-9_]+)(\s*=\s*("[^"]*"|\'[^\']*\'|[^\s]+)|)!im',$m1[2][0],$m2,PREG_SET_ORDER);
		switch(strtolower($m2[0][1]))
		{
		case 'a':
		case 'strong':
			if($m1[1][0])
			{	if(($i=array_search($m2[0][1],$tags))!==false)
					unset($tags[$i]);
			}
			else
				array_unshift($tags,$m2[0][1]);
			$t=array($m1[1][0].$m2[0][1]);
			if(strtolower($m2[0][1])=='a' && !$m1[1][0])
			{	array_shift($m2);
				while($param=array_shift($m2))
				{	switch(strtolower($param[1]))
					{
					case 'href':
					case 'name':
						$t[]=$param[0];
						break;
					}
				}
			}
			$r[]='<'.implode(" ",$t).'>';
			break;
		case 'br':
			$r[]='<br/>';
			break;
		default:
			$r[]='&lt;'.$m1[1][0].$m1[2][0].'&gt;';
			break;
		}
	}
	$r[]=substr($str,$offs);
	while($tag=array_shift($tags)) // 閉じわすれタグを閉じる
		$r[]="</$tag>";
	return implode("",$r);
}

echo safehtml(<<<EOT
<a href='www.google.com' target=_blank>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>
EOT
);
?>

oceanさんのコードで確かめたのでここにぶら下げますが、他のポストにも同じ問題があるかもしれません。

入力文字列が次のようなものだとまずいのでは:

  <z foo='<script>alert("Boo")</script>'>

>http://ja.doukaku.org/comment/2722/
失念してました。

文中に">"等が入っても通るように正規表現修正。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if (WScript.Arguments.length != 1) 
	WScript.Quit();
var target = WScript.Arguments.item(0);

var ret = target.replace(
	/<(\/?)(\w+)((\s*|\w+|\w+\s*=\s*('[^']*'|"[^"]*"|\w+))*)(\/?)>/ig,
    function(all, head, tag, attr, nouse1, nouse2, tail){
    	switch (tag.toUpperCase())
    	{
			case "A":
				var attrs = attr.match(/\s?(href|name)\s*=\s*('[^']*'|"[^"]*"|\w+)/ig);
				var newAttr = "";
				if (attrs)
					for(var i = 0; i < attrs.length; i++)
						newAttr += attrs[i];
				return "<" + head + tag + newAttr + tail +">";

			case "BR":
			case "STRONG":
				return "<" + head + tag + tail + ">";

			default:
				return "&lt;" + head + tag + attr + tail + ">";
    	}
    });

WScript.Echo(ret);

あ、いけね。開いたタグと閉じたタグで大文字小文字が合ってないと閉じ忘れ扱いになってしまう。
かさばるので修正版は続編のときに…。

なるほど、タグを無効化した場合、属性に含まれる < を放置すると今度はそちらがタグとみなされるということですか。

というわけで再修正パッチです。でもきっとまだあるな・・・
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
--- a.py.orig	Fri Aug 31 16:20:21 2007
+++ a.py	Fri Aug 31 16:20:35 2007
@@ -14,11 +14,12 @@
         elif tag_name.lower() in ('br', 'strong'):
             return combine()
         else:
-            return "&lt;" + m.group(0)[1:]
+            return m.group(0).replace("<", "&lt;")
     return re.compile(r"""<(/?)(\w+)((?:\s*\w+\s*=\s*(["']).+?\4)*)\s*(/?)>""", re.S).sub(repl, html)
 
 def main():
     print filter("""<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>""")
+    print filter(""" <z foo='<script>alert("Boo")</script>'>""")
 
 if __name__ == '__main__':
     main()

 DOM を使った方がより JavaScript らしいのかもしれないが…。

javascript:document.body.innerHTML=(function(s,r,x){r=/\b(?:name|href) *= *(?:".*?"|'.*?'|[^ >]*)/gi;x=/^\/?(a)|br|strong\b/i;return s.replace(/<([^ >]+ ?)((?:".*?"|'.*?'|[^\/>])*)/g,function(m,t,a){return x.test(t)?'<'+t+(RegExp.$1&&(m=a.match(r))?m.join(' '):''):'&lt;'+t+a})})(document.body.innerHTML)
1
2
3
4
5
6
7
8
function doukaku54(s){
  var rx_nh = /\b(?:name|href) *= *(?:".*?"|'.*?'|[^ >]*)/gi;
  var rx_ok = /^\/?(a)|br|strong\b/i, LT = '<', lt = '&lt;', sp = ' ';
  return s.replace(/<([^ >]+ ?)((?:".*?"|'.*?'|[^\/>])*)/g, function(m, tag, ats){
    return rx_ok.test(tag)
      ? LT + tag + (RegExp.$1 && (m = ats.match(rx_nh)) ? m.join(sp) : '')
      : lt + tag + ats });
}

素直にライブラリを使いましたが、お題によれば無効にしたタグ部分は
置き換えた文字以外、そのままにすべきなんでしょうが
(例えば大文字小文字など)、ライブラリの都合上、end側タグまわりの情報が劣化しています(27行目あたり)。
これは基底クラスのソース見てなんとかする必要がありそうなので
とりあえず手抜き版として投稿させていただきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from HTMLParser import HTMLParser

class HTMLParser2(HTMLParser):

    def reset(self):
        HTMLParser.reset(self)
        self.buf = ''

    def handle_starttag(self, tag, attrs):
        if tag == 'a':
            self.buf += '<a '+' '.join(["%s='%s'" % t for t in attrs if t[0] in ['href', 'name']])+'>'
        elif tag in ['br', 'strong']:
            self.buf += '<%s>' % tag
        else:
            self.buf += self.get_starttag_text().replace('<', '&lt;')

    def handle_startendtag(self, tag, attrs):
        if tag == 'br':
            self.buf += '<br/>'
        else:
            self.buf += self.get_starttag_text().replace('<', '&lt;')

    def handle_endtag(self, tag):
        if tag in ['a', 'br', 'strong']:
            self.buf += '</%s>' % tag
        else:
            self.buf += '&lt;/%s>' % tag

    def handle_data(self, data):
        self.buf += data

s = """<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='al
ert("NG")'>click<br/>me!</strong>"""
h = HTMLParser2()
h.feed(s)
print h.buf

#2727の指摘に対応。
タグの大文字小文字の問題修正と通すタグに関する情報をまとめて汎用化してみた。
タグは開いたのと逆順に閉じることを強制する。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
function safehtml($str)
{
	$safetag=array('a'=>array(1,array('href','name')),'strong'=>array(1),'br'=>array(0));
	$r=array();
	$tags=array();
	$offs=0;
	while(preg_match('!<(\s*(/|)\s*(([^>\'"/]+|\'[^\']*\'|"[^"]*")*)(/|)\s*)>!',$str,$m1,PREG_OFFSET_CAPTURE,$offs))
	{	$r[]=substr($str,$offs,$m1[0][1]-$offs);
		$offs=$m1[0][1]+strlen($m1[0][0]);
		preg_match_all('!([^\s\'"=]+)(\s*=\s*("[^"]*"|\'[^\']*\'|[^\s]+)|)!im',$m1[3][0],$m2,PREG_SET_ORDER);
		$tag=strtolower($m2[0][1]);
		if(isset($safetag[$tag]))
		{	if($safetag[$tag][0]&1)
			{	if($m1[2][0])
				{	if(array_search($tag,$tags)===false)
						continue; // 開いてないタグは閉じない
					while(($t=array_shift($tags))) // 開いたのと逆順に閉じる
					{	$r[]="</$t>";
						if($t==$tag)
							break;
					}
					continue;
				}
				if(!$m1[5][0])
					array_unshift($tags,$tag);
			}
			$t=array($tag);
			if(isset($safetag[$tag][1]) && !$m1[2][0])
			{	array_shift($m2);
				while($param=array_shift($m2))
				{	if(array_search(strtolower($param[1]),$safetag[$tag][1])!==false)
						$t[]=$param[0];
				}
			}
			$r[]='<'.$m1[2][0].implode(" ",$t).$m1[5][0].'>';
		}
		else
			$r[]=str_replace(array('<','>'),array('&lt;','&gt;'),$m1[0][0]);
	}
	$r[]=substr($str,$offs);
	while(($tag=array_shift($tags))) // 閉じわすれタグを閉じる
		$r[]="</$tag>";
	return implode("",$r);
}

echo safehtml(<<<EOT
<a href='www.google.com' target=_blank>link</a> <blink dummy='<'>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong> <z foo='<script>alert("Boo")</script>'>
EOT
);
?>