Zeichensatzvorgaben für MySQL und PHP – SET NAMES – und leere Strings durch htmlentities()

Gestern ist mir ein dummer Fehler passiert, dessen Analyse mich Zeit gekostet hat. Dabei erwies sich die Sache am Ende als trivial. Es ging letztlich um konsistente Zeichensatzeinstellungen für PHP und MySQL - allerdings mit einem mir bislang unbekannten Nebeneffekt.

Zeichensätze im Kontext von PHP und MySQL sind ein Thema vieler Foren- und Q&A-Artikel im Internet - und nicht immer trifft man auf zufriedenstellende Antworten. Ich hoffe, dieser Artikel trägt anhand eines Beispiels zu etwas mehr Klarheit bei.

Voraussetzungen und aufgetretener Fehler

Unsere hausinternen Apache- und Datenbank-Server laufen normalerweise vollständig unter UTF-8. Inklusive der eigenen Datenbank- und Tabellen-Kollationen unter MySQL- oder MariaDB-Systemen. Aber für Tests müssen wir immer mal wieder gezielt eine Kompatibilität zu den festgelegten Kollationen für MySQL-Banken/Tabellen auf Kundenservern herstellen. Meist kommt dann Latin1 (iso-8859-1) ins Spiel.

Gestern mussten wir für einen solchen Test Datensätze eines von uns entwickelten, php-basierten CMS von einem gehosteten MySQL-Kundenserver in unsere lokale Test-Datenbank übernehmen. Diese Datensätze beinhalteten viele Text-Strings mit deutschen Umlauten. Da es sich nur um wenige Records handelte, haben wir die Daten in diesem Fall mit Copy/Paste und mit Hilfe von Eingabefeldern unserer CMS-Verwaltungsoberfläche übernommen. Das CMS lief dabei unter einer lokalen UTF8-Standarddomäne. Unsere aktuellen CMS-Programme zeigten die fraglichen Textstrings denn auch korrekt in der Web-Oberfläche des CMS an.

Es gab aber andere zu untersuchende Punkte im Layout. Um die Unstimmigkeiten zu testen, haben wir zusätzlich eine separate Testdomäne angelegt und diverse PHP-Klassen vom Kundenserver (auf dem dortigen Versionsstand) in bestimmte Verzeichnisse des zugehörigen Web-Spaces auf unserem lokalen Server geladen. Ergebnis: 2 Domainen mit z.T. unterschiedlichen Versionsständen von PHP-Klassen.

Und dann passierte es:

Bei einem Aufruf der Webseiten in der Testdomäne verschwanden plötzlich fast alle Text-Strings aus der Web-Oberfläche.

Bilder und grundsätzlicher Aufbau der Webseiten bleiben jedoch erhalten. Ich war zunächst völlig verblüfft über diesen Effekt. Zumal der Einsatz unserer lokalen Versionen der gleichen PHP-Klassen kein Verschwinden der Textstrings zeitigte.

Die Analyse war nicht ohne, da ich zunächst nicht wusste, wonach ich zu suchen hatte. Man denkt da zunächst natürlich an Unterschiede im Programmcode selbst. Am Schluss entpuppten sich aber eine im wesentlichen unmodifizierte Klasse, die die Datenbankverbindung steuert, sowie eine Klasse, die die Strings aus Sicherheitsgründen nach unerlaubten Sequenzen filtert, als Kerne des Übels. Ausschlaggebend waren allerdings nicht Programmunterschiede sondern bestimmte Parameter-Setzungen sowie Server- und Zeichensatzeinstellungen. Aber der Reihe nach.

Zeichensatzeinstellungen in der Kommunikationskette zwischen einem PHP-Server und einer MySQL-Datenbank

In der Datenaustausch-Kette zwischen einer MySQL-Datenbank und PHP-Modulen auf einem Web-Server (und natürlich auch bei der Übersendung eines HTML-/XML- oder JSON-Outputs an Web-Clients) spielen verschiedene Zeichensatzeinstellungen eine Rolle. Einige davon können vom Entwickler beeinflusst werden. Andere wiederum nicht immer.

Für unseren Fall waren vor allem die nachfolgenden Punkte relevant; sie betreffen den Web-Server (mit PHP-Modul) und die MySQL-Datenbank:

  1. Zeichensätze ("Kollationen") der MySQL-Datenbank und zugehöriger Datenbanktabellen.
  2. Zeichensatz-Vorgaben zur MySQL-Verbindung und zum Datentransfer von und zu (PHP-)Programmen, die mittels der SQL-Direktive "SET NAMES" vorgenommen wurden.
  3. Einstellungen für den Default-Character-Set des PHP-Apache-Moduls.
  4. Zeichensatzeinstellungen für die PHP-Funktion "htmlentities()".

Zeichensätze in der Datenbank und die Direktive "SET NAMES"

Für die Kollation der relevanten MySQL-Datenbank und ihrer Tabellen war auf dem Kundenserver "latin1_german2_ci" gewählt worden. Unsere Einstellungen im lokalen Testsystem waren dazu auf Tabellenebene kompatibel. Die Kollation einer Datenbanktabelle bestimmt letztlich aber nur die interne Ablage der Daten in der Tabelle und nicht den Zeichensatz, unter dem z.B. per SQL ermittelte Resultsets an weiterverarbeitende Programme übermittelt werden.

Für letzteres sind andere Parameter verantwortlich, die man als Entwickler für eine spezifische Verbindung zur MySQL-Datenbank einstellen kann. Unter MySQL und der MariaDB nutzt man dafür etwa die SQL-Direktive "SET NAMES" (oder aber die Funktion mysqli_set_charset(); s.u.).

"SET NAMES" führt zur gleichzeitigen Festlegung dreier RDBMS-Parameter für die Behandlung einer spezifischen Datenbankverbindung. Diese Parameter sind: character_set_client, character_set_connection, character_set_results.

Siehe hierzu etwa
https://dev.mysql.com/doc/refman/5.7/en/set-names.html
und
https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_character_set_results.

Die Datenbankverbindung wird unter dem PHP/mysqli-Interface dabei über das Connection-Objekt

$dbi = mysqli_connect(....)

identifiziert. Im Wesentlichen legen die genannten Parameter Folgendes fest:

  • character_set_client: Zeichensatz, unter dem Daten, die vom Datenbank-Client zum RDBMS-Server transferiert werden, interpretiert werden.
  • character_set_connection: Handhabung bestimmterErsetzungen und Konversionen, u.a. von Numbers zu Strings.
  • character_set_results: Zeichensatz, unter dem Daten, die vom RDBMS zu Client-Programmen übermittelt werden interpretiert werden.

Der "Client" ist in unserem Fall natürlich ein PHP-Programm auf einem Apache-Server. Die Anwendung des "SET NAMES"-Statements durch ein PHP-Programm, z.B.

$sql_unames = "SET NAMES 'utf8'";
$this->dbi->query($sql_unames);
//dbi->Datenbank-Connection Object
//$this->dbi = mysqli_connect(....)

ist somit von zentraler Bedeutung für die Kommunikation zwischen einem PHP-Programm und einer MySQL/MariaDB! Solange eine hinreichende Konvertierung verwendeter Zeichen gewährleistet ist, muss der Zeichensatz, unter dem Resultsets zum PHP-Programm übermittelt werden, nicht zwingend mit dem der Datenbanktabellen selbst identisch sein.

Es sei darauf hingewiesen, dass es zu "SET NAMES" auch Alternativen gibt, die man im Rahmen des mysqli-Interfaces einsetzen kann und sollte (s.u.). Dass wir in einigen Programmen noch SET NAMES verwenden, hat lediglich historische Gründe. Die hier beschriebene Problematik gilt aber unabhängig vom genauen Werkzeug zur Einstellung der Zeichensatzparameter.

Konsistenz zu Zeichensatzvorgaben für PHP - Einstellungen in der php.ini

Das PHP-Programm muss in jedem Fall mit dem gewählten Zeichensatz für Resultsets adäquat umgehen können; s.u.. Der rechte Bereich der folgenden Skizze, die ich aus einem anderen Artikel dieses Blogs entliehen habe, verdeutlicht das für den Fall "SET NAMES 'latin1'" :

Nun könnte man in seinen PHP-Programmen natürlich spezifische Umwandlungsfunktionen (u.a. iconv(), mb_convert_encoding(), utf8-encode(), utf8-decode) für die Zeichensatzkonvertierung aus- und eingehender Strings bemühen. Die linke Seite der Skizze liefert hierfür Beispiele (s. den dazu gehörigen Abschnitt weiter unten).

Wenn möglich, kann man aber auch einen Standardzeichensatz für die PHP-Verarbeitung von Strings vorgeben und sich darauf bzgl. der Datenbankinteraktion verlassen.

Entsprechende Einstellungen nimmt man in der Konfigurationsdatei
/etc/php7/apache2/php.ini
vor. Die relevanten Parameter sind dort:

; PHP's default character set is set to UTF-8.
; http://php.net/default-charset
default_charset = "UTF-8"

; PHP internal character encoding is set to empty.
; If empty, default_charset is used.
; http://php.net/internal-encoding
;internal_encoding =

; PHP input character encoding is set to empty.
; If empty, default_charset is used.
; http://php.net/input-encoding
;input_encoding =

; PHP output character encoding is set to empty.
; If empty, default_charset is used.
; mbstring or iconv output handler is used.
; See also output_buffer.
; http://php.net/output-encoding
;output_encoding =

Die hierfür gesetzten Werten sollte natürlich mit den Einstellungen für die Datenbankverbindung zusammenpassen.

Kundenvorgaben

Wir parametrieren in Kundenprojekten die PHP-Methoden für den Verbindungsaufbau zum Datenbankserver meist gemäß expliziter Kundenvorgaben. In unserem Fall hatten wir "SET NAMES 'latin1'" gewählt. (Hinweis: Die Zeichensatz-Namen auf einem MySQL-Server enthalten grundsätzlich keine Trennzeichen; daher ist statt iso8859-1 "latin1" zu verwenden).

Der Grund dafür war, dass das Apache-PHP-Modul des (gehosteten) Kunden-Web-Servers auf "iso-8859-1" eingestellt war. Das bestätigte eine Überprüfung mit phpinfo(). Diese Einstellungen sollten wir gem. Kundenvorgabe nicht ändern, da auf dem Web-Server auch andere PHP-Programme als unsere eigenen laufen müssen.

Auf dem Kundenserver waren die Zeichensatzeinstellungen also konsistent.

Ursache unseres Problems und die Rolle von htmlentities()

Man ahnt es bereits: Durch das Kopieren der PHP-Klasse für die Steuerung von Datenbankverbindungen vom Kundenserver auf die Testdomäne unseres lokalen Web-Servers entstand dort u.a. eine Inkonsistenz zwischen der Zeichensatzbehandlung unter PHP (utf8!) und dem Zeichensatz für den Transfer von Resultsets aus der MySQL-Datenbank (latin1).

Diese Inkonsistenz kam jedoch noch nicht zum Tragen, als wir die Daten per Copy/Paste über lokale Web-Interfaces einer UTF8-Standard-Domäne in die Bank einbrachten.

Für unsere Testdomäne dagegen muss man jedoch u.a. eine fehlerhafte Konvertierung von deutschen Umlauten erwarten! Warum aber verschwanden die Text-Strings in Gänze aus den Web-Oberflächen?

Die Antwort lieferte schließlich eine von mir bislang zu wenig beachtete Eigenschaft der PHP-Funktion "htmlentities()".

Unsere Web-Generatoren jagen Strings vor einer Web-Darstellung durch eine Reihe von Prüfroutinen, Transformatoren für erlaubte Zeichenfolgen und durch Filter (u.a. HTMLPurifier, aber auch eigene Filter). Dabei wird in einem Zwischenschritt (nach einer vorhergehenden Konvertierung erlaubter HTML-Zeichenfolgen) auch "htmlentities()" eingesetzt.

htmlentities() erlaubt selbst eine Vorgabe des "Character Sets" über einen Parameter. Ein Check zeigte: Dieser Parameter stand in der lokalen Testdomäne explizit auf "UTF-8". Diese Einstellung betrifft jedoch eine Konvertierung in den gewünschten Ziel-Zeichensatz für den HTML-Output. Hier hatten wir kein Problem, da die HTML-Header der Webseiten bzw. die vom Apache-Server generierten HTTP-Header tatsächlich auf UTF-8 ausgerichtet waren.

Allerdings sorgten schon die vorhergehenden Widersprüche zwischen dem Zeichensatz der Datenbank-Resultsets und dem Zeichensatz für die anschließende Behandlung von Strings durch PHP. Das hatte gravierende Folgen. Unter http://php.net/manual/de/function.htmlentities.php findet man nämlich folgenden Hinweis:

Rückgabewerte
Gibt die kodierte Zeichenkette zurück. Enthält der string eine in dem übergebenen encoding ungültige Code Unit Sequenz, wird eine leere Zeichenkette zurückgegeben, sofern weder das ENT_IGNORE noch das ENT_SUBSITUTE Flag gesetzt sind.

(Hervorhebung durch mich).

Das war des Rätsels Lösung:

Eine Inkonsistenz in der Zeichensatzbehandlung im Datenaustausch zwischen unserer MySQL-Datenbank und PHP führte zu nicht behandelbaren Zeichen bei der Anwendung von htmlentities() auf Strings - und diese Funktion produzierte dann gemäß ihrer Default-Einstellungen leere Strings.

Trivial - man muss es halt nur wissen! Ein Test mit

"SET NAMES 'utf8'"

ließ denn alle verschwundenen Strings auch in unserer Testdomäne prompt wieder erscheinen!

Notwendige Checks vor der Anwendung von SET NAMES (oder von mysqli_set_charset())

Hat man die Kette der Zeichensatzsetzungen bzw. Zeichensatzbehandlung im Austausch zwischen PHP und einer MySQL-Datenbank erst einmal verstanden, so ist auch klar, wo man mit vorbeugenden Maßnahmen ansetzen kann. Solche Vorkehrungen sind - wie das Beispiel zeigt - vor allem dann notwendig, wenn man die Zeichensatz-Einstellungen für PHP nicht auf allen involvierten Web-Servern beeinflussen kann oder darf.

Bevor man "SET NAMES" (oder mysqli_set_charset(); s.u.) in einem PHP-Programm tatsächlich anwendet, sollte man die Setzung des "Default Character Sets" in der php.ini für den aktuellen Server explizit abfragen - mittels

ini_get('default_charset');

- und dann mit der ebenfalls abfragbaren Kollation der Datenbanktabellen vergleichen. Das Ergebnis dieses Vergleichs kann man dann nach bestimmten Regeln behandeln:

Z.B. Warnhinweise bei (ernsthafter) Inkompatibilität ausgeben. Oder wenn man wirklich sicher ist, dass nur deutsche Umlaute zu potentiellen Problemen führen können: Wahl des zu den PHP-Einstellungen konsistenten Zeichensatzes für den Transfer von Resultsets aus der Datenbank. In unserem Fall also "utf8".

Alternative: Ändern der php.ini-Vorgaben für den Zeichensatz durch und für das laufende PHP-Programm

Auf festgestellte Inkonsistenzen in den Zeichensatzeinstellungen kann man u.U. - nämlich wenn man die dafür nötigen Rechte besitzt - auch mit einer Modifikation der php.ini-Vorgaben für das laufende Programm reagieren. Dazu muss man natürlich die Funktion ini_set() bemühen; Bsp.:

ini_set( string 'default_charset', 'ISO-8859-1')

bemühen.

Empfohlener Weg zum Setzen des "Character Sets" für eine MySQL-Verbindung

Ich möchte explizit darauf hinweisen, dass es andere Möglichkeiten als die SQL-Direktive "SET NAMES" gibt, Character Sets für die Datenbankverbindung zu setzen. Das PHP-Manual empfiehlt explizit, die Funktion

mysqli_set_charset(mysqli $link , string $charset)

anstelle von SQL und "Set Names" zu verwenden. Siehe: http://php.net/manual/de/mysqli.set-charset.php

Zeichensätze und die Web-Client-Seite

Obwohl nicht Kernthema dieses Artikels werfen wir noch einen ergänzenden Blick auf den Datenaustausch des PHP/Web-Servers mit Web-Clients (z.B. einem Browser). Die Zeichensatzthematik setzt sich natürlich auch auf dieser Kommunikationsstrecke fort; der linke Teil der obigen Skizze verdeutlicht das am Beispiel von Ajax/Ajaj-Programmen. Diese erfordern i.d.R. UTF-8 für einen ordnungsgemäßen Datenaustausch mit dem Web-Server.

Ist der Parameter "default_charset" in der php.ini-Datei aber auf "iso-8859-1" gesetzt, so muss man ein- und ausgehende Daten entsprechend konvertieren. Dafür eignen sich die Funktionen utf8_decode() für einlaufende POST-/GET-Daten aus Ajax-Programmen und utf8_encode() bei der Erzeugung des Ajax/Ajaj-Outputs in Richtung Web-Client.

Für reinen HTML-Output gilt analoges; dabei sind aber auch HTTP- und HTML-Header-Anweisungen für Character Sets zu setzen. Siehe hierzu:
http://www.html-info.eu/php/php-als-script-sprache/item/zeichensatz-latin1-oder-unicode-utf-8.html

Fazit

In unbeabsichtigter Weise kann htmlentities() plötzlich zu einer Testfunktion für die Konsistenz zwischen

  • der Zeichensatz-Einstellungen durch "SET NAMES" bzw. mysqli_set_charset() für die MySQL/MariaDB-Datenbankanbindung an ein PHP-Programm
  • und den Zeichensatzeinstellungen für das PHP-Modul selbst auf dem Webserver

werden - und in letzter Konsequenz zu leeren Strings auf Webseiten führen.

Es lohnt sich vor einem Einsatz von "SET NAMES" - oder besser mysqli_set_charset() - eigentlich immer, die Einstellungen in der "php.ini" bzgl. des "default_charset" und verwandter Parameter abzufragen und daraus angemessene Konsequenzen für den Aufbau der Datenbankverbindung zu ziehen.

Dies gilt vor allem dann, wenn man im Rahmen von Tests und Produktivierungen auf verschiedenen Servern arbeiten will oder muss - und dabei nicht alle Konfigurationsparameter der Server selbst beeinflussen kann und darf.

Neben dem Setzen von Server- und Verbindungsparametern kann man auf festgestellte oder vorgegebene Zeichensatzanforderungen oder Zeichensatzdiskrepanzen aber auch gezielt mit verschiedenen Funktionen reagieren, die PHP für eine Zeichensatz-Detektion und eine explizite Zeichensatzkonvertierung von Strings anbietet.

PHP und Apache Rewrite von Web-Requests – Ausschluss von Dateien des Typs CSS, JPG, etc. ?

Gestern bin ich in eine klassische Falle im Zusammenhang mit Apache Rewrites gestolpert.

Für ein CMS-Projekt hatte ich in einer ".htacces"-Datei eines Apache-Servers Rewrite-Direktiven für externe HTTP-Requests nach HTML-Dateien hinterlegt. Das CMS arbeitet intern ausschließlich mit PHP-Dateien und Parametern zur Erzeugung von Webseiten. Nach außen hin werden aber reguläre Adressen von HTML-Dateien angeboten. Angeforderte HTML-Seiten müssen daher auf dem Server auf bestimmte Generatorprogramme und zugehörige GET/POST-Parameter abgebildet werden.

Rewriting ist für solche Anforderungen eine Standardlösung (siehe etwa auch das Vorgehen von WordPress):

Der Request wird an eine zentrale PHP-Datei weitergereicht. Diese zerlegt den URL-String der angeforderten HTML-Datei; über Datenbank-Informationen werden dann Parameter für Webseitengeneratoren (PHP-Programme) ermittelt. Die zentrale Datei gibt danach die Kontrolle an die Generatoren ab. Die notwendige Datenbankinformation wird vom CMS bereits während der Anlage und Konfiguration der Webseiten durch den User erzeugt.

Im meinem Fall war ich bzgl. der Rewrite-Anweisung allerdings ein wenig bequem:

Alle (!) Abfragen zu nicht existierenden Dateien wurden zur Behandlung an eine zentrale PHP-Datei "pager.php5" meines CMS verwiesen.

Das funktionierte auch wunderbar - solange nur HTML-Dateien abgefragt wurden, zu denen die Website Links anbot und die im CMS auch mal angelegt worden waren. Traten bzgl. solcher Anfragen Fehler auf oder lies sich aus der Datenbank keine adäquate Info zur angeforderten HTML-Seite ermitteln, wich das PHP-Programm "pager.php5" kontrolliert auf Fehlerroutinen aus.

Nun sah ich bei der Überprüfung des Netzwerkverkehrs bei bestimmten Seiten allerdings, dass es gleich zig-fach zu einem wiederholten Abrufversuch für eine Datei "err_page.php5" in einem bestimmten Bild-Verzeichnis kam; diese PHP-Fehler-Datei existierte dort jedoch gar nicht und war dort auch nie vorgesehen.

Ursachenanalyse

Tatsächlich rufe ich solche PHP-Files zur Behandlung bestimmter Fehler auf, die im CMS im Zuge der Seitengenerierung entstehen können. Allerdings nicht in einem Bildverzeichnis ....

Nach einer Weile fand ich heraus, dass das Problem dennoch durch eine angeforderte, aber auf dem Test-Server nicht vorhandene Bilddatei ausgelöst wurde.

Das war keineswegs so einfach zu erkennen, wie man vielleicht meinen möchte - bei nicht vorhandener Datei übernimmt ja ordnungsgemäß "pager.php5" die Kontrolle - und somit erscheint im Browser nicht zwingend eine Warnung. Eine Warnung auf HTTP-Ebene würde im Einzelfall ja das gezielte Absetzen einer HTTP-Protokoll-Meldung im Verlauf der Situationsbehandlung erfordern. So schlau war ich bei der Konzeption aber nicht gewesen.

Ich dachte deshalb zunächst an einen Fehler in einer PHP-Routine zur automatischen Bildskalierung auf vom CMS-User vorgegebene Größen. Ein Fehler bzw. eine Fehlerbehandlung für nicht existierende Bilddateien in der festgestellten Form lag dort aber nicht vor.

Weitere Tests und ein genauerer Blick in den HTTP-Verkehr zeigten schließlich, dass der "Referrer" der fehlerhaften Datei-Anforderung eine CSS-Datei war! Selbige CSS-Datei existierte und wurde auch ordnungsgemäß gefunden.

Was war das eigentliche Problem?

In der CSS-Datei gab es eine Anweisung der Art

background-image:url(Pfad_zum_(fehlenden)_Bild);

für ein Hintergrundsbild - leider für eines, das auf dem Server nicht existierte.

Der entsprechende Abruf führte dann in Kombination mit der Rewrite-Anweisung zu einer Reaktion nach dem Muster

  • Abruf nicht existierende Datei aus CSS-Anweisung
  • => pager.php5
  • => Auslösen einer "Fehlerbehandlung" durch eine err_page.php5, die aus Gründen mangelnder Voraussicht im Bildverzeichnis erwartet wurde, dort aber nicht existierte
  • => Abruf einer nicht existierenden PHP-Datei
  • => pager.php5 =>. Erneuter Verweis auf Fehlerbehandlung durch eine nicht existierende "err_page.php5"
  • => Abruf einer nicht existierenden PHP-Datei
  • etc., etc.

Apache versucht es dann mehrfach und bricht schließlich ab.

Lösungsansatz 1: Klammere Dateien bestimmter Typen (jpg, png, css, js, ...) aus der Rewrite-Anweisung aus

Das Erlebnis brachte mich dazu, genauer darüber nachzudenken, wie ich eigentlich mit Rewrites normaler Dateien der Typen ".jpg, .gif, .png, .swf, .css, .js" etc. umgehen sollte, für die eine Ersetzung durch PHP-Programme gar nicht vorgesehen ist.

Eine Lösungsvariante ist das Ausklammern dieser Dateitypen von der Rewrite-Anweisung in der ".htaccess"-Datei. Das sieht im einfachsten Fall etwa so aus:

Options +FollowSymLinks
RewriteEngine On
RewriteBase /
RewriteRule ^php/hmenu/pager.php5(.*)$ - [L] 

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule \.(js|css|ico|gif|jpg|png|swf|ttf|eot)$ - [NC,L]

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ my_Rw_Php_Path/pager.php5?adr=$1 [PT]   

 
Hier werden zwei "Condition/Rewrite"-Sequenzen eingesetzt, da ohne besondere Tricks (Skip-Direktiven) zu einem Block aus Condition-Anweisungen nur genau eine Rewrite-Anweisung gehören sollte. "NC" sorgt für eine Nichtbeachtung von Groß-/Klein-Schreibung. "L" beendet die Rewrite-Analyse. "my_Rw_Php_Path" steht für einen Pfad zu einem Serververzeichnis, das die zentralen Programme zur Rewrite-Behandlung beherbergt.

Wird nun eine nicht vorhandene Datei der genannten Typen von einem Web-Client angefordert, wird diese Anforderung durchgereicht und vom Apache-Server mit HTTP-Fehlern der Art "404 Not Found" quittiert. Das reicht in Testphasen zur Prüfung der Lauffähigkeit einer CMS-basierten Website normalerweise aus.

Lösung 2: Behandle fehlende Dateien bestimmter, ausgewählter Typen als Sonderfälle in einer speziellen zentralen PHP-Datei

Eine kontrollierte Reaktion des Systems auf nicht vorhandene Dateien bestimmter Typen jenseits von HTML-Dateien lässt sich natürlich auch in einer weiteren zentralen PHP-Datei (etwa "missing.php5") vorsehen, auf die eine gesonderte Rewrite-Anweisung verweist. Beispielsweise könnte man den mittleren Teil der obigen ".htaccess" in diesem Sinne ersetzen durch:

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)\.(js|css|ico|gif|jpg|png|swf|ttf|eot)$ my_Rw_Php_Path/missing.php5?missadr=$1\.$2 [NC,L]

Bzgl. der Problembehandlung in der "missing.php5" muss man sich aber genau überlegen, für welche Dateien man tatsächlich eine offene und für den User auch erkennbare Fehlermeldung vorsehen will. Ein fehlendes Bild z.B. ist meist nicht überlebenskritisch.

Ich tendiere im Moment dazu, gezielt Meldungen in eine eigene Log-Datei auf dem Server zu schreiben, die man sowohl im Test- als auch Produktivbetrieb regelmäßig auswertet. Ein Minimal-PHP-Skript "missing.php5" könnte für diesen Zweck dann in etwa so aussehen:

<?php
$missadr = 'unknown'; 
if (isset($_GET['missadr']) ) {
	$missadr = $_GET['missadr']; 
}

$fh = fopen("missing.log", 'a+'); 
$out_str = "\r\n" . date('d.m.Y :: H.I.s') . " :: A requested file (" .$missadr . ") is missing"; 
fputs($fh, $out_str); 
fclose($fh); 

header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
exit;
?> 

 
Natürlich wäre das in dieser Einfachheit fahrlässig; der Inhalt von $_GET['missadr'] ist im produktiven Einsatz zu prüfen und ggf. zu bereinigen, um den Inhalt als Teil eines Angriffsvektors auszuschalten. In diesem Artikel geht es aber nur um einen ersten Ansatz.

Der Header-Output ist wichtig; durch ihn kann man z.B. auch in Browser-Tools (bei FF etwa in der Web-Konsole) erkennen, dass ein Fehler vorliegt und eine Datei tatsächlich nicht vorhanden ist.

Ein typischer Output in der Datei "missing.log" hat nach zwei Aufrufen bestimmter Webseite, für die indirekt eine Bilddatei "hg_dxm_7.jpg" angefordert wird, dann ggf. folgenden Inhalt:

27.06.2017 :: 12:0:38 :: A requested file (image/hg_dxm_7.jpg) is missing
27.06.2017 :: 12:0:39 :: A requested file (image/hg_dxm_7.jpg) is missing
27.06.2017 :: 12:0:02 :: A requested file (image/hg_dxm_7.jpg) is missing
27.06.2017 :: 12:0:02 :: A requested file (image/hg_dxm_7.jpg) is missing

Man erkennt hier an der Zeitangabe, dass die fehlende Datei pro Seitenaufruf gleich zweimal angefordert wird; in meinem Fall aus einer CSS-Datei heraus, aber auch direkt über ein HTML-Tag.

Fazit

Nicht nur in einem CMS will man ggf. Requests nach HTML-Dateien durch den gezielten Einsatz von PHP-Webgeneratoren beantworten. Die Nutzer (und auch Suchmaschinen) glauben, reguläre HTML-Dateien abzurufen. In Wirklichkeit sind die Dateien nicht vorhanden; Apache Rewrites sorgen vielmehr für die Erzeugung von HTML-Seiten durch PHP-Programme.

Zu einfach gehaltene Rewrite-Anweisungen für nicht vorhandene Dateien können dabei allerdings schnell zu schwer zu durchschauenden bis rekursiven Fehlern führen. Fordern HTTP-Requests evtl. nicht vorhandene Dateien eines bestimmten Typs an, für die eine gezielte Ersetzung gar nicht vorgesehen ist, so hängt es allein von der Voraussicht der Entwickler ab, was im Detail über Ersetzungen passiert. Es empfiehlt sich deshalb, solche Datei-Anforderungen

  • entweder von vornherein aus der Rewrite-Behandlung auszuschließen
  • oder sie aber einer gezielten Sonderbehandlung durch eine eigene PHP-Datei zuzuführen. Dabei sollten angemessene HTTP-Antwortcodes erzeugt werden.

Make PEAR IT-templates work on a PHP 7 server

Some of my older PHP programs use PEAR-IT-templates. At the core of this PHP template engine is the "IT.php" file defining a class "HTML_Template_IT" via an old fashioned "contructor" function of the same name (stemming from old PHP4's way to define classes).

Yesterday, I needed to make some IT-templates work on an Apache2 server with PHP 7.1. This was easier than expected: Setting PHP to display all warnings and errors leads directly to the right code statements which must be corrected:

Correcting the call of preg_replace

In a first step we can ignore the warning about the old fashioned class declaration, which will sooner or later disappear from PHP. What absolutely needs to be changed, however, is an error that arises due to a special usage of the function preg_replace():

Deprecated: preg_replace(): The /e modifier is deprecated, use preg_replace_callback

In my PEAR IT-installation the problem is located around line 956 of the file "IT.php". Fortunately, some other clever people have already provided a method, how to replace the critical statement:

        // Replace the following lines by the subsequent statements
        /*
        return preg_replace(
            "#<!-- INCLUDE (.*) -->#ime", "\$this->getFile('\\1')", $content
        );
        */
	return preg_replace_callback(
	   "#<!-- INCLUDE (.*) -->#im",
	    array($this, 'getFile'),
	    $content
	);

See the contribution of A. Preuss in the following stack overflow post:
http://stackoverflow.com/questions/22597102/preg-replace-the-e-modifier-is-deprecated-error

Worked perfectly for me. Many thanks to A. Preuss!

Changing to a __construct() function

To get rid of the warning regarding the old fashioned class constructor function just replace the function definition around line 376 by

    // function HTML_Template_IT($root = "", $options=null) {
    function __construct($root = "", $options=null) {

With both the replacements described above I could use my old IT-template dependent PHP programs successfully on a PHP 7.1 system! This, hopefully, gives me enough time to replace the PEAR IT engine by the Smarty engine wherever reasonable :-).