PHP regex Pattern läuft nicht wie erwartet


ruNN0r

Erfahrenes Mitglied
Moin,
ich bin gerade dabei mithilfe von Regex einen eine art BBCode auseinander zu ziehen. Leider kommt nicht das dabei raus was ich gerne hätte und ich stehe gerade auf dem Schlauch.
Vielleicht hat jemand eine zündende Idee für mich.

Folgendes möchte ich Auswerten:
Code:
[BOX width="100%" title="Test"][/BOX]
Ziel soll es sein, dass ich die einzelnen Parameter erhalte inkl. key erhalte.
also etwa so:
PHP:
array(
    0 => 'width="100%"',
    1 => 'title="Test"'
)
Mein Pattern sieht wie folgt aus:
Ich habe es etwas auseinandergezogen um es lesbarer zu machen und Variabeln nur pseudo eingesetzt.
PHP:
$wert = "[a-zA-Z\-_]+";
$value = "[\s\d\w\b%\-_&$§!.,;|<>\(\)"'#+*~/\\?ßäöü{}]*";
$pattern = '\[BOX ( $wert="$value")* *\]';
Meine Idee war, dadurch dass ich das Key->Value Paar in die Klammern gesetzt habe, sollte das match_all mir jeden Teil rausfiltern der diesem Pattern entspricht. Funktioniert super, wenn ich nur ein Key->Value Paar habe! Bei zwei oder mehreren erhalte ich immer nur das letzte.

Woran habe ich nicht gedacht?

Ich danke euch und wünsche einen schönen Montag.

Edit:
Das Problem ist gelöst, aber für jeden Anfänger oder jeden der sich vielleicht denkt, man könnte den unten stehenden Code einfach in seinen übernehmen, leßt auch das von @ComFreek und achtet auf die Sicherheit. Das ist nur ein Beispiel und bei mir für interne Anwendung gedacht. Keinesfalls sollte man diesen Code ungefiltert in einer Live Umgebung nutzen!
 
Zuletzt bearbeitet:

Yaslaw

n/a
Moderator
Ich würde 2 Schritte machen.
1) Die Box und andere BBCode finden
2) Die Attribute auslesen

1)\[(BOX|LNK|SONSTWAS) [^\]]+\](.*)\[\/\1]
  1. BBCode
  2. Attribut-String
  3. BBCode-Inhalt
2)(\w+)\s*=\s*"([^"]+)"
  1. Attribut-Name
  2. Attribut-Wert
 

ruNN0r

Erfahrenes Mitglied
Eine tolle Idee, danke dir! Ich werde das nochmal versuchen. Ich habe meinen Teststring mal durchgejagt aber das passt noch nicht so ganz.

Als Test habe ich mir folgendes gebaut:
Code:
[ROW]
    [COL]
        [BOX title="Test Text" width="50%"]
            Hallo ich bin ein Test
        [/BOX]
    [/COL]
    [COL title="" only_for="26"]
        [BOX title="Test HTML" width="50px"]
            Hier mal ein Link
            <img src="x.png">
            <a href="test.pdf">Yehaa</a>
        [/BOX]
    [/COL]
[/ROW]
[ROW]
    [COL]
        [BOX title="Test BBCODE" width="100px"]
            [testelement]
        [/BOX]
        [BOX][testelement1][/BOX]
        [BOX title="Test"][testelement2][/BOX]
        [BOX title="Test" ][testelement3][/BOX]
    [/COL]
[/ROW]
Das Pattern habe ich wie folgt geändert:
\[(BOX|COL|ROW) ?([^\]]*)[ ]*\](.*)\[\/\1]

Er kommt nicht mit den Zeilenumbrüchen zurecht. Die letzten drei mit dem [testelement1][testelement2][testelement3] funktionieren wurderbar nur die anderen kommen nicht mit.

Dann habe ich s gesetzt damit der Punkt auch Steuerzeichen berücksichtigt #\[(BOX|COL|ROW) ?([^\]]*)[ ]*\](.*)\[\/\1]#s
aber dann bekomme ich nur das eine ROW als Resultat. Ändere ich das Pattern ab, damit nur BOX berücksichtigt fängt er beim start [BOX] an und hört beim letzten [/BOX] auf die dazwischen sind dann aber mit drin.

Gibt es da noch andere Möglichkeiten \r \n \t in Kombination mit dem . zu berücksichtigen ohne dabei die s Option zu benutzen? Oder passt der ganze Ansatz in dieser Konstellation nicht mehr?

Aber deinen Ansatz eins nach dem andern zu machen ist gut, das gibt mir noch andere Möglichkeiten.
 

Yaslaw

n/a
Moderator
Ersetze den Punkt durch [\s\S]. Das nimmt auch die Zeilenumbrüche. Und dann den * durch *?. Das bedeutet, dass er nicht gierig ist.
Code:
\[(BOX|COL|ROW) ?([^\]]*)[ ]*\]([\s\S]*?)\[\/\1]
Natürlich musst dud en Inhalt selber wieder durch die Funktion schicken um alles in der [ROW]...[/ROW] auch zu parsen.
 
Zuletzt bearbeitet von einem Moderator:

ruNN0r

Erfahrenes Mitglied
Du bist ein Schatz! Das Fragezeichen ist wohl der Schlüssel zum Erfolg! Ich hatte das [\s\S]* auch schon probiert aber mit dem ? dahinter sieht es auf den ersten Blick zumindest genau so aus wie ich es haben wollte.
Das ich das ganze Recursive bearbeiten muss war mir klar. Aber erstmal musste ich ein Pattern zusammen bekommen, dass mir Liefert was ich will.

Ich teste die dann immer hier: PHP Live Regex
und dann kommt der Code. Sonst programmiere ich mir ja einen Wolf ^^

Aber super! Danke dir vielmals, das sieht schon verdammt gut aus. Ich werde das nachher nochmal genauer testen und einen Code draus bauen und diesen hier reinstellen.
 

ruNN0r

Erfahrenes Mitglied
Ja, den kenne ich auch, aber der andere gefällt mir besser. Das ist aber Ansichtssache denke ich.

Ich habe das ganze jetzt mal schnell in eine einfache Klasse gebastellt.

PHP:
<?php
class BBCodeParser{

    /**
     * Gibt an wie oft ein BBCode gefunden wurde um festzulegen ob ein weiterer Durchlauf notwenig ist. Erst wenn ein Durchlauf keine ersetzung hervorgebracht hat wird abgebrochen!
     */
    private $replace = 0;

    /**
     * Beginnt die Umwandlung eines mit BBCodes versetzten Strings in einen mit HTML Code versetzten Strings
     * @param string $content String mit BBCodes
     * @return string String mit umgewandelten BBCodes
     */
    public function ParseBBCode($content){

        do {

            $this->replace = 0;
            $content = $this->BBCodeParser($content);

        } while($this->replace > 0);

        return $content;

    }

    /**
     * Wandelt den gefilterten Attributstring in ein Attribut-Array um
     * @param string $attribute_text String mit den Attributen
     * @return array Array mit den Attributen. Der Key spiegelt den Namen des Attributes wieder.
     */
    private function ParseAttributes($attribute_text){
        preg_match_all('/(\w+)\s*=\s*"([^"]+)/si', $attribute_text, $output_array);
   
        $attributes = [];
   
        if(is_array($output_array)){
           
            foreach($output_array[1] as $key => $attribute_name){
                $attributes[$attribute_name] = $output_array[2][$key];
            }
   
        }
   
        return $attributes;
    }

    /**
     * Wandelt die BBCodes in HTML um
     * @param string $content String mit BBCodes
     * @return string String mit umgewandelten BBCodes in 1 Ebene (ggf. wiederholung notwendig).
     */
    private function BBCodeParser($content){
        preg_match_all('/\[([A-Za-z_-]+) ?([^\]]*)[ ]*\]([\s\S]*?)\[\/\1]/', $content, $output_array);
   
        if(is_array($output_array)){
   
            // Aufteilen des Arrays in die einzelnen Bereiche
            $codes = $output_array[1];
            $attribute_lines = $output_array[2];
            $bbcontents = $output_array[3];

            // Sicherheitshalber mal den Bereich freigaben.
            unset($output_array);
   
            // Gefundenen Elemente durchlaufen
            for($i = 0; $i < count($codes); $i++){
   
                $code = $codes[$i]; // Der gefundene BBCODE
                $attributes = $this->ParseAttributes($attribute_lines[$i]); // Die Attribute
                $bbcontent = $bbcontents[$i]; // Der Inhalt im BBCODE
                   
                // Abfragen um welchen BBCODE es sich handelt
                if($code == "ROW"){

                    $this->replace++;
                   
                    // Ersetzen durch HTML Code
                    $content = preg_replace('/\[(ROW) ?([^\]]*)[ ]*\]([\s\S]*?)\[\/\1]/', '<div class="row">$3</div>', $content);

                }
   
                if($code == "COL"){

                    $this->replace++;

                    // Ersetzen durch HTML Code
                    $content = preg_replace('/\[(COL) ?([^\]]*)[ ]*\]([\s\S]*?)\[\/\1]/', '<div class="col">$3</div>', $content);

                }
               
                if($code == "BOX"){

                    $this->replace++;

                    $style="";

                    // Attribute verarbeiten
                    if(!isset($attributes['title']))
                    {
                        $attributes['title'] = "NO TITLE";
                    }
                   
                    $style = "width: ".($attributes['width'] ?? '100%');
                   
   
                    // Ersetzen durch HTML Code
                    $content = preg_replace('/\[(BOX) ?([^\]]*)[ ]*\]([\s\S]*?)\[\/\1]/', '<div class="box"><div class="title" style="'.$style.'">'.($attributes['title']).'</div><div class="content">$3</div></div>', $content);

                }
   
            }
   
        }
   
        return $content;
    }

}

$content = '
[ROW]
    [COL]
        [BOX title="Test Text" width="50%"]
            Hallo ich bin ein Test
        [/BOX]
    [/COL]
    [COL title="" only_for="26"]
        [BOX title="Test HTML" width="50px"]
            Hier mal ein Link
            <img src="x.png">
            <a href="test.pdf">Yehaa</a>
        [/BOX]
    [/COL]
[/ROW]

[ROW]
    [COL]
        [BOX title="Test BBCODE" width="100px"]
            [testelement]
        [/BOX]
        [BOX][testelement1][/BOX]
        [BOX title="Test"][testelement2][/BOX]
        [BOX title="Test" ][testelement3][/BOX]
    [/COL]
[/ROW]
';

$bbcode_parser = new BBCodeParser();
$content = $bbcode_parser->ParseBBCode($content);

echo nl2br(htmlspecialchars($content));  
?>
Ich hoffe das hilft jemandem.
Aber es ist jetzt wirklich nur ganz grob in eine Klasse gebaut. Der Code lässt sich noch vereinfachen und verschönern! Fürs bessere Verständnis und der Übersichtlichkeit halber habe ich es so gelassen.

Vorsicht beim erweitern!!!:
wer noch weitere BBCodes einbauen möchte sollte vorsichtshalber eine weitere Countervariable einsetzen (sofern man via Print debuggt). Denn wenn man wie in Zeile 92 nach einem BBCode abfragt dieser auch gefunden wird, dann aber durch den preg_replace nicht korrekt ersetzt wird, befindet man sich in einer Endlosschleife!

@Yaslaw darf ich dich noch Fragen welche Entwicklungsumgebung du nutzt (auch wenn es nicht ganz zum Thema passt)? Ich nutze momentan Visual Studio Code. Finde das eigentlich sehr gut aber mit Codevervollständigung funktioniert nur bedingt. Gerade wenn man mit Namespaces arbeitet. Ich habe auch schon Eclipse versucht, aber das war gar nichts für mich.
 
Zuletzt bearbeitet:

ComFreek

Mod | @comfreek
Moderator
Ich wäre vorsichtig damit, BB-Codes mit Regex zu parsen. Die Sprache von BB-Codes ist keine reguläre Sprache (so wie HTML), d.h. diese mit regulären Audrücken (i. S. v. Programmiersprachen, nicht TCS) zu parsen ist immer ein entweder fehlerbehaftetes oder aufwändiges Unterfangen.

Bist du dir sicher, dass du

1. alle gültigen BB-Code-Sequenzen parsen kannst und
2. du keine XSS-Angriffe zulässt?

Letzteres scheint mir bei dir nicht der Fall zu sein. HTML zu sanitizen ist auch ein nichttriviales Unterfangen, google hierfür nach "HTML Sanitizer".
 

ruNN0r

Erfahrenes Mitglied
Ich danke dir sehr @ComFreek für diesen Hinweis und schätze es sehr, dass du dir auch um die Sicherheit gedanken machst. Ich erkläre mein Unterfangen aber mal genauer:
Ich möchte mit diesen Codes lediglich das Design eines Dashboards bestimmen und es etwas vereinfachen das Dashboard schnell mal zu verändern oder auch mit eigenen Elementen auszustatten ohne, dass man gleich den Editor zücken muss ([testelement1] wird z.B. mittels str_replace durch einen generierten Code ersetzt.).

Verwaltet wir das ganze durch die Administratoren, von denen zwar eher wenig Risiko ausgeht aber dennoch:
Nach der Codeeingabe wird alles durch den HTMLPurifier geschickt bevor es in die Datenbank geschrieben wird. Soweit ich weiß ist der gut gewartet und bisher hatte ich keine Probleme. Auch Links mit <a href="javascript: xss();"> oder onclick werden rausgefiltert.

In diesem Thread wollte ich mich wirklich nur rein um die Ausabe des ganzen kümmern und da habe ich gemerkt, dass ich bei Regex meine Probleme habe und hier eine gute Hilfe gefunden habe.

Mir allerdings für das Parsen der BB-Codes auch nichts Besseres eingefallen als Regex da sie in diesem Umfang ja schon einen relativ simplen Muster folgen. Wenn du da einen Vorschlag hast greife ich den auch gerne auf!
 

Yaslaw

n/a
Moderator
@Yaslaw darf ich dich noch Fragen welche Entwicklungsumgebung du nutzt (auch wenn es nicht ganz zum Thema passt)? Ich nutze momentan Visual Studio Code. Finde das eigentlich sehr gut aber mit Codevervollständigung funktioniert nur bedingt. Gerade wenn man mit Namespaces arbeitet. Ich habe auch schon Eclipse versucht, aber das war gar nichts für mich.
Wenn ich mal was in PHP programmiere, also so ein zwei mal im Jahr, dann mit Eclipse. Das kenne ich halt vom Java her.
 

ruNN0r

Erfahrenes Mitglied
Achso, dann suche ich dazu nochmal weiter. Danke dir vielmals auch für die gute Hilfe. Ich markiere als gelöst und wünsche einen schönen Tag.
@ComFreek zum Thema Sicherheit bearbeite ich noch nochmal den ersten Post.
 

ComFreek

Mod | @comfreek
Moderator
Wenn du da einen Vorschlag hast greife ich den auch gerne auf!
Sich einen richtigen Parser selbst zu schreiben, ist nicht so einfach. Vielleichst hast du Glück und es gibt einen fertigen PHP BB-Code Parser als Paket irgendwo online. Falls nicht, würde ich als nächstes auf einen Parsergenerator setzen, wo du eine EBNF Grammatik eingibst, und einen Parser zurückbekommst.

Das Muster von BB-Codes ist tatsächlich nicht so einfach. Gut sehen kann man das daran, dass dein Parser übrigens keine Boxtitel mit Double Quotes oder schließenden eckigen Klammern erlaubt.

Nach der Codeeingabe wird alles durch den HTMLPurifier geschickt bevor es in die Datenbank geschrieben wird.
Der schlussendliche Datenfluss ist also: Nutzereingabe --> HTMLPurifier --> "BB-to-HTML-Compiler" von dir.

Code:
<!-- CSS Injection -->
[BOX title="..." width="50%;display:none"]...[/BOX]

<!-- Angenommen, du würdest Attribute mit Single Quotes zulassen, so hättest du nun XSS -->
[BOX title="..." width='50%" onclick="alert(1)"']...[/BOX]
Es gibt sicher noch viele weitere Varianten zum Exploiten des Codes. Hier findest du unzählige Anreize: XSS Filter Evasion Cheat Sheet | OWASP.

Es wäre möglicherweise besser, den Datenfluss auf Folgendes abzuändern: Nutzereingabe --> "BB-to-HTML-Compiler" von dir --> HTMLPurifier

Das hätte den Nachteil, dass man nun auch HTML eingeben kann und dieses auch verwertet wird (sofern HTMLPurifier es durchlässt). Auf der anderen Seite kannst du sehr sicher sein, dass das, was am Ende herauskommt, auch sicher* ist, denn HTMLPurifier ist die letzte Instanz.

*) so sicher wie HTMLPurifier eben
 

ruNN0r

Erfahrenes Mitglied
Ja, ok, das ist nachvollziehbar.
Um die Attribute mache ich mir aber weniger Sorgen (abgesehen von den Double Quotes, die Berücksichtigung fehlt tatsächlich, das rüste ich nach!). Die Attribute sind aber sehr strikt gehalten. Nur ausgewählte Attribute werden ausgewertet, alles andere wird verworfen. Ebenso wird der Inhalt des Attributes nochmal geprüft ob dieser auch zu dem Feld passt. Damit möchte ich zum einen die von dir genannten Beispiele verhindern und zum anderen Anzeigefehler durch Fehleingaben verringern.

Hier mal ein Beispiel aus meiner Attributauswertung:
PHP:
if(isset($attributes['corner']) && is_numeric($attributes['corner'])){
    $style_main_box .= " border-radius: ".$attributes['corner']."px;";
}
Bei der width sind z.B. auch nur Werte zulässig die %, px, em am Ende haben und der vorangehende Wert muss in float geparsed werden können.

Ich hatte auch noch daran gedacht geschlossene Tags für die keine offene bestehen auszusieben aber das übernimmt der Purifier für mich, wie ich feststellen habe :)

Den HTMLPurifier werde ich dann Sicherheitshalber bei der Ausgabe ausführen.
Damit sollte ich dann wirklich alles notwendige getan haben. Was die Sicherheit angeht zumindest.
 

Neue Beiträge