' . htmlspecialchars($m[1], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '';
return "__CODEBLOCK_{$idx}__";
}, $text);
// Escape remaining text to avoid XSS
$text = htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
// Basic tags
$simple = [
'/\[b\](.*?)\[\/b\]/is' => '$1',
'/\[i\](.*?)\[\/i\]/is' => '$1',
'/\[u\](.*?)\[\/u\]/is' => '$1',
'/\[s\](.*?)\[\/s\]/is' => '$1',
];
foreach ($simple as $pat => $rep) {
$text = preg_replace($pat, $rep, $text);
}
// [url=link]text[/url] and [url]link[/url]
$text = preg_replace_callback('/\[url=(.*?)\](.*?)\[\/url\]/is', function ($m) {
$url = $this->sanitizeUrl(html_entity_decode($m[1]));
$label = $m[2];
return '' . $label . '';
}, $text);
$text = preg_replace_callback('/\[url\](.*?)\[\/url\]/is', function ($m) {
$url = $this->sanitizeUrl(html_entity_decode($m[1]));
return '' . $url . '';
}, $text);
// [img]url[/img]
$text = preg_replace_callback('/\[img\](.*?)\[\/img\]/is', function ($m) {
$src = $this->sanitizeUrl(html_entity_decode($m[1]));
return '
';
}, $text);
// [quote]...[/quote]
$text = preg_replace('/\[quote\](.*?)\[\/quote\]/is', '$1
', $text);
// [list] and [*]
// Convert [list]...[*]item[*]...[/list] to
$text = preg_replace_callback('/\[list\](.*?)\[\/list\]/is', function ($m) {
$items = preg_split('/\[\*\]/', $m[1]);
$out = '';
foreach ($items as $it) {
$it = trim($it);
if ($it === '') continue;
$out .= '' . $it . '';
}
return '';
}, $text);
// sizes and colors: simple inline styles
$text = preg_replace('/\[size=(\d+)\](.*?)\[\/size\]/is', '$2', $text);
$text = preg_replace('/\[color=(#[0-9a-fA-F]{3,6}|[a-zA-Z]+)\](.*?)\[\/color\]/is', '$2', $text);
// Preserve line breaks
$text = nl2br($text);
// Restore code blocks
if (!empty($codeBlocks)) {
foreach ($codeBlocks as $i => $html) {
$text = str_replace('__CODEBLOCK_' . $i . '__', $html, $text);
}
}
return $text;
}
protected function sanitizeUrl($url)
{
$url = trim($url);
// allow relative paths
if (strpos($url, 'http://') === 0 || strpos($url, 'https://') === 0 || strpos($url, '/') === 0) {
return htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
// fallback: prefix with http:// if looks like domain
if (preg_match('/^[A-Za-z0-9\-\.]+(\:[0-9]+)?(\/.*)?$/', $url)) {
return 'http://' . htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
return '#';
}
}