DavidBLN.de

4. Oktober 2011

Absolut relativ.

Für Internetunternehmungen, die – wie zum Beispiel diese hier – etwas mit Selbstdarstellung zu tun haben, ist das kostenlose, quelloffene Redaktions-/”Blog”-System Wordpress inzwischen wohl erste Wahl. Das liegt sicher und vor allem am relativ überschaubaren Installations- und Anpassungsaufwand. Andere Systeme wie Drupal sind hier deutlich flexibler und mächtiger, aber eben auch viel komplexer. Und mit Hilfe vieler optional erhältlicher Erweiterungen (”Plugins”) und/oder etwas PHP-Grundwissen können auch ungeübte Selbstdarsteller erstaunlich individuelle Projekte verwirklichen.

Einen grundlegenden Schönheitsfehler hat Wordpress allerdings. Zumindest für Menschen, die wie ich gerne selbst kontrollieren, was so ein System letztlich an Daten in die weite Welt pustet. Im Gegensatz zu allen anderen mir bekannten Redaktionssystemen herrscht im harten Kern der “Entwicklergemeinde” die felsenfeste Überzeugung, daß absolute und vollqualifizierte URL-Pfade so etwas wie die reine Lehre und der Weisheit letzter Schluß in einem sind.

Argumente wie “mit ein und demselben Datenbestand auf verschiedenen Hosts, etwa einem für Testzwecke und einem produktiven, arbeiten” oder “bei Domainwechsel unkompliziert umziehen” läßt diese Gemeinde nicht gelten. Man könne ja “mit wenigen SQL-Befehlen alle Inhalte durchsuchen und die fraglichen Teile ersetzen”. Auch, daß wirklich jeder in den letzten 15 Jahren veröffentlichte Internetbrowser längst in der Lage ist, einen relativen Link wie /inhalt/uebersicht/unterschied-zwischen-relativen-und-absoluten-pfaden.html selbständig um den Host-Teil der verlinkenden Seite zu ergänzen, also hier zum Beispiel zu http://www.davidbln.de/inhalt/uebersicht/unterschied-zwischen-relativen-und-absoluten-pfaden.html, galt in den zahllosen bisher hierüber zu findenden Diskussionen als völlig unerheblich (siehe oben: reine Lehre und so). Und, als ob es Sache des Redaktionssystem sei, seinen Anwender vor “Bestrafungen” durch Google zu schützen, liest man gern, daß “duplicate content” ohnehin unter allen Umständen zu vermeiden sei und es schon deshalb keine Notwendigkeit gäbe, den status quo zu überdenken. Immerhin: Man hat sich, da die Frage eben doch viel öfter aufkommt als ihre Gegner sie wegzuargumentieren vermögen, inzwischen darauf verständigt, die Entscheidung über “richtig” und “falsch” künftig dem Benutzer selbst zu überlassen und in einer zeitlich noch nicht näher definierten Zukunft beide Möglichkeiten als Optionen anzubieten.

Da ich zu den ungeduldigen Menschen gehöre, die noch nicht mal auf absehbare und feststehende Termine gerne warten, habe ich also nach Alternativen gesucht. Ein gangbarer, aber aus meiner Sicht unzuverlässiger Weg ist das Definieren von Ausgabefiltern direkt in Wordpress. Unzuverlässig, weil es im System etliche zu filternde Funktionen gibt, die allesamt standardmäßig absolute Pfade ausgeben und weil es, wenn nachträglich appliziert, unter Umständen immer noch nicht alles abfängt. Letzteres kann zum Problem werden, wenn man Wert darauf legt, daß identische Inhalte auf gar keinen Fall anhand eindeutiger Hostadressen miteinander in Verbindung gebracht werden können (ja, es soll sogar Leute geben, die nicht nur keinen Wert auf fantastische SERP-Ränge legen, sondern ganz gezielt eine Suchmaschinenindizierung vermeiden!).

Für mich war daher nach kurzer Analyse klar, daß die einzig sichere und saubere Lösung nicht “vor” und nicht in, sondern erst “nach” Wordpress liegen kann. Sprich, daß jeder HTML- und XML-Quelltext erst gesäubert wird, wenn die gutmeinende Wortmamsell ihre bevormundenden Pfoten nicht mehr an den zum Leser transportierten Datenstrom anlegen kann. Es gäbe hierfür mehrere Ansätze: Einer wäre etwa ein PHP-Wrapper, der über die .htaccess-Datei anstelle der Wordpress-index.php eingebunden wird, die er seinerseits aufruft und deren Ausgabe er anschließend mit einem einfachen Ersetzungsmuster bereinigt. Allerdings gehört PHP schon von Haus aus nicht zu den ressourcenschonendsten Systemen und hat seine Stärken eindeutig woanders. Auch wäre mir das Risiko, vordefinierte Funktionen und Abläufe zu stören, zu hoch.

Nein, PHP kann hier nur im Notfall das Mittel der Wahl sein. Wenn es ein Serverwerkzeug gibt, das sich wie kein anderes für die schnelle und effiziente Manipulation und Filterung von (Quell-)Textdaten eignet, dann ist es vielmehr PERL, das auf jedem ordentlichen Webserver ohnehin installiert ist und auf allen anderen System schnell nachgerüstet werden kann. Auch hier gibt es verschiedene Möglichkeiten, eine Perl-Schicht um die eigentliche Wordpress-PHP-Maschine “herumzuwickeln”. Es gibt integrative Module, die beide Systeme auf verschiedene Art miteinander verbinden können. Bei komplexen Projekten ist allerdings eine ressourcenschonende und, sofern installiert, fastcgi nicht ausbremsende Methode nötig.

Idealerweise wird die Perl-Schicht jedoch direkt vom Webserver selbst bereitgestellt. Im Falle des Apache-Webservers gibt es hierfür ein fertiges Modul, das sich vor allem in Linux-/Unix-Umgebungen mit Paketverwaltungen sehr einfach installieren und aktivieren läßt. Es heißt mod_perl und ist so hilfreich, wie es derzeit noch mangelhaft dokumentiert ist. Es integriert Perl quasi nahtlos in den Webserver und ermöglicht unter anderem das einbinden eigener Filter, die zum Beispiel Datenströme unmittelbar vor der Ausgabe an den Client schnelll und einfach manipulieren können. Voraussetzung ist lediglich ein eigener Webserver, soll heißen, einer, auf dem man selbst Module einbinden und .htaccess-Dateien und/oder Vhost-Konfigurationen bearbeiten kann oder, wenn man den nicht hat, dann ein hilfsbereiter Hosting-Anbieter, der einem mod_perl nachrüstet und die Möglichkeit bietet, eigene Apache-Perl-Module in einem dem Apache bekannten Systempfad einzubinden.

Wie gesagt: mod_perl ist sehr leistungsfähig und vielseitig. Ich beschränke mich hier auf die Dokumentation meiner Behelfslösung unter Apache2 für das Problem der sturen Wordpress-Entwickler und hoffe, es hilft anderen, die hierfür ebenfalls eine Lösung suchen. Wer tiefer einsteigen will, muß sich schon selbst mit der Dokumentation des Moduls befassen und, natürlich, Perl-Kenntnisse besitzen. Diese lassen sich ja glücklicherweise aus vielen Quellen anlesen.

Beispieldefinition

In meinem Beispiel soll eine Wordpress-Installation auf zwei Servern laufen. Einer davon heißt test.domain.xy, der andere www.domain.ab. Egal, auf welchem Server geschrieben oder gelesen wird: In den ausgegebenen Dokumenten soll im Quelltext niemals einer dieser beiden Server hartkodiert auftauchen. Stattdessen sollen alle Pfaden relativ zum Web-Stammverzeichnis (DOCUMENT_ROOT) erscheinen, also stets mit “/” beginnen. Das erfordert in der Praxis, daß alle Quelltexte auf das Auftauchen der Zeichenfolgen

  • http://test.domain.xy
  • https://test.domain.xy
  • http://test.domain.xy
  • https://www.domain.ab

überprüft werden und alle Vorkommen dieser Suchtexte ersatzlos gestrichen, also mit einer leeren Zeichenfolge ersetzt werden und das jeweilige Dokument erst anschließend ausgeliefert wird.

1. mod_perl installieren und aktivieren

Unter Ubuntu/Debian am besten mit Hilfe der Paketverwaltung libapache2-mod-perl suchen und installieren, in anderen Systemen wird es kaum schwieriger sein. Falls Perl selbst noch nicht installiert ist, sollte es als Abhängigkeit ohnehin automatisch mitinstalliert werden.

Anschließend das Modul in Apache aktivieren, je nach Systemstandards in der apache2.conf oder einer eigenen Modulkonfigurationsdatei:

LoadModule perl_module /usr/lib/apache2/modules/mod_perl.so

Nach dem Apache-Neustart steht das Modul bereit.

2. Filter in alle betreffenden VHost-Konfigurationen einfügen

Das Apache-Perl-Modul durchsucht, sobald es mit der Anwendung eines sogenannten Perl-Handlers beauftragt wird, alle ihm bekannten Systempfade nach zusätzlichen Modulen. Es gibt auch schon eine Menge fertiger Module, die bei der Installation eingerichtet werden und auf die man bei der Erstellung eigener Filter bequem zurückgreifen kann, um zum Beispiel Umgebungsvariablen des Serverkontextes abzufragen oder überhaupt an den jeweils zu bearbeitenden Datenstrom heranzugelangen.

Damit unser noch zu erstellendes Modul – ich nenne es mal fixWordpress.pm – zur Filterung von ausgegebenen Datenströmen als Filter aufgerufen wird, ist für jeden bestehende Vhost oder Server (im Beispiel ist es ein Host mit zwei Servernamen, was den ursprünglichen Zweck der Übung natürlich ad absurdum führt, für den weiteren Inhalt aber unwichtig ist) die Konfiguration wie folgt zu ergänzen:


DocumentRoot /home/userx/webs/beispiel
ServerName www.domain.ab
ServerAlias test.domain.xy
<Directory /home/userx/webs/beispiel>
  # (beliebige Direktiven, die hier auch vorher schon standen)
  # Die folgende Zeile ist die entscheidende und wird weiter unten erklärt:
  PerlOutputFilterHandler MyApache2::fixWordpress
</Directory>

3. Den Filter erstellen

Der gerade eingefügte Handler weist mod_perl in typischer Perl-Modul-Notation an, die ihm bekannten Systempfade nach einem Verzeichnis namens MyApache2 zu durchsuchen und von dort das Perl-Modul namens fixWordpress einzubinden. Ich habe das Verzeichnis unterhalb von /etc/apache2 angelegt. Verzeichnisrechte für den Webserver, der das dort zu erstellende Modul aufrufen soll, nicht vergessen!

Als nächstes wird der eigentliche Filter in Form der Datei /etc/apache2/MyApache2/fixWordpress.pm erstellt. Ich habe den Quelltext so gut es geht kommentiert und sogleich versucht, keine allzuweiten Detail-Ausflüge zu unternehmen.


#file:MyApache2/fixWordpress.pm
#--------------------------------

#Namensraum für diesen Handler festlegen
package MyApache2::fixWordpress;

#Konventionen und includes definieren
use strict;
use warnings;
use Apache2::Filter ();
use Apache2::RequestRec ();
use Apache2::RequestIO ();
use Apache2::ServerRec ();
use APR::Table ();
use Apache2::Const -compile => qw(OK DECLINED);
use constant BUFF_LEN => 1024;

# Jedes Handler-Package muß die Methode "handler" besitzen und definieren.
# Sie ist das eigentliche Programm für unseren Zweck.

sub handler {
	# die Methode erhält als erstes Argument eine Referenz auf den Datenstrom
	my $f = shift;

	# Eine Eigenschaft des Datenstromobjekts ist der zugrundeliegende Server-Request.
	# Von dessen Eigenschaften interessiert hier der Content-type des Datenstroms,
	# denn wir wollen nur Quelltexte filtern. Entspricht dem mime-Typ text/*
	if ($f->r->content_type =~ /^text\//) {

		# Die Datenströme werden häppchenweise in sogenannten Buckets übergeben.
		# diese lassen sich nicht ohne weiteres filtern, da sie zerhackt sein können.
		# Also müssen wir sie solange sammeln, bis der letzte Bucket kommt (weiter
		# unten).
		# Beim ersten Aufruf, den man an der noch nicht existierenden Eigenschaft
		# "ctx" (=context) erkennt, müssen wir außerdem die bereits erstellte
		# Request-Eigenschaft "Content-length" löschen, da wir den gesamten
		# Content kürzen.
		# Diesen setzen wir nach der Modifikation erneut; falls weitere
		# Filter/Module wie mod_deflate aktiv sind, kann man das auch lassen,
		# da sie abermals die Datenmenge verändern.

		# Routinen für den ersten Daten-Bucket
		unless ($f->ctx) {
		  	# Die ctx-Variable bleibt für weitere Durchläufe erhalten und
		  	# kann nach belieben verwendet werden. Hier wird sie als
		  	# Hash verwendet, der sich zunächst merkt,
		  	# daß der erste Bucket angekommen ist ("invoked").
		  	my $ctx = $f->ctx;
		  	$ctx->{invoked}=1;
		  	$f->ctx($ctx);
		  	$f->r->headers_out->unset('Content-Length');
		}

		# Routine für alle Buckets, einschl. des ersten und des letzten aufrufen
		# (siehe weiter unten)
		process($f);

		# Routinen nur für das letzte Bucket. Bei diesem ist das Flag
		# "seen_eos" gesetzt.
		if ($f->seen_eos) {
		  finalize($f); # siehe wiederum weiter unten
		}

		# Daß wir den Datenstrom bearbeitet haben, teilen wir Apache mit:
		return Apache2::Const::OK;
	} else {
		# Es sei denn, der Datenstrom hat einen nicht-text-mime-Typ.
		# Dann tun wir nichts und melden auch das zurück:
		return Apache2::Const::DECLINED;
	}
}

# Die folgende Routine wird weiter oben für jedes Datenbucket aufgerufen.
# Sie hängt den Inhalt an eine "Merk"-Eigenschaft des anfangs definierten Context-
# Hashes an. Erst beim letzten Bucket wird dieser komplette Inhalt
# verarbeitet und ausgegeben.
sub process  {
	# Wiederum wurde der Routine das Handle für den Datenstrom übergeben,...
	my $f = shift;

	# ...der eingehende Datenstrom-Bucket ausgelesen...
	my $ctx = $f->ctx;
	while ($f->read(my $buf,BUFF_LEN)) {
		$ctx->{raw}.=$buf;
	}
	# ...und der aktualisierte Kontext wieder dem Datenstrom-Handle mitgeteilt
	$f->ctx($ctx);
}
# Erst mit dieser Routine für das letzte Bucket geschieht das eigentliche Filtern.
# In der Eigenschaft "raw" des Kontext-Objektes liegt uns nun der ganze Quelltext so
# vor, wie ihn Wordpress erzeugt hat. Wir können ihn nach Belieben manipulieren:
sub finalize {
	my $f = shift;
	my $ctx = $f->ctx;
	my $out = $ctx->{raw};

	# mit einem simplen regulären Ausdruck suchen und ersetzen wir alle unerwünschten
	# Spuren hartkodierter URL-Host-Teile ganz nach Belieben:
	$out =~ s/https?:\/\/(test\.domain\.xy|www\.domain\.ab)//gi;

	# Da wir nun die neue Länge des Datenstroms kennen, setzen wir den
	# oben gelöschten Header-Wert mit dem neuen Wert:
	$f->r->headers_out->set('Content-Length',length($out));

	# zu guter Letzt geben wir noch das Ergebnis der Manipulation,
	# also den neuen Quelltext, in einem Rutsch zurück:
	$f->print($out);
}

# Das folgende muß so sein in jedem Perl-Modul:
1;

Nach einem Neustart des Webservers und sofern alles nach Plan läuft, kann man das Ergebnis der Mühe nun im Quelltext seiner Wordpress-Dokumente bestaunen: Nirgendwo tauchen mehr absolute Pfade auf.

3a. Alternative Manipulationen

Im Gegensatz zu klassischem Browsing kann es beim Erstellen von Feeds, die ja ebenfalls aus Quelltexten bestehen, je nach Feedreader zu Problemen kommen, wenn keine server-absoluten URLs angegeben werden. Anstatt also die fraglichen Suchmuster aus dem Code-Beispiel mit einer leeren Zeichenfolge zu ersetzen, kann es sinnvoll sein, stattdessen den Serverteil jeweils dynamisch mit dem tatsächlich angesprochenen Server zu ersetzen. Das erfodert gerade mal eine zusätzliche und eine geänderte Zeile.

Statt:


$out =~ s/https?:\/\/(test\.domain\.xy|www\.domain\.ab)//gi;

wird


my $servername = $f->r->server->server_hostname;
$out =~ s/(https?:\/\/)(test\.domain\.xy|www\.domain\.ab)/$1$servername/gi;

eingefügt. Fertig!

Wie gesagt: Bei mir und für meinen Zweck funktioniert diese Lösung wunderbar und ohne irgendeinen erwähnenswerten, spürbaren Leistungsverlust und ich hoffe, daß das lästige Problem der “Host-Bevormundung” durch allzu dogmatische Wordpress-Entwickler (die ansonsten eine meist tolle Arbeit für umme machen) damit auch für einige andere lösbar wird. Wer Ergänzungen oder Korrekturen hat, bitte melden! Viel Erfolg!


Keine Kommentare »

Noch keine Kommentare.

RSS-Feed für Kommentare zu diesem Artikel. TrackBack-URL

Einen Kommentar hinterlassen