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 - Nested

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

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

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

なるほど。エンティティや実体参照を許すとどういう脆弱性につながるのかは寡聞にして知りませんが、セミコロンをエスケープしても要件を満たしていそうなので気になるのならそうしてもいいと思います。

ちなみに&hearts;のセミコロンを&semicolon;に置き換えてもハートマークは表示されるみたいですね。

このサイトはセミコロンをエスケープしていませんが、アンパサンドはエスケープしています。

きゅ~勘違いしてました。
「;」のエンティティって存在しないみたいです。
「&」で十分です。

昔、エンティティを無効にするときは「;」というのを読んだ気がしたんだけど、うーんソースが見つからない。

なんでエンティティを無効にしないといけなかったんだっけな~?
何か理由があったはずなんだけど。
実体参照の話であれば ; というよりも & を &amp; にしなくていいいのかということですよね。違うかな。。
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. プリンタ (構文木→文字列)
と分けて

パーザは Tag Soupライブラリのものを使い、
タグのエスケープはトランスレータで行うようにし、
プリンタは自前で書いてみました。
コメントは保存されます。

こちらの方が前の投稿のものよりモジュラリティが
高くなった気がします。

urlエンコーディングによるuriの値表現の
validatingは手を抜いておこなっていません。
(元元要求にはなかったような気がしますと言い訳してみる:p)
 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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
module Main (main) where

import Data.Char
import Data.Maybe
import Text.HTML.TagSoup

-- Parsing
-- Text.HTML.TagSoup.parseTags :: String -> [Tag Char]

-- Translating
translate :: [Tag Char] -> [Tag Char]
translate = map trans

trans :: Tag Char -> Tag Char
trans tag = case tag of
  TagOpen t attrs | ignore t  -> escapeTagOpen t attrs
                  | otherwise -> TagOpen t (filterAttr t attrs)
  TagClose t      | ignore t  -> escapeTagClose t
  _                           -> tag

ignore :: String -> Bool
ignore = flip notElem (map fst filterTable) . map toLower

escapeTagOpen t attrs
 = TagText $ "&lt;"++t++escape (' ':unwords (map showAttr attrs))++"&gt;"
escapeTagClose t
 = TagText $ "&lt;/"++t++"&gt;"

filterAttr :: String -> [Attribute Char] -> [Attribute Char]
filterAttr t = filter ((maybe (const True) id (lookup t filterTable)) . fst)

filterTable :: [(String,String->Bool)]
filterTable = [("a",flip elem ["href","name"])
	      ,("strong",const False)
	      ,("br",const False)]

-- Showing

showTags :: [Tag Char] -> String
showTags [] = ""
showTags (TagOpen s attrs : TagClose e : ts) | isEmptyTag e 
 = angle (s ++ ' ':unwords (map showAttr attrs)++" /")++showTags ts
showTags (t:ts) 
 = showTag t ++ showTags ts

showTag tag = case tag of
  TagOpen t attrs -> angle $ t ++ ' ':unwords (map showAttr attrs)
  TagClose t      -> angle $ t ++ "/"
  TagText s       -> s
  TagComment c    -> angle $ "!--" ++ c ++ "--"
  TagSpecial s t  -> angle $ "!" ++ s ++ ' ':t
  TagWarning s    -> ""

angle :: String -> String
angle s = "<"++s++">"

isEmptyTag :: String -> Bool
isEmptyTag = flip elem ["br","hr"]     -- not full fledged

showAttr :: Attribute Char -> String
showAttr (a,v) = a ++ "=" ++ q v
  where q v = if elem sq v then dq:v++[dq]
              else sq:v++[sq]
        sq = '\''
        dq = '\"'

escape :: String -> String
escape = concatMap esc
 where esc '<' = "&lt;"
       esc '>' = "&gt;"
       esc '&' = "&amp;"
       esc c   = [c]

--

main :: IO ()
-- main = putStrLn . showTags . translate . parseTags =<< getContents

main = do { putStrLn . showTags . translate . parseTags $ testdata1
          ; putStrLn . showTags . translate . parseTags $ testdata2
          ; putStrLn . showTags . translate . parseTags $ testdata3
	  }

testdata1 = "<script foo=\"<script>alert('bar')</script>\">alert('foo')</script>"
testdata2 = "<script foo=\"<a href='link'>link</a>\">alert('foo')</script>"
testdata3 = "<a href='www.g>oogle.com'>link</a>"

{-
*Main> :main
&lt;script foo="&lt;script&gt;alert('bar')&lt;/script&gt;"&gt;alert('foo')&lt;/script&gt;
&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/>
-}

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

 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));
ちょっと無駄に難しく作りすぎました。
開きタグと閉じタグは個別に扱っても構わなかったのか。
ところで、(要件に含まれるのかわかりませんが)属性に">"が含まれるケースを考慮してない回答が結構ありますね。
 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);

>属性に">"が含まれるケースを考慮してない回答が結構ありますね。

ちなみに僕はどのコードがダメなのか公表してしまった方が(そのコードを書いた人が将来その罠に気づかずに脆弱性のあるプログラムを書いてしまうよりは)いいと思っているので、その手の指摘はたいがいプラス評価しています。

バグありました… orz
1
2
3
4
5
@@ -15,3 +15,3 @@
            case 'A' : attrs = deleteAttr(attrs); break;
-           default : return all.replace('&', '&amp;').replace('<', '&lt;');
+           default : return all.replace(/&/g, '&amp;').replace(/</g, '&lt;');
          }
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);
>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);
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
タグに大文字小文字が混在している場合や、属性が大文字の場合が考慮されていないのでは?少なくともtagnameはdowncaseしてから比較したほうがよい気がします。
修正。
 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
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()
属性値の中の">"が考慮されていませんね。

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

というようなケースで

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

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

属性をパースする正規表現が二度出てくるのが美しくないですが・・・このあたりを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()
oceanさんのコードで確かめたのでここにぶら下げますが、他のポストにも同じ問題があるかもしれません。

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

  <z foo='<script>alert("Boo")</script>'>
なるほど、タグを無効化した場合、属性に含まれる < を放置すると今度はそちらがタグとみなされるということですか。

というわけで再修正パッチです。でもきっとまだあるな・・・
 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()
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