249 lines
9.6 KiB
PHP
249 lines
9.6 KiB
PHP
<?php
|
|
if ( ! defined( 'ABSPATH' ) ) exit;
|
|
|
|
/**
|
|
* WBF_BBCode — Sicherer BBCode → HTML Parser
|
|
*
|
|
* Unterstützte Tags:
|
|
* [b], [i], [u], [s], [h2], [h3]
|
|
* [code], [icode]
|
|
* [quote], [quote=Name]
|
|
* [spoiler], [spoiler=Titel]
|
|
* [color=...], [size=small|large|xlarge]
|
|
* [url=...], [url]
|
|
* [img]
|
|
* [list], [list=1], [*]
|
|
* [center], [right]
|
|
* [hr]
|
|
*/
|
|
class WBF_BBCode {
|
|
|
|
// Erlaubte Farben (Hex oder benannt, whitelist zur XSS-Prävention)
|
|
private static $allowed_colors = [
|
|
'red','blue','green','orange','yellow','purple','pink',
|
|
'cyan','white','gray','grey','black','gold','silver','lime','teal','navy',
|
|
];
|
|
|
|
/**
|
|
* Hauptmethode: BBCode → sicheres HTML
|
|
* Immer über diese Methode rendern!
|
|
*/
|
|
public static function render( $content ) {
|
|
if ( empty( $content ) ) return '';
|
|
|
|
// 1. HTML-Entities escapen (verhindert XSS aus rohem Input)
|
|
$out = htmlspecialchars( $content, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
|
|
|
|
// 2. Zeilenumbrüche vormerken (nach Tag-Parsing ersetzen)
|
|
$out = str_replace( "\r\n", "\n", $out );
|
|
|
|
// 3. [code]-Blöcke VOR allem anderen rausziehen & schützen
|
|
$placeholders = [];
|
|
$out = preg_replace_callback(
|
|
'/\[code\](.*?)\[\/code\]/is',
|
|
function ( $m ) use ( &$placeholders ) {
|
|
$key = '%%CODE_' . count($placeholders) . '%%';
|
|
$placeholders[$key] = '<pre class="wbf-bb-code"><code>'
|
|
. $m[1] // bereits html-escaped durch htmlspecialchars oben
|
|
. '</code></pre>';
|
|
return $key;
|
|
},
|
|
$out
|
|
);
|
|
|
|
// Inline-Code
|
|
$out = preg_replace_callback(
|
|
'/\[icode\](.*?)\[\/icode\]/is',
|
|
function ( $m ) use ( &$placeholders ) {
|
|
$key = '%%ICODE_' . count($placeholders) . '%%';
|
|
$placeholders[$key] = '<code class="wbf-bb-icode">' . $m[1] . '</code>';
|
|
return $key;
|
|
},
|
|
$out
|
|
);
|
|
|
|
// 4. Alle anderen Tags parsen
|
|
$out = self::parse( $out );
|
|
|
|
// 5. Zeilenumbrüche zu <br> (nur außerhalb von Block-Tags)
|
|
$out = self::nl_to_br( $out );
|
|
|
|
// 6. Code-Blöcke wieder einsetzen
|
|
foreach ( $placeholders as $key => $html ) {
|
|
$out = str_replace( $key, $html, $out );
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Sanitize bei Speicherung: nur HTML streifen, BBCode-Tags bleiben
|
|
*/
|
|
public static function sanitize( $raw ) {
|
|
// Alle echten HTML-Tags entfernen, BBCode-Tags [xxx] bleiben erhalten
|
|
return strip_tags( $raw );
|
|
}
|
|
|
|
// ── Interner Parser ──────────────────────────────────────────────────────
|
|
|
|
private static function parse( $s ) {
|
|
|
|
// [b] [i] [u] [s]
|
|
$s = preg_replace( '/\[b\](.*?)\[\/b\]/is', '<strong>$1</strong>', $s );
|
|
$s = preg_replace( '/\[i\](.*?)\[\/i\]/is', '<em>$1</em>', $s );
|
|
$s = preg_replace( '/\[u\](.*?)\[\/u\]/is', '<u>$1</u>', $s );
|
|
$s = preg_replace( '/\[s\](.*?)\[\/s\]/is', '<s>$1</s>', $s );
|
|
|
|
// [h2] [h3]
|
|
$s = preg_replace( '/\[h2\](.*?)\[\/h2\]/is', '<h2 class="wbf-bb-h2">$1</h2>', $s );
|
|
$s = preg_replace( '/\[h3\](.*?)\[\/h3\]/is', '<h3 class="wbf-bb-h3">$1</h3>', $s );
|
|
|
|
// [center] [right]
|
|
$s = preg_replace( '/\[center\](.*?)\[\/center\]/is', '<div class="wbf-bb-center">$1</div>', $s );
|
|
$s = preg_replace( '/\[right\](.*?)\[\/right\]/is', '<div class="wbf-bb-right">$1</div>', $s );
|
|
|
|
// [hr]
|
|
$s = str_replace( '[hr]', '<hr class="wbf-bb-hr">', $s );
|
|
|
|
// [color=...]
|
|
$s = preg_replace_callback(
|
|
'/\[color=([a-zA-Z0-9#]{1,20})\](.*?)\[\/color\]/is',
|
|
function ( $m ) {
|
|
$color = $m[1];
|
|
// Hex-Farben direkt erlauben, benannte aus Whitelist
|
|
if ( preg_match( '/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $color ) ) {
|
|
$safe = esc_attr( $color );
|
|
} elseif ( in_array( strtolower($color), self::$allowed_colors ) ) {
|
|
$safe = esc_attr( strtolower($color) );
|
|
} else {
|
|
return $m[2]; // Unbekannte Farbe → nur Text
|
|
}
|
|
return '<span style="color:' . $safe . '">' . $m[2] . '</span>';
|
|
},
|
|
$s
|
|
);
|
|
|
|
// [size=small|large|xlarge]
|
|
$s = preg_replace_callback(
|
|
'/\[size=(small|large|xlarge)\](.*?)\[\/size\]/is',
|
|
function ( $m ) {
|
|
$map = [ 'small' => '.8em', 'large' => '1.2em', 'xlarge' => '1.5em' ];
|
|
return '<span style="font-size:' . $map[$m[1]] . '">' . $m[2] . '</span>';
|
|
},
|
|
$s
|
|
);
|
|
|
|
// [url=...] und [url]...[/url]
|
|
$s = preg_replace_callback(
|
|
'/\[url=([^\]]{1,500})\](.*?)\[\/url\]/is',
|
|
function ( $m ) {
|
|
$href = esc_url( $m[1] );
|
|
if ( ! $href ) return $m[2];
|
|
return '<a href="' . $href . '" target="_blank" rel="noopener noreferrer" class="wbf-bb-link">' . $m[2] . '</a>';
|
|
},
|
|
$s
|
|
);
|
|
$s = preg_replace_callback(
|
|
'/\[url\](https?:\/\/[^\[]{1,500})\[\/url\]/is',
|
|
function ( $m ) {
|
|
$href = esc_url( $m[1] );
|
|
if ( ! $href ) return $m[1];
|
|
return '<a href="' . $href . '" target="_blank" rel="noopener noreferrer" class="wbf-bb-link">' . $href . '</a>';
|
|
},
|
|
$s
|
|
);
|
|
|
|
// [img]
|
|
$s = preg_replace_callback(
|
|
'/\[img\](https?:\/\/[^\[]{1,1000})\[\/img\]/is',
|
|
function ( $m ) {
|
|
$src = esc_url( $m[1] );
|
|
if ( ! $src ) return '';
|
|
return '<img src="' . $src . '" class="wbf-bb-img" alt="" loading="lazy">';
|
|
},
|
|
$s
|
|
);
|
|
|
|
// [quote] und [quote=Name]
|
|
$s = preg_replace_callback(
|
|
'/\[quote=([^\]]{1,80})\](.*?)\[\/quote\]/is',
|
|
function ( $m ) {
|
|
$author = '<strong>' . htmlspecialchars( $m[1], ENT_QUOTES ) . ' schrieb:</strong>';
|
|
return '<blockquote class="wbf-bb-quote"><span class="wbf-bb-quote__author">'
|
|
. $author . '</span>' . $m[2] . '</blockquote>';
|
|
},
|
|
$s
|
|
);
|
|
$s = preg_replace(
|
|
'/\[quote\](.*?)\[\/quote\]/is',
|
|
'<blockquote class="wbf-bb-quote">$1</blockquote>',
|
|
$s
|
|
);
|
|
|
|
// [spoiler] und [spoiler=Titel]
|
|
static $spoiler_id = 0;
|
|
$s = preg_replace_callback(
|
|
'/\[spoiler=([^\]]{0,80})\](.*?)\[\/spoiler\]/is',
|
|
function ( $m ) use ( &$spoiler_id ) {
|
|
$spoiler_id++;
|
|
$title = htmlspecialchars( $m[1] ?: 'Spoiler', ENT_QUOTES );
|
|
return '<div class="wbf-bb-spoiler" id="wbf-spoiler-' . $spoiler_id . '">'
|
|
. '<button type="button" class="wbf-bb-spoiler__btn" onclick="var c=this.nextElementSibling;c.style.display=c.style.display===\'none\'?\'block\':\'none\'">'
|
|
. '<i class="fas fa-eye-slash"></i> ' . $title
|
|
. '</button>'
|
|
. '<div class="wbf-bb-spoiler__body" style="display:none">' . $m[2] . '</div>'
|
|
. '</div>';
|
|
},
|
|
$s
|
|
);
|
|
$s = preg_replace_callback(
|
|
'/\[spoiler\](.*?)\[\/spoiler\]/is',
|
|
function ( $m ) use ( &$spoiler_id ) {
|
|
$spoiler_id++;
|
|
return '<div class="wbf-bb-spoiler" id="wbf-spoiler-' . $spoiler_id . '">'
|
|
. '<button type="button" class="wbf-bb-spoiler__btn" onclick="var c=this.nextElementSibling;c.style.display=c.style.display===\'none\'?\'block\':\'none\'">'
|
|
. '<i class="fas fa-eye-slash"></i> Spoiler'
|
|
. '</button>'
|
|
. '<div class="wbf-bb-spoiler__body" style="display:none">' . $m[1] . '</div>'
|
|
. '</div>';
|
|
},
|
|
$s
|
|
);
|
|
|
|
// [list] [list=1] [*]
|
|
$s = preg_replace_callback(
|
|
'/\[list(=1)?\](.*?)\[\/list\]/is',
|
|
function ( $m ) {
|
|
$tag = $m[1] ? 'ol' : 'ul';
|
|
$items = preg_replace( '/\[\*\]\s*/s', '<li>', $m[2] );
|
|
// Auto-close li tags
|
|
$items = preg_replace( '/(<li>)(.*?)(?=<li>|$)/s', '$1$2</li>', $items );
|
|
return '<' . $tag . ' class="wbf-bb-list">' . trim($items) . '</' . $tag . '>';
|
|
},
|
|
$s
|
|
);
|
|
|
|
// @Erwähnungen → klickbare Profil-Links
|
|
$s = preg_replace_callback(
|
|
'/@([a-zA-Z0-9_]{3,60})\b/',
|
|
function( $m ) {
|
|
global $wpdb;
|
|
$user = $wpdb->get_row( $wpdb->prepare(
|
|
"SELECT id, username FROM {$wpdb->prefix}forum_users WHERE username=%s", $m[1]
|
|
) );
|
|
if ( ! $user ) return esc_html($m[0]);
|
|
return '<a href="?forum_profile=' . (int)$user->id . '" class="wbf-mention">@' . esc_html($user->username) . '</a>';
|
|
},
|
|
$s
|
|
);
|
|
|
|
return $s;
|
|
}
|
|
|
|
// Zeilenumbrüche → <br>, aber nicht innerhalb von Block-Elementen
|
|
private static function nl_to_br( $s ) {
|
|
// Einfaches nl2br — ausreichend da Block-Tags (<h2>, <ul>, etc.)
|
|
// bereits eigene Zeilenumbrüche erzeugen
|
|
return nl2br( $s );
|
|
}
|
|
} |