UBB (Ultimate Bulletin Board) 代碼曾是早期論壇(Discuz!、phpWind、PHPWind)的核心富文本標記語言。雖然現代項目多採用 Markdown 或 HTML 直接輸出,但在老系統遷移、數據清洗、兼容歷史內容場景下,UBB 解析器仍具實戰價值。本文深度解析經典 UBB 轉 HTML 實現,並給出安全加固、擴展性重構、現代替代方案建議。
<?php
function ubb2html($content) {
global $article; // 依賴全局變量,耦合度高
// 1. 自動識別 URL(配置開關)
if ($article['isparseurl'] == "1") {
$content = parseurl($content); // 外部函數,未定義
}
// 2. 基礎標籤替換(使用已棄用 eregi_replace)
$content = eregi_replace(quotemeta("[b]"), "<b>", $content);
$content = eregi_replace(quotemeta("[/b]"), "</b>", $content);
$content = eregi_replace(quotemeta("[i]"), "<i>", $content);
$content = eregi_replace(quotemeta("[/i]"), "</i>", $content);
$content = eregi_replace(quotemeta("[u]"), "<u>", $content);
$content = eregi_replace(quotemeta("[/u]"), "</u>", $content);
$content = eregi_replace(quotemeta("[center]"), "<center>", $content);
$content = eregi_replace(quotemeta("[/center]"), "</center>", $content);
// 3. 複雜標籤:引用、代碼、圖片、鏈接、郵箱
$content = eregi_replace(quotemeta("[quote]"), '<table...><b>引用:</b>...', $content);
$content = eregi_replace(quotemeta("[/quote]"), '</font></td></tr>...', $content);
$content = eregi_replace(quotemeta("[code]"), '<table...><b>代碼:</b>...', $content);
$content = eregi_replace(quotemeta("[/code]"), '</font></td></tr></table>', $content);
$content = eregi_replace("/\[img\]([^\[]*)\[\/img\]/", '<a href="\\1" target="_blank"><img src="\\1" border=0...></a>', $content);
$content = eregi_replace("/\[url\]www\.([^\[]*)\[\/url\]/", '<a href="https://www.\\1" target=_blank>www.\\1</a>', $content);
$content = eregi_replace("/\[url\]([^\[]*)\[\/url\]/", '<a href="\\1" target=_blank>\\1</a>', $content);
$content = eregi_replace("/\[url=([^\[]*)\]([^\[]*)\[\/url\]/", '<a href="\\1" target=_blank>\\2</a>', $content);
$content = eregi_replace("/\[email\]([^\[]*)\[\/email\]/", '<a href="mailto:\\1">\\1</a>', $content);
return $content;
}
?>
| 問題 | 風險等級 | 說明 |
|---|---|---|
使用 eregi_replace |
🔴 嚴重 | PHP 5.3 棄用,7.0 移除,代碼無法在現代 PHP 運行 |
| 正則缺乏邊界約束 | 🟠 高 | [^\[]* 貪婪匹配導致跨標籤吞噬、嵌套標籤錯亂 |
| 無 XSS 過濾 | 🔴 嚴重 | 用戶輸入 [url=javascript:alert(1)]點我[/url] 直接輸出可執行 JS |
| HTML 結構硬編碼 | 🟡 中 | 表格佈局、內聯樣式難以適配響應式設計、CSP 策略 |
依賴全局變量 $article |
🟡 中 | 難以測試、復用,違反單一職責原則 |
| 不支持嵌套標籤 | 🟠 高 | [b][i]文字[/b][/i] 會產生無效 HTML |
<?php
class UbbParser {
private $config = [
'allow_url' => true,
'allow_img' => true,
'auto_link' => true,
'max_nesting' => 10,
'xss_filter' => true,
];
// 標籤定義:正則 => 替換回調
private $tags = [
// 簡單標籤
'/\[b\](.+?)\[\/b\]/is' => fn($m) => "<strong>{$m[1]}</strong>",
'/\[i\](.+?)\[\/i\]/is' => fn($m) => "<em>{$m[1]}</em>",
'/\[u\](.+?)\[\/u\]/is' => fn($m) => "<u>{$m[1]}</u>",
'/\[center\](.+?)\[\/center\]/is' => fn($m) => "<div style='text-align:center'>{$m[1]}</div>",
// 帶屬性標籤
'/\[url=([^\]]+)\](.+?)\[\/url\]/is' => function($m) {
$url = $this->sanitize_url($m[1]);
$text = $this->parse_inline($m[2]); // 遞歸解析內層
return $url ? "<a href='{$url}' target='_blank' rel='noopener'>{$text}</a>" : $m[0];
},
'/\[img\]([^\]]+)\[\/img\]/is' => function($m) {
$src = $this->sanitize_url($m[1]);
return $src ? "<img src='{$src}' alt='' loading='lazy'>" : $m[0];
},
'/\[email\]([^\]]+)\[\/email\]/is' => fn($m) => "<a href='mailto:{$m[1]}'>{$m[1]}</a>",
// 區塊標籤
'/\[quote\](.+?)\[\/quote\]/is' => function($m) {
$content = $this->parse_block($m[1]);
return "<blockquote class='quote'>{$content}</blockquote>";
},
'/\[code\](.+?)\[\/code\]/is' => function($m) {
$code = htmlspecialchars($m[1], ENT_QUOTES, 'UTF-8');
return "<pre><code>{$code}</code></pre>";
},
];
public function parse($text) {
// 1. 預處理:自動識別 URL
if ($this->config['auto_link']) {
$text = $this->auto_link($text);
}
// 2. 多輪迭代解析(處理嵌套)
for ($depth = 0; $depth < $this->config['max_nesting']; $depth++) {
$newText = $text;
foreach ($this->tags as $pattern => $callback) {
$newText = preg_replace_callback($pattern, $callback, $newText);
}
if ($newText === $text) break; // 無變化提前退出
$text = $newText;
}
// 3. 兜底:轉義殘留方括號防止輸出破壞
$text = str_replace(['[', ']'], ['[', ']'], $text);
return $text;
}
private function sanitize_url($url) {
$url = trim($url);
// 僅允許 http/https/mailto 協議,防 XSS
if (!preg_match('/^(https?|mailto):/i', $url)) {
// 相對路徑補全或拒絕
if (str_starts_with($url, '/')) return $url;
return false;
}
// 進一步可集成 HTMLPurifier 深度過濾
return htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
}
private function auto_link($text) {
// 簡易自動鏈接,實際建議使用 league/url-highlighter
return preg_replace_callback(
'/(?])(https?:\/\/[^\s<]+)/i',
fn($m) => "<a href='{$m[1]}' target='_blank' rel='noopener'>{$m[1]}</a>",
$text
);
}
// 遞歸解析內層內容
private function parse_inline($text) { return $this->parse($text); }
private function parse_block($text) { return $this->parse($text); }
}
// 使用
$parser = new UbbParser();
$html = $parser->parse("[b]粗體 [url=https://example.com]鏈接[/url] 文字[/b]");
?>
league/commonmark (CommonMark 標準)、erusev/parsedown,生態成熟、擴展豐富、安全默認。HTMLPurifier 或框架內置 e()/htmlspecialchars()。Toast UI Editor、TipTap、Quill 等現代富文本編輯器,輸出 Markdown/JSON,避免用戶手寫標記語言。經典 UBB 解析器承載了中國早期 Web 社區的記憶,但其正則替換架構、缺乏安全防護、難以處理嵌套的缺陷已不適合現代開發。理解其原理有助於數據遷移與歷史代碼維護,新項目請堅定選擇 Markdown + 成熟解析庫 + HTMLPurifier 的成熟技術棧。