核心解析器架構
<?php
function ubb(string $string): string {
// 階段一:正則替換 (含屬性、巢狀結構)
$searchRegex = [
// [quote]...[/quote]
'/\s*\[quote\][\n\r]*(.+?)[\n\r]*\[\/quote\]\s*/is' =>
'
',
// [box=#color]...[/box]
'/\[box=([#0-9A-F]{6}|[a-z]+)\][\n\r]*(.+?)[\n\r]*\[\/box\]/is' =>
'$title\2
',
// [url]https://...[/url] 自動連結
'/\[url\][\s]*(www\.|https?://|ftp://|gopher://|news://|telnet://|rtsp://|mms://){1}([^\[\"\']+?)[\s]*\[\/url\]/ie' =>
"cuturl('\1\2')",
// [url=...]text[/url]
'/\[url=www\.([^\[\"\']+?)\](.+?)\[\/url\]/is' =>
'\2',
'/\[url=(https?|ftp|gopher|news|telnet|rtsp|mms)://([^\[\"\']+?)\](.+?)\[\/url\]/is' =>
'\3',
// [email]...[/email]
'/\[email\][\s]*([A-Za-z0-9-_.]+)@([A-Za-z0-9-_]+\.[A-Za-z0-9-_.]+)[\s]*\[\/email\]/i' =>
'\1@\2',
'/\[email=([A-Za-z0-9-_.]+)@([A-Za-z0-9-_]+\.[A-Za-z0-9-_.]+)\](.+?)\[\/email\]/is' =>
'\3',
// 文字樣式
'/\[color=([^\[\]]+?)\]/i' => '',
'/\[size=([^\[\]]+?)\]/i' => '',
'/\[font=([^\[\]]+?)\]/i' => '',
'/\[align=([^\[\]]+?)\]/i' => '',
'/\[center\]/i' => '
',
// 多媒體
'/\[swf\][\s]*([^\[\]]+?)[\s]*\[\/swf\]/ies' =>
"bbcodeurl('\1', '
Flash: %s ')",
'/\[img\][\s]*([^\[\]]+?)[\s]*\[\/img\]/ies' =>
"bbcodeurl('\1', '
screen.width*0.7){this.resized=true;this.width=screen.width*0.7;this.alt=\'Click to open\';}\" onmouseover=\"if(this.resized)this.style.cursor=\'pointer\'\" onclick=\"if(this.resized){window.open(\'%s\');}\">')",
'/\[img=(\d{1,3})[x|,](\d{1,3})\][\s]*([^\[\]]+?)[\s]*\[\/img\]/ies' =>
"bbcodeurl('\3', '
')",
];
// 階段二:簡單標籤直接替換
$searchStr = ['[/color]', '[/size]', '[/font]', '[/align]', '[b]', '[/b]', '[i]', '[/i]', '[u]', '[/u]', '[list]', '[list=1]', '[list=a]', '[list=A]', '[*]', '[/list]', '[/center]'];
$replaceStr = ['
', '', '', '', '', '', '', '', '', '', '', ''];
// 執行替換:先正則後字串
$string = str_replace($searchStr, $replaceStr, preg_replace(array_keys($searchRegex), array_values($searchRegex), $string));
return $string;
}
// 輔助函數:URL 連結生成與 XSS 過濾
function bbcodeurl(string $url, string $tags): string {
if (!preg_match('/<.+?>/s', $url)) { // 非 HTML 標籤
if (!in_array(strtolower(substr($url, 0, 6)), ['http:/', 'ftp://', 'rtsp:/', 'mms://'])) {
$url = 'https://' . $url;
}
return str_replace('submit', '', sprintf($tags, $url, $url));
}
return ' ' . $url; // 含 HTML 標籤視為不安全,僅顯示
}
function cuturl(string $url): string {
$len = 65;
$link = '';
if (strlen($url) > $len) {
$url = substr($url, 0, intval($len * 0.5)) . ' ... ' . substr($url, -intval($len * 0.3));
}
return $link . $url . '';
}
?>
安全性關鍵點分析
| 風險 | 原始碼處理 | 建議加強 |
| XSS 注入 | bbcodeurl() 檢測 `<...>` 標籤拒絕處理 | 輸出前統一 htmlspecialchars(),採用 CSP 標頭 |
| 巢狀標籤錯誤 | 正則貪婪匹配可能導致跨區塊 | 改用狀態機/遞迴下降解析器 (如 s9e/TextFormatter) |
| 屬性注入 | 色碼/尺寸/字體未白名單驗證 | 屬性值嚴格驗證:color 限 hex/關鍵字、size 限 1-7 |
| 效能災難 | 大量 /e 修飾符 (PHP 7 移除) + 複雜正則 | 預編譯模板、快取編譯結果、避免 preg_replace_callback 重複編譯 |
現代化重構方向
- 採用 s9e/TextFormatter 或 BBCode\Parser (Composer 套件),支援巢狀、安全屬性、可擴充標籤。
- 編譯階段產出 AST,渲染階段輸出 HTML,分離關注點。
- 整合
HTMLPurifier 做最終輸出清洗。
- 編輯器前端改用 CKEditor/TinyMCE 直接輸出 HTML,後端僅做 Purify,棄用 BBCode。