Update from Git Manager GUI

This commit is contained in:
2026-03-21 00:54:10 +01:00
parent eab5084c06
commit 4fb7c0608e
7 changed files with 4200 additions and 0 deletions

View File

@@ -0,0 +1,249 @@
<?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 );
}
}