Regworld Domainrobot

Wer eigene (v)Server betreibt kann Domains unabhängig vom Provider direkt bei einem Domainrobot registrieren. Das hat zum einen den Vorteil, dass man Handles (Kontaktdaten) und IP-Adressen selbstständig verwalten kann, zum anderen gibt es die Domains zu unschlagbar günstigen Preisen.

Wie ich bereits erwähnt habe, habe ich alle meine privaten Domains über den Domainrobot vom Regworld registriert. Anfangs war ich skeptisch, da man über diesen Anbieter wenig im Internet findet (Früher war Regworld unter dem Namen “Silverbird Consulting” unterwegs .. warum auch immer). Zudem macht die Webseite auch nicht den seriösesten Eindruck. Bei Regworld gibt es eine .de-Domain aber für schlappe 2,22€/Jahr. Für die Neuregistrierung kommen nochmals einmalig 2,22€ dazu. Auch CNO-Domains sind mit 8,88€ ab der ersten Domain wirklich günstig.

Der Preis?

Denic-Mitglieder bezahlen für eine de-Domain an die Denic 1,92 Euro netto (vermutlich). Das sind 2,28 Euro inkl. MwSt. Somit verkauft Regworld die de-Domains wohl unter dem Einkaufspreis bei der Denic. Eventuell genießt Regworld hier auch Sonderkonditionen bei der Denic – aber mit Kosten für Nameserver, Buchhaltung/Rechnungserstellung und Technik für den Robot dürfte da so oder so nichts hängen bleiben.

Alle anderen Domainendungen (.mobi, .tel und Länderendungen wie .tv) sind dafür bei Regworld im Vergleich zu INWX extrem teuer. Aber irgendwie müssen die ja auch Geld verdienen…

Die Preise für de-Domains scheinen übrigens kein Lockangebot zu sein. Ich kann nicht genau sagen, wie lange dieser Preis schon gilt, mindestens aber 18 Monate. Die Sicherheit, dass das so bleibt hat man natürlich nicht. Aber dank Robot sind Domains dann auch relativ leicht zu einem anderen Robot umgezogen (IP-Adressen bleiben ja die selben).

Die Technik

Regworld verwendet den Domainrobot AutoDNS2. Seit einiger Zeit ist auch ein neues Webinterface mit AutoDNS3 verfügbar. Das Ganze funktioniert relativ fix – auch wenn es hier nicht wirklich auf Geschwindigkeit ankommt.

AutoDNS2 von Regworld

AutoDNS3 von Regworld

Nameserver gibt es für alle über Regworld registrierten Domains gratis. Einen Primary und einen Secondary. Einträge werden auf den Nameservern automatisch angelegt, wenn eine Domain registriert wird. Außerdem hat man selbstverständlich die Möglichkeit eigene Nameserver zu verwenden.

Die technischen Details habe ich bereits in einem früheren Blogbeitrag beschrieben.

DNS-Namen der zwei Nameserver sind (bei mir) ns3.regworld.com und ns4.regworld.com. Alternativ auch ns1.bces.de und ns2.bces.de. Man kann selbstverständlich per GLUE-Record auch seine eigenen Domains als Regworld-Nameserver verwenden.

Support

Für 2,22€ pro de-Domain kann man nicht all zu viel erwarten. Dennoch ist der Support gut erreichbar – sowohl per E-Mail als auch telefonisch. Bei Problemen mit einem Nameserver konnte man mir aber nicht wirklich weiterhelfen. So ist es also offenbar nicht möglich einen eigenen primären Nameserver zu betreiben und den sekundären bei Regworld. Ärgerlich, da es sich so über den Robot konfigurieren lässt, aber einfach nicht funktioniert. Primären und sekundären bei Regworld sowie weitere eigene sekundäre funktioniert aber.

Fazit

Bei Domains finde ich einzig und allein den Preis ausschlaggebend. Die Inklusiv-Nameserver nehme ich gerne mit, weil sie gut funktionieren. Wenn diese nicht zuverlässig wären, würde ich eigene betreiben. Und dann kann von Regworld-Seite eigentlich fast nichts mehr schief gehen.

Wer exotische Endungen benötigt sollte sich die Preisliste genau anschauen – hier ist die ein oder andere Überraschung versteckt. Zu den anderen Services von Regworld (Webspace/Server) kann ich nichts sagen. Da es bei diesen Produkten aber eher auf den Support ankommt würde ich mal spontan davon abraten. Siehe auch die Kommentare bei Webhostlist (hier geht es nicht um den Robot).

Webserver-Umgebung erkennen mit PHP

Development oder Production – das ist hier die Frage. Bei der Webseiten-Entwicklung bietet es sich selbst bei kleinsten Projekten an, zunächst in einer lokalen Entwicklungsumgebung die Anwendung zu testen. Ist der Test erfolgreich werden die Änderungen online gestellt.

Oftmals unterscheidet sich die Produktivumgebung im Internet von der lokalen. So müssen z.B. unterschiedliche MySQL-Konfigurationen  oder Pfade berücksichtigt werden.

Doch wie kann man zwischen den verschiedenen Umgebungen in einem PHP-Skript automatisch unterscheiden? Schließlich wollen wir ja nicht vor jedem Upload die Konfiguration händisch anpassen.

Das Auslesen des Server-Namens per $_SERVER['HTTP_HOST'] scheint die einfachste Möglichkeit. Doch da DNS-Namen und IP-Adressen, speziell in Entwicklungsumgebungen, vergänglich sind, ist das nicht immer die sicherste Methode seinen Entwicklungsserver zu identifizieren.

Besser ist es, in der lokalen Webserver-Konfiguration eine Umgebungsvariable zu setzen und diese dann per PHP abzufragen. Im Falle eines Apache-Webservers mit mod_env wäre das setEnv in der conf (httpd.conf), in einer VHOST-Direktive oder per .htaccess. Beispiel .htaccess:

SetEnv DEVELOP true

Auslesen mit PHP:

if (getenv('DEVELOP')) {
    // DEVELOP
} else {
    // Production
}

Unter manchen Konfigurationen sind die Umgebungsvariablen nur in $_SERVER verfügbar und nicht per getenv (IIS z.B.)

GET-Requests minimieren. Heute: Externe CSS-Dateien

Selbst bei kleineren Webprojekten hat man oft mehrere CSS-Dateien. Zumindest das CSS-Reset ist bei mir, zwecks Wiederverwendbarkeit, immer in einer separaten Datei. Eine weitere Unterteilung in typo.css, lists.css usw. macht dann bei größeren Projekten Sinn. Zudem kommen von externen Paketen oft weitere, separate CSS-Dateien hinzu (Lightbox, TinyMCE, ..).

So sieht das dann im HTML <head> aus:

<link href="css/reset.css" rel="stylesheet" type="text/css" media="screen" />
<link href="css/style.css" rel="stylesheet" type="text/css" media="screen" />
[...]

Doch hier benötigt der Browser für jede CSS-Datei einen extra HTTP GET-Request! Das geht auch schöner: ein PHP-Skript kann alle Dateien zusammenfassen und ausgeben. Damit wir die Endung .css beibehalten können, schreiben wir mittels mod_rewrite die URI zu unser CSS-Datei um:

.htaccess:
RewriteEngine On
RewriteRule css/init.css css/init.php
Im <head>:
<link href="css/init.css" rel="stylesheet" type="text/css" media="screen" />

In unserer  Haupt-CSS-Datei (init.php) machen wir dem Browser klar, dass diese Datei CSS ausgibt. Gleichzeitig buffern wir den Output mittels ob_start(). Durch die Angabe von “ob_gzhandler” wird der Output gzip-komprimiert ausgegeben, sofern der Browser das Unterstützt. (je nach HTTP “accept” header – Keine Sorge, PHP kümmert sich von ganz allein darum).

init.php:
header("Content-Type: text/css");
ob_start("ob_gzhandler");

Danach lesen wir bestimmte CSS-Dateien in einem Verzeichnis ein, und speichern den jeweiligen Inhalt in einer Variablen:

$css = '';
foreach (explode(',', 'reset,typo,style') as $file) {
    $css .= "/* File: $file.css */\n";
    if ($_SERVER['HTTP_HOST'] == 'webserver') {
        $css .= '@import url("'.$file.'.css");';
    } else {
        $css .= file_get_contents($file . '.css');
    }
    $css .= "\n\n";
}

In diesem Fall speichern wir die Dateien “reset.css“, “typo.css” und “style.css” in $css. Wenn das Ganze lokal ausgeführt wird,  geben wir nur “@import url(..)” und nicht den eigentlichen Dateiinhalt aus. Das hat den Vorteil, dass z.B. in Firebug noch immer die richtigen Dateinamen und Zeilen angezeigt werden. “webserver” wäre der DNS-Name des lokalen Entwicklungsservers.

Der Cache

Da die CSS-Datei dynamisch generiert wird, trifft hier kein ”If-Modified-Since“-Cache, wie bei Bilddateien oder statischen CSS-Dateien.

Wir müssen uns also selbst um das Cachen unserer dynamisch generierte CSS-Datei auf Clientseite kümmern. Cache mittels ETag bietet sich hier an. Dazu hashen wir den Dateiinhalt aller Dateien ($css) mit MD5. Diesen Hash übergeben wir dann als ETag an den Client. Ändert sich eine CSS-Datei, so ändert sich auch der MD5-Hash bzw. ETag.

Hat der Client einmal einen ETag empfangen, so sendet er diesen mit jedem weiteren Request zu dieser Datei im HTTP-Header mit. Auf Serverseite können wir also in unserem PHP-Script überprüfen ob der gesendete ETag noch aktuell ist. Wenn das der Fall ist, setzen wir einen 304 Not Modified Header und brechen ab.

$etag = md5($css);
header('ETag: '.$etag);
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
 trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag) {
    header("HTTP/1.1 304 Not Modified");
    die();
}
echo $css

Die komplette init.php kann hier nochmals angeguckt werden (mit gehighlightetem Syntax, yay!) .

Wir überprüfen:

(Screenshots stammen aus FireBug)

1. Request

Im Antwort-Header taucht der ETag auf. Da im Anfrage-Header kein ETag gesendet wurde, trifft hier kein Cache, und somit Status 200 OK.

2. Request

Unter IF_NONE_MATCH wird der ETag an den Server gesendet. Dieser antwortet dann mit 304 Not Modified.

Blättern mit MySQL und SQL_CALC_FOUND_ROWS

Verwendet man in einem MySQL-Query die Option SQL_CALC_FOUND_ROWS im Zusammenspiel mit LIMIT, berechnet MySQL die Gesamtzahl der Datensätze unabhängig vom verwendeten Limit.

Beispiel: Will man Daten seitenweise darstellen (-> Pagination), so braucht man z.B. für eine Blätternavigation die Gesamtzahl der vorhandenen Seiten bzw. Zeilen/Datensätze. Die Option SQL_CALC_FOUND_ROWS erspart das doppelte Ausführen des selben Queries, um an die Gesamtzahl zu kommen.

Schlecht

SELECT
    a, b, c
FROM
    table
WHERE  
    /* wahnsinnig kompliziertes Zeug */
LIMIT
    0, 10

--

SELECT
    COUNT(*)
FROM
    table
WHERE
    /* wahnsinnig kompliziertes Zeug */

Besser:

SELECT SQL_CALC_FOUND_ROWS
    a, b, c
FROM
    table
WHERE
    /* wahnsinnig kompliziertes Zeug */
LIMIT
    0, 10

Besonders bei komplexeren Queries schont das den Datenbankserver. Der MySQL-Query-Cache trifft nämlich nicht, wenn das LIMIT (oder sonst irgendwas) verändert wird.

Das Query mit SQL_CALC_FOUND_ROWS und LIMIT gibt wie gewohnt eine eingeschränkte Ergebnismenge zurück. In diesem Beispiel also die ersten 10 Datensätze. Um an die Gesamtzahl der Datensätze zu kommen muss ein zweites Query ausgeführt werden:

SELECT FOUND_ROWS()

Unterm Strich sind das dann trotzdem zwei Queries – aber FOUND_ROWS() gibt lediglich die bereits im ersten Query berechnete Gesamtzahl zurück, zählt also nicht wirklich als komplexes Query.

Eine Sache noch: Unter bestimmten Umständen, z.B. dann wenn nicht sortiert wird, ist das Query mit LIMIT und ohne SQL_CALC_FOUND_ROWS schneller, da das Query beendet ist, sobald das LIMIT erreicht ist. Das ändert aber nichts an der Tatsache, dass unterm Strich die Variante mit SQL_CALC_FOUND_ROWS schneller ist.

SOHO Mailserver unter Debian Lenny mit Getmail

Hier geht’s um die Idee, einen schicken, lokalen SOHO Mailserver unter Debian Lenny einzurichten. SOHO bedeutet Small Office, Home Office – wir reden hier also vom klassischen Serverlein daheim im Schrank, bis hin zum ausgewachsenem Server mit einigen (wenigen) Clients. Das ist kein Copy/Paste-Tutorial. Vielmehr geht es hier um die Möglichkeiten von Debian, einen solchen Mailserver zu realisieren.

Wozu überhaupt ein lokaler Mailserver?

E-Mails direkt am Client per POP abzuholen hat einige Nachteile. Besonders dann, wenn man von verschiedenen Rechner auf seine E-Mails zugreifen will. Außerdem ist die Datensicherung, auch bei nur einem Client, echt anstrengend.

Deswegen wurde IMAP erfunden! Mich stört aber:

  • Die Daten liegen irgendwo (im schlimmsten Fall bei Google!).
  • Begrenzter Speicherplatz: schon allein die Tatsache, dass eine Grenze existiert, stört.
  • Lag! Egal ob xxMbit und lokaler Cache: inet < LAN

Ein lokaler Mailserver löst all diese Probleme: E-Mails werden von diversen Konten per POP3 beim jeweiligen Provider abgeholt, und dann per IMAP im lokalen Netz zur Verfügung gestellt.

Die Anforderungen

  • Abholung von E-Mails verschiedener POP3-Konten bei verschiedenen Providern (GMX, 1und1, usw.)
  • Serverseitige (Spam-)Filter und Weiterleitungen
  • Bereitstellung der Mails mehrerer externer POP-Konten in einem IMAP-Postfach (in Unterordnern)
  • Versand über den SMTP-Server des jeweiligen Providers
  • Internen Mailversand direkt zustellen
  • Shared IMAP Ordner

Die Installation

Glücklicherweise sind alle benötigten Tools als Debian-Pakete verfügbar. Als MTA verwenden wir Postfix, IMAP-Postfächer werden von Dovecot bereitgestellt. Sieve filtert die E-Mails serverseitig. Getmail holt E-Mails von POP3-Konten und Spamassassin kümmert sich um den SPAM. Als Bonus gibt’s den Webmailer Squirrelmail mit Avelsieve, zum Erstellen/Bearbeiten von Sieve-Regeln per Browser.

Benutzer

Die verschiedenen Mail-Benutzer sind “normale” Shell-Benutzer. Die Mail-Verzeichnisse sind jeweils im Home-Verzeichnis des Users. Wir entscheiden uns mal für ~/mail. Das Format ist Maildir und die Verzeichnisse cur, tmp und new müssen wir von Hand mit maildirmake unterhalb von ~/mail anlegen oder einmalig in /etc/skel.

Postfix

Eigentlich könnte man die Mails auch direkt vom Client aus wegschicken. Der “Umweg” über den lokalen Mailserver hat aber den Vorteil, dass wir interne Mails direkt zustellen können (dazu später mehr). Außerdem finde ich es schicker, die SMTP-Daten einmalig am Server zu konfigurieren und dann am Client die einfachen Shell-Logindaten einzutippen. Für serverseitige Weiterleitungen (mit Sieve) brauchen wir sowieso einen MTA.

apt-get install postfix postfix-tls

Wir wählen Internetbetrieb mit Smarthost. Wir wollen ausgehende Mails über den jeweiligen SMTP-Server des Mail-Anbieters senden (also alles von me@gmx.net über smtp.gmx.net, alles von you@1und1.de über smtp.1und1.de usw.). Deshalb müssen wir Postfix so konfigurieren, dass es je nach Absenderadresse verschiedene SMTP-Server mit Authentifizierung verwendet. Das geht mit sender_dependent_relayhost_maps:

/etc/postfix/main.cf:

sender_dependent_relayhost_maps = hash:/etc/postfix/sender_relay
smtp_tls_auth_enable = yes
smtp_tls_password_maps = hash:/etc/postfix/tls_passwd

/etc/postfix/sender_relay:

me@gmx.net     [smtp.gmx.net]
you@1und1.de   [smtp.1und1.de]
...

/etc/postfix/tls_passwd:

me@gmx.net     Benutzer:Passwort
you@1und1.de   you:geheim

Dovecot

Wir installieren Dovecot 1.2 aus den Lenny-Backports. Warum wir die Backports-Version von Dovecot nehmen, und wie man die Backports zur sources.list hinzufügt, habe ich in einem früheren Beitrag schon einmal erwähnt.

apt-get install -t lenny-backports dovecot-common dovecot-imap

Getmail4 /Spamassassin

apt-get install getmail4 spamassassin

Jeder Benutzer hat pro Account eine getmailrc_*-Datei in ~/home/.getmail/. Ein Cronjob führt Getmail regelmäßig aus. Ein Lockfile sorgt dafür, dass E-Mails nicht doppelt abgeholt werden, wenn ein von Cron angestoßener Getmail-Prozess einmal länger dauert.

~$ vi ~/.getmail/getmailrc_me_gmx.net
[options]
delete = true

[retriever]
type = SimplePOP3Retriever
server = pop.gmx.net
port = 110
username = me@gmx.net
password = geheim
use_apop = false
timeout = 180
delete_dup_msgids = false

[destination]
type = MDA_external
path = /usr/sbin/sendmail
arguments = ('-f', me@gmx.net.de', '-oi', 'Shell-Login-Name@Lokaler-Host)

[filter]
type = Filter_external
path = /usr/bin/spamc
arguments = ("-s", "250000", "-u", "Shell-Login-Name", "-p", "783", )

An Postfix wird einmal die lokale Adresse (Shell-User@Hostname) und die externe E-Mail-Adresse übergeben. Das erlaubt die spätere Einsortierung in IMAP-Unterordner anhand des POP-Kontos mittels Envelope-From-Header.

Die Spamassassin-Filterung muss direkt von Getmail über [Filter] konfiguriert werden. Die Weiterleitung an Postfix erfolgt nämlich NICHT per SMTP, und somit ist der Spamassassin in Postfix nutzlos.

Der Cronjob

Der Cronjob wird von root ausgeführt und sucht in Home-Verzeichnissen nach getmailrc_*-Dateien. In diesem Beispiel macht er das jede Minute.

~$ sudo crontab -u root -e
*/1 * * * * /usr/local/bin/getthemail.sh >/dev/null 2>&1

~$ sudo vi /usr/local/bin/getthemail.sh
#!/bin/sh
 LOCK_FILE="/var/lock/getthemail"
 if [ ! -f "${LOCK_FILE}".lock ];
 then
 lockfile-create "${LOCK_FILE}"
 lockfile-touch "${LOCK_FILE}" &

 for getfile in $( find /home/*/.getmail/ -name getmailrc\* -print )
 do
 Y=`ls -l $getfile | awk '{print $3; }'`
 touch $getfile
 sudo -u $Y /usr/bin/getmail --getmaildir /home/$Y/.getmail/ --rcfile $getfile
 done
 lockfile-remove "${LOCK_FILE}"
 fi
exit 0;

Sieve-Filterung

Dovecot 1.2 kommt mit eigener Sieve-Implementierung. Ganz fix:

~$ sudo apt-get install squirrelmail avelsieve
~$ sudo vi /etc/dovecot/dovecot.conf
protocols = imap pop3 pop3s imaps managesieve
protocol lda {
   [...]
   mail_plugins = sieve
}

Per Sieve können wir jetzt auch E-Mails von verschiedenen POP3-Accounts in einem IMAP-Account organisieren. Getmail kann Mails verschiedener Konten in einem IMAP-Ordner zur Verfügung stellen. Mit einer Sieve-Regel lassen sich diese E-Mails in verschiedene Ordner schieben. Der Envelope-To-Header entspricht dabei immer der ursprünglichen Ziel-Adresse.

Interne Mails

Da alle ausgehenden Mails über den Postfix laufen ist das ein Klacks! Wir “mappen” externe Adressen auf lokale User-Accounts. Das ganze passiert mit Hilfe von alias_maps

~$ postconf -e alias_maps = hash:/etc/postfix/local_delivery
~$ sudo vi /etc/postfix/local_delivery
test@gmx.net    test

Wobei “test” ein lokaler Shell-Benutzer wäre.

Shared IMAP Ordner

Auch hier bietet Dovecot verschiedene Möglichkeiten. Die einfachste: wir symlinken das Shared-Folder einfach in das jeweilige IMAP-Root-Verzeichnis:

ln -s /home/share/mail /home/user/.Shared

In diesem Fall sharen wir die INBOX des users “share”. Das funktioniert natürlich auch für Unterverzeichnisse. Wichtig ist nur, dass dann der user “user” Schreibrechte auf cur, tmp, new und auf die Dovecot-Index-Dateien des “share”-Benutzers besitzt.

Google!

Sommerloch auf neunzehn83? Von wegen!

Es sind ja bekanntlich die kleinen Dinge im Leben, die oft den Unterschied machen. Das ist bei Suchmaschinen nicht anders — schon gar nicht bei Google.

Google ist nicht umsonst die Nr.1 unter den Suchmaschinen. Die Google-Homepage kommt so unscheinbar daher. Und dennoch steckt so unfassbar viel dahinter. Hier mal ein kleines Beispiel:

Google nach “simplexml delete child”

Mysteriöserweise wird auch “remove” als Suchbegriff erkannt und somit fett dargestellt. Wieso? Google benutzt eine ziemlich komplexe Synonym-Erkennung. Das Ganze funktioniert erstaunlich gut. Den meisten Suchenden wird dieses Feature aber gar nicht auffallen.

Was macht die Konkurrenz?

Yahoo

Bing

Enttäuschend. Remove wird offensichtlich nicht berücksichtigt.

Die ganze Thematik  gibt es auch noch einmal in einem lesenswerten Artikel im Google-Blog zum Nachlesen.

Interessanterweise funktioniert das nicht nur für Synonyme, sondern auch für Abkürzungen. Beispiel: “IE6″ bei Google, und Yahoo und Bing.

Was dieses Feature betrifft, kommt nach Google erstmal eine ganze Weile nichts. Bei Bing funktionieren Abkürzungen teilweise. Yahoo scheint keine Abkürzungen zu erkennen.

Runde Ecken mit CSS und ohne Stress

Es könnte alles so einfach sein. Runde Ecken sind mit der CSS3-Eigenschaft “border-radius” leicht zu realisieren. Das funktioniert in allen modernen Browsers (Opera, Firefox, Chrome und Safari). Nicht aber im Internet-Explorer.

Um auch dem IE runde Ecken beizubringen, habe ich bisher immer absolut positionierte Grafiken benutzt. Für einen einfachen Web-Entwickler wie mich stellt die Erstellung solcher “Eck-Grafiken” aber schon eine enorme Herausforderung dar. Gott sei Dank gibt es Generatoren :). Außerdem sind solche Eck-Grafiken unflexibel, da sie bei jeder Farbänderung ausgetauscht werden müssen.

DD_roundies nimmt sich diesem Problem an.  Es ist eine kleine (9KB) Javascript-Datei, die dem IE unter Zuhilfenahme von VML runde Ecken beibringt. Wenn man das Ganze dann per IE-Conditional-Comment einbindet erhöht sich die Ladezeit für die übrigen Browser nicht. Optimal!

<!--[if IE]>
<script type="text/javascript" src="js/dd_roundies.js"></script>

<script type="text/javascript">
    DD_roundies.addRule('.border', 10, false);
    DD_roundies.addRule('.border-top', '10px 10px 0 0', false);
</script>
<![endif]-->

Hier bekommen allen Elementen mit der Klasse “border” rundrum runde Ecken mit 10px Radius. Elemente mit der Klasse “border-top” bekommen nur oben links und oben rechts runde Ecken.

Für alle anderen Browser fügen wir die folgenden CSS3-Eigenschaften hinzu. Da CSS3 in manchen Rendering-Engines nur experimentell umgesetzt ist, müssen wir verschiedene Prefixe verwerden.

.border {
    border: 1px solid #f00;
    border-radius: 10px;
    -moz-border-radius: 10px;
    -webkit-border-radius: 10px;
    -khtml-border-radius: 10px;
}
.border-top {
    border: 1px solid #f00;
    border-top-left-radius: 10px;
    -moz-border-radius-topleft: 10px;
    -webkit-border-top-left-radius: 10px;
    -khtml-border-radius-topleft: 10px;
    border-top-right-radius: 10px; 
    -moz-border-radius-topright: 10px;
    -webkit-border-top-right-radius: 10px;
    -khtml-border-radius-topright: 10px;
}

Man beachte die vertauschte Reihenfolge bei -moz und -khtml im Vergleich zu -webkit und dem CSS3-Standard.

Ganz schön viel CSS für ein bisschen runde Ecken – aber immer noch bequemer als Grafiken!

MySQL’s GROUP_CONCAT()

Die GROUP_CONCAT-Funktion von MySQL ist eine tolle Erweiterung zu GROUP BY.  Insbesondere wenn man es mit one-to-many oder many-to-many Beziehungen zu tun hat. GROUP_CONCAT() ist seit der Version 4.1 Bestandteil von MySQL.

Beispiel: Personen haben Hobbys. Die Personen werden in der Personen-Tabelle mit dem Namen (name) und einer eindeutigen ID (person_id) gespeichert. Die zugehörigen Hobbys stehen in der Hobbys-Tabelle. Die Hobbys haben ebenfalls eine fortlaufende ID (hobby_id). Der Name des Hobbys steht in der Spalte hobby, die Verknüpfung zur Person wird über die person_id-Spalte hersgestellt.

mysql> SELECT * FROM personen;
+-----------+-------+
| person_id | name  |
+-----------+-------+
|         1 | Peter |
|         2 | Paul  |
+-----------+-------+

mysql> SELECT * FROM hobbys;
+----------+-----------+-----------+
| hobby_id | person_id | hobby     |
+----------+-----------+-----------+
|        1 |         1 | Schwimmen |
|        2 |         1 | Reiten    |
|        3 |         2 | Radfahren |
+----------+-----------+-----------+

Da eine Person mehrere Hobbys haben, und das selbe Hobby von mehreren Personen ausgeübt werden kann, wäre hier eine many-to-many Beziehung mit extra Join-Tabelle sinnvoller. Der Einfachheit halber belassen wir es aber bei zwei Tabellen. Die Funktionsweise von GROUP_CONCAT() ist in beiden Fällen sowieso die selbe.

Peter hat also die Hobbys “Schwimmen und Reiten“, Paul hat das Hobby Radfahren. Um jetzt mit PHP eine einfache Tabelle auszugeben, in der jeder Name mit den jeweiligen Hobbys aufgelistet wird, könnte man zunächst alle Namen holen, und dann pro Reihe die jeweiligen Hobbys in einem separatem Query. Das würde aber bedeuten, dass wir für n Datensätze n+1 Queries brauchen. Viel zu viel!

Packt man das alles in ein einziges  Query, und joint die Hobbys-Tabelle, taucht im Result der Peter leider doppelt auf. Auch doof…

Abhilfe schafft die GROUP_CONCAT()-Funktion in Verbindung mit einem GROUP BY. GROUP_CONCAT() konkateniert (=verbindet) alle Werte einer Gruppe zu einem String. Null-Werte werden ignoriert. Standardmäßig wird das ganze Komma-getrennt.

SELECT
    p.name
    , GROUP_CONCAT(h.hobby SEPARATOR ', ') AS hobbys
FROM
    personen p
LEFT JOIN
    hobbys h ON h.person_id = p.person_id
GROUP BY
    p.person_id

Mit der Angabe von SEPARATOR innerhalb der GROUP_CONCAT-Funktion trennen wir die einzelnen Hobbys zusätzlich mit einer Leertaste. Ergebnis:

+-------+-------------------+
| name  | hobbys            |
+-------+-------------------+
| Peter | Schwimmen, Reiten |
| Paul  | Radfahren         |
+-------+-------------------+

Perfekt! Für die besonders aufmerksamen Leser gibt’s hier nochmals das komplette Beispiel als SQL-File zum auswendig lernen ;)

SPAM im Griff mit Catch-All E-Mail-Adresse und Subdomain

Die meisten web2.0-Anwendungen verlangen eine Anmeldung, und somit auch eine E-Mail-Adresse, bevor man in den Genuss des kostenlosen Dienstes kommt. Eine Wegwerf-E-Mail-Adresse bietet sich hier nicht immer an. Schließlich will man später eventuell von Updates erfahren. So wird die verwendete E-Mail-Adresse früher oder später zugespammt. Doch welcher der vielen Anbieter, bei denen diese E-Mail-Adresse registriert ist, hat meine Adresse an Spammer verkauft? Oder anders gefragt:

Wer spammt mich hier eigentlich zu?

Wenn ein und dieselbe Adresse für mehrere Dienste benutzt wird, lässt sich das leider nicht herausfinden. Das Anlegen einzelner E-Mail-Adressen für jeden Dienst ist auch mehr als umständlich. Deshalb richten wir eine Catch-All E-Mail-Adresse ein. Damit landen alle E-Mails einer (Sub-)Domain, die an einen nicht existierenden Account gerichtet sind, in einem bestimmten Postfach.

Catch-All auf der Hauptdomain ist generell eine schlechte Idee. Hier ist das Grundrauschen von vornherein viel höher. Deshalb richten wir den Catch-All nur auf einer speziellen Subdomain ein. Zum Beispiel mail.example.org mit catchall@mail.example.org als POP3/IMAP-Account.

Bei Anmeldung an einer Webseite verwenden wir den Namen der selbigen als local-part unser Mailadresse. Beispiel: facebook@mail.example.org oder myspace@mail.example.org. Dank Catch-All laden all diese Mails in unserem catchall@-Account.

Nun lässt sich ganz leicht feststellen, wo der Spam denn herkommt.  Das hilf einem jetzt vielleicht auch nicht unbedingt weiter, aber zumindest lässt sich nun der gesamte Spam anhand der Empfänger-Adresse blockieren. Zum Beispiel mit einer einfachen Sieve-Filterregel. So sollte das Konto auch über längere Zeit sauber bleiben.

PHP’s PDO-Klasse erweitern

PDO ist eine PHP-Erweiterung, die einen konsistenten Zugriff auf Datenbanken ermöglicht. Seit PHP 5.1.0 ist PDO fester Bestandteil von PHP – also eine native (in C geschriebene) PHP-Extension. Dennoch lässt sich PDO mit PHP erweitern. Wozu? Ein Query-Counter, also das simple Mitzählen der ausgeführten SQL-Queries, ist wohl das einfachste Beispiel. Nicht weit davon entfernt sind dann so nette Dinge wie Query-Log oder Query-Zeitmessung.

PDO kommt mit zwei Klassen: PDO und PDOStatement*. Letztere kommt nur bei Prepared Statements ins Spiel. Wir beginnen also mit der PDO-Klasse. Diese muss ihre Eigenschaften an unsere eigene PDO-Klasse vererben. Das geht wie gewöhnlich mit dem Schlüsselwort “extends”. Der Name unserer eigenen PDO-Klasse ist ePDO (e wie enhanced, extended, was auch immer..).

[* und PDOException, welche für dieses Beispiel aber irrelevant ist]

<?php
class ePDO extends pdo {
    private $queryCount;
    public function __construct($dsn, $user = NULL, $pass = NULL, $options = NULL) {
        $this->queryCount = 0;

        parent::__construct($dsn, $user, $pass, $options);

        $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('ePDOStatement', array($this)));
        $this->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    }
}
?>

Wichtig ist hier immer den parent-contructor aufzurufen. Die benötigten Parameter gibt’s im Manual. Außerdem legen wir das private Attribut $queryCount an, in welchem wir die Anzahl der ausgeführten SQL-Queries speichern. Weil wir auch die PDOStatement-Klasse erweitern wollen, teilen wir PDO mittels setAttribute mit, unsere eigene PDOStatement-Klasse zu verwenden. Diese hat sinnigerweise den Namen ePDOStatement (e wie .. ihr wisst schon ..). Der Konstruktor bietet sich übrigens auch dazu an, gewisse Standard-Optionen für PDO zu setzen. In diesem Beispiel die DEFAULT_FETCH_MODE.

Der Beispielcode ist so schon voll funktionsfähig – auch wenn er noch nicht viel macht. Beim Herstellen der Datenbank-Verbindung schreiben wir statt $pdo = new PDO(..) einfach
$pdo = new ePDO(..). Die Parameter, sowie auch alles andere, bleiben die selben.

Um nun die Anzahl der Queries mitzuzählen, müssen wir alle Methoden die Queries ausführen erweitern. Das sind bei der PDO-Klasse: exec() und query():

public function exec($sql) {
    $this->increaseQueryCount();
    return parent::exec($sql);
}
public function query($sql) {
    $this->increaseQueryCount();
    $args = func_get_args();
    return call_user_func_array(array($this, 'parent::query'), $args);
}
public function getQueryCount() {
    return $this->queryCount;
}
public function increaseQueryCount() {
    $this->queryCount++;
}

getQueryCount() ist eine getter-Methode für das privat-Attribut $queryCount. Damit der Query-Count auch von unserer ePDOStatement-Klasse aus erhöht werden kann, packen wir die Logik dazu in die Public-Methode increaseQueryCount(). Da die query()-Methode variable Argumente (siehe PHP-Manual) hat, müssen wir alle übergebenen Argumente mit call_user_func_array an parent::query() übergeben.

Prepared Statements werden von der PDOStatement-Klasse ausgeführt, namentlich von der execute()-Methode. Um auch diese Queries zu zählen, müssen wir uns auch hier der Vererbung bedienen.

class ePDOStatement extends PDOStatement {
    private $pdo;

    protected function __construct(ePDO $pdo) {
        $this->pdo = $pdo
   }

    public function execute($params = array()) {
        $this->pdo->increaseQueryCount();
        return parent::execute($params);
    }
}

Der Konstruktor bekommt das ePDO-Objekt übergeben, in dem unser aktueller Query-Count steht. Wir speichern es als private Eigenschaft ab. In der execute()-Methode rufen wir die increaseQueryCount()-Methode des ePDO-Objektes auf, um unseren Query-Counter zu erhöhen.

Die fertigen Klassen gibt’s es hier nochmal zur Übersicht zum Download: ePDO und ePDOStatement.

So oder so ähnlich wird das dann benutzt:

$pdo = new ePDO("mysql:host=127.0.0.1;dbname=mydb", 'myuser', 'mypass');

$pdo->query("SELECT 1");
$pdo->exec("SELECT 1");

$stmt = $pdo->prepare("SELECT 1");
$stmt->execute();

echo $pdo->getQueryCount(); // 3