Symfony Guard component without using the whole framework

Implementing authentication in Symfony can be quite complicated. Even more so, if you attempt to use only the Security component without the whole framework. In this post I’ll show you, how you can use the Symfony Guard component with a form login and a logout link. So here’s what you can expect from implementing the code from this blog entry

  • Login with username and password by form
  • Stay logged in by using a session
  • Logout via Hyperlink „/logout“

My aim is to use as little components from Symfony as possible in this tutorial. Every project is different and I don’t know which components you might want to use.

Continue reading

Probleme mit RedmineCRM und Ubuntu 14.04 lösen

RedmineCRM bietet mehrere hervorragende Plugins für Redmine an. Besonders interessant war für mich das Invoices-Plugin, dass ich gestern installiert habe. Wenn es erstmal installiert ist, erleichtert Arbeitsabläufe ungemein. Leider war die Installation unter Ubuntu 14.04 nicht ganz problemlos, falls jemand anderes ebenfalls damit zu kämpfen hatte, hier ein paar Hinweise (vollständige Pfade in den folgenden Beispielen zwecks weniger Verwirrung):

  1. Falls dies die ersten Plugins sind, die man einrichtet, gibt es in /usr/share/redmine ggf. kein plugins-Verzeichnis. Dies kann einfach per mkdir /usr/share/redmine/plugins angelegt werden. Die Verwendung von vendor/plugins ist deprecated und führt zu Problemen, wie ich feststellen musste. In das neu angelegte Plugins-Verzeichnis werden die von RedmineCRM heruntergeladenen Plugins kopiert und entpackt.
  2. Es steht zwar in der Dokumentation, aber ich habs trotzdem überlesen: Manche Plugins (Invoices, Helpdesk) erwarten als Dependency das CRM-Plugin (redmine_contacts), dieses also vor den anderen installieren!
  3. Es gibt zum Thema Ubuntu 14.04 und RedmineCRM einen (etwas versteckten, da nicht direkt von der Dokumentation verlinkten) FAQ-Eintrag von RedmineCRM. Die Hinweise daraus sollten unbedingt befolgt werden! Da ich allerdings etwas besorgt war, meine Datenbankkonfiguration (database.yml) durch die dort zum Download angegebene Datei zu ersetzen, habe ich nur in /etc/redmine/default/database.yml den Adapter von mysql in mysql2 geändert und einen Symlink zur database.yml in /usr/share/redmine/config/ platziert, das hat auch gut funktioniert: ln -s /etc/redmine/default/database.yml /usr/share/redmine/config/
  4. Aus mir bisher unbekannten Gründen wurden die plugin-assets (Javascript, Bilder, CSS) nicht in das vorgesehene Verzeichnis verschoben (dies sollte eigentlich automatisch geschehen und zwar von /usr/share/redmine/plugins/*PLUGIN-NAME*/assets jeweils nach /usr/share/redmine/public/plugin_assets/*PLUGIN-NAME*/
    Ich war zudem Zeitpunkt etwas genervt und habe einfach alles manuell kopiert, da gibts sicher eine bessere Lösung für. So ging ich vor:
    mkdir /usr/share/redmine/public/plugin_assets/plugin_contacts
    cp -R /usr/share/redmine/public/plugins/plugin_contacts/assets/* /usr/share/redmine/public/plugin_assets/plugin_contacts/
  5. Die Datenbankeinrichtung des CRM-Plugins hat bei mir nicht richtig funktioniert, weshalb ich die Tabelle contacts manuell anlegen musste (andere Tabellen wurden seltsamerweise korrekt angelegt). Das Datenbankschema wird durch die Dateien in /usr/share/redmine/plugins/redmine_contacts/db/migrate/ definiert. Diese habe ich mir durchgelesen und manuell die Tabelle contacts angelegt (siehe 016_create_contacts.rb, 028_add_cached_tag_list_to_contacts.rb, 029_add_visibility_to_contacts.rb). Ich habe als Datentyp für alle t.string-Einträge VARCHAR, für t.boolean TINYINT(1) UNSIGNED verwendet. Keine Ahnung ob das so intendiert ist, aber es funktioniert. Wenn nicht explizit :null => false dabei steht, ist NULL erlaubt. Mir ist nach wie vor unklar, wie es zu dem Fehler kommen konnte und kann daher nur empfehlen nach der Installation zu prüfen, ob alle benötigten Tabellen mit allen benötigten Feldern angelegt wurden.
  6. Nicht vergessen nach der Installation den Webserver neu zu starten, z. B. bei Apache via service apache2 restart

Vielleicht hilfts ja jemandem weiter.

HTML-Tags inkl. Inhalt mit DOMDocument aus String entfernen (PHP)

Häufig muss man aus einem Stück HTML bestimmte andere HTML-Tags entfernen. In einem aktuellen Fall nutze ich im Backend eines CMS einen WYSIWYG-Editor, der für Abbildungen <figure><img …><figcaption>Bildunterschrift</figcaption></figure> einsetzt. Auf einer Übersichtsseite benötige ich den von diesem Editor generierten Text allerdings ohne Abbildungen.

Ich halte es für einen Fehler, in diesem Fall Regular Expressions (REGEXPs) zu verwenden. Ja, sie sind schnell, aber damit sie auch verschachtelte HTML-Tags greifen muss man unglaublich komplizierten Code einsetzen, der recht schnell zur Wartungshölle wird.

Stattdessen finde ich für die Manipulation von HTML DOMDocument sinnvoll. Es gibt allerdings einige Fallstricke:

  1. Das von DOMDocument eingesetzte libxml kann mit HTML5 nicht richtig umgehen und spuckt Warnungen aus
  2. UTF8-Zeichen werden zerstört, scheint ein Fehler von DOMDocument/PHP/libxml zu sein, müsste ich mal näher recherchieren
  3. PHP ergänzt automatisch eine DOCTYPE-Deklaration, sowie <html> und <body>-Tags, die man nicht benötigt, wenn man nur mit einem HTML-Fragment arbeitet

Die gute Nachricht: All diese Probleme lassen sich lösen! :-)

So habe ich es gemacht:

/**
 * Entfernt die HTML-Node des HTML-Tags $tag aus dem HTML-Fragment $text
 * 
 * @param string $text
 * @param string $tag
 * @return string
 */
function removeTag($text, $tag = 'figure') {
   // Wichtig, denn bei leerem Input wirft loadHTML eine Fehlermeldung
   if(empty($text)) return $text;

   $dom = new DOMDocument;

   // Olle libxml-Warnungen unterdrücken
   libxml_use_internal_errors(true);
        
   /* UTF-8-Problematik lösen - wenn das aus irgendwelchen Gründen
      NICHT klappen sollte, könnte auch ein voranstellen von
      <!--?xml version="1.0" encoding="UTF-8"?--> vor $text helfen - habe ich
      allerdings nie ausprobiert */
   $dom->loadHTML(mb_convert_encoding($text, 'HTML-ENTITIES', 'UTF-8'));
        
   /* loadHTML fügt eine DOCTYPE-Deklaration, sowie und ein, die DOCTYPE-Deklaration kann sehr leicht entfernt werden, denn sie ist immer das erste Child */
   $dom->removeChild($dom->firstChild);

   /* Ich baue zuerst einen Array mit allen zu entfernenden Elementen
      Sie direkt im foreach zu entfernen scheiterte, ich nehme an, es
      hat irgendwas mit internen Indizes zu tun */
   $domElemsToRemove = array();
   $figureElements = $dom->getElementsByTagName($tag);
   foreach($figureElements as $figureElement) {
      $domElemsToRemove[] = $figureElement;
   }
   
   /* Hier werden die Elemente entfernt. Nicht das parentNode vergessen,
      ist mir zuerst passiert, kann ja nicht klappen ... */
   foreach($domElemsToRemove as $domElem) {
      $domElem->parentNode->removeChild($domElem);
   }
        
   $text = $dom->saveHTML();
                
   libxml_clear_errors();

   /* $text ist nach wie vor von... umschlossen
    * Hierfür gibt es vielfältige Lösungen, darunter str_replace, REGEXPs,
    * strip_tags(), oder replaceChild() - ich selbst brauchte 
    * GAR kein HTML-Tags in meiner Ausgabe und konnte daher strip_tags 
    * verwenden */
   return trim(strip_tags($text));
}

Dateiuploadfenster in tinyMCE4 aufrufen, ohne HTML der Seite zu ändern

Heute wollte ich in tinyMCE4 einen Dateiupload einbauen, der direkt nach Klick auf das Ordnersymbol (im Bild neben dem Eingabefeld von Quelle, rechts) das jeweilige Dateiuploadfenster des Browsers/Systems aufruft und Dateien hochläd.

Bild einfügen Dialog von tinyMCE4

D. h. ohne Zwischenschritte (wie weitere Popups oder externe Bibliotheken) soll nach dem Klick auf das Ordner-mit-Lupe-Symbol direkt dass hier angezeigt werden:

Dateiuploaddialog von Firefox auf Ubuntu Linux

Als zusätzliche Hürde kam hinzu, dass ich den Quellcode der Seite ungern verändern wollte (sonst hätte ich u. A. Code für die Validierung der dort vorhandenen Formulare umschreiben müssen). Dieser Fall ist sicherlich nicht besonders häufig, aber vielleicht geht es ja auch anderen so. Glücklicherweise gibt es eine sehr einfache Lösung via AJAX. So habe ich das ganze umgesetzt:

  1. Bevor es losgeht wird das jquery-iframe-transport-plugin benötigt, da XHR1 normalerweise keine Dateiuploads unterstützt, muss dieser Umweg gegangen werden. Einfach runterladen und einbinden
  2. Im Initialisierungscode von tinyMCE ($(„textarea“).tinymce({…}); kommt ein file_browser_callback hinzu, damit der Button überhaupt angezeigt wird, Code siehe unten
  3. Dann hole ich mir ein Formular mit dem <input type=“file“>-Button via $.get (HTML-Quellcode des Formulars weiter unten) und füge es an die aktuelle Seite an; falls es bereits existiert, spare ich den Ajax-Call ein und es geht weiter zu 3. (Übrigens: AJAX ist für die 3 Zeilen HTML-Code natürlich Overkill, man kann sie auch einfach per JavaScript direkt einfügen – allerdings werde ich in Zukunft vermutlich doch noch ein bisschen mehr serverseitige Logik hinter dem Formular brauchen, daher hab ich das so gelöst)
  4. Dann triggere ich einen Klick auf den versteckten Upload-Button, was den Dateiuploaddialog aufruft
  5. Sobald eine Datei ausgewählt wurde, wird das Formular an ein serverseitiges Skript geschickt, es gibt ggf. noch ein bisschen Errorhandling – ich lasse Fehlercodes und sonstige Daten per JSON zurückliefern, kann man natürlich auch anders machen
  6. Per win.document.getElementById(field_name).value = $(„#fupload“).val(); wird der Dateiname noch in das tinyMCE-Formularfeld gesetzt – ggf. ist es notwendig stattdessen einen Dateinamen zu verwenden, der vom serverseitigen Skript zurückgeliefert wird, je nach Situation
  7. Am Schluss wird das temporäre Formular wieder aus dem Dokument gelöscht und hinterlässt keine Spuren. (Falls jemand den Dateiuploaddialog abbricht, findet dieser Schritt nicht statt)

Der tinyMCE-Initialisierungscode

file_browser_callback: function(field_name, url, type, win) {
   if($("#fuploadform").length) {
      $("#fupload").click();
   } else { 
      $.get('uploadform.html', function(data) {
         $("body").append(data);
         $("#fupload").click();
         $("#fupload").on("change", function() {
            $.ajax({
               type: "POST",
               url: 'fileupload.php',
               files: $("#fupload", this),
               dataType: 'json',
               iframe: true,
               success: function(data) {
                  if(data.error=='none') win.document.getElementById(field_name).value = data.filename;
                  else alert('Ein Fehler ist beim Upload aufgetreten: '+data.error);
               }
            });
            $("#fuploadform").remove();
         });
      });
   }
}

Das HTML-Formular in uploadform.html

Nicht vergessen, den enctype zu setzen, sonst wird das mit dem Upload nix. 😉 Und natürlich per display: none; dafür sorgen, dass das temporäre Formular nicht angezeigt wird. Ggf. kann es auch noch sinnvoll sein mit dem accept-Attribut die möglichen MIME-Types zu setzen, habe ich hier nicht gemacht.

<form id="fuploadform" enctype="multipart/form-data" method="post">
   <input id="fupload" style="display: none;" name="fupload" type="file" />
</form>

Zu guter letzt noch die fileupload.php

(Ich verwende ein anderes serverseitiges Skript, als hier dargestellt, aber es wäre für dieses Beispiel zu kompliziert, daher zitiere ich einfach mal aus teilen der PHP-Dokumentation) – natürlich könnte man noch weitere Fehler hier abfangen (Datei zu groß, falscher Dateityp, usw. usf.).

<?php 
/* Verzeichnis wo die Datei hinsoll hier einstellen */
$uploaddir = '/var/www/uploads/';

$uploadfile = $uploaddir . basename($_FILES['fupload']['name']);

if (move_uploaded_file($_FILES['fupload']['tmp_name'], $uploadfile)) {
    $error = 'none';
} else {
    $error = 'Möglicherweise eine Dateiupload-Attacke';
}

print(json_encode(array('error', $error)));

Ich hoffe, dieses Beispiel hilft dem ein oder anderen weiter, wie immer freue ich mich über Kommentare.

Agavi project not found nach Umbenennung des pub-Verzeichnisses

Agavi erwartet eine bestimmte Verzeichnisstruktur, damit all diese magischen Wizards um Module und Aktionen usw. anzulegen funktionieren. Wenn man beispielsweise das pub-Verzeichnis umbenennt, scheitert Agavis project-locate. Wusste ich bis heute nicht. Zu meiner Verteidigung, ich war nicht derjenige, der pub umbenannt hat. Zum Glück reicht ein einfacher Symlink (ln -s dasVerzeichnisInDasJemandPubUmbenanntHat pub) um das Problem zu lösen. Vielleicht hilft das ja jemandem weiter.

In WordPress mittels wpdb Objekte in die Datenbank schreiben

Die wpdb-Klasse in WordPress kam mir schon immer ein bisschen seltsam vor, da ich an PDO gewöhnt war und lange danach gesucht habe, wie ich denn nun meine Objekte in die Datenbank schreibe. Die einzige Lösung, die ich bisher gefunden habe, ist per Reflection, vielleicht hat ja jemand eine bessere Idee? Ich freue mich über Kommentare! Hier der Code:

 

    /**
     *  
     *
     * @param string $table Die Tabelle, in die per INSERT geschrieben werden soll
     * @param object $object Das Objekt
     * @return int|false
     */
   function insertObject($table, $object) {
        if(!is_object($object)) return false;

        $reflection = new ReflectionObject($object);
        // getProperties() als Parameter ReflectionProperty::IS_PUBLIC
        // übergeben, falls nur public-member des Objekts
        // in die Datenbank geschrieben werden sollen
        $properties = $reflection->getProperties();
        $dataArray = array();
        foreach($properties as $property) {
            // Die folgende Zeile löschen, falls nur public-member des Objekts in die Datenbank geschrieben werden sollen
            // Für beides gibt es meiner Meinung nach sinnvolle Use Cases, ggf. einen entsprechenden Parameter hinzufügen
            $property->setAccessible(true);
            $dataArray[$property->getName()] = $property->getValue($object);
        }

        global $wpdb;
        return $wpdb->insert($table, $dataArray);
    }

Üblicherweise verzichte ich in meinem Code auf den $table-Parameter um es noch zu vereinfachen und verwende entweder den Klassennamen als Tabellennamen oder habe eine statische Konstante const TABLE = ‚tabellenname‘; in der jeweiligen Klasse.

Agavi Form Population Filter (FPF) und HTML5

Ich arbeite gelegentlich mit dem PHP-Framework Agavi. Um hier Formularfelder vorauszufüllen und insbesondere auch um Fehlermeldungen nach dem Ausfüllen von Formularen anzeigen zu können, setzt Agavi den Form Population Filter (FPF) ein. Die Idee ist, dass das Framework den DOM-Baum analysiert und die Meldungen direkt an die entsprechende Stelle einfügt, die der Entwickler somit nur einmal global spezifizieren muss (z. B. immer vor dem jeweiligen Feld). Soweit die Theorie.

In der Praxis ist der Form Population Filter einer der größten Teufeleien, die ich jemals erlebt habe, er verursacht alle möglichen ärgerlichen Seiteneffekte, da er ja das Markup der ausgegebenen Seite massiv verändert und funktioniert häufiger nicht, als er es tut.  So führt zum Beispiel eine nicht-wohlgeformte Seite (falsche Tags, illegale Entities etc.) dazu, dass der FPF entweder seinen Dienst komplett einstellt, der zugrunde liegenden libxml sei Dank, oder im Besten Fall die Seite mit falsch codierten Entities wieder ausgibt. Insbesondere bei HTML5-Seiten ist dies ein Problem, da libxml kein HTML5 unterstützt (oder zumindest der FPF nicht). Laut Dokumentation sollte es ausreichen, dem FPF mit dem Parameter ignore_parse_errors klar zu machen, dass er eben dies tun solle, dies funktioniert jedoch nicht.

Wer das gleiche Problem hat, so konnte ich mir helfen:

<ae:parameter name="ignore_parse_errors">true</ae:parameter>
 <ae:parameter name="force_output_mode">xhtml</ae:parameter>
 <ae:parameter name="parse_xhtml_as_xml">false</ae:parameter>

Durch force_output_mode wird der FPF gezwungen, anstatt HTML XHTML auszugeben. Das ist natürlich großer Unsinn und führt zu allen möglichen Fehlern, die man jedoch wieder abfangen kann, indem man parse_xhtml_as_xml abstellt, sodass intern wieder loadHtml anstatt loadXml verwendet wird. Alle verbleibenden Fehler werden mit ignore_parse_errors ignoriert (was kurioserweise beim output_mode xhtml funktioniert, nicht jedoch bei html). Bei ignore_parse_errors sind neben true und false auch noch LIBXML_ERR_… möglich, siehe Release Notes von Agavi 1.0.5.

Man beachte, dass diese Methode ebenfalls gewisse Seiteneffekte nach sich zieht (aus <script src=“foo“></script> wird z. B. <script src=“foo“/>, was aus XHTML-Sicht völlig korrekt ist, aber, zumindest bei meinem Browser in Verbindung mit HTML5  nicht mehr funktioniert).

Redirected Port in Windows 8 erstellen (RedMon)

Beim Erstellen eines „Redirected Ports“ (RedMon) bekam ich immer wieder die Fehlermeldung „Der angegebene Anschluss konnte nicht hinzugefügt werden. Unzulässige Funktion.“

Mich betraf das, weil ich dieser Anleitung folgte: http://www.stat.tamu.edu/~henrik/GSWriter/GSWriter.html

Zum Glück gibt es eine einfache Lösung: Auf einen bestehenden Drucker (egal welchen, es kann ein lokaler, ein Netzwerkdrucker, der Microsoft XPS Document Writer, usw. sein) mit rechts klicken und „Druckereigenschaften“ (nicht Eigenschaften!) wählen. Ggf. muss nun noch ein Klick auf den Button links unten mit dem Schutzschild erfolgen, damit Administratorberechtigungen vorliegen. Jetzt unter der Registerkarte „Anschlüsse“ auf „Hinzufügen…“ klicken, „Redirected Port“ wählen. Erstaunlicherweise funktionierte das bei mir problemlos. Wichtig: Nachdem der Anschluss erstellt wurde, den Haken wieder beim richtigen Port des Druckers, der für die Erstellung des neuen Anschlusses kurzfristig verwendet wurde, setzen.

Ich hoffe der kleine Tipp hilft jemandem weiter.

Liste aller Blogs in einem WordPress-Netzwerk (Multisite)

Ich verwende ungern Funktionen, die als deprecated (veraltet) markiert sind, wie in WordPress schon seit langer Zeit die Funktion get_blog_list(). Eine Alternative zu schreiben ist allerdings recht problemlos möglich, hier der Code inkl. Kommentaren (ich habe bewusst darauf verzichtet, die Liste direkt aus der Datenbank zu holen, da ich im Zweifelsfall lieber die API verwende – mir ist bewusst, dass die Performance ggf. – gerade bei größeren Netzwerken – bei meiner Methode nicht besonders gut ist):

 /**
* Liefert einen Array mit allen Blogs in einem Netzwerk zurück
*
* @returns array
*/
function listBlogs() {
   $blogList = array();        

   // Array aller Super-Admins - das sind die Netzwerk-Administratoren, die die Blogs anlegen
   $superAdmins = get_super_admins();

   foreach($superAdmins as $admin) {
      // Leider liefert get_super_admins() einen Array von login-Namen (und nicht IDs oder WP_User-Objekten), auch die keys des Arrays sind keine IDs
      // Daher wird für jeden Eintrag zunächst das zugehörige User-Objekt und daraus die ID gesucht
      $admin = get_user_by('login', $admin);

      // get_blogs_of_user() ist im Gegensatz zu get_blog_list() nicht als deprecated markiert und kann also verwendet werden
      $blogList = array_merge($blogList, get_blogs_of_user($admin->ID));
   }

   return $blogList;
}

 

Wichtig: Das größte Problem mit dieser Funktion ist, dass die Ergebnisliste nun nach den anlegenden Admins (und nicht etwa alphabetisch etc.) sortiert ist. In meinem Anwendungsfall war das egal, wer eine andere Sortierung benötigt, muss die Funktion entsprechend umschreiben