PHP Tidy完美的XHTML糾錯&過濾

輸入和輸出javascript

輸入和輸出應該說是不少網站的基本功能。用戶輸入數據,網站輸出數據供其餘人瀏覽。php

拿目前流行的Blog爲例,這裏的輸入輸出就是做者編輯文章後生成博客文章頁面供他人閱讀。
這裏有一個問題,即用戶輸入一般是不受控制的,它可能包含不正確的格式亦或者含有有安全隱患的代碼;而最終網站輸出的內容卻必須是正確的HTML代碼。這就須要對用戶輸入的內容進行糾錯和過濾。html

永遠不要相信用戶的輸入java

你可能會說:如今處處都是所見即所得的編輯器(WYSIWYG),FCKeditorTinyMCE...你可能會舉出一大堆。是的,它們均可以自動生成標準的XHTML代碼,可是做爲web開發人員,你確定聽過"永遠不要相信用戶遞交的數據"。node

所以對用戶輸入數據進行糾錯和過濾是必需的。web

須要更好的糾錯和過濾瀏覽器

目前爲止我還沒見過有讓我滿意的相關實現,能接觸到的一般都是效率低下、效果不太理想,有這樣那樣的明顯缺陷。舉個比較知名的例子:WordPress是一種使用很是普遍的blog系統,操做簡單功能強大且有豐富的插件支持,可是它集成的TinyMCE和後臺一堆有些自做聰明的糾錯過濾代碼卻使人至關頭痛,對半角字符的強制替換,過於保守的替換規則等等.....致使像貼一段代碼讓它正確顯示這種需求都很難作到。安全

這裏順便抱怨一下,這個blog是用WordPress架的,爲了讓這幾篇文章能正確顯示代碼,網上搜了不少也試用了一些插件,最終仍是翻了它的代碼把一些過濾規則註釋掉才勉強能夠顯示得體面一點 -.-b編輯器

固然,我不想過多的指責它(wordpress),只是想說明它還能夠作的更好。wordpress

Tidy是什麼,它如何工做?

摘自Tidy ManPage的說明這樣描述:

Tidy reads HTML, XHTML and XML files and writes cleaned up markup. For HTML variants, it detects and corrects many common coding errors and strives to produce visually equivalent markup that is both W3C compliant and works on most browsers. A common use of Tidy is to convert plain HTML to XHTML. For generic XML files, Tidy is limited to correcting basic well-formedness errors and pretty printing.

簡單說Tidy是清理HTML代碼的,生成乾淨的符合W3C標準的HTML代碼,支持HTML,XHTML,XML。Tidy提供一個庫TidyLib,以方便在其餘應用中利用Tidy的強大功能。很是幸運,PHP有相應的tidy模塊可使用。

老兄,爲何又是PHP?

呃,這個問題... 慚愧,由於我只會那麼點PHP而已 -.-v
不過還好,我這裏講的都不是純粹的代碼,好歹也有些分析的過程,分享這些東西比貼代碼有用多了。

PHP中使用Tidy

要在PHP中使用Tidy須要安裝Tidy模塊,也就是加載tidy.so這個PHP extension,具體過程就略了,純粹是體力活。最後能在phpinfo()中看到"Tidy support enabled" 就OK。

在這個模塊的支持下,PHP中就可使用Tidy提供的幾乎全部的功能。經常使用的HTML清理是異常輕鬆的事情,甚至能夠生成文檔的解析樹,像在客戶端操做DOM那樣的操做HTML的各個Node。下面將會有具體的代碼說明,也能夠看看PHP官方的相關手冊

糾錯和過濾的PHP+Tidy實現

上面說了這麼多背景素材,彷佛太羅唆了,具體的解決問題的代碼才最最直接。

1. 簡單的糾錯實現

function HtmlFix($html)
{

if(!function_exists('tidy_repair_string'))
return $html;
//use tidy to repair html code

//repair
$str = tidy_repair_string($html,
array('output-xhtml'=>true),
'utf8');
//parse
$str = tidy_parse_string($str,
array('output-xhtml'=>true),
'utf8');
$s = '';

$nodes = @tidy_get_body($str)->child;

if(!is_array($nodes)){
$returnVal = 0;
return $s;
}

foreach($nodes as $n){
$s .= $n->value;
}
return $s;
}

上面的代碼就是對可能不規範的XHTML代碼進行清理糾錯,輸出標準的XHTML代碼(輸入輸出都是UTF-8編碼)。實現代碼不是最精簡的,由於爲了配合下面的過濾功能,我寫的儘量細緻了一些。

2. 高級實現: 糾錯+過濾

功能:

  1. XHTML的糾錯,輸出標準的XHTML代碼。

  2. 過濾不安全的代碼可是不影響內容展現,只是對style/javascript中不安全代碼進行清除。

  3. 對超長字符串插入<wbr>標記以實現瀏覽器兼容的自動換行功能,相關文章可參考網頁中超長文字的斷行問題

function HtmlFixSafe($html)
{

if(!function_exists('tidy_repair_string'))
return $html;
//use tidy to repair html code

// tidy 的參數設定
$conf = array(
'output-xhtml'=>true
,'drop-empty-paras'=>FALSE
,'join-classes'=>TRUE
,'show-body-only'=>TRUE
);

//repair
$str = tidy_repair_string($html,$conf,'utf8');
//生成解析樹
$str = tidy_parse_string($str,$conf,'utf8');

$s ='';

//獲得body節點
$body = @tidy_get_body($str);

//函數 _dumpnode,檢查每一個節點,過濾後輸出
function _dumpnode($node,&$s){

//查看節點名,若是是<script> 和<style>就直接清除
switch($node->name){
case 'script':
case 'style':
return;
break;
default:
}

if($node->type == TIDY_NODETYPE_TEXT){
/*
若是該節點內是文字,作額外的處理:
過長文字的自動換行問題;
超連接的自動識別(未實現)
*/
// insert <wbr>
$s .= HtmlInsertWbrs($node->value,30,'','&?/\');

// auto links ??? *** TODO ***
return;
}

//不是文字節點,那麼處理標籤和它的屬性
$s .= '<'.$node->name;

//檢查每一個屬性
if($node->attribute){
foreach($node->attribute as $name=>$value){

/*
清理一些DOM事件,一般是on開頭的,
好比onclick onmouseover等....
或者屬性值有javascript:字樣的,
好比href="javascript:"的也被清除.
*/
if(strpos($name,'on') === 0
||
stripos(trim($value),'javascript:') ===0
){
continue;
}

//保留安全的屬性
$s .= ' '.$name.'="'.HtmlEscape($value).'"';

}
}

//遞歸檢查該節點下的子節點
if($node->child){

$s .= '>';

foreach($node->child as $child){
_dumpnode($child,$s);
}

//子節點處理完畢,閉合標籤
$s .= '</'.$node->name.'>';
}else{

/*
已經沒有子節點了,將標籤閉合
(事實上也能夠考慮直接刪除掉空的節點)
*/
if($node->type == TIDY_NODETYPE_START)
$s .= '></'.$node->name.'>';
else
/*
對非配對標籤,好比<hr/> <br/> <img/>等
直接以 />閉合之
*/
$s .= '/>';
}
}
//函數定義end

//經過上面的函數 對 body節點開始過濾。
if($body->child){

foreach($body->child as $child)
_dumpnode($child,$s);
}else
return '';

return $s;
}

上面代碼中註釋應該比較詳細,工做原理就配合代碼看吧。
更嚴格的過濾也很容易擴展,好比實現文中的連接自動識別。


一點補充網頁中超長文字的斷行問題,你可能發現上面代碼中處理自動換行的函數有所不一樣:

若是你看過我以前寫的

以前介紹的是HtmlEscapeInsertWbrs(),而上面使用的是HtmlInsertWbrs()。

這裏要作一下解釋:
HtmlEscapeInsertWbrs()要求輸入的字符串未做特殊字符轉義的,也就是沒有通過htmlspecialchars()對<>&等做&lt;&gt;&amp;處理的。由於函數內部有專門的處理。
而在處理經Tidy處理事後的文字節點的時候,由於Tidy的關係,已經自動把<>&等字符做相應 的&lt;&gt;&amp;轉義,所以須要用一個專門的函數避免重複的轉義,這個函數就是HtmlInsertWbrs(), 從名字上就知道它只插入<wbr>標記,不作額外工做。

那麼你可能有個問題:
若是<wbr>被插入到HTML標籤中間,好比在<div>或者&gt;的中間插入了<wbr>,變成<d<wbr>iv>和&<wbr>gt;,那就會影響到原始信息的展現。

沒錯,的確是個新問題,不過使用一些技巧就能夠有效解決:

  1. 由於咱們處理的是Tidy獲得的文字節點,意味着不可能碰到HTML標籤,所以不會碰到在標籤中間插入<wbr>的狀況。

  2. 對於第二種狀況,轉義後的字符都是&xxxxx;這樣的形式,那麼只要在1全部&符號前面都插入<wbr>標記就能夠了(注意看調用時的第四個參數),由於下一個<wbr>標記將會插在30(以上面代碼中實際調用的第二個參數爲例)個字符以後,這個已經2遠遠大於xxxxx的長度。這樣由上面一、2兩點能夠保證不會插到轉義字符的中間。

下面給出HtmlInsertWbrs()的PHP實現:

function HtmlInsertWbrs($str, $n=10, $chars_to_break_after='',$chars_to_break_before='') { $out = ''; $strpos = 0; $spc = 0; $len = mb_strlen($str,'UTF-8'); for ($i = 1; $i < $len; ++$i) { $prev_char = mb_substr($str,$i-1,1,'UTF-8'); $next_char = mb_substr($str,$i,1,'UTF-8'); if (_u_IsSpace($next_char)) { $spc = $i; } else { if ($i - $spc == $n || mb_strpos( $chars_to_break_after, $prev_char,0,'UTF-8' ) !== FALSE || mb_strpos( $chars_to_break_before, $next_char,0,'UTF-8') !== FALSE ) { $out .= mb_substr($str,$strpos, $i-$strpos,'UTF-8') . '<wbr>'; $strpos = $i; $spc = $i; } } } $out .= mb_substr($str,$strpos,$len-$strpos,'UTF-8'); return $out; }

相關文章
相關標籤/搜索