Files
WP-Business-Forum/includes/class-forum-bbcode.php
2026-03-29 22:25:37 +02:00

260 lines
10 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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] oder [size=17] (klassisches BBCode)
$s = preg_replace_callback(
'/\[size=([a-zA-Z0-9]+)\](.*?)\[\/size\]/is',
function ( $m ) {
$val = strtolower( $m[1] );
// Benannte Größen
$named = [ 'small' => '.8em', 'large' => '1.2em', 'xlarge' => '1.5em' ];
if ( isset( $named[ $val ] ) ) {
$size = $named[ $val ];
// Numerische Größen 17 (klassisches BBCode-Schema)
} elseif ( ctype_digit( $val ) && (int)$val >= 1 && (int)$val <= 7 ) {
$num_map = [ 1 => '.7em', 2 => '.85em', 3 => '1em', 4 => '1.2em', 5 => '1.4em', 6 => '1.6em', 7 => '2em' ];
$size = $num_map[ (int)$val ];
} else {
return $m[2]; // Unbekannter Wert → nur Text
}
return '<span style="font-size:' . $size . '">' . $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 );
}
}